/** * 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; } 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 [args...] [--cwd ] [--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 ] [--limit N] [--all] [--json]', handler: runsCommand, }, { name: 'approve', description: 'Approve a paused workflow run', usage: 'workflow approve [comment] [--json]', handler: approveCommand, }, { name: 'reject', description: 'Reject a paused workflow run', usage: 'workflow reject [reason] [--json]', handler: rejectCommand, }, { name: 'resume', description: 'Resume a failed workflow run', usage: 'workflow resume [--json]', handler: resumeCommand, }, { name: 'abandon', description: 'Cancel a non-terminal workflow run', usage: 'workflow abandon [--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 [--json]', handler: validateCommand, }, { name: 'convert', description: 'Convert a .sop.md file to a YAML workflow definition', usage: 'workflow convert [--output ]', handler: convertCommand, }, ]; // --------------------------------------------------------------------------- // Help output // --------------------------------------------------------------------------- function printHelp(): void { console.log(''); console.log('Ion — Workflow Engine CLI'); console.log(''); console.log('Usage:'); console.log(' workflow [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 Set working directory'); console.log(' --store Path to workflow store'); console.log(' --db-path

Path to database file'); console.log(''); console.log('Run "workflow --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 { 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); }); }