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