From e975f5fc701b8fd88b1fa8ed2b77e7e9cda4dc91 Mon Sep 17 00:00:00 2001 From: "gominimal-aw-bot[bot]" <281738952+gominimal-aw-bot[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 01:06:56 +0000 Subject: [PATCH 1/3] fix(min): add canonical `min session list` command `min session list` did not exist: `SessionCommand` had no `List` variant, so the guessable ` list` form of the flagship session noun errored under clap where every other noun (e.g. `min loadout list`) accepts it. Add `SessionCommand::List`, delegating to the existing `cmd_ls` so output is byte-identical, with `ls` as a visible noun-level alias. `min ls` keeps its bare top-level form as a visible alias. Docs now present `min session list` as canonical. --- crates/minimal/src/lib.rs | 65 +++++++++++++++++++++++++++++++-------- docs/reference/cli-min.md | 10 +++--- docs/reference/cli.md | 2 +- 3 files changed, 59 insertions(+), 18 deletions(-) diff --git a/crates/minimal/src/lib.rs b/crates/minimal/src/lib.rs index a11164355..f4d151deb 100644 --- a/crates/minimal/src/lib.rs +++ b/crates/minimal/src/lib.rs @@ -21,7 +21,6 @@ mod file_upload; pub mod git_remote; pub mod loadouts; pub mod prompt; -pub mod theme; #[derive(Parser)] #[command(name = "min", version = version::VERSION, long_version = version::LONG_VERSION)] @@ -45,9 +44,9 @@ pub enum Command { /// List sessions // // Deliberate exception to the ` ` convention (documented in - // docs/reference/cli.md): `min ls` is the highest-traffic command in the - // CLI and keeps its bare top-level form. Not an oversight — do not move it - // under `session`. + // docs/reference/cli.md): `min session list` is the canonical spelling, + // and `min ls` — the highest-traffic command in the CLI — keeps this bare + // top-level form as its visible alias. Not an oversight — do not remove it. Ls(LsArgs), /// Shut down the minimald daemon // @@ -163,6 +162,9 @@ pub struct SessionArgs { #[derive(Debug, Subcommand)] pub enum SessionCommand { + /// List sessions + #[command(visible_alias = "ls")] + List(LsArgs), /// Activate (create) a new session Activate(ActivateArgs), /// Attach to an existing session @@ -649,6 +651,7 @@ async fn run_command(cli: Cli) -> Result<(), anyhow::Error> { Some(Command::Ls(args)) => cmd_ls(&cli.global_args, args).await, Some(Command::Stop(args)) => cmd_stop(&cli.global_args, args).await, Some(Command::Session(SessionArgs { command })) => match command { + SessionCommand::List(args) => cmd_ls(&cli.global_args, args).await, SessionCommand::Activate(args) => cmd_activate(&cli.global_args, args).await, SessionCommand::Attach(args) => cmd_attach(&cli.global_args, args).await, SessionCommand::Destroy(args) => cmd_destroy(&cli.global_args, args).await, @@ -985,11 +988,11 @@ fn format_memory(bytes: u64) -> String { /// renders on stderr (so stderr must be a terminal) and reads /// keypresses from stdin (so stdin must be a terminal too). If /// either side is redirected we take the `--no-prompt` path — going -/// interactive when stdin is a pipe just hangs the prompt and then +/// interactive when stdin is a pipe just hangs `dialoguer` and then /// aborts with a much less helpful error than the `--no-prompt` /// snippet the operator actually wants to paste. fn can_prompt_interactively() -> bool { - std::io::stdin().is_terminal() && std::io::stderr().is_terminal() + dialoguer::console::user_attended() && dialoguer::console::user_attended_stderr() } /// Phase 3 gate: run the user policy + hooks over the daemon's @@ -1205,13 +1208,13 @@ async fn upload_and_finalize( /// Guard that tears down a half-built session if the user interrupts the /// activation with Ctrl-C. /// -/// inquire (crossterm raw mode) captures a Ctrl-C at the -/// composition-gating prompt as an error return, so the abort-cleanup -/// that [`drive_pending_to_active`] runs gets to execute. During the -/// non-prompt phases (waiting on the daemon) a Ctrl-C is a plain -/// SIGINT, which would kill `min` before that cleanup, leaving the -/// daemon holding a `Pending` session that blocks its name. -/// [`arm_activation_interrupt`] installs a SIGINT handler that best-effort +/// `dialoguer`/`console` re-raise SIGINT to this process on a Ctrl-C at +/// the composition-gating prompt (console `unix_term.rs`). With no +/// handler installed the default disposition kills `min` mid-prompt — +/// before the abort-cleanup that [`drive_pending_to_active`] runs — so +/// the daemon is left holding a `Pending` session that blocks its name. +/// [`arm_activation_interrupt`] installs a SIGINT handler (keeping the +/// process alive past console's re-raise) that best-effort /// `AbortSession`s the in-flight session over a fresh connection — the /// activation borrows the primary one — then exits. The daemon's /// connection-close reap is the backstop if the abort can't be @@ -2820,6 +2823,42 @@ mod tests { assert!(!cli.global_args.use_minvmd()); } + /// `min session list` is the canonical ` list` spelling of the + /// flagship list command; `min ls` (top-level) and `min session ls` + /// (noun-level) are visible aliases. All three parse to the same `LsArgs`, + /// so `--raw`/`--json` reach the one `cmd_ls` implementation identically. + #[test] + fn session_list_spellings_all_reach_ls() { + use clap::Parser as _; + let ls_args = |args: &[&str]| -> LsArgs { + match Cli::try_parse_from(args).unwrap().command { + Some(Command::Ls(a)) + | Some(Command::Session(SessionArgs { + command: SessionCommand::List(a), + })) => a, + _ => panic!("expected an ls command for {args:?}"), + } + }; + + for spelling in [ + ["min", "session", "list"].as_slice(), + ["min", "session", "ls"].as_slice(), + ["min", "ls"].as_slice(), + ] { + let a = ls_args(spelling); + assert!( + !a.raw && !a.json, + "{spelling:?} must default both flags off" + ); + } + + // `--raw`/`--json` are accepted on the canonical and the bare form alike. + let canonical = ls_args(&["min", "session", "list", "--raw", "--json"]); + assert!(canonical.raw && canonical.json); + let bare = ls_args(&["min", "ls", "--json"]); + assert!(bare.json && !bare.raw); + } + /// `repo_dir` and `minimal_dir` are global, so they must be accepted after /// the subcommand — not just before it (#1039). #[test] diff --git a/docs/reference/cli-min.md b/docs/reference/cli-min.md index ff62e8dd5..f5d6ccb35 100644 --- a/docs/reference/cli-min.md +++ b/docs/reference/cli-min.md @@ -37,10 +37,10 @@ These apply to every subcommand. ## Commands -### `ls` +### `session list` (aliases: `min ls`, `min session ls`) ``` -min ls [--raw] [--json] +min session list [--raw] [--json] ``` Lists sessions. `--raw` prints raw session IDs one per line for piping @@ -49,8 +49,10 @@ JSON. When the daemon reports a shared resource pool, the table is headed by a `RESOURCE POOL:` line (CPU cores, memory, and the number of sessions sharing them); `--raw` omits it. -A deliberate exception to the `min ` convention: `ls` is the -highest-traffic command in the CLI and keeps its bare top-level form. +`min ls` is the same command kept bare at the top level — a deliberate +exception to the `min ` convention, since it is the +highest-traffic command in the CLI; `min session ls` is the noun-level alias. +All three spellings take the same flags and produce identical output. ### `session activate` diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 6391b901a..1662b1008 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -54,7 +54,7 @@ ergonomic choices, not leftovers — do not "fix" them: | Form | Why it stays | |------|--------------| -| `min ls` | The highest-traffic command in the CLI; the break is not worth the consistency. | +| `min ls` | Visible top-level alias of the canonical `min session list`; the highest-traffic command keeps its bare form. | | `min stop` | Acts on the daemon backend rather than any session, and is the daemon-lifecycle command people reach for. | | `min init`, `min add`, `min update` | Passthroughs to the `mip` commands of the same name; keeping the spelling identical across the two CLIs beats the hierarchy. | From b7786d8a2c2749cc22877d9fe57b3e98ab79b7a4 Mon Sep 17 00:00:00 2001 From: Norrie Taylor Date: Thu, 30 Jul 2026 21:20:19 -0700 Subject: [PATCH 2/3] fix: restore #1114 content and add the daemon-stop half Un-clobber the theme module, TTY predicate, and doc comments the stale base reverted, and complete the issue's second half: min daemon stop is canonical with min stop kept as the visible top-level alias, mirroring the session list / ls pattern. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KyZLpkRf9G4A2hUDgDvn5f --- crates/minimal/src/lib.rs | 76 ++++++++++++++++++++++++++++++++------- docs/reference/cli-min.md | 9 ++--- docs/reference/cli.md | 2 +- 3 files changed, 70 insertions(+), 17 deletions(-) diff --git a/crates/minimal/src/lib.rs b/crates/minimal/src/lib.rs index f4d151deb..eb2bb40b7 100644 --- a/crates/minimal/src/lib.rs +++ b/crates/minimal/src/lib.rs @@ -21,6 +21,7 @@ mod file_upload; pub mod git_remote; pub mod loadouts; pub mod prompt; +pub mod theme; #[derive(Parser)] #[command(name = "min", version = version::VERSION, long_version = version::LONG_VERSION)] @@ -50,13 +51,17 @@ pub enum Command { Ls(LsArgs), /// Shut down the minimald daemon // - // Stays top-level: it acts on the daemon backend, not on any session, and - // it is the daemon-lifecycle command people reach for. Documented as a - // deliberate exception in docs/reference/cli.md. + // Deliberate exception to the ` ` convention (documented in + // docs/reference/cli.md): `min daemon stop` is the canonical spelling, + // and `min stop` — acting on the daemon backend, not on any session — + // keeps this bare top-level form as its visible alias. Not an oversight — + // do not remove it. Stop(StopArgs), /// Session management subcommands #[command(visible_alias = "sessions")] Session(SessionArgs), + /// Daemon management subcommands + Daemon(DaemonArgs), /// Loadout management subcommands #[command(visible_alias = "loadouts")] Loadout(LoadoutArgs), @@ -184,6 +189,18 @@ pub struct PolicyArgs { pub session: String, } +#[derive(Debug, Args)] +pub struct DaemonArgs { + #[command(subcommand)] + pub command: DaemonCommand, +} + +#[derive(Debug, Subcommand)] +pub enum DaemonCommand { + /// Shut down the minimald daemon + Stop(StopArgs), +} + #[derive(Debug, Args)] pub struct LoadoutArgs { #[command(subcommand)] @@ -658,6 +675,9 @@ async fn run_command(cli: Cli) -> Result<(), anyhow::Error> { SessionCommand::Rename(args) => cmd_rename(&cli.global_args, args).await, SessionCommand::Policy(args) => cmd_session_policy(&cli.global_args, args).await, }, + Some(Command::Daemon(DaemonArgs { command })) => match command { + DaemonCommand::Stop(args) => cmd_stop(&cli.global_args, args).await, + }, Some(Command::Loadout(LoadoutArgs { command: LoadoutCommand::List(args), })) => loadouts::cmd_loadout_list(args, &cli.global_args), @@ -988,11 +1008,11 @@ fn format_memory(bytes: u64) -> String { /// renders on stderr (so stderr must be a terminal) and reads /// keypresses from stdin (so stdin must be a terminal too). If /// either side is redirected we take the `--no-prompt` path — going -/// interactive when stdin is a pipe just hangs `dialoguer` and then +/// interactive when stdin is a pipe just hangs the prompt and then /// aborts with a much less helpful error than the `--no-prompt` /// snippet the operator actually wants to paste. fn can_prompt_interactively() -> bool { - dialoguer::console::user_attended() && dialoguer::console::user_attended_stderr() + std::io::stdin().is_terminal() && std::io::stderr().is_terminal() } /// Phase 3 gate: run the user policy + hooks over the daemon's @@ -1208,13 +1228,13 @@ async fn upload_and_finalize( /// Guard that tears down a half-built session if the user interrupts the /// activation with Ctrl-C. /// -/// `dialoguer`/`console` re-raise SIGINT to this process on a Ctrl-C at -/// the composition-gating prompt (console `unix_term.rs`). With no -/// handler installed the default disposition kills `min` mid-prompt — -/// before the abort-cleanup that [`drive_pending_to_active`] runs — so -/// the daemon is left holding a `Pending` session that blocks its name. -/// [`arm_activation_interrupt`] installs a SIGINT handler (keeping the -/// process alive past console's re-raise) that best-effort +/// inquire (crossterm raw mode) captures a Ctrl-C at the +/// composition-gating prompt as an error return, so the abort-cleanup +/// that [`drive_pending_to_active`] runs gets to execute. During the +/// non-prompt phases (waiting on the daemon) a Ctrl-C is a plain +/// SIGINT, which would kill `min` before that cleanup, leaving the +/// daemon holding a `Pending` session that blocks its name. +/// [`arm_activation_interrupt`] installs a SIGINT handler that best-effort /// `AbortSession`s the in-flight session over a fresh connection — the /// activation borrows the primary one — then exits. The daemon's /// connection-close reap is the backstop if the abort can't be @@ -2859,6 +2879,38 @@ mod tests { assert!(bare.json && !bare.raw); } + /// `min daemon stop` is the canonical ` ` spelling of the + /// daemon-shutdown command; `min stop` (top-level) is its visible alias. + /// Both parse to the same `StopArgs`, so `--force` reaches the one + /// `cmd_stop` implementation identically. + #[test] + fn daemon_stop_spellings_all_reach_stop() { + use clap::Parser as _; + let stop_args = |args: &[&str]| -> StopArgs { + match Cli::try_parse_from(args).unwrap().command { + Some(Command::Stop(a)) + | Some(Command::Daemon(DaemonArgs { + command: DaemonCommand::Stop(a), + })) => a, + _ => panic!("expected a stop command for {args:?}"), + } + }; + + for spelling in [ + ["min", "daemon", "stop"].as_slice(), + ["min", "stop"].as_slice(), + ] { + let a = stop_args(spelling); + assert!(!a.force, "{spelling:?} must default --force off"); + } + + // `--force` is accepted on the canonical and the bare form alike. + let canonical = stop_args(&["min", "daemon", "stop", "--force"]); + assert!(canonical.force); + let bare = stop_args(&["min", "stop", "--force"]); + assert!(bare.force); + } + /// `repo_dir` and `minimal_dir` are global, so they must be accepted after /// the subcommand — not just before it (#1039). #[test] diff --git a/docs/reference/cli-min.md b/docs/reference/cli-min.md index f5d6ccb35..8c2f74239 100644 --- a/docs/reference/cli-min.md +++ b/docs/reference/cli-min.md @@ -100,10 +100,10 @@ min session rename Renames an existing session. -### `stop` +### `daemon stop` (alias: `min stop`) ``` -min stop [-f|--force] +min daemon stop [-f|--force] ``` Shuts down the `minimald` daemon. `--force` shuts down even if active @@ -112,8 +112,9 @@ sessions themselves survive it (contrast [`session destroy`](#session-destroy), which removes one session and leaves the daemon running). -`stop` stays bare at the top level — a deliberate exception to the -`min ` convention: it acts on the daemon, not on any session. +`min stop` is the same command kept bare at the top level — a deliberate +exception to the `min ` convention: it acts on the daemon, not +on any session. Both spellings take the same flags and behave identically. ### `loadout list` (alias: `ls`) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 1662b1008..0e21eba4c 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -55,7 +55,7 @@ ergonomic choices, not leftovers — do not "fix" them: | Form | Why it stays | |------|--------------| | `min ls` | Visible top-level alias of the canonical `min session list`; the highest-traffic command keeps its bare form. | -| `min stop` | Acts on the daemon backend rather than any session, and is the daemon-lifecycle command people reach for. | +| `min stop` | Visible top-level alias of the canonical `min daemon stop`; acts on the daemon backend rather than any session. | | `min init`, `min add`, `min update` | Passthroughs to the `mip` commands of the same name; keeping the spelling identical across the two CLIs beats the hierarchy. | ## Platform availability From 467fcbfd64118f846caa2309c2ce49461be49cda Mon Sep 17 00:00:00 2001 From: Norrie Taylor Date: Thu, 30 Jul 2026 21:57:53 -0700 Subject: [PATCH 3/3] refactor(minimal): drop the daemon-noun stop spelling from this change Keep the PR to the session-list half: `min session list` canonical with `min ls`/`min session ls` as visible aliases. `min stop` stays the bare top-level command it is on main, unchanged. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01KyZLpkRf9G4A2hUDgDvn5f --- crates/minimal/src/lib.rs | 57 +++------------------------------------ docs/reference/cli-min.md | 9 +++---- docs/reference/cli.md | 2 +- 3 files changed, 8 insertions(+), 60 deletions(-) diff --git a/crates/minimal/src/lib.rs b/crates/minimal/src/lib.rs index eb2bb40b7..bfec7865c 100644 --- a/crates/minimal/src/lib.rs +++ b/crates/minimal/src/lib.rs @@ -51,17 +51,13 @@ pub enum Command { Ls(LsArgs), /// Shut down the minimald daemon // - // Deliberate exception to the ` ` convention (documented in - // docs/reference/cli.md): `min daemon stop` is the canonical spelling, - // and `min stop` — acting on the daemon backend, not on any session — - // keeps this bare top-level form as its visible alias. Not an oversight — - // do not remove it. + // Stays top-level: it acts on the daemon backend, not on any session, and + // it is the daemon-lifecycle command people reach for. Documented as a + // deliberate exception in docs/reference/cli.md. Stop(StopArgs), /// Session management subcommands #[command(visible_alias = "sessions")] Session(SessionArgs), - /// Daemon management subcommands - Daemon(DaemonArgs), /// Loadout management subcommands #[command(visible_alias = "loadouts")] Loadout(LoadoutArgs), @@ -189,18 +185,6 @@ pub struct PolicyArgs { pub session: String, } -#[derive(Debug, Args)] -pub struct DaemonArgs { - #[command(subcommand)] - pub command: DaemonCommand, -} - -#[derive(Debug, Subcommand)] -pub enum DaemonCommand { - /// Shut down the minimald daemon - Stop(StopArgs), -} - #[derive(Debug, Args)] pub struct LoadoutArgs { #[command(subcommand)] @@ -675,9 +659,6 @@ async fn run_command(cli: Cli) -> Result<(), anyhow::Error> { SessionCommand::Rename(args) => cmd_rename(&cli.global_args, args).await, SessionCommand::Policy(args) => cmd_session_policy(&cli.global_args, args).await, }, - Some(Command::Daemon(DaemonArgs { command })) => match command { - DaemonCommand::Stop(args) => cmd_stop(&cli.global_args, args).await, - }, Some(Command::Loadout(LoadoutArgs { command: LoadoutCommand::List(args), })) => loadouts::cmd_loadout_list(args, &cli.global_args), @@ -2879,38 +2860,6 @@ mod tests { assert!(bare.json && !bare.raw); } - /// `min daemon stop` is the canonical ` ` spelling of the - /// daemon-shutdown command; `min stop` (top-level) is its visible alias. - /// Both parse to the same `StopArgs`, so `--force` reaches the one - /// `cmd_stop` implementation identically. - #[test] - fn daemon_stop_spellings_all_reach_stop() { - use clap::Parser as _; - let stop_args = |args: &[&str]| -> StopArgs { - match Cli::try_parse_from(args).unwrap().command { - Some(Command::Stop(a)) - | Some(Command::Daemon(DaemonArgs { - command: DaemonCommand::Stop(a), - })) => a, - _ => panic!("expected a stop command for {args:?}"), - } - }; - - for spelling in [ - ["min", "daemon", "stop"].as_slice(), - ["min", "stop"].as_slice(), - ] { - let a = stop_args(spelling); - assert!(!a.force, "{spelling:?} must default --force off"); - } - - // `--force` is accepted on the canonical and the bare form alike. - let canonical = stop_args(&["min", "daemon", "stop", "--force"]); - assert!(canonical.force); - let bare = stop_args(&["min", "stop", "--force"]); - assert!(bare.force); - } - /// `repo_dir` and `minimal_dir` are global, so they must be accepted after /// the subcommand — not just before it (#1039). #[test] diff --git a/docs/reference/cli-min.md b/docs/reference/cli-min.md index 8c2f74239..f5d6ccb35 100644 --- a/docs/reference/cli-min.md +++ b/docs/reference/cli-min.md @@ -100,10 +100,10 @@ min session rename Renames an existing session. -### `daemon stop` (alias: `min stop`) +### `stop` ``` -min daemon stop [-f|--force] +min stop [-f|--force] ``` Shuts down the `minimald` daemon. `--force` shuts down even if active @@ -112,9 +112,8 @@ sessions themselves survive it (contrast [`session destroy`](#session-destroy), which removes one session and leaves the daemon running). -`min stop` is the same command kept bare at the top level — a deliberate -exception to the `min ` convention: it acts on the daemon, not -on any session. Both spellings take the same flags and behave identically. +`stop` stays bare at the top level — a deliberate exception to the +`min ` convention: it acts on the daemon, not on any session. ### `loadout list` (alias: `ls`) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 0e21eba4c..1662b1008 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -55,7 +55,7 @@ ergonomic choices, not leftovers — do not "fix" them: | Form | Why it stays | |------|--------------| | `min ls` | Visible top-level alias of the canonical `min session list`; the highest-traffic command keeps its bare form. | -| `min stop` | Visible top-level alias of the canonical `min daemon stop`; acts on the daemon backend rather than any session. | +| `min stop` | Acts on the daemon backend rather than any session, and is the daemon-lifecycle command people reach for. | | `min init`, `min add`, `min update` | Passthroughs to the `mip` commands of the same name; keeping the spelling identical across the two CLIs beats the hierarchy. | ## Platform availability