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