From c046c3bb1b0bde28b3a427e5b6808f7a90a18124 Mon Sep 17 00:00:00 2001 From: Zinadin Zidan Date: Thu, 5 Nov 2026 11:49:12 +0600 Subject: [PATCH] feat(cli): commands --json lists every command's details (#744) soundcraft-cli commands printed only a text table, so a program that wanted the parameters had to call describe once per command. With --json it prints a JSON array of each command's details, the same object describe prints, as PhotoCraft and FilmCraft do; --compact puts it on one line. The filter applies, and no match prints []. As in describe, enabled and disabled_reason are worked out against an empty session; docs/cli.md says so, and the README mentions the flag. Co-Authored-By: Claude Opus 5.5 --- README.md | 3 ++- apps/soundcraft-cli/src/args.rs | 6 ++++++ apps/soundcraft-cli/src/catalog.rs | 14 +++++++++----- apps/soundcraft-cli/tests/it/cli.rs | 28 ++++++++++++++++++++++++++++ apps/soundcraft-cli/tests/it/run.rs | 2 +- docs/cli.md | 17 ++++++++++------- 6 files changed, 56 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index eff0ad64..7bb3cd41 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,8 @@ soundcraft-cli mcp --automation-read-root DIR --automation-write-root DIR See [`docs/cli.md`](docs/cli.md) for the command-line tool (subcommands, exit codes, environment variables, man pages and completions), [`docs/control-protocol.md`](docs/control-protocol.md) and -[`docs/mcp.md`](docs/mcp.md). `soundcraft-cli commands` lists every command. +[`docs/mcp.md`](docs/mcp.md). `soundcraft-cli commands` lists every command (`--json` for each +one's parameters and details, as JSON). Open **SoundCraft › Session Audio Health** to check loaded audio availability, sample-rate mismatches and clip source bounds across all playlists, including alternate takes. The report diff --git a/apps/soundcraft-cli/src/args.rs b/apps/soundcraft-cli/src/args.rs index 6b583992..d04fdeff 100644 --- a/apps/soundcraft-cli/src/args.rs +++ b/apps/soundcraft-cli/src/args.rs @@ -168,6 +168,12 @@ pub(crate) struct CommandsArgs { /// Only the commands whose id, or label in any case, contains this text #[arg(value_parser = NonEmptyStringValueParser::new())] pub(crate) filter: Option, + /// Print a JSON array of each command's details, as describe prints them + #[arg(long)] + pub(crate) json: bool, + /// One line of JSON instead of pretty-printed + #[arg(long, requires = "json")] + pub(crate) compact: bool, } /// Print one engine command's details as JSON diff --git a/apps/soundcraft-cli/src/catalog.rs b/apps/soundcraft-cli/src/catalog.rs index c701d66c..8d15fe8d 100644 --- a/apps/soundcraft-cli/src/catalog.rs +++ b/apps/soundcraft-cli/src/catalog.rs @@ -7,11 +7,15 @@ use std::io::Write; pub(crate) fn commands(args: &CommandsArgs, out: &mut dyn Write) { let filter = args.filter.clone().unwrap_or_default(); let e = Engine::default(); - for c in soundcraft_engine::command_specs() { - if !filter.is_empty() && !c.id.contains(&filter) && !c.label.to_ascii_lowercase().contains(&filter.to_ascii_lowercase()) { - continue; - } - let i = c.info(&e); + let infos = soundcraft_engine::command_specs() + .iter() + .filter(|c| filter.is_empty() || c.id.contains(&filter) || c.label.to_ascii_lowercase().contains(&filter.to_ascii_lowercase())) + .map(|c| c.info(&e)); + if args.json { + print_json(out, &infos.collect::>(), args.compact); + return; + } + for i in infos { outln!(out, "{:<36} {:<34} {}", i.id, i.label, i.menu.join(" > ")); } } diff --git a/apps/soundcraft-cli/tests/it/cli.rs b/apps/soundcraft-cli/tests/it/cli.rs index 2a95cb38..de8e454a 100644 --- a/apps/soundcraft-cli/tests/it/cli.rs +++ b/apps/soundcraft-cli/tests/it/cli.rs @@ -253,3 +253,31 @@ fn script_bounce_without_range_covers_the_session() { assert!((secs - 19.2).abs() < 0.01, "bounced {secs} s: {stdout}"); let _ = std::fs::remove_dir_all(base); } + +/// `commands --json` lists every command with the details `describe` prints for one (#744). +#[test] +fn commands_json_lists_what_describe_prints() { + let json = |o: &Output| -> Value { + assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr)); + serde_json::from_slice(&o.stdout).unwrap() + }; + let all = json(&run(&["commands", "--json"])); + let all = all.as_array().unwrap(); + let table = run(&["commands"]); + assert_eq!(all.len(), String::from_utf8_lossy(&table.stdout).lines().count()); + let bounce = json(&run(&["describe", "file.bounce_mix"])); + assert!(all.contains(&bounce), "{bounce}"); + assert!(all.iter().all(|c| c["id"].is_string() && c["params"].is_string() && c["menu"].is_array())); + // The filter applies, and --compact prints one line. + let compact = run(&["commands", "bounce", "--json", "--compact"]); + assert_eq!(String::from_utf8_lossy(&compact.stdout).lines().count(), 1); + let some = json(&compact); + let some = some.as_array().unwrap(); + assert!(some.contains(&bounce) && some.len() < all.len(), "{some:?}"); + // No match is an empty list, still JSON. + assert_eq!(json(&run(&["commands", "zzzzqq", "--json"])), Value::Array(Vec::new())); + // --compact shapes JSON output only. + let bad = run(&["commands", "--compact"]); + assert_eq!(bad.status.code(), Some(2)); + assert!(String::from_utf8_lossy(&bad.stderr).contains("--json"), "{}", String::from_utf8_lossy(&bad.stderr)); +} diff --git a/apps/soundcraft-cli/tests/it/run.rs b/apps/soundcraft-cli/tests/it/run.rs index 60ffff70..4f9baf23 100644 --- a/apps/soundcraft-cli/tests/it/run.rs +++ b/apps/soundcraft-cli/tests/it/run.rs @@ -151,7 +151,7 @@ fn every_subcommand_has_help_listing_its_arguments() { ), ("run", "run [OPTIONS]", &["--cmd "]), ("script", "script [OPTIONS] ", &["", "--keep-going"]), - ("commands", "commands [FILTER]", &["[FILTER]"]), + ("commands", "commands [OPTIONS] [FILTER]", &["[FILTER]", "--json", "--compact"]), ("describe", "describe [OPTIONS] ", &["", "--compact"]), ( "app", diff --git a/docs/cli.md b/docs/cli.md index 28d36fcd..360427f8 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -14,7 +14,7 @@ flags, values and defaults. This page does not repeat them. | `convert` | Converts an audio file: resample, change the bit depth, normalize | | `run` | Runs engine commands on a new, demo or saved session; can bounce, save and inspect | | `script` | Runs a file of engine commands, one `ID JSON` per line | -| `commands` | Lists the engine commands | +| `commands` | Lists the engine commands; with `--json`, each one's details as `describe` prints them | | `describe` | Prints one engine command's details as JSON | | `app` | Calls a running app's control channel (see [the control protocol](control-protocol.md)) | | `mcp` | Serves MCP on stdin and stdout (see [the MCP guide](mcp.md)) | @@ -29,16 +29,19 @@ soundcraft-cli convert in.wav out.flac --bit-depth 16 --rate 44100 soundcraft-cli run --demo --cmd 'mix.volume={"track":"Kick","db":-6}' --bounce mix.wav soundcraft-cli script steps.txt --demo --keep-going --save out.scraft soundcraft-cli commands bounce +soundcraft-cli commands --json --compact soundcraft-cli describe file.bounce_mix ``` ## Output and exit codes Results go to standard output, as JSON where a program is meant to read them. `info` (on an audio -file), `describe`, `app` and the `--inspect` output of `run` and `script` pretty-print it; `--compact` -prints the same JSON on one line. The result lines of `run` and `script` (`ID: JSON`) and the -`--bounce` and `--save` results are always one line each. `commands`, `plugins`, `parity` and the -`.scraft` summary of `info` are text. Errors go to standard error as `error: `, and so do the warnings and errors SoundCraft and the plugin hosts log (`warning: `, `error: `), such as a plugin that fails to load. `mcp` writes only the protocol +file), `describe`, `commands --json`, `app` and the `--inspect` output of `run` and `script` +pretty-print it; `--compact` prints the same JSON on one line. The result lines of `run` and +`script` (`ID: JSON`) and the `--bounce` and `--save` results are always one line each. `commands` +(without `--json`), `plugins`, `parity` and the `.scraft` summary of `info` are text. The +`enabled` and `disabled_reason` fields of `describe` and `commands --json` are worked out against +an empty session, not a session you have open. Errors go to standard error as `error: `, and so do the warnings and errors SoundCraft and the plugin hosts log (`warning: `, `error: `), such as a plugin that fails to load. `mcp` writes only the protocol to standard output. | Exit code | Meaning | @@ -138,8 +141,8 @@ output replacing an input without `--in-place`, and the ast-grep rules in `lint/ Rules not followed yet: -- **JSON flag (rule 1.3).** `commands`, `plugins`, `parity` and the `info` summary of a `.scraft` - session print text and have no `--json` flag. Whether such listings need one is an open question +- **JSON flag (rule 1.3).** `plugins`, `parity` and the `info` summary of a `.scraft` session + print text and have no `--json` flag (`commands` has one). Whether such listings need one is an open question in the RFC itself. - **`string-error` rule (section 3).** Its ast-grep rule is on for the CLI code and the shared flags, not yet for the rest of the workspace.