Skip to main content

majordomus_cli/commands/
mod.rs

1//! Command dispatch: the one place a parsed command line becomes a call.
2//!
3//! Each command is a function from its own argument type to an exit code, and [`run`] is
4//! the only mapping between the two. That is the whole of what lives here — no argument is
5//! interpreted, no repository is opened, nothing is rendered. A command that does more than
6//! render is a command that has taken business logic out of the capability it should be
7//! calling: the modules below reach the registry through
8//! [`Context::execute`](crate::capability::Context::execute), the same call MCP and the
9//! HTTP routes make, so a command cannot answer differently from the API.
10//!
11//! # The exit-code contract
12//!
13//! `Ok(0)` is success. A command that decides a verdict answers with the code for it —
14//! `10` for a contract unmet, which `scope --check` and `quality report` both use. An `Err`
15//! carries its own code through [`crate::Error::exit_code`], so a failure has one code
16//! whether the command chose it or an error did.
17//!
18//! # Which commands are here, and which are not
19//!
20//! A command is the projection of a capability, or [`crate::cli::local`] says why it is
21//! not. Both are checked by [`crate::quality::parity`]. Nothing in this module decides it.
22//!
23//! ```
24//! use majordomus_cli::cli::{Cli, Command};
25//! use clap::Parser;
26//!
27//! // dispatch is total over the command line: every variant clap can parse has an arm
28//! let cli = Cli::try_parse_from(["majordomus", "quality", "report", "--summary"]).unwrap();
29//! assert!(matches!(cli.command, Command::Quality(_)));
30//! ```
31
32pub(crate) mod bench;
33pub(crate) mod brief;
34pub(crate) mod capabilities;
35pub(crate) mod command_graph;
36pub(crate) mod commit;
37pub(crate) mod completion;
38pub(crate) mod continuity;
39pub(crate) mod convergence;
40pub(crate) mod dashboard;
41pub(crate) mod delivery;
42pub(crate) mod devcontext;
43pub(crate) mod devtask;
44pub(crate) mod distribution;
45pub(crate) mod economics;
46pub(crate) mod entity;
47pub(crate) mod env;
48pub(crate) mod evidence;
49pub(crate) mod executions;
50pub(crate) mod generate;
51pub(crate) mod guard;
52pub(crate) mod intent;
53pub(crate) mod knowledge;
54pub(crate) mod mcp;
55pub(crate) mod mesh;
56pub(crate) mod models;
57pub(crate) mod monitor;
58pub(crate) mod offload;
59pub(crate) mod pack;
60pub(crate) mod product;
61pub(crate) mod prs;
62pub(crate) mod quality;
63pub(crate) mod reasoning;
64pub(crate) mod release;
65pub(crate) mod rules;
66pub(crate) mod scope;
67pub(crate) mod serve;
68pub(crate) mod served;
69pub(crate) mod shell;
70pub(crate) mod skills;
71pub(crate) mod web;
72pub(crate) mod why;
73pub(crate) mod worktree;
74
75use crate::cli::{Cli, Command};
76use crate::error::Result;
77
78/// Run the selected command. `Ok(0)` is success; an `Err` carries its own exit code.
79pub fn run(cli: Cli) -> Result<u8> {
80    match cli.command {
81        Command::Mcp(args) => mcp::run(args),
82        Command::Serve(args) => serve::run(args),
83        Command::Capabilities(args) => capabilities::run(args),
84        Command::Generate(args) => generate::run(args),
85        Command::Bench(args) => bench::run(args),
86        Command::Scope(args) => scope::run(args),
87        Command::Web(args) => web::run(args),
88        Command::Why(args) => why::run(args),
89        Command::Devtask(args) => devtask::run(args),
90        Command::Distribution(args) => distribution::run(args),
91        Command::Env(args) => env::run(args),
92        Command::Commands(args) => command_graph::run(args),
93        Command::Completion(args) => completion::run(args),
94        Command::Worktree(args) => worktree::run(args),
95        Command::Prs(args) => prs::run(args),
96        Command::Offload(args) => offload::run(args),
97        Command::Convergence(args) => convergence::run(args),
98        Command::Commit(args) => commit::run(args),
99        Command::Product(args) => product::run(args),
100        Command::Release(args) => release::run(args),
101        Command::Quality(args) => quality::run(args),
102        Command::Run(args) => executions::run(args),
103        Command::Executions(args) => executions::executions(args),
104        Command::Devcontext(args) => devcontext::run(args),
105        Command::Guard(args) => guard::run(args),
106        Command::Mesh(args) => mesh::run(args),
107        Command::Monitor(args) => monitor::run(args),
108        Command::Models(args) => models::run(args),
109        Command::Reasoning(args) => reasoning::run(args),
110        Command::Evidence(args) => evidence::run(args),
111        Command::Served(args) => served::run(args),
112        Command::Rules(args) => rules::run(args),
113        Command::Intent(args) => intent::run(args),
114        Command::Delivery(args) => delivery::run(args),
115        Command::Economics(args) => economics::run(args),
116        Command::Entity(args) => entity::run(args),
117        Command::Shell(args) => shell::run(args),
118        Command::Pack(args) => pack::run(args),
119        Command::Dashboard(args) => dashboard::run(args),
120        Command::Brief(args) => brief::run(args),
121        Command::Skills(args) => skills::run(args),
122        Command::Knowledge(args) => knowledge::run(args),
123        Command::Continuity(args) => continuity::run(args),
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use clap::Parser;
131
132    #[test]
133    fn every_command_the_command_line_parses_reaches_a_dispatch_arm() {
134        // the match in `run` is exhaustive by construction — a new variant of `Command` is a
135        // compile error here — and this is the other half: that the words a person types
136        // reach the variant this module then dispatches
137        /// A command line, and the predicate that says which variant it must reach.
138        type Case = (&'static [&'static str], fn(&Command) -> bool);
139        let cases: &[Case] = &[
140            (&["majordomus", "mcp"], |c| matches!(c, Command::Mcp(_))),
141            (&["majordomus", "monitor", "--follow"], |c| {
142                matches!(c, Command::Monitor(_))
143            }),
144            (&["majordomus", "serve"], |c| matches!(c, Command::Serve(_))),
145            (&["majordomus", "serve", "status"], |c| {
146                matches!(c, Command::Serve(_))
147            }),
148            (&["majordomus", "capabilities", "list"], |c| {
149                matches!(c, Command::Capabilities(_))
150            }),
151            (&["majordomus", "generate"], |c| {
152                matches!(c, Command::Generate(_))
153            }),
154            (&["majordomus", "bench", "coverage"], |c| {
155                matches!(c, Command::Bench(_))
156            }),
157            (&["majordomus", "scope"], |c| matches!(c, Command::Scope(_))),
158            (&["majordomus", "web", "list"], |c| {
159                matches!(c, Command::Web(_))
160            }),
161            (&["majordomus", "why", "list"], |c| {
162                matches!(c, Command::Why(_))
163            }),
164            (&["majordomus", "intent", "list"], |c| {
165                matches!(c, Command::Intent(_))
166            }),
167            (&["majordomus", "skills", "status"], |c| {
168                matches!(c, Command::Skills(_))
169            }),
170            (&["majordomus", "devtask", "issue", "I0001"], |c| {
171                matches!(c, Command::Devtask(_))
172            }),
173            (&["majordomus", "devtask", "milestone", "foundation"], |c| {
174                matches!(c, Command::Devtask(_))
175            }),
176            (&["majordomus", "distribution", "show"], |c| {
177                matches!(c, Command::Distribution(_))
178            }),
179            (&["majordomus", "worktree", "status"], |c| {
180                matches!(c, Command::Worktree(_))
181            }),
182            (&["majordomus", "quality", "report"], |c| {
183                matches!(c, Command::Quality(_))
184            }),
185            (&["majordomus", "economics", "summary"], |c| {
186                matches!(c, Command::Economics(_))
187            }),
188            (&["majordomus", "dashboard", "overview"], |c| {
189                matches!(c, Command::Dashboard(_))
190            }),
191            (&["majordomus", "brief", "--line"], |c| {
192                matches!(c, Command::Brief(_))
193            }),
194            (
195                &["majordomus", "devcontext", "compile", "--issue", "I1"],
196                |c| matches!(c, Command::Devcontext(_)),
197            ),
198            (&["majordomus", "knowledge", "status"], |c| {
199                matches!(c, Command::Knowledge(_))
200            }),
201            (
202                &["majordomus", "knowledge", "record", "e1-0123456789ab"],
203                |c| matches!(c, Command::Knowledge(_)),
204            ),
205            (&["majordomus", "continuity", "status"], |c| {
206                matches!(c, Command::Continuity(_))
207            }),
208            (
209                &["majordomus", "continuity", "resume", "--record", "a1b2"],
210                |c| matches!(c, Command::Continuity(_)),
211            ),
212        ];
213        for (argv, is_expected) in cases {
214            let cli =
215                Cli::try_parse_from(*argv).unwrap_or_else(|e| panic!("{}: {e}", argv.join(" ")));
216            assert!(
217                is_expected(&cli.command),
218                "{} dispatched elsewhere",
219                argv.join(" ")
220            );
221        }
222    }
223
224    #[test]
225    fn a_command_line_nothing_declares_is_refused_rather_than_dispatched() {
226        assert!(Cli::try_parse_from(["majordomus", "nonesuch"]).is_err());
227        assert!(
228            Cli::try_parse_from(["majordomus"]).is_err(),
229            "a command is required"
230        );
231    }
232}