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}