Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions src/cli.core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | boolean>;
Expand Down Expand Up @@ -1284,8 +1284,30 @@ function formatClassificationBatchTable(
}

/** Main CLI entry point. Exported for testability. */
export async function main(argv: string[] = process.argv): Promise<number> {
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<string, string | boolean>,
layers: string[],
): Promise<number> {
const jsonMode = flags["json"] === true;

// Enable profiling when --profile flag is set
Expand Down Expand Up @@ -4217,6 +4239,15 @@ export async function main(argv: string[] = process.argv): Promise<number> {
}
}

/**
* 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<number> {
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.
Expand Down
457 changes: 357 additions & 100 deletions src/cli.yargs.ts

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions src/cli/commands/analyze.ts
Original file line number Diff line number Diff line change
@@ -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" });
}
13 changes: 13 additions & 0 deletions src/cli/commands/classify.ts
Original file line number Diff line number Diff line change
@@ -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" });
}
40 changes: 40 additions & 0 deletions src/cli/commands/explore.ts
Original file line number Diff line number Diff line change
@@ -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" });
});
}
23 changes: 19 additions & 4 deletions src/cli/commands/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
32 changes: 32 additions & 0 deletions src/cli/commands/library.ts
Original file line number Diff line number Diff line change
@@ -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" });
});
}
14 changes: 9 additions & 5 deletions src/cli/commands/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 3 additions & 1 deletion src/cli/commands/play.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ export const command = "play <file>";
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) {
Expand Down
22 changes: 22 additions & 0 deletions src/cli/commands/sequence.ts
Original file line number Diff line number Diff line change
@@ -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" });
});
}
5 changes: 4 additions & 1 deletion src/cli/commands/show.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ export const command = "show <name>";
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) {
Expand Down
17 changes: 17 additions & 0 deletions src/cli/commands/stack.ts
Original file line number Diff line number Diff line change
@@ -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" });
});
}
6 changes: 6 additions & 0 deletions src/cli/commands/tui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const command = "tui";
export const desc = "Launch the interactive terminal UI";

export function builder(yargs: any) {
return yargs;
}
2 changes: 1 addition & 1 deletion src/cli/commands/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down