use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser)]
#[command(
name = "ai-rsk",
version,
about = "Security gate for AI-generated code. Scans, blocks, and educates.",
long_about = "ai-rsk scans your project for security vulnerabilities, missing tools, and bad practices.\n\
It blocks the build until every issue is fixed, so AI-generated code is safe to deploy.\n\n\
Quick start:\n \
1. ai-rsk init Set up your project (hooks, config, LLM rules)\n \
2. ai-rsk Scan for issues (same as 'ai-rsk scan')\n \
3. ai-rsk update Keep ai-rsk up to date\n\n\
No subcommand needed: running 'ai-rsk' alone scans the current directory.",
after_help = "Examples:\n \
ai-rsk Scan current directory\n \
ai-rsk scan --strict Promote warnings to blockers\n \
ai-rsk scan --all Enable all compliance profiles\n \
ai-rsk scan --json JSON output for CI/CD\n \
ai-rsk init Set up security gate in your project\n \
ai-rsk update Update to latest version\n \
ai-rsk check Show installed tools and versions"
)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
#[command(after_help = "Examples:\n \
ai-rsk scan Scan current directory\n \
ai-rsk scan /path/to/project Scan a specific path\n \
ai-rsk scan --strict Warnings become blockers\n \
ai-rsk scan --full Everything becomes a blocker\n \
ai-rsk scan --json Machine-readable output for CI/CD\n \
ai-rsk scan --gdpr Add GDPR compliance checks\n \
ai-rsk scan --all All profiles (security+gdpr+ai-act+seo+a11y)\n\n\
Tip: just run 'ai-rsk' without arguments to scan the current directory.")]
Scan {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long)]
strict: bool,
#[arg(long)]
full: bool,
#[arg(long)]
json: bool,
#[arg(long)]
gdpr: bool,
#[arg(long)]
seo: bool,
#[arg(long)]
a11y: bool,
#[arg(long)]
ai_act: bool,
#[arg(long)]
all: bool,
#[arg(long)]
mode: Option<String>,
},
#[command(after_help = "Examples:\n \
ai-rsk init Set up the current project\n \
ai-rsk init /path/to/project Set up a specific project\n\n\
This creates: config file, security rules, LLM discipline files, git hooks.\n\
Run this once when you start using ai-rsk on a project.")]
Init {
#[arg(default_value = ".")]
path: PathBuf,
},
#[command(
after_help = "Shows the status of all tools ai-rsk needs (semgrep, gitleaks, osv-scanner, etc.).\n\
Useful for debugging when a scan fails because of a missing tool."
)]
Check {
#[arg(default_value = ".")]
path: PathBuf,
},
#[command(after_help = "Checks crates.io for a newer version and installs it.\n\
Uses cargo if available, otherwise downloads from GitHub Releases.")]
Update,
}
#[cfg(test)]
mod tests {
use super::*;
use clap::error::ErrorKind;
#[test]
fn test_scan_default_path() {
let cli = Cli::try_parse_from(["ai-rsk", "scan"]);
assert!(cli.is_ok());
if let Ok(cli) = cli {
match cli.command {
Commands::Scan {
path,
strict,
full,
json,
gdpr,
seo,
a11y,
ai_act,
all,
mode,
} => {
assert_eq!(path, PathBuf::from("."));
assert!(!strict);
assert!(!full);
assert!(!json);
assert!(!gdpr);
assert!(!seo);
assert!(!a11y);
assert!(!ai_act);
assert!(!all);
assert!(mode.is_none());
}
_ => panic!("Expected Scan command"),
}
}
}
#[test]
fn test_scan_with_strict() {
let cli = Cli::try_parse_from(["ai-rsk", "scan", "--strict"]);
assert!(cli.is_ok());
if let Ok(cli) = cli {
match cli.command {
Commands::Scan { strict, .. } => assert!(strict),
_ => panic!("Expected Scan command"),
}
}
}
#[test]
fn test_scan_with_full() {
let cli = Cli::try_parse_from(["ai-rsk", "scan", "--full"]);
assert!(cli.is_ok());
if let Ok(cli) = cli {
match cli.command {
Commands::Scan { full, .. } => assert!(full),
_ => panic!("Expected Scan command"),
}
}
}
#[test]
fn test_scan_with_json() {
let cli = Cli::try_parse_from(["ai-rsk", "scan", "--json"]);
assert!(cli.is_ok());
if let Ok(cli) = cli {
match cli.command {
Commands::Scan { json, .. } => assert!(json),
_ => panic!("Expected Scan command"),
}
}
}
#[test]
fn test_scan_with_custom_path() {
let cli = Cli::try_parse_from(["ai-rsk", "scan", "/tmp/myproject"]);
assert!(cli.is_ok());
if let Ok(cli) = cli {
match cli.command {
Commands::Scan { path, .. } => {
assert_eq!(path, PathBuf::from("/tmp/myproject"));
}
_ => panic!("Expected Scan command"),
}
}
}
#[test]
fn test_init_command() {
let cli = Cli::try_parse_from(["ai-rsk", "init"]);
assert!(cli.is_ok());
}
#[test]
fn test_check_command() {
let cli = Cli::try_parse_from(["ai-rsk", "check"]);
assert!(cli.is_ok());
}
#[test]
fn test_unknown_command_fails() {
let cli = Cli::try_parse_from(["ai-rsk", "nonexistent"]);
assert!(cli.is_err());
if let Err(e) = cli {
assert!(!matches!(
e.kind(),
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
));
}
}
#[test]
fn test_help_is_display_help() {
let cli = Cli::try_parse_from(["ai-rsk", "--help"]);
assert!(cli.is_err());
if let Err(e) = cli {
assert_eq!(e.kind(), ErrorKind::DisplayHelp);
}
}
#[test]
fn test_version_is_display_version() {
let cli = Cli::try_parse_from(["ai-rsk", "--version"]);
assert!(cli.is_err());
if let Err(e) = cli {
assert_eq!(e.kind(), ErrorKind::DisplayVersion);
}
}
}