use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
use clap_complete::{Shell, generate};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Parser, Debug, Clone)]
#[command(name = "prview")]
#[command(author = "vetcoders <hello@vetcoders.io>")]
#[command(version)]
#[command(about = "PR Review & Artifact Generator by vetcoders")]
#[command(args_conflicts_with_subcommands = true)]
#[command(subcommand_precedence_over_arg = true)]
#[command(
long_about = "PR Review & Artifact Generator by vetcoders - cross-language PR analysis tool\n\n\
Generates Artifact Pack v1 with structured, verifiable PR review artifacts:\n\n\
Artifact layout:\n\
00_summary/ RUN.json, MANIFEST.json, SANITY.json, MERGE_GATE,\n\
ARTIFACT_VERSION.txt, system/git meta, failures summary\n\
10_diff/ full.patch, per-commit-diffs/, per-file-diffs/\n\
20_quality/ <gate>.result.json + <gate>.log per check,\n\
full-checks.log, breaking changes, coverage delta\n\
30_context/ changed-tests.txt, optional INLINE_FINDINGS.sarif, tooling\n\
review.html Standard portable human review export\n\
dashboard.html Interactive HTML report (optional via --no-dashboard)\n\n\
Trustworthiness:\n\
- CheckProvenance: command, exit_code, cwd, timestamps per gate\n\
- Hard fail signatures: Rust panics, Node rejections, Python tracebacks,\n\
segfaults, sanitizers (live + cached results validated)\n\
- MANIFEST.json: SHA256 hashes of all generated files\n\
- SANITY.json: post-generation integrity checks (4 validators)\n\n\
Supported profiles: Rust, JavaScript/TypeScript, Python, Mixed, Generic (with Loctree heuristics)"
)]
#[command(after_help = "Examples:
prview Standard review (tests + lint, current branch vs develop/main)
prview --quick Quick review (skip tests/lint/heuristics)
prview --deep feature/x dev Full review with all checks
prview --skip-tests feature/x dev Review without tests
prview --no-dashboard Skip HTML dashboard generation
prview --pr 42 Analyze GitHub PR #42
prview --pr 42 --gh-repo owner/repo PR from specific repository
prview --update feat/x main Incremental update after new commits
prview --json --quiet Compact JSON summary for CI and agents
prview --ci CI mode: all checks, strict exit codes
prview runs List previous runs for the current repository
prview runs --all List runs across all repositories
prview open Open dashboard for the latest run
prview open <run-id> Open dashboard for a specific run
prview open <run-id> --dir Print the artifact directory path for a run
prview completions zsh Generate shell completions for zsh")]
pub struct Cli {
#[command(subcommand)]
pub command: Option<CliCommand>,
#[arg(index = 1)]
pub target: Option<String>,
#[arg(index = 2, num_args = 0..)]
pub bases: Vec<String>,
#[arg(long, conflicts_with_all = ["deep", "ci", "ai_only"])]
pub quick: bool,
#[arg(long, conflicts_with_all = ["quick", "ci", "ai_only"])]
pub deep: bool,
#[arg(long, conflicts_with_all = ["quick", "deep", "ai_only"])]
pub ci: bool,
#[arg(long = "ai-only", conflicts_with_all = ["quick", "deep", "ci"])]
pub ai_only: bool,
#[arg(long = "with-tests")]
pub with_tests: bool,
#[arg(long = "skip-tests", conflicts_with = "with_tests")]
pub skip_tests: bool,
#[arg(long = "with-lint")]
pub with_lint: bool,
#[arg(long = "skip-lint", conflicts_with = "with_lint")]
pub skip_lint: bool,
#[arg(long = "with-bundle")]
pub with_bundle: bool,
#[arg(long = "skip-bundle", conflicts_with = "with_bundle")]
pub skip_bundle: bool,
#[arg(
long = "with-security",
long_help = "Enable the heavy security posture. This raises the security tier used by \
--deep and --ci. cargo-geiger is NOT part of this tier — it is opt-in via \
--security-full. Lightweight checks like cargo-audit always run regardless."
)]
pub with_security: bool,
#[arg(long = "skip-security", conflicts_with = "with_security")]
pub skip_security: bool,
#[arg(
long = "security-full",
long_help = "Opt in to the full security tier, which adds cargo-geiger's unsafe-usage \
scan. Geiger can take several minutes on large dependency trees, so it is \
off by default even under --deep; source-side unsafe is already audited \
in-process. Without this flag geiger is simply absent from the profile."
)]
pub security_full: bool,
#[arg(long, value_enum, default_value = "auto")]
pub profile: Profile,
#[arg(long)]
pub pr: Option<u64>,
#[arg(long = "gh-repo", requires = "pr")]
pub gh_repo: Option<String>,
#[arg(short = 'R', long)]
pub remote: bool,
#[arg(long)]
pub update: bool,
#[arg(long)]
pub watch: bool,
#[arg(long = "no-fetch")]
pub no_fetch: bool,
#[arg(long = "local-only", conflicts_with = "remote_only")]
pub local_only: bool,
#[arg(long = "remote-only", conflicts_with = "local_only")]
pub remote_only: bool,
#[arg(long = "no-cache")]
pub no_cache: bool,
#[arg(short, long)]
pub quiet: bool,
#[arg(long)]
pub json: bool,
#[arg(long = "no-color")]
pub no_color: bool,
#[arg(long = "no-heuristics")]
pub no_heuristics: bool,
#[arg(long = "no-zip")]
pub no_zip: bool,
#[arg(long = "no-dashboard")]
pub no_dashboard: bool,
#[arg(long = "current-only")]
pub current_only: bool,
#[arg(long = "soft-exit")]
pub soft_exit: bool,
#[arg(long = "tests-pattern", value_name = "REGEX")]
pub tests_pattern: Option<String>,
#[arg(long = "pr-url", value_name = "URL")]
pub pr_url: Option<String>,
#[arg(long = "policy-file", value_name = "PATH")]
pub policy_file: Option<PathBuf>,
#[arg(long = "policy-mode", value_enum)]
pub policy_mode: Option<PolicyModeArg>,
#[arg(long = "bridge-stage", default_value_t = 0, hide = true)]
pub bridge_stage: u8,
#[arg(long)]
pub tui: bool,
#[arg(long = "output-dir", value_name = "PATH")]
pub output_dir: Option<PathBuf>,
#[arg(long = "shell-setup")]
pub shell_setup: bool,
#[arg(long = "why-blocked")]
pub why_blocked: bool,
}
#[derive(Subcommand, Debug, Clone, PartialEq, Eq)]
pub enum CliCommand {
State(StateArgs),
Doctor,
Runs(RunsArgs),
Open(OpenArgs),
Completions(CompletionsArgs),
Fix,
Init,
Scope(ScopeArgs),
Mcp(McpArgs),
}
#[derive(Args, Debug, Clone, PartialEq, Eq)]
pub struct McpArgs {}
#[derive(Args, Debug, Clone, PartialEq, Eq)]
pub struct ScopeArgs {
#[arg(long, num_args = 1..)]
pub include: Vec<String>,
#[arg(long, num_args = 1..)]
pub exclude: Vec<String>,
#[arg(long)]
pub wip: bool,
#[arg(long = "per-file")]
pub per_file: bool,
#[arg(long)]
pub base: Option<String>,
#[arg(short, long, default_value = "prview-scope")]
pub output: PathBuf,
}
#[derive(Args, Debug, Clone, PartialEq, Eq)]
pub struct CompletionsArgs {
#[arg(value_enum)]
pub shell: Shell,
}
impl CompletionsArgs {
pub fn run(&self) {
let mut cmd = Cli::command();
generate(self.shell, &mut cmd, "prview", &mut std::io::stdout());
}
}
#[derive(Args, Debug, Clone, PartialEq, Eq)]
pub struct StateArgs {
#[arg(long)]
pub fast: bool,
#[arg(long)]
pub json: bool,
#[arg(long)]
pub hot: bool,
#[arg(long)]
pub tui: bool,
#[arg(long = "repo", value_name = "PATH")]
pub repo_path: Option<PathBuf>,
}
#[derive(Args, Debug, Clone, PartialEq, Eq)]
pub struct RunsArgs {
#[arg(long, short = 'a')]
pub all: bool,
#[arg(long, short = 'b')]
pub branch: Option<String>,
#[arg(long, short = 's')]
pub status: Option<String>,
#[arg(long)]
pub json: bool,
#[arg(long)]
pub rebuild: bool,
}
#[derive(Args, Debug, Clone, PartialEq, Eq)]
pub struct OpenArgs {
pub run_id: Option<String>,
#[arg(long, short = 'd')]
pub dir: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
pub enum Profile {
#[default]
Auto,
Js,
Rust,
Python,
Mixed,
Generic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum PolicyModeArg {
Shadow,
Warn,
Block,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ExecutionMode {
Standard,
Quick,
Deep,
Ci,
AiOnly,
Update,
}
impl ExecutionMode {
pub fn as_str(self) -> &'static str {
match self {
Self::Standard => "standard",
Self::Quick => "quick",
Self::Deep => "deep",
Self::Ci => "ci",
Self::AiOnly => "ai-only",
Self::Update => "update",
}
}
}
impl Cli {
pub fn execution_mode(&self) -> ExecutionMode {
if self.quick {
ExecutionMode::Quick
} else if self.ai_only {
ExecutionMode::AiOnly
} else if self.update {
ExecutionMode::Update
} else if self.ci {
ExecutionMode::Ci
} else if self.deep {
ExecutionMode::Deep
} else {
ExecutionMode::Standard
}
}
pub fn color_disabled(&self) -> bool {
self.no_color || self.ci
}
pub(crate) fn uses_fast_remote_only_standard_preset(&self) -> bool {
matches!(self.execution_mode(), ExecutionMode::Standard)
&& (self.remote_only || (self.pr.is_some() && !self.local_only))
}
pub fn should_run_tests(&self) -> bool {
if self.skip_tests {
return false;
}
if self.with_tests {
return true;
}
if self.uses_fast_remote_only_standard_preset() {
return false;
}
!matches!(
self.execution_mode(),
ExecutionMode::Quick | ExecutionMode::AiOnly | ExecutionMode::Update
)
}
pub fn should_run_lint(&self) -> bool {
if self.skip_lint {
return false;
}
if self.with_lint {
return true;
}
!matches!(
self.execution_mode(),
ExecutionMode::Quick | ExecutionMode::AiOnly | ExecutionMode::Update
)
}
pub fn should_run_security(&self) -> bool {
if self.quick || self.ai_only || self.update {
return self.with_security; }
if self.skip_security {
return false;
}
self.with_security || self.deep || self.ci || self.security_full
}
pub fn should_run_bundle(&self) -> bool {
if self.quick || self.ai_only || self.update {
return false;
}
if self.skip_bundle {
return false;
}
self.with_bundle || self.ci
}
pub fn should_run_heuristics(&self) -> bool {
if self.quick || self.ai_only || self.update || self.no_heuristics {
return false;
}
if self.uses_fast_remote_only_standard_preset() {
return false;
}
true
}
pub fn effective_bases(&self) -> Vec<String> {
if self.bases.is_empty() {
vec![
"develop".to_string(),
"main".to_string(),
"master".to_string(),
]
} else {
self.bases.clone()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_cli() -> Cli {
Cli {
command: None,
target: None,
bases: vec![],
quick: false,
deep: false,
ci: false,
ai_only: false,
with_tests: false,
skip_tests: false,
with_lint: false,
skip_lint: false,
with_bundle: false,
skip_bundle: false,
with_security: false,
skip_security: false,
security_full: false,
profile: Profile::Auto,
pr: None,
gh_repo: None,
remote: false,
update: false,
watch: false,
no_fetch: false,
local_only: false,
remote_only: false,
no_cache: false,
quiet: false,
json: false,
no_color: false,
no_heuristics: false,
no_zip: false,
no_dashboard: false,
current_only: false,
soft_exit: false,
tests_pattern: None,
pr_url: None,
policy_file: None,
policy_mode: None,
bridge_stage: 0,
tui: false,
output_dir: None,
shell_setup: false,
why_blocked: false,
}
}
#[test]
fn test_should_run_tests_default() {
let cli = default_cli();
assert!(cli.should_run_tests());
}
#[test]
fn test_security_full_implies_security_in_default_mode() {
let mut cli = default_cli();
cli.security_full = true;
assert!(cli.should_run_security());
}
#[test]
fn test_skip_security_overrides_security_full() {
let mut cli = default_cli();
cli.security_full = true;
cli.skip_security = true;
assert!(!cli.should_run_security());
}
#[test]
fn test_quick_does_not_promote_security_full() {
let mut cli = default_cli();
cli.security_full = true;
cli.quick = true;
assert!(!cli.should_run_security());
}
#[test]
fn test_color_disabled_default_keeps_color() {
assert!(!default_cli().color_disabled());
}
#[test]
fn test_color_disabled_by_no_color_flag() {
let mut cli = default_cli();
cli.no_color = true;
assert!(cli.color_disabled());
}
#[test]
fn test_color_disabled_by_ci() {
let mut cli = default_cli();
cli.ci = true;
assert!(cli.color_disabled());
}
#[test]
fn test_should_run_tests_with_tests() {
let mut cli = default_cli();
cli.with_tests = true;
assert!(cli.should_run_tests());
}
#[test]
fn test_should_run_tests_deep_mode() {
let mut cli = default_cli();
cli.deep = true;
assert!(cli.should_run_tests());
}
#[test]
fn test_should_run_tests_ci_mode() {
let mut cli = default_cli();
cli.ci = true;
assert!(cli.should_run_tests());
}
#[test]
fn test_should_run_tests_with_tests_overrides_quick() {
let mut cli = default_cli();
cli.with_tests = true;
cli.quick = true;
assert!(cli.should_run_tests());
}
#[test]
fn test_should_run_tests_skip_tests() {
let mut cli = default_cli();
cli.deep = true;
cli.skip_tests = true;
assert!(!cli.should_run_tests());
}
#[test]
fn test_should_run_tests_remote_only_standard_disables_by_default() {
let mut cli = default_cli();
cli.remote_only = true;
assert!(!cli.should_run_tests());
}
#[test]
fn test_should_run_tests_remote_only_with_tests_overrides_default() {
let mut cli = default_cli();
cli.remote_only = true;
cli.with_tests = true;
assert!(cli.should_run_tests());
}
#[test]
fn test_should_run_tests_pr_standard_disables_by_default() {
let mut cli = default_cli();
cli.pr = Some(42);
assert!(!cli.should_run_tests());
}
#[test]
fn test_should_run_tests_pr_with_tests_overrides_default() {
let mut cli = default_cli();
cli.pr = Some(42);
cli.with_tests = true;
assert!(cli.should_run_tests());
}
#[test]
fn test_should_run_tests_pr_local_only_keeps_standard_behavior() {
let mut cli = default_cli();
cli.pr = Some(42);
cli.local_only = true;
assert!(cli.should_run_tests());
}
#[test]
fn test_should_run_lint_default() {
let cli = default_cli();
assert!(cli.should_run_lint());
}
#[test]
fn test_should_run_lint_with_lint() {
let mut cli = default_cli();
cli.with_lint = true;
assert!(cli.should_run_lint());
}
#[test]
fn test_should_run_lint_deep_mode() {
let mut cli = default_cli();
cli.deep = true;
assert!(cli.should_run_lint());
}
#[test]
fn test_should_run_lint_skip_lint() {
let mut cli = default_cli();
cli.with_lint = true;
cli.skip_lint = true;
assert!(!cli.should_run_lint());
}
#[test]
fn test_should_run_bundle_default() {
let cli = default_cli();
assert!(!cli.should_run_bundle());
}
#[test]
fn test_should_run_bundle_with_bundle() {
let mut cli = default_cli();
cli.with_bundle = true;
assert!(cli.should_run_bundle());
}
#[test]
fn test_should_run_bundle_ci_mode() {
let mut cli = default_cli();
cli.ci = true;
assert!(cli.should_run_bundle());
}
#[test]
fn test_should_run_heuristics_default() {
let cli = default_cli();
assert!(cli.should_run_heuristics());
}
#[test]
fn test_should_run_heuristics_quick_disables() {
let mut cli = default_cli();
cli.quick = true;
assert!(!cli.should_run_heuristics());
}
#[test]
fn test_should_run_heuristics_no_heuristics() {
let mut cli = default_cli();
cli.no_heuristics = true;
assert!(!cli.should_run_heuristics());
}
#[test]
fn test_should_run_heuristics_remote_only_standard_disables_by_default() {
let mut cli = default_cli();
cli.remote_only = true;
assert!(!cli.should_run_heuristics());
}
#[test]
fn test_should_run_heuristics_remote_only_deep_stays_enabled() {
let mut cli = default_cli();
cli.remote_only = true;
cli.deep = true;
assert!(cli.should_run_heuristics());
}
#[test]
fn test_should_run_heuristics_pr_standard_disables_by_default() {
let mut cli = default_cli();
cli.pr = Some(42);
assert!(!cli.should_run_heuristics());
}
#[test]
fn test_should_run_heuristics_pr_deep_stays_enabled() {
let mut cli = default_cli();
cli.pr = Some(42);
cli.deep = true;
assert!(cli.should_run_heuristics());
}
#[test]
fn test_parse_no_dashboard_flag() {
let cli = Cli::parse_from(["prview", "--no-dashboard"]);
assert!(cli.no_dashboard);
}
#[test]
fn test_effective_bases_default() {
let cli = default_cli();
let bases = cli.effective_bases();
assert_eq!(
bases,
vec![
"develop".to_string(),
"main".to_string(),
"master".to_string()
]
);
}
#[test]
fn test_effective_bases_custom() {
let mut cli = default_cli();
cli.bases = vec!["master".to_string(), "staging".to_string()];
let bases = cli.effective_bases();
assert_eq!(bases, vec!["master".to_string(), "staging".to_string()]);
}
#[test]
fn test_should_run_tests_with_tests_overrides_ai_only() {
let mut cli = default_cli();
cli.with_tests = true;
cli.ai_only = true;
assert!(cli.should_run_tests());
}
#[test]
fn test_should_run_tests_with_tests_overrides_update() {
let mut cli = default_cli();
cli.with_tests = true;
cli.update = true;
assert!(cli.should_run_tests());
}
#[test]
fn test_should_run_lint_with_lint_overrides_ai_only() {
let mut cli = default_cli();
cli.with_lint = true;
cli.ai_only = true;
assert!(cli.should_run_lint());
}
#[test]
fn test_should_run_lint_with_lint_overrides_update() {
let mut cli = default_cli();
cli.with_lint = true;
cli.update = true;
assert!(cli.should_run_lint());
}
#[test]
fn test_should_run_lint_ci_mode() {
let mut cli = default_cli();
cli.ci = true;
assert!(cli.should_run_lint());
}
#[test]
fn test_execution_mode_default() {
let cli = default_cli();
assert_eq!(cli.execution_mode(), ExecutionMode::Standard);
}
#[test]
fn test_execution_mode_update_overrides_deep() {
let mut cli = default_cli();
cli.deep = true;
cli.update = true;
assert_eq!(cli.execution_mode(), ExecutionMode::Update);
}
#[test]
fn test_should_run_bundle_skip_bundle() {
let mut cli = default_cli();
cli.with_bundle = true;
cli.skip_bundle = true;
assert!(!cli.should_run_bundle());
}
#[test]
fn test_should_run_bundle_ai_only_disables() {
let mut cli = default_cli();
cli.with_bundle = true;
cli.ai_only = true;
assert!(!cli.should_run_bundle());
}
#[test]
fn test_should_run_bundle_update_disables() {
let mut cli = default_cli();
cli.with_bundle = true;
cli.update = true;
assert!(!cli.should_run_bundle());
}
#[test]
fn test_should_run_bundle_deep_mode() {
let mut cli = default_cli();
cli.deep = true;
assert!(!cli.should_run_bundle()); }
#[test]
fn test_should_run_bundle_quick_disables() {
let mut cli = default_cli();
cli.ci = true;
cli.quick = true; assert!(!cli.should_run_bundle());
}
#[test]
fn test_should_run_heuristics_ai_only_disables() {
let mut cli = default_cli();
cli.ai_only = true;
assert!(!cli.should_run_heuristics());
}
#[test]
fn test_should_run_heuristics_update_disables() {
let mut cli = default_cli();
cli.update = true;
assert!(!cli.should_run_heuristics());
}
#[test]
fn test_profile_default() {
let cli = default_cli();
assert_eq!(cli.profile, Profile::Auto);
}
#[test]
fn test_effective_bases_single() {
let mut cli = default_cli();
cli.bases = vec!["feature".to_string()];
let bases = cli.effective_bases();
assert_eq!(bases.len(), 1);
}
#[test]
fn test_cli_clone() {
let cli = default_cli();
let cloned = cli.clone();
assert_eq!(cli.profile, cloned.profile);
assert_eq!(cli.quick, cloned.quick);
}
#[test]
fn test_parse_state_subcommand() {
let cli = Cli::try_parse_from(["prview", "state", "--fast", "--repo", "."]).unwrap();
assert_eq!(
cli.command,
Some(CliCommand::State(StateArgs {
fast: true,
json: false,
hot: false,
tui: false,
repo_path: Some(PathBuf::from(".")),
}))
);
assert_eq!(cli.target, None);
}
#[test]
fn test_parse_runs_subcommand() {
let cli =
Cli::try_parse_from(["prview", "runs", "--all", "--branch", "feature/test"]).unwrap();
assert_eq!(
cli.command,
Some(CliCommand::Runs(RunsArgs {
all: true,
branch: Some("feature/test".to_string()),
status: None,
json: false,
rebuild: false,
}))
);
}
#[test]
fn test_parse_open_subcommand() {
let cli = Cli::try_parse_from(["prview", "open", "20260306-010203", "--dir"]).unwrap();
assert_eq!(
cli.command,
Some(CliCommand::Open(OpenArgs {
run_id: Some("20260306-010203".to_string()),
dir: true,
}))
);
}
#[test]
fn test_parse_reserved_branch_name_with_double_dash() {
let cli = Cli::try_parse_from(["prview", "--", "state"]).unwrap();
assert_eq!(cli.command, None);
assert_eq!(cli.target, Some("state".to_string()));
}
}