From 25aa096274ed6f859d615cc02e29ebab22e3b8f0 Mon Sep 17 00:00:00 2001 From: xxx Date: Wed, 29 Jul 2026 14:56:08 -0700 Subject: [PATCH] feat(min): min with no args prints the help text --- crates/minimal/src/lib.rs | 145 +++++++++----------------------------- docs/reference/cli-min.md | 9 +-- docs/reference/cli.md | 3 +- 3 files changed, 39 insertions(+), 118 deletions(-) diff --git a/crates/minimal/src/lib.rs b/crates/minimal/src/lib.rs index 377a516e0..1e3463304 100644 --- a/crates/minimal/src/lib.rs +++ b/crates/minimal/src/lib.rs @@ -29,9 +29,9 @@ pub mod prompt; )] #[command(subcommand_required = false)] pub struct Cli { - // Optional: a bare `min` (no subcommand) resolves or activates a session - // for the current directory — see `cmd_default`. Keeps every named - // subcommand reachable unchanged when one is supplied. + // Optional: a bare `min` (no subcommand) prints the top-level help — see + // `cmd_default`. Keeps every named subcommand reachable unchanged when one + // is supplied. #[command(subcommand)] pub command: Option, @@ -292,10 +292,10 @@ pub struct GlobalArgs { pub provider: Option, /// Skip interactive prompts that need a terminal. /// - /// Affects e.g. the session picker shown by bare `min` or `min session - /// attach` with no session argument. When a choice is ambiguous, the - /// command errors with a list of candidates instead of opening a picker. - /// Implied when stdin/stdout is not a terminal. + /// Affects e.g. the session picker shown by `min session attach` with no + /// session argument. When a choice is ambiguous, the command errors with a + /// list of candidates instead of opening a picker. Implied when + /// stdin/stdout is not a terminal. #[arg(long, global = true, default_value_t = false)] pub no_input: bool, } @@ -638,10 +638,9 @@ async fn run_command(cli: Cli) -> Result<(), anyhow::Error> { client::migrate_legacy_provider_dirs(cli.global_args.minimal_dir.as_deref()); match cli.command { - // A bare `min` (no subcommand) resolves-or-activates and attaches. A - // deliberate exception to the ` ` convention, documented - // in docs/reference/cli.md — see `cmd_default`. - None => cmd_default(&cli.global_args).await, + // A bare `min` (no subcommand) prints the top-level help — see + // `cmd_default`. + None => cmd_default(), 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 { @@ -709,68 +708,18 @@ fn session_announce_label(id: &sessions::SessionId, name: Option<&str>) -> Strin } } -/// The default action for a bare `min` (no subcommand): get the operator into -/// a session for the current directory with the least ceremony. +/// The default action for a bare `min` (no subcommand): print the top-level +/// help and exit successfully, the same text `min --help` produces. /// -/// - No sessions exist → [`cmd_activate`] a new one with `--attach`, so a -/// fresh `min` lands the user in a shell. -/// - A session built from the current directory exists → attach to it -/// (auto-resolve, or picker if ambiguous). -/// - Otherwise → attach to the only session, or open a picker over all. -/// -/// Shares smart resolution with `min session attach` (no session arg) via -/// [`resolve_smart_attach`]; the only difference is the `NoSessions` case, -/// which activates here instead of erroring. -async fn cmd_default(global: &GlobalArgs) -> Result<(), anyhow::Error> { - ensure_daemon(global)?; - - let sock = client::resolve_socket_path(global.minimal_dir.as_deref(), global.use_minvmd()) - .context("Failed to resolve daemon socket path")?; - - let mut client = client::Client::connect(&sock) - .await - .context("Failed to connect to minimald")?; - - match resolve_smart_attach(&mut client, global).await? { - Some(entry) => { - tracing::info!( - session_id = %entry.id, - session_name = ?entry.name, - "bare `min`: attaching to resolved session" - ); - // Drop the listing connection before shelling out; the ssh child - // holds its own proxy connection to the daemon. - drop(client); - attach_to_session(&sock, entry.id, None).await - } - None => { - // No sessions exist: activate a new one for the current directory - // (or -C/--repo-dir if set) and chain into attach, mirroring - // `min session activate --attach`. `cmd_activate` resolves the path from - // `global.repo_dir` when no positional is given, matching the - // attach side's `cwd_host_path(global)`. But refuse first when that - // chained attach could never run: creating then failing orphans the - // session (#1031). - ensure_activate_on_empty_allowed(global.no_input, std::io::stdin().is_terminal())?; - drop(client); - let activate_args = ActivateArgs { - name: None, - path: None, - sync: SyncMode::Tarball, - network: CliNetworkMode::HostNet, - ingress: Vec::new(), - loadout: Vec::new(), - no_loadouts: false, - // A non-interactive caller (--no-input, CI, a script) can't - // answer the activation policy prompt; `cmd_activate` already - // falls back to the `--no-prompt` path when stderr isn't a - // terminal, so mirror that here rather than forcing a hang. - no_prompt: global.no_input || !can_prompt_interactively(), - attach: true, - }; - cmd_activate(global, activate_args).await - } - } +/// Deliberately inert — it starts no daemon, creates no session, and attaches +/// to nothing. Getting into a session is spelled explicitly: +/// `min session attach` (which still auto-resolves from the current directory) +/// or `min session activate`. +pub fn cmd_default() -> Result<(), anyhow::Error> { + use clap::CommandFactory as _; + Cli::command() + .print_help() + .context("Failed to write help output") } /// Connect to the daemon, resolving the socket path from global args. @@ -1795,9 +1744,8 @@ pub async fn cmd_attach(global: &GlobalArgs, args: AttachArgs) -> Result<(), any /// and either attaches directly (unambiguous), opens the interactive picker /// (ambiguous), or errors (ambiguous but non-interactive). /// -/// Returns `Ok(None)` when no sessions exist at all — the caller decides -/// whether that is an error (`min session attach`) or a cue to activate a new session -/// (bare `min`). +/// Returns `Ok(None)` when no sessions exist at all, which `min session attach` +/// reports as an error pointing at `min session activate`. async fn resolve_smart_attach( client: &mut client::Client, global: &GlobalArgs, @@ -1835,25 +1783,6 @@ async fn resolve_smart_attach( } } -/// Guard for bare `min` on an empty daemon: activating there chains straight -/// into an interactive attach, which needs a TTY on stdin. Under `--no-input`, -/// or when stdin is not a terminal, that attach can never succeed — so refuse -/// before creating anything, emitting the same message the explicit -/// `min session attach` produces, instead of creating a session and then -/// failing the attach guard, which leaves the session orphaned (#1031). -/// -/// Pure in its inputs so both branches are unit-testable without a controlled -/// terminal. -fn ensure_activate_on_empty_allowed( - no_input: bool, - stdin_is_tty: bool, -) -> Result<(), anyhow::Error> { - if no_input || !stdin_is_tty { - bail!("no sessions exist; use 'min session activate' to create one"); - } - Ok(()) -} - /// Guard for the interactive attach path: the PTY-backed session shell must be /// driven from a real terminal. When stdin is not a TTY there is nothing to /// drive the remote shell and no EOF ever reaches it through the forced `-tt` @@ -1879,9 +1808,9 @@ fn ensure_interactive_attach_tty(stdin_is_tty: bool) -> Result<(), anyhow::Error /// daemon's shell_request handler mints a PTY-backed shell, and ssh handles /// termios/PTY management. /// -/// Split from [`cmd_attach`] so the bare-`min` default dispatch -/// ([`cmd_default`]) and the smart-resolution picker can attach without -/// re-resolving an entry they already hold. +/// Split from [`cmd_attach`] so the activate-then-attach chain and the +/// smart-resolution picker can attach without re-resolving an entry they +/// already hold. async fn attach_to_session( sock: &std::path::Path, id: sessions::SessionId, @@ -2772,23 +2701,13 @@ mod tests { ensure_interactive_attach_tty(true).expect("a real terminal must pass the guard"); } - /// Bare `min` on an empty daemon must refuse before creating a session when - /// it could not follow through with the interactive attach — under - /// `--no-input` or over a non-TTY stdin — so it never orphans a session - /// (#1031). Only a plain interactive invocation proceeds to activate. + /// A bare `min` must be inert: it prints the top-level help and succeeds, + /// touching no daemon and creating no session. The `Cli` command tree has + /// to stay renderable for that (a malformed clap definition panics in + /// `print_help`, not at parse time). #[test] - fn bare_min_refuses_to_activate_on_empty_daemon_non_interactively() { - let err = ensure_activate_on_empty_allowed(true, true) - .unwrap_err() - .to_string(); - assert!( - err.contains("no sessions exist") && err.contains("min session activate"), - "expected the explicit-verb refusal, got: {err}" - ); - ensure_activate_on_empty_allowed(false, false) - .expect_err("a non-TTY stdin must be refused even without --no-input"); - ensure_activate_on_empty_allowed(false, true) - .expect("an interactive terminal without --no-input must be allowed to activate"); + fn bare_min_prints_help() { + cmd_default().expect("bare `min` must print help and succeed"); } /// The attach/create confirmation prefers the session name and appends a diff --git a/docs/reference/cli-min.md b/docs/reference/cli-min.md index 0dc68fe76..9adef73e5 100644 --- a/docs/reference/cli-min.md +++ b/docs/reference/cli-min.md @@ -11,12 +11,13 @@ development sessions. Most commands start the daemon automatically when it isn't running, with `bug`, `stop`, and `version` being the exceptions. The daemon starts natively on Linux (and under `--provider local-minvmd`) or inside the [`minvmd`](./cli-minvmd.md) microVM host daemon on macOS. -Running bare `min` with no subcommand resolves a session for the current -directory (activating one if needed) and attaches to it. +Running bare `min` with no subcommand prints this help and exits; use +`min session attach` to get into a session for the current directory, or +`min session activate` to create one. Commands are spelled `min `, and every noun accepts its singular -and plural form (`session`/`sessions`, `loadout`/`loadouts`). Bare `min` and a -handful of bare verbs (`ls`, `stop`, +and plural form (`session`/`sessions`, `loadout`/`loadouts`). A handful of +bare verbs (`ls`, `stop`, `init`, `add`, `update`) survive at the top level as deliberate ergonomic exceptions, called out as such below; see [the CLI convention](./cli.md#command-naming-convention) for the rule and the diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 128c2211d..6391b901a 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -47,12 +47,13 @@ Three rules follow from that: ### Documented exceptions +`min` with no arguments prints the top-level help. + A few high-traffic forms stay bare at the top level. These are deliberate ergonomic choices, not leftovers — do not "fix" them: | Form | Why it stays | |------|--------------| -| `min` (no subcommand) | Resolve-or-activate-and-attach for the current directory: the single most common thing anyone does with `min`. | | `min ls` | The highest-traffic command in the CLI; the break is not worth the consistency. | | `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. |