UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

516 lines 20.6 kB
/** * V3 CLI Command Parser * Advanced argument parsing with validation and type coercion */ export class CommandParser { options; commands = new Map(); lazyCommandNames = new Set(); globalOptions = []; constructor(options = {}) { this.options = { stopAtFirstNonFlag: false, allowUnknownFlags: false, ...options }; this.initializeGlobalOptions(); } initializeGlobalOptions() { this.globalOptions = [ { name: 'help', short: 'h', description: 'Show help information', type: 'boolean', default: false }, { name: 'version', short: 'V', description: 'Show version number', type: 'boolean', default: false }, { name: 'verbose', short: 'v', description: 'Enable verbose output', type: 'boolean', default: false }, { name: 'quiet', short: 'Q', description: 'Suppress non-essential output', type: 'boolean', default: false }, { name: 'config', short: 'c', description: 'Path to configuration file', type: 'string' }, { name: 'format', // Note: removed global short flag 'f' — it collides with 50+ subcommand // flags (force, follow, file, feature, full) causing unpredictable behavior // depending on parser resolution order (#1425). Use --format instead. description: 'Output format (text, json, table)', type: 'string', default: 'text', choices: ['text', 'json', 'table'] }, { name: 'no-color', description: 'Disable colored output', type: 'boolean', default: false }, { name: 'interactive', short: 'i', description: 'Enable interactive mode', type: 'boolean', default: true } ]; } registerCommand(command) { this.commands.set(command.name, command); if (command.aliases) { for (const alias of command.aliases) { this.commands.set(alias, command); } } } /** * Register a lazy-loaded command's name so Pass 1/Pass 2 can recognize it as * a command position even though its full definition hasn't been loaded yet. * Fix for #1596: without this, lazy commands like `daemon start` were * mis-routed because Pass 1 walked past `daemon` and greedy-matched `start`. */ registerLazyCommandName(name) { this.lazyCommandNames.add(name); } /** * #1791.2 — true when `name` is a lazy command that hasn't been promoted * to a fully registered Command yet. The CLI uses this to eagerly load * the module before parsing so its subcommand flags (e.g. `-d` for * `hive-mind task --description`) are scoped into the alias map. Without * this, lazy commands' short flags silently fall through to global * resolution and the action handler sees an empty `flags.description`. */ isLazyOnly(name) { return this.lazyCommandNames.has(name) && !this.commands.has(name); } isKnownCommandName(name) { return this.commands.has(name) || this.lazyCommandNames.has(name); } getCommand(name) { return this.commands.get(name); } getAllCommands() { // Return unique commands (filter out aliases) const seen = new Set(); return Array.from(this.commands.values()).filter(cmd => { if (seen.has(cmd)) return false; seen.add(cmd); return true; }); } parse(args) { const result = { command: [], flags: { _: [] }, positional: [], raw: [...args] }; // Pass 1: Identify command and subcommand (skip flags). // Fix for #1596: the first non-flag positional is ALWAYS the command slot. // If it's a known command (sync or lazy) we resolve it; otherwise we stop // searching — we MUST NOT walk past it and greedy-match a later arg as the // command, because that's what caused `daemon start` to resolve as `start` // with `daemon` left as a positional. let resolvedCmd; let resolvedSub; let sawFirstPositional = false; for (const arg of args) { if (arg.startsWith('-')) continue; if (!sawFirstPositional) { sawFirstPositional = true; if (this.commands.has(arg)) { resolvedCmd = this.commands.get(arg); continue; } // Lazy command: we know its name but not its subcommands. Stop the // walk here — we'll rely on Pass 2 to push it onto commandPath. if (this.lazyCommandNames.has(arg)) { break; } // Unknown first positional — not a command. Stop walking. break; } if (resolvedCmd && !resolvedSub && resolvedCmd.subcommands) { resolvedSub = resolvedCmd.subcommands.find(sc => sc.name === arg || sc.aliases?.includes(arg)); } } // Pass 2: Build aliases scoped to the resolved subcommand // Subcommand-specific aliases take priority over global ones const aliases = this.buildScopedAliases(resolvedSub || resolvedCmd); const booleanFlags = this.getScopedBooleanFlags(resolvedSub || resolvedCmd); let i = 0; let parsingFlags = true; while (i < args.length) { const arg = args[i]; // Check for end of flags marker if (arg === '--') { parsingFlags = false; i++; continue; } // Handle flags if (parsingFlags && arg.startsWith('-')) { const parseResult = this.parseFlag(args, i, aliases, booleanFlags); // Apply to result flags Object.assign(result.flags, parseResult.flags); i = parseResult.nextIndex; continue; } // Handle positional arguments. // Fix for #1596: treat lazy command names as commands here too so that // downstream dispatch sees `commandPath = ['daemon', 'start']` instead of // `commandPath = ['start'], positional = ['daemon']`. if (result.command.length === 0 && this.isKnownCommandName(arg)) { // This is a command result.command.push(arg); // Check for subcommand (level 1) — only possible for sync commands // whose subcommand definitions are already loaded. const cmd = this.commands.get(arg); if (cmd?.subcommands && i + 1 < args.length) { const nextArg = args[i + 1]; const subCmd = cmd.subcommands.find(sc => sc.name === nextArg || sc.aliases?.includes(nextArg)); if (subCmd) { result.command.push(nextArg); i++; // Check for nested subcommand (level 2) if (subCmd.subcommands && i + 1 < args.length) { const nestedArg = args[i + 1]; const nestedCmd = subCmd.subcommands.find(sc => sc.name === nestedArg || sc.aliases?.includes(nestedArg)); if (nestedCmd) { result.command.push(nestedArg); i++; // Check for deeply nested subcommand (level 3) if (nestedCmd.subcommands && i + 1 < args.length) { const deepArg = args[i + 1]; const deepCmd = nestedCmd.subcommands.find(sc => sc.name === deepArg || sc.aliases?.includes(deepArg)); if (deepCmd) { result.command.push(deepArg); i++; } } } } } } } else { // Positional argument result.positional.push(arg); result.flags._.push(arg); } i++; } // Apply defaults this.applyDefaults(result.flags); return result; } parseFlag(args, index, aliases, booleanFlags) { const flags = { _: [] }; const arg = args[index]; let nextIndex = index + 1; if (arg.startsWith('--')) { // Long flag const equalIndex = arg.indexOf('='); if (equalIndex !== -1) { // --flag=value const key = arg.slice(2, equalIndex); const value = arg.slice(equalIndex + 1); flags[this.normalizeKey(key)] = this.parseValue(value); } else if (arg.startsWith('--no-')) { // --no-flag (boolean negation) const key = arg.slice(5); flags[this.normalizeKey(key)] = false; } else { const key = arg.slice(2); const normalizedKey = this.normalizeKey(key); if (booleanFlags.has(normalizedKey)) { // #explore-flag: allow an explicit boolean value (`--explore false`, // `--explore true`). Without this, a default-true boolean could never // be disabled via the space form — the value was dropped and the flag // forced to true. The `=` form already worked via parseValue. if (nextIndex < args.length && this.isBooleanLiteral(args[nextIndex])) { flags[normalizedKey] = args[nextIndex].toLowerCase() === 'true'; nextIndex++; } else { flags[normalizedKey] = true; } } else if (nextIndex < args.length && this.isFlagValue(args[nextIndex])) { flags[normalizedKey] = this.parseValue(args[nextIndex]); nextIndex++; } else { flags[normalizedKey] = true; } } } else if (arg.startsWith('-')) { // Short flag(s) const chars = arg.slice(1); if (chars.length === 1) { // Single short flag const key = aliases[chars] || chars; const normalizedKey = this.normalizeKey(key); if (booleanFlags.has(normalizedKey)) { // #explore-flag: short boolean flags also accept an explicit value // (`-e false`) so a default-true boolean can be turned off. if (nextIndex < args.length && this.isBooleanLiteral(args[nextIndex])) { flags[normalizedKey] = args[nextIndex].toLowerCase() === 'true'; nextIndex++; } else { flags[normalizedKey] = true; } } else if (nextIndex < args.length && this.isFlagValue(args[nextIndex])) { flags[normalizedKey] = this.parseValue(args[nextIndex]); nextIndex++; } else { flags[normalizedKey] = true; } } else { // Multiple short flags combined (e.g., -abc) for (const char of chars) { const key = aliases[char] || char; flags[this.normalizeKey(key)] = true; } } } return { flags, nextIndex }; } /** * Decide whether `arg` should be consumed as the VALUE of the preceding flag, * rather than treated as the next flag. * * Bug fix (audit #1, follow-up to #2222): a negative numeric value such as * `-1.0` starts with '-', so the old `!arg.startsWith('-')` test rejected it * as a value and parsed it as a (bogus) short flag. For `route feedback * -r -1.0` this silently dropped the value and coerced reward to `true` → 1.0, * so NEGATIVE feedback REINFORCED the agent. Only `--reward=-1.0` worked. * * Anything not starting with '-' is a value (unchanged). Anything that starts * with '-' is a value ONLY if it is a pure negative number (e.g. `-1`, `-1.0`, * `-3.14`, `-1e3`). Real flags like `-r`, `--reward`, `-abc` are never numeric * after the leading dash, so they are still correctly treated as flags. */ isFlagValue(arg) { if (!arg.startsWith('-')) return true; // Negative number: '-' followed by a parseable numeric literal. return /^-\d*\.?\d+(?:[eE][+-]?\d+)?$/.test(arg); } /** True for the literal tokens `true`/`false` (case-insensitive). Used so a * boolean flag can take an explicit value in the space form, e.g. * `--explore false` / `-e true`. */ isBooleanLiteral(arg) { const a = arg.toLowerCase(); return a === 'true' || a === 'false'; } parseValue(value) { // Boolean if (value.toLowerCase() === 'true') return true; if (value.toLowerCase() === 'false') return false; // Number const num = Number(value); if (!isNaN(num) && value.trim() !== '') return num; // String return value; } normalizeKey(key) { // Convert kebab-case to camelCase return key.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); } buildAliases() { const aliases = {}; for (const opt of this.globalOptions) { if (opt.short) { aliases[opt.short] = opt.name; } } // Add aliases from all commands and subcommands for (const cmd of this.commands.values()) { if (cmd.options) { for (const opt of cmd.options) { if (opt.short) { aliases[opt.short] = opt.name; } } } // Also include subcommands' options if (cmd.subcommands) { for (const sub of cmd.subcommands) { if (sub.options) { for (const opt of sub.options) { if (opt.short) { aliases[opt.short] = opt.name; } } } } } } return { ...aliases, ...this.options.aliases }; } /** * Build aliases scoped to a specific command/subcommand. * The resolved command's short flags take priority over global ones, * fixing collisions where multiple subcommands use the same short flag (e.g. -t). */ buildScopedAliases(resolvedCmd) { // Start with global aliases as base const aliases = this.buildAliases(); // Override with the resolved command's own options (these take priority) if (resolvedCmd?.options) { for (const opt of resolvedCmd.options) { if (opt.short) { aliases[opt.short] = opt.name; } } } return aliases; } /** * Get boolean flags scoped to a specific command/subcommand. */ getScopedBooleanFlags(resolvedCmd) { const flags = this.getBooleanFlags(); if (resolvedCmd?.options) { for (const opt of resolvedCmd.options) { if (opt.type === 'boolean') { flags.add(this.normalizeKey(opt.name)); } } } return flags; } getBooleanFlags() { const flags = new Set(); for (const opt of this.globalOptions) { if (opt.type === 'boolean') { flags.add(this.normalizeKey(opt.name)); } } // Add boolean flags from all commands and subcommands for (const cmd of this.commands.values()) { if (cmd.options) { for (const opt of cmd.options) { if (opt.type === 'boolean') { flags.add(this.normalizeKey(opt.name)); } } } // Also include subcommands' boolean flags if (cmd.subcommands) { for (const sub of cmd.subcommands) { if (sub.options) { for (const opt of sub.options) { if (opt.type === 'boolean') { flags.add(this.normalizeKey(opt.name)); } } } } } } if (this.options.booleanFlags) { for (const flag of this.options.booleanFlags) { flags.add(this.normalizeKey(flag)); } } return flags; } applyDefaults(flags) { // Apply global option defaults for (const opt of this.globalOptions) { const key = this.normalizeKey(opt.name); if (flags[key] === undefined && opt.default !== undefined) { flags[key] = opt.default; } } // Apply custom defaults if (this.options.defaults) { for (const [key, value] of Object.entries(this.options.defaults)) { const normalizedKey = this.normalizeKey(key); if (flags[normalizedKey] === undefined) { flags[normalizedKey] = value; } } } } validateFlags(flags, command) { const errors = []; const allOptions = [...this.globalOptions]; if (command?.options) { allOptions.push(...command.options); } // Check required flags for (const opt of allOptions) { const key = this.normalizeKey(opt.name); if (opt.required && (flags[key] === undefined || flags[key] === '')) { errors.push(`Required option missing: --${opt.name}`); } // Check choices if (opt.choices && flags[key] !== undefined) { const value = String(flags[key]); if (!opt.choices.includes(value)) { errors.push(`Invalid value for --${opt.name}: ${value}. Must be one of: ${opt.choices.join(', ')}`); } } // Run custom validator if (opt.validate && flags[key] !== undefined) { const result = opt.validate(flags[key]); if (result !== true) { errors.push(typeof result === 'string' ? result : `Invalid value for --${opt.name}`); } } } // Check for unknown flags if not allowed if (!this.options.allowUnknownFlags) { const knownFlags = new Set(allOptions.map(opt => this.normalizeKey(opt.name))); knownFlags.add('_'); // Positional args for (const key of Object.keys(flags)) { if (!knownFlags.has(key) && key !== '_') { errors.push(`Unknown option: --${key}`); } } } return errors; } getGlobalOptions() { return [...this.globalOptions]; } } // Export singleton parser instance export const commandParser = new CommandParser({ allowUnknownFlags: true }); //# sourceMappingURL=parser.js.map