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