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::Policy(args) => run_policy(repo_root, args),
9 Command::Advise(args) => run_advise(repo_root, *args),
10 Command::Check(args) => run_check(repo_root, args),
11 Command::Compare(args) => run_compare(repo_root, args),
12 Command::Baseline(args) => run_baseline(repo_root, args),
13 Command::Report(args) => run_report(repo_root, args),
14 Command::Sarif(args) => run_sarif(repo_root, args),
15 Command::Health(args) => run_health(repo_root, args),
16 Command::Config(args) => run_config(repo_root, args),
17 Command::Doctor(args) => run_doctor(repo_root, args),
18 Command::List(args) => run_list(repo_root, args),
19 Command::Prune(args) => run_prune(repo_root, args),
20 Command::Cache(args) => run_cache(repo_root, args),
21 Command::Completions(args) => run_completions(args),
22 Command::Man(args) => run_man(args),
23 Command::Reference(args) => run_reference(args),
24 Command::Html(args) => run_html(repo_root, args),
25 Command::Version => {
26 println!("{PROJECT_NAME} {VERSION}");
27 Ok(0)
28 }
29 Command::BuildInfo(args) => {
30 match args.format {
31 BuildInfoFormat::Json => {
32 println!("{}", serde_json::to_string_pretty(&build_info::current())?)
33 }
34 }
35 Ok(0)
36 }
37 Command::Schema(args) => {
38 let rendered = match args.contract {
39 SchemaContract::Report => render_json(&report::schema())?,
40 SchemaContract::Config => render_json(&config::schema())?,
41 contract => {
42 let source = match contract {
43 SchemaContract::Compare => include_str!("../../schemas/compare-1.json"),
44 SchemaContract::Init => include_str!("../../schemas/init-1.json"),
45 SchemaContract::Explain => include_str!("../../schemas/explain-2.json"),
46 SchemaContract::Plan => include_str!("../../schemas/plan-2.json"),
47 SchemaContract::Sarif => include_str!("../../schemas/sarif-1.json"),
48 SchemaContract::Health => include_str!("../../schemas/health-1.json"),
49 SchemaContract::Check => include_str!("../../schemas/check-1.json"),
50 SchemaContract::Doctor => include_str!("../../schemas/doctor-1.json"),
51 SchemaContract::BuildInfo => include_str!("../../schemas/build-info-2.json"),
52 SchemaContract::ReleaseManifest => {
53 include_str!("../../schemas/release-manifest-3.json")
54 }
55 SchemaContract::List => include_str!("../../schemas/list-1.json"),
56 SchemaContract::Show => include_str!("../../schemas/show-1.json"),
57 SchemaContract::PromptManifest => {
58 include_str!("../../schemas/prompt-manifest-1.json")
59 }
60 SchemaContract::Error => include_str!("../../schemas/error-1.json"),
61 SchemaContract::FindEstimate => {
62 include_str!("../../schemas/find-estimate-1.json")
63 }
64 SchemaContract::CacheStatus => {
65 include_str!("../../schemas/cache-status-1.json")
66 }
67 SchemaContract::CachePrune => {
68 include_str!("../../schemas/cache-prune-1.json")
69 }
70 SchemaContract::Baseline => include_str!("../../schemas/baseline-1.json"),
71 SchemaContract::Prune => include_str!("../../schemas/prune-1.json"),
72 SchemaContract::CompareNdjson => {
73 include_str!("../../schemas/compare-ndjson-1.json")
74 }
75 SchemaContract::PolicyPack => {
76 include_str!("../../schemas/policy-pack-1.json")
77 }
78 SchemaContract::PolicyLock => {
79 include_str!("../../schemas/policy-lock-1.json")
80 }
81 SchemaContract::AdviceInput => {
82 include_str!("../../schemas/advice-input-1.json")
83 }
84 SchemaContract::AdviceResponse => {
85 include_str!("../../schemas/advice-response-1.json")
86 }
87 SchemaContract::Advice => include_str!("../../schemas/advice-1.json"),
88 SchemaContract::AdvisorCorpus => {
89 include_str!("../../schemas/advisor-corpus-1.json")
90 }
91 SchemaContract::AdvisorRatings => {
92 include_str!("../../schemas/advisor-ratings-1.json")
93 }
94 SchemaContract::AdvisorRatingsV2 => {
95 include_str!("../../schemas/advisor-ratings-2.json")
96 }
97 SchemaContract::AdvisorReviewArtifact => {
98 include_str!("../../schemas/advisor-review-artifact-1.json")
99 }
100 SchemaContract::AdvisorReviewManifest => {
101 include_str!("../../schemas/advisor-review-manifest-1.json")
102 }
103 SchemaContract::AdvisorOperationReceipt => {
104 include_str!("../../schemas/advisor-operation-receipt-1.json")
105 }
106 SchemaContract::AdvisorThresholds => {
107 include_str!("../../schemas/advisor-thresholds-1.json")
108 }
109 SchemaContract::AdvisorBenchmark => {
110 include_str!("../../schemas/advisor-benchmark-1.json")
111 }
112 SchemaContract::AdvisorCapacity => {
113 include_str!("../../schemas/advisor-capacity-1.json")
114 }
115 SchemaContract::Report | SchemaContract::Config => unreachable!(),
116 };
117 let value: Value = serde_json::from_str(source)?;
118 render_json(&value)?
119 }
120 };
121 write_generated_output(args.output.as_deref(), rendered.as_bytes())?;
122 Ok(0)
123 }
124 }
125}
126
127fn command_requires_repository(command: &Command) -> bool {
128 match command {
129 Command::Completions(_)
130 | Command::Man(_)
131 | Command::Reference(_)
132 | Command::Version
133 | Command::BuildInfo(_)
134 | Command::Schema(_)
135 | Command::Report(_)
136 | Command::Config(ConfigArgs {
137 command: ConfigCommand::Schema,
138 }) => false,
139 Command::Show(args) => args.report.is_none() || args.require_current,
140 Command::Compare(args) => args.base_ref.is_some() || args.baseline.is_some(),
141 Command::Baseline(_) => true,
142 Command::Policy(_) => true,
143 Command::Advise(_) => true,
144 Command::Explain(args) => {
145 args.report.is_none() || args.include_repository_context || args.require_current
146 }
147 Command::Plan(args) => {
148 args.report.is_none() || args.include_repository_context || args.require_current
149 }
150 Command::Check(args) => args.report.is_none() || args.require_current,
151 Command::Sarif(args) => args.report.is_none() || args.require_current,
152 Command::Health(args) => args.report.is_none() || args.require_current,
153 Command::Html(args) => args.report.is_none() || args.require_current,
154 Command::List(args) => {
155 let output = match &args.command {
156 ListCommand::PolicyFailures(args)
157 | ListCommand::Interventions(args)
158 | ListCommand::Observations(args)
159 | ListCommand::HealthFindings(args)
160 | ListCommand::Findings(args) => &args.output,
161 ListCommand::Relationships(args) => &args.output,
162 ListCommand::Clusters(args) => &args.output,
163 ListCommand::Profiles(args) => &args.output,
164 };
165 output.report.is_none() || output.require_current
166 }
167 _ => true,
168 }
169}
170
171pub fn run() -> i32 {
172 let raw_args = std::env::args_os().collect::<Vec<_>>();
173 if raw_args.len() == 1 {
174 println!(
175 "Git Slop finds repository surfaces that cost too much context.\n\n\
176 QUICK START\n git slop find Run a safe, cacheable first scan\n \
177 git slop health Review the current health snapshot\n \
178 git slop list interventions Review bounded maintenance candidates\n \
179 git slop explain --top 5 Understand why candidates surfaced\n\n\
180 INSPECT\n show · explain · plan · list · html · doctor\n\n\
181 AUTOMATION\n check · compare · baseline · sarif\n\n\
182 ADOPTION AND STATE\n init · config · cache · prune\n\n\
183 POLICY-GUIDED ADVICE\n policy · advise\n\n\
184 GENERATE AND INFO\n completions · man · reference · schema · version · build-info\n\n\
185 Run `git slop --help` for every command and option."
186 );
187 return 0;
188 }
189 let requested_error_format = requested_error_format(&raw_args);
190 let parser_command = parser_command_name(&raw_args);
191 let cli = match Cli::try_parse_from(&raw_args) {
192 Ok(cli) => cli,
193 Err(error) => {
194 let code = error.exit_code();
195 if code == 0 || requested_error_format == ErrorFormat::Human {
196 let _ = error.print();
197 } else {
198 let classified =
199 ClassifiedError::new(ErrorKind::Contract, "parser_error", error.to_string())
200 .at("/arguments")
201 .with_details(json!({"clap_kind": format!("{:?}", error.kind())}));
202 render_runtime_error(
203 requested_error_format,
204 &classified,
205 parser_command.as_deref(),
206 );
207 }
208 return code;
209 }
210 };
211 let error_format = if cli.error_format == ErrorFormat::Human
212 && matches!(cli.command, Command::Init(InitArgs { format: InitFormat::Json, .. }))
213 {
214 ErrorFormat::Json
215 } else {
216 cli.error_format
217 };
218 let command_name = cli.command.name();
219 let repo_root = if cli.repo.is_some() || command_requires_repository(&cli.command) {
220 match git::resolve_repo_root_from(cli.repo.as_deref()) {
221 Ok(root) => root,
222 Err(error) => {
223 let requested_path = cli.repo.clone().or_else(|| std::env::current_dir().ok());
224 let requested_display = requested_path.as_deref().map_or_else(
225 || "the current directory".to_string(),
226 |path| path.display().to_string(),
227 );
228 render_runtime_error(
229 error_format,
230 &ClassifiedError::new(
231 ErrorKind::Repository,
232 "repository_not_found",
233 format!(
234 "Not inside a Git repository: {requested_display}. Run this command from a repository, or pass `--repo <PATH>`."
235 ),
236 )
237 .at("/repo")
238 .with_details(json!({"path": requested_path, "cause": format!("{error:#}")})),
239 Some(command_name),
240 );
241 return 3;
242 }
243 }
244 } else {
245 PathBuf::new()
246 };
247 match execute(&repo_root, cli.command) {
248 Ok(code) => code,
249 Err(error) => {
250 let classified = error.downcast_ref::<ClassifiedError>();
251 let fallback;
252 let classified = if let Some(classified) = classified {
253 classified
254 } else {
255 fallback = if error.downcast_ref::<std::io::Error>().is_some() {
256 ClassifiedError::new(ErrorKind::Io, "io_failure", format!("{error:#}"))
257 } else {
258 ClassifiedError::new(
259 ErrorKind::Repository,
260 "operation_failed",
261 format!("{error:#}"),
262 )
263 };
264 &fallback
265 };
266 render_runtime_error(error_format, classified, Some(command_name));
267 classified.kind.exit_code()
268 }
269 }
270}
271
272fn requested_error_format(args: &[std::ffi::OsString]) -> ErrorFormat {
273 let explicit = args.iter()
274 .filter_map(|arg| arg.to_str())
275 .enumerate()
276 .find_map(|(index, arg)| {
277 if arg == "--error-format" {
278 args.get(index + 1).and_then(|value| value.to_str())
279 } else {
280 arg.strip_prefix("--error-format=")
281 }
282 })
283 .is_some_and(|value| value == "json");
284 let init_json = args.iter().filter_map(|arg| arg.to_str()).any(|arg| arg == "init")
285 && args
286 .iter()
287 .filter_map(|arg| arg.to_str())
288 .enumerate()
289 .any(|(index, arg)| {
290 arg == "--format=json"
291 || (arg == "--format"
292 && args.get(index + 1).and_then(|value| value.to_str()) == Some("json"))
293 });
294 if explicit || init_json {
295 ErrorFormat::Json
296 } else {
297 ErrorFormat::Human
298 }
299}
300
301fn parser_command_name(args: &[std::ffi::OsString]) -> Option<String> {
302 const COMMANDS: &[&str] = &[
303 "init",
304 "find",
305 "show",
306 "explain",
307 "plan",
308 "policy",
309 "advise",
310 "check",
311 "compare",
312 "baseline",
313 "report",
314 "sarif",
315 "health",
316 "config",
317 "doctor",
318 "list",
319 "prune",
320 "cache",
321 "completions",
322 "man",
323 "reference",
324 "html",
325 "version",
326 "build-info",
327 "schema",
328 ];
329 args.iter()
330 .skip(1)
331 .filter_map(|arg| arg.to_str())
332 .find(|arg| COMMANDS.contains(arg))
333 .map(str::to_string)
334}
335
336fn render_runtime_error(format: ErrorFormat, error: &ClassifiedError, command: Option<&str>) {
337 match format {
338 ErrorFormat::Human => eprintln!("{}", error.message),
339 ErrorFormat::Json => eprintln!(
340 "{}",
341 serde_json::to_string(&json!({
342 "schema_version": 1,
343 "error": {
344 "kind": error.kind,
345 "code": error.code,
346 "pointer": error.pointer,
347 "message": error.message,
348 "details": error.details,
349 "command": command,
350 "exit_code": error.kind.exit_code()
351 }
352 }))
353 .unwrap_or_else(|_| {
354 "{\"schema_version\":1,\"error\":{\"code\":\"serialization_failed\"}}".to_string()
355 })
356 ),
357 }
358}