1use std::fs;
2use std::io::IsTerminal;
3use std::path::{Component, Path, PathBuf};
4
5use anyhow::{Context, Result};
6use clap::{ArgGroup, Args, CommandFactory, Parser, Subcommand, ValueEnum};
7use clap_complete::{Shell, generate};
8use serde_json::{Value, json};
9use sha2::Digest;
10
11use crate::build_info;
12use crate::config;
13use crate::error::{ClassifiedError, ErrorKind};
14use crate::health;
15use crate::report;
16use crate::report_ops::{
17 ExplainSelector, PlanSelector, PromptPackOptions, compare_payload_with_policy, explain_payload,
18 failing_records_in, health_json_payload, plan_payload, render_compare_text,
19 render_explain_text, render_github_annotations, render_json, render_plan_text,
20 render_show_text, sarif_payload, show_payload, write_prompt_pack,
21};
22use crate::{PROJECT_NAME, VERSION, analyze, git};
23
24#[derive(Debug, Parser)]
25#[command(
26 name = "git-slop",
27 about = "Find the files that cost too much context.",
28 version = VERSION
29)]
30struct Cli {
31 #[arg(long, global = true, value_name = "PATH")]
33 repo: Option<PathBuf>,
34 #[arg(long, global = true, value_enum, default_value_t = ErrorFormat::Human)]
36 error_format: ErrorFormat,
37 #[command(subcommand)]
38 command: Command,
39}
40
41#[derive(Debug, Subcommand)]
42enum Command {
43 Init(InitArgs),
45 Find(FindArgs),
47 Show(ShowArgs),
49 Explain(ExplainArgs),
51 Plan(PlanArgs),
53 Check(CheckArgs),
55 Compare(CompareArgs),
57 Baseline(BaselineArgs),
59 Report(ReportArgs),
61 Sarif(SarifArgs),
63 Health(HealthArgs),
65 Config(ConfigArgs),
67 Doctor(DoctorArgs),
69 List(ListArgs),
71 Prune(PruneArgs),
73 Cache(CacheArgs),
75 Completions(CompletionsArgs),
77 Man(ManArgs),
79 Reference(ReferenceArgs),
81 Html(HtmlArgs),
83 Version,
85 BuildInfo(BuildInfoArgs),
87 Schema(SchemaArgs),
89}
90
91impl Command {
92 fn name(&self) -> &'static str {
93 match self {
94 Self::Init(_) => "init",
95 Self::Find(_) => "find",
96 Self::Show(_) => "show",
97 Self::Explain(_) => "explain",
98 Self::Plan(_) => "plan",
99 Self::Check(_) => "check",
100 Self::Compare(_) => "compare",
101 Self::Baseline(_) => "baseline",
102 Self::Report(_) => "report",
103 Self::Sarif(_) => "sarif",
104 Self::Health(_) => "health",
105 Self::Config(_) => "config",
106 Self::Doctor(_) => "doctor",
107 Self::List(_) => "list",
108 Self::Prune(_) => "prune",
109 Self::Cache(_) => "cache",
110 Self::Completions(_) => "completions",
111 Self::Man(_) => "man",
112 Self::Reference(_) => "reference",
113 Self::Html(_) => "html",
114 Self::Version => "version",
115 Self::BuildInfo(_) => "build-info",
116 Self::Schema(_) => "schema",
117 }
118 }
119}
120
121#[derive(Debug, Args)]
122struct SchemaArgs {
123 #[arg(value_enum)]
125 contract: SchemaContract,
126 #[arg(long)]
128 output: Option<PathBuf>,
129}
130
131#[derive(Debug, Clone, Copy, ValueEnum)]
132enum SchemaContract {
133 Report,
134 Config,
135 Compare,
136 Explain,
137 Plan,
138 Sarif,
139 Health,
140 Check,
141 Doctor,
142 BuildInfo,
143 List,
144 Show,
145 PromptManifest,
146 Error,
147 FindEstimate,
148 CacheStatus,
149 CachePrune,
150 Baseline,
151 Prune,
152 CompareNdjson,
153}
154
155#[derive(Debug, Args)]
156struct FindArgs {
157 #[arg(long)]
159 allow_shallow: bool,
160 #[arg(long)]
162 scope: Option<String>,
163 #[arg(long)]
165 allow_empty_scope: bool,
166 #[arg(long)]
168 quiet: bool,
169 #[arg(long)]
171 no_progress: bool,
172 #[arg(long, value_name = "PATH")]
174 state_dir: Option<PathBuf>,
175 #[arg(long, value_name = "PATH")]
177 output_dir: Option<PathBuf>,
178 #[arg(long)]
180 no_cache: bool,
181 #[arg(long)]
183 allow_degraded: bool,
184 #[arg(long, value_name = "RFC3339")]
186 as_of: Option<String>,
187 #[arg(long, value_enum, default_value_t = ReportProfile::Standard)]
189 report_profile: ReportProfile,
190 #[arg(long, value_enum, default_value_t = ReportCompression::None)]
192 compression: ReportCompression,
193 #[arg(long)]
195 estimate_only: bool,
196}
197
198#[derive(Debug, Clone, Copy, ValueEnum)]
199enum ReportProfile {
200 Compact,
201 Standard,
202 FullEvidence,
203}
204
205impl ReportProfile {
206 fn as_str(self) -> &'static str {
207 match self {
208 Self::Compact => "compact",
209 Self::Standard => "standard",
210 Self::FullEvidence => "full_evidence",
211 }
212 }
213}
214
215#[derive(Debug, Clone, Copy, ValueEnum)]
216enum ReportCompression {
217 None,
218 Gzip,
219 Zstd,
220}
221
222impl ReportCompression {
223 fn as_str(self) -> &'static str {
224 match self {
225 Self::None => "none",
226 Self::Gzip => "gzip",
227 Self::Zstd => "zstd",
228 }
229 }
230}
231
232#[derive(Debug, Args)]
233struct BuildInfoArgs {
234 #[arg(long, value_enum, default_value_t = BuildInfoFormat::Json)]
236 format: BuildInfoFormat,
237}
238
239#[derive(Debug, Args)]
240struct InitArgs {
241 #[arg(long)]
243 force: bool,
244}
245
246#[derive(Debug, Args)]
247struct ShowArgs {
248 target_path: String,
250 #[arg(long)]
252 report: Option<PathBuf>,
253 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
255 format: DisplayFormat,
256}
257
258#[derive(Debug, Args)]
259#[command(group(
260 ArgGroup::new("selector")
261 .args(["path", "cluster", "relationship", "top"])
262 .multiple(false)
263))]
264struct ExplainArgs {
265 #[arg(long)]
267 report: Option<PathBuf>,
268 #[arg(long)]
270 path: Option<String>,
271 #[arg(long)]
273 cluster: Option<String>,
274 #[arg(long)]
276 relationship: Option<String>,
277 #[arg(long)]
279 top: Option<i64>,
280 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
282 format: DisplayFormat,
283 #[arg(long)]
285 prompt_pack: Option<PathBuf>,
286 #[arg(long, requires = "prompt_pack")]
288 force: bool,
289 #[arg(long, requires = "prompt_pack")]
291 include_repository_context: bool,
292 #[arg(long, default_value_t = 2048, requires = "include_repository_context")]
294 excerpt_bytes: usize,
295 #[arg(long, requires = "prompt_pack")]
297 include_local_paths: bool,
298}
299
300#[derive(Debug, Args)]
301#[command(group(
302 ArgGroup::new("selector")
303 .args(["path", "cluster", "relationship"])
304 .required(true)
305 .multiple(false)
306))]
307struct PlanArgs {
308 #[arg(long)]
310 report: Option<PathBuf>,
311 #[arg(long)]
313 path: Option<String>,
314 #[arg(long)]
316 cluster: Option<String>,
317 #[arg(long)]
319 relationship: Option<String>,
320 #[arg(long, default_value_t = 3)]
322 max_slices: i64,
323 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
325 format: DisplayFormat,
326 #[arg(long)]
328 prompt_pack: Option<PathBuf>,
329 #[arg(long, requires = "prompt_pack")]
331 force: bool,
332 #[arg(long, requires = "prompt_pack")]
334 include_repository_context: bool,
335 #[arg(long, default_value_t = 2048, requires = "include_repository_context")]
337 excerpt_bytes: usize,
338 #[arg(long, requires = "prompt_pack")]
340 include_local_paths: bool,
341}
342
343#[derive(Debug, Args)]
344struct CheckArgs {
345 #[arg(long)]
347 report: Option<PathBuf>,
348 #[arg(long, value_enum)]
350 fail_on_context_band: Option<ContextBand>,
351 #[arg(long, value_enum)]
353 fail_on_slop_band: Option<SlopBand>,
354 #[arg(long, value_enum, default_value_t = CheckFormat::Text)]
356 format: CheckFormat,
357 #[arg(long)]
359 details: bool,
360 #[arg(long)]
362 include_folders: bool,
363 #[arg(long, default_value_t = 0, requires = "details")]
365 offset: usize,
366 #[arg(long, default_value_t = 1000, requires = "details")]
368 limit: usize,
369 #[arg(long)]
371 allow_incomplete_evidence: bool,
372 #[arg(long)]
374 evaluate_only: bool,
375}
376
377#[derive(Debug, Args)]
378struct CompareArgs {
379 #[arg(
381 long,
382 required_unless_present_any = ["base_ref", "baseline"],
383 conflicts_with_all = ["base_ref", "baseline"]
384 )]
385 base: Option<PathBuf>,
386 #[arg(long, conflicts_with_all = ["base", "baseline"])]
388 base_ref: Option<String>,
389 #[arg(long, conflicts_with_all = ["base", "base_ref"])]
391 baseline: Option<String>,
392 #[arg(long, default_value = ".slop/latest/report.json")]
394 head: PathBuf,
395 #[arg(long)]
397 scope: Option<String>,
398 #[arg(long)]
400 allow_shallow: bool,
401 #[arg(long)]
403 allow_incomplete_evidence: bool,
404 #[arg(long, default_value_t = 10)]
406 top: i64,
407 #[arg(long, value_enum, default_value_t = CompareFormat::Text)]
409 format: CompareFormat,
410 #[arg(long, value_enum, default_value_t = CompareDetail::Top)]
412 detail: CompareDetail,
413 #[arg(long, default_value_t = 0)]
415 offset: usize,
416 #[arg(long, default_value_t = 1000)]
418 limit: usize,
419 #[arg(long)]
421 force: bool,
422 #[arg(long)]
424 include_local_paths: bool,
425 #[arg(long)]
427 include_unchanged: bool,
428 #[arg(long, value_enum, default_value_t = PolicySource::Base)]
430 policy_from: PolicySource,
431 #[arg(long)]
433 fail_on_regression: bool,
434}
435
436#[derive(Debug, Args)]
437struct BaselineArgs {
438 #[command(subcommand)]
439 command: BaselineCommand,
440}
441
442#[derive(Debug, Subcommand)]
443enum BaselineCommand {
444 Ensure {
446 #[arg(long, default_value = "default")]
448 name: String,
449 #[arg(long)]
451 report: Option<PathBuf>,
452 #[arg(long)]
454 replace: bool,
455 #[arg(long)]
457 allow_dirty: bool,
458 #[arg(long)]
460 allow_incomplete_evidence: bool,
461 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
463 format: DisplayFormat,
464 },
465 Create {
467 #[arg(long, default_value = "default")]
469 name: String,
470 #[arg(long)]
472 report: Option<PathBuf>,
473 #[arg(long)]
475 force: bool,
476 #[arg(long)]
478 allow_dirty: bool,
479 #[arg(long)]
481 allow_incomplete_evidence: bool,
482 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
484 format: DisplayFormat,
485 },
486 Update {
488 #[arg(long, default_value = "default")]
490 name: String,
491 #[arg(long)]
493 report: Option<PathBuf>,
494 #[arg(long)]
496 allow_dirty: bool,
497 #[arg(long)]
499 allow_incomplete_evidence: bool,
500 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
502 format: DisplayFormat,
503 },
504 List {
506 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
508 format: DisplayFormat,
509 },
510 Inspect {
512 #[arg(long, default_value = "default")]
514 name: String,
515 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
517 format: DisplayFormat,
518 },
519 Validate {
521 #[arg(long, default_value = "default")]
523 name: String,
524 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
526 format: DisplayFormat,
527 },
528 Remove {
530 #[arg(long, default_value = "default")]
532 name: String,
533 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
535 format: DisplayFormat,
536 },
537}
538
539#[derive(Debug, Args)]
540struct ReportArgs {
541 #[command(subcommand)]
542 command: ReportCommand,
543}
544
545#[derive(Debug, Subcommand)]
546enum ReportCommand {
547 Validate {
549 #[arg(value_name = "REPORT_JSON")]
551 path: PathBuf,
552 #[arg(long)]
554 allow_legacy: bool,
555 },
556 Migrate {
558 #[arg(value_name = "REPORT_JSON")]
560 path: PathBuf,
561 #[arg(long, value_name = "PATH")]
563 output: PathBuf,
564 },
565 Schema,
567}
568
569#[derive(Debug, Args)]
570struct SarifArgs {
571 #[arg(long)]
573 report: Option<PathBuf>,
574 #[arg(long)]
576 top: Option<i64>,
577 #[arg(long)]
579 output: Option<PathBuf>,
580 #[arg(long)]
582 include_local_paths: bool,
583}
584
585#[derive(Debug, Args)]
586struct HealthArgs {
587 #[arg(long)]
589 report: Option<PathBuf>,
590 #[arg(long, value_enum, default_value_t = HealthFormat::Text)]
592 format: HealthFormat,
593 #[arg(long, default_value_t = 10)]
595 max_annotations: usize,
596}
597
598#[derive(Debug, Args)]
599struct ConfigArgs {
600 #[command(subcommand)]
601 command: ConfigCommand,
602}
603
604#[derive(Debug, Subcommand)]
605enum ConfigCommand {
606 Show {
608 #[arg(long)]
610 effective: bool,
611 },
612 Validate,
614 DiffDefaults,
616 Migrate,
618 Schema,
620}
621
622#[derive(Debug, Args)]
623struct DoctorArgs {
624 #[arg(long, num_args = 0..=1, default_missing_value = ".slop/diagnostic-bundle.json")]
626 bundle: Option<PathBuf>,
627 #[arg(long, value_enum, default_value_t = DoctorFormat::Text)]
629 format: DoctorFormat,
630 #[arg(long)]
632 scope: Option<String>,
633}
634
635#[derive(Debug, Clone, Copy, ValueEnum)]
636enum DoctorFormat {
637 Text,
638 Json,
639}
640
641#[derive(Debug, Args)]
642struct ListArgs {
643 #[command(subcommand)]
644 command: ListCommand,
645}
646
647#[derive(Debug, Subcommand)]
648enum ListCommand {
649 Findings(ListFilterArgs),
650 Relationships(ListFilterArgs),
651 Clusters(ListFilterArgs),
652 Profiles(ListFilterArgs),
653}
654
655#[derive(Debug, Args)]
656struct ListFilterArgs {
657 #[arg(long)]
659 report: Option<PathBuf>,
660 #[arg(long)]
662 path: Option<String>,
663 #[arg(long)]
665 profile: Option<String>,
666 #[arg(long)]
668 language: Option<String>,
669 #[arg(long, visible_alias = "class")]
671 classification: Option<String>,
672 #[arg(long)]
674 severity: Option<String>,
675 #[arg(long, default_value_t = 50)]
677 top: usize,
678 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
680 format: DisplayFormat,
681 #[arg(long)]
683 wide: bool,
684 #[arg(long)]
686 no_truncate: bool,
687}
688
689#[derive(Debug, Args)]
690struct PruneArgs {
691 #[arg(long)]
693 keep: Option<usize>,
694 #[arg(long)]
696 max_bytes: Option<u64>,
697 #[arg(long)]
699 dry_run: bool,
700 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
702 format: DisplayFormat,
703}
704
705#[derive(Debug, Args)]
706struct CacheArgs {
707 #[arg(long, value_name = "PATH", global = true)]
709 state_dir: Option<PathBuf>,
710 #[command(subcommand)]
711 command: CacheCommand,
712}
713
714#[derive(Debug, Subcommand)]
715enum CacheCommand {
716 Status {
717 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
719 format: DisplayFormat,
720 },
721 Prune {
722 #[arg(long, default_value_t = 10_000)]
724 max_entries: usize,
725 #[arg(long, default_value_t = 536_870_912)]
727 max_bytes: u64,
728 #[arg(long)]
730 dry_run: bool,
731 #[arg(long)]
733 compact: bool,
734 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
736 format: DisplayFormat,
737 },
738}
739
740#[derive(Debug, Args)]
741struct CompletionsArgs {
742 shell: CompletionShell,
744}
745
746#[derive(Debug, Args)]
747struct ManArgs {
748 #[arg(long)]
750 output: Option<PathBuf>,
751}
752
753#[derive(Debug, Args)]
754struct ReferenceArgs {
755 #[arg(long)]
757 output: Option<PathBuf>,
758}
759
760#[derive(Debug, Args)]
761struct HtmlArgs {
762 #[arg(long)]
764 report: Option<PathBuf>,
765 #[arg(long)]
767 output: Option<PathBuf>,
768 #[arg(long)]
770 include_local_paths: bool,
771}
772
773#[derive(Debug, Clone, Copy, ValueEnum)]
774enum CompletionShell {
775 Bash,
776 Zsh,
777 Fish,
778 Powershell,
779 Nushell,
780}
781
782#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
783enum DisplayFormat {
784 Text,
785 Json,
786 Yaml,
787}
788
789#[derive(Debug, Clone, Copy, ValueEnum)]
790enum CompareFormat {
791 Text,
792 Json,
793 Yaml,
794 Ndjson,
795}
796
797#[derive(Debug, Clone, Copy, ValueEnum)]
798enum CompareDetail {
799 Summary,
800 Top,
801 Full,
802}
803
804#[derive(Debug, Clone, Copy, ValueEnum)]
805enum HealthFormat {
806 Text,
807 Markdown,
808 Github,
809 Json,
810}
811
812#[derive(Debug, Clone, Copy, ValueEnum)]
813enum BuildInfoFormat {
814 Json,
815}
816
817#[derive(Debug, Clone, Copy, ValueEnum)]
818enum CheckFormat {
819 Text,
820 Json,
821 Github,
822}
823
824#[derive(Debug, Clone, Copy, ValueEnum)]
825enum PolicySource {
826 Base,
827 Head,
828}
829
830impl PolicySource {
831 fn as_str(self) -> &'static str {
832 match self {
833 Self::Base => "base",
834 Self::Head => "head",
835 }
836 }
837}
838
839#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
840enum ErrorFormat {
841 Human,
842 Json,
843}
844
845#[derive(Debug, Clone, Copy, ValueEnum)]
846enum ContextBand {
847 Compact,
848 Healthy,
849 Warning,
850 Critical,
851}
852
853impl ContextBand {
854 fn as_str(self) -> &'static str {
855 match self {
856 Self::Compact => "compact",
857 Self::Healthy => "healthy",
858 Self::Warning => "warning",
859 Self::Critical => "critical",
860 }
861 }
862}
863
864#[derive(Debug, Clone, Copy, ValueEnum)]
865enum SlopBand {
866 Low,
867 Moderate,
868 High,
869 Critical,
870}
871
872impl SlopBand {
873 fn as_str(self) -> &'static str {
874 match self {
875 Self::Low => "low",
876 Self::Moderate => "moderate",
877 Self::High => "high",
878 Self::Critical => "critical",
879 }
880 }
881}
882
883include!("cli/support.rs");
884include!("cli/analysis.rs");
885include!("cli/check.rs");
886include!("cli/baseline_compare.rs");
887include!("cli/reporting.rs");
888include!("cli/listing.rs");
889include!("cli/generation.rs");
890fn execute(repo_root: &Path, command: Command) -> Result<i32> {
891 match command {
892 Command::Init(args) => run_init(repo_root, args),
893 Command::Find(args) => run_find(repo_root, args),
894 Command::Show(args) => run_show(repo_root, args),
895 Command::Explain(args) => run_explain(repo_root, args),
896 Command::Plan(args) => run_plan(repo_root, args),
897 Command::Check(args) => run_check(repo_root, args),
898 Command::Compare(args) => run_compare(repo_root, args),
899 Command::Baseline(args) => run_baseline(repo_root, args),
900 Command::Report(args) => run_report(args),
901 Command::Sarif(args) => run_sarif(repo_root, args),
902 Command::Health(args) => run_health(repo_root, args),
903 Command::Config(args) => run_config(repo_root, args),
904 Command::Doctor(args) => run_doctor(repo_root, args),
905 Command::List(args) => run_list(repo_root, args),
906 Command::Prune(args) => run_prune(repo_root, args),
907 Command::Cache(args) => run_cache(repo_root, args),
908 Command::Completions(args) => run_completions(args),
909 Command::Man(args) => run_man(args),
910 Command::Reference(args) => run_reference(args),
911 Command::Html(args) => run_html(repo_root, args),
912 Command::Version => {
913 println!("{PROJECT_NAME} {VERSION}");
914 Ok(0)
915 }
916 Command::BuildInfo(args) => {
917 match args.format {
918 BuildInfoFormat::Json => {
919 println!("{}", serde_json::to_string_pretty(&build_info::current())?)
920 }
921 }
922 Ok(0)
923 }
924 Command::Schema(args) => {
925 let rendered = match args.contract {
926 SchemaContract::Report => render_json(&report::schema())?,
927 SchemaContract::Config => render_json(&config::schema())?,
928 contract => {
929 let source = match contract {
930 SchemaContract::Compare => include_str!("../schemas/compare-1.json"),
931 SchemaContract::Explain => include_str!("../schemas/explain-2.json"),
932 SchemaContract::Plan => include_str!("../schemas/plan-2.json"),
933 SchemaContract::Sarif => include_str!("../schemas/sarif-1.json"),
934 SchemaContract::Health => include_str!("../schemas/health-1.json"),
935 SchemaContract::Check => include_str!("../schemas/check-1.json"),
936 SchemaContract::Doctor => include_str!("../schemas/doctor-1.json"),
937 SchemaContract::BuildInfo => include_str!("../schemas/build-info-1.json"),
938 SchemaContract::List => include_str!("../schemas/list-1.json"),
939 SchemaContract::Show => include_str!("../schemas/show-1.json"),
940 SchemaContract::PromptManifest => {
941 include_str!("../schemas/prompt-manifest-1.json")
942 }
943 SchemaContract::Error => include_str!("../schemas/error-1.json"),
944 SchemaContract::FindEstimate => {
945 include_str!("../schemas/find-estimate-1.json")
946 }
947 SchemaContract::CacheStatus => {
948 include_str!("../schemas/cache-status-1.json")
949 }
950 SchemaContract::CachePrune => include_str!("../schemas/cache-prune-1.json"),
951 SchemaContract::Baseline => include_str!("../schemas/baseline-1.json"),
952 SchemaContract::Prune => include_str!("../schemas/prune-1.json"),
953 SchemaContract::CompareNdjson => {
954 include_str!("../schemas/compare-ndjson-1.json")
955 }
956 SchemaContract::Report | SchemaContract::Config => unreachable!(),
957 };
958 let value: Value = serde_json::from_str(source)?;
959 render_json(&value)?
960 }
961 };
962 write_generated_output(args.output.as_deref(), rendered.as_bytes())?;
963 Ok(0)
964 }
965 }
966}
967
968fn command_requires_repository(command: &Command) -> bool {
969 match command {
970 Command::Completions(_)
971 | Command::Man(_)
972 | Command::Reference(_)
973 | Command::Version
974 | Command::BuildInfo(_)
975 | Command::Schema(_)
976 | Command::Report(_)
977 | Command::Config(ConfigArgs {
978 command: ConfigCommand::Schema,
979 }) => false,
980 Command::Show(args) => args.report.is_none(),
981 Command::Compare(args) => args.base_ref.is_some() || args.baseline.is_some(),
982 Command::Baseline(_) => true,
983 Command::Explain(args) => args.report.is_none() || args.include_repository_context,
984 Command::Plan(args) => args.report.is_none() || args.include_repository_context,
985 Command::Check(args) => args.report.is_none(),
986 Command::Sarif(args) => args.report.is_none(),
987 Command::Health(args) => args.report.is_none(),
988 Command::Html(args) => args.report.is_none(),
989 Command::List(ListArgs {
990 command:
991 ListCommand::Findings(args)
992 | ListCommand::Relationships(args)
993 | ListCommand::Clusters(args)
994 | ListCommand::Profiles(args),
995 }) => args.report.is_none(),
996 _ => true,
997 }
998}
999
1000pub fn run() -> i32 {
1001 let raw_args = std::env::args_os().collect::<Vec<_>>();
1002 let requested_error_format = requested_error_format(&raw_args);
1003 let parser_command = parser_command_name(&raw_args);
1004 let cli = match Cli::try_parse_from(&raw_args) {
1005 Ok(cli) => cli,
1006 Err(error) => {
1007 let code = error.exit_code();
1008 if code == 0 || requested_error_format == ErrorFormat::Human {
1009 let _ = error.print();
1010 } else {
1011 let classified =
1012 ClassifiedError::new(ErrorKind::Contract, "parser_error", error.to_string())
1013 .at("/arguments")
1014 .with_details(json!({"clap_kind": format!("{:?}", error.kind())}));
1015 render_runtime_error(
1016 requested_error_format,
1017 &classified,
1018 parser_command.as_deref(),
1019 );
1020 }
1021 return code;
1022 }
1023 };
1024 let error_format = cli.error_format;
1025 let command_name = cli.command.name();
1026 let repo_root = if command_requires_repository(&cli.command) {
1027 match git::resolve_repo_root_from(cli.repo.as_deref()) {
1028 Ok(root) => root,
1029 Err(error) => {
1030 render_runtime_error(
1031 error_format,
1032 &ClassifiedError::new(
1033 ErrorKind::Repository,
1034 "repository_not_found",
1035 format!("{error:#}"),
1036 ),
1037 Some(command_name),
1038 );
1039 return 3;
1040 }
1041 }
1042 } else {
1043 PathBuf::new()
1044 };
1045 match execute(&repo_root, cli.command) {
1046 Ok(code) => code,
1047 Err(error) => {
1048 let classified = error.downcast_ref::<ClassifiedError>();
1049 let fallback;
1050 let classified = if let Some(classified) = classified {
1051 classified
1052 } else {
1053 fallback = if error.downcast_ref::<std::io::Error>().is_some() {
1054 ClassifiedError::new(ErrorKind::Io, "io_failure", format!("{error:#}"))
1055 } else {
1056 ClassifiedError::new(
1057 ErrorKind::Repository,
1058 "operation_failed",
1059 format!("{error:#}"),
1060 )
1061 };
1062 &fallback
1063 };
1064 render_runtime_error(error_format, classified, Some(command_name));
1065 classified.kind.exit_code()
1066 }
1067 }
1068}
1069
1070fn requested_error_format(args: &[std::ffi::OsString]) -> ErrorFormat {
1071 args.iter()
1072 .filter_map(|arg| arg.to_str())
1073 .enumerate()
1074 .find_map(|(index, arg)| {
1075 if arg == "--error-format" {
1076 args.get(index + 1).and_then(|value| value.to_str())
1077 } else {
1078 arg.strip_prefix("--error-format=")
1079 }
1080 })
1081 .filter(|value| *value == "json")
1082 .map_or(ErrorFormat::Human, |_| ErrorFormat::Json)
1083}
1084
1085fn parser_command_name(args: &[std::ffi::OsString]) -> Option<String> {
1086 const COMMANDS: &[&str] = &[
1087 "init",
1088 "find",
1089 "show",
1090 "explain",
1091 "plan",
1092 "check",
1093 "compare",
1094 "baseline",
1095 "report",
1096 "sarif",
1097 "health",
1098 "config",
1099 "doctor",
1100 "list",
1101 "prune",
1102 "cache",
1103 "completions",
1104 "man",
1105 "reference",
1106 "html",
1107 "version",
1108 "build-info",
1109 "schema",
1110 ];
1111 args.iter()
1112 .skip(1)
1113 .filter_map(|arg| arg.to_str())
1114 .find(|arg| COMMANDS.contains(arg))
1115 .map(str::to_string)
1116}
1117
1118fn render_runtime_error(format: ErrorFormat, error: &ClassifiedError, command: Option<&str>) {
1119 match format {
1120 ErrorFormat::Human => eprintln!("{}", error.message),
1121 ErrorFormat::Json => eprintln!(
1122 "{}",
1123 serde_json::to_string(&json!({
1124 "schema_version": 1,
1125 "error": {
1126 "kind": error.kind,
1127 "code": error.code,
1128 "pointer": error.pointer,
1129 "message": error.message,
1130 "details": error.details,
1131 "command": command,
1132 "exit_code": error.kind.exit_code()
1133 }
1134 }))
1135 .unwrap_or_else(|_| {
1136 "{\"schema_version\":1,\"error\":{\"code\":\"serialization_failed\"}}".to_string()
1137 })
1138 ),
1139 }
1140}
1141
1142#[cfg(test)]
1143mod tests {
1144 use clap::CommandFactory;
1145
1146 use super::Cli;
1147
1148 fn assert_descriptions(command: &clap::Command, path: &str) {
1149 for argument in command.get_arguments() {
1150 assert!(
1151 argument
1152 .get_help()
1153 .is_some_and(|help| !help.to_string().trim().is_empty()),
1154 "{path} argument {} has no generated reference description",
1155 argument.get_id()
1156 );
1157 }
1158 for subcommand in command.get_subcommands() {
1159 assert_descriptions(subcommand, &format!("{path} {}", subcommand.get_name()));
1160 }
1161 }
1162
1163 #[test]
1164 fn generated_reference_has_no_blank_argument_descriptions() {
1165 assert_descriptions(&Cli::command(), "git-slop");
1166 }
1167}