1fn execute(repo_root: &Path, command: Command) -> Result<i32> {
2 match command {
3 Command::Init(args) => run_init(repo_root, args),
4 Command::Find(args) => run_find(repo_root, args),
5 Command::Show(args) => run_show(repo_root, args),
6 Command::Explain(args) => run_explain(repo_root, args),
7 Command::Plan(args) => run_plan(repo_root, args),
8 Command::Check(args) => run_check(repo_root, args),
9 Command::Compare(args) => run_compare(repo_root, args),
10 Command::Baseline(args) => run_baseline(repo_root, args),
11 Command::Report(args) => run_report(args),
12 Command::Sarif(args) => run_sarif(repo_root, args),
13 Command::Health(args) => run_health(repo_root, args),
14 Command::Config(args) => run_config(repo_root, args),
15 Command::Doctor(args) => run_doctor(repo_root, args),
16 Command::List(args) => run_list(repo_root, args),
17 Command::Prune(args) => run_prune(repo_root, args),
18 Command::Cache(args) => run_cache(repo_root, args),
19 Command::Completions(args) => run_completions(args),
20 Command::Man(args) => run_man(args),
21 Command::Reference(args) => run_reference(args),
22 Command::Html(args) => run_html(repo_root, args),
23 Command::Version => {
24 println!("{PROJECT_NAME} {VERSION}");
25 Ok(0)
26 }
27 Command::BuildInfo(args) => {
28 match args.format {
29 BuildInfoFormat::Json => {
30 println!("{}", serde_json::to_string_pretty(&build_info::current())?)
31 }
32 }
33 Ok(0)
34 }
35 Command::Schema(args) => {
36 let rendered = match args.contract {
37 SchemaContract::Report => render_json(&report::schema())?,
38 SchemaContract::Config => render_json(&config::schema())?,
39 contract => {
40 let source = match contract {
41 SchemaContract::Compare => include_str!("../../schemas/compare-1.json"),
42 SchemaContract::Explain => include_str!("../../schemas/explain-2.json"),
43 SchemaContract::Plan => include_str!("../../schemas/plan-2.json"),
44 SchemaContract::Sarif => include_str!("../../schemas/sarif-1.json"),
45 SchemaContract::Health => include_str!("../../schemas/health-1.json"),
46 SchemaContract::Check => include_str!("../../schemas/check-1.json"),
47 SchemaContract::Doctor => include_str!("../../schemas/doctor-1.json"),
48 SchemaContract::BuildInfo => include_str!("../../schemas/build-info-2.json"),
49 SchemaContract::ReleaseManifest => {
50 include_str!("../../schemas/release-manifest-3.json")
51 }
52 SchemaContract::List => include_str!("../../schemas/list-1.json"),
53 SchemaContract::Show => include_str!("../../schemas/show-1.json"),
54 SchemaContract::PromptManifest => {
55 include_str!("../../schemas/prompt-manifest-1.json")
56 }
57 SchemaContract::Error => include_str!("../../schemas/error-1.json"),
58 SchemaContract::FindEstimate => {
59 include_str!("../../schemas/find-estimate-1.json")
60 }
61 SchemaContract::CacheStatus => {
62 include_str!("../../schemas/cache-status-1.json")
63 }
64 SchemaContract::CachePrune => {
65 include_str!("../../schemas/cache-prune-1.json")
66 }
67 SchemaContract::Baseline => include_str!("../../schemas/baseline-1.json"),
68 SchemaContract::Prune => include_str!("../../schemas/prune-1.json"),
69 SchemaContract::CompareNdjson => {
70 include_str!("../../schemas/compare-ndjson-1.json")
71 }
72 SchemaContract::Report | SchemaContract::Config => unreachable!(),
73 };
74 let value: Value = serde_json::from_str(source)?;
75 render_json(&value)?
76 }
77 };
78 write_generated_output(args.output.as_deref(), rendered.as_bytes())?;
79 Ok(0)
80 }
81 }
82}
83
84fn command_requires_repository(command: &Command) -> bool {
85 match command {
86 Command::Completions(_)
87 | Command::Man(_)
88 | Command::Reference(_)
89 | Command::Version
90 | Command::BuildInfo(_)
91 | Command::Schema(_)
92 | Command::Report(_)
93 | Command::Config(ConfigArgs {
94 command: ConfigCommand::Schema,
95 }) => false,
96 Command::Show(args) => args.report.is_none() || args.require_current,
97 Command::Compare(args) => args.base_ref.is_some() || args.baseline.is_some(),
98 Command::Baseline(_) => true,
99 Command::Explain(args) => {
100 args.report.is_none() || args.include_repository_context || args.require_current
101 }
102 Command::Plan(args) => {
103 args.report.is_none() || args.include_repository_context || args.require_current
104 }
105 Command::Check(args) => args.report.is_none() || args.require_current,
106 Command::Sarif(args) => args.report.is_none() || args.require_current,
107 Command::Health(args) => args.report.is_none() || args.require_current,
108 Command::Html(args) => args.report.is_none() || args.require_current,
109 Command::List(args) => {
110 let output = match &args.command {
111 ListCommand::Findings(args) => &args.output,
112 ListCommand::Relationships(args) => &args.output,
113 ListCommand::Clusters(args) => &args.output,
114 ListCommand::Profiles(args) => &args.output,
115 };
116 output.report.is_none() || output.require_current
117 }
118 _ => true,
119 }
120}
121
122pub fn run() -> i32 {
123 let raw_args = std::env::args_os().collect::<Vec<_>>();
124 let requested_error_format = requested_error_format(&raw_args);
125 let parser_command = parser_command_name(&raw_args);
126 let cli = match Cli::try_parse_from(&raw_args) {
127 Ok(cli) => cli,
128 Err(error) => {
129 let code = error.exit_code();
130 if code == 0 || requested_error_format == ErrorFormat::Human {
131 let _ = error.print();
132 } else {
133 let classified =
134 ClassifiedError::new(ErrorKind::Contract, "parser_error", error.to_string())
135 .at("/arguments")
136 .with_details(json!({"clap_kind": format!("{:?}", error.kind())}));
137 render_runtime_error(
138 requested_error_format,
139 &classified,
140 parser_command.as_deref(),
141 );
142 }
143 return code;
144 }
145 };
146 let error_format = cli.error_format;
147 let command_name = cli.command.name();
148 let repo_root = if command_requires_repository(&cli.command) {
149 match git::resolve_repo_root_from(cli.repo.as_deref()) {
150 Ok(root) => root,
151 Err(error) => {
152 let requested_path = cli.repo.clone().or_else(|| std::env::current_dir().ok());
153 let requested_display = requested_path.as_deref().map_or_else(
154 || "the current directory".to_string(),
155 |path| path.display().to_string(),
156 );
157 render_runtime_error(
158 error_format,
159 &ClassifiedError::new(
160 ErrorKind::Repository,
161 "repository_not_found",
162 format!(
163 "Not inside a Git repository: {requested_display}. Run this command from a repository, or pass `--repo <PATH>`."
164 ),
165 )
166 .at("/repo")
167 .with_details(json!({"path": requested_path, "cause": format!("{error:#}")})),
168 Some(command_name),
169 );
170 return 3;
171 }
172 }
173 } else {
174 PathBuf::new()
175 };
176 match execute(&repo_root, cli.command) {
177 Ok(code) => code,
178 Err(error) => {
179 let classified = error.downcast_ref::<ClassifiedError>();
180 let fallback;
181 let classified = if let Some(classified) = classified {
182 classified
183 } else {
184 fallback = if error.downcast_ref::<std::io::Error>().is_some() {
185 ClassifiedError::new(ErrorKind::Io, "io_failure", format!("{error:#}"))
186 } else {
187 ClassifiedError::new(
188 ErrorKind::Repository,
189 "operation_failed",
190 format!("{error:#}"),
191 )
192 };
193 &fallback
194 };
195 render_runtime_error(error_format, classified, Some(command_name));
196 classified.kind.exit_code()
197 }
198 }
199}
200
201fn requested_error_format(args: &[std::ffi::OsString]) -> ErrorFormat {
202 args.iter()
203 .filter_map(|arg| arg.to_str())
204 .enumerate()
205 .find_map(|(index, arg)| {
206 if arg == "--error-format" {
207 args.get(index + 1).and_then(|value| value.to_str())
208 } else {
209 arg.strip_prefix("--error-format=")
210 }
211 })
212 .filter(|value| *value == "json")
213 .map_or(ErrorFormat::Human, |_| ErrorFormat::Json)
214}
215
216fn parser_command_name(args: &[std::ffi::OsString]) -> Option<String> {
217 const COMMANDS: &[&str] = &[
218 "init",
219 "find",
220 "show",
221 "explain",
222 "plan",
223 "check",
224 "compare",
225 "baseline",
226 "report",
227 "sarif",
228 "health",
229 "config",
230 "doctor",
231 "list",
232 "prune",
233 "cache",
234 "completions",
235 "man",
236 "reference",
237 "html",
238 "version",
239 "build-info",
240 "schema",
241 ];
242 args.iter()
243 .skip(1)
244 .filter_map(|arg| arg.to_str())
245 .find(|arg| COMMANDS.contains(arg))
246 .map(str::to_string)
247}
248
249fn render_runtime_error(format: ErrorFormat, error: &ClassifiedError, command: Option<&str>) {
250 match format {
251 ErrorFormat::Human => eprintln!("{}", error.message),
252 ErrorFormat::Json => eprintln!(
253 "{}",
254 serde_json::to_string(&json!({
255 "schema_version": 1,
256 "error": {
257 "kind": error.kind,
258 "code": error.code,
259 "pointer": error.pointer,
260 "message": error.message,
261 "details": error.details,
262 "command": command,
263 "exit_code": error.kind.exit_code()
264 }
265 }))
266 .unwrap_or_else(|_| {
267 "{\"schema_version\":1,\"error\":{\"code\":\"serialization_failed\"}}".to_string()
268 })
269 ),
270 }
271}