From 8e251bf8be8f257021d81c1e11be7f703fc911de Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Tue, 25 Aug 2026 12:34:36 +0100 Subject: [PATCH 1/2] feat(maestro): default a missing subcommand to the TUI --- crates/maestro/src/main.rs | 49 ++++++++++++++++++++++++++++--------- crates/maestro/tests/cli.rs | 30 ++++++++++++++++++++--- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/crates/maestro/src/main.rs b/crates/maestro/src/main.rs index d571636..ea157c0 100644 --- a/crates/maestro/src/main.rs +++ b/crates/maestro/src/main.rs @@ -19,8 +19,7 @@ use tracing_subscriber::EnvFilter; #[command( name = "maestro", version, - about = "Unified CLI / daemon / TUI for the Maestro terminal-session orchestrator", - arg_required_else_help = true + about = "Unified CLI / daemon / TUI for the Maestro terminal-session orchestrator" )] struct Cli { /// Override the daemon HTTP base clients talk to (e.g. @@ -37,8 +36,11 @@ struct Cli { #[arg(short, long, global = true, action = clap::ArgAction::Count)] verbose: u8, + /// A missing subcommand defaults to the TUI (#119): a bare `maestro` opens + /// the terminal UI. Builds without the `tui` feature have no UI surface, + /// so they print help and exit 0 instead. #[command(subcommand)] - command: Command, + command: Option, } #[derive(Debug, Subcommand)] @@ -89,7 +91,22 @@ fn main() -> ExitCode { } async fn dispatch(cli: Cli) -> Result<(), Box> { - match cli.command { + let command = match cli.command { + Some(command) => command, + None => { + #[cfg(feature = "tui")] + { + Command::Tui(maestro_tui::cli::TuiArgs { daemon_url: None }) + } + #[cfg(not(feature = "tui"))] + { + use clap::CommandFactory; + let _ = ::command().print_help(); + return Ok(()); + } + } + }; + match command { #[cfg(feature = "cli")] Command::List(args) => { maestro_cli::dispatch( @@ -166,19 +183,19 @@ fn init_tracing(cli: &Cli) { } #[cfg(feature = "cli")] -fn is_cli_subcommand(command: &Command) -> bool { +fn is_cli_subcommand(command: &Option) -> bool { matches!( command, - Command::List(_) - | Command::Spawn(_) - | Command::Attach(_) - | Command::Kill(_) - | Command::Hooks(_) + Some(Command::List(_)) + | Some(Command::Spawn(_)) + | Some(Command::Attach(_)) + | Some(Command::Kill(_)) + | Some(Command::Hooks(_)) ) } #[cfg(not(feature = "cli"))] -fn is_cli_subcommand(_command: &Command) -> bool { +fn is_cli_subcommand(_command: &Option) -> bool { false } @@ -216,12 +233,20 @@ mod tests { } /// Parse argv the way `main` does, returning just the dispatched subcommand. - fn command_of(argv: &[&str]) -> Command { + fn command_of(argv: &[&str]) -> Option { Cli::try_parse_from(argv) .unwrap_or_else(|e| panic!("parse {argv:?}: {e}")) .command } + #[test] + fn bare_invocation_parses_to_no_subcommand() { + // The TUI-vs-help default is applied in `dispatch`; parsing must not + // reject an empty argv. + assert!(command_of(&["maestro"]).is_none()); + assert!(command_of(&["maestro", "--verbose"]).is_none()); + } + #[cfg(feature = "cli")] #[test] fn cli_subcommands_are_classified_as_cli() { diff --git a/crates/maestro/tests/cli.rs b/crates/maestro/tests/cli.rs index 90a72fe..84468ac 100644 --- a/crates/maestro/tests/cli.rs +++ b/crates/maestro/tests/cli.rs @@ -54,11 +54,35 @@ fn version_flag_succeeds() { assert!(text.contains("maestro"), "--version output: {text}"); } +/// A bare `maestro` must never fall back to help on a build that has a TUI: +/// the missing subcommand defaults to `tui` (#119). Point at a port with +/// nothing listening so the TUI's health probe fails fast instead of blocking +/// on a terminal. +#[cfg(feature = "tui")] #[test] -fn no_args_prints_help_and_fails() { - // `arg_required_else_help = true` => exit non-zero but show usage. +fn bare_invocation_dispatches_to_the_tui() { + let (ok, text) = run(&["--daemon-url", "http://127.0.0.1:9"]); + assert!( + !ok, + "TUI against a dead daemon should exit non-zero:\n{text}" + ); + assert!( + text.contains("daemon unreachable"), + "bare invocation should have run the TUI's daemon probe:\n{text}" + ); + assert!( + !text.contains("Usage"), + "bare invocation must not print help when a TUI exists:\n{text}" + ); +} + +/// Builds without the `tui` feature keep the historical behaviour: no args => +/// help, exit 0 (#119). +#[cfg(not(feature = "tui"))] +#[test] +fn bare_invocation_prints_help_and_exits_zero() { let (ok, text) = run(&[]); - assert!(!ok, "bare invocation should fail:\n{text}"); + assert!(ok, "bare invocation should exit 0:\n{text}"); assert!(text.contains("Usage"), "expected usage text:\n{text}"); } From f02510465cf339148cf5aeece5f63d698571f70e Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Tue, 25 Aug 2026 12:34:36 +0100 Subject: [PATCH 2/2] docs(maestro): bare maestro opens the TUI --- docs/INSTALL.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/INSTALL.md b/docs/INSTALL.md index a6b2a63..73a966d 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -119,6 +119,9 @@ The wizard always installs the default-feature build. `maestro tui` is the keyboard-and-mouse driven terminal UI client. It ships in the default feature set, so a vanilla `cargo install maestro` builds it. +A bare `maestro` opens the TUI too — the missing subcommand defaults to `tui` +(#119), so `maestro --daemon-url http://host:7070` works as well. Builds +without the `tui` feature have no UI to open and print help instead (exit 0). Drop the `tui` feature to skip it on hosts that don't need the UI surface (`--no-default-features --features cli,daemon`).