Skip to content

CLI Subcommands

or1k edited this page Aug 6, 2026 · 1 revision

CLI Subcommands

Most tools are TUI-only. The SSH User Manager (sshuser) is currently the one exception, and it's the template to copy if a new tool should also be scriptable/non-interactive (e.g. for cron jobs or CI).

How it's wired (src/main.rs)

clap's derive API, nested subcommands:

#[derive(Parser)]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,   // None => launch the TUI
}

#[derive(Subcommand)]
enum Commands {
    SshUser { #[command(subcommand)] action: SshUserCmd },
    // a new tool adds one more variant here
}

SshUserCmd then nests further (User { action: UserAction }, Profiles { action: ProfilesAction }, Settings { action: SettingsAction }) — as deep as the tool's own command surface needs. run() matches on cli.command: None calls tui::run(), Some(Commands::SshUser { action }) calls a run_sshuser(action) function that does the actual work.

The key point: the subcommand handler calls the same module the screen does

run_sshuser in main.rs loads sshuser::config::load(), calls sshuser::commands::add_user_commands/remove_user_commands to build the command list, and executes it with ssh_exec::run_commands — the exact same sshuser module functions tui/sshuser_screen.rs calls. Nothing CLI-specific lives inside the sshuser module itself; main.rs is just another caller, same as the screen is. This only works because of the rule in Architecture that a <tool>/ module never depends on ratatui — if it did, a CLI-only build path couldn't call into it without dragging in the whole TUI stack.

Adding a subcommand for a new tool

  1. Define the clap enum(s) for its command surface in main.rs, nested under a new Commands::<Tool> { ... } variant — model it on SshUserCmd/UserAction/ProfilesAction/SettingsAction.
  2. Write a run_<tool>(action: ...) -> Result<(), Box<dyn Error>> function that loads the tool's config and calls straight into <tool>:: functions — no new logic here, just argument plumbing and println!ing results.
  3. Add the new arm to run()'s match cli.command.
  4. Keep exit-code semantics consistent with run_sshuser: on partial failure across multiple hosts, print a per-host result line either way, then return Err(...) at the end so the process exits non-zero if anything failed (see the failed > 0 check pattern at the end of UserAction::Add/Remove in main.rs) — a script depending on this exit code shouldn't have to parse stdout to know if it worked.

Only add a CLI surface if there's an actual scripting use case — it's extra API surface to keep in sync with the TUI, not something every tool needs by default.

Clone this wiki locally