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
48 let result = dispatch(&matches, workspace_override, verbosity)?;
49 emit_result(result, output)
50}
51
52fn init_tracing() {
53 let _ = tracing_subscriber::fmt()
54 .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
55 .try_init();
56}
57
58fn build_cli() -> Command {
62 Command::new(NAME)
63 .about("SpecMan CLI")
64 .arg(
65 Arg::new("workspace")
66 .long("workspace")
67 .value_name("PATH")
68 .help("Specify the workspace root. Defaults to the nearest ancestor with a .specman folder."),
69 )
70 .arg(
71 Arg::new("json")
72 .long("json")
73 .action(ArgAction::SetTrue)
74 .help("Emit newline-delimited JSON instead of human-readable text."),
75 )
76 .arg(
77 Arg::new("verbose")
78 .long("verbose")
79 .action(ArgAction::SetTrue)
80 .help("Emit additional logging about template locators, workspace paths, and adapters."),
81 )
82 .subcommand_required(true)
83 .subcommand(commands::init::command())
84 .subcommand(commands::status::command())
85 .subcommand(commands::spec::command())
86 .subcommand(commands::implementation::command())
87 .subcommand(commands::scratch::command())
88 .subcommand(commands::templates::command())
89}
90
91fn dispatch(
95 matches: &ArgMatches,
96 workspace_override: Option<String>,
97 verbosity: Verbosity,
98) -> Result<commands::CommandResult, CliError> {
99 match matches.subcommand() {
100 Some(("init", sub)) => commands::init::run(workspace_override, sub),
101 _ => {
102 let session = CliSession::bootstrap(workspace_override, verbosity)?;
103 if session.verbosity.verbose {
104 tracing::info!(
105 workspace = %session.workspace_paths.root().display(),
106 spec_dir = %session.workspace_paths.spec_dir().display(),
107 impl_dir = %session.workspace_paths.impl_dir().display(),
108 scratch_dir = %session.workspace_paths.scratchpad_dir().display(),
109 "resolved workspace context"
110 );
111 }
112 dispatch_with_session(&session, matches)
113 }
114 }
115}
116
117fn dispatch_with_session(
118 session: &CliSession,
119 matches: &ArgMatches,
120) -> Result<commands::CommandResult, CliError> {
121 match matches.subcommand() {
122 Some(("status", sub)) => commands::status::run(session, sub),
123 Some(("spec", sub)) => commands::spec::run(session, sub),
124 Some(("impl", sub)) => commands::implementation::run(session, sub),
125 Some(("scratch", sub)) => commands::scratch::run(session, sub),
126 Some(("template", sub)) => commands::templates::run(session, sub),
127 _ => Err(CliError::new("missing command", ExitStatus::Usage)),
128 }
129}