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::PolicyFailures(args)
112 | ListCommand::Interventions(args)
113 | ListCommand::Observations(args)
114 | ListCommand::HealthFindings(args)
115 | ListCommand::Findings(args) => &args.output,
116 ListCommand::Relationships(args) => &args.output,
117 ListCommand::Clusters(args) => &args.output,
118 ListCommand::Profiles(args) => &args.output,
119 };
120 output.report.is_none() || output.require_current
121 }
122 _ => true,
123 }
124}
125
126pub fn run() -> i32 {
127 let raw_args = std::env::args_os().collect::<Vec<_>>();
128 if raw_args.len() == 1 {
129 let mut command = Cli::command();
130 let _ = command.print_help();
131 println!();
132 return 0;
133 }
134 let requested_error_format = requested_error_format(&raw_args);
135 let parser_command = parser_command_name(&raw_args);
136 let cli = match Cli::try_parse_from(&raw_args) {
137 Ok(cli) => cli,
138 Err(error) => {
139 let code = error.exit_code();
140 if code == 0 || requested_error_format == ErrorFormat::Human {
141 let _ = error.print();
142 } else {
143 let classified =
144 ClassifiedError::new(ErrorKind::Contract, "parser_error", error.to_string())
145 .at("/arguments")
146 .with_details(json!({"clap_kind": format!("{:?}", error.kind())}));
147 render_runtime_error(
148 requested_error_format,
149 &classified,
150 parser_command.as_deref(),
151 );
152 }
153 return code;
154 }
155 };
156 let error_format = cli.error_format;
157 let command_name = cli.command.name();
158 let repo_root = if command_requires_repository(&cli.command) {
159 match git::resolve_repo_root_from(cli.repo.as_deref()) {
160 Ok(root) => root,
161 Err(error) => {
162 let requested_path = cli.repo.clone().or_else(|| std::env::current_dir().ok());
163 let requested_display = requested_path.as_deref().map_or_else(
164 || "the current directory".to_string(),
165 |path| path.display().to_string(),
166 );
167 render_runtime_error(
168 error_format,
169 &ClassifiedError::new(
170 ErrorKind::Repository,
171 "repository_not_found",
172 format!(
173 "Not inside a Git repository: {requested_display}. Run this command from a repository, or pass `--repo <PATH>`."
174 ),
175 )
176 .at("/repo")
177 .with_details(json!({"path": requested_path, "cause": format!("{error:#}")})),
178 Some(command_name),
179 );
180 return 3;
181 }
182 }
183 } else {
184 PathBuf::new()
185 };
186 match execute(&repo_root, cli.command) {
187 Ok(code) => code,
188 Err(error) => {
189 let classified = error.downcast_ref::<ClassifiedError>();
190 let fallback;
191 let classified = if let Some(classified) = classified {
192 classified
193 } else {
194 fallback = if error.downcast_ref::<std::io::Error>().is_some() {
195 ClassifiedError::new(ErrorKind::Io, "io_failure", format!("{error:#}"))
196 } else {
197 ClassifiedError::new(
198 ErrorKind::Repository,
199 "operation_failed",
200 format!("{error:#}"),
201 )
202 };
203 &fallback
204 };
205 render_runtime_error(error_format, classified, Some(command_name));
206 classified.kind.exit_code()
207 }
208 }
209}
210
211fn requested_error_format(args: &[std::ffi::OsString]) -> ErrorFormat {
212 args.iter()
213 .filter_map(|arg| arg.to_str())
214 .enumerate()
215 .find_map(|(index, arg)| {
216 if arg == "--error-format" {
217 args.get(index + 1).and_then(|value| value.to_str())
218 } else {
219 arg.strip_prefix("--error-format=")
220 }
221 })
222 .filter(|value| *value == "json")
223 .map_or(ErrorFormat::Human, |_| ErrorFormat::Json)
224}
225
226fn parser_command_name(args: &[std::ffi::OsString]) -> Option<String> {
227 const COMMANDS: &[&str] = &[
228 "init",
229 "find",
230 "show",
231 "explain",
232 "plan",
233 "check",
234 "compare",
235 "baseline",
236 "report",
237 "sarif",
238 "health",
239 "config",
240 "doctor",
241 "list",
242 "prune",
243 "cache",
244 "completions",
245 "man",
246 "reference",
247 "html",
248 "version",
249 "build-info",
250 "schema",
251 ];
252 args.iter()
253 .skip(1)
254 .filter_map(|arg| arg.to_str())
255 .find(|arg| COMMANDS.contains(arg))
256 .map(str::to_string)
257}
258
259fn render_runtime_error(format: ErrorFormat, error: &ClassifiedError, command: Option<&str>) {
260 match format {
261 ErrorFormat::Human => eprintln!("{}", error.message),
262 ErrorFormat::Json => eprintln!(
263 "{}",
264 serde_json::to_string(&json!({
265 "schema_version": 1,
266 "error": {
267 "kind": error.kind,
268 "code": error.code,
269 "pointer": error.pointer,
270 "message": error.message,
271 "details": error.details,
272 "command": command,
273 "exit_code": error.kind.exit_code()
274 }
275 }))
276 .unwrap_or_else(|_| {
277 "{\"schema_version\":1,\"error\":{\"code\":\"serialization_failed\"}}".to_string()
278 })
279 ),
280 }
281}