Files
shannon/apps/cli/src/suggest.ts
T
ezl-keygraph d41ae9c20d feat(cli): overhaul commands and add live scan status (#424)
* 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
2026-08-18 15:46:25 +05:30

59 lines
2.0 KiB
TypeScript

/**
* "Did you mean?" suggestions for mistyped commands and flags.
*
* A single Levenshtein-based matcher powers both the unknown-command path in the
* dispatcher and the unknown-option path in `parseArgs`, so a typo like `statsu`
* or `--workspce` points the user at the closest real name instead of just failing.
*/
/** Levenshtein edit distance between two strings (insertions, deletions, substitutions). */
export function editDistance(a: string, b: string): number {
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;
// Rolling single row; `diagonal` and `above` carry the two neighbours a full grid would.
const row = Array.from({ length: b.length + 1 }, (_, j) => j);
for (let i = 1; i <= a.length; i++) {
let diagonal = row[0] as number;
row[0] = i;
for (let j = 1; j <= b.length; j++) {
const above = row[j] as number;
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
row[j] = Math.min(above + 1, (row[j - 1] as number) + 1, diagonal + cost);
diagonal = above;
}
}
return row[b.length] as number;
}
/**
* The candidate closest to `input`, or undefined if none is near enough.
*
* A prefix match ("stat" -> "status") wins first; otherwise the lowest edit
* distance within a length-scaled threshold, so unrelated words don't match.
*/
export function closestMatch(input: string, candidates: readonly string[]): string | undefined {
if (input.length >= 2) {
const prefix = candidates.find((candidate) => candidate.startsWith(input));
if (prefix) return prefix;
}
let best: string | undefined;
let bestDistance = Number.POSITIVE_INFINITY;
for (const candidate of candidates) {
if (candidate.length <= 3) continue;
const distance = editDistance(input, candidate);
if (distance < bestDistance) {
bestDistance = distance;
best = candidate;
}
}
if (best === undefined) return undefined;
const threshold = Math.max(2, Math.floor(best.length / 3));
return bestDistance <= threshold ? best : undefined;
}