From f31f9d721770a3e40893e64946303eb937635e15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:27:11 +0000 Subject: [PATCH 1/3] Initial plan From ecaae84fdadf6979820a796a144ea6aaeb37e53c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:07:46 +0000 Subject: [PATCH 2/3] refactor: eliminate runLegacy, makeLegacyHandler, MIGRATED_COMMANDS from cli.yargs Co-authored-by: SorraTheOrc <250240+SorraTheOrc@users.noreply.github.com> --- src/cli.yargs.ts | 268 +++++++++++++++++++++-------------- src/cli/commands/analyze.ts | 12 ++ src/cli/commands/classify.ts | 13 ++ src/cli/commands/explore.ts | 40 ++++++ src/cli/commands/generate.ts | 23 ++- src/cli/commands/library.ts | 32 +++++ src/cli/commands/list.ts | 14 +- src/cli/commands/play.ts | 4 +- src/cli/commands/sequence.ts | 22 +++ src/cli/commands/show.ts | 5 +- src/cli/commands/stack.ts | 17 +++ src/cli/commands/tui.ts | 6 + src/cli/commands/version.ts | 2 +- 13 files changed, 336 insertions(+), 122 deletions(-) create mode 100644 src/cli/commands/analyze.ts create mode 100644 src/cli/commands/classify.ts create mode 100644 src/cli/commands/explore.ts create mode 100644 src/cli/commands/library.ts create mode 100644 src/cli/commands/sequence.ts create mode 100644 src/cli/commands/stack.ts create mode 100644 src/cli/commands/tui.ts diff --git a/src/cli.yargs.ts b/src/cli.yargs.ts index 28b20ae..9ca1c12 100644 --- a/src/cli.yargs.ts +++ b/src/cli.yargs.ts @@ -2,6 +2,19 @@ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; +import * as generateCmd from "./cli/commands/generate.js"; +import * as listCmd from "./cli/commands/list.js"; +import * as showCmd from "./cli/commands/show.js"; +import * as playCmd from "./cli/commands/play.js"; +import * as versionCmd from "./cli/commands/version.js"; +import * as stackCmd from "./cli/commands/stack.js"; +import * as sequenceCmd from "./cli/commands/sequence.js"; +import * as analyzeCmd from "./cli/commands/analyze.js"; +import * as classifyCmd from "./cli/commands/classify.js"; +import * as exploreCmd from "./cli/commands/explore.js"; +import * as libraryCmd from "./cli/commands/library.js"; +import * as tuiCmd from "./cli/commands/tui.js"; + export const FRAMEWORK_COMMANDS = [ "generate", "list", @@ -17,133 +30,168 @@ export const FRAMEWORK_COMMANDS = [ "tui", ]; -const MIGRATED_COMMANDS = new Set(FRAMEWORK_COMMANDS); - -async function runLegacy(argv: string[]): Promise { - const core = await import("./cli.core.js"); - if (typeof core.main !== "function") { - return 1; - } - return core.main(argv); -} - -function makeLegacyHandler(raw: string[], commandPrefix: string[] = []): () => Promise { - return async () => { - return runLegacy(["node", "cli.ts", ...commandPrefix, ...raw.slice(commandPrefix.length)]); - }; -} - export async function yargsMain(argv: string[] = process.argv): Promise { const raw = hideBin(argv); - const command = raw[0]; - if (raw.includes("--help") || raw.includes("-h") || raw.includes("--version") || raw.includes("-V")) { - return runLegacy(argv); - } + const { main: coreMain } = await import("./cli.core.js"); - if (typeof command !== "string" || !MIGRATED_COMMANDS.has(command)) { - return runLegacy(argv); + // When no arguments or only flags (help/version), delegate directly to coreMain. + const firstNonFlag = raw.find((a) => !a.startsWith("-")); + if (!firstNonFlag) { + return coreMain(["node", "cli.ts", ...raw]); } const y = yargs(raw).scriptName("toneforge"); let exitCode: number | undefined; - // When used programmatically we must avoid yargs calling process.exit(). - // Disable automatic exiting and let the caller decide how to handle exit codes. y.exitProcess(false); - y.strictCommands(); - y.parserConfiguration({ "unknown-options-as-args": true }); y.showHelpOnFail(false); - y.fail((msg, err) => { - throw err ?? new Error(msg ?? "yargs parse failed"); - }); - - y.command("generate", false, () => {}, async () => { - exitCode = await makeLegacyHandler(raw)(); - }); - y.command("list [resource]", false, () => {}, async () => { - exitCode = await makeLegacyHandler(raw)(); - }); - y.command("show ", false, () => {}, async () => { - exitCode = await makeLegacyHandler(raw)(); - }); - y.command("play ", false, () => {}, async () => { - exitCode = await makeLegacyHandler(raw)(); - }); - y.command("version", false, () => {}, async () => { - exitCode = await makeLegacyHandler(raw)(); - }); - y.command( - "stack ", - false, - (cmd) => cmd - .command("render", false, () => {}, () => {}) - .command("inspect", false, () => {}, () => {}), - async () => { - exitCode = await makeLegacyHandler(raw)(); - }, - ); - y.command( - "sequence ", - false, - (cmd) => cmd - .command("generate", false, () => {}, () => {}) - .command("simulate", false, () => {}, () => {}) - .command("inspect", false, () => {}, () => {}), - async () => { - exitCode = await makeLegacyHandler(raw)(); - }, - ); - y.command("analyze", false, () => {}, async () => { - exitCode = await makeLegacyHandler(raw)(); - }); - y.command( - "classify [subcommand]", - false, - (cmd) => cmd.command("search", false, () => {}, () => {}), - async () => { - exitCode = await makeLegacyHandler(raw)(); - }, - ); - y.command( - "explore ", - false, - (cmd) => cmd - .command("sweep", false, () => {}, () => {}) - .command("mutate", false, () => {}, () => {}) - .command("promote", false, () => {}, () => {}) - .command("show", false, () => {}, () => {}) - .command("runs", false, () => {}, () => {}), - async () => { - exitCode = await makeLegacyHandler(raw)(); - }, - ); - y.command( - "library ", - false, - (cmd) => cmd - .command("list", false, () => {}, () => {}) - .command("search", false, () => {}, () => {}) - .command("similar", false, () => {}, () => {}) - .command("export", false, () => {}, () => {}) - .command("regenerate", false, () => {}, () => {}), - async () => { - exitCode = await makeLegacyHandler(raw)(); - }, - ); - y.command("tui", false, () => {}, async () => { - exitCode = await makeLegacyHandler(raw)(); - }); + y.version(false); + y.help(false); + + // Suppress yargs validation errors — coreMain is the authoritative error + // handler and outputs plain-text or JSON depending on the --json flag. + y.fail((_msg, _err) => { /* noop */ }); + + // Single shared handler: all command execution is delegated to coreMain. + // yargs provides routing structure and help-text; coreMain is the + // authoritative implementation. + const handle = async () => { + exitCode = await coreMain(["node", "cli.ts", ...raw]); + }; + + // ── Simple commands ────────────────────────────────────────────────────── + y.command(generateCmd.command, generateCmd.desc, generateCmd.builder, handle); + y.command(listCmd.command, listCmd.desc, listCmd.builder, handle); + y.command(showCmd.command, showCmd.desc, showCmd.builder, handle); + y.command(playCmd.command, playCmd.desc, playCmd.builder, handle); + y.command(versionCmd.command, versionCmd.desc, versionCmd.builder, handle); + y.command(analyzeCmd.command, analyzeCmd.desc, analyzeCmd.builder, handle); + y.command(tuiCmd.command, tuiCmd.desc, tuiCmd.builder, handle); + + // ── Stack (subcommands) ────────────────────────────────────────────────── + y.command(stackCmd.command, stackCmd.desc, (y2) => { + y2.command("render", "Render a stack preset to audio", (y3) => { + y3.option("preset", { type: "string", describe: "Path to stack preset JSON" }) + .option("seed", { type: "string", describe: "Seed for rendering" }) + .option("output", { type: "string", describe: "Output WAV path" }) + .option("layer", { type: "array", describe: "Inline layer spec overrides" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }, handle); + y2.command("inspect", "Inspect a stack preset structure", (y3) => { + y3.option("preset", { type: "string", describe: "Path to stack preset JSON" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }, handle); + }, handle); + + // ── Sequence (subcommands) ─────────────────────────────────────────────── + y.command(sequenceCmd.command, sequenceCmd.desc, (y2) => { + y2.command("generate", "Render a sequence to audio", (y3) => { + y3.option("preset", { type: "string", describe: "Path to sequence preset JSON" }) + .option("seed", { type: "number", describe: "Seed for rendering" }) + .option("output", { type: "string", describe: "Output WAV path" }) + .option("duration", { type: "number", describe: "Duration override in seconds" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }, handle); + y2.command("simulate", "Simulate a sequence and show event schedule", (y3) => { + y3.option("preset", { type: "string", describe: "Path to sequence preset JSON" }) + .option("seed", { type: "number", describe: "Seed for simulation" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }, handle); + y2.command("inspect", "Inspect a sequence preset structure", (y3) => { + y3.option("preset", { type: "string", describe: "Path to sequence preset JSON" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }, handle); + }, handle); + + // ── Classify (optional subcommand) ────────────────────────────────────── + y.command(classifyCmd.command, classifyCmd.desc, (y2) => { + y2.command("search", "Search for classified sounds in a directory", (y3) => { + y3.option("category", { type: "string", describe: "Filter by category" }) + .option("intensity", { type: "string", describe: "Filter by intensity" }) + .option("texture", { type: "string", describe: "Filter by texture" }) + .option("dir", { type: "string", describe: "Directory to search" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }, handle); + }, handle); + + // ── Explore (subcommands) ──────────────────────────────────────────────── + y.command(exploreCmd.command, exploreCmd.desc, (y2) => { + y2.command("sweep", "Sweep a seed range and rank candidates", (y3) => { + y3.option("recipe", { type: "string", describe: "Recipe name" }) + .option("seed-range", { type: "string", describe: "Seed range (start:end)" }) + .option("keep-top", { type: "number", default: 5, describe: "Number of top candidates to keep" }) + .option("rank-by", { type: "string", describe: "Metric to rank by" }) + .option("clusters", { type: "number", default: 3, describe: "Number of clusters" }) + .option("concurrency", { type: "number", default: 4, describe: "Concurrency level" }) + .option("output", { type: "string", describe: "Output directory" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }, handle); + y2.command("mutate", "Mutate a seed to explore nearby sounds", (y3) => { + y3.option("recipe", { type: "string", describe: "Recipe name" }) + .option("seed", { type: "number", describe: "Seed to mutate" }) + .option("jitter", { type: "number", default: 0.1, describe: "Jitter amount (0-1)" }) + .option("count", { type: "number", default: 20, describe: "Number of mutations" }) + .option("rank-by", { type: "string", describe: "Metric to rank by" }) + .option("output", { type: "string", describe: "Output directory" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }, handle); + y2.command("promote", "Promote a candidate to the library", (y3) => { + y3.option("run", { type: "string", describe: "Run ID" }) + .option("latest", { type: "boolean", describe: "Use the latest run" }) + .option("id", { type: "string", describe: "Candidate ID to promote" }) + .option("category", { type: "string", describe: "Override category" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }, handle); + y2.command("show", "Show details of an exploration run", (y3) => { + y3.option("run", { type: "string", describe: "Run ID" }) + .option("latest", { type: "boolean", describe: "Use the latest run" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }, handle); + y2.command("runs", "List all exploration runs", (y3) => { + y3.option("json", { type: "boolean", describe: "Output JSON" }); + }, handle); + }, handle); + + // ── Library (subcommands) ──────────────────────────────────────────────── + y.command(libraryCmd.command, libraryCmd.desc, (y2) => { + y2.command("list", "List library entries", (y3) => { + y3.option("category", { type: "string", describe: "Filter by category" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }, handle); + y2.command("search", "Search library entries", (y3) => { + y3.option("category", { type: "string", describe: "Filter by category" }) + .option("intensity", { type: "string", describe: "Filter by intensity" }) + .option("texture", { type: "string", describe: "Filter by texture" }) + .option("tags", { type: "string", describe: "Filter by tags" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }, handle); + y2.command("similar", "Find similar library entries", (y3) => { + y3.option("id", { type: "string", describe: "Entry ID to compare" }) + .option("limit", { type: "number", default: 10, describe: "Maximum results" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }, handle); + y2.command("export", "Export library entries to WAV files", (y3) => { + y3.option("output", { type: "string", describe: "Output directory" }) + .option("category", { type: "string", describe: "Filter by category" }) + .option("format", { type: "string", default: "wav", describe: "Output format" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }, handle); + y2.command("regenerate", "Regenerate a library entry", (y3) => { + y3.option("id", { type: "string", describe: "Entry ID to regenerate" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }, handle); + }, handle); try { await y.parse(); if (typeof exitCode === "number") { return exitCode; } - return runLegacy(argv); - } catch (err) { - // Preserve legacy UX and machine output for any parse edge cases. - return runLegacy(argv); + // No handler fired — unknown command, fall back to coreMain for proper error. + return coreMain(["node", "cli.ts", ...raw]); + } catch { + return coreMain(["node", "cli.ts", ...raw]); } } diff --git a/src/cli/commands/analyze.ts b/src/cli/commands/analyze.ts new file mode 100644 index 0000000..51bea9e --- /dev/null +++ b/src/cli/commands/analyze.ts @@ -0,0 +1,12 @@ +export const command = "analyze"; +export const desc = "Analyze a sound's acoustic properties"; + +export function builder(yargs: any) { + return yargs + .option("recipe", { type: "string", describe: "Recipe name" }) + .option("seed", { type: "number", describe: "Seed for rendering" }) + .option("input", { type: "string", describe: "Path to input WAV file" }) + .option("output", { type: "string", describe: "Output directory for analysis files" }) + .option("format", { type: "string", describe: "Output format (json or table)" }) + .option("json", { type: "boolean", describe: "Output JSON" }); +} diff --git a/src/cli/commands/classify.ts b/src/cli/commands/classify.ts new file mode 100644 index 0000000..f8c2f6c --- /dev/null +++ b/src/cli/commands/classify.ts @@ -0,0 +1,13 @@ +export const command = "classify"; +export const desc = "Classify a sound by category, intensity, and texture"; + +export function builder(yargs: any) { + return yargs + .option("recipe", { type: "string", describe: "Recipe name" }) + .option("seed", { type: "number", describe: "Seed for rendering" }) + .option("input", { type: "string", describe: "Path to input WAV file" }) + .option("analysis", { type: "string", describe: "Path to analysis directory" }) + .option("output", { type: "string", describe: "Output directory" }) + .option("format", { type: "string", describe: "Output format (json or table)" }) + .option("json", { type: "boolean", describe: "Output JSON" }); +} diff --git a/src/cli/commands/explore.ts b/src/cli/commands/explore.ts new file mode 100644 index 0000000..9130de3 --- /dev/null +++ b/src/cli/commands/explore.ts @@ -0,0 +1,40 @@ +export const command = "explore"; +export const desc = "Explore and discover sounds through sweep, mutate, and promote workflows"; + +export function builder(yargs: any) { + return yargs + .command("sweep", "Sweep a seed range and rank candidates", (y: any) => { + y.option("recipe", { type: "string", describe: "Recipe name" }) + .option("seed-range", { type: "string", describe: "Seed range (start:end)" }) + .option("keep-top", { type: "number", default: 5, describe: "Number of top candidates to keep" }) + .option("rank-by", { type: "string", describe: "Metric to rank by" }) + .option("clusters", { type: "number", default: 3, describe: "Number of clusters" }) + .option("concurrency", { type: "number", default: 4, describe: "Concurrency level" }) + .option("output", { type: "string", describe: "Output directory" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }) + .command("mutate", "Mutate a seed to explore nearby sounds", (y: any) => { + y.option("recipe", { type: "string", describe: "Recipe name" }) + .option("seed", { type: "number", describe: "Seed to mutate" }) + .option("jitter", { type: "number", default: 0.1, describe: "Jitter amount (0-1)" }) + .option("count", { type: "number", default: 20, describe: "Number of mutations" }) + .option("rank-by", { type: "string", describe: "Metric to rank by" }) + .option("output", { type: "string", describe: "Output directory" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }) + .command("promote", "Promote a candidate to the library", (y: any) => { + y.option("run", { type: "string", describe: "Run ID" }) + .option("latest", { type: "boolean", describe: "Use the latest run" }) + .option("id", { type: "string", describe: "Candidate ID to promote" }) + .option("category", { type: "string", describe: "Override category" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }) + .command("show", "Show details of an exploration run", (y: any) => { + y.option("run", { type: "string", describe: "Run ID" }) + .option("latest", { type: "boolean", describe: "Use the latest run" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }) + .command("runs", "List all exploration runs", (y: any) => { + y.option("json", { type: "boolean", describe: "Output JSON" }); + }); +} diff --git a/src/cli/commands/generate.ts b/src/cli/commands/generate.ts index fc35eb8..d454151 100644 --- a/src/cli/commands/generate.ts +++ b/src/cli/commands/generate.ts @@ -10,10 +10,25 @@ export const desc = "Render and export procedural sounds"; export function builder(yargs: any) { return yargs - .option("recipe", { type: "string", demandOption: true }) - .option("seed", { type: "number" }) - .option("output", { type: "string" }) - .option("json", { type: "boolean" }); + .option("recipe", { + type: "string", + describe: "Recipe name (e.g. ui-scifi-confirm)", + }) + .option("seed", { + type: "string", + describe: "Integer seed for deterministic rendering", + }) + .option("seed-range", { + type: "string", + describe: "Seed range for batch generation (e.g. 1:10)", + }) + .option("output", { + type: "string", + describe: "Output WAV path or directory (for --seed-range)", + }) + .option("json", { type: "boolean", describe: "Output JSON" }) + .example("generate --recipe ui-scifi-confirm --seed 42", "Render a specific seed") + .example("generate --recipe ui-scifi-confirm", "Render with a random seed"); } export async function handler(argv: Arguments) { diff --git a/src/cli/commands/library.ts b/src/cli/commands/library.ts new file mode 100644 index 0000000..5c3c6d6 --- /dev/null +++ b/src/cli/commands/library.ts @@ -0,0 +1,32 @@ +export const command = "library"; +export const desc = "Manage the ToneForge sound library"; + +export function builder(yargs: any) { + return yargs + .command("list", "List library entries", (y: any) => { + y.option("category", { type: "string", describe: "Filter by category" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }) + .command("search", "Search library entries", (y: any) => { + y.option("category", { type: "string", describe: "Filter by category" }) + .option("intensity", { type: "string", describe: "Filter by intensity" }) + .option("texture", { type: "string", describe: "Filter by texture" }) + .option("tags", { type: "string", describe: "Filter by tags" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }) + .command("similar", "Find similar library entries", (y: any) => { + y.option("id", { type: "string", describe: "Entry ID to compare" }) + .option("limit", { type: "number", default: 10, describe: "Maximum results" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }) + .command("export", "Export library entries to WAV files", (y: any) => { + y.option("output", { type: "string", describe: "Output directory" }) + .option("category", { type: "string", describe: "Filter by category" }) + .option("format", { type: "string", default: "wav", describe: "Output format" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }) + .command("regenerate", "Regenerate a library entry", (y: any) => { + y.option("id", { type: "string", describe: "Entry ID to regenerate" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }); +} diff --git a/src/cli/commands/list.ts b/src/cli/commands/list.ts index f46f4df..c994851 100644 --- a/src/cli/commands/list.ts +++ b/src/cli/commands/list.ts @@ -7,11 +7,15 @@ export const desc = "List available resources"; export function builder(yargs: any) { return yargs - .positional("resource", { type: "string", default: "recipes" }) - .option("search", { type: "string" }) - .option("category", { type: "string" }) - .option("tags", { type: "string" }) - .option("json", { type: "boolean" }); + .positional("resource", { + type: "string", + describe: "Resource type to list (e.g. recipes)", + default: "recipes", + }) + .option("search", { type: "string", describe: "Search filter" }) + .option("category", { type: "string", describe: "Filter by category" }) + .option("tags", { type: "string", describe: "Filter by tags" }) + .option("json", { type: "boolean", describe: "Output JSON" }); } export async function handler(argv: Arguments) { diff --git a/src/cli/commands/play.ts b/src/cli/commands/play.ts index 7430980..bff067d 100644 --- a/src/cli/commands/play.ts +++ b/src/cli/commands/play.ts @@ -8,7 +8,9 @@ export const command = "play "; export const desc = "Play a WAV file"; export function builder(yargs: any) { - return yargs.positional("file", { type: "string" }).option("json", { type: "boolean" }); + return yargs + .positional("file", { type: "string", describe: "Path to WAV file to play" }) + .option("json", { type: "boolean", describe: "Output JSON" }); } export async function handler(argv: Arguments) { diff --git a/src/cli/commands/sequence.ts b/src/cli/commands/sequence.ts new file mode 100644 index 0000000..2002e1f --- /dev/null +++ b/src/cli/commands/sequence.ts @@ -0,0 +1,22 @@ +export const command = "sequence"; +export const desc = "Work with sound sequences defined by preset files"; + +export function builder(yargs: any) { + return yargs + .command("generate", "Render a sequence to audio", (y: any) => { + y.option("preset", { type: "string", describe: "Path to sequence preset JSON" }) + .option("seed", { type: "number", describe: "Seed for rendering" }) + .option("output", { type: "string", describe: "Output WAV path" }) + .option("duration", { type: "number", describe: "Duration override in seconds" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }) + .command("simulate", "Simulate a sequence and show event schedule", (y: any) => { + y.option("preset", { type: "string", describe: "Path to sequence preset JSON" }) + .option("seed", { type: "number", describe: "Seed for simulation" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }) + .command("inspect", "Inspect a sequence preset structure", (y: any) => { + y.option("preset", { type: "string", describe: "Path to sequence preset JSON" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }); +} diff --git a/src/cli/commands/show.ts b/src/cli/commands/show.ts index 1835a92..8294de0 100644 --- a/src/cli/commands/show.ts +++ b/src/cli/commands/show.ts @@ -7,7 +7,10 @@ export const command = "show "; export const desc = "Show recipe metadata"; export function builder(yargs: any) { - return yargs.positional("name", { type: "string" }).option("json", { type: "boolean" }); + return yargs + .positional("name", { type: "string", describe: "Recipe name to inspect" }) + .option("seed", { type: "string", describe: "Integer seed for parameter defaults" }) + .option("json", { type: "boolean", describe: "Output JSON" }); } export async function handler(argv: Arguments) { diff --git a/src/cli/commands/stack.ts b/src/cli/commands/stack.ts new file mode 100644 index 0000000..75da365 --- /dev/null +++ b/src/cli/commands/stack.ts @@ -0,0 +1,17 @@ +export const command = "stack"; +export const desc = "Work with layered sound stacks defined by preset files"; + +export function builder(yargs: any) { + return yargs + .command("render", "Render a stack preset to audio", (y: any) => { + y.option("preset", { type: "string", describe: "Path to stack preset JSON" }) + .option("seed", { type: "string", describe: "Seed for rendering" }) + .option("output", { type: "string", describe: "Output WAV path" }) + .option("layer", { type: "array", describe: "Inline layer spec overrides" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }) + .command("inspect", "Inspect a stack preset structure", (y: any) => { + y.option("preset", { type: "string", describe: "Path to stack preset JSON" }) + .option("json", { type: "boolean", describe: "Output JSON" }); + }); +} diff --git a/src/cli/commands/tui.ts b/src/cli/commands/tui.ts new file mode 100644 index 0000000..cf9aa5c --- /dev/null +++ b/src/cli/commands/tui.ts @@ -0,0 +1,6 @@ +export const command = "tui"; +export const desc = "Launch the interactive terminal UI"; + +export function builder(yargs: any) { + return yargs; +} diff --git a/src/cli/commands/version.ts b/src/cli/commands/version.ts index a9dbbcf..bd51936 100644 --- a/src/cli/commands/version.ts +++ b/src/cli/commands/version.ts @@ -2,7 +2,7 @@ export const command = "version"; export const desc = "Print the ToneForge version"; export function builder(yargs: any) { - return yargs; + return yargs.option("json", { type: "boolean", describe: "Output JSON" }); } export async function handler() { From c812661274e3798cc6f15231cf38fe48f8d06974 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:50:57 +0000 Subject: [PATCH 3/3] refactor: per-command yargs handlers via dispatchCommand - remove coreMain delegation Co-authored-by: SorraTheOrc <250240+SorraTheOrc@users.noreply.github.com> --- src/cli.core.ts | 37 +++++- src/cli.yargs.ts | 315 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 296 insertions(+), 56 deletions(-) diff --git a/src/cli.core.ts b/src/cli.core.ts index e2eb158..b800fb7 100644 --- a/src/cli.core.ts +++ b/src/cli.core.ts @@ -81,7 +81,7 @@ import { renderSequence } from "./sequence/renderer.js"; import type { SequenceDefinition } from "./sequence/schema.js"; /** Parse command-line arguments into a structured map. */ -function parseArgs(argv: string[]): { +export function parseArgs(argv: string[]): { command: string | undefined; subcommand: string | undefined; flags: Record; @@ -1284,8 +1284,30 @@ function formatClassificationBatchTable( } /** Main CLI entry point. Exported for testability. */ -export async function main(argv: string[] = process.argv): Promise { - const { command, subcommand, flags, layers } = parseArgs(argv); +/** + * Dispatch a command using pre-parsed arguments. + * + * yargs (or any other caller) should perform argument parsing and pass the + * structured result here. `coreMain.main()` is now a thin wrapper around this + * function so that the legacy entry-point remains available. + * + * @param command - Top-level command name (e.g. "generate", "stack"), or + * `undefined` for global flags only (--help, --version). + * @param subcommand - Subcommand name (e.g. "render" for `stack render`), or + * `undefined` when no subcommand is present. + * @param flags - Parsed flag values keyed by their hyphenated CLI name + * (e.g. `"seed-range"`, `"keep-top"`). Boolean flags have + * value `true`; option values are stored as strings so that + * the command handlers can perform their own validation. + * @param layers - Ordered list of `--layer` inline spec strings (stack cmd). + * @returns Promise resolving to a numeric exit code (0 = success). + */ +export async function dispatchCommand( + command: string | undefined, + subcommand: string | undefined, + flags: Record, + layers: string[], +): Promise { const jsonMode = flags["json"] === true; // Enable profiling when --profile flag is set @@ -4217,6 +4239,15 @@ export async function main(argv: string[] = process.argv): Promise { } } +/** + * Legacy entry point — parses raw argv then delegates to dispatchCommand. + * Retained for backwards compatibility (bin scripts, tests that import main). + */ +export async function main(argv: string[] = process.argv): Promise { + const { command, subcommand, flags, layers } = parseArgs(argv); + return dispatchCommand(command, subcommand, flags, layers); +} + // Run when executed directly (via ./bin/dev-cli.js or tf/toneforge commands). // The path check uses /cli.ts and /cli.js (with separator) to avoid // matching bin/dev-cli.js which invokes main() itself. diff --git a/src/cli.yargs.ts b/src/cli.yargs.ts index 9ca1c12..0bf2d35 100644 --- a/src/cli.yargs.ts +++ b/src/cli.yargs.ts @@ -1,6 +1,8 @@ #!/usr/bin/env node import yargs from "yargs"; import { hideBin } from "yargs/helpers"; +import type { Arguments } from "yargs"; +import { dispatchCommand, parseArgs } from "./cli.core.js"; import * as generateCmd from "./cli/commands/generate.js"; import * as listCmd from "./cli/commands/list.js"; @@ -30,46 +32,116 @@ export const FRAMEWORK_COMMANDS = [ "tui", ]; +/** + * Build a flags Record from a yargs argv object. + * + * @param argv - Parsed yargs arguments for the matched command. + * @param extras - Additional flag key/value pairs to merge in (take precedence). + * @returns A `Record` compatible with `dispatchCommand`. + * + * Common boolean flags (`--json`, `--help`) are always captured automatically. + * Numeric yargs values must be stringified before passing as `extras` because + * `dispatchCommand` uses `parseInt` / `parseFloat` internally. + */ +function buildFlags( + argv: Arguments, + extras: Record = {}, +): Record { + const f: Record = {}; + if (argv.json === true) f.json = true; + if (argv.help === true) f.help = true; + Object.assign(f, extras); + return f; +} + export async function yargsMain(argv: string[] = process.argv): Promise { const raw = hideBin(argv); - const { main: coreMain } = await import("./cli.core.js"); - - // When no arguments or only flags (help/version), delegate directly to coreMain. + // When no positional command is present (e.g. `--help`, `--version`, or bare + // invocation), dispatch with the global flags only — yargs routing is not needed. const firstNonFlag = raw.find((a) => !a.startsWith("-")); if (!firstNonFlag) { - return coreMain(["node", "cli.ts", ...raw]); + const globalFlags: Record = {}; + if (raw.includes("--help") || raw.includes("-h")) globalFlags.help = true; + if (raw.includes("--version") || raw.includes("-V")) globalFlags.version = true; + if (raw.includes("--json")) globalFlags.json = true; + return dispatchCommand(undefined, undefined, globalFlags, []); } const y = yargs(raw).scriptName("toneforge"); let exitCode: number | undefined; + // Prevent yargs from calling process.exit() or printing its own error messages. + // dispatchCommand is the authoritative error handler for all commands. y.exitProcess(false); y.showHelpOnFail(false); y.version(false); y.help(false); - - // Suppress yargs validation errors — coreMain is the authoritative error - // handler and outputs plain-text or JSON depending on the --json flag. y.fail((_msg, _err) => { /* noop */ }); - // Single shared handler: all command execution is delegated to coreMain. - // yargs provides routing structure and help-text; coreMain is the - // authoritative implementation. - const handle = async () => { - exitCode = await coreMain(["node", "cli.ts", ...raw]); - }; - - // ── Simple commands ────────────────────────────────────────────────────── - y.command(generateCmd.command, generateCmd.desc, generateCmd.builder, handle); - y.command(listCmd.command, listCmd.desc, listCmd.builder, handle); - y.command(showCmd.command, showCmd.desc, showCmd.builder, handle); - y.command(playCmd.command, playCmd.desc, playCmd.builder, handle); - y.command(versionCmd.command, versionCmd.desc, versionCmd.builder, handle); - y.command(analyzeCmd.command, analyzeCmd.desc, analyzeCmd.builder, handle); - y.command(tuiCmd.command, tuiCmd.desc, tuiCmd.builder, handle); - - // ── Stack (subcommands) ────────────────────────────────────────────────── + // ── generate ────────────────────────────────────────────────────────────── + y.command(generateCmd.command, generateCmd.desc, generateCmd.builder, async (argv) => { + exitCode = await dispatchCommand("generate", undefined, buildFlags(argv, { + ...(argv.recipe !== undefined ? { recipe: String(argv.recipe) } : {}), + ...(argv.seed !== undefined ? { seed: String(argv.seed) } : {}), + ...(argv["seed-range"] !== undefined ? { "seed-range": String(argv["seed-range"]) } : {}), + ...(argv.output !== undefined ? { output: String(argv.output) } : {}), + }), []); + }); + + // ── list ────────────────────────────────────────────────────────────────── + y.command(listCmd.command, listCmd.desc, listCmd.builder, async (argv) => { + exitCode = await dispatchCommand( + "list", + argv.resource as string | undefined, + buildFlags(argv, { + ...(argv.search !== undefined ? { search: String(argv.search) } : {}), + ...(argv.category !== undefined ? { category: String(argv.category) } : {}), + ...(argv.tags !== undefined ? { tags: String(argv.tags) } : {}), + }), + [], + ); + }); + + // ── show ────────────────────────────────────────────────────────────────── + y.command(showCmd.command, showCmd.desc, showCmd.builder, async (argv) => { + exitCode = await dispatchCommand( + "show", + argv.name as string | undefined, + buildFlags(argv, { + ...(argv.seed !== undefined ? { seed: String(argv.seed) } : {}), + }), + [], + ); + }); + + // ── play ────────────────────────────────────────────────────────────────── + y.command(playCmd.command, playCmd.desc, playCmd.builder, async (argv) => { + exitCode = await dispatchCommand("play", argv.file as string | undefined, buildFlags(argv), []); + }); + + // ── version ─────────────────────────────────────────────────────────────── + y.command(versionCmd.command, versionCmd.desc, versionCmd.builder, async (argv) => { + exitCode = await dispatchCommand("version", undefined, buildFlags(argv), []); + }); + + // ── analyze ─────────────────────────────────────────────────────────────── + y.command(analyzeCmd.command, analyzeCmd.desc, analyzeCmd.builder, async (argv) => { + exitCode = await dispatchCommand("analyze", undefined, buildFlags(argv, { + ...(argv.input !== undefined ? { input: String(argv.input) } : {}), + ...(argv.recipe !== undefined ? { recipe: String(argv.recipe) } : {}), + ...(argv.seed !== undefined ? { seed: String(argv.seed) } : {}), + ...(argv.format !== undefined ? { format: String(argv.format) } : {}), + ...(argv.output !== undefined ? { output: String(argv.output) } : {}), + }), []); + }); + + // ── tui ─────────────────────────────────────────────────────────────────── + y.command(tuiCmd.command, tuiCmd.desc, tuiCmd.builder, async (argv) => { + exitCode = await dispatchCommand("tui", undefined, buildFlags(argv), []); + }); + + // ── stack ───────────────────────────────────────────────────────────────── y.command(stackCmd.command, stackCmd.desc, (y2) => { y2.command("render", "Render a stack preset to audio", (y3) => { y3.option("preset", { type: "string", describe: "Path to stack preset JSON" }) @@ -77,14 +149,30 @@ export async function yargsMain(argv: string[] = process.argv): Promise .option("output", { type: "string", describe: "Output WAV path" }) .option("layer", { type: "array", describe: "Inline layer spec overrides" }) .option("json", { type: "boolean", describe: "Output JSON" }); - }, handle); + }, async (argv) => { + exitCode = await dispatchCommand("stack", "render", buildFlags(argv, { + ...(argv.preset !== undefined ? { preset: String(argv.preset) } : {}), + ...(argv.seed !== undefined ? { seed: String(argv.seed) } : {}), + ...(argv.output !== undefined ? { output: String(argv.output) } : {}), + }), (argv.layer as string[] | undefined) ?? []); + }); y2.command("inspect", "Inspect a stack preset structure", (y3) => { y3.option("preset", { type: "string", describe: "Path to stack preset JSON" }) .option("json", { type: "boolean", describe: "Output JSON" }); - }, handle); - }, handle); + }, async (argv) => { + exitCode = await dispatchCommand("stack", "inspect", buildFlags(argv, { + ...(argv.preset !== undefined ? { preset: String(argv.preset) } : {}), + }), []); + }); + }, async (_argv) => { + // Re-parse raw argv so dispatchCommand receives the unknown subcommand name + // for proper error output. yargs only calls this handler when no subcommand + // matched, so `argv._` isn't reliable; `raw` (captured in outer scope) is. + const parsed = parseArgs(["node", "cli.ts", ...raw]); + exitCode = await dispatchCommand(parsed.command, parsed.subcommand, parsed.flags, parsed.layers); + }); - // ── Sequence (subcommands) ─────────────────────────────────────────────── + // ── sequence ────────────────────────────────────────────────────────────── y.command(sequenceCmd.command, sequenceCmd.desc, (y2) => { y2.command("generate", "Render a sequence to audio", (y3) => { y3.option("preset", { type: "string", describe: "Path to sequence preset JSON" }) @@ -92,19 +180,45 @@ export async function yargsMain(argv: string[] = process.argv): Promise .option("output", { type: "string", describe: "Output WAV path" }) .option("duration", { type: "number", describe: "Duration override in seconds" }) .option("json", { type: "boolean", describe: "Output JSON" }); - }, handle); + }, async (argv) => { + exitCode = await dispatchCommand("sequence", "generate", buildFlags(argv, { + ...(argv.preset !== undefined ? { preset: String(argv.preset) } : {}), + ...(argv.seed !== undefined ? { seed: String(argv.seed) } : {}), + ...(argv.output !== undefined ? { output: String(argv.output) } : {}), + ...(argv.duration !== undefined ? { duration: String(argv.duration) } : {}), + }), []); + }); y2.command("simulate", "Simulate a sequence and show event schedule", (y3) => { y3.option("preset", { type: "string", describe: "Path to sequence preset JSON" }) .option("seed", { type: "number", describe: "Seed for simulation" }) + .option("duration", { type: "number", describe: "Maximum simulated duration in seconds" }) .option("json", { type: "boolean", describe: "Output JSON" }); - }, handle); + }, async (argv) => { + exitCode = await dispatchCommand("sequence", "simulate", buildFlags(argv, { + ...(argv.preset !== undefined ? { preset: String(argv.preset) } : {}), + ...(argv.seed !== undefined ? { seed: String(argv.seed) } : {}), + ...(argv.duration !== undefined ? { duration: String(argv.duration) } : {}), + }), []); + }); y2.command("inspect", "Inspect a sequence preset structure", (y3) => { y3.option("preset", { type: "string", describe: "Path to sequence preset JSON" }) + .option("validate", { type: "boolean", describe: "Validate the preset and report errors" }) .option("json", { type: "boolean", describe: "Output JSON" }); - }, handle); - }, handle); + }, async (argv) => { + exitCode = await dispatchCommand("sequence", "inspect", buildFlags(argv, { + ...(argv.preset !== undefined ? { preset: String(argv.preset) } : {}), + ...(argv.validate === true ? { validate: true } : {}), + }), []); + }); + }, async (_argv) => { + // Re-parse raw argv so dispatchCommand receives the unknown subcommand name + // for proper error output. yargs only calls this handler when no subcommand + // matched, so `argv._` isn't reliable; `raw` (captured in outer scope) is. + const parsed = parseArgs(["node", "cli.ts", ...raw]); + exitCode = await dispatchCommand(parsed.command, parsed.subcommand, parsed.flags, parsed.layers); + }); - // ── Classify (optional subcommand) ────────────────────────────────────── + // ── classify ────────────────────────────────────────────────────────────── y.command(classifyCmd.command, classifyCmd.desc, (y2) => { y2.command("search", "Search for classified sounds in a directory", (y3) => { y3.option("category", { type: "string", describe: "Filter by category" }) @@ -112,10 +226,29 @@ export async function yargsMain(argv: string[] = process.argv): Promise .option("texture", { type: "string", describe: "Filter by texture" }) .option("dir", { type: "string", describe: "Directory to search" }) .option("json", { type: "boolean", describe: "Output JSON" }); - }, handle); - }, handle); + }, async (argv) => { + exitCode = await dispatchCommand("classify", "search", buildFlags(argv, { + ...(argv.category !== undefined ? { category: String(argv.category) } : {}), + ...(argv.intensity !== undefined ? { intensity: String(argv.intensity) } : {}), + ...(argv.texture !== undefined ? { texture: String(argv.texture) } : {}), + ...(argv.dir !== undefined ? { dir: String(argv.dir) } : {}), + }), []); + }); + }, async (argv) => { + // Pass classify without subcommand (for recipe/input/analysis modes); only + // use parseArgs when an unrecognized positional is present (argv._[0] != "classify"). + const sub = (argv._ as string[])[1] as string | undefined; + exitCode = await dispatchCommand("classify", sub, buildFlags(argv, { + ...(argv.recipe !== undefined ? { recipe: String(argv.recipe) } : {}), + ...(argv.input !== undefined ? { input: String(argv.input) } : {}), + ...(argv.analysis !== undefined ? { analysis: String(argv.analysis) } : {}), + ...(argv.seed !== undefined ? { seed: String(argv.seed) } : {}), + ...(argv.format !== undefined ? { format: String(argv.format) } : {}), + ...(argv.output !== undefined ? { output: String(argv.output) } : {}), + }), []); + }); - // ── Explore (subcommands) ──────────────────────────────────────────────── + // ── explore ─────────────────────────────────────────────────────────────── y.command(exploreCmd.command, exploreCmd.desc, (y2) => { y2.command("sweep", "Sweep a seed range and rank candidates", (y3) => { y3.option("recipe", { type: "string", describe: "Recipe name" }) @@ -126,7 +259,17 @@ export async function yargsMain(argv: string[] = process.argv): Promise .option("concurrency", { type: "number", default: 4, describe: "Concurrency level" }) .option("output", { type: "string", describe: "Output directory" }) .option("json", { type: "boolean", describe: "Output JSON" }); - }, handle); + }, async (argv) => { + exitCode = await dispatchCommand("explore", "sweep", buildFlags(argv, { + ...(argv.recipe !== undefined ? { recipe: String(argv.recipe) } : {}), + ...(argv["seed-range"] !== undefined ? { "seed-range": String(argv["seed-range"]) } : {}), + ...(argv["keep-top"] !== undefined ? { "keep-top": String(argv["keep-top"]) } : {}), + ...(argv["rank-by"] !== undefined ? { "rank-by": String(argv["rank-by"]) } : {}), + ...(argv.clusters !== undefined ? { clusters: String(argv.clusters) } : {}), + ...(argv.concurrency !== undefined ? { concurrency: String(argv.concurrency) } : {}), + ...(argv.output !== undefined ? { output: String(argv.output) } : {}), + }), []); + }); y2.command("mutate", "Mutate a seed to explore nearby sounds", (y3) => { y3.option("recipe", { type: "string", describe: "Recipe name" }) .option("seed", { type: "number", describe: "Seed to mutate" }) @@ -135,63 +278,129 @@ export async function yargsMain(argv: string[] = process.argv): Promise .option("rank-by", { type: "string", describe: "Metric to rank by" }) .option("output", { type: "string", describe: "Output directory" }) .option("json", { type: "boolean", describe: "Output JSON" }); - }, handle); + }, async (argv) => { + exitCode = await dispatchCommand("explore", "mutate", buildFlags(argv, { + ...(argv.recipe !== undefined ? { recipe: String(argv.recipe) } : {}), + ...(argv.seed !== undefined ? { seed: String(argv.seed) } : {}), + ...(argv.jitter !== undefined ? { jitter: String(argv.jitter) } : {}), + ...(argv.count !== undefined ? { count: String(argv.count) } : {}), + ...(argv["rank-by"] !== undefined ? { "rank-by": String(argv["rank-by"]) } : {}), + ...(argv.output !== undefined ? { output: String(argv.output) } : {}), + }), []); + }); y2.command("promote", "Promote a candidate to the library", (y3) => { y3.option("run", { type: "string", describe: "Run ID" }) .option("latest", { type: "boolean", describe: "Use the latest run" }) .option("id", { type: "string", describe: "Candidate ID to promote" }) .option("category", { type: "string", describe: "Override category" }) .option("json", { type: "boolean", describe: "Output JSON" }); - }, handle); + }, async (argv) => { + exitCode = await dispatchCommand("explore", "promote", buildFlags(argv, { + ...(argv.run !== undefined ? { run: String(argv.run) } : {}), + ...(argv.latest === true ? { latest: true } : {}), + ...(argv.id !== undefined ? { id: String(argv.id) } : {}), + ...(argv.category !== undefined ? { category: String(argv.category) } : {}), + }), []); + }); y2.command("show", "Show details of an exploration run", (y3) => { y3.option("run", { type: "string", describe: "Run ID" }) .option("latest", { type: "boolean", describe: "Use the latest run" }) .option("json", { type: "boolean", describe: "Output JSON" }); - }, handle); + }, async (argv) => { + exitCode = await dispatchCommand("explore", "show", buildFlags(argv, { + ...(argv.run !== undefined ? { run: String(argv.run) } : {}), + ...(argv.latest === true ? { latest: true } : {}), + }), []); + }); y2.command("runs", "List all exploration runs", (y3) => { y3.option("json", { type: "boolean", describe: "Output JSON" }); - }, handle); - }, handle); + }, async (argv) => { + exitCode = await dispatchCommand("explore", "runs", buildFlags(argv), []); + }); + }, async (_argv) => { + // Re-parse raw argv so dispatchCommand receives the unknown subcommand name + // for proper error output. yargs only calls this handler when no subcommand + // matched, so `argv._` isn't reliable; `raw` (captured in outer scope) is. + const parsed = parseArgs(["node", "cli.ts", ...raw]); + exitCode = await dispatchCommand(parsed.command, parsed.subcommand, parsed.flags, parsed.layers); + }); - // ── Library (subcommands) ──────────────────────────────────────────────── + // ── library ─────────────────────────────────────────────────────────────── y.command(libraryCmd.command, libraryCmd.desc, (y2) => { y2.command("list", "List library entries", (y3) => { y3.option("category", { type: "string", describe: "Filter by category" }) .option("json", { type: "boolean", describe: "Output JSON" }); - }, handle); + }, async (argv) => { + exitCode = await dispatchCommand("library", "list", buildFlags(argv, { + ...(argv.category !== undefined ? { category: String(argv.category) } : {}), + }), []); + }); y2.command("search", "Search library entries", (y3) => { y3.option("category", { type: "string", describe: "Filter by category" }) .option("intensity", { type: "string", describe: "Filter by intensity" }) .option("texture", { type: "string", describe: "Filter by texture" }) .option("tags", { type: "string", describe: "Filter by tags" }) .option("json", { type: "boolean", describe: "Output JSON" }); - }, handle); + }, async (argv) => { + exitCode = await dispatchCommand("library", "search", buildFlags(argv, { + ...(argv.category !== undefined ? { category: String(argv.category) } : {}), + ...(argv.intensity !== undefined ? { intensity: String(argv.intensity) } : {}), + ...(argv.texture !== undefined ? { texture: String(argv.texture) } : {}), + ...(argv.tags !== undefined ? { tags: String(argv.tags) } : {}), + }), []); + }); y2.command("similar", "Find similar library entries", (y3) => { y3.option("id", { type: "string", describe: "Entry ID to compare" }) .option("limit", { type: "number", default: 10, describe: "Maximum results" }) .option("json", { type: "boolean", describe: "Output JSON" }); - }, handle); + }, async (argv) => { + exitCode = await dispatchCommand("library", "similar", buildFlags(argv, { + ...(argv.id !== undefined ? { id: String(argv.id) } : {}), + ...(argv.limit !== undefined ? { limit: String(argv.limit) } : {}), + }), []); + }); y2.command("export", "Export library entries to WAV files", (y3) => { y3.option("output", { type: "string", describe: "Output directory" }) .option("category", { type: "string", describe: "Filter by category" }) - .option("format", { type: "string", default: "wav", describe: "Output format" }) + .option("format", { type: "string", describe: "Output format" }) .option("json", { type: "boolean", describe: "Output JSON" }); - }, handle); + }, async (argv) => { + exitCode = await dispatchCommand("library", "export", buildFlags(argv, { + ...(argv.output !== undefined ? { output: String(argv.output) } : {}), + ...(argv.category !== undefined ? { category: String(argv.category) } : {}), + ...(argv.format !== undefined ? { format: String(argv.format) } : {}), + }), []); + }); y2.command("regenerate", "Regenerate a library entry", (y3) => { y3.option("id", { type: "string", describe: "Entry ID to regenerate" }) .option("json", { type: "boolean", describe: "Output JSON" }); - }, handle); - }, handle); + }, async (argv) => { + exitCode = await dispatchCommand("library", "regenerate", buildFlags(argv, { + ...(argv.id !== undefined ? { id: String(argv.id) } : {}), + }), []); + }); + }, async (_argv) => { + // Re-parse raw argv so dispatchCommand receives the unknown subcommand name + // for proper error output. yargs only calls this handler when no subcommand + // matched, so `argv._` isn't reliable; `raw` (captured in outer scope) is. + const parsed = parseArgs(["node", "cli.ts", ...raw]); + exitCode = await dispatchCommand(parsed.command, parsed.subcommand, parsed.flags, parsed.layers); + }); try { await y.parse(); if (typeof exitCode === "number") { return exitCode; } - // No handler fired — unknown command, fall back to coreMain for proper error. - return coreMain(["node", "cli.ts", ...raw]); + // No handler matched (e.g. unknown command) — re-parse and dispatch so + // dispatchCommand can output the proper error message. + const parsed = parseArgs(["node", "cli.ts", ...raw]); + return dispatchCommand(parsed.command, parsed.subcommand, parsed.flags, parsed.layers); } catch { - return coreMain(["node", "cli.ts", ...raw]); + // Edge-case yargs parse error — fall through to dispatchCommand for + // consistent error output. + const parsed = parseArgs(["node", "cli.ts", ...raw]); + return dispatchCommand(parsed.command, parsed.subcommand, parsed.flags, parsed.layers); } }