Files
boocode/packages/ion/src/cli/index.ts
indifferentketchup 02063072ab chore: add ion package, codesight wiki, work plans, ascli config
New @boocode/ion package (v0.0.1) for inference optimization network.
.codesight/ wiki artifacts for codebase documentation.
.omo/ work plans for openspec cleanup and enhanced file panel.
2026-06-07 22:16:45 +00:00

207 lines
6.1 KiB
TypeScript

/**
* Ion workflow engine CLI entry point.
*
* Pure Node.js CLI using process.argv parsing — no external argparse library.
* Routes subcommands to their respective handler modules.
*
* @example
* node dist/cli/index.js workflow list --json
* node dist/cli/index.js workflow run deploy --cwd /tmp/project
*/
import { parseArgs, buildCliOptions, printJson } from './utils.js';
import type { CliOptions } from './utils.js';
import { listCommand } from './commands/list.js';
import { runCommand } from './commands/run.js';
import { statusCommand } from './commands/status.js';
import { runsCommand } from './commands/runs.js';
import { approveCommand } from './commands/approve.js';
import { rejectCommand } from './commands/reject.js';
import { resumeCommand } from './commands/resume.js';
import { abandonCommand } from './commands/abandon.js';
import { cleanupCommand } from './commands/cleanup.js';
import { validateCommand } from './commands/validate.js';
import { convertCommand } from './commands/convert.js';
// ---------------------------------------------------------------------------
// Command registry
// ---------------------------------------------------------------------------
interface CommandEntry {
name: string;
description: string;
usage: string;
handler: (args: string[], options: CliOptions) => Promise<void>;
}
const COMMANDS: CommandEntry[] = [
{
name: 'list',
description: 'List all available workflows',
usage: 'workflow list [--json]',
handler: listCommand,
},
{
name: 'run',
description: 'Execute a workflow by name',
usage: 'workflow run <name> [args...] [--cwd <path>] [--detach] [--json]',
handler: runCommand,
},
{
name: 'status',
description: 'Show active (running + paused) workflow runs',
usage: 'workflow status [--json]',
handler: statusCommand,
},
{
name: 'runs',
description: 'List recent workflow runs with filters',
usage: 'workflow runs [--status <status>] [--limit N] [--all] [--json]',
handler: runsCommand,
},
{
name: 'approve',
description: 'Approve a paused workflow run',
usage: 'workflow approve <run-id> [comment] [--json]',
handler: approveCommand,
},
{
name: 'reject',
description: 'Reject a paused workflow run',
usage: 'workflow reject <run-id> [reason] [--json]',
handler: rejectCommand,
},
{
name: 'resume',
description: 'Resume a failed workflow run',
usage: 'workflow resume <run-id> [--json]',
handler: resumeCommand,
},
{
name: 'abandon',
description: 'Cancel a non-terminal workflow run',
usage: 'workflow abandon <run-id> [--json]',
handler: abandonCommand,
},
{
name: 'cleanup',
description: 'Remove old workflow run artifacts',
usage: 'workflow cleanup [days] [--json]',
handler: cleanupCommand,
},
{
name: 'validate',
description: 'Validate a workflow definition without executing',
usage: 'workflow validate <name> [--json]',
handler: validateCommand,
},
{
name: 'convert',
description: 'Convert a .sop.md file to a YAML workflow definition',
usage: 'workflow convert <file.sop.md> [--output <path>]',
handler: convertCommand,
},
];
// ---------------------------------------------------------------------------
// Help output
// ---------------------------------------------------------------------------
function printHelp(): void {
console.log('');
console.log('Ion — Workflow Engine CLI');
console.log('');
console.log('Usage:');
console.log(' workflow <command> [options]');
console.log('');
console.log('Commands:');
const maxNameLen = Math.max(...COMMANDS.map((c) => c.name.length));
for (const cmd of COMMANDS) {
const padded = cmd.name.padEnd(maxNameLen + 2);
console.log(` ${padded}${cmd.description}`);
}
console.log('');
console.log('Global options:');
console.log(' --json Output as JSON (suppresses all other output)');
console.log(' --cwd <path> Set working directory');
console.log(' --store <path> Path to workflow store');
console.log(' --db-path <p> Path to database file');
console.log('');
console.log('Run "workflow <command> --help" for command-specific usage.');
console.log('');
}
function printCommandHelp(cmd: CommandEntry): void {
console.log('');
console.log(`workflow ${cmd.name}`);
console.log('');
console.log(` ${cmd.description}`);
console.log('');
console.log('Usage:');
console.log(` ${cmd.usage}`);
console.log('');
}
// ---------------------------------------------------------------------------
// Main entry
// ---------------------------------------------------------------------------
export async function main(argv: string[] = process.argv.slice(2)): Promise<void> {
const { args, options } = parseArgs(argv);
const cliOptions = buildCliOptions(options);
// --help with no command → general help
if (args.length === 0 || options.help === true) {
printHelp();
process.exit(0);
}
const commandName = args[0];
const commandArgs = args.slice(1);
// --help after a command name → command-specific help
if (options.help) {
const cmd = COMMANDS.find((c) => c.name === commandName);
if (cmd) {
printCommandHelp(cmd);
} else {
console.error(`Unknown command: ${commandName}`);
printHelp();
}
process.exit(0);
}
const command = COMMANDS.find((c) => c.name === commandName);
if (!command) {
console.error(`Unknown command: ${commandName}`);
printHelp();
process.exit(1);
return; // unreachable, but satisfies TS control flow
}
try {
await command.handler(commandArgs, cliOptions);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
if (cliOptions.json) {
printJson({ error: message });
} else {
console.error(`Error: ${message}`);
}
process.exit(1);
}
}
// Run when executed directly (not imported).
// In ESM, check import.meta.url to detect direct execution.
const _directRun = typeof import.meta !== 'undefined' && import.meta.url;
if (_directRun) {
main().catch((err: unknown) => {
console.error(err);
process.exit(1);
});
}