Skip to main content

git_slop/
cli.rs

1use std::fs;
2use std::io::{IsTerminal, Read, Write};
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_summary_text, render_explain_text, render_github_annotations, render_json,
20    render_plan_text, 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    after_help = "QUICK START:\n  git slop find                       Run a safe first scan\n  git slop health                     Review repository health\n  git slop list interventions         Review maintenance candidates\n  git slop html                       Build an interactive local report\n  git slop init                       Adopt durable reports and ignore rules",
29    version = VERSION
30)]
31struct Cli {
32    /// Repository or path inside a repository to analyze.
33    #[arg(long, global = true, value_name = "PATH")]
34    repo: Option<PathBuf>,
35    /// Render runtime errors as human text or stable JSON.
36    #[arg(long, global = true, value_enum, default_value_t = ErrorFormat::Human)]
37    error_format: ErrorFormat,
38    #[command(subcommand)]
39    command: Command,
40}
41
42#[derive(Debug, Subcommand)]
43enum Command {
44    /// Scaffold .slop/ config, ignore rules, and state directories.
45    Init(InitArgs),
46    /// Scan the repository and generate hotspot reports.
47    Find(FindArgs),
48    /// Show metrics and reasons for one file or folder.
49    Show(ShowArgs),
50    /// Explain why selected hotspots or structural findings are expensive.
51    Explain(ExplainArgs),
52    /// Propose bounded maintenance slices from the current detector report.
53    Plan(PlanArgs),
54    /// Manage declarative policy packs used only by the optional advisor.
55    Policy(PolicyArgs),
56    /// Evaluate deterministic plan candidates with locked policies and an explicit local model.
57    Advise(AdviseArgs),
58    /// Evaluate an existing report against CI thresholds.
59    Check(CheckArgs),
60    /// Compare two existing schema-5 reports without rerunning the detector.
61    Compare(CompareArgs),
62    /// Manage named comparison baselines in Git-private runtime storage.
63    Baseline(BaselineArgs),
64    /// Validate or inspect the versioned report contract.
65    Report(ReportArgs),
66    /// Export action-queue findings from an existing schema-5 report as SARIF.
67    Sarif(SarifArgs),
68    /// Render repository health for CI summaries, annotations, or automation.
69    Health(HealthArgs),
70    /// Inspect or migrate effective configuration.
71    Config(ConfigArgs),
72    /// Diagnose repository readiness and optionally write a redacted bundle.
73    Doctor(DoctorArgs),
74    /// List findings, relationships, clusters, or profiles.
75    List(ListArgs),
76    /// Remove old immutable run snapshots according to retention policy.
77    Prune(PruneArgs),
78    /// Inspect or prune the packed token cache.
79    Cache(CacheArgs),
80    /// Generate shell completion source.
81    Completions(CompletionsArgs),
82    /// Generate the roff manual from the live Clap command tree.
83    Man(ManArgs),
84    /// Generate Markdown command reference from the live Clap command tree.
85    Reference(ReferenceArgs),
86    /// Write a self-contained, local, searchable HTML report.
87    Html(HtmlArgs),
88    /// Print version information.
89    Version,
90    /// Print package and source-build provenance.
91    BuildInfo(BuildInfoArgs),
92    /// Print a published JSON Schema for a machine contract.
93    Schema(SchemaArgs),
94}
95
96impl Command {
97    fn name(&self) -> &'static str {
98        match self {
99            Self::Init(_) => "init",
100            Self::Find(_) => "find",
101            Self::Show(_) => "show",
102            Self::Explain(_) => "explain",
103            Self::Plan(_) => "plan",
104            Self::Policy(_) => "policy",
105            Self::Advise(_) => "advise",
106            Self::Check(_) => "check",
107            Self::Compare(_) => "compare",
108            Self::Baseline(_) => "baseline",
109            Self::Report(_) => "report",
110            Self::Sarif(_) => "sarif",
111            Self::Health(_) => "health",
112            Self::Config(_) => "config",
113            Self::Doctor(_) => "doctor",
114            Self::List(_) => "list",
115            Self::Prune(_) => "prune",
116            Self::Cache(_) => "cache",
117            Self::Completions(_) => "completions",
118            Self::Man(_) => "man",
119            Self::Reference(_) => "reference",
120            Self::Html(_) => "html",
121            Self::Version => "version",
122            Self::BuildInfo(_) => "build-info",
123            Self::Schema(_) => "schema",
124        }
125    }
126}
127
128#[derive(Debug, Args)]
129struct FindArgs {
130    /// Acknowledge incomplete history and continue in a shallow clone.
131    #[arg(long)]
132    allow_shallow: bool,
133    /// Analyze only this repo-relative path while retaining repository-wide Git evidence.
134    #[arg(long)]
135    scope: Option<String>,
136    /// Permit a scope that selects no tracked paths and emit an empty analysis.
137    #[arg(long)]
138    allow_empty_scope: bool,
139    /// Suppress human progress and report-path messages.
140    #[arg(long)]
141    quiet: bool,
142    /// Suppress phase progress while preserving the final result.
143    #[arg(long)]
144    no_progress: bool,
145    /// Mutable cache/state directory. Relative paths resolve from the repository root.
146    #[arg(long, value_name = "PATH")]
147    state_dir: Option<PathBuf>,
148    /// Report output directory. Relative paths resolve from the repository root.
149    #[arg(long, value_name = "PATH")]
150    output_dir: Option<PathBuf>,
151    /// Disable token-cache reads and writes for an ephemeral scan.
152    #[arg(long)]
153    no_cache: bool,
154    /// Keep disposable state and reports under Git-private storage, without adopting `.slop/`.
155    #[arg(long, conflicts_with_all = ["state_dir", "output_dir", "persist_unadopted"])]
156    ephemeral: bool,
157    /// Explicitly allow persistent `.slop/` output before repository adoption.
158    #[arg(long, conflicts_with = "ephemeral")]
159    persist_unadopted: bool,
160    /// Deterministically analyze the largest path prefix that fits the memory budget.
161    #[arg(long)]
162    allow_degraded: bool,
163    /// Fixed RFC 3339 analysis clock for reproducible recency and history windows.
164    #[arg(long, value_name = "RFC3339")]
165    as_of: Option<String>,
166    /// Report evidence profile.
167    #[arg(long, value_enum, default_value_t = ReportProfile::Standard)]
168    report_profile: ReportProfile,
169    /// Also write a compressed report beside report.json.
170    #[arg(long, value_enum, default_value_t = ReportCompression::None)]
171    compression: ReportCompression,
172    /// Estimate scope, memory, cache, report size, time, and inodes without scanning.
173    #[arg(long)]
174    estimate_only: bool,
175    /// Estimate output format. Defaults to text on a terminal and JSON when piped.
176    #[arg(long, value_enum, requires = "estimate_only")]
177    format: Option<DisplayFormat>,
178}
179
180#[derive(Debug, Clone, Copy, ValueEnum)]
181enum ReportProfile {
182    Compact,
183    Standard,
184    FullEvidence,
185}
186
187impl ReportProfile {
188    fn as_str(self) -> &'static str {
189        match self {
190            Self::Compact => "compact",
191            Self::Standard => "standard",
192            Self::FullEvidence => "full_evidence",
193        }
194    }
195}
196
197#[derive(Debug, Clone, Copy, ValueEnum)]
198enum ReportCompression {
199    None,
200    Gzip,
201    Zstd,
202}
203
204impl ReportCompression {
205    fn as_str(self) -> &'static str {
206        match self {
207            Self::None => "none",
208            Self::Gzip => "gzip",
209            Self::Zstd => "zstd",
210        }
211    }
212}
213
214#[derive(Debug, Args)]
215struct BuildInfoArgs {
216    /// Machine-readable build provenance format.
217    #[arg(long, value_enum, default_value_t = BuildInfoFormat::Json)]
218    format: BuildInfoFormat,
219}
220
221#[derive(Debug, Args)]
222#[command(group(
223    ArgGroup::new("mode")
224        .args(["force", "repair", "check"])
225        .multiple(false)
226))]
227struct InitArgs {
228    /// Replace generated files atomically and keep ignored `.bak` recovery copies.
229    #[arg(long)]
230    force: bool,
231    /// Add missing generated ignore rules without replacing repository configuration.
232    #[arg(long)]
233    repair: bool,
234    /// Inspect adoption files without changing the repository.
235    #[arg(long)]
236    check: bool,
237    /// Limit initialization, repair, force, or check to .slop/.gitignore.
238    #[arg(long)]
239    gitignore_only: bool,
240}
241
242#[derive(Debug, Args)]
243struct ShowArgs {
244    /// Repo-relative file or folder path.
245    target_path: String,
246    /// Report path. Defaults to the durable latest report, then the Git-private first-run report.
247    #[arg(long)]
248    report: Option<PathBuf>,
249    /// Fail when the report does not match current HEAD, worktree, config, scope, or analyzer.
250    #[arg(long)]
251    require_current: bool,
252    /// Output format.
253    #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
254    format: DisplayFormat,
255}
256
257#[derive(Debug, Args)]
258#[command(group(
259    ArgGroup::new("selector")
260        .args(["path", "cluster", "relationship", "top"])
261        .multiple(false)
262))]
263struct ExplainArgs {
264    /// Report path. Defaults to the durable latest report, then the Git-private first-run report.
265    #[arg(long)]
266    report: Option<PathBuf>,
267    /// Fail when the report does not match current HEAD, worktree, config, scope, or analyzer.
268    #[arg(long)]
269    require_current: bool,
270    /// Repo-relative file or folder path.
271    #[arg(long)]
272    path: Option<String>,
273    /// Cluster identifier.
274    #[arg(long)]
275    cluster: Option<String>,
276    /// Relationship identifier.
277    #[arg(long)]
278    relationship: Option<String>,
279    /// Explain the top N hotspots from the action queue.
280    #[arg(long)]
281    top: Option<i64>,
282    /// Output format.
283    #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
284    format: DisplayFormat,
285    /// Include raw normalized metrics, provenance, and bounded supporting evidence in text output.
286    #[arg(long)]
287    verbose: bool,
288    /// Write a deterministic local-model prompt pack to this directory.
289    #[arg(long)]
290    prompt_pack: Option<PathBuf>,
291    /// Atomically replace an existing prompt-pack directory.
292    #[arg(long, requires = "prompt_pack")]
293    force: bool,
294    /// Include bounded local source/test excerpts, guidance, and verification hints.
295    #[arg(long, requires = "prompt_pack")]
296    include_repository_context: bool,
297    /// Maximum bytes read from each included repository file.
298    #[arg(long, default_value_t = 2048, requires = "include_repository_context")]
299    excerpt_bytes: usize,
300    /// Include local filesystem paths in prompt-pack provenance and commands.
301    #[arg(long, requires = "prompt_pack")]
302    include_local_paths: bool,
303}
304
305#[derive(Debug, Args)]
306#[command(group(
307    ArgGroup::new("selector")
308        .args(["path", "cluster", "relationship"])
309        .required(true)
310        .multiple(false)
311))]
312struct PlanArgs {
313    /// Report path. Defaults to the durable latest report, then the Git-private first-run report.
314    #[arg(long)]
315    report: Option<PathBuf>,
316    /// Fail when the report does not match current HEAD, worktree, config, scope, or analyzer.
317    #[arg(long)]
318    require_current: bool,
319    /// Repo-relative file or folder path.
320    #[arg(long)]
321    path: Option<String>,
322    /// Cluster identifier.
323    #[arg(long)]
324    cluster: Option<String>,
325    /// Relationship identifier.
326    #[arg(long)]
327    relationship: Option<String>,
328    /// Maximum number of bounded maintenance slices to propose.
329    #[arg(long, default_value_t = 3)]
330    max_slices: i64,
331    /// Output format.
332    #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
333    format: DisplayFormat,
334    /// Write a deterministic local-model prompt pack to this directory.
335    #[arg(long)]
336    prompt_pack: Option<PathBuf>,
337    /// Atomically replace an existing prompt-pack directory.
338    #[arg(long, requires = "prompt_pack")]
339    force: bool,
340    /// Include bounded local source/test excerpts, guidance, and verification hints.
341    #[arg(long, requires = "prompt_pack")]
342    include_repository_context: bool,
343    /// Maximum bytes read from each included repository file.
344    #[arg(long, default_value_t = 2048, requires = "include_repository_context")]
345    excerpt_bytes: usize,
346    /// Include local filesystem paths in prompt-pack provenance and commands.
347    #[arg(long, requires = "prompt_pack")]
348    include_local_paths: bool,
349}
350
351#[derive(Debug, Args)]
352struct CheckArgs {
353    /// Report path. Defaults to the durable latest report, then the Git-private first-run report.
354    #[arg(long)]
355    report: Option<PathBuf>,
356    /// Override the config default fail threshold for context_band.
357    #[arg(long, value_enum)]
358    fail_on_context_band: Option<ContextBand>,
359    /// Override the config default fail threshold for slop_band.
360    #[arg(long, value_enum)]
361    fail_on_slop_band: Option<SlopBand>,
362    /// Output format, including escaped GitHub workflow commands.
363    #[arg(long, value_enum, default_value_t = CheckFormat::Text)]
364    format: CheckFormat,
365    /// Include complete finding records in JSON output.
366    #[arg(long)]
367    details: bool,
368    /// Include folder records in addition to the versioned file-only gate.
369    #[arg(long)]
370    include_folders: bool,
371    /// Zero-based finding offset used with --details.
372    #[arg(long, default_value_t = 0, requires = "details")]
373    offset: usize,
374    /// Maximum finding records returned with --details.
375    #[arg(long, default_value_t = 1000, requires = "details")]
376    limit: usize,
377    /// Permit policy evaluation when selected inventory records are incomplete.
378    #[arg(long)]
379    allow_incomplete_evidence: bool,
380    /// Evaluate and report the canonical policy result without returning exit 1 for findings.
381    #[arg(long)]
382    evaluate_only: bool,
383    /// Fail when the report does not match current HEAD, worktree, config, scope, or analyzer.
384    #[arg(long)]
385    require_current: bool,
386}
387
388#[derive(Debug, Args)]
389struct CompareArgs {
390    /// Mutable state directory for named baselines. Relative paths resolve from the repository root.
391    #[arg(long, value_name = "PATH")]
392    state_dir: Option<PathBuf>,
393    /// Base report.json path.
394    #[arg(
395        long,
396        required_unless_present_any = ["base_ref", "baseline"],
397        conflicts_with_all = ["base_ref", "baseline"]
398    )]
399    base: Option<PathBuf>,
400    /// Safely resolve and scan this Git revision in an isolated worktree.
401    #[arg(long, conflicts_with_all = ["base", "baseline"])]
402    base_ref: Option<String>,
403    /// Use a named baseline from Git-private runtime storage.
404    #[arg(long, conflicts_with_all = ["base", "base_ref"])]
405    baseline: Option<String>,
406    /// Head report.json path.
407    #[arg(long, default_value = ".slop/latest/report.json")]
408    head: PathBuf,
409    /// Apply the head repository's scope to an isolated --base-ref scan.
410    #[arg(long)]
411    scope: Option<String>,
412    /// Permit incomplete history in an isolated --base-ref scan.
413    #[arg(long)]
414    allow_shallow: bool,
415    /// Permit comparison when selected inventory records are incomplete.
416    #[arg(long)]
417    allow_incomplete_evidence: bool,
418    /// Maximum number of changed files and queue movements to show.
419    #[arg(long, default_value_t = 10)]
420    top: i64,
421    /// Output format.
422    #[arg(long, value_enum, default_value_t = CompareFormat::Text)]
423    format: CompareFormat,
424    /// Detail level for machine output.
425    #[arg(long, value_enum, default_value_t = CompareDetail::Top)]
426    detail: CompareDetail,
427    /// Zero-based record offset for --detail full.
428    #[arg(long, default_value_t = 0)]
429    offset: usize,
430    /// Maximum records per collection for --detail full.
431    #[arg(long, default_value_t = 1000)]
432    limit: usize,
433    /// Compare reports with incompatible identity or analyzer metadata.
434    #[arg(long)]
435    force: bool,
436    /// Include local filesystem report paths in output descriptors.
437    #[arg(long)]
438    include_local_paths: bool,
439    /// Include unchanged file and folder records in bounded compare collections.
440    #[arg(long)]
441    include_unchanged: bool,
442    /// Select which report supplies regression thresholds and evidence-drift policy.
443    #[arg(long, value_enum, default_value_t = PolicySource::Base)]
444    policy_from: PolicySource,
445    /// Exit 1 when an existing file worsens or a newly added file is a finding.
446    #[arg(long)]
447    fail_on_regression: bool,
448}
449
450#[derive(Debug, Args)]
451struct BaselineArgs {
452    /// Mutable state directory. Relative paths resolve from the repository root.
453    #[arg(long, value_name = "PATH", global = true)]
454    state_dir: Option<PathBuf>,
455    #[command(subcommand)]
456    command: BaselineCommand,
457}
458
459#[derive(Debug, Subcommand)]
460enum BaselineCommand {
461    /// Idempotently save a named baseline, failing closed when stored content differs.
462    Ensure {
463        /// Stable baseline name.
464        #[arg(long, default_value = "default")]
465        name: String,
466        /// Report path. Defaults to the durable latest report, then the Git-private first-run report.
467        #[arg(long)]
468        report: Option<PathBuf>,
469        /// Fail when the report does not match current HEAD, worktree, config, scope, or analyzer.
470        #[arg(long)]
471        require_current: bool,
472        /// Explicitly replace a differing stored baseline.
473        #[arg(long)]
474        replace: bool,
475        /// Permit a report produced from a dirty worktree.
476        #[arg(long)]
477        allow_dirty: bool,
478        /// Permit incomplete inventory or history evidence.
479        #[arg(long)]
480        allow_incomplete_evidence: bool,
481        /// Output format.
482        #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
483        format: DisplayFormat,
484    },
485    /// Create a named baseline from a validated report.
486    Create {
487        /// Stable baseline name.
488        #[arg(long, default_value = "default")]
489        name: String,
490        /// Report path. Defaults to the durable latest report, then the Git-private first-run report.
491        #[arg(long)]
492        report: Option<PathBuf>,
493        /// Fail when the report does not match current HEAD, worktree, config, scope, or analyzer.
494        #[arg(long)]
495        require_current: bool,
496        /// Replace an existing named baseline.
497        #[arg(long)]
498        force: bool,
499        /// Permit a report produced from a dirty worktree.
500        #[arg(long)]
501        allow_dirty: bool,
502        /// Permit incomplete inventory or history evidence.
503        #[arg(long)]
504        allow_incomplete_evidence: bool,
505        /// Output format.
506        #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
507        format: DisplayFormat,
508    },
509    /// Replace an existing named baseline from a validated report.
510    Update {
511        /// Stable baseline name.
512        #[arg(long, default_value = "default")]
513        name: String,
514        /// Report path. Defaults to the durable latest report, then the Git-private first-run report.
515        #[arg(long)]
516        report: Option<PathBuf>,
517        /// Fail when the report does not match current HEAD, worktree, config, scope, or analyzer.
518        #[arg(long)]
519        require_current: bool,
520        /// Permit a report produced from a dirty worktree.
521        #[arg(long)]
522        allow_dirty: bool,
523        /// Permit incomplete inventory or history evidence.
524        #[arg(long)]
525        allow_incomplete_evidence: bool,
526        /// Output format.
527        #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
528        format: DisplayFormat,
529    },
530    /// List named baselines with identity and readiness metadata.
531    List {
532        /// Output format.
533        #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
534        format: DisplayFormat,
535    },
536    /// Inspect baseline identity and evidence status.
537    Inspect {
538        /// Stable baseline name.
539        #[arg(long, default_value = "default")]
540        name: String,
541        /// Output format.
542        #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
543        format: DisplayFormat,
544    },
545    /// Validate a named baseline against the current report contract.
546    Validate {
547        /// Stable baseline name.
548        #[arg(long, default_value = "default")]
549        name: String,
550        /// Output format.
551        #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
552        format: DisplayFormat,
553    },
554    /// Remove a named baseline.
555    Remove {
556        /// Stable baseline name.
557        #[arg(long, default_value = "default")]
558        name: String,
559        /// Output format.
560        #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
561        format: DisplayFormat,
562        /// Apply the removal. Without this flag the command is a read-only preview.
563        #[arg(long)]
564        yes: bool,
565    },
566}
567
568#[derive(Debug, Args)]
569struct ReportArgs {
570    #[command(subcommand)]
571    command: ReportCommand,
572}
573
574#[derive(Debug, Subcommand)]
575enum ReportCommand {
576    /// Validate one report against the complete schema-5 contract.
577    Validate {
578        /// Report JSON to validate.
579        #[arg(value_name = "REPORT_JSON", required_unless_present = "report")]
580        path: Option<PathBuf>,
581        /// Report JSON to validate (alias for the positional path).
582        #[arg(long, value_name = "REPORT_JSON", required_unless_present = "path")]
583        report: Option<PathBuf>,
584        /// Accept schema 4 as migration input and validate its normalized schema-5 form.
585        #[arg(long)]
586        allow_legacy: bool,
587        /// Success output format.
588        #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
589        format: DisplayFormat,
590    },
591    /// Migrate a schema-4 report to normalized schema 5.
592    Migrate {
593        /// Legacy report to migrate.
594        #[arg(value_name = "REPORT_JSON")]
595        path: PathBuf,
596        /// Destination for the normalized schema-5 report.
597        #[arg(long, value_name = "PATH")]
598        output: PathBuf,
599    },
600    /// Print the published JSON Schema for report schema 5.
601    Schema,
602}
603
604#[derive(Debug, Args)]
605struct SarifArgs {
606    /// Report path. Defaults to the durable latest report, then the Git-private first-run report.
607    #[arg(long)]
608    report: Option<PathBuf>,
609    /// Fail when the report does not match current HEAD, worktree, config, scope, or analyzer.
610    #[arg(long)]
611    require_current: bool,
612    /// Maximum number of action-queue findings to export.
613    #[arg(long)]
614    top: Option<i64>,
615    /// Export configured policy failures or action-queue intervention candidates.
616    #[arg(long, value_enum, default_value_t = SarifScope::ActionQueue)]
617    scope: SarifScope,
618    /// Optional SARIF output path. Defaults to stdout.
619    #[arg(long)]
620    output: Option<PathBuf>,
621    /// Include the local source report path in SARIF invocation properties.
622    #[arg(long)]
623    include_local_paths: bool,
624}
625
626#[derive(Debug, Args)]
627struct HealthArgs {
628    /// Report path. Defaults to the durable latest report, then the Git-private first-run report.
629    #[arg(long)]
630    report: Option<PathBuf>,
631    /// Output suited for a job summary, workflow annotations, or automation.
632    #[arg(long, value_enum, default_value_t = HealthFormat::Text)]
633    format: HealthFormat,
634    /// Maximum number of GitHub workflow annotations to emit.
635    #[arg(long, default_value_t = 10)]
636    max_annotations: usize,
637    /// Fail when the report does not match current HEAD, worktree, config, scope, or analyzer.
638    #[arg(long)]
639    require_current: bool,
640}
641
642#[derive(Debug, Args)]
643struct ConfigArgs {
644    #[command(subcommand)]
645    command: ConfigCommand,
646}
647
648#[derive(Debug, Subcommand)]
649enum ConfigCommand {
650    /// Show configuration; --effective includes defaults.
651    Show {
652        /// Include defaults after applying repository overrides.
653        #[arg(long)]
654        effective: bool,
655    },
656    /// Validate the local configuration.
657    Validate,
658    /// Show only values that differ from defaults.
659    DiffDefaults,
660    /// Rewrite legacy schema configuration as a minimal schema-2 override.
661    Migrate {
662        /// Print the migrated configuration without writing it.
663        #[arg(long)]
664        dry_run: bool,
665        /// Do not retain the existing configuration as config.yaml.bak.
666        #[arg(long)]
667        no_backup: bool,
668    },
669    /// Print the supported configuration schema as JSON.
670    Schema,
671}
672
673#[derive(Debug, Args)]
674struct DoctorArgs {
675    /// Write a privacy-safe diagnostic JSON bundle; defaults to the active state root.
676    #[arg(long, num_args = 0..=1, default_missing_value = "__git_slop_active_state_bundle__")]
677    bundle: Option<PathBuf>,
678    /// Output format.
679    #[arg(long, value_enum, default_value_t = DoctorFormat::Text)]
680    format: DoctorFormat,
681    /// Estimate only this repo-relative scope.
682    #[arg(long)]
683    scope: Option<String>,
684    /// Return exit 2 when the latest report is valid but stale.
685    #[arg(long)]
686    require_current: bool,
687}
688
689#[derive(Debug, Clone, Copy, ValueEnum)]
690enum DoctorFormat {
691    Text,
692    Json,
693}
694
695include!("cli/list_args.rs");
696
697#[derive(Debug, Args)]
698struct PruneArgs {
699    /// Mutable state directory. Defaults to the same active root as find.
700    #[arg(long, value_name = "PATH")]
701    state_dir: Option<PathBuf>,
702    /// Number of newest run snapshots to retain; defaults to output.retention_runs.
703    #[arg(long)]
704    keep: Option<usize>,
705    /// Maximum total bytes retained; defaults to output.retention_bytes.
706    #[arg(long)]
707    max_bytes: Option<u64>,
708    /// Explicitly request preview behavior (preview is already the default).
709    #[arg(long, conflicts_with = "yes")]
710    dry_run: bool,
711    /// Apply the selected removals. Without this flag the command is read-only.
712    #[arg(long, conflicts_with = "dry_run")]
713    yes: bool,
714    /// Select text, JSON, or YAML output.
715    #[arg(long, value_enum, default_value_t = DisplayFormat::Text)]
716    format: DisplayFormat,
717}
718
719include!("cli/cache_args.rs");
720include!("cli/policy_args.rs");
721include!("cli/advice_args.rs");
722
723#[derive(Debug, Args)]
724struct CompletionsArgs {
725    /// Shell whose completion source should be generated.
726    shell: CompletionShell,
727}
728
729#[derive(Debug, Args)]
730struct ManArgs {
731    /// Destination file. Defaults to stdout.
732    #[arg(long)]
733    output: Option<PathBuf>,
734}
735
736#[derive(Debug, Args)]
737struct ReferenceArgs {
738    /// Index destination. Detailed command pages use the sibling stem directory.
739    /// Without an output, the complete reference is written to stdout.
740    #[arg(long)]
741    output: Option<PathBuf>,
742}
743
744#[derive(Debug, Args)]
745struct HtmlArgs {
746    /// Report path. Defaults to the durable latest report, then the Git-private first-run report.
747    #[arg(long)]
748    report: Option<PathBuf>,
749    /// Fail when the report does not match current HEAD, worktree, config, scope, or analyzer.
750    #[arg(long)]
751    require_current: bool,
752    /// Destination. Defaults beside the selected report.
753    #[arg(long)]
754    output: Option<PathBuf>,
755    /// Embed the local source report path in the otherwise portable HTML file.
756    #[arg(long)]
757    include_local_paths: bool,
758    /// Serve the report temporarily over a loopback-only HTTP endpoint.
759    #[arg(long)]
760    serve: bool,
761    /// Open the temporary loopback URL in the system browser.
762    #[arg(long, requires = "serve")]
763    open: bool,
764    /// Maximum lifetime of the temporary loopback server.
765    #[arg(long, default_value_t = 120, value_parser = clap::value_parser!(u64).range(1..=3600), requires = "serve")]
766    serve_seconds: u64,
767}
768
769include!("cli/formats.rs");
770include!("cli/support.rs");
771include!("cli/support/validation.rs");
772include!("cli/init.rs");
773include!("cli/analysis.rs");
774include!("cli/analysis/reporting.rs");
775include!("cli/analysis_receipt.rs");
776include!("cli/check.rs");
777include!("cli/baseline_compare.rs");
778include!("cli/reporting.rs");
779include!("cli/doctor/support.rs");
780include!("cli/doctor.rs");
781include!("cli/listing.rs");
782include!("cli/policy_cmd.rs");
783include!("cli/advice_cmd.rs");
784include!("cli/generation/server.rs");
785include!("cli/generation.rs");
786include!("cli/generation/artifacts.rs");
787include!("cli/generation/reference.rs");
788include!("cli/generation/reference/pages.rs");
789include!("cli/generation/reference/bundle.rs");
790include!("cli/entry.rs");
791
792#[cfg(test)]
793mod tests {
794    use clap::CommandFactory;
795
796    use super::Cli;
797
798    fn assert_descriptions(command: &clap::Command, path: &str) {
799        for argument in command.get_arguments() {
800            assert!(
801                argument
802                    .get_help()
803                    .is_some_and(|help| !help.to_string().trim().is_empty()),
804                "{path} argument {} has no generated reference description",
805                argument.get_id()
806            );
807        }
808        for subcommand in command.get_subcommands() {
809            assert_descriptions(subcommand, &format!("{path} {}", subcommand.get_name()));
810        }
811    }
812
813    #[test]
814    fn generated_reference_has_no_blank_argument_descriptions() {
815        assert_descriptions(&Cli::command(), "git-slop");
816    }
817}