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    /// 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!(
772                args.shared.project,
773                std::path::PathBuf::from("/tmp/project")
774            );
775        } else {
776            panic!("Expected Audit command");
777        }
778    }
779
780    #[test]
781    fn test_audit_args_defaults() {
782        let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
783        if let Commands::Audit(args) = cli.command {
784            assert!(!args.full);
785            assert!(!args.quick);
786            assert!(!args.summary);
787            assert!(!args.ai);
788            assert_eq!(args.ai_provider, "openai");
789            assert_eq!(args.ai_model, "gpt-5");
790            assert!(args.ai_api_key.is_none());
791            assert!(!args.exploit);
792            assert!(!args.gas);
793            assert!(!args.all_chains);
794            assert_eq!(args.sources, "src");
795            assert!(args.exclude.is_none());
796        } else {
797            panic!("Expected Audit command");
798        }
799    }
800
801    #[test]
802    fn test_audit_args_full() {
803        let cli = Cli::try_parse_from([
804            "forge-guard",
805            "audit",
806            "--full",
807            "--chain",
808            "arbitrum",
809            "--json",
810            "--report",
811            "--exploit",
812            "--gas",
813        ])
814        .unwrap();
815        if let Commands::Audit(args) = cli.command {
816            assert!(args.full);
817            assert!(args.exploit);
818            assert!(args.gas);
819            assert!(args.shared.json);
820            assert!(args.shared.report);
821            assert_eq!(args.shared.chain, "arbitrum");
822        } else {
823            panic!("Expected Audit command");
824        }
825    }
826
827    #[test]
828    fn test_audit_ai_args() {
829        let cli = Cli::try_parse_from([
830            "forge-guard",
831            "audit",
832            "--ai",
833            "--ai-provider",
834            "claude",
835            "--ai-model",
836            "claude-5-sonnet-20260701",
837            "--ai-full",
838        ])
839        .unwrap();
840        if let Commands::Audit(args) = cli.command {
841            assert!(args.ai);
842            assert_eq!(args.ai_provider, "claude");
843            assert_eq!(args.ai_model, "claude-5-sonnet-20260701");
844            assert!(args.ai_full);
845        } else {
846            panic!("Expected Audit command");
847        }
848    }
849
850    #[test]
851    fn test_deploy_args() {
852        let cli = Cli::try_parse_from([
853            "forge-guard",
854            "deploy",
855            "MyContract",
856            "--force",
857            "--salt",
858            "0xabc",
859            "--verify",
860            "--args",
861            "arg1,arg2",
862        ])
863        .unwrap();
864        if let Commands::Deploy(args) = cli.command {
865            assert_eq!(args.contract.as_deref(), Some("MyContract"));
866            assert!(args.force);
867            assert_eq!(args.salt.as_deref(), Some("0xabc"));
868            assert!(args.verify);
869            assert_eq!(args.args.as_deref(), Some("arg1,arg2"));
870        } else {
871            panic!("Expected Deploy command");
872        }
873    }
874
875    #[test]
876    fn test_fuzz_args() {
877        let cli = Cli::try_parse_from([
878            "forge-guard",
879            "fuzz",
880            "--runs",
881            "50000",
882            "--seed",
883            "42",
884            "--test",
885            "testFuzzDeposit",
886            "Vault",
887        ])
888        .unwrap();
889        if let Commands::Fuzz(args) = cli.command {
890            assert_eq!(args.runs, 50_000);
891            assert_eq!(args.seed, Some(42));
892            assert_eq!(args.test.as_deref(), Some("testFuzzDeposit"));
893            assert_eq!(args.contract.as_deref(), Some("Vault"));
894        } else {
895            panic!("Expected Fuzz command");
896        }
897    }
898
899    #[test]
900    fn test_simulate_args() {
901        let cli = Cli::try_parse_from([
902            "forge-guard",
903            "simulate",
904            "MyContract",
905            "--blocks",
906            "200",
907            "--mev",
908        ])
909        .unwrap();
910        if let Commands::Simulate(args) = cli.command {
911            assert_eq!(args.contract.as_deref(), Some("MyContract"));
912            assert_eq!(args.blocks, 200);
913            assert!(args.mev);
914        } else {
915            panic!("Expected Simulate command");
916        }
917    }
918
919    #[test]
920    fn test_invariant_args() {
921        let cli = Cli::try_parse_from([
922            "forge-guard",
923            "invariant",
924            "--runs",
925            "2000",
926            "--depth",
927            "150",
928            "--fail-on-revert",
929        ])
930        .unwrap();
931        if let Commands::Invariant(args) = cli.command {
932            assert_eq!(args.runs, 2000);
933            assert_eq!(args.depth, 150);
934            assert!(args.fail_on_revert);
935        } else {
936            panic!("Expected Invariant command");
937        }
938    }
939
940    #[test]
941    fn test_ci_args_defaults() {
942        let cli = Cli::try_parse_from(["forge-guard", "ci"]).unwrap();
943        if let Commands::Ci(args) = cli.command {
944            assert_eq!(args.platform, "github");
945            assert_eq!(args.output, std::path::PathBuf::from(".github/workflows"));
946            assert!(!args.include_deploy);
947            assert!(!args.overwrite);
948        } else {
949            panic!("Expected Ci command");
950        }
951    }
952
953    #[test]
954    fn test_ci_args_custom() {
955        let cli = Cli::try_parse_from([
956            "forge-guard",
957            "ci",
958            "--platform",
959            "gitlab",
960            "--include-deploy",
961            "--overwrite",
962            "--output",
963            ".gitlab",
964        ])
965        .unwrap();
966        if let Commands::Ci(args) = cli.command {
967            assert_eq!(args.platform, "gitlab");
968            assert!(args.include_deploy);
969            assert!(args.overwrite);
970            assert_eq!(args.output, std::path::PathBuf::from(".gitlab"));
971        } else {
972            panic!("Expected Ci command");
973        }
974    }
975
976    #[test]
977    fn test_benchmark_args() {
978        let cli = Cli::try_parse_from([
979            "forge-guard",
980            "benchmark",
981            "--iterations",
982            "50",
983            "--warmup",
984            "5",
985            "--module",
986            "pattern_matching",
987        ])
988        .unwrap();
989        if let Commands::Benchmark(args) = cli.command {
990            assert_eq!(args.iterations, 50);
991            assert_eq!(args.warmup, 5);
992            assert_eq!(args.module.as_deref(), Some("pattern_matching"));
993        } else {
994            panic!("Expected Benchmark command");
995        }
996    }
997
998    #[test]
999    fn test_gas_args() {
1000        let cli =
1001            Cli::try_parse_from(["forge-guard", "gas", "--all", "--warn-threshold", "100000"])
1002                .unwrap();
1003        if let Commands::Gas(args) = cli.command {
1004            assert!(args.all);
1005            assert_eq!(args.warn_threshold, 100000);
1006            assert!(args.contract.is_none());
1007        } else {
1008            panic!("Expected Gas command");
1009        }
1010    }
1011
1012    #[test]
1013    fn test_scan_args() {
1014        let cli = Cli::try_parse_from([
1015            "forge-guard",
1016            "scan",
1017            "--depth",
1018            "2",
1019            "--update",
1020            "--vulnerable-only",
1021            "--fail-fast",
1022        ])
1023        .unwrap();
1024        if let Commands::Scan(args) = cli.command {
1025            assert_eq!(args.depth, 2);
1026            assert!(args.update);
1027            assert!(args.vulnerable_only);
1028            assert!(args.fail_fast);
1029        } else {
1030            panic!("Expected Scan command");
1031        }
1032    }
1033
1034    #[test]
1035    fn test_upgrade_check_args() {
1036        let cli = Cli::try_parse_from([
1037            "forge-guard",
1038            "upgrade-check",
1039            "--storage-collision",
1040            "--uups",
1041            "--all",
1042        ])
1043        .unwrap();
1044        if let Commands::UpgradeCheck(args) = cli.command {
1045            assert!(args.storage_collision);
1046            assert!(args.uups);
1047            assert!(args.all);
1048        } else {
1049            panic!("Expected UpgradeCheck command");
1050        }
1051    }
1052
1053    #[test]
1054    fn test_doctor_args() {
1055        let cli = Cli::try_parse_from(["forge-guard", "doctor", "--fix", "--verbose"]).unwrap();
1056        if let Commands::Doctor(args) = cli.command {
1057            assert!(args.fix);
1058            assert!(args.verbose);
1059            assert!(args.check.is_none());
1060        } else {
1061            panic!("Expected Doctor command");
1062        }
1063    }
1064
1065    #[test]
1066    fn test_verify_args() {
1067        let cli = Cli::try_parse_from([
1068            "forge-guard",
1069            "verify",
1070            "0x1234",
1071            "MyContract",
1072            "--api-key",
1073            "test-key",
1074            "--all",
1075        ])
1076        .unwrap();
1077        if let Commands::Verify(args) = cli.command {
1078            assert_eq!(args.address.as_deref(), Some("0x1234"));
1079            assert_eq!(args.name.as_deref(), Some("MyContract"));
1080            assert_eq!(args.api_key.as_deref(), Some("test-key"));
1081            assert!(args.all);
1082        } else {
1083            panic!("Expected Verify command");
1084        }
1085    }
1086
1087    #[test]
1088    fn test_watch_args() {
1089        let cli = Cli::try_parse_from([
1090            "forge-guard",
1091            "watch",
1092            "--dirs",
1093            "src,test-contracts",
1094            "--debounce-ms",
1095            "1000",
1096            "--full",
1097        ])
1098        .unwrap();
1099        if let Commands::Watch(args) = cli.command {
1100            assert_eq!(args.dirs, "src,test-contracts");
1101            assert_eq!(args.debounce_ms, 1000);
1102            assert!(args.full);
1103        } else {
1104            panic!("Expected Watch command");
1105        }
1106    }
1107
1108    #[test]
1109    fn test_plugins_list() {
1110        let cli = Cli::try_parse_from(["forge-guard", "plugins", "list"]).unwrap();
1111        if let Commands::Plugins(args) = cli.command {
1112            assert!(matches!(args.action, Some(PluginAction::List)));
1113        } else {
1114            panic!("Expected Plugins command");
1115        }
1116    }
1117
1118    #[test]
1119    fn test_plugins_install() {
1120        let cli = Cli::try_parse_from(["forge-guard", "plugins", "install", "my-plugin"]).unwrap();
1121        if let Commands::Plugins(args) = cli.command {
1122            assert!(matches!(args.action, Some(PluginAction::Install { .. })));
1123        } else {
1124            panic!("Expected Plugins command");
1125        }
1126    }
1127
1128    #[test]
1129    fn test_plugins_remove() {
1130        let cli = Cli::try_parse_from(["forge-guard", "plugins", "remove", "bad-plugin"]).unwrap();
1131        if let Commands::Plugins(args) = cli.command {
1132            assert!(matches!(args.action, Some(PluginAction::Remove { .. })));
1133        } else {
1134            panic!("Expected Plugins command");
1135        }
1136    }
1137
1138    #[test]
1139    fn test_plugins_enable() {
1140        let cli = Cli::try_parse_from(["forge-guard", "plugins", "enable", "my-plugin"]).unwrap();
1141        if let Commands::Plugins(args) = cli.command {
1142            assert!(matches!(args.action, Some(PluginAction::Enable { .. })));
1143        } else {
1144            panic!("Expected Plugins command");
1145        }
1146    }
1147
1148    #[test]
1149    fn test_plugins_disable() {
1150        let cli = Cli::try_parse_from(["forge-guard", "plugins", "disable", "my-plugin"]).unwrap();
1151        if let Commands::Plugins(args) = cli.command {
1152            assert!(matches!(args.action, Some(PluginAction::Disable { .. })));
1153        } else {
1154            panic!("Expected Plugins command");
1155        }
1156    }
1157
1158    #[test]
1159    fn test_plugins_new() {
1160        let cli =
1161            Cli::try_parse_from(["forge-guard", "plugins", "new", "my-awesome-plugin"]).unwrap();
1162        if let Commands::Plugins(args) = cli.command {
1163            assert!(matches!(args.action, Some(PluginAction::New { .. })));
1164        } else {
1165            panic!("Expected Plugins command");
1166        }
1167    }
1168
1169    #[test]
1170    fn test_chain_list() {
1171        let cli = Cli::try_parse_from(["forge-guard", "chain", "list"]).unwrap();
1172        if let Commands::Chain(args) = cli.command {
1173            assert!(matches!(args.action, Some(ChainAction::List)));
1174        } else {
1175            panic!("Expected Chain command");
1176        }
1177    }
1178
1179    #[test]
1180    fn test_chain_info() {
1181        let cli = Cli::try_parse_from(["forge-guard", "chain", "info", "base"]).unwrap();
1182        if let Commands::Chain(args) = cli.command {
1183            assert!(matches!(args.action, Some(ChainAction::Info { .. })));
1184        } else {
1185            panic!("Expected Chain command");
1186        }
1187    }
1188
1189    #[test]
1190    fn test_chain_add() {
1191        let cli = Cli::try_parse_from([
1192            "forge-guard",
1193            "chain",
1194            "add",
1195            "my-chain",
1196            "https://rpc.my-chain.io",
1197            "99999",
1198        ])
1199        .unwrap();
1200        if let Commands::Chain(args) = cli.command {
1201            assert!(matches!(args.action, Some(ChainAction::Add { .. })));
1202        } else {
1203            panic!("Expected Chain command");
1204        }
1205    }
1206
1207    #[test]
1208    fn test_security_list() {
1209        let cli = Cli::try_parse_from(["forge-guard", "security", "list"]).unwrap();
1210        if let Commands::Security(args) = cli.command {
1211            assert!(matches!(args.action, Some(SecurityAction::List)));
1212        } else {
1213            panic!("Expected Security command");
1214        }
1215    }
1216
1217    #[test]
1218    fn test_security_threshold() {
1219        let cli = Cli::try_parse_from(["forge-guard", "security", "threshold", "85"]).unwrap();
1220        if let Commands::Security(args) = cli.command {
1221            assert!(matches!(
1222                args.action,
1223                Some(SecurityAction::Threshold { .. })
1224            ));
1225        } else {
1226            panic!("Expected Security command");
1227        }
1228    }
1229
1230    #[test]
1231    fn test_markdown_flag() {
1232        let cli = Cli::try_parse_from(["forge-guard", "audit", "--markdown", "--report"]).unwrap();
1233        if let Commands::Audit(args) = cli.command {
1234            assert!(args.shared.markdown);
1235            assert!(args.shared.report);
1236        } else {
1237            panic!("Expected Audit command");
1238        }
1239    }
1240
1241    #[test]
1242    fn test_quick_audit() {
1243        let cli = Cli::try_parse_from(["forge-guard", "audit", "--quick", "--summary"]).unwrap();
1244        if let Commands::Audit(args) = cli.command {
1245            assert!(args.quick);
1246            assert!(args.summary);
1247        } else {
1248            panic!("Expected Audit command");
1249        }
1250    }
1251
1252    #[test]
1253    fn test_all_chains_flag() {
1254        let cli = Cli::try_parse_from(["forge-guard", "audit", "--all-chains"]).unwrap();
1255        if let Commands::Audit(args) = cli.command {
1256            assert!(args.all_chains);
1257        } else {
1258            panic!("Expected Audit command");
1259        }
1260    }
1261
1262    #[test]
1263    fn test_deploy_safe_args() {
1264        let cli =
1265            Cli::try_parse_from(["forge-guard", "deploy-safe", "SecureVault", "--verify"]).unwrap();
1266        if let Commands::DeploySafe(args) = cli.command {
1267            assert_eq!(args.contract.as_deref(), Some("SecureVault"));
1268            assert!(args.verify);
1269        } else {
1270            panic!("Expected DeploySafe command");
1271        }
1272    }
1273
1274    #[test]
1275    fn test_report_args_json() {
1276        let cli = Cli::try_parse_from(["forge-guard", "report", "--format", "json"]).unwrap();
1277        if let Commands::Report(args) = cli.command {
1278            assert_eq!(args.format, "json");
1279            assert!(!args.summary);
1280        } else {
1281            panic!("Expected Report command");
1282        }
1283    }
1284
1285    #[test]
1286    fn test_verify_all_without_address() {
1287        let cli = Cli::try_parse_from(["forge-guard", "verify", "--all"]).unwrap();
1288        if let Commands::Verify(args) = cli.command {
1289            assert!(args.all);
1290            assert!(args.address.is_none());
1291        } else {
1292            panic!("Expected Verify command");
1293        }
1294    }
1295
1296    #[test]
1297    fn test_report_summary() {
1298        let cli = Cli::try_parse_from(["forge-guard", "report", "--summary"]).unwrap();
1299        if let Commands::Report(args) = cli.command {
1300            assert!(args.summary);
1301        } else {
1302            panic!("Expected Report command");
1303        }
1304    }
1305
1306    #[test]
1307    fn test_scan_vulnerable_only() {
1308        let cli = Cli::try_parse_from(["forge-guard", "scan", "--vulnerable-only"]).unwrap();
1309        if let Commands::Scan(args) = cli.command {
1310            assert!(args.vulnerable_only);
1311        } else {
1312            panic!("Expected Scan command");
1313        }
1314    }
1315
1316    #[test]
1317    fn test_doctor_check_category() {
1318        let cli =
1319            Cli::try_parse_from(["forge-guard", "doctor", "--check", "dependencies"]).unwrap();
1320        if let Commands::Doctor(args) = cli.command {
1321            assert_eq!(args.check.as_deref(), Some("dependencies"));
1322        } else {
1323            panic!("Expected Doctor command");
1324        }
1325    }
1326
1327    #[test]
1328    fn test_gas_contract_specific() {
1329        let cli =
1330            Cli::try_parse_from(["forge-guard", "gas", "Vault", "--diff", "prev.json"]).unwrap();
1331        if let Commands::Gas(args) = cli.command {
1332            assert_eq!(args.contract.as_deref(), Some("Vault"));
1333            assert_eq!(args.diff.as_deref(), Some("prev.json"));
1334        } else {
1335            panic!("Expected Gas command");
1336        }
1337    }
1338
1339    #[test]
1340    fn test_verify_with_constructor_args() {
1341        let cli = Cli::try_parse_from([
1342            "forge-guard",
1343            "verify",
1344            "0xabc",
1345            "Token",
1346            "--constructor-args",
1347            "0x0001",
1348        ])
1349        .unwrap();
1350        if let Commands::Verify(args) = cli.command {
1351            assert_eq!(args.constructor_args.as_deref(), Some("0x0001"));
1352        } else {
1353            panic!("Expected Verify command");
1354        }
1355    }
1356
1357    #[test]
1358    fn test_upgrade_check_proxy() {
1359        let cli =
1360            Cli::try_parse_from(["forge-guard", "upgrade-check", "0xproxy", "0ximpl"]).unwrap();
1361        if let Commands::UpgradeCheck(args) = cli.command {
1362            assert_eq!(args.proxy.as_deref(), Some("0xproxy"));
1363            assert_eq!(args.implementation.as_deref(), Some("0ximpl"));
1364        } else {
1365            panic!("Expected UpgradeCheck command");
1366        }
1367    }
1368
1369    #[test]
1370    fn test_benchmark_save_and_compare() {
1371        let cli = Cli::try_parse_from([
1372            "forge-guard",
1373            "benchmark",
1374            "--save",
1375            "results.json",
1376            "--compare",
1377            "baseline.json",
1378        ])
1379        .unwrap();
1380        if let Commands::Benchmark(args) = cli.command {
1381            assert_eq!(
1382                args.save.as_deref(),
1383                Some(std::path::Path::new("results.json"))
1384            );
1385            assert_eq!(
1386                args.compare.as_deref(),
1387                Some(std::path::Path::new("baseline.json"))
1388            );
1389        } else {
1390            panic!("Expected Benchmark command");
1391        }
1392    }
1393
1394    #[test]
1395    fn test_watch_exclude() {
1396        let cli = Cli::try_parse_from(["forge-guard", "watch", "--exclude", "*.test.sol"]).unwrap();
1397        if let Commands::Watch(args) = cli.command {
1398            assert_eq!(args.exclude.as_deref(), Some("*.test.sol"));
1399        } else {
1400            panic!("Expected Watch command");
1401        }
1402    }
1403
1404    #[test]
1405    fn test_simulate_deployer() {
1406        let cli =
1407            Cli::try_parse_from(["forge-guard", "simulate", "--deployer", "0xdeployer"]).unwrap();
1408        if let Commands::Simulate(args) = cli.command {
1409            assert_eq!(args.deployer.as_deref(), Some("0xdeployer"));
1410        } else {
1411            panic!("Expected Simulate command");
1412        }
1413    }
1414
1415    #[test]
1416    fn test_security_disable_check() {
1417        let cli = Cli::try_parse_from(["forge-guard", "security", "disable", "FA-H-001"]).unwrap();
1418        if let Commands::Security(args) = cli.command {
1419            assert!(matches!(args.action, Some(SecurityAction::Disable { .. })));
1420        } else {
1421            panic!("Expected Security command");
1422        }
1423    }
1424
1425    #[test]
1426    fn test_from_env_try_parse() {
1427        // Test that try_parse_from works identically to parse()
1428        // by verifying the convenience constructor's underlying mechanism
1429        let cli = Cli::try_parse_from(["forge-guard", "audit", "--report", "--json"]).unwrap();
1430        assert!(matches!(cli.command, Commands::Audit(_)));
1431        if let Commands::Audit(args) = cli.command {
1432            assert!(args.shared.report);
1433            assert!(args.shared.json);
1434        }
1435    }
1436}