Skip to main content

forge_guard/cli/
mod.rs

1//! CLI argument parsing and command dispatch.
2
3mod audit;
4mod benchmark;
5mod chain;
6mod ci;
7mod deploy;
8mod deploy_safe;
9mod doctor;
10mod fuzz;
11mod gas;
12mod invariant;
13mod plugins;
14mod report;
15mod scan;
16mod security;
17mod simulate;
18mod upgrade_check;
19mod verify;
20mod watch;
21
22use clap::{Parser, Subcommand};
23use std::path::PathBuf;
24
25/// Forge Guard — pre-deployment smart contract auditing for Foundry.
26#[derive(Parser, Debug)]
27#[command(
28    name = "forge-guard",
29    version,
30    about = "Pre-deployment smart contract auditing framework for Foundry",
31    long_about = "The most comprehensive pre-deployment smart contract auditing framework for Foundry.\n\nTransforms security auditing from an optional step into a mandatory pre-deployment process.\nBlocks unsafe deployments by default while providing detailed vulnerability reports.",
32    author
33)]
34pub struct Cli {
35    #[command(subcommand)]
36    pub command: Commands,
37}
38
39impl Cli {
40    /// Create a CLI instance from environment arguments.
41    pub fn from_env() -> Self {
42        Self::parse()
43    }
44
45    /// Run the selected command.
46    pub fn run(&self) -> anyhow::Result<()> {
47        use anyhow::Context;
48        match &self.command {
49            Commands::Audit(args) => audit::run(args).context("Audit failed"),
50            Commands::Deploy(args) => deploy::run(args).context("Deploy failed"),
51            Commands::DeploySafe(args) => deploy_safe::run(args).context("Safe deploy failed"),
52            Commands::Fuzz(args) => fuzz::run(args).context("Fuzzing failed"),
53            Commands::Invariant(args) => invariant::run(args).context("Invariant test failed"),
54            Commands::Simulate(args) => simulate::run(args).context("Simulation failed"),
55            Commands::Gas(args) => gas::run(args).context("Gas analysis failed"),
56            Commands::Report(args) => report::run(args).context("Report generation failed"),
57            Commands::Verify(args) => verify::run(args).context("Verification failed"),
58            Commands::Doctor(args) => doctor::run(args).context("Doctor analysis failed"),
59            Commands::Watch(args) => watch::run(args).context("Watch failed"),
60            Commands::Ci(args) => ci::run(args).context("CI generation failed"),
61            Commands::Benchmark(args) => benchmark::run(args).context("Benchmark failed"),
62            Commands::Scan(args) => scan::run(args).context("Scan failed"),
63            Commands::UpgradeCheck(args) => {
64                upgrade_check::run(args).context("Upgrade check failed")
65            }
66            Commands::Plugins(args) => plugins::run(args).context("Plugin operation failed"),
67            Commands::Chain(args) => chain::run(args).context("Chain operation failed"),
68            Commands::Security(args) => security::run(args).context("Security operation failed"),
69        }
70    }
71}
72
73/// All available subcommands.
74#[derive(Subcommand, Debug)]
75pub enum Commands {
76    /// Run a comprehensive security audit
77    Audit(AuditArgs),
78    /// Deploy contracts with automatic security checks
79    Deploy(DeployArgs),
80    /// Deploy with mandatory security pass requirement
81    #[command(name = "deploy-safe")]
82    DeploySafe(DeploySafeArgs),
83    /// Run fuzzing campaigns
84    Fuzz(FuzzArgs),
85    /// Run invariant tests
86    Invariant(InvariantArgs),
87    /// Run deployment simulations
88    Simulate(SimulateArgs),
89    /// Analyze gas usage
90    Gas(GasArgs),
91    /// Generate audit reports from existing results
92    Report(ReportArgs),
93    /// Verify contract deployments
94    Verify(VerifyArgs),
95    /// Analyze project health and configuration
96    Doctor(DoctorArgs),
97    /// Watch files for changes and re-audit
98    Watch(WatchArgs),
99    /// Generate CI/CD pipeline configurations
100    Ci(CiArgs),
101    /// Run performance benchmarks
102    Benchmark(BenchmarkArgs),
103    /// Scan dependencies for vulnerabilities
104    Scan(ScanArgs),
105    /// Analyze upgrade paths and proxy safety
106    #[command(name = "upgrade-check")]
107    UpgradeCheck(UpgradeCheckArgs),
108    /// Manage audit plugins
109    Plugins(PluginArgs),
110    /// Configure chain settings
111    Chain(ChainArgs),
112    /// Configure security settings
113    Security(SecurityArgs),
114}
115
116// ── Shared CLI Flags ─────────────────────────────────────────────
117
118/// Common flags shared across many commands.
119#[derive(Debug, Default, clap::Args)]
120pub struct SharedFlags {
121    /// Target chain to audit for
122    #[arg(long, global = true, default_value = "ethereum")]
123    pub chain: String,
124
125    /// Path to Foundry project root
126    #[arg(long, global = true, default_value = ".")]
127    pub project: PathBuf,
128
129    /// Output as JSON
130    #[arg(long, global = true)]
131    pub json: bool,
132
133    /// Output as Markdown
134    #[arg(long, global = true)]
135    pub markdown: bool,
136
137    /// Output as HTML
138    #[arg(long, global = true)]
139    pub html: bool,
140
141    /// Strict mode: fail on any finding
142    #[arg(long, global = true)]
143    pub strict: bool,
144
145    /// Offline mode: skip RPC calls
146    #[arg(long, global = true)]
147    pub offline: bool,
148
149    /// Production mode: extra checks
150    #[arg(long, global = true)]
151    pub production: bool,
152
153    /// Generate report file
154    #[arg(long, global = true)]
155    pub report: bool,
156
157    /// Number of parallel workers
158    #[arg(long, global = true, default_value = "4")]
159    pub parallelism: usize,
160}
161
162// ── Per-command args ─────────────────────────────────────────────
163
164#[derive(Debug, clap::Args)]
165pub struct AuditArgs {
166    #[command(flatten)]
167    pub shared: SharedFlags,
168
169    /// Run all available checks
170    #[arg(long, short)]
171    pub full: bool,
172
173    /// Quick mode — skip parser-heavy and expensive checks for fast results
174    #[arg(long)]
175    pub quick: bool,
176
177    /// Show executive summary instead of full terminal report
178    #[arg(long)]
179    pub summary: bool,
180
181    /// Enable AI-powered auditing (uses configured LLM provider)
182    #[arg(long)]
183    pub ai: bool,
184
185    /// AI provider: openai, claude, or ollama
186    #[arg(long, default_value = "openai")]
187    pub ai_provider: String,
188
189    /// AI model identifier (e.g. gpt-5, claude-5-sonnet-20260701)
190    #[arg(long, default_value = "gpt-5")]
191    pub ai_model: String,
192
193    /// AI API key (reads OPENAI_API_KEY or ANTHROPIC_API_KEY env var when empty)
194    #[arg(long)]
195    pub ai_api_key: Option<String>,
196
197    /// Ollama endpoint URL (default: http://localhost:11434)
198    #[arg(long)]
199    pub ollama_endpoint: Option<String>,
200
201    /// Run a full AI audit (security + gas + logic auditors)
202    #[arg(long)]
203    pub ai_full: bool,
204
205    /// Include exploit path analysis
206    #[arg(long)]
207    pub exploit: bool,
208
209    /// Include gas analysis
210    #[arg(long)]
211    pub gas: bool,
212
213    /// Audit all supported chains
214    #[arg(long)]
215    pub all_chains: bool,
216
217    /// Source directories to audit (comma-separated)
218    #[arg(long, default_value = "src")]
219    pub sources: String,
220
221    /// Exclude patterns (comma-separated)
222    #[arg(long)]
223    pub exclude: Option<String>,
224}
225
226#[derive(Debug, clap::Args)]
227pub struct DeployArgs {
228    #[command(flatten)]
229    pub shared: SharedFlags,
230
231    /// Contract name to deploy
232    pub contract: Option<String>,
233
234    /// Bypass deployment guard (warnings still shown)
235    #[arg(long)]
236    pub force: bool,
237
238    /// Constructor arguments (comma-separated)
239    #[arg(long)]
240    pub args: Option<String>,
241
242    /// Create2 salt
243    #[arg(long)]
244    pub salt: Option<String>,
245
246    /// Verify contract after deployment
247    #[arg(long)]
248    pub verify: bool,
249}
250
251#[derive(Debug, clap::Args)]
252pub struct DeploySafeArgs {
253    #[command(flatten)]
254    pub shared: SharedFlags,
255
256    /// Contract name to deploy
257    pub contract: Option<String>,
258
259    /// Constructor arguments (comma-separated)
260    #[arg(long)]
261    pub args: Option<String>,
262
263    /// Verify contract after deployment
264    #[arg(long)]
265    pub verify: bool,
266}
267
268#[derive(Debug, clap::Args)]
269pub struct FuzzArgs {
270    #[command(flatten)]
271    pub shared: SharedFlags,
272
273    /// Number of fuzz runs
274    #[arg(long, default_value = "10000")]
275    pub runs: u32,
276
277    /// Fuzz seed
278    #[arg(long)]
279    pub seed: Option<u64>,
280
281    /// Test function filter
282    #[arg(long)]
283    pub test: Option<String>,
284
285    /// Fuzz contract
286    pub contract: Option<String>,
287}
288
289#[derive(Debug, clap::Args)]
290pub struct InvariantArgs {
291    #[command(flatten)]
292    pub shared: SharedFlags,
293
294    /// Number of runs
295    #[arg(long, default_value = "1000")]
296    pub runs: u32,
297
298    /// Depth of calls per run
299    #[arg(long, default_value = "100")]
300    pub depth: u32,
301
302    /// Invariant contract
303    pub contract: Option<String>,
304
305    /// Fail on revert
306    #[arg(long)]
307    pub fail_on_revert: bool,
308}
309
310#[derive(Debug, clap::Args)]
311pub struct SimulateArgs {
312    #[command(flatten)]
313    pub shared: SharedFlags,
314
315    /// Number of simulation blocks
316    #[arg(long, default_value = "100")]
317    pub blocks: u32,
318
319    /// Simulate with specific deployer address
320    #[arg(long)]
321    pub deployer: Option<String>,
322
323    /// Include MEV analysis
324    #[arg(long)]
325    pub mev: bool,
326
327    /// Contract to simulate deployment for
328    pub contract: Option<String>,
329}
330
331#[derive(Debug, clap::Args)]
332pub struct GasArgs {
333    #[command(flatten)]
334    pub shared: SharedFlags,
335
336    /// Check specific contract
337    pub contract: Option<String>,
338
339    /// Compare with previous report
340    #[arg(long)]
341    pub diff: Option<String>,
342
343    /// Report gas for all functions
344    #[arg(long)]
345    pub all: bool,
346
347    /// Minimum gas threshold for warnings
348    #[arg(long, default_value = "50000")]
349    pub warn_threshold: u64,
350}
351
352#[derive(Debug, clap::Args)]
353pub struct ReportArgs {
354    #[command(flatten)]
355    pub shared: SharedFlags,
356
357    /// Path to audit result JSON
358    pub input: Option<PathBuf>,
359
360    /// Output report format
361    #[arg(long, default_value = "markdown")]
362    pub format: String,
363
364    /// Output file path
365    #[arg(long)]
366    pub output: Option<PathBuf>,
367
368    /// Include exploit paths
369    #[arg(long)]
370    pub exploit_paths: bool,
371
372    /// Summary only
373    #[arg(long)]
374    pub summary: bool,
375}
376
377#[derive(Debug, clap::Args)]
378pub struct VerifyArgs {
379    #[command(flatten)]
380    pub shared: SharedFlags,
381
382    /// Contract address to verify
383    pub address: Option<String>,
384
385    /// Contract name
386    pub name: Option<String>,
387
388    /// Explorer API key
389    #[arg(long)]
390    pub api_key: Option<String>,
391
392    /// Constructor arguments ABI-encoded
393    #[arg(long)]
394    pub constructor_args: Option<String>,
395
396    /// Check all deployments
397    #[arg(long)]
398    pub all: bool,
399}
400
401#[derive(Debug, clap::Args)]
402pub struct DoctorArgs {
403    #[command(flatten)]
404    pub shared: SharedFlags,
405
406    /// Fix issues automatically where possible
407    #[arg(long)]
408    pub fix: bool,
409
410    /// Verbose output
411    #[arg(long, short)]
412    pub verbose: bool,
413
414    /// Check only specific category
415    #[arg(long)]
416    pub check: Option<String>,
417}
418
419#[derive(Debug, clap::Args)]
420pub struct WatchArgs {
421    #[command(flatten)]
422    pub shared: SharedFlags,
423
424    /// Watch specific directories
425    #[arg(long, default_value = "src")]
426    pub dirs: String,
427
428    /// Debounce interval in ms
429    #[arg(long, default_value = "500")]
430    pub debounce_ms: u64,
431
432    /// Exclude patterns
433    #[arg(long)]
434    pub exclude: Option<String>,
435
436    /// Run full audit on change
437    #[arg(long)]
438    pub full: bool,
439}
440
441#[derive(Debug, clap::Args)]
442pub struct CiArgs {
443    #[command(flatten)]
444    pub shared: SharedFlags,
445
446    /// CI platform to generate for
447    #[arg(long, default_value = "github")]
448    pub platform: String,
449
450    /// Output directory for CI configs
451    #[arg(long, default_value = ".github/workflows")]
452    pub output: PathBuf,
453
454    /// Include deployment pipeline
455    #[arg(long)]
456    pub include_deploy: bool,
457
458    /// Overwrite existing files
459    #[arg(long)]
460    pub overwrite: bool,
461}
462
463#[derive(Debug, clap::Args)]
464pub struct BenchmarkArgs {
465    #[command(flatten)]
466    pub shared: SharedFlags,
467
468    /// Number of benchmark iterations
469    #[arg(long, default_value = "10")]
470    pub iterations: u32,
471
472    /// Compare with baseline
473    #[arg(long)]
474    pub compare: Option<PathBuf>,
475
476    /// Save benchmark results
477    #[arg(long)]
478    pub save: Option<PathBuf>,
479
480    /// Benchmark specific module
481    #[arg(long)]
482    pub module: Option<String>,
483
484    /// Warmup iterations
485    #[arg(long, default_value = "3")]
486    pub warmup: u32,
487}
488
489#[derive(Debug, clap::Args)]
490pub struct ScanArgs {
491    #[command(flatten)]
492    pub shared: SharedFlags,
493
494    /// Scan depth (0=direct, 1=direct+indirect, etc.)
495    #[arg(long, default_value = "1")]
496    pub depth: u32,
497
498    /// Update vulnerability database
499    #[arg(long)]
500    pub update: bool,
501
502    /// Output only vulnerable packages
503    #[arg(long)]
504    pub vulnerable_only: bool,
505
506    /// Fail on any vulnerability
507    #[arg(long)]
508    pub fail_fast: bool,
509}
510
511#[derive(Debug, clap::Args)]
512pub struct UpgradeCheckArgs {
513    #[command(flatten)]
514    pub shared: SharedFlags,
515
516    /// Proxy contract address
517    pub proxy: Option<String>,
518
519    /// Implementation contract address
520    pub implementation: Option<String>,
521
522    /// Check all proxies
523    #[arg(long)]
524    pub all: bool,
525
526    /// Check storage collision
527    #[arg(long)]
528    pub storage_collision: bool,
529
530    /// Check UUPS upgrade path
531    #[arg(long)]
532    pub uups: bool,
533}
534
535#[derive(Debug, clap::Args)]
536pub struct PluginArgs {
537    #[command(flatten)]
538    pub shared: SharedFlags,
539
540    /// Plugin subcommand
541    #[command(subcommand)]
542    pub action: Option<PluginAction>,
543}
544
545#[derive(Debug, Subcommand)]
546pub enum PluginAction {
547    /// List installed plugins
548    List,
549    /// Install a plugin
550    Install {
551        name: String,
552        source: Option<String>,
553    },
554    /// Remove a plugin
555    Remove { name: String },
556    /// Enable a plugin
557    Enable { name: String },
558    /// Disable a plugin
559    Disable { name: String },
560    /// Create a new plugin scaffold
561    New { name: String },
562}
563
564#[derive(Debug, clap::Args)]
565pub struct ChainArgs {
566    #[command(flatten)]
567    pub shared: SharedFlags,
568
569    /// Chain subcommand
570    #[command(subcommand)]
571    pub action: Option<ChainAction>,
572}
573
574#[derive(Debug, Subcommand)]
575pub enum ChainAction {
576    /// List supported chains
577    List,
578    /// Show chain information
579    Info { chain: String },
580    /// Add a custom chain
581    Add {
582        name: String,
583        rpc_url: Option<String>,
584        chain_id: Option<u64>,
585    },
586    /// Remove a custom chain
587    Remove { name: String },
588    /// Test chain RPC connectivity
589    Test {
590        chain: String,
591        rpc_url: Option<String>,
592    },
593}
594
595#[derive(Debug, clap::Args)]
596pub struct SecurityArgs {
597    #[command(flatten)]
598    pub shared: SharedFlags,
599
600    /// Security subcommand
601    #[command(subcommand)]
602    pub action: Option<SecurityAction>,
603}
604
605#[derive(Debug, Subcommand)]
606pub enum SecurityAction {
607    /// Show security configuration
608    Config,
609    /// Set security threshold
610    Threshold { score: u8 },
611    /// Enable a specific check
612    Enable { check: String },
613    /// Disable a specific check
614    Disable { check: String },
615    /// List all security checks
616    List,
617    /// Show security check details
618    Info { check: String },
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624    use clap::Parser;
625
626    #[test]
627    fn test_cli_parse_audit() {
628        let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
629        assert!(matches!(cli.command, Commands::Audit(_)));
630    }
631
632    #[test]
633    fn test_cli_parse_deploy() {
634        let cli = Cli::try_parse_from(["forge-guard", "deploy"]).unwrap();
635        assert!(matches!(cli.command, Commands::Deploy(_)));
636    }
637
638    #[test]
639    fn test_cli_parse_deploy_safe() {
640        let cli = Cli::try_parse_from(["forge-guard", "deploy-safe"]).unwrap();
641        assert!(matches!(cli.command, Commands::DeploySafe(_)));
642    }
643
644    #[test]
645    fn test_cli_parse_fuzz() {
646        let cli = Cli::try_parse_from(["forge-guard", "fuzz"]).unwrap();
647        assert!(matches!(cli.command, Commands::Fuzz(_)));
648    }
649
650    #[test]
651    fn test_cli_parse_invariant() {
652        let cli = Cli::try_parse_from(["forge-guard", "invariant"]).unwrap();
653        assert!(matches!(cli.command, Commands::Invariant(_)));
654    }
655
656    #[test]
657    fn test_cli_parse_simulate() {
658        let cli = Cli::try_parse_from(["forge-guard", "simulate"]).unwrap();
659        assert!(matches!(cli.command, Commands::Simulate(_)));
660    }
661
662    #[test]
663    fn test_cli_parse_gas() {
664        let cli = Cli::try_parse_from(["forge-guard", "gas"]).unwrap();
665        assert!(matches!(cli.command, Commands::Gas(_)));
666    }
667
668    #[test]
669    fn test_cli_parse_report() {
670        let cli = Cli::try_parse_from(["forge-guard", "report"]).unwrap();
671        assert!(matches!(cli.command, Commands::Report(_)));
672    }
673
674    #[test]
675    fn test_cli_parse_verify() {
676        let cli = Cli::try_parse_from(["forge-guard", "verify"]).unwrap();
677        assert!(matches!(cli.command, Commands::Verify(_)));
678    }
679
680    #[test]
681    fn test_cli_parse_doctor() {
682        let cli = Cli::try_parse_from(["forge-guard", "doctor"]).unwrap();
683        assert!(matches!(cli.command, Commands::Doctor(_)));
684    }
685
686    #[test]
687    fn test_cli_parse_watch() {
688        let cli = Cli::try_parse_from(["forge-guard", "watch"]).unwrap();
689        assert!(matches!(cli.command, Commands::Watch(_)));
690    }
691
692    #[test]
693    fn test_cli_parse_ci() {
694        let cli = Cli::try_parse_from(["forge-guard", "ci"]).unwrap();
695        assert!(matches!(cli.command, Commands::Ci(_)));
696    }
697
698    #[test]
699    fn test_cli_parse_benchmark() {
700        let cli = Cli::try_parse_from(["forge-guard", "benchmark"]).unwrap();
701        assert!(matches!(cli.command, Commands::Benchmark(_)));
702    }
703
704    #[test]
705    fn test_cli_parse_scan() {
706        let cli = Cli::try_parse_from(["forge-guard", "scan"]).unwrap();
707        assert!(matches!(cli.command, Commands::Scan(_)));
708    }
709
710    #[test]
711    fn test_cli_parse_upgrade_check() {
712        let cli = Cli::try_parse_from(["forge-guard", "upgrade-check"]).unwrap();
713        assert!(matches!(cli.command, Commands::UpgradeCheck(_)));
714    }
715
716    #[test]
717    fn test_cli_parse_plugins() {
718        let cli = Cli::try_parse_from(["forge-guard", "plugins"]).unwrap();
719        assert!(matches!(cli.command, Commands::Plugins(_)));
720    }
721
722    #[test]
723    fn test_cli_parse_chain() {
724        let cli = Cli::try_parse_from(["forge-guard", "chain"]).unwrap();
725        assert!(matches!(cli.command, Commands::Chain(_)));
726    }
727
728    #[test]
729    fn test_cli_parse_security() {
730        let cli = Cli::try_parse_from(["forge-guard", "security"]).unwrap();
731        assert!(matches!(cli.command, Commands::Security(_)));
732    }
733
734    #[test]
735    fn test_shared_flags_defaults() {
736        let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
737        if let Commands::Audit(args) = cli.command {
738            assert_eq!(args.shared.chain, "ethereum");
739            assert!(!args.shared.json);
740            assert!(!args.shared.markdown);
741            assert!(!args.shared.strict);
742            assert!(!args.shared.offline);
743            assert!(!args.shared.production);
744            assert!(!args.shared.report);
745            assert_eq!(args.shared.parallelism, 4);
746        } else {
747            panic!("Expected Audit command");
748        }
749    }
750
751    #[test]
752    fn test_shared_flags_custom() {
753        let cli = Cli::try_parse_from([
754            "forge-guard",
755            "audit",
756            "--chain",
757            "base",
758            "--json",
759            "--strict",
760            "--offline",
761            "--production",
762            "--parallelism",
763            "8",
764            "--project",
765            "/tmp/project",
766        ])
767        .unwrap();
768        if let Commands::Audit(args) = cli.command {
769            assert_eq!(args.shared.chain, "base");
770            assert!(args.shared.json);
771            assert!(args.shared.strict);
772            assert!(args.shared.offline);
773            assert!(args.shared.production);
774            assert_eq!(args.shared.parallelism, 8);
775            assert_eq!(
776                args.shared.project,
777                std::path::PathBuf::from("/tmp/project")
778            );
779        } else {
780            panic!("Expected Audit command");
781        }
782    }
783
784    #[test]
785    fn test_audit_args_defaults() {
786        let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
787        if let Commands::Audit(args) = cli.command {
788            assert!(!args.full);
789            assert!(!args.quick);
790            assert!(!args.summary);
791            assert!(!args.ai);
792            assert_eq!(args.ai_provider, "openai");
793            assert_eq!(args.ai_model, "gpt-5");
794            assert!(args.ai_api_key.is_none());
795            assert!(!args.exploit);
796            assert!(!args.gas);
797            assert!(!args.all_chains);
798            assert_eq!(args.sources, "src");
799            assert!(args.exclude.is_none());
800        } else {
801            panic!("Expected Audit command");
802        }
803    }
804
805    #[test]
806    fn test_audit_args_full() {
807        let cli = Cli::try_parse_from([
808            "forge-guard",
809            "audit",
810            "--full",
811            "--chain",
812            "arbitrum",
813            "--json",
814            "--report",
815            "--exploit",
816            "--gas",
817        ])
818        .unwrap();
819        if let Commands::Audit(args) = cli.command {
820            assert!(args.full);
821            assert!(args.exploit);
822            assert!(args.gas);
823            assert!(args.shared.json);
824            assert!(args.shared.report);
825            assert_eq!(args.shared.chain, "arbitrum");
826        } else {
827            panic!("Expected Audit command");
828        }
829    }
830
831    #[test]
832    fn test_audit_ai_args() {
833        let cli = Cli::try_parse_from([
834            "forge-guard",
835            "audit",
836            "--ai",
837            "--ai-provider",
838            "claude",
839            "--ai-model",
840            "claude-5-sonnet-20260701",
841            "--ai-full",
842        ])
843        .unwrap();
844        if let Commands::Audit(args) = cli.command {
845            assert!(args.ai);
846            assert_eq!(args.ai_provider, "claude");
847            assert_eq!(args.ai_model, "claude-5-sonnet-20260701");
848            assert!(args.ai_full);
849        } else {
850            panic!("Expected Audit command");
851        }
852    }
853
854    #[test]
855    fn test_deploy_args() {
856        let cli = Cli::try_parse_from([
857            "forge-guard",
858            "deploy",
859            "MyContract",
860            "--force",
861            "--salt",
862            "0xabc",
863            "--verify",
864            "--args",
865            "arg1,arg2",
866        ])
867        .unwrap();
868        if let Commands::Deploy(args) = cli.command {
869            assert_eq!(args.contract.as_deref(), Some("MyContract"));
870            assert!(args.force);
871            assert_eq!(args.salt.as_deref(), Some("0xabc"));
872            assert!(args.verify);
873            assert_eq!(args.args.as_deref(), Some("arg1,arg2"));
874        } else {
875            panic!("Expected Deploy command");
876        }
877    }
878
879    #[test]
880    fn test_fuzz_args() {
881        let cli = Cli::try_parse_from([
882            "forge-guard",
883            "fuzz",
884            "--runs",
885            "50000",
886            "--seed",
887            "42",
888            "--test",
889            "testFuzzDeposit",
890            "Vault",
891        ])
892        .unwrap();
893        if let Commands::Fuzz(args) = cli.command {
894            assert_eq!(args.runs, 50_000);
895            assert_eq!(args.seed, Some(42));
896            assert_eq!(args.test.as_deref(), Some("testFuzzDeposit"));
897            assert_eq!(args.contract.as_deref(), Some("Vault"));
898        } else {
899            panic!("Expected Fuzz command");
900        }
901    }
902
903    #[test]
904    fn test_simulate_args() {
905        let cli = Cli::try_parse_from([
906            "forge-guard",
907            "simulate",
908            "MyContract",
909            "--blocks",
910            "200",
911            "--mev",
912        ])
913        .unwrap();
914        if let Commands::Simulate(args) = cli.command {
915            assert_eq!(args.contract.as_deref(), Some("MyContract"));
916            assert_eq!(args.blocks, 200);
917            assert!(args.mev);
918        } else {
919            panic!("Expected Simulate command");
920        }
921    }
922
923    #[test]
924    fn test_invariant_args() {
925        let cli = Cli::try_parse_from([
926            "forge-guard",
927            "invariant",
928            "--runs",
929            "2000",
930            "--depth",
931            "150",
932            "--fail-on-revert",
933        ])
934        .unwrap();
935        if let Commands::Invariant(args) = cli.command {
936            assert_eq!(args.runs, 2000);
937            assert_eq!(args.depth, 150);
938            assert!(args.fail_on_revert);
939        } else {
940            panic!("Expected Invariant command");
941        }
942    }
943
944    #[test]
945    fn test_ci_args_defaults() {
946        let cli = Cli::try_parse_from(["forge-guard", "ci"]).unwrap();
947        if let Commands::Ci(args) = cli.command {
948            assert_eq!(args.platform, "github");
949            assert_eq!(args.output, std::path::PathBuf::from(".github/workflows"));
950            assert!(!args.include_deploy);
951            assert!(!args.overwrite);
952        } else {
953            panic!("Expected Ci command");
954        }
955    }
956
957    #[test]
958    fn test_ci_args_custom() {
959        let cli = Cli::try_parse_from([
960            "forge-guard",
961            "ci",
962            "--platform",
963            "gitlab",
964            "--include-deploy",
965            "--overwrite",
966            "--output",
967            ".gitlab",
968        ])
969        .unwrap();
970        if let Commands::Ci(args) = cli.command {
971            assert_eq!(args.platform, "gitlab");
972            assert!(args.include_deploy);
973            assert!(args.overwrite);
974            assert_eq!(args.output, std::path::PathBuf::from(".gitlab"));
975        } else {
976            panic!("Expected Ci command");
977        }
978    }
979
980    #[test]
981    fn test_benchmark_args() {
982        let cli = Cli::try_parse_from([
983            "forge-guard",
984            "benchmark",
985            "--iterations",
986            "50",
987            "--warmup",
988            "5",
989            "--module",
990            "pattern_matching",
991        ])
992        .unwrap();
993        if let Commands::Benchmark(args) = cli.command {
994            assert_eq!(args.iterations, 50);
995            assert_eq!(args.warmup, 5);
996            assert_eq!(args.module.as_deref(), Some("pattern_matching"));
997        } else {
998            panic!("Expected Benchmark command");
999        }
1000    }
1001
1002    #[test]
1003    fn test_gas_args() {
1004        let cli =
1005            Cli::try_parse_from(["forge-guard", "gas", "--all", "--warn-threshold", "100000"])
1006                .unwrap();
1007        if let Commands::Gas(args) = cli.command {
1008            assert!(args.all);
1009            assert_eq!(args.warn_threshold, 100000);
1010            assert!(args.contract.is_none());
1011        } else {
1012            panic!("Expected Gas command");
1013        }
1014    }
1015
1016    #[test]
1017    fn test_scan_args() {
1018        let cli = Cli::try_parse_from([
1019            "forge-guard",
1020            "scan",
1021            "--depth",
1022            "2",
1023            "--update",
1024            "--vulnerable-only",
1025            "--fail-fast",
1026        ])
1027        .unwrap();
1028        if let Commands::Scan(args) = cli.command {
1029            assert_eq!(args.depth, 2);
1030            assert!(args.update);
1031            assert!(args.vulnerable_only);
1032            assert!(args.fail_fast);
1033        } else {
1034            panic!("Expected Scan command");
1035        }
1036    }
1037
1038    #[test]
1039    fn test_upgrade_check_args() {
1040        let cli = Cli::try_parse_from([
1041            "forge-guard",
1042            "upgrade-check",
1043            "--storage-collision",
1044            "--uups",
1045            "--all",
1046        ])
1047        .unwrap();
1048        if let Commands::UpgradeCheck(args) = cli.command {
1049            assert!(args.storage_collision);
1050            assert!(args.uups);
1051            assert!(args.all);
1052        } else {
1053            panic!("Expected UpgradeCheck command");
1054        }
1055    }
1056
1057    #[test]
1058    fn test_doctor_args() {
1059        let cli = Cli::try_parse_from(["forge-guard", "doctor", "--fix", "--verbose"]).unwrap();
1060        if let Commands::Doctor(args) = cli.command {
1061            assert!(args.fix);
1062            assert!(args.verbose);
1063            assert!(args.check.is_none());
1064        } else {
1065            panic!("Expected Doctor command");
1066        }
1067    }
1068
1069    #[test]
1070    fn test_verify_args() {
1071        let cli = Cli::try_parse_from([
1072            "forge-guard",
1073            "verify",
1074            "0x1234",
1075            "MyContract",
1076            "--api-key",
1077            "test-key",
1078            "--all",
1079        ])
1080        .unwrap();
1081        if let Commands::Verify(args) = cli.command {
1082            assert_eq!(args.address.as_deref(), Some("0x1234"));
1083            assert_eq!(args.name.as_deref(), Some("MyContract"));
1084            assert_eq!(args.api_key.as_deref(), Some("test-key"));
1085            assert!(args.all);
1086        } else {
1087            panic!("Expected Verify command");
1088        }
1089    }
1090
1091    #[test]
1092    fn test_watch_args() {
1093        let cli = Cli::try_parse_from([
1094            "forge-guard",
1095            "watch",
1096            "--dirs",
1097            "src,test-contracts",
1098            "--debounce-ms",
1099            "1000",
1100            "--full",
1101        ])
1102        .unwrap();
1103        if let Commands::Watch(args) = cli.command {
1104            assert_eq!(args.dirs, "src,test-contracts");
1105            assert_eq!(args.debounce_ms, 1000);
1106            assert!(args.full);
1107        } else {
1108            panic!("Expected Watch command");
1109        }
1110    }
1111
1112    #[test]
1113    fn test_plugins_list() {
1114        let cli = Cli::try_parse_from(["forge-guard", "plugins", "list"]).unwrap();
1115        if let Commands::Plugins(args) = cli.command {
1116            assert!(matches!(args.action, Some(PluginAction::List)));
1117        } else {
1118            panic!("Expected Plugins command");
1119        }
1120    }
1121
1122    #[test]
1123    fn test_plugins_install() {
1124        let cli = Cli::try_parse_from(["forge-guard", "plugins", "install", "my-plugin"]).unwrap();
1125        if let Commands::Plugins(args) = cli.command {
1126            assert!(matches!(args.action, Some(PluginAction::Install { .. })));
1127        } else {
1128            panic!("Expected Plugins command");
1129        }
1130    }
1131
1132    #[test]
1133    fn test_plugins_remove() {
1134        let cli = Cli::try_parse_from(["forge-guard", "plugins", "remove", "bad-plugin"]).unwrap();
1135        if let Commands::Plugins(args) = cli.command {
1136            assert!(matches!(args.action, Some(PluginAction::Remove { .. })));
1137        } else {
1138            panic!("Expected Plugins command");
1139        }
1140    }
1141
1142    #[test]
1143    fn test_plugins_enable() {
1144        let cli = Cli::try_parse_from(["forge-guard", "plugins", "enable", "my-plugin"]).unwrap();
1145        if let Commands::Plugins(args) = cli.command {
1146            assert!(matches!(args.action, Some(PluginAction::Enable { .. })));
1147        } else {
1148            panic!("Expected Plugins command");
1149        }
1150    }
1151
1152    #[test]
1153    fn test_plugins_disable() {
1154        let cli = Cli::try_parse_from(["forge-guard", "plugins", "disable", "my-plugin"]).unwrap();
1155        if let Commands::Plugins(args) = cli.command {
1156            assert!(matches!(args.action, Some(PluginAction::Disable { .. })));
1157        } else {
1158            panic!("Expected Plugins command");
1159        }
1160    }
1161
1162    #[test]
1163    fn test_plugins_new() {
1164        let cli =
1165            Cli::try_parse_from(["forge-guard", "plugins", "new", "my-awesome-plugin"]).unwrap();
1166        if let Commands::Plugins(args) = cli.command {
1167            assert!(matches!(args.action, Some(PluginAction::New { .. })));
1168        } else {
1169            panic!("Expected Plugins command");
1170        }
1171    }
1172
1173    #[test]
1174    fn test_chain_list() {
1175        let cli = Cli::try_parse_from(["forge-guard", "chain", "list"]).unwrap();
1176        if let Commands::Chain(args) = cli.command {
1177            assert!(matches!(args.action, Some(ChainAction::List)));
1178        } else {
1179            panic!("Expected Chain command");
1180        }
1181    }
1182
1183    #[test]
1184    fn test_chain_info() {
1185        let cli = Cli::try_parse_from(["forge-guard", "chain", "info", "base"]).unwrap();
1186        if let Commands::Chain(args) = cli.command {
1187            assert!(matches!(args.action, Some(ChainAction::Info { .. })));
1188        } else {
1189            panic!("Expected Chain command");
1190        }
1191    }
1192
1193    #[test]
1194    fn test_chain_add() {
1195        let cli = Cli::try_parse_from([
1196            "forge-guard",
1197            "chain",
1198            "add",
1199            "my-chain",
1200            "https://rpc.my-chain.io",
1201            "99999",
1202        ])
1203        .unwrap();
1204        if let Commands::Chain(args) = cli.command {
1205            assert!(matches!(args.action, Some(ChainAction::Add { .. })));
1206        } else {
1207            panic!("Expected Chain command");
1208        }
1209    }
1210
1211    #[test]
1212    fn test_security_list() {
1213        let cli = Cli::try_parse_from(["forge-guard", "security", "list"]).unwrap();
1214        if let Commands::Security(args) = cli.command {
1215            assert!(matches!(args.action, Some(SecurityAction::List)));
1216        } else {
1217            panic!("Expected Security command");
1218        }
1219    }
1220
1221    #[test]
1222    fn test_security_threshold() {
1223        let cli = Cli::try_parse_from(["forge-guard", "security", "threshold", "85"]).unwrap();
1224        if let Commands::Security(args) = cli.command {
1225            assert!(matches!(
1226                args.action,
1227                Some(SecurityAction::Threshold { .. })
1228            ));
1229        } else {
1230            panic!("Expected Security command");
1231        }
1232    }
1233
1234    #[test]
1235    fn test_markdown_flag() {
1236        let cli = Cli::try_parse_from(["forge-guard", "audit", "--markdown", "--report"]).unwrap();
1237        if let Commands::Audit(args) = cli.command {
1238            assert!(args.shared.markdown);
1239            assert!(args.shared.report);
1240        } else {
1241            panic!("Expected Audit command");
1242        }
1243    }
1244
1245    #[test]
1246    fn test_quick_audit() {
1247        let cli = Cli::try_parse_from(["forge-guard", "audit", "--quick", "--summary"]).unwrap();
1248        if let Commands::Audit(args) = cli.command {
1249            assert!(args.quick);
1250            assert!(args.summary);
1251        } else {
1252            panic!("Expected Audit command");
1253        }
1254    }
1255
1256    #[test]
1257    fn test_all_chains_flag() {
1258        let cli = Cli::try_parse_from(["forge-guard", "audit", "--all-chains"]).unwrap();
1259        if let Commands::Audit(args) = cli.command {
1260            assert!(args.all_chains);
1261        } else {
1262            panic!("Expected Audit command");
1263        }
1264    }
1265
1266    #[test]
1267    fn test_deploy_safe_args() {
1268        let cli =
1269            Cli::try_parse_from(["forge-guard", "deploy-safe", "SecureVault", "--verify"]).unwrap();
1270        if let Commands::DeploySafe(args) = cli.command {
1271            assert_eq!(args.contract.as_deref(), Some("SecureVault"));
1272            assert!(args.verify);
1273        } else {
1274            panic!("Expected DeploySafe command");
1275        }
1276    }
1277
1278    #[test]
1279    fn test_report_args_json() {
1280        let cli = Cli::try_parse_from(["forge-guard", "report", "--format", "json"]).unwrap();
1281        if let Commands::Report(args) = cli.command {
1282            assert_eq!(args.format, "json");
1283            assert!(!args.summary);
1284        } else {
1285            panic!("Expected Report command");
1286        }
1287    }
1288
1289    #[test]
1290    fn test_verify_all_without_address() {
1291        let cli = Cli::try_parse_from(["forge-guard", "verify", "--all"]).unwrap();
1292        if let Commands::Verify(args) = cli.command {
1293            assert!(args.all);
1294            assert!(args.address.is_none());
1295        } else {
1296            panic!("Expected Verify command");
1297        }
1298    }
1299
1300    #[test]
1301    fn test_report_summary() {
1302        let cli = Cli::try_parse_from(["forge-guard", "report", "--summary"]).unwrap();
1303        if let Commands::Report(args) = cli.command {
1304            assert!(args.summary);
1305        } else {
1306            panic!("Expected Report command");
1307        }
1308    }
1309
1310    #[test]
1311    fn test_scan_vulnerable_only() {
1312        let cli = Cli::try_parse_from(["forge-guard", "scan", "--vulnerable-only"]).unwrap();
1313        if let Commands::Scan(args) = cli.command {
1314            assert!(args.vulnerable_only);
1315        } else {
1316            panic!("Expected Scan command");
1317        }
1318    }
1319
1320    #[test]
1321    fn test_doctor_check_category() {
1322        let cli =
1323            Cli::try_parse_from(["forge-guard", "doctor", "--check", "dependencies"]).unwrap();
1324        if let Commands::Doctor(args) = cli.command {
1325            assert_eq!(args.check.as_deref(), Some("dependencies"));
1326        } else {
1327            panic!("Expected Doctor command");
1328        }
1329    }
1330
1331    #[test]
1332    fn test_gas_contract_specific() {
1333        let cli =
1334            Cli::try_parse_from(["forge-guard", "gas", "Vault", "--diff", "prev.json"]).unwrap();
1335        if let Commands::Gas(args) = cli.command {
1336            assert_eq!(args.contract.as_deref(), Some("Vault"));
1337            assert_eq!(args.diff.as_deref(), Some("prev.json"));
1338        } else {
1339            panic!("Expected Gas command");
1340        }
1341    }
1342
1343    #[test]
1344    fn test_verify_with_constructor_args() {
1345        let cli = Cli::try_parse_from([
1346            "forge-guard",
1347            "verify",
1348            "0xabc",
1349            "Token",
1350            "--constructor-args",
1351            "0x0001",
1352        ])
1353        .unwrap();
1354        if let Commands::Verify(args) = cli.command {
1355            assert_eq!(args.constructor_args.as_deref(), Some("0x0001"));
1356        } else {
1357            panic!("Expected Verify command");
1358        }
1359    }
1360
1361    #[test]
1362    fn test_upgrade_check_proxy() {
1363        let cli =
1364            Cli::try_parse_from(["forge-guard", "upgrade-check", "0xproxy", "0ximpl"]).unwrap();
1365        if let Commands::UpgradeCheck(args) = cli.command {
1366            assert_eq!(args.proxy.as_deref(), Some("0xproxy"));
1367            assert_eq!(args.implementation.as_deref(), Some("0ximpl"));
1368        } else {
1369            panic!("Expected UpgradeCheck command");
1370        }
1371    }
1372
1373    #[test]
1374    fn test_benchmark_save_and_compare() {
1375        let cli = Cli::try_parse_from([
1376            "forge-guard",
1377            "benchmark",
1378            "--save",
1379            "results.json",
1380            "--compare",
1381            "baseline.json",
1382        ])
1383        .unwrap();
1384        if let Commands::Benchmark(args) = cli.command {
1385            assert_eq!(
1386                args.save.as_deref(),
1387                Some(std::path::Path::new("results.json"))
1388            );
1389            assert_eq!(
1390                args.compare.as_deref(),
1391                Some(std::path::Path::new("baseline.json"))
1392            );
1393        } else {
1394            panic!("Expected Benchmark command");
1395        }
1396    }
1397
1398    #[test]
1399    fn test_watch_exclude() {
1400        let cli = Cli::try_parse_from(["forge-guard", "watch", "--exclude", "*.test.sol"]).unwrap();
1401        if let Commands::Watch(args) = cli.command {
1402            assert_eq!(args.exclude.as_deref(), Some("*.test.sol"));
1403        } else {
1404            panic!("Expected Watch command");
1405        }
1406    }
1407
1408    #[test]
1409    fn test_simulate_deployer() {
1410        let cli =
1411            Cli::try_parse_from(["forge-guard", "simulate", "--deployer", "0xdeployer"]).unwrap();
1412        if let Commands::Simulate(args) = cli.command {
1413            assert_eq!(args.deployer.as_deref(), Some("0xdeployer"));
1414        } else {
1415            panic!("Expected Simulate command");
1416        }
1417    }
1418
1419    #[test]
1420    fn test_security_disable_check() {
1421        let cli = Cli::try_parse_from(["forge-guard", "security", "disable", "FA-H-001"]).unwrap();
1422        if let Commands::Security(args) = cli.command {
1423            assert!(matches!(args.action, Some(SecurityAction::Disable { .. })));
1424        } else {
1425            panic!("Expected Security command");
1426        }
1427    }
1428
1429    #[test]
1430    fn test_from_env_try_parse() {
1431        // Test that try_parse_from works identically to parse()
1432        // by verifying the convenience constructor's underlying mechanism
1433        let cli = Cli::try_parse_from(["forge-guard", "audit", "--report", "--json"]).unwrap();
1434        assert!(matches!(cli.command, Commands::Audit(_)));
1435        if let Commands::Audit(args) = cli.command {
1436            assert!(args.shared.report);
1437            assert!(args.shared.json);
1438        }
1439    }
1440}