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
25pub 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
68fn 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
100fn 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}