1use std::fs;
2use std::io::IsTerminal;
3use std::path::{Component, Path, PathBuf};
4
5use anyhow::{Context, Result, bail};
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 FindArgs {
123 #[arg(long)]
125 allow_shallow: bool,
126 #[arg(long)]
128 scope: Option<String>,
129 #[arg(long)]
131 allow_empty_scope: bool,
132 #[arg(long)]
134 quiet: bool,
135 #[arg(long)]
137 no_progress: bool,
138 #[arg(long, value_name = "PATH")]
140 state_dir: Option<PathBuf>,
141 #[arg(long, value_name = "PATH")]
143 output_dir: Option<PathBuf>,
144 #[arg(long)]
146 no_cache: bool,
147 #[arg(long)]
149 allow_degraded: bool,
150 #[arg(long, value_name = "RFC3339")]
152 as_of: Option<String>,
153 #[arg(long, value_enum, default_value_t = ReportProfile::Standard)]
155 report_profile: ReportProfile,
156 #[arg(long, value_enum, default_value_t = ReportCompression::None)]
158 compression: ReportCompression,
159 #[arg(long)]
161 estimate_only: bool,
162}
163
164#[derive(Debug, Clone, Copy, ValueEnum)]
165enum ReportProfile {
166 Compact,
167 Standard,
168 FullEvidence,
169}
170
171impl ReportProfile {
172 fn as_str(self) -> &'static str {
173 match self {
174 Self::Compact => "compact",
175 Self::Standard => "standard",
176 Self::FullEvidence => "full_evidence",
177 }
178 }
179}
180
181#[derive(Debug, Clone, Copy, ValueEnum)]
182enum ReportCompression {
183 None,
184 Gzip,
185 Zstd,
186}
187
188impl ReportCompression {
189 fn as_str(self) -> &'static str {
190 match self {
191 Self::None => "none",
192 Self::Gzip => "gzip",
193 Self::Zstd => "zstd",
194 }
195 }
196}
197
198#[derive(Debug, Args)]
199struct BuildInfoArgs {
200 #[arg(long, value_enum, default_value_t = BuildInfoFormat::Json)]
202 format: BuildInfoFormat,
203}
204
205#[derive(Debug, Args)]
206struct InitArgs {
207 #[arg(long)]
209 force: bool,
210}
211
212#[derive(Debug, Args)]
213struct ShowArgs {
214 target_path: String,
216 #[arg(long)]
218 report: Option<PathBuf>,
219 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
221 format: DisplayFormat,
222}
223
224#[derive(Debug, Args)]
225#[command(group(
226 ArgGroup::new("selector")
227 .args(["path", "cluster", "relationship", "top"])
228 .multiple(false)
229))]
230struct ExplainArgs {
231 #[arg(long)]
233 report: Option<PathBuf>,
234 #[arg(long)]
236 path: Option<String>,
237 #[arg(long)]
239 cluster: Option<String>,
240 #[arg(long)]
242 relationship: Option<String>,
243 #[arg(long)]
245 top: Option<i64>,
246 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
248 format: DisplayFormat,
249 #[arg(long)]
251 prompt_pack: Option<PathBuf>,
252 #[arg(long, requires = "prompt_pack")]
254 force: bool,
255 #[arg(long, requires = "prompt_pack")]
257 include_repository_context: bool,
258 #[arg(long, default_value_t = 2048, requires = "include_repository_context")]
260 excerpt_bytes: usize,
261 #[arg(long, requires = "prompt_pack")]
263 include_local_paths: bool,
264}
265
266#[derive(Debug, Args)]
267#[command(group(
268 ArgGroup::new("selector")
269 .args(["path", "cluster", "relationship"])
270 .required(true)
271 .multiple(false)
272))]
273struct PlanArgs {
274 #[arg(long)]
276 report: Option<PathBuf>,
277 #[arg(long)]
279 path: Option<String>,
280 #[arg(long)]
282 cluster: Option<String>,
283 #[arg(long)]
285 relationship: Option<String>,
286 #[arg(long, default_value_t = 3)]
288 max_slices: i64,
289 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
291 format: DisplayFormat,
292 #[arg(long)]
294 prompt_pack: Option<PathBuf>,
295 #[arg(long, requires = "prompt_pack")]
297 force: bool,
298 #[arg(long, requires = "prompt_pack")]
300 include_repository_context: bool,
301 #[arg(long, default_value_t = 2048, requires = "include_repository_context")]
303 excerpt_bytes: usize,
304 #[arg(long, requires = "prompt_pack")]
306 include_local_paths: bool,
307}
308
309#[derive(Debug, Args)]
310struct CheckArgs {
311 #[arg(long)]
313 report: Option<PathBuf>,
314 #[arg(long, value_enum)]
316 fail_on_context_band: Option<ContextBand>,
317 #[arg(long, value_enum)]
319 fail_on_slop_band: Option<SlopBand>,
320 #[arg(long, value_enum, default_value_t = CheckFormat::Text)]
322 format: CheckFormat,
323 #[arg(long)]
325 details: bool,
326 #[arg(long)]
328 include_folders: bool,
329 #[arg(long, default_value_t = 0, requires = "details")]
331 offset: usize,
332 #[arg(long, default_value_t = 1000, requires = "details")]
334 limit: usize,
335 #[arg(long)]
337 allow_incomplete_evidence: bool,
338 #[arg(long)]
340 evaluate_only: bool,
341}
342
343#[derive(Debug, Args)]
344struct CompareArgs {
345 #[arg(long, value_name = "PATH")]
347 state_dir: Option<PathBuf>,
348 #[arg(
350 long,
351 required_unless_present_any = ["base_ref", "baseline"],
352 conflicts_with_all = ["base_ref", "baseline"]
353 )]
354 base: Option<PathBuf>,
355 #[arg(long, conflicts_with_all = ["base", "baseline"])]
357 base_ref: Option<String>,
358 #[arg(long, conflicts_with_all = ["base", "base_ref"])]
360 baseline: Option<String>,
361 #[arg(long, default_value = ".slop/latest/report.json")]
363 head: PathBuf,
364 #[arg(long)]
366 scope: Option<String>,
367 #[arg(long)]
369 allow_shallow: bool,
370 #[arg(long)]
372 allow_incomplete_evidence: bool,
373 #[arg(long, default_value_t = 10)]
375 top: i64,
376 #[arg(long, value_enum, default_value_t = CompareFormat::Text)]
378 format: CompareFormat,
379 #[arg(long, value_enum, default_value_t = CompareDetail::Top)]
381 detail: CompareDetail,
382 #[arg(long, default_value_t = 0)]
384 offset: usize,
385 #[arg(long, default_value_t = 1000)]
387 limit: usize,
388 #[arg(long)]
390 force: bool,
391 #[arg(long)]
393 include_local_paths: bool,
394 #[arg(long)]
396 include_unchanged: bool,
397 #[arg(long, value_enum, default_value_t = PolicySource::Base)]
399 policy_from: PolicySource,
400 #[arg(long)]
402 fail_on_regression: bool,
403}
404
405#[derive(Debug, Args)]
406struct BaselineArgs {
407 #[arg(long, value_name = "PATH", global = true)]
409 state_dir: Option<PathBuf>,
410 #[command(subcommand)]
411 command: BaselineCommand,
412}
413
414#[derive(Debug, Subcommand)]
415enum BaselineCommand {
416 Ensure {
418 #[arg(long, default_value = "default")]
420 name: String,
421 #[arg(long)]
423 report: Option<PathBuf>,
424 #[arg(long)]
426 replace: bool,
427 #[arg(long)]
429 allow_dirty: bool,
430 #[arg(long)]
432 allow_incomplete_evidence: bool,
433 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
435 format: DisplayFormat,
436 },
437 Create {
439 #[arg(long, default_value = "default")]
441 name: String,
442 #[arg(long)]
444 report: Option<PathBuf>,
445 #[arg(long)]
447 force: bool,
448 #[arg(long)]
450 allow_dirty: bool,
451 #[arg(long)]
453 allow_incomplete_evidence: bool,
454 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
456 format: DisplayFormat,
457 },
458 Update {
460 #[arg(long, default_value = "default")]
462 name: String,
463 #[arg(long)]
465 report: Option<PathBuf>,
466 #[arg(long)]
468 allow_dirty: bool,
469 #[arg(long)]
471 allow_incomplete_evidence: bool,
472 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
474 format: DisplayFormat,
475 },
476 List {
478 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
480 format: DisplayFormat,
481 },
482 Inspect {
484 #[arg(long, default_value = "default")]
486 name: String,
487 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
489 format: DisplayFormat,
490 },
491 Validate {
493 #[arg(long, default_value = "default")]
495 name: String,
496 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
498 format: DisplayFormat,
499 },
500 Remove {
502 #[arg(long, default_value = "default")]
504 name: String,
505 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
507 format: DisplayFormat,
508 },
509}
510
511#[derive(Debug, Args)]
512struct ReportArgs {
513 #[command(subcommand)]
514 command: ReportCommand,
515}
516
517#[derive(Debug, Subcommand)]
518enum ReportCommand {
519 Validate {
521 #[arg(value_name = "REPORT_JSON", required_unless_present = "report")]
523 path: Option<PathBuf>,
524 #[arg(long, value_name = "REPORT_JSON", required_unless_present = "path")]
526 report: Option<PathBuf>,
527 #[arg(long)]
529 allow_legacy: bool,
530 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
532 format: DisplayFormat,
533 },
534 Migrate {
536 #[arg(value_name = "REPORT_JSON")]
538 path: PathBuf,
539 #[arg(long, value_name = "PATH")]
541 output: PathBuf,
542 },
543 Schema,
545}
546
547#[derive(Debug, Args)]
548struct SarifArgs {
549 #[arg(long)]
551 report: Option<PathBuf>,
552 #[arg(long)]
554 top: Option<i64>,
555 #[arg(long, value_enum, default_value_t = SarifScope::ActionQueue)]
557 scope: SarifScope,
558 #[arg(long)]
560 output: Option<PathBuf>,
561 #[arg(long)]
563 include_local_paths: bool,
564}
565
566#[derive(Debug, Args)]
567struct HealthArgs {
568 #[arg(long)]
570 report: Option<PathBuf>,
571 #[arg(long, value_enum, default_value_t = HealthFormat::Text)]
573 format: HealthFormat,
574 #[arg(long, default_value_t = 10)]
576 max_annotations: usize,
577}
578
579#[derive(Debug, Args)]
580struct ConfigArgs {
581 #[command(subcommand)]
582 command: ConfigCommand,
583}
584
585#[derive(Debug, Subcommand)]
586enum ConfigCommand {
587 Show {
589 #[arg(long)]
591 effective: bool,
592 },
593 Validate,
595 DiffDefaults,
597 Migrate,
599 Schema,
601}
602
603#[derive(Debug, Args)]
604struct DoctorArgs {
605 #[arg(long, num_args = 0..=1, default_missing_value = ".slop/diagnostic-bundle.json")]
607 bundle: Option<PathBuf>,
608 #[arg(long, value_enum, default_value_t = DoctorFormat::Text)]
610 format: DoctorFormat,
611 #[arg(long)]
613 scope: Option<String>,
614}
615
616#[derive(Debug, Clone, Copy, ValueEnum)]
617enum DoctorFormat {
618 Text,
619 Json,
620}
621
622#[derive(Debug, Args)]
623struct ListArgs {
624 #[command(subcommand)]
625 command: ListCommand,
626}
627
628#[derive(Debug, Subcommand)]
629enum ListCommand {
630 Findings(ListFilterArgs),
631 Relationships(ListFilterArgs),
632 Clusters(ListFilterArgs),
633 Profiles(ListFilterArgs),
634}
635
636#[derive(Debug, Args)]
637struct ListFilterArgs {
638 #[arg(long)]
640 report: Option<PathBuf>,
641 #[arg(long)]
643 path: Option<String>,
644 #[arg(long)]
646 profile: Option<String>,
647 #[arg(long)]
649 language: Option<String>,
650 #[arg(long, visible_alias = "class")]
652 classification: Option<String>,
653 #[arg(long)]
655 severity: Option<String>,
656 #[arg(long, default_value_t = 50)]
658 top: usize,
659 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
661 format: DisplayFormat,
662 #[arg(long)]
664 wide: bool,
665 #[arg(long)]
667 no_truncate: bool,
668}
669
670#[derive(Debug, Args)]
671struct PruneArgs {
672 #[arg(long)]
674 keep: Option<usize>,
675 #[arg(long)]
677 max_bytes: Option<u64>,
678 #[arg(long)]
680 dry_run: bool,
681 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
683 format: DisplayFormat,
684}
685
686#[derive(Debug, Args)]
687struct CacheArgs {
688 #[arg(long, value_name = "PATH", global = true)]
690 state_dir: Option<PathBuf>,
691 #[command(subcommand)]
692 command: CacheCommand,
693}
694
695#[derive(Debug, Subcommand)]
696enum CacheCommand {
697 Status {
698 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
700 format: DisplayFormat,
701 },
702 Prune {
703 #[arg(long, default_value_t = 10_000)]
705 max_entries: usize,
706 #[arg(long, default_value_t = 536_870_912)]
708 max_bytes: u64,
709 #[arg(long)]
711 dry_run: bool,
712 #[arg(long)]
714 compact: bool,
715 #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
717 format: DisplayFormat,
718 },
719}
720
721#[derive(Debug, Args)]
722struct CompletionsArgs {
723 shell: CompletionShell,
725}
726
727#[derive(Debug, Args)]
728struct ManArgs {
729 #[arg(long)]
731 output: Option<PathBuf>,
732}
733
734#[derive(Debug, Args)]
735struct ReferenceArgs {
736 #[arg(long)]
738 output: Option<PathBuf>,
739}
740
741#[derive(Debug, Args)]
742struct HtmlArgs {
743 #[arg(long)]
745 report: Option<PathBuf>,
746 #[arg(long)]
748 output: Option<PathBuf>,
749 #[arg(long)]
751 include_local_paths: bool,
752}
753
754#[derive(Debug, Clone, Copy, ValueEnum)]
755enum CompletionShell {
756 Bash,
757 Zsh,
758 Fish,
759 Powershell,
760 Nushell,
761}
762
763#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
764enum DisplayFormat {
765 Text,
766 Json,
767 Yaml,
768}
769
770#[derive(Debug, Clone, Copy, ValueEnum)]
771enum CompareFormat {
772 Text,
773 Json,
774 Yaml,
775 Ndjson,
776}
777
778#[derive(Debug, Clone, Copy, ValueEnum)]
779enum CompareDetail {
780 Summary,
781 Top,
782 Full,
783}
784
785#[derive(Debug, Clone, Copy, ValueEnum)]
786enum HealthFormat {
787 Text,
788 Markdown,
789 Github,
790 Json,
791}
792
793#[derive(Debug, Clone, Copy, ValueEnum)]
794enum CheckFormat {
795 Text,
796 Json,
797 Github,
798}
799
800#[derive(Debug, Clone, Copy, ValueEnum)]
801enum PolicySource {
802 Base,
803 Head,
804}
805
806impl PolicySource {
807 fn as_str(self) -> &'static str {
808 match self {
809 Self::Base => "base",
810 Self::Head => "head",
811 }
812 }
813}
814
815#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
816enum ErrorFormat {
817 Human,
818 Json,
819}
820
821#[derive(Debug, Clone, Copy, ValueEnum)]
822enum ContextBand {
823 Compact,
824 Healthy,
825 Warning,
826 Critical,
827}
828
829impl ContextBand {
830 fn as_str(self) -> &'static str {
831 match self {
832 Self::Compact => "compact",
833 Self::Healthy => "healthy",
834 Self::Warning => "warning",
835 Self::Critical => "critical",
836 }
837 }
838}
839
840#[derive(Debug, Clone, Copy, ValueEnum)]
841enum SlopBand {
842 Low,
843 Moderate,
844 High,
845 Critical,
846}
847
848impl SlopBand {
849 fn as_str(self) -> &'static str {
850 match self {
851 Self::Low => "low",
852 Self::Moderate => "moderate",
853 Self::High => "high",
854 Self::Critical => "critical",
855 }
856 }
857}
858
859include!("cli/support.rs");
860include!("cli/analysis.rs");
861include!("cli/check.rs");
862include!("cli/baseline_compare.rs");
863include!("cli/reporting.rs");
864include!("cli/listing.rs");
865include!("cli/generation.rs");
866fn execute(repo_root: &Path, command: Command) -> Result<i32> {
867 match command {
868 Command::Init(args) => run_init(repo_root, args),
869 Command::Find(args) => run_find(repo_root, args),
870 Command::Show(args) => run_show(repo_root, args),
871 Command::Explain(args) => run_explain(repo_root, args),
872 Command::Plan(args) => run_plan(repo_root, args),
873 Command::Check(args) => run_check(repo_root, args),
874 Command::Compare(args) => run_compare(repo_root, args),
875 Command::Baseline(args) => run_baseline(repo_root, args),
876 Command::Report(args) => run_report(args),
877 Command::Sarif(args) => run_sarif(repo_root, args),
878 Command::Health(args) => run_health(repo_root, args),
879 Command::Config(args) => run_config(repo_root, args),
880 Command::Doctor(args) => run_doctor(repo_root, args),
881 Command::List(args) => run_list(repo_root, args),
882 Command::Prune(args) => run_prune(repo_root, args),
883 Command::Cache(args) => run_cache(repo_root, args),
884 Command::Completions(args) => run_completions(args),
885 Command::Man(args) => run_man(args),
886 Command::Reference(args) => run_reference(args),
887 Command::Html(args) => run_html(repo_root, args),
888 Command::Version => {
889 println!("{PROJECT_NAME} {VERSION}");
890 Ok(0)
891 }
892 Command::BuildInfo(args) => {
893 match args.format {
894 BuildInfoFormat::Json => {
895 println!("{}", serde_json::to_string_pretty(&build_info::current())?)
896 }
897 }
898 Ok(0)
899 }
900 Command::Schema(args) => {
901 let rendered = match args.contract {
902 SchemaContract::Report => render_json(&report::schema())?,
903 SchemaContract::Config => render_json(&config::schema())?,
904 contract => {
905 let source = match contract {
906 SchemaContract::Compare => include_str!("../schemas/compare-1.json"),
907 SchemaContract::Explain => include_str!("../schemas/explain-2.json"),
908 SchemaContract::Plan => include_str!("../schemas/plan-2.json"),
909 SchemaContract::Sarif => include_str!("../schemas/sarif-1.json"),
910 SchemaContract::Health => include_str!("../schemas/health-1.json"),
911 SchemaContract::Check => include_str!("../schemas/check-1.json"),
912 SchemaContract::Doctor => include_str!("../schemas/doctor-1.json"),
913 SchemaContract::BuildInfo => include_str!("../schemas/build-info-2.json"),
914 SchemaContract::ReleaseManifest => {
915 include_str!("../schemas/release-manifest-3.json")
916 }
917 SchemaContract::List => include_str!("../schemas/list-1.json"),
918 SchemaContract::Show => include_str!("../schemas/show-1.json"),
919 SchemaContract::PromptManifest => {
920 include_str!("../schemas/prompt-manifest-1.json")
921 }
922 SchemaContract::Error => include_str!("../schemas/error-1.json"),
923 SchemaContract::FindEstimate => {
924 include_str!("../schemas/find-estimate-1.json")
925 }
926 SchemaContract::CacheStatus => {
927 include_str!("../schemas/cache-status-1.json")
928 }
929 SchemaContract::CachePrune => include_str!("../schemas/cache-prune-1.json"),
930 SchemaContract::Baseline => include_str!("../schemas/baseline-1.json"),
931 SchemaContract::Prune => include_str!("../schemas/prune-1.json"),
932 SchemaContract::CompareNdjson => {
933 include_str!("../schemas/compare-ndjson-1.json")
934 }
935 SchemaContract::Report | SchemaContract::Config => unreachable!(),
936 };
937 let value: Value = serde_json::from_str(source)?;
938 render_json(&value)?
939 }
940 };
941 write_generated_output(args.output.as_deref(), rendered.as_bytes())?;
942 Ok(0)
943 }
944 }
945}
946
947fn command_requires_repository(command: &Command) -> bool {
948 match command {
949 Command::Completions(_)
950 | Command::Man(_)
951 | Command::Reference(_)
952 | Command::Version
953 | Command::BuildInfo(_)
954 | Command::Schema(_)
955 | Command::Report(_)
956 | Command::Config(ConfigArgs {
957 command: ConfigCommand::Schema,
958 }) => false,
959 Command::Show(args) => args.report.is_none(),
960 Command::Compare(args) => args.base_ref.is_some() || args.baseline.is_some(),
961 Command::Baseline(_) => true,
962 Command::Explain(args) => args.report.is_none() || args.include_repository_context,
963 Command::Plan(args) => args.report.is_none() || args.include_repository_context,
964 Command::Check(args) => args.report.is_none(),
965 Command::Sarif(args) => args.report.is_none(),
966 Command::Health(args) => args.report.is_none(),
967 Command::Html(args) => args.report.is_none(),
968 Command::List(ListArgs {
969 command:
970 ListCommand::Findings(args)
971 | ListCommand::Relationships(args)
972 | ListCommand::Clusters(args)
973 | ListCommand::Profiles(args),
974 }) => args.report.is_none(),
975 _ => true,
976 }
977}
978
979pub fn run() -> i32 {
980 let raw_args = std::env::args_os().collect::<Vec<_>>();
981 let requested_error_format = requested_error_format(&raw_args);
982 let parser_command = parser_command_name(&raw_args);
983 let cli = match Cli::try_parse_from(&raw_args) {
984 Ok(cli) => cli,
985 Err(error) => {
986 let code = error.exit_code();
987 if code == 0 || requested_error_format == ErrorFormat::Human {
988 let _ = error.print();
989 } else {
990 let classified =
991 ClassifiedError::new(ErrorKind::Contract, "parser_error", error.to_string())
992 .at("/arguments")
993 .with_details(json!({"clap_kind": format!("{:?}", error.kind())}));
994 render_runtime_error(
995 requested_error_format,
996 &classified,
997 parser_command.as_deref(),
998 );
999 }
1000 return code;
1001 }
1002 };
1003 let error_format = cli.error_format;
1004 let command_name = cli.command.name();
1005 let repo_root = if command_requires_repository(&cli.command) {
1006 match git::resolve_repo_root_from(cli.repo.as_deref()) {
1007 Ok(root) => root,
1008 Err(error) => {
1009 render_runtime_error(
1010 error_format,
1011 &ClassifiedError::new(
1012 ErrorKind::Repository,
1013 "repository_not_found",
1014 format!("{error:#}"),
1015 ),
1016 Some(command_name),
1017 );
1018 return 3;
1019 }
1020 }
1021 } else {
1022 PathBuf::new()
1023 };
1024 match execute(&repo_root, cli.command) {
1025 Ok(code) => code,
1026 Err(error) => {
1027 let classified = error.downcast_ref::<ClassifiedError>();
1028 let fallback;
1029 let classified = if let Some(classified) = classified {
1030 classified
1031 } else {
1032 fallback = if error.downcast_ref::<std::io::Error>().is_some() {
1033 ClassifiedError::new(ErrorKind::Io, "io_failure", format!("{error:#}"))
1034 } else {
1035 ClassifiedError::new(
1036 ErrorKind::Repository,
1037 "operation_failed",
1038 format!("{error:#}"),
1039 )
1040 };
1041 &fallback
1042 };
1043 render_runtime_error(error_format, classified, Some(command_name));
1044 classified.kind.exit_code()
1045 }
1046 }
1047}
1048
1049fn requested_error_format(args: &[std::ffi::OsString]) -> ErrorFormat {
1050 args.iter()
1051 .filter_map(|arg| arg.to_str())
1052 .enumerate()
1053 .find_map(|(index, arg)| {
1054 if arg == "--error-format" {
1055 args.get(index + 1).and_then(|value| value.to_str())
1056 } else {
1057 arg.strip_prefix("--error-format=")
1058 }
1059 })
1060 .filter(|value| *value == "json")
1061 .map_or(ErrorFormat::Human, |_| ErrorFormat::Json)
1062}
1063
1064fn parser_command_name(args: &[std::ffi::OsString]) -> Option<String> {
1065 const COMMANDS: &[&str] = &[
1066 "init",
1067 "find",
1068 "show",
1069 "explain",
1070 "plan",
1071 "check",
1072 "compare",
1073 "baseline",
1074 "report",
1075 "sarif",
1076 "health",
1077 "config",
1078 "doctor",
1079 "list",
1080 "prune",
1081 "cache",
1082 "completions",
1083 "man",
1084 "reference",
1085 "html",
1086 "version",
1087 "build-info",
1088 "schema",
1089 ];
1090 args.iter()
1091 .skip(1)
1092 .filter_map(|arg| arg.to_str())
1093 .find(|arg| COMMANDS.contains(arg))
1094 .map(str::to_string)
1095}
1096
1097fn render_runtime_error(format: ErrorFormat, error: &ClassifiedError, command: Option<&str>) {
1098 match format {
1099 ErrorFormat::Human => eprintln!("{}", error.message),
1100 ErrorFormat::Json => eprintln!(
1101 "{}",
1102 serde_json::to_string(&json!({
1103 "schema_version": 1,
1104 "error": {
1105 "kind": error.kind,
1106 "code": error.code,
1107 "pointer": error.pointer,
1108 "message": error.message,
1109 "details": error.details,
1110 "command": command,
1111 "exit_code": error.kind.exit_code()
1112 }
1113 }))
1114 .unwrap_or_else(|_| {
1115 "{\"schema_version\":1,\"error\":{\"code\":\"serialization_failed\"}}".to_string()
1116 })
1117 ),
1118 }
1119}
1120
1121#[cfg(test)]
1122mod tests {
1123 use clap::CommandFactory;
1124
1125 use super::Cli;
1126
1127 fn assert_descriptions(command: &clap::Command, path: &str) {
1128 for argument in command.get_arguments() {
1129 assert!(
1130 argument
1131 .get_help()
1132 .is_some_and(|help| !help.to_string().trim().is_empty()),
1133 "{path} argument {} has no generated reference description",
1134 argument.get_id()
1135 );
1136 }
1137 for subcommand in command.get_subcommands() {
1138 assert_descriptions(subcommand, &format!("{path} {}", subcommand.get_name()));
1139 }
1140 }
1141
1142 #[test]
1143 fn generated_reference_has_no_blank_argument_descriptions() {
1144 assert_descriptions(&Cli::command(), "git-slop");
1145 }
1146}