import { readline, execSync, spawnSync } from './node-shell-deps'; import type { Interface as ReadlineInterface } from 'readline'; import { stringToArgs } from './utils'; import { AnyInternalCLI } from './internal-cli'; import { getBin } from '@cli-forge/parser'; export interface InteractiveShellOptions { prompt?: string; prependArgs?: string[]; } type NormalizedInteractiveShellOptions = Required; function normalizeShellOptions( cli: AnyInternalCLI, options?: InteractiveShellOptions ): NormalizedInteractiveShellOptions { return { prompt: options?.prompt ?? (() => { const chain = [cli.name]; if (cli.commandChain.length > 2) { chain.push('...', ...cli.commandChain[cli.command.length - 1]); } else { chain.push(...cli.commandChain); } return chain.join(' ') + ' > '; })(), prependArgs: options?.prependArgs ?? [], }; } export let INTERACTIVE_SHELL: InteractiveShell | undefined; export class InteractiveShell { private readonly rl: ReadlineInterface; private listeners: any[] = []; constructor(cli: AnyInternalCLI, opts?: InteractiveShellOptions) { if (INTERACTIVE_SHELL) { throw new Error( 'Only one interactive shell can be created at a time. Make sure the other instance is closed.' ); } // eslint-disable-next-line @typescript-eslint/no-this-alias INTERACTIVE_SHELL = this; const { prompt, prependArgs } = normalizeShellOptions(cli, opts); this.rl = readline .createInterface({ input: process.stdin, output: process.stdout, prompt: prompt, }) .once('SIGINT', () => { process.emit('SIGINT'); }); // Show the cursor (in case it was hidden) process.stdout.write('\x1b[?25h'); this.rl.prompt(); this.registerLineListener(async (line) => { const nextArgs = stringToArgs(line); let currentCommand = cli; for (const subcommand of cli.commandChain) { currentCommand = currentCommand.registeredCommands[subcommand]; } if (currentCommand.registeredCommands[nextArgs[0]]) { spawnSync( process.execPath, [ ...process.execArgv, getBin(process.argv), ...prependArgs, ...nextArgs, ], { stdio: 'inherit' } ); } else if (nextArgs[0] === 'help') { currentCommand.clone().printHelp(); } else if (nextArgs[0] === 'exit') { this.close(); return true; } else if (line.trim()) { try { execSync(line, { stdio: 'inherit' }); } catch { // ignore } } return false; }); } registerLineListener(callback: (line: string) => Promise) { const wrapped = async (line: string) => { this.rl.pause(); const shouldHalt = await callback(line); if (!shouldHalt) this.rl.prompt(); }; this.listeners.push(wrapped); this.rl.on('line', wrapped); } close() { this.listeners.forEach((listener) => this.rl.off('line', listener)); this.rl.close(); readline.moveCursor(process.stdout, -1 * this.rl.getCursorPos().cols, 0); readline.clearScreenDown(process.stdout); if (INTERACTIVE_SHELL === this) { INTERACTIVE_SHELL = undefined; } } }