UNPKG

@m-ld/m-ld-cli

Version:

m-ld Node.js terminal app for local persistence & data loading

137 lines (129 loc) 3.97 kB
const { ChildProcs } = require('./ChildProcs'); const { execute } = require('./Exec'); const { Proc, NOOP } = require('./Proc'); const createYargs = require('yargs/yargs'); const readline = require('readline'); /** * @typedef {object} CmdContext * @property {string} cmdId * @property {ChildProcs} childProcs * @property {import('stream').Readable} stdin * @property {string[]} args * @property {(proc: () => Proc) => void} exec * @property {GlobalOpts} opts */ /** * @abstract * Abstract class for executing command lines */ class CommandLine { /** * @param {GlobalOpts} opts * @param {string} opts.prompt */ constructor(opts) { this.opts = opts; // Running child processes this.childProcs = new ChildProcs; let cmdId = 0; this.nextCmdId = () => `${cmdId++}`; } /** * Executes a command line. * @param {string} line user input * @param {(data: any, ...args: any[]) => void} lineOut a receiver for output lines * @param {(data: any, ...args: any[]) => void} [lineErr] a receiver for error lines * @return {Promise<void>} */ execute(line, lineOut, lineErr = NOOP) { let cleanup = []; return new Promise((resolve, reject) => { const proc = execute(line, this.cmdExecutor(lineOut, lineErr)); if (proc != null) { // Root output (if any) is written to the console until "done" cleanup.push( this.toOut(proc.stdout, lineOut), this.toOut(proc.stderr, lineErr)); // Log host messages to console // noinspection JSUnresolvedFunction proc.on('message', lineOut); // noinspection JSUnresolvedFunction proc.on('error', reject); // noinspection JSUnresolvedFunction proc.on('done', resolve); } else { resolve(); } }).finally(() => { cleanup.forEach(close => close()); }); } /** * @returns {CommandExec} */ cmdExecutor(lineOut, lineErr) { // These context items apply to all composed commands on one line const ctx = /**@type {CmdContext}*/{ cmdId: this.nextCmdId(), childProcs: this.childProcs, opts: this.opts }; return /**@type {CommandExec}*/((args, stdin) => { let /**@type Proc | null*/proc = null, errors = []; const exec = createProc => { if (errors.length === 0) proc = createProc(); }; // These context items are per-command Object.assign(ctx, { args, stdin, exec }); const yargs = createYargs(args) // Not using strict options, some commands delegate .strictCommands(true) .scriptName(this.opts.prompt) .version(false) .exitProcess(false); // Add custom commands and global commands this.buildCommands(yargs, ctx) .command('exit', 'Exit this REPL', yargs => yargs, () => { this.close().catch(lineErr); }) .middleware(argv => { if (argv['help']) yargs.showHelp(lineOut); }, true) .fail(msg => { // Custom fail behaviour, because with exitProcess(false) yargs will // call a command handler even if the parse fails errors.push(msg); }) .parseSync(); if (errors.length > 0) { // Parse failure is a user error, not a process error yargs.showHelp(lineOut); for (let err of errors) lineOut(err); } return proc; }); } /** * Override to add available commands * @param {yargs.Argv} yargs * @param {CmdContext} ctx */ buildCommands(yargs, ctx) { return yargs; } toOut(input, lineOut) { const sl = readline.createInterface({ input, crlfDelay: Infinity }); sl.on('line', lineOut); // Errors in the proc should be handled by "done" sl.on('error', NOOP); return sl.close.bind(sl); } async close() { await this.childProcs.close(); } } exports.CommandLine = CommandLine;