cargo-rail 0.12.0

Graph-aware testing, dependency unification, and crate extraction for Rust monorepos
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
//! CLI argument definitions for cargo-rail.
//!
//! This module defines all CLI structures using clap. These are internal
//! and used by main.rs and the dispatch logic in commands/mod.rs.
//!
//! **Note:** This is not part of the stable public API.

use super::common::{OutputFormat, PlanOutputFormat, UnifyOutputFormat};
use crate::sync::ConflictStrategy;
use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::Shell;
use std::path::PathBuf;

const MAIN_HELP: &str = "\
Monorepo orchestration for Rust workspaces.

Quick start:
  cargo rail init              # Generate .config/rail.toml (default)
  cargo rail plan              # Build deterministic change plan
  cargo rail run               # Execute planner-selected surfaces
  cargo rail unify --check     # Preview dependency unification

Docs: https://github.com/loadingalias/cargo-rail";

/// Root CLI wrapper for cargo subcommand integration
///
/// This wrapper allows cargo-rail to be invoked as `cargo rail <subcommand>`.
#[derive(Parser)]
#[command(name = "cargo")]
#[command(bin_name = "cargo")]
#[command(styles = get_styles())]
pub enum CargoCli {
  /// The rail subcommand
  Rail(RailCli),
}

/// Main CLI structure for cargo-rail
///
/// Contains global options and the subcommand to execute.
#[derive(Parser)]
#[command(name = "rail")]
#[command(version)]
#[command(about = "Monorepo orchestration for Rust workspaces")]
#[command(long_about = MAIN_HELP)]
#[command(propagate_version = true)]
#[command(styles = get_styles())]
pub struct RailCli {
  /// Suppress progress messages (for CI/automation)
  #[arg(long, short, global = true)]
  pub quiet: bool,

  /// Output in JSON format (shorthand for -f json)
  #[arg(long, global = true)]
  pub json: bool,

  /// Path to rail.toml config file (bypass search order)
  #[arg(long, global = true, value_name = "PATH")]
  pub config: Option<PathBuf>,

  /// Workspace root directory (default: current directory)
  #[arg(long, global = true, value_name = "PATH")]
  pub workspace_root: Option<PathBuf>,

  /// The subcommand to execute
  #[command(subcommand)]
  pub command: Commands,
}

const RUN_HELP: &str = "\
Examples:
  cargo rail run                              # Execute planner-selected test surface
  cargo rail run --merge-base                 # Compare from branch point (CI)
  cargo rail run --surface build --surface test
  cargo rail run --profile ci                 # Built-in profile (local|ci|nightly)
  cargo rail run --workflow commit            # Resolve profile from [run.workflow.commit]
  cargo rail run --profile bench              # User-defined profile from [run.profile.bench]
  cargo rail run --all --surface test         # Force full test run
  cargo rail run --dry-run --print-cmd        # Preview exact execution
  cargo rail run -- --nocapture               # Pass args to underlying runner";

const PLAN_HELP: &str = "\
Examples:
  cargo rail plan                           # Changes since default branch
  cargo rail plan --merge-base              # Changes since branch point (CI recommended)
  cargo rail plan --confidence-profile strict  # Conservative planner profile
  cargo rail plan --since HEAD~5            # Changes in last 5 commits
  cargo rail plan --from abc --to def       # Changes between two SHAs
  cargo rail plan --explain                 # Show concise proof chain
  cargo rail plan -f json                   # Full machine-readable contract
  cargo rail plan -f github                 # Compact GitHub Actions key=value output
  cargo rail plan -f github-debug           # GitHub Actions output plus plan_json";

const UNIFY_HELP: &str = "\
Examples:
  cargo rail unify --check                # Preview changes (CI mode)
  cargo rail unify --check --explain      # Show why each decision was made
  cargo rail unify --check -f json -o out.json  # Write JSON output to file
  cargo rail unify                        # Apply changes
  cargo rail unify --backup               # Apply with backup
  cargo rail unify --show-diff            # Show manifest changes
  cargo rail unify undo                   # Restore from backup
  cargo rail unify undo --list            # List available backups";

const SPLIT_HELP: &str = "\
This is an advanced feature for extracting crates to standalone repositories
while preserving git history. Most teams should start with 'plan', 'run',
and 'unify' before using split/sync.

Examples:
  cargo rail split init my-crate          # Configure split for my-crate
  cargo rail split init my-crate --check  # Preview generated config
  cargo rail split run my-crate --check   # Preview the split
  cargo rail split run my-crate           # Execute the split
  cargo rail split run my-crate --yes     # Non-interactive apply confirmation
  cargo rail split run --all              # Split all configured crates";

const SYNC_HELP: &str = "\
This is an advanced feature for bidirectional sync between monorepo and split
repositories. Requires 'split' to be configured first.

Examples:
  cargo rail sync my-crate                # Bidirectional sync
  cargo rail sync my-crate --to-remote    # Push monorepo -> split repo
  cargo rail sync my-crate --from-remote  # Pull split repo -> monorepo (PR branch)
  cargo rail sync my-crate --to-remote --yes  # Non-interactive apply confirmation
  cargo rail sync --all                   # Sync all configured crates";

const RELEASE_HELP: &str = "\
Examples:
  cargo rail release init my-crate              # Configure release for my-crate
  cargo rail release check my-crate             # Validate release readiness
  cargo rail release check my-crate --extended  # Run extended checks (dry-run, MSRV)
  cargo rail release run my-crate --check       # Preview release plan
  cargo rail release run my-crate               # Release (patch bump)
  cargo rail release run my-crate --yes         # Non-interactive apply confirmation
  cargo rail release run my-crate --bump minor
  cargo rail release run my-crate --bump prerelease  # 1.0.0 -> 1.0.0-rc.1
  cargo rail release run my-crate --bump release     # 1.0.0-rc.2 -> 1.0.0
  cargo rail release run --all --bump patch     # Release all crates
  cargo rail release run my-crate --skip-publish  # Tag only, no crates.io";

const INIT_HELP: &str = "\
Examples:
  cargo rail init                       # Generate .config/rail.toml
  cargo rail init --check               # Preview generated config
  cargo rail init -o rail.toml          # Custom output path
  cargo rail init --force               # Overwrite existing config";

const CLEAN_HELP: &str = "\
Examples:
  cargo rail clean                      # Clean all artifacts
  cargo rail clean --cache              # Clean metadata cache only
  cargo rail clean --backups            # Prune old backups
  cargo rail clean --reports            # Clean generated reports
  cargo rail clean --check              # Preview what would be cleaned";

const CONFIG_HELP: &str = "\
Examples:
  cargo rail config locate              # Show which config file is active
  cargo rail config print               # Show effective config with defaults
  cargo rail config validate            # Validate rail.toml
  cargo rail config validate -f json    # JSON output for CI
  cargo rail config sync --check        # Preview config updates
  cargo rail config sync                # Add missing fields, sync targets";

const HASH_HELP: &str = "\
Examples:
  cargo rail hash                          # Hash current planner contract
  cargo rail hash --merge-base             # Hash planner contract at merge-base comparison
  cargo rail hash -f json                  # Structured hash output
  cargo rail diff-hash plan-a.json plan-b.json
  cargo rail diff-hash plan-a.json plan-b.json -f json";

const GRAPH_HELP: &str = "\
Examples:
  cargo rail graph                             # Planner reasoning graph (json)
  cargo rail graph --merge-base                # Graph against merge-base comparison
  cargo rail graph --dot                       # GraphViz DOT output
  cargo rail graph --since HEAD~3 -o graph.dot # Write graph output to file";

const COMPLETIONS_HELP: &str = "\
Examples:
  cargo rail completions bash           # Output bash completions
  cargo rail completions zsh            # Output zsh completions
  cargo rail completions fish           # Output fish completions
  cargo rail completions powershell     # Output PowerShell completions

Installation:
  # Bash (~/.bashrc)
  eval \"$(cargo rail completions bash)\"

  # Zsh (~/.zshrc)
  eval \"$(cargo rail completions zsh)\"

  # Fish (~/.config/fish/config.fish)
  cargo rail completions fish | source

  # PowerShell
  cargo rail completions powershell | Out-String | Invoke-Expression";

/// Available subcommands
#[derive(Subcommand)]
pub enum Commands {
  /// Execute planner-selected surfaces
  #[command(after_long_help = RUN_HELP)]
  Run {
    /// Git ref to compare against (auto-detects default branch)
    #[arg(long)]
    since: Option<String>,
    /// Use merge-base with default branch (better for feature branches)
    #[arg(long, conflicts_with = "since")]
    merge_base: bool,
    /// Skip change detection and run all workspace crates
    #[arg(long, short = 'a')]
    all: bool,
    /// Surface(s) to execute (repeatable)
    #[arg(long = "surface", value_name = "SURFACE")]
    surfaces: Vec<String>,
    /// Named profile to map to one or more surfaces
    #[arg(long, value_name = "PROFILE", conflicts_with_all = ["surfaces", "workflow"])]
    profile: Option<String>,
    /// Named workflow mapped to a profile via `[run.workflow]`
    #[arg(long, value_name = "WORKFLOW", conflicts_with_all = ["surfaces", "profile"])]
    workflow: Option<String>,
    /// Preview selected execution without spawning subprocesses
    #[arg(long)]
    dry_run: bool,
    /// Print command(s) prior to execution
    #[arg(long)]
    print_cmd: bool,
    /// Explain why surfaces and targets were selected
    #[arg(long)]
    explain: bool,
    /// Ignore binary-only crates (packages with `[[bin]]` but no lib target)
    #[arg(long)]
    ignore_bin_crates: bool,
    /// Disable automatic use of cargo-nextest
    #[arg(long)]
    skip_nextest: bool,
    /// Pass additional arguments to the selected runner
    #[arg(last = true)]
    run_args: Vec<String>,
  },

  /// Build a deterministic file-first change plan
  #[command(after_long_help = PLAN_HELP)]
  Plan {
    /// Git ref to compare against (auto-detects default branch)
    #[arg(long)]
    since: Option<String>,
    /// Start ref (for SHA pair mode)
    #[arg(long, conflicts_with = "since", requires = "to")]
    from: Option<String>,
    /// End ref (for SHA pair mode)
    #[arg(long, requires = "from")]
    to: Option<String>,
    /// Use merge-base with default branch (better for feature branches)
    #[arg(long, conflicts_with_all = ["since", "from", "to"])]
    merge_base: bool,
    /// Output format
    #[arg(long, short = 'f', default_value_t, value_enum)]
    format: PlanOutputFormat,
    /// Write output to file (overwrites existing content)
    #[arg(long, short = 'o', value_name = "PATH")]
    output: Option<PathBuf>,
    /// Show concise human reasoning chain
    #[arg(long)]
    explain: bool,
    /// Planner confidence profile override (strict|balanced|fast)
    #[arg(long, value_name = "PROFILE", value_parser = ["strict", "balanced", "fast"])]
    confidence_profile: Option<String>,
  },

  /// Unify workspace dependencies (replaces workspace-hack crates)
  #[command(after_long_help = UNIFY_HELP)]
  Unify {
    /// Subcommand (undo)
    #[command(subcommand)]
    command: Option<UnifyCommand>,
    /// Dry-run mode: preview changes without modifying files
    #[arg(long, short = 'c')]
    check: bool,
    /// Apply from a previously generated mutation plan file
    #[arg(long, value_name = "PATH", conflicts_with = "check")]
    plan: Option<PathBuf>,
    /// Output format
    #[arg(long, short = 'f', default_value_t, value_enum)]
    format: UnifyOutputFormat,
    /// Create backups of all modified files
    #[arg(long)]
    backup: bool,
    /// Skip generating the unify report
    #[arg(long)]
    skip_report: bool,
    /// Custom path for the unify report (default: target/cargo-rail/unify-report.md)
    #[arg(long)]
    report_path: Option<PathBuf>,
    /// Write output to file (overwrites existing content)
    #[arg(long, short = 'o', value_name = "PATH", requires = "check")]
    output: Option<PathBuf>,
    /// Show diff of changes to each manifest
    #[arg(long)]
    show_diff: bool,
    /// Explain why each decision was made
    #[arg(long)]
    explain: bool,
  },

  /// Initialize configuration (rail.toml)
  #[command(after_long_help = INIT_HELP)]
  Init {
    /// Output path for rail.toml
    #[arg(long, short, default_value = ".config/rail.toml")]
    output: String,
    /// Overwrite existing configuration
    #[arg(long)]
    force: bool,
    /// Dry-run mode: preview generated config without writing
    #[arg(long, short = 'c')]
    check: bool,
  },

  /// (Advanced) Split a crate to a standalone repository with git history
  #[command(after_long_help = SPLIT_HELP)]
  Split {
    /// Split subcommand
    #[command(subcommand)]
    command: SplitCommand,
  },

  /// (Advanced) Sync changes between monorepo and split repos
  #[command(after_long_help = SYNC_HELP)]
  Sync {
    /// Crate name to sync (mutually exclusive with --all)
    #[arg(conflicts_with = "all")]
    crate_name: Option<String>,
    /// Sync all configured crates (mutually exclusive with crate name)
    #[arg(short, long)]
    all: bool,
    /// Override remote repository
    #[arg(long)]
    remote: Option<String>,
    /// Sync from remote to monorepo only
    #[arg(long)]
    from_remote: bool,
    /// Sync from monorepo to remote only
    #[arg(long)]
    to_remote: bool,
    /// Conflict resolution strategy
    #[arg(long, default_value_t, value_enum)]
    strategy: ConflictStrategy,
    /// Dry-run mode: preview changes without executing
    #[arg(long, short = 'c')]
    check: bool,
    /// Apply from a previously generated mutation plan file
    #[arg(long, value_name = "PATH", conflicts_with = "check")]
    plan: Option<PathBuf>,
    /// Allow running on dirty worktree (uncommitted changes)
    #[arg(long)]
    allow_dirty: bool,
    /// Skip confirmation prompts (for CI/automation)
    #[arg(short = 'y', long)]
    yes: bool,
    /// Output format
    #[arg(long, short = 'f', default_value_t, value_enum)]
    format: OutputFormat,
  },

  /// Publish releases (version bump, changelog, tag, publish)
  #[command(after_long_help = RELEASE_HELP)]
  Release {
    /// Release subcommand
    #[command(subcommand)]
    command: ReleaseCommand,
  },

  /// Clean generated artifacts (cache, backups, reports)
  #[command(after_long_help = CLEAN_HELP)]
  Clean {
    /// Clean metadata cache only
    #[arg(long)]
    cache: bool,
    /// Prune old backups
    #[arg(long)]
    backups: bool,
    /// Clean generated reports
    #[arg(long)]
    reports: bool,
    /// Dry-run mode: preview what would be cleaned
    #[arg(long, short = 'c')]
    check: bool,
    /// Output format
    #[arg(long, short = 'f', default_value_t, value_enum)]
    format: OutputFormat,
  },

  /// Configuration management
  #[command(name = "config")]
  #[command(after_long_help = CONFIG_HELP)]
  Config {
    /// Subcommand
    #[command(subcommand)]
    command: ConfigCommand,
  },

  /// Hash and compare planner contracts
  #[command(after_long_help = HASH_HELP)]
  Hash {
    /// Git ref to compare against (auto-detects default branch)
    #[arg(long)]
    since: Option<String>,
    /// Start ref (for SHA pair mode)
    #[arg(long, conflicts_with = "since", requires = "to")]
    from: Option<String>,
    /// End ref (for SHA pair mode)
    #[arg(long, requires = "from")]
    to: Option<String>,
    /// Use merge-base with default branch (better for feature branches)
    #[arg(long, conflicts_with_all = ["since", "from", "to"])]
    merge_base: bool,
    /// Planner confidence profile override (strict|balanced|fast)
    #[arg(long, value_name = "PROFILE", value_parser = ["strict", "balanced", "fast"])]
    confidence_profile: Option<String>,
    /// Output format
    #[arg(long, short = 'f', default_value_t, value_enum)]
    format: OutputFormat,
  },

  /// Explain why two planner hashes differ
  #[command(after_long_help = HASH_HELP)]
  DiffHash {
    /// First planner JSON path
    a: PathBuf,
    /// Second planner JSON path
    b: PathBuf,
    /// Output format
    #[arg(long, short = 'f', default_value_t, value_enum)]
    format: OutputFormat,
  },

  /// Planner reasoning graph for explainability
  #[command(after_long_help = GRAPH_HELP)]
  Graph {
    /// Git ref to compare against (auto-detects default branch)
    #[arg(long)]
    since: Option<String>,
    /// Start ref (for SHA pair mode)
    #[arg(long, conflicts_with = "since", requires = "to")]
    from: Option<String>,
    /// End ref (for SHA pair mode)
    #[arg(long, requires = "from")]
    to: Option<String>,
    /// Use merge-base with default branch (better for feature branches)
    #[arg(long, conflicts_with_all = ["since", "from", "to"])]
    merge_base: bool,
    /// Planner confidence profile override (strict|balanced|fast)
    #[arg(long, value_name = "PROFILE", value_parser = ["strict", "balanced", "fast"])]
    confidence_profile: Option<String>,
    /// Output GraphViz DOT instead of JSON
    #[arg(long)]
    dot: bool,
    /// Write output to file (overwrites existing content)
    #[arg(long, short = 'o', value_name = "PATH")]
    output: Option<PathBuf>,
  },

  /// Generate shell completions
  #[command(after_long_help = COMPLETIONS_HELP)]
  Completions {
    /// Shell to generate completions for
    #[arg(value_enum, value_name = "SHELL")]
    shell: Shell,
  },
}

/// Subcommands for `cargo rail config`
#[derive(Subcommand)]
pub enum ConfigCommand {
  /// Print the path to the active config file
  ///
  /// Shows which config file is being used. Searches in order:
  /// rail.toml, .rail.toml, .cargo/rail.toml, .config/rail.toml
  Locate {
    /// Output format
    #[arg(long, short = 'f', default_value_t, value_enum)]
    format: OutputFormat,
  },
  /// Print the effective configuration with defaults
  ///
  /// Shows the merged configuration: user settings plus defaults for
  /// any unset fields. Useful for debugging and understanding what
  /// cargo-rail will actually use.
  Print {
    /// Output format
    #[arg(long, short = 'f', default_value_t, value_enum)]
    format: OutputFormat,
  },
  /// Validate the configuration file
  ///
  /// Checks for parse errors, unknown keys, and semantic issues.
  /// By default, unknown keys warn locally but error in CI environments
  /// (detected via CI, GITHUB_ACTIONS, GITLAB_CI, or CIRCLECI env vars).
  Validate {
    /// Output format
    #[arg(long, short = 'f', default_value_t, value_enum)]
    format: OutputFormat,
    /// Treat warnings as errors (auto-enabled in CI)
    #[arg(long, conflicts_with = "no_strict")]
    strict: bool,
    /// Never treat warnings as errors (overrides CI auto-detection)
    #[arg(long, conflicts_with = "strict")]
    no_strict: bool,
  },
  /// Sync configuration: add missing fields and update targets
  ///
  /// Scans the workspace for target triples and adds any missing config
  /// fields with their default values. Preserves all existing settings,
  /// comments, and formatting.
  ///
  /// Use this after upgrading cargo-rail to get new configuration options.
  Sync {
    /// Preview changes without modifying rail.toml
    #[arg(long, short = 'c')]
    check: bool,
    /// Output format
    #[arg(long, short = 'f', default_value_t, value_enum)]
    format: OutputFormat,
  },
}

/// Subcommands for `cargo rail unify`
#[derive(Subcommand)]
pub enum UnifyCommand {
  /// Restore manifests from a previous backup
  Undo {
    /// List available backups instead of restoring
    #[arg(long)]
    list: bool,
    /// Specific backup ID to restore (defaults to most recent)
    #[arg(long = "backup-id")]
    backup_id: Option<String>,
  },
}

/// Subcommands for `cargo rail split`
#[derive(Subcommand)]
pub enum SplitCommand {
  /// Configure split for crate(s)
  Init {
    /// Crate name(s) to configure
    #[arg(value_name = "CRATE")]
    crate_names: Vec<String>,
    /// Preview generated config without writing
    #[arg(long, short = 'c')]
    check: bool,
  },
  /// Execute split operation
  Run {
    /// Crate name to split (mutually exclusive with --all)
    #[arg(conflicts_with = "all", value_name = "CRATE")]
    crate_name: Option<String>,
    /// Split all configured crates
    #[arg(short, long)]
    all: bool,
    /// Override remote repository
    #[arg(long)]
    remote: Option<String>,
    /// Dry-run mode: preview changes
    #[arg(long, short = 'c')]
    check: bool,
    /// Apply from a previously generated mutation plan file
    #[arg(long, value_name = "PATH", conflicts_with = "check")]
    plan: Option<PathBuf>,
    /// Allow running on dirty worktree (uncommitted changes)
    #[arg(long)]
    allow_dirty: bool,
    /// Skip confirmation prompts (for CI/automation)
    #[arg(short = 'y', long)]
    yes: bool,
    /// Output format
    #[arg(long, short = 'f', default_value_t, value_enum)]
    format: OutputFormat,
  },
}

/// Subcommands for `cargo rail release`
#[derive(Subcommand)]
pub enum ReleaseCommand {
  /// Configure release settings
  Init {
    /// Crate name(s) to configure (optional)
    #[arg(value_name = "CRATE")]
    crate_names: Vec<String>,
    /// Preview generated config without writing
    #[arg(long, short = 'c')]
    check: bool,
  },
  /// Execute release (plan or publish)
  Run {
    /// Crate name(s) to release (mutually exclusive with --all)
    #[arg(conflicts_with = "all", value_name = "CRATE")]
    crate_names: Vec<String>,
    /// Release all workspace crates
    #[arg(short, long)]
    all: bool,
    /// Version bump [major, minor, patch, prerelease, release, or "x.y.z"]
    #[arg(long, default_value = "patch")]
    bump: String,
    /// Dry-run mode: preview release plan
    #[arg(long, short = 'c')]
    check: bool,
    /// Apply from a previously generated mutation plan file
    #[arg(long, value_name = "PATH", conflicts_with = "check")]
    plan: Option<PathBuf>,
    /// Skip publishing to crates.io
    #[arg(long)]
    skip_publish: bool,
    /// Skip git tag creation
    #[arg(long)]
    skip_tag: bool,
    /// Skip confirmation prompts and allow non-default branch
    #[arg(short = 'y', long)]
    yes: bool,
    /// Output format
    #[arg(long, short = 'f', default_value_t, value_enum)]
    format: OutputFormat,
  },
  /// Validate release readiness
  Check {
    /// Crate name(s) to check (mutually exclusive with --all)
    #[arg(conflicts_with = "all", value_name = "CRATE")]
    crate_names: Vec<String>,
    /// Check all workspace crates (mutually exclusive with crate names)
    #[arg(short, long)]
    all: bool,
    /// Run extended validation (cargo publish --dry-run, MSRV check)
    #[arg(long, short = 'e')]
    extended: bool,
    /// Output format
    #[arg(long, short = 'f', default_value_t, value_enum)]
    format: OutputFormat,
  },
}

fn get_styles() -> clap::builder::Styles {
  clap::builder::Styles::styled()
}

impl Commands {
  /// Check if this command uses JSON-like output format
  ///
  /// Returns true for any format that produces structured output.
  /// Used for early JSON mode detection to suppress progress messages.
  pub fn is_json_format(&self) -> bool {
    match self {
      Commands::Sync { format, .. } | Commands::Clean { format, .. } => format.is_json_like(),
      Commands::Plan { format, .. } => format.is_json_like(),
      Commands::Unify { format, .. } => format.is_json_like(),
      Commands::Split { command } => match command {
        SplitCommand::Init { .. } => false,
        SplitCommand::Run { format, .. } => format.is_json_like(),
      },
      Commands::Release { command } => match command {
        ReleaseCommand::Init { .. } => false,
        ReleaseCommand::Run { format, .. } | ReleaseCommand::Check { format, .. } => format.is_json_like(),
      },
      Commands::Config { command } => match command {
        ConfigCommand::Locate { format }
        | ConfigCommand::Print { format }
        | ConfigCommand::Validate { format, .. }
        | ConfigCommand::Sync { format, .. } => format.is_json_like(),
      },
      Commands::Hash { format, .. } | Commands::DiffHash { format, .. } => format.is_json_like(),
      Commands::Graph { dot, .. } => !dot,
      _ => false,
    }
  }

  /// Apply global --json flag by overriding format to Json
  pub fn apply_json_override(&mut self) {
    match self {
      Commands::Sync { format, .. } | Commands::Clean { format, .. } => *format = OutputFormat::Json,
      Commands::Plan { format, .. } => *format = PlanOutputFormat::Json,
      Commands::Unify { format, .. } => *format = UnifyOutputFormat::Json,
      Commands::Split {
        command: SplitCommand::Run { format, .. },
      } => *format = OutputFormat::Json,
      Commands::Split { .. } => {}
      Commands::Release {
        command: ReleaseCommand::Run { format, .. } | ReleaseCommand::Check { format, .. },
      } => *format = OutputFormat::Json,
      Commands::Release { .. } => {}
      Commands::Config { command } => match command {
        ConfigCommand::Locate { format }
        | ConfigCommand::Print { format }
        | ConfigCommand::Validate { format, .. }
        | ConfigCommand::Sync { format, .. } => *format = OutputFormat::Json,
      },
      Commands::Hash { format, .. } | Commands::DiffHash { format, .. } => *format = OutputFormat::Json,
      Commands::Graph { .. } => {}
      _ => {}
    }
  }
}

/// Generate shell completions and print to stdout
pub fn generate_completions(shell: Shell) {
  let mut cmd = CargoCli::command();
  clap_complete::generate(shell, &mut cmd, "cargo-rail", &mut std::io::stdout());
}