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
JavaScript
/**
* 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