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";
#[derive(Parser)]
#[command(name = "cargo")]
#[command(bin_name = "cargo")]
#[command(styles = get_styles())]
pub enum CargoCli {
Rail(RailCli),
}
#[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 {
#[arg(long, short, global = true)]
pub quiet: bool,
#[arg(long, global = true)]
pub json: bool,
#[arg(long, global = true, value_name = "PATH")]
pub config: Option<PathBuf>,
#[arg(long, global = true, value_name = "PATH")]
pub workspace_root: Option<PathBuf>,
#[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";
#[derive(Subcommand)]
pub enum Commands {
#[command(after_long_help = RUN_HELP)]
Run {
#[arg(long)]
since: Option<String>,
#[arg(long, conflicts_with = "since")]
merge_base: bool,
#[arg(long, short = 'a')]
all: bool,
#[arg(long = "surface", value_name = "SURFACE")]
surfaces: Vec<String>,
#[arg(long, value_name = "PROFILE", conflicts_with_all = ["surfaces", "workflow"])]
profile: Option<String>,
#[arg(long, value_name = "WORKFLOW", conflicts_with_all = ["surfaces", "profile"])]
workflow: Option<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
print_cmd: bool,
#[arg(long)]
explain: bool,
#[arg(long)]
ignore_bin_crates: bool,
#[arg(long)]
skip_nextest: bool,
#[arg(last = true)]
run_args: Vec<String>,
},
#[command(after_long_help = PLAN_HELP)]
Plan {
#[arg(long)]
since: Option<String>,
#[arg(long, conflicts_with = "since", requires = "to")]
from: Option<String>,
#[arg(long, requires = "from")]
to: Option<String>,
#[arg(long, conflicts_with_all = ["since", "from", "to"])]
merge_base: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: PlanOutputFormat,
#[arg(long, short = 'o', value_name = "PATH")]
output: Option<PathBuf>,
#[arg(long)]
explain: bool,
#[arg(long, value_name = "PROFILE", value_parser = ["strict", "balanced", "fast"])]
confidence_profile: Option<String>,
},
#[command(after_long_help = UNIFY_HELP)]
Unify {
#[command(subcommand)]
command: Option<UnifyCommand>,
#[arg(long, short = 'c')]
check: bool,
#[arg(long, value_name = "PATH", conflicts_with = "check")]
plan: Option<PathBuf>,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: UnifyOutputFormat,
#[arg(long)]
backup: bool,
#[arg(long)]
skip_report: bool,
#[arg(long)]
report_path: Option<PathBuf>,
#[arg(long, short = 'o', value_name = "PATH", requires = "check")]
output: Option<PathBuf>,
#[arg(long)]
show_diff: bool,
#[arg(long)]
explain: bool,
},
#[command(after_long_help = INIT_HELP)]
Init {
#[arg(long, short, default_value = ".config/rail.toml")]
output: String,
#[arg(long)]
force: bool,
#[arg(long, short = 'c')]
check: bool,
},
#[command(after_long_help = SPLIT_HELP)]
Split {
#[command(subcommand)]
command: SplitCommand,
},
#[command(after_long_help = SYNC_HELP)]
Sync {
#[arg(conflicts_with = "all")]
crate_name: Option<String>,
#[arg(short, long)]
all: bool,
#[arg(long)]
remote: Option<String>,
#[arg(long)]
from_remote: bool,
#[arg(long)]
to_remote: bool,
#[arg(long, default_value_t, value_enum)]
strategy: ConflictStrategy,
#[arg(long, short = 'c')]
check: bool,
#[arg(long, value_name = "PATH", conflicts_with = "check")]
plan: Option<PathBuf>,
#[arg(long)]
allow_dirty: bool,
#[arg(short = 'y', long)]
yes: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: OutputFormat,
},
#[command(after_long_help = RELEASE_HELP)]
Release {
#[command(subcommand)]
command: ReleaseCommand,
},
#[command(after_long_help = CLEAN_HELP)]
Clean {
#[arg(long)]
cache: bool,
#[arg(long)]
backups: bool,
#[arg(long)]
reports: bool,
#[arg(long, short = 'c')]
check: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: OutputFormat,
},
#[command(name = "config")]
#[command(after_long_help = CONFIG_HELP)]
Config {
#[command(subcommand)]
command: ConfigCommand,
},
#[command(after_long_help = HASH_HELP)]
Hash {
#[arg(long)]
since: Option<String>,
#[arg(long, conflicts_with = "since", requires = "to")]
from: Option<String>,
#[arg(long, requires = "from")]
to: Option<String>,
#[arg(long, conflicts_with_all = ["since", "from", "to"])]
merge_base: bool,
#[arg(long, value_name = "PROFILE", value_parser = ["strict", "balanced", "fast"])]
confidence_profile: Option<String>,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: OutputFormat,
},
#[command(after_long_help = HASH_HELP)]
DiffHash {
a: PathBuf,
b: PathBuf,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: OutputFormat,
},
#[command(after_long_help = GRAPH_HELP)]
Graph {
#[arg(long)]
since: Option<String>,
#[arg(long, conflicts_with = "since", requires = "to")]
from: Option<String>,
#[arg(long, requires = "from")]
to: Option<String>,
#[arg(long, conflicts_with_all = ["since", "from", "to"])]
merge_base: bool,
#[arg(long, value_name = "PROFILE", value_parser = ["strict", "balanced", "fast"])]
confidence_profile: Option<String>,
#[arg(long)]
dot: bool,
#[arg(long, short = 'o', value_name = "PATH")]
output: Option<PathBuf>,
},
#[command(after_long_help = COMPLETIONS_HELP)]
Completions {
#[arg(value_enum, value_name = "SHELL")]
shell: Shell,
},
}
#[derive(Subcommand)]
pub enum ConfigCommand {
Locate {
#[arg(long, short = 'f', default_value_t, value_enum)]
format: OutputFormat,
},
Print {
#[arg(long, short = 'f', default_value_t, value_enum)]
format: OutputFormat,
},
Validate {
#[arg(long, short = 'f', default_value_t, value_enum)]
format: OutputFormat,
#[arg(long, conflicts_with = "no_strict")]
strict: bool,
#[arg(long, conflicts_with = "strict")]
no_strict: bool,
},
Sync {
#[arg(long, short = 'c')]
check: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: OutputFormat,
},
}
#[derive(Subcommand)]
pub enum UnifyCommand {
Undo {
#[arg(long)]
list: bool,
#[arg(long = "backup-id")]
backup_id: Option<String>,
},
}
#[derive(Subcommand)]
pub enum SplitCommand {
Init {
#[arg(value_name = "CRATE")]
crate_names: Vec<String>,
#[arg(long, short = 'c')]
check: bool,
},
Run {
#[arg(conflicts_with = "all", value_name = "CRATE")]
crate_name: Option<String>,
#[arg(short, long)]
all: bool,
#[arg(long)]
remote: Option<String>,
#[arg(long, short = 'c')]
check: bool,
#[arg(long, value_name = "PATH", conflicts_with = "check")]
plan: Option<PathBuf>,
#[arg(long)]
allow_dirty: bool,
#[arg(short = 'y', long)]
yes: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: OutputFormat,
},
}
#[derive(Subcommand)]
pub enum ReleaseCommand {
Init {
#[arg(value_name = "CRATE")]
crate_names: Vec<String>,
#[arg(long, short = 'c')]
check: bool,
},
Run {
#[arg(conflicts_with = "all", value_name = "CRATE")]
crate_names: Vec<String>,
#[arg(short, long)]
all: bool,
#[arg(long, default_value = "patch")]
bump: String,
#[arg(long, short = 'c')]
check: bool,
#[arg(long, value_name = "PATH", conflicts_with = "check")]
plan: Option<PathBuf>,
#[arg(long)]
skip_publish: bool,
#[arg(long)]
skip_tag: bool,
#[arg(short = 'y', long)]
yes: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: OutputFormat,
},
Check {
#[arg(conflicts_with = "all", value_name = "CRATE")]
crate_names: Vec<String>,
#[arg(short, long)]
all: bool,
#[arg(long, short = 'e')]
extended: bool,
#[arg(long, short = 'f', default_value_t, value_enum)]
format: OutputFormat,
},
}
fn get_styles() -> clap::builder::Styles {
clap::builder::Styles::styled()
}
impl Commands {
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,
}
}
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 { .. } => {}
_ => {}
}
}
}
pub fn generate_completions(shell: Shell) {
let mut cmd = CargoCli::command();
clap_complete::generate(shell, &mut cmd, "cargo-rail", &mut std::io::stdout());
}