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