specman_cli/
cli.rs

1use std::ffi::OsString;
2use std::process::ExitCode;
3
4use clap::{Arg, ArgAction, ArgMatches, Command};
5
6use crate::commands;
7use crate::context::CliSession;
8use crate::error::{CliError, ExitStatus};
9use crate::formatter::{OutputFormat, emit_result};
10use crate::util::Verbosity;
11
12const NAME: &str = "specman";
13
14pub fn run() -> ExitCode {
15    init_tracing();
16    match run_cli(std::env::args()) {
17        Ok(code) => code,
18        Err(err) => {
19            err.print();
20            err.exit_code()
21        }
22    }
23}
24
25/// Parses CLI arguments, resolves the workspace, and dispatches to the appropriate
26/// command while enforcing the CLI Invocation Model (spec/specman-cli/spec.md#concept-cli-invocation-model).
27/// Returns a POSIX `sysexits`-compatible `ExitCode` so automation can react deterministically.
28pub fn run_cli<I, S>(args: I) -> Result<ExitCode, CliError>
29where
30    I: IntoIterator<Item = S>,
31    S: Into<OsString> + Clone,
32{
33    let command = build_cli();
34    let matches = command.try_get_matches_from(args)?;
35
36    let verbosity = Verbosity {
37        json: matches.get_flag("json"),
38        verbose: matches.get_flag("verbose"),
39    };
40    let output = if verbosity.json {
41        OutputFormat::Json
42    } else {
43        OutputFormat::Text
44    };
45
46    let workspace_override = matches.get_one::<String>("workspace").cloned();
47    let session = CliSession::bootstrap(workspace_override, verbosity)?;
48    if session.verbosity.verbose {
49        tracing::info!(
50            workspace = %session.workspace_paths.root().display(),
51            spec_dir = %session.workspace_paths.spec_dir().display(),
52            impl_dir = %session.workspace_paths.impl_dir().display(),
53            scratch_dir = %session.workspace_paths.scratchpad_dir().display(),
54            "resolved workspace context"
55        );
56    }
57
58    let result = dispatch(&session, &matches)?;
59    emit_result(result, output)
60}
61
62fn init_tracing() {
63    let _ = tracing_subscriber::fmt()
64        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
65        .try_init();
66}
67
68/// Defines the root `clap::Command` tree, including global flags and subcommands for
69/// `status`, `spec`, `impl`, and `scratch`. Keeping the tree centralized ensures every
70/// command advertises its help text per the CLI Invocation Model requirements.
71fn build_cli() -> Command {
72    Command::new(NAME)
73        .about("SpecMan CLI")
74        .arg(
75            Arg::new("workspace")
76                .long("workspace")
77                .value_name("PATH")
78                .help("Specify the workspace root. Defaults to the nearest ancestor with a .specman folder."),
79        )
80        .arg(
81            Arg::new("json")
82                .long("json")
83                .action(ArgAction::SetTrue)
84                .help("Emit newline-delimited JSON instead of human-readable text."),
85        )
86        .arg(
87            Arg::new("verbose")
88                .long("verbose")
89                .action(ArgAction::SetTrue)
90                .help("Emit additional logging about template locators, workspace paths, and adapters."),
91        )
92        .subcommand_required(true)
93        .subcommand(commands::status::command())
94        .subcommand(commands::spec::command())
95        .subcommand(commands::implementation::command())
96        .subcommand(commands::scratch::command())
97        .subcommand(commands::templates::command())
98}
99
100/// Delegates parsed subcommands to their respective modules, ensuring the Lifecycle
101/// Command Surface stays thin and predictable. Unknown subcommands map to `EX_USAGE` so
102/// callers receive actionable feedback.
103fn dispatch(
104    session: &CliSession,
105    matches: &ArgMatches,
106) -> Result<commands::CommandResult, CliError> {
107    match matches.subcommand() {
108        Some(("status", sub)) => commands::status::run(session, sub),
109        Some(("spec", sub)) => commands::spec::run(session, sub),
110        Some(("impl", sub)) => commands::implementation::run(session, sub),
111        Some(("scratch", sub)) => commands::scratch::run(session, sub),
112        Some(("template", sub)) => commands::templates::run(session, sub),
113        _ => Err(CliError::new("missing command", ExitStatus::Usage)),
114    }
115}