// lib/parsers/arg-parser.mjs // Parse $ARGUMENTS strings for the four voyage commands. // // Each command has its own valid-flag set; passing flags from another command // produces an `unknown_flags` array but does not error — the caller decides. const FLAG_SCHEMA = { trekbrief: { boolean: ['--quick', '--fg'], valued: ['--profile', '--approve'], aliases: {}, }, trekresearch: { boolean: ['--quick', '--local', '--external', '--fg'], valued: ['--project', '--profile'], aliases: {}, }, trekplan: { boolean: ['--quick', '--fg'], valued: ['--project', '--brief', '--export', '--decompose', '--profile'], multi: ['--research'], aliases: {}, }, trekexecute: { boolean: ['--resume', '--dry-run', '--validate', '--fg'], valued: ['--project', '--step', '--session', '--profile'], aliases: {}, }, trekreview: { boolean: ['--quick', '--fg', '--dry-run', '--validate', '--workflow'], valued: ['--project', '--since', '--profile'], aliases: {}, }, trekcontinue: { boolean: ['--help', '--cleanup', '--confirm', '--dry-run'], valued: ['--profile'], aliases: {}, }, }; /** * @param {string} argString Raw $ARGUMENTS as the command sees it. * @param {keyof FLAG_SCHEMA} command * @returns {{ * command: string, * flags: Record, * positional: string[], * unknown: string[], * errors: Array<{code: string, message: string}>, * }} */ export function parseArgs(argString, command) { const schema = FLAG_SCHEMA[command]; if (!schema) { return { command, flags: {}, positional: [], unknown: [], errors: [{ code: 'ARG_UNKNOWN_COMMAND', message: `Unknown command: ${command}` }], }; } const tokens = tokenize(argString); const flags = {}; const positional = []; const unknown = []; const errors = []; for (let i = 0; i < tokens.length; i++) { const tok = tokens[i]; if (!tok.startsWith('--')) { positional.push(tok); continue; } if (schema.boolean.includes(tok)) { flags[tok] = true; continue; } if (schema.valued.includes(tok)) { const next = tokens[i + 1]; if (next === undefined || next.startsWith('--')) { errors.push({ code: 'ARG_MISSING_VALUE', message: `Flag ${tok} requires a value` }); } else { flags[tok] = next; i++; } continue; } if (schema.multi && schema.multi.includes(tok)) { const collected = []; while (i + 1 < tokens.length && !tokens[i + 1].startsWith('--')) { collected.push(tokens[i + 1]); i++; } if (collected.length === 0) { errors.push({ code: 'ARG_MISSING_VALUE', message: `Flag ${tok} requires at least one value` }); } else { flags[tok] = collected; } continue; } unknown.push(tok); } return { command, flags, positional, unknown, errors }; } function tokenize(s) { if (typeof s !== 'string') return []; const trimmed = s.trim(); if (trimmed === '') return []; const out = []; const re = /"([^"]*)"|'([^']*)'|(\S+)/g; let m; while ((m = re.exec(trimmed)) !== null) { out.push(m[1] !== undefined ? m[1] : m[2] !== undefined ? m[2] : m[3]); } return out; } export { FLAG_SCHEMA }; // Flags a command cannot run without, and its usage line. The CLI below emits // the error, so the command prose relays code output instead of composing the // message (a model does not always repeat prose word for word — proevesett // case review-requires-project failed 2 of 10 on backticks around the flag). const REQUIRED = { trekreview: { flags: [['--project', '']], usage: 'Usage: /trekreview --project [--since ] [--quick] [--validate] [--dry-run]', }, }; // CLI: node lib/parsers/arg-parser.mjs --command -- $ARGUMENTS // stdout: the parse result as JSON. exit 0, or exit 1 with each error as // "Error: " plus the usage line on stderr. if (import.meta.url === `file://${process.argv[1]}`) { const argv = process.argv.slice(2); const ci = argv.indexOf('--command'); const command = ci >= 0 ? argv[ci + 1] : undefined; const dd = argv.indexOf('--'); const rest = dd >= 0 ? argv.slice(dd + 1) : []; const r = parseArgs(rest.map((t) => (/\s/.test(t) ? `"${t}"` : t)).join(' '), command); const req = REQUIRED[command]; if (req) { for (const [flag, metavar] of req.flags) { if (!(flag in r.flags)) { r.errors.push({ code: 'ARG_REQUIRED_MISSING', message: `${flag} ${metavar} is required.` }); } } } process.stdout.write(JSON.stringify(r) + '\n'); if (r.errors.length > 0) { for (const e of r.errors) process.stderr.write(`Error: ${e.message}\n`); if (req) process.stderr.write(`${req.usage}\n`); process.exit(1); } process.exit(0); }