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}