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