From 734c8e18bc73207e646b53dfe3576674fe8fdd22 Mon Sep 17 00:00:00 2001 From: Vikram Bhaskaran Date: Sat, 10 Oct 2026 16:23:44 +1100 Subject: [PATCH] Treat dotted command ids as engine.execute in the CLI app shorthand. app used to leave every ui., engine., session., and app. method untouched, so engine.history, ui.theme, and session.new were sent as unknown control methods. Only the real control-channel methods stay methods. Every other dotted id is engine.execute. --- apps/soundcraft-cli/src/main.rs | 63 +++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/apps/soundcraft-cli/src/main.rs b/apps/soundcraft-cli/src/main.rs index 2ba9112..7e61868 100644 --- a/apps/soundcraft-cli/src/main.rs +++ b/apps/soundcraft-cli/src/main.rs @@ -294,6 +294,39 @@ fn describe(args: &[String]) -> ExitCode { } } +/// Control-channel methods. Every other dotted id is an engine command. +fn is_control_method(method: &str) -> bool { + matches!( + method, + "engine.execute" + | "command" + | "engine.commands" + | "engine.parity" + | "session.inspect" + | "document.inspect" + | "ui.inspect" + | "ui.menu.list" + | "ui.menu.invoke" + | "ui.set" + | "ui.move" + | "ui.click" + | "ui.drag" + | "ui.key" + | "ui.text" + | "ui.screenshot" + | "app.quit" + ) +} + +/// A command id is shorthand for `engine.execute`. Documented control methods stay methods. +fn app_request(method: &str, params: Value) -> (String, Value) { + if is_control_method(method) || !method.contains('.') { + (method.to_string(), params) + } else { + ("engine.execute".to_string(), json!({"command": method, "params": params})) + } +} + fn app(args: &[String]) -> ExitCode { let port = arg_value(args, "--port").or_else(|| std::env::var("SOUNDCRAFT_CONTROL_PORT").ok()).unwrap_or_else(|| "7979".into()); let positional: Vec<&String> = args @@ -311,17 +344,8 @@ fn app(args: &[String]) -> ExitCode { None => json!({}), }; let mut r = Remote::new(&port); - // A bare command id is shorthand for engine.execute. - let (m, p) = if method.contains('.') - && !method.starts_with("ui.") - && !method.starts_with("engine.") - && !method.starts_with("session.") - && !method.starts_with("app.") - { - ("engine.execute".to_string(), json!({"command": method, "params": params})) - } else { - (method.to_string(), params) - }; + // A bare command id is shorthand for engine.execute. Control-channel methods stay methods. + let (m, p) = app_request(method, params); match r.call(&m, p) { Ok(v) => { outln!("{}", serde_json::to_string_pretty(&v).unwrap_or_default()); @@ -380,6 +404,23 @@ fn parity(args: &[String]) -> ExitCode { mod tests { use super::*; + #[test] + fn command_ids_are_execute_shorthand_and_control_methods_stay_methods() { + let (m, p) = app_request("engine.history", json!({})); + assert_eq!(m, "engine.execute"); + assert_eq!(p["command"], "engine.history"); + let (m, p) = app_request("ui.theme", json!({"mode": "light"})); + assert_eq!(m, "engine.execute"); + assert_eq!(p["params"]["mode"], "light"); + let (m, _) = app_request("session.new", json!({"name": "Session A"})); + assert_eq!(m, "engine.execute"); + let (m, _) = app_request("track.new", json!({})); + assert_eq!(m, "engine.execute"); + for method in ["session.inspect", "engine.commands", "engine.parity", "ui.screenshot", "app.quit"] { + assert_eq!(app_request(method, json!({})).0, method); + } + } + #[test] fn missing_media_gets_one_warning_each() { assert!(missing_warnings(&[]).is_empty());