UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

501 lines 21 kB
/** * V3 CLI Interactive Prompt System * Modern interactive prompts for user input */ import * as readline from 'readline'; import { output } from './output.js'; // ============================================ // Core Prompt Infrastructure // ============================================ class PromptManager { rl = null; formatter; constructor(formatter = output) { this.formatter = formatter; } createInterface() { if (!this.rl) { this.rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true }); // Handle cleanup on exit this.rl.on('close', () => { this.rl = null; }); } return this.rl; } close() { if (this.rl) { this.rl.close(); this.rl = null; } } async question(prompt) { return new Promise((resolve) => { const rl = this.createInterface(); rl.question(prompt, (answer) => { resolve(answer); }); }); } // ============================================ // Select Prompt // ============================================ async select(options) { const { message, options: choices, default: defaultValue, pageSize = 10 } = options; this.formatter.writeln(); this.formatter.writeln(this.formatter.bold(`? ${message}`)); this.formatter.writeln(this.formatter.dim(' (Use arrow keys to navigate, enter to select)')); this.formatter.writeln(); // Find default index let selectedIndex = 0; if (defaultValue !== undefined) { const idx = choices.findIndex(c => c.value === defaultValue); if (idx !== -1) selectedIndex = idx; } // Display options const displayChoices = (currentIndex, startIndex = 0) => { // Move cursor up to overwrite if (startIndex > 0 || currentIndex > 0) { process.stdout.write(`\x1b[${Math.min(choices.length, pageSize)}A`); } const endIndex = Math.min(startIndex + pageSize, choices.length); for (let i = startIndex; i < endIndex; i++) { const choice = choices[i]; const isSelected = i === currentIndex; const prefix = isSelected ? this.formatter.info('>') : ' '; const label = isSelected ? this.formatter.highlight(choice.label) : choice.label; const hint = choice.hint ? this.formatter.dim(` - ${choice.hint}`) : ''; const disabled = choice.disabled ? this.formatter.dim(' (disabled)') : ''; this.formatter.writeln(` ${prefix} ${label}${hint}${disabled}`); } }; // Initial display displayChoices(selectedIndex); return new Promise((resolve, reject) => { const rl = this.createInterface(); // Enable raw mode for arrow key detection if (process.stdin.isTTY) { process.stdin.setRawMode(true); } process.stdin.resume(); const handleKeypress = (key) => { const keyStr = key.toString(); // Arrow keys if (keyStr === '\x1b[A') { // Up do { selectedIndex = (selectedIndex - 1 + choices.length) % choices.length; } while (choices[selectedIndex].disabled && selectedIndex !== 0); displayChoices(selectedIndex); } else if (keyStr === '\x1b[B') { // Down do { selectedIndex = (selectedIndex + 1) % choices.length; } while (choices[selectedIndex].disabled && selectedIndex !== choices.length - 1); displayChoices(selectedIndex); } else if (keyStr === '\r' || keyStr === '\n') { // Enter cleanup(); const selected = choices[selectedIndex]; if (!selected.disabled) { this.formatter.writeln(); this.formatter.writeln(this.formatter.success(`Selected: ${selected.label}`)); resolve(selected.value); } } else if (keyStr === '\x03') { // Ctrl+C cleanup(); reject(new Error('User cancelled')); } }; const cleanup = () => { process.stdin.removeListener('data', handleKeypress); if (process.stdin.isTTY) { process.stdin.setRawMode(false); } this.close(); }; process.stdin.on('data', handleKeypress); }); } // ============================================ // Confirm Prompt // ============================================ async confirm(options) { const { message, default: defaultValue = false, active = 'Yes', inactive = 'No' } = options; const defaultText = defaultValue ? `${active}/${inactive}` : `${active}/${inactive}`; const hint = defaultValue ? `[${active}]` : `[${inactive}]`; const prompt = `${this.formatter.bold('?')} ${message} ${this.formatter.dim(hint)} `; const answer = await this.question(prompt); this.close(); if (answer === '') { return defaultValue; } const normalized = answer.toLowerCase().trim(); if (['y', 'yes', 'true', '1'].includes(normalized)) { return true; } if (['n', 'no', 'false', '0'].includes(normalized)) { return false; } return defaultValue; } // ============================================ // Input Prompt // ============================================ async input(options) { const { message, default: defaultValue, placeholder, validate, mask } = options; let prompt = `${this.formatter.bold('?')} ${message}`; if (defaultValue) { prompt += ` ${this.formatter.dim(`(${defaultValue})`)}`; } else if (placeholder) { prompt += ` ${this.formatter.dim(placeholder)}`; } prompt += ': '; while (true) { let answer; if (mask) { answer = await this.inputMasked(prompt); } else { answer = await this.question(prompt); } // Use default if empty if (answer === '' && defaultValue !== undefined) { answer = defaultValue; } // Validate if (validate) { const result = validate(answer); if (result !== true) { const errorMsg = typeof result === 'string' ? result : 'Invalid input'; this.formatter.writeln(this.formatter.error(` ${errorMsg}`)); continue; } } this.close(); return answer; } } async inputMasked(prompt) { return new Promise((resolve) => { const rl = this.createInterface(); let password = ''; // Don't echo characters process.stdout.write(prompt); if (process.stdin.isTTY) { process.stdin.setRawMode(true); } process.stdin.resume(); const handleData = (chunk) => { const char = chunk.toString(); if (char === '\n' || char === '\r') { // Enter pressed process.stdin.removeListener('data', handleData); if (process.stdin.isTTY) { process.stdin.setRawMode(false); } process.stdout.write('\n'); resolve(password); } else if (char === '\x7f' || char === '\x08') { // Backspace if (password.length > 0) { password = password.slice(0, -1); process.stdout.write('\b \b'); } } else if (char === '\x03') { // Ctrl+C process.stdin.removeListener('data', handleData); if (process.stdin.isTTY) { process.stdin.setRawMode(false); } resolve(''); } else if (char.charCodeAt(0) >= 32) { // Printable character password += char; process.stdout.write('*'); } }; process.stdin.on('data', handleData); }); } // ============================================ // Multi-Select Prompt // ============================================ async multiSelect(options) { const { message, options: choices, default: defaultValues = [], required = false, min, max } = options; this.formatter.writeln(); this.formatter.writeln(this.formatter.bold(`? ${message}`)); this.formatter.writeln(this.formatter.dim(' (Use arrow keys to navigate, space to select, enter to confirm)')); this.formatter.writeln(); // Initialize selection state const selected = new Set(); for (let i = 0; i < choices.length; i++) { // Check both default array and individual selected property if (defaultValues.includes(choices[i].value) || choices[i].selected) { selected.add(i); } } let currentIndex = 0; // Display options const displayChoices = () => { // Move cursor up to overwrite process.stdout.write(`\x1b[${choices.length}A`); for (let i = 0; i < choices.length; i++) { const choice = choices[i]; const isCurrentRow = i === currentIndex; const isSelected = selected.has(i); const cursor = isCurrentRow ? this.formatter.info('>') : ' '; const checkbox = isSelected ? this.formatter.success('[x]') : this.formatter.dim('[ ]'); const label = isCurrentRow ? this.formatter.highlight(choice.label) : choice.label; const hint = choice.hint ? this.formatter.dim(` - ${choice.hint}`) : ''; const disabled = choice.disabled ? this.formatter.dim(' (disabled)') : ''; this.formatter.writeln(` ${cursor} ${checkbox} ${label}${hint}${disabled}`); } }; // Initial display for (let i = 0; i < choices.length; i++) { this.formatter.writeln(''); } displayChoices(); return new Promise((resolve, reject) => { const rl = this.createInterface(); if (process.stdin.isTTY) { process.stdin.setRawMode(true); } process.stdin.resume(); const handleKeypress = (key) => { const keyStr = key.toString(); if (keyStr === '\x1b[A') { // Up currentIndex = (currentIndex - 1 + choices.length) % choices.length; displayChoices(); } else if (keyStr === '\x1b[B') { // Down currentIndex = (currentIndex + 1) % choices.length; displayChoices(); } else if (keyStr === ' ') { // Space if (!choices[currentIndex].disabled) { if (selected.has(currentIndex)) { selected.delete(currentIndex); } else { // Check max limit if (!max || selected.size < max) { selected.add(currentIndex); } } displayChoices(); } } else if (keyStr === '\r' || keyStr === '\n') { // Enter // Validate selection if (required && selected.size === 0) { this.formatter.writeln(this.formatter.error(' At least one option must be selected')); return; } if (min && selected.size < min) { this.formatter.writeln(this.formatter.error(` At least ${min} options must be selected`)); return; } cleanup(); const selectedValues = Array.from(selected).map(i => choices[i].value); const selectedLabels = Array.from(selected).map(i => choices[i].label); this.formatter.writeln(); this.formatter.writeln(this.formatter.success(`Selected: ${selectedLabels.join(', ')}`)); resolve(selectedValues); } else if (keyStr === '\x03') { // Ctrl+C cleanup(); reject(new Error('User cancelled')); } else if (keyStr === 'a') { // Select all if (!max || choices.length <= max) { for (let i = 0; i < choices.length; i++) { if (!choices[i].disabled) { selected.add(i); } } displayChoices(); } } else if (keyStr === 'n') { // Select none selected.clear(); displayChoices(); } }; const cleanup = () => { process.stdin.removeListener('data', handleKeypress); if (process.stdin.isTTY) { process.stdin.setRawMode(false); } this.close(); }; process.stdin.on('data', handleKeypress); }); } // ============================================ // Text Prompt (Multi-line) // ============================================ async text(message, placeholder) { this.formatter.writeln(); this.formatter.writeln(this.formatter.bold(`? ${message}`)); if (placeholder) { this.formatter.writeln(this.formatter.dim(` ${placeholder}`)); } this.formatter.writeln(this.formatter.dim(' (Enter an empty line to finish)')); this.formatter.writeln(); const lines = []; while (true) { const line = await this.question(' > '); if (line === '') { break; } lines.push(line); } this.close(); return lines.join('\n'); } // ============================================ // Number Prompt // ============================================ async number(message, options = {}) { const { default: defaultValue, min, max } = options; const validate = (value) => { const num = Number(value); if (isNaN(num)) { return 'Please enter a valid number'; } if (min !== undefined && num < min) { return `Value must be at least ${min}`; } if (max !== undefined && num > max) { return `Value must be at most ${max}`; } return true; }; const result = await this.input({ message, default: defaultValue?.toString(), validate }); return Number(result); } // ============================================ // Autocomplete Prompt // ============================================ async autocomplete(message, choices, options = {}) { const { limit = 10 } = options; this.formatter.writeln(); this.formatter.writeln(this.formatter.bold(`? ${message}`)); this.formatter.writeln(this.formatter.dim(' (Type to filter, arrow keys to navigate)')); let query = ''; let selectedIndex = 0; let filteredChoices = choices.slice(0, limit); const filterChoices = (q) => { if (q === '') return choices.slice(0, limit); const normalized = q.toLowerCase(); return choices .filter(c => c.label.toLowerCase().includes(normalized)) .slice(0, limit); }; const displayChoices = () => { // Clear previous output process.stdout.write(`\x1b[${filteredChoices.length + 1}A`); process.stdout.write('\x1b[J'); // Show input this.formatter.writeln(` ${this.formatter.dim('>')} ${query}`); // Show filtered options for (let i = 0; i < filteredChoices.length; i++) { const choice = filteredChoices[i]; const isSelected = i === selectedIndex; const prefix = isSelected ? this.formatter.info('>') : ' '; const label = isSelected ? this.formatter.highlight(choice.label) : choice.label; this.formatter.writeln(` ${prefix} ${label}`); } }; // Initial display this.formatter.writeln(''); for (let i = 0; i < limit; i++) { this.formatter.writeln(''); } displayChoices(); return new Promise((resolve, reject) => { const rl = this.createInterface(); if (process.stdin.isTTY) { process.stdin.setRawMode(true); } process.stdin.resume(); const handleKeypress = (key) => { const keyStr = key.toString(); if (keyStr === '\x1b[A') { // Up selectedIndex = Math.max(0, selectedIndex - 1); displayChoices(); } else if (keyStr === '\x1b[B') { // Down selectedIndex = Math.min(filteredChoices.length - 1, selectedIndex + 1); displayChoices(); } else if (keyStr === '\r' || keyStr === '\n') { // Enter if (filteredChoices.length > 0) { cleanup(); const selected = filteredChoices[selectedIndex]; this.formatter.writeln(); this.formatter.writeln(this.formatter.success(`Selected: ${selected.label}`)); resolve(selected.value); } } else if (keyStr === '\x7f' || keyStr === '\x08') { // Backspace query = query.slice(0, -1); filteredChoices = filterChoices(query); selectedIndex = 0; displayChoices(); } else if (keyStr === '\x03') { // Ctrl+C cleanup(); reject(new Error('User cancelled')); } else if (keyStr.charCodeAt(0) >= 32 && keyStr.charCodeAt(0) < 127) { // Printable character query += keyStr; filteredChoices = filterChoices(query); selectedIndex = 0; displayChoices(); } }; const cleanup = () => { process.stdin.removeListener('data', handleKeypress); if (process.stdin.isTTY) { process.stdin.setRawMode(false); } this.close(); }; process.stdin.on('data', handleKeypress); }); } } // Export singleton and convenience functions export const promptManager = new PromptManager(); export const select = (options) => promptManager.select(options); export const confirm = (options) => promptManager.confirm(options); export const input = (options) => promptManager.input(options); export const multiSelect = (options) => promptManager.multiSelect(options); export const text = (message, placeholder) => promptManager.text(message, placeholder); export const number = (message, options) => promptManager.number(message, options); export const autocomplete = (message, choices, options) => promptManager.autocomplete(message, choices, options); //# sourceMappingURL=prompt.js.map