Merge pull request #1372 from ZIDAN44/fix/744-commands-json

feat(cli): commands --json lists every command's details (#744)
This commit is contained in:
FredApps authored and GitHub committed 2026-11-05 14:57:21 +01:00
commit 6c8a9dd300
6 files changed
+56 -14

No files matched your search

+2 -1
View File
@@ -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
+6
View File
@@ -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<String>,
/// 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
+9 -5
View File
@@ -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::<Vec<_>>(), args.compact);
return;
}
for i in infos {
outln!(out, "{:<36} {:<34} {}", i.id, i.label, i.menu.join(" > "));
}
}
+28
View File
@@ -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));
}
+1 -1
View File
@@ -151,7 +151,7 @@ fn every_subcommand_has_help_listing_its_arguments() {
),
("run", "run [OPTIONS]", &["--cmd <ID[=JSON]>"]),
("script", "script [OPTIONS] <FILE>", &["<FILE>", "--keep-going"]),
("commands", "commands [FILTER]", &["[FILTER]"]),
("commands", "commands [OPTIONS] [FILTER]", &["[FILTER]", "--json", "--compact"]),
("describe", "describe [OPTIONS] <ID>", &["<ID>", "--compact"]),
(
"app",
+10 -7
View File
@@ -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: <message>`, and so do the warnings and errors SoundCraft and the plugin hosts log (`warning: <message>`, `error: <message>`), 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: <message>`, and so do the warnings and errors SoundCraft and the plugin hosts log (`warning: <message>`, `error: <message>`), 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.