Skip to content

CommandSystem

runtoolkit edited this page Apr 13, 2026 · 1 revision

CommandSystem

Safe, general-purpose command execution. Strings are parsed into name + args; registered handlers are called. Nothing reaches eval or the shell.

Constructor

import { CommandSystem } from './src/command.js';

const commands = new CommandSystem({
  mode: 'series',      // default execution mode for runMany: 'series' | 'parallel'
  concurrency: 4,      // max parallel runners (parallel mode only)
  stopOnError: false,  // stop runMany on first failure
});

Registering commands

commands.register(name, handler, options);

The handler receives a single context object:

commands.register('echo', ({ args, command, registry }) => {
  return args.join(' ');
});

commands.register('add', ({ args }) => {
  return args.map(Number).reduce((a, b) => a + b, 0);
});
Context field Description
args Array of string arguments (parsed from input)
command Normalized command object (name, args, raw)
registry The CommandRegistry instance
+ spread Any fields passed as context to run()

Handler may be async.

Options

commands.register('greet', handler, {
  aliases: ['hi', 'hello'],  // alternative names
  description: 'Greets someone',
  meta: {},                  // arbitrary metadata (deep-cloned)
});

register() returns an unregister function.


Running commands

Single command

const result = await commands.run(input, context);

input can be:

  • a string — parsed as name arg1 arg2 ... (supports quoting and backslash escaping)
  • an object{ name, args, context, meta }
  • a function — called directly as fn({ ...context, command })
  • an array — treated as a batch, run in series
await commands.run('echo hello world');
await commands.run('echo "hello world"');  // single quoted arg
await commands.run({ name: 'add', args: ['3', '4'] });
await commands.run((ctx) => ctx.args.join(','), { args: ['a', 'b'] });

Result shape

{
  ok: boolean,
  value: any,           // return value of handler (if ok)
  error: Error,         // (if not ok)
  command: normalized,
  durationMs: number,
  skipped: boolean,     // true if allowUnknown and command not found
}

Multiple commands

const result = await commands.runMany(inputs, context, options);
await commands.runMany(['echo first', 'echo second']);
await commands.runMany(['echo a', 'echo b'], {}, { mode: 'parallel' });

Result shape:

{
  ok: boolean,       // true if all succeeded
  total: number,
  success: number,
  failed: number,
  results: [...],    // individual result objects
  durationMs: number,
}

Enqueue / flush

For deferred execution (e.g. within a tick loop):

commands.enqueue('echo queued');
commands.enqueue('add 1 2', { userId: 'alice' });

// Drain the queue (called automatically by Engine every tick)
await commands.flush({ stopOnError: false });
// → same shape as runMany result

Registry management

commands.unregister('echo');
commands.has('echo');    // → boolean
commands.list();
// → [{ name, description, meta }, ...]

Tokenizer

tokenizeCommandLine is exported separately if you need to parse a command line manually:

import { tokenizeCommandLine } from './src/command.js';

tokenizeCommandLine('echo "hello world"');  // → ['echo', 'hello world']
tokenizeCommandLine("say it\\'s fine");     // → ['say', "it's fine"]

Throws SyntaxError on unclosed quotes.


normalizeCommand

Normalizes any supported input shape into a consistent object:

import { normalizeCommand } from './src/command.js';

normalizeCommand('echo hi');
// → { kind: 'command', name: 'echo', args: ['hi'], raw: 'echo hi' }

normalizeCommand(() => {});
// → { kind: 'function', fn: [Function], args: [] }

Clone this wiki locally