CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-stdio

Standard input/output manager for Node.js with command-line parsing, async file reading, interactive terminal, and progress bars

Pending
Overview
Eval results
Files

ask.mddocs/

Interactive Terminal Questions

Ask questions in terminal with option validation, retry mechanisms, and customizable input streams for building interactive command-line applications.

Capabilities

Main Ask Function

Displays questions and waits for user responses with optional validation and retry logic.

/**
 * Ask an interactive question and wait for user response
 * @param question - The question text to display
 * @param config - Configuration options for validation and behavior
 * @returns Promise resolving to the user's answer as string
 */
function ask(question: string, config?: AskConfig): Promise<string>;

interface AskConfig {
  /** Array of valid answer options for validation */
  options?: string[];
  /** Maximum number of retry attempts on invalid input (default: 3) */
  maxRetries?: number;
  /** Custom input stream (defaults to process.stdin) */
  inputStream?: any;
}

Usage Examples:

import { ask } from "stdio";

// Basic question
const name = await ask("What's your name?");
console.log(`Hello, ${name}!`);

// Multiple choice with validation
const choice = await ask("Choose an option", { 
  options: ['yes', 'no', 'maybe'] 
});
console.log(`You chose: ${choice}`);

// Custom retry limit
const action = await ask("Confirm action", { 
  options: ['confirm', 'cancel'],
  maxRetries: 5 
});

// Free-form input (no validation)
const feedback = await ask("Any additional comments?");

// Complex interactive flow
const language = await ask("Select language", { 
  options: ['javascript', 'typescript', 'python'] 
});

const framework = language === 'javascript' || language === 'typescript'
  ? await ask("Select framework", { options: ['react', 'vue', 'angular'] })
  : await ask("Select framework", { options: ['django', 'flask', 'fastapi'] });

console.log(`Selected: ${language} with ${framework}`);

Configuration Interface

Comprehensive configuration for question behavior and validation.

interface AskConfig {
  /** 
   * Valid answer options - if provided, only these answers are accepted
   * Invalid answers trigger retry with helpful error message
   */
  options?: string[];
  
  /** 
   * Maximum retry attempts for invalid answers (default: 3)
   * After exhausting retries, promise rejects with error
   */
  maxRetries?: number;
  
  /** 
   * Custom input stream for testing or alternative input sources
   * Defaults to process.stdin for normal terminal interaction
   */
  inputStream?: any;
}

Question Display Format

Questions are automatically formatted with helpful context:

// Basic question
await ask("Enter your email");
// Displays: "Enter your email: "

// With options
await ask("Continue?", { options: ['yes', 'no'] });
// Displays: "Continue? [yes/no]: "

// Multiple options
await ask("Select environment", { 
  options: ['development', 'staging', 'production'] 
});
// Displays: "Select environment [development/staging/production]: "

Advanced Features

Input Validation and Retry Logic

Automatic validation with user-friendly error handling:

import { ask } from "stdio";

// This will retry until valid input
const answer = await ask("Are you sure?", { 
  options: ['yes', 'no'],
  maxRetries: 3 
});

// Invalid inputs show helpful messages:
// User types "maybe"
// Output: "Unexpected answer. 2 retries left."
// Prompt: "Are you sure? [yes/no]: "

// After 3 invalid attempts, promise rejects with:
// Error: "Retries spent"

Custom Input Streams

Support for alternative input sources for testing and automation:

import { ask } from "stdio";
import { Readable } from 'stream';

// Create mock input stream for testing
const mockInput = new Readable({
  read() {
    this.push('yes\n');
    this.push(null); // End stream
  }
});

const response = await ask("Proceed?", { 
  options: ['yes', 'no'],
  inputStream: mockInput 
});
// response === 'yes'

// Testing interactive flows
async function testInteractiveFlow() {
  const responses = ['john', 'developer', 'yes'];
  let responseIndex = 0;
  
  const mockStream = new Readable({
    read() {
      if (responseIndex < responses.length) {
        this.push(responses[responseIndex++] + '\n');
      } else {
        this.push(null);
      }
    }
  });
  
  const name = await ask("Name?", { inputStream: mockStream });
  const role = await ask("Role?", { inputStream: mockStream });
  const confirm = await ask("Confirm?", { 
    options: ['yes', 'no'], 
    inputStream: mockStream 
  });
  
  return { name, role, confirm };
}

Error Handling

Comprehensive error handling for various failure scenarios:

try {
  const answer = await ask("Choose option", { 
    options: ['a', 'b', 'c'],
    maxRetries: 2 
  });
  console.log(`Selected: ${answer}`);
} catch (error) {
  if (error.message === 'Retries spent') {
    console.error('Too many invalid attempts');
    process.exit(1);
  } else {
    console.error('Unexpected error:', error.message);
  }
}

// Stream-related errors
try {
  const brokenStream = new BrokenReadableStream();
  await ask("Question?", { inputStream: brokenStream });
} catch (error) {
  console.error('Input stream error:', error);
}

Integration Patterns

Common patterns for building interactive applications:

Setup Wizard:

async function setupWizard() {
  console.log('=== Application Setup ===');
  
  const config = {};
  
  config.name = await ask("Project name?");
  
  config.type = await ask("Project type?", { 
    options: ['web', 'api', 'cli'] 
  });
  
  if (config.type === 'web') {
    config.framework = await ask("Frontend framework?", { 
      options: ['react', 'vue', 'angular'] 
    });
  }
  
  config.database = await ask("Database?", { 
    options: ['mysql', 'postgresql', 'mongodb', 'none'] 
  });
  
  const proceed = await ask("Create project with these settings?", { 
    options: ['yes', 'no'] 
  });
  
  if (proceed === 'yes') {
    await createProject(config);
  } else {
    console.log('Setup cancelled');
  }
}

Confirmation Dialogs:

async function confirmAction(action: string): Promise<boolean> {
  const response = await ask(`Are you sure you want to ${action}?`, { 
    options: ['yes', 'no'],
    maxRetries: 1 
  });
  return response === 'yes';
}

// Usage
if (await confirmAction('delete all files')) {
  await deleteFiles();
} else {
  console.log('Operation cancelled');
}

Menu Selection:

async function showMenu(): Promise<string> {
  console.log('Available actions:');
  console.log('1. Create new item');
  console.log('2. List items');
  console.log('3. Delete item');
  console.log('4. Exit');
  
  const choice = await ask("Select action", { 
    options: ['1', '2', '3', '4'],
    maxRetries: 5 
  });
  
  const actions = {
    '1': 'create',
    '2': 'list', 
    '3': 'delete',
    '4': 'exit'
  };
  
  return actions[choice];
}

Constants and Defaults

/** Default maximum retry attempts */
const DEFAULT_MAX_RETRIES = 3;

The ask function uses sensible defaults:

  • Input Stream: process.stdin for normal terminal interaction
  • Max Retries: 3 attempts before rejecting with error
  • Output: Questions display to process.stdout with formatted options
  • Validation: Case-sensitive exact matching for option validation

Install with Tessl CLI

npx tessl i tessl/npm-stdio

docs

ask.md

getopt.md

index.md

progress-bar.md

read.md

readLine.md

tile.json