mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-09-15 14:35:32 +02:00
* refactor(cli): list workspaces natively instead of via the worker image * feat(cli): preflight that Docker is installed and running * feat(cli): stop scans by workspace or --all, terminating their Temporal workflows * fix(worker): abort the running agent on cancellation so Temporal cancel takes effect * refactor(cli): split destructive teardown out of stop into a reset command * refactor(cli): centralise flag parsing and confirmation across commands * fix(cli): pass provider credentials to docker by name to keep secrets out of argv * feat(cli): add per-command help via <command> --help/-h and help <command> * feat(cli): replace raw docker output with clack spinners for infra and scan teardown * fix(cli): verify scan stop by re-querying container and workflow state instead of assuming success * fix(cli): resolve running state before prompting on stop and report no-op stops honestly * refactor(cli): show splash first and drive start with one spinner resolving to a clean line * fix(cli): validate --url up front so a bad value fails cleanly instead of a late crash * refactor(cli): centralize error reporting with fail() for expected errors and a crash handler that logs the stack and links the issue tracker * feat(cli): add --json/--plain machine-readable output to workspaces and status * refactor(cli): remove the workspaces command * refactor(cli): remove the status command * feat(cli): add 'progress <workspace>' — live scan progress from Temporal * fix(cli): mark metric-less agents as skipped in progress, not done * feat(cli): animate running agents in progress with a clack-style spinner * feat(cli): rename progress->status, reveal agents as they run, show live per-agent elapsed * fix(cli): mark passed-over phases as skipped live, not pending * style(cli): rename status footer 'Wall-clock' to 'Time Taken', drop the parenthetical * style(cli): drop '(sum of agents)' from status total cost line * style(cli): green filled circle for completed, Shannon gold for running * style(cli): use Shannon gold in place of green in status * feat(cli): suggest closest command or flag on typo * refactor(cli): single-source start help and drop ./repos bare-name shortcut * feat(cli): name providers and fix in multi-provider credential error * feat(cli): support --flag=value syntax and expand leading ~ in paths * refactor(cli): centralize ANSI color codes in colors.ts * feat(cli): add scans command listing completed scans with cost and duration * fix(cli): keep stdout clean off-TTY for logs and start * feat(cli): add repo link to top-level help * feat(worker): record auth-validation metrics and register resume attempts early * refactor(cli): share resume-aware workflow-id resolution and surface root-cause failures * feat(cli): add status --json, auth phase, dashboard link, and stable live redraw * refactor(cli): drop cost from status and scans output * feat(worker): surface both PDF and markdown report at run root * refactor(cli): normalize error/warning prefixing through fail and warn * feat(cli): add version --json for machine-readable output * refactor(cli): rename start --debug to --keep-container * refactor(cli): point start's progress hint at status instead of the Temporal dashboard * refactor(cli): centralize the mode-aware command prefix * refactor(cli): trim start and logs output to durable facts off-TTY * feat(cli): require typed confirmation for reset instead of --yes reset permanently wipes all Temporal data and volumes — a severe, irreversible action. Replace its default y/N confirm (bypassable with --yes) with a typed-word confirmation that has no bypass, so the wipe can only be triggered by a deliberate interactive answer. * feat(cli): surface logs and status hints after start on a TTY * feat(cli): exit 2 on usage errors, distinct from operational failures * feat(cli): add start --follow to stream logs and exit on scan outcome * refactor(cli): redesign splash with sunset-gradient wordmark and truecolor * refactor(cli): remove the uninstall command * docs: sync CLI docs with removed uninstall/workspaces, new scans and --follow * docs: fix reset confirmation — typed confirm, not --yes/-y * style(cli): restructure status footer with divider, aligned Logs/Temporal rows * feat(cli): show splash in the status command * fix(worker): validate auth-state shape, not entry count * docs: correct reset confirmation and add markdown report to run-root docs
107 lines
3.7 KiB
TypeScript
107 lines
3.7 KiB
TypeScript
/**
|
|
* Shared argument parsing for CLI commands.
|
|
*
|
|
* Every command declares which boolean flags, value options, and positionals it
|
|
* accepts; `parseArgs` resolves aliases, rejects anything unrecognized, and hands
|
|
* back a typed result. This centralizes the common flags (notably `--yes`/`-y`) so
|
|
* each command no longer re-hardcodes `args.includes('--yes')`, and it makes
|
|
* unknown flags and stray arguments fail loudly instead of being silently ignored.
|
|
*/
|
|
|
|
import { closestMatch } from './suggest.js';
|
|
|
|
/** Thrown when argv does not match a command's schema. The dispatcher formats it. */
|
|
export class ArgError extends Error {}
|
|
|
|
/** Tokens that set the "skip confirmation" flag, declared once for every command. */
|
|
export const YES_FLAGS = ['--yes', '-y'] as const;
|
|
|
|
export interface ArgSchema {
|
|
/** Boolean flags: result key -> accepted tokens (canonical plus any aliases). */
|
|
readonly booleans?: Record<string, readonly string[]>;
|
|
/** Value-taking options: result key -> accepted tokens. */
|
|
readonly values?: Record<string, readonly string[]>;
|
|
/** Maximum positional arguments allowed. Defaults to 0. */
|
|
readonly maxPositionals?: number;
|
|
/** Extra guidance appended to the error when too many positionals are given. */
|
|
readonly positionalHint?: string;
|
|
}
|
|
|
|
export interface ParsedArgs {
|
|
readonly flags: Record<string, boolean>;
|
|
readonly values: Record<string, string>;
|
|
readonly positionals: readonly string[];
|
|
}
|
|
|
|
/** Build a token -> result-key lookup from a schema section. */
|
|
function indexTokens(section: Record<string, readonly string[]>): Map<string, string> {
|
|
const byToken = new Map<string, string>();
|
|
for (const [key, tokens] of Object.entries(section)) {
|
|
for (const token of tokens) {
|
|
byToken.set(token, key);
|
|
}
|
|
}
|
|
return byToken;
|
|
}
|
|
|
|
export function parseArgs(argv: readonly string[], schema: ArgSchema): ParsedArgs {
|
|
const booleanByToken = indexTokens(schema.booleans ?? {});
|
|
const valueByToken = indexTokens(schema.values ?? {});
|
|
const maxPositionals = schema.maxPositionals ?? 0;
|
|
|
|
const flags: Record<string, boolean> = {};
|
|
const values: Record<string, string> = {};
|
|
const positionals: string[] = [];
|
|
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const arg = argv[i];
|
|
if (arg === undefined) {
|
|
continue;
|
|
}
|
|
|
|
const equalsIndex = arg.startsWith('--') ? arg.indexOf('=') : -1;
|
|
const token = equalsIndex === -1 ? arg : arg.slice(0, equalsIndex);
|
|
const inlineValue = equalsIndex === -1 ? undefined : arg.slice(equalsIndex + 1);
|
|
|
|
const booleanKey = booleanByToken.get(token);
|
|
if (booleanKey !== undefined) {
|
|
if (inlineValue !== undefined) {
|
|
throw new ArgError(`Flag ${token} does not take a value`);
|
|
}
|
|
flags[booleanKey] = true;
|
|
continue;
|
|
}
|
|
|
|
const valueKey = valueByToken.get(token);
|
|
if (valueKey !== undefined) {
|
|
if (inlineValue !== undefined) {
|
|
values[valueKey] = inlineValue;
|
|
continue;
|
|
}
|
|
const next = argv[i + 1];
|
|
if (next === undefined || next.startsWith('-')) {
|
|
throw new ArgError(`Option ${token} requires a value`);
|
|
}
|
|
values[valueKey] = next;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if (arg.startsWith('-')) {
|
|
const suggestion = closestMatch(token, [...booleanByToken.keys(), ...valueByToken.keys()]);
|
|
const hint = suggestion ? `\nDid you mean '${suggestion}'?` : '';
|
|
throw new ArgError(`Unknown option: ${token}${hint}`);
|
|
}
|
|
|
|
positionals.push(arg);
|
|
}
|
|
|
|
if (positionals.length > maxPositionals) {
|
|
const extra = positionals[maxPositionals];
|
|
const hint = schema.positionalHint ? `\n${schema.positionalHint}` : '';
|
|
throw new ArgError(`Unexpected argument: ${extra}${hint}`);
|
|
}
|
|
|
|
return { flags, values, positionals };
|
|
}
|