Skip to main content

muster/adapter/cli/
args.rs

1use std::path::PathBuf;
2
3use clap::{
4    Parser, Subcommand,
5    builder::styling::{AnsiColor, Styles},
6};
7
8use super::{completions::CompletionShell, run::RunArgs};
9use crate::{constants::APP_NAME, domain::process::AgentTool};
10
11/// Help styling matching the CLI's report palette: cyan structure, green
12/// literals, yellow placeholders.
13const HELP_STYLES: Styles = Styles::styled()
14    .header(AnsiColor::Cyan.on_default().bold())
15    .usage(AnsiColor::Cyan.on_default().bold())
16    .literal(AnsiColor::Green.on_default())
17    .placeholder(AnsiColor::Yellow.on_default());
18
19/// Command-line arguments. With no subcommand, muster launches its TUI.
20#[derive(Parser)]
21#[command(
22    name = APP_NAME,
23    bin_name = APP_NAME,
24    version,
25    about = "A terminal workspace for running CLI agents and dev processes",
26    styles = HELP_STYLES
27)]
28pub struct Args {
29    /// Path to the workspace config file. Global, so it is recognized before or
30    /// after a subcommand rather than being swallowed by `run`'s command args.
31    #[arg(short, long, global = true)]
32    pub config: Option<PathBuf>,
33    #[command(subcommand)]
34    pub command: Option<Command>,
35}
36
37/// Subcommands. Absent, muster runs the TUI.
38#[derive(Subcommand)]
39pub enum Command {
40    /// Create a starter muster.yml here and register this folder as a project.
41    Init,
42    /// Validate the workspace config and exit non-zero on problems.
43    Check,
44    /// Register a command in a project, then run it.
45    Run(RunArgs),
46    /// List registered projects, or add and remove them.
47    Projects {
48        #[command(subcommand)]
49        command: Option<ProjectsCommand>,
50    },
51    /// Install provider integrations used to preserve native agent sessions.
52    Hooks {
53        #[command(subcommand)]
54        command: HooksCommand,
55    },
56    /// Diagnose config, projects, sessions, hooks, clipboard, and completions.
57    Doctor,
58    /// Print the shell hook that enables completions.
59    Completions {
60        /// Shell to print the hook for.
61        #[arg(value_enum)]
62        shell: CompletionShell,
63    },
64    /// Internal provider-hook receiver.
65    #[command(hide = true)]
66    Hook {
67        #[command(subcommand)]
68        command: InternalHookCommand,
69    },
70}
71
72/// Registry management actions under `muster projects`.
73#[derive(Subcommand)]
74pub enum ProjectsCommand {
75    /// Register an existing folder containing a muster.yml.
76    Add {
77        /// Folder to register (defaults to the current directory).
78        path: Option<PathBuf>,
79    },
80    /// Unregister a project by name; files on disk are untouched.
81    Remove {
82        /// Registered project name.
83        name: String,
84    },
85}
86
87/// User-facing lifecycle-integration commands.
88#[derive(Subcommand)]
89pub enum HooksCommand {
90    /// Install idempotent session-ID hooks/plugins for supported agents.
91    Setup,
92}
93
94/// Commands invoked by installed provider integrations.
95#[derive(Subcommand)]
96pub enum InternalHookCommand {
97    /// Capture a provider session ID from JSON on standard input.
98    Capture {
99        /// Provider integration that emitted this lifecycle event.
100        #[arg(long)]
101        provider: AgentTool,
102        /// Parent provider process that invoked the capture hook.
103        #[arg(long)]
104        process_id: u32,
105        /// Parent of the provider process, when the provider was launched by a shell wrapper.
106        #[arg(long)]
107        parent_process_id: Option<u32>,
108    },
109    /// Bind a durable session to this process, then start its provider command.
110    Launch {
111        /// Stable Muster identity of the session being launched.
112        #[arg(long)]
113        session: String,
114        /// Original provider command, preserved as one shell expression.
115        #[arg(last = true, allow_hyphen_values = true)]
116        command: String,
117    },
118}
119
120#[cfg(test)]
121mod tests {
122    use clap::Parser;
123
124    use super::*;
125
126    /// Completions and usage must register the binary name, not the package
127    /// name muster-workspace, or every emitted shell hook is inert.
128    #[test]
129    fn the_command_is_named_after_the_binary() {
130        use clap::CommandFactory;
131
132        assert_eq!(Args::command().get_name(), APP_NAME);
133        assert_eq!(Args::command().get_bin_name(), Some(APP_NAME));
134        assert_eq!(
135            Args::command().get_version(),
136            Some(env!("CARGO_PKG_VERSION")),
137            "muster --version must work on installed builds"
138        );
139    }
140
141    /// The global config flag parses before and after a subcommand.
142    #[test]
143    fn global_config_flag_parses_anywhere() {
144        let before = Args::try_parse_from(["muster", "-c", "x.yml", "run", "--", "ls"]).unwrap();
145        assert_eq!(
146            before.config.as_deref(),
147            Some(std::path::Path::new("x.yml"))
148        );
149        let bare = Args::try_parse_from(["muster"]).unwrap();
150        assert!(bare.command.is_none());
151    }
152}