Skip to main content

scope/cli/
mod.rs

1//! # CLI Module
2//!
3//! This module defines the command-line interface using `clap` with derive macros.
4//! It provides the main `Cli` struct and `Commands` enum that define all
5//! available commands and their arguments.
6//!
7//! ## UX Features
8//!
9//! - **Progress indicators** — Spinners and step counters via the `progress` module
10//! - **Help with examples** — `after_help` blocks with example invocations
11//! - **Command grouping** — Commands ordered by task (entity lookup, token
12//!   analysis, compliance, data/export, config)
13//! - **Shell completion** — `scope completions bash|zsh|fish` via `clap_complete`
14//! - **Typo suggestions** — Built-in clap fuzzy matching for misspelled commands
15//!
16//! ## Command Structure
17//!
18//! ```text
19//! scope [OPTIONS] <COMMAND>
20//!
21//! Entity lookup:
22//!   address      Analyze a blockchain address (alias: addr)
23//!   tx           Analyze a transaction (alias: transaction)
24//!   insights     Auto-detect target type and run analyses (alias: insight)
25//!
26//! Token analysis:
27//!   crawl        Crawl a token for DEX analytics (alias: token)
28//!   token-health DEX analytics + optional order book (alias: health)
29//!   discover     Browse trending/boosted tokens (alias: disc)
30//!   monitor      Live TUI dashboard (alias: mon)
31//!   market       Peg and order book health for stablecoins
32//!
33//! Compliance:
34//!   compliance   Risk, trace, analyze, compliance-report
35//!
36//! Data & export:
37//!   portfolio    Add, remove, list, summary (alias: port)
38//!   export       Export to JSON/CSV
39//!   report       Batch and combined reports
40//!
41//! Config & interactive:
42//!   interactive  REPL with preserved context (alias: shell)
43//!   setup        Configure API keys and preferences (alias: config)
44//!   completions  Generate shell completions for bash/zsh/fish
45//!
46//! Options:
47//!   --config <PATH>   Path to configuration file
48//!   -v, --verbose...  Increase logging verbosity
49//!   --ai              Markdown output for agent parsing
50//!   --no-color        Disable colored output
51//!   -h, --help        Print help (with examples)
52//!   -V, --version     Print version
53//! ```
54
55pub mod address;
56pub mod address_report;
57pub mod compliance;
58pub mod crawl;
59pub mod discover;
60pub mod export;
61pub mod insights;
62pub mod interactive;
63pub mod market;
64pub mod monitor;
65pub mod portfolio;
66pub mod progress;
67pub mod report;
68pub mod setup;
69pub mod token_health;
70pub mod tx;
71
72use clap::{Parser, Subcommand};
73use std::path::PathBuf;
74
75pub use address::AddressArgs;
76pub use crawl::CrawlArgs;
77pub use export::ExportArgs;
78pub use interactive::InteractiveArgs;
79pub use monitor::MonitorArgs;
80pub use portfolio::PortfolioArgs;
81pub use setup::SetupArgs;
82pub use tx::TxArgs;
83
84/// Blockchain Analysis CLI - A tool for blockchain data analysis.
85///
86/// Scope provides comprehensive blockchain analysis capabilities including
87/// address investigation, transaction decoding, portfolio tracking, and
88/// data export functionality.
89#[derive(Debug, Parser)]
90#[command(
91    name = "scope",
92    version,
93    about = "Scope Blockchain Analysis - A tool for blockchain data analysis",
94    long_about = format!(
95        "Scope Blockchain Analysis v{}\n\n\
96         A production-grade tool for blockchain data analysis, portfolio tracking,\n\
97         and transaction investigation.\n\n\
98         Use --help with any subcommand for detailed usage information.",
99        env!("CARGO_PKG_VERSION")
100    ),
101    after_help = "\x1b[1mExamples:\x1b[0m\n  \
102                  scope address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2\n  \
103                  scope crawl USDC --chain ethereum\n  \
104                  scope insights 0xabc123...\n  \
105                  scope monitor USDC\n  \
106                  scope compliance risk 0x742d...\n  \
107                  scope setup\n\n\
108                  \x1b[1mDocumentation:\x1b[0m\n  \
109                  https://github.com/robot-accomplice/scope-blockchain-analysis\n  \
110                  Quickstart guide: docs/QUICKSTART.md"
111)]
112pub struct Cli {
113    /// Subcommand to execute.
114    #[command(subcommand)]
115    pub command: Commands,
116
117    /// Path to configuration file.
118    ///
119    /// Overrides the default location (~/.config/scope/config.yaml).
120    #[arg(long, global = true, value_name = "PATH")]
121    pub config: Option<PathBuf>,
122
123    /// Increase logging verbosity.
124    ///
125    /// Can be specified multiple times:
126    /// -v    = INFO level
127    /// -vv   = DEBUG level
128    /// -vvv  = TRACE level
129    #[arg(short, long, global = true, action = clap::ArgAction::Count)]
130    pub verbose: u8,
131
132    /// Disable colored output.
133    #[arg(long, global = true)]
134    pub no_color: bool,
135
136    /// Output markdown to console for agent parsing.
137    ///
138    /// Forces all commands to emit markdown-formatted output to stdout
139    /// instead of tables or JSON. Useful for LLM/agent consumption.
140    #[arg(long, global = true)]
141    pub ai: bool,
142}
143
144/// Available CLI subcommands.
145#[derive(Debug, Subcommand)]
146pub enum Commands {
147    // -- Entity lookup --------------------------------------------------------
148    /// Analyze a blockchain address.
149    ///
150    /// Retrieves balance, transaction history, and token holdings
151    /// for the specified address.
152    #[command(visible_alias = "addr")]
153    Address(AddressArgs),
154
155    /// Analyze a transaction.
156    ///
157    /// Decodes transaction data, traces execution, and displays
158    /// detailed information about the transaction.
159    #[command(visible_alias = "transaction")]
160    Tx(TxArgs),
161
162    /// Unified insights: infer chain and type, run relevant analyses.
163    ///
164    /// Provide any target (address, tx hash, or token) and Scope will
165    /// detect it, run the appropriate analyses, and present observations.
166    #[command(visible_alias = "insight")]
167    Insights(insights::InsightsArgs),
168
169    // -- Token analysis -------------------------------------------------------
170    /// Crawl a token for analytics data.
171    ///
172    /// Retrieves comprehensive token information including top holders,
173    /// volume statistics, price data, and liquidity. Displays results
174    /// with ASCII charts and can generate markdown reports.
175    #[command(visible_alias = "token")]
176    Crawl(CrawlArgs),
177
178    /// Token health suite: DEX analytics + optional order book (crawl + market).
179    ///
180    /// Combines liquidity, volume, and holder data with optional market/peg
181    /// summary for stablecoins. Use --with-market for order book data.
182    #[command(visible_alias = "health")]
183    TokenHealth(token_health::TokenHealthArgs),
184
185    /// Discover trending and boosted tokens from DexScreener.
186    ///
187    /// Browse featured token profiles, recently boosted tokens,
188    /// or top boosted tokens by activity.
189    #[command(visible_alias = "disc")]
190    Discover(discover::DiscoverArgs),
191
192    /// Live token monitor with real-time TUI dashboard.
193    ///
194    /// Launches a terminal UI with price/volume charts, buy/sell gauges,
195    /// transaction feed, and more. Shortcut for `scope interactive` + `monitor <token>`.
196    #[command(visible_alias = "mon")]
197    Monitor(MonitorArgs),
198
199    /// Peg and order book health for stablecoin markets.
200    ///
201    /// Fetches level-2 depth from exchange APIs and reports
202    /// peg deviation, spread, depth, and configurable health checks.
203    #[command(subcommand)]
204    Market(market::MarketCommands),
205
206    // -- Compliance -----------------------------------------------------------
207    /// Compliance and risk analysis commands.
208    ///
209    /// Assess risk, trace taint, detect patterns, and generate
210    /// compliance reports for blockchain addresses.
211    #[command(subcommand)]
212    Compliance(compliance::ComplianceCommands),
213
214    // -- Data & export --------------------------------------------------------
215    /// Portfolio management commands.
216    ///
217    /// Add, remove, and list watched addresses. View aggregated
218    /// portfolio balances across multiple chains.
219    #[command(visible_alias = "port")]
220    Portfolio(PortfolioArgs),
221
222    /// Export analysis data.
223    ///
224    /// Export transaction history, balances, or analysis results
225    /// to various formats (JSON, CSV).
226    Export(ExportArgs),
227
228    /// Batch and combined report generation.
229    #[command(subcommand)]
230    Report(report::ReportCommands),
231
232    // -- Config & interactive -------------------------------------------------
233    /// Interactive mode with preserved context.
234    ///
235    /// Launch a REPL where chain, format, and other settings persist
236    /// between commands for faster workflow.
237    #[command(visible_alias = "shell")]
238    Interactive(InteractiveArgs),
239
240    /// Configure Scope settings and API keys.
241    ///
242    /// Run the setup wizard to configure API keys and preferences,
243    /// or use --status to view current configuration.
244    #[command(visible_alias = "config")]
245    Setup(SetupArgs),
246
247    /// Generate shell completions for bash, zsh, or fish.
248    ///
249    /// Output shell completion script to stdout for the specified shell.
250    /// Source the output in your shell profile for tab completion.
251    Completions(CompletionsArgs),
252
253    /// Start the web UI server (localhost).
254    ///
255    /// Serves the same Scope features as the CLI via a local web interface
256    /// at http://127.0.0.1:8080 (default). Use --daemon to run in the
257    /// background, --stop to halt a running daemon.
258    #[command(visible_alias = "serve")]
259    Web(WebArgs),
260}
261
262/// Arguments for the web server command.
263#[derive(Debug, Clone, clap::Args)]
264pub struct WebArgs {
265    /// Port to listen on.
266    #[arg(long, short, default_value = "8080")]
267    pub port: u16,
268
269    /// Address to bind to.
270    ///
271    /// Use 127.0.0.1 for local-only (default) or 0.0.0.0 for LAN access.
272    #[arg(long, default_value = "127.0.0.1")]
273    pub bind: String,
274
275    /// Run as a background daemon.
276    #[arg(long, short)]
277    pub daemon: bool,
278
279    /// Stop a running daemon.
280    #[arg(long)]
281    pub stop: bool,
282}
283
284/// Arguments for the completions command.
285#[derive(Debug, Clone, clap::Args)]
286pub struct CompletionsArgs {
287    /// The shell to generate completions for.
288    #[arg(value_enum)]
289    pub shell: clap_complete::Shell,
290}
291
292impl Cli {
293    /// Parses CLI arguments from the environment.
294    ///
295    /// This is a convenience wrapper around `clap::Parser::parse()`.
296    pub fn parse_args() -> Self {
297        Self::parse()
298    }
299
300    /// Returns the log level based on verbosity flag.
301    ///
302    /// Maps the `-v` count to tracing log levels:
303    /// - 0: WARN (default)
304    /// - 1: INFO
305    /// - 2: DEBUG
306    /// - 3+: TRACE
307    pub fn log_level(&self) -> tracing::Level {
308        match self.verbose {
309            0 => tracing::Level::WARN,
310            1 => tracing::Level::INFO,
311            2 => tracing::Level::DEBUG,
312            _ => tracing::Level::TRACE,
313        }
314    }
315}
316
317// ============================================================================
318// Unit Tests
319// ============================================================================
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324    use clap::Parser;
325
326    #[test]
327    fn test_cli_parse_address_command() {
328        let cli = Cli::try_parse_from([
329            "scope",
330            "address",
331            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
332        ])
333        .unwrap();
334
335        assert!(matches!(cli.command, Commands::Address(_)));
336        assert!(cli.config.is_none());
337        assert_eq!(cli.verbose, 0);
338    }
339
340    #[test]
341    fn test_cli_parse_address_alias() {
342        let cli = Cli::try_parse_from([
343            "scope",
344            "addr",
345            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
346        ])
347        .unwrap();
348
349        assert!(matches!(cli.command, Commands::Address(_)));
350    }
351
352    #[test]
353    fn test_cli_parse_tx_command() {
354        let cli = Cli::try_parse_from([
355            "scope",
356            "tx",
357            "0xabc123def456789012345678901234567890123456789012345678901234abcd",
358        ])
359        .unwrap();
360
361        assert!(matches!(cli.command, Commands::Tx(_)));
362    }
363
364    #[test]
365    fn test_cli_parse_tx_alias() {
366        let cli = Cli::try_parse_from([
367            "scope",
368            "transaction",
369            "0xabc123def456789012345678901234567890123456789012345678901234abcd",
370        ])
371        .unwrap();
372
373        assert!(matches!(cli.command, Commands::Tx(_)));
374    }
375
376    #[test]
377    fn test_cli_parse_portfolio_command() {
378        let cli = Cli::try_parse_from(["scope", "portfolio", "list"]).unwrap();
379
380        assert!(matches!(cli.command, Commands::Portfolio(_)));
381    }
382
383    #[test]
384    fn test_cli_parse_export_command() {
385        let cli = Cli::try_parse_from([
386            "scope",
387            "export",
388            "--address",
389            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
390            "--output",
391            "data.json",
392        ])
393        .unwrap();
394
395        assert!(matches!(cli.command, Commands::Export(_)));
396    }
397
398    #[test]
399    fn test_cli_parse_interactive_command() {
400        let cli = Cli::try_parse_from(["scope", "interactive"]).unwrap();
401
402        assert!(matches!(cli.command, Commands::Interactive(_)));
403    }
404
405    #[test]
406    fn test_cli_parse_interactive_alias() {
407        let cli = Cli::try_parse_from(["scope", "shell"]).unwrap();
408
409        assert!(matches!(cli.command, Commands::Interactive(_)));
410    }
411
412    #[test]
413    fn test_cli_parse_interactive_no_banner() {
414        let cli = Cli::try_parse_from(["scope", "interactive", "--no-banner"]).unwrap();
415
416        if let Commands::Interactive(args) = cli.command {
417            assert!(args.no_banner);
418        } else {
419            panic!("Expected Interactive command");
420        }
421    }
422
423    #[test]
424    fn test_cli_verbose_flag_counting() {
425        let cli = Cli::try_parse_from([
426            "scope",
427            "-vvv",
428            "address",
429            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
430        ])
431        .unwrap();
432
433        assert_eq!(cli.verbose, 3);
434    }
435
436    #[test]
437    fn test_cli_verbose_separate_flags() {
438        let cli = Cli::try_parse_from([
439            "scope",
440            "-v",
441            "-v",
442            "address",
443            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
444        ])
445        .unwrap();
446
447        assert_eq!(cli.verbose, 2);
448    }
449
450    #[test]
451    fn test_cli_global_config_option() {
452        let cli = Cli::try_parse_from([
453            "scope",
454            "--config",
455            "/custom/path.yaml",
456            "tx",
457            "0xabc123def456789012345678901234567890123456789012345678901234abcd",
458        ])
459        .unwrap();
460
461        assert_eq!(cli.config, Some(PathBuf::from("/custom/path.yaml")));
462    }
463
464    #[test]
465    fn test_cli_config_long_flag() {
466        let cli = Cli::try_parse_from([
467            "scope",
468            "--config",
469            "/custom/config.yaml",
470            "address",
471            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
472        ])
473        .unwrap();
474
475        assert_eq!(cli.config, Some(PathBuf::from("/custom/config.yaml")));
476    }
477
478    #[test]
479    fn test_cli_no_color_flag() {
480        let cli = Cli::try_parse_from([
481            "scope",
482            "--no-color",
483            "address",
484            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
485        ])
486        .unwrap();
487
488        assert!(cli.no_color);
489    }
490
491    #[test]
492    fn test_cli_missing_required_args_fails() {
493        let result = Cli::try_parse_from(["scope", "address"]);
494        assert!(result.is_err());
495    }
496
497    #[test]
498    fn test_cli_invalid_subcommand_fails() {
499        let result = Cli::try_parse_from(["scope", "invalid"]);
500        assert!(result.is_err());
501    }
502
503    #[test]
504    fn test_cli_log_level_default() {
505        let cli = Cli::try_parse_from([
506            "scope",
507            "address",
508            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
509        ])
510        .unwrap();
511
512        assert_eq!(cli.log_level(), tracing::Level::WARN);
513    }
514
515    #[test]
516    fn test_cli_log_level_info() {
517        let cli = Cli::try_parse_from([
518            "scope",
519            "-v",
520            "address",
521            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
522        ])
523        .unwrap();
524
525        assert_eq!(cli.log_level(), tracing::Level::INFO);
526    }
527
528    #[test]
529    fn test_cli_log_level_debug() {
530        let cli = Cli::try_parse_from([
531            "scope",
532            "-vv",
533            "address",
534            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
535        ])
536        .unwrap();
537
538        assert_eq!(cli.log_level(), tracing::Level::DEBUG);
539    }
540
541    #[test]
542    fn test_cli_log_level_trace() {
543        let cli = Cli::try_parse_from([
544            "scope",
545            "-vvvv",
546            "address",
547            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
548        ])
549        .unwrap();
550
551        assert_eq!(cli.log_level(), tracing::Level::TRACE);
552    }
553
554    #[test]
555    fn test_cli_debug_impl() {
556        let cli = Cli::try_parse_from([
557            "scope",
558            "address",
559            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
560        ])
561        .unwrap();
562
563        let debug_str = format!("{:?}", cli);
564        assert!(debug_str.contains("Cli"));
565        assert!(debug_str.contains("Address"));
566    }
567
568    // ========================================================================
569    // Monitor command tests
570    // ========================================================================
571
572    #[test]
573    fn test_cli_parse_monitor_command() {
574        let cli = Cli::try_parse_from([
575            "scope",
576            "monitor",
577            "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
578        ])
579        .unwrap();
580
581        assert!(matches!(cli.command, Commands::Monitor(_)));
582    }
583
584    #[test]
585    fn test_cli_parse_monitor_alias_mon() {
586        let cli = Cli::try_parse_from(["scope", "mon", "USDC"]).unwrap();
587
588        assert!(matches!(cli.command, Commands::Monitor(_)));
589        if let Commands::Monitor(args) = cli.command {
590            assert_eq!(args.token, "USDC");
591            assert_eq!(args.chain, "ethereum"); // default
592            assert!(args.layout.is_none());
593            assert!(args.refresh.is_none());
594            assert!(args.scale.is_none());
595            assert!(args.color_scheme.is_none());
596            assert!(args.export.is_none());
597        }
598    }
599
600    #[test]
601    fn test_cli_parse_monitor_with_chain() {
602        let cli = Cli::try_parse_from(["scope", "monitor", "USDC", "--chain", "solana"]).unwrap();
603
604        if let Commands::Monitor(args) = cli.command {
605            assert_eq!(args.token, "USDC");
606            assert_eq!(args.chain, "solana");
607        } else {
608            panic!("Expected Monitor command");
609        }
610    }
611
612    #[test]
613    fn test_cli_parse_monitor_chain_short_flag() {
614        let cli = Cli::try_parse_from(["scope", "monitor", "PEPE", "-c", "ethereum"]).unwrap();
615
616        if let Commands::Monitor(args) = cli.command {
617            assert_eq!(args.token, "PEPE");
618            assert_eq!(args.chain, "ethereum");
619        } else {
620            panic!("Expected Monitor command");
621        }
622    }
623
624    #[test]
625    fn test_cli_parse_monitor_with_layout() {
626        let cli =
627            Cli::try_parse_from(["scope", "monitor", "USDC", "--layout", "chart-focus"]).unwrap();
628
629        if let Commands::Monitor(args) = cli.command {
630            assert_eq!(args.layout, Some(monitor::LayoutPreset::ChartFocus));
631        } else {
632            panic!("Expected Monitor command");
633        }
634    }
635
636    #[test]
637    fn test_cli_parse_monitor_with_refresh() {
638        let cli = Cli::try_parse_from(["scope", "monitor", "USDC", "--refresh", "3"]).unwrap();
639
640        if let Commands::Monitor(args) = cli.command {
641            assert_eq!(args.refresh, Some(3));
642        } else {
643            panic!("Expected Monitor command");
644        }
645    }
646
647    #[test]
648    fn test_cli_parse_monitor_with_scale() {
649        let cli = Cli::try_parse_from(["scope", "monitor", "USDC", "--scale", "log"]).unwrap();
650
651        if let Commands::Monitor(args) = cli.command {
652            assert_eq!(args.scale, Some(monitor::ScaleMode::Log));
653        } else {
654            panic!("Expected Monitor command");
655        }
656    }
657
658    #[test]
659    fn test_cli_parse_monitor_with_color_scheme() {
660        let cli =
661            Cli::try_parse_from(["scope", "monitor", "USDC", "--color-scheme", "blue-orange"])
662                .unwrap();
663
664        if let Commands::Monitor(args) = cli.command {
665            assert_eq!(args.color_scheme, Some(monitor::ColorScheme::BlueOrange));
666        } else {
667            panic!("Expected Monitor command");
668        }
669    }
670
671    #[test]
672    fn test_cli_parse_monitor_with_export() {
673        let cli =
674            Cli::try_parse_from(["scope", "monitor", "USDC", "--export", "/tmp/out.csv"]).unwrap();
675
676        if let Commands::Monitor(args) = cli.command {
677            assert_eq!(args.export, Some(std::path::PathBuf::from("/tmp/out.csv")));
678        } else {
679            panic!("Expected Monitor command");
680        }
681    }
682
683    #[test]
684    fn test_cli_parse_monitor_short_flags() {
685        let cli = Cli::try_parse_from([
686            "scope", "mon", "USDC", "-c", "solana", "-l", "compact", "-r", "10", "-s", "log", "-e",
687            "data.csv",
688        ])
689        .unwrap();
690
691        if let Commands::Monitor(args) = cli.command {
692            assert_eq!(args.token, "USDC");
693            assert_eq!(args.chain, "solana");
694            assert_eq!(args.layout, Some(monitor::LayoutPreset::Compact));
695            assert_eq!(args.refresh, Some(10));
696            assert_eq!(args.scale, Some(monitor::ScaleMode::Log));
697            assert_eq!(args.export, Some(std::path::PathBuf::from("data.csv")));
698        } else {
699            panic!("Expected Monitor command");
700        }
701    }
702
703    #[test]
704    fn test_cli_parse_monitor_missing_token_fails() {
705        let result = Cli::try_parse_from(["scope", "monitor"]);
706        assert!(result.is_err());
707    }
708
709    #[test]
710    fn test_cli_parse_monitor_invalid_layout_fails() {
711        let result = Cli::try_parse_from(["scope", "monitor", "USDC", "--layout", "invalid"]);
712        assert!(result.is_err());
713    }
714
715    #[test]
716    fn test_cli_parse_monitor_invalid_scale_fails() {
717        let result = Cli::try_parse_from(["scope", "monitor", "USDC", "--scale", "quadratic"]);
718        assert!(result.is_err());
719    }
720
721    #[test]
722    fn test_cli_parse_market_summary() {
723        let cli = Cli::try_parse_from(["scope", "market", "summary"]).unwrap();
724        assert!(matches!(cli.command, Commands::Market(_)));
725    }
726
727    #[test]
728    fn test_cli_parse_market_summary_with_pair() {
729        let cli = Cli::try_parse_from(["scope", "market", "summary", "PUSD_USDT"]).unwrap();
730        if let Commands::Market(market::MarketCommands::Summary(args)) = cli.command {
731            assert_eq!(args.pair, "PUSD_USDT");
732        } else {
733            panic!("Expected Market Summary command");
734        }
735    }
736
737    #[test]
738    fn test_cli_parse_report_batch() {
739        let cli = Cli::try_parse_from([
740            "scope",
741            "report",
742            "batch",
743            "--addresses",
744            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
745            "--output",
746            "report.md",
747        ])
748        .unwrap();
749        assert!(matches!(cli.command, Commands::Report(_)));
750    }
751
752    #[test]
753    fn test_cli_parse_market_summary_with_thresholds() {
754        let cli = Cli::try_parse_from([
755            "scope",
756            "market",
757            "summary",
758            "--peg-range",
759            "0.002",
760            "--min-bid-ask-ratio",
761            "0.1",
762            "--max-bid-ask-ratio",
763            "10",
764        ])
765        .unwrap();
766        if let Commands::Market(market::MarketCommands::Summary(args)) = cli.command {
767            assert_eq!(args.peg_range, 0.002);
768            assert_eq!(args.min_bid_ask_ratio, 0.1);
769            assert_eq!(args.max_bid_ask_ratio, 10.0);
770        } else {
771            panic!("Expected Market Summary command");
772        }
773    }
774
775    #[test]
776    fn test_cli_parse_token_health() {
777        let cli = Cli::try_parse_from(["scope", "token-health", "USDC"]).unwrap();
778        if let Commands::TokenHealth(args) = cli.command {
779            assert_eq!(args.token, "USDC");
780            assert!(!args.with_market);
781        } else {
782            panic!("Expected TokenHealth command");
783        }
784    }
785
786    #[test]
787    fn test_cli_parse_token_health_alias() {
788        let cli = Cli::try_parse_from(["scope", "health", "USDC", "--with-market"]).unwrap();
789        if let Commands::TokenHealth(args) = cli.command {
790            assert_eq!(args.token, "USDC");
791            assert!(args.with_market);
792            assert!(matches!(
793                args.market_venue,
794                crate::market::MarketVenue::Binance
795            ));
796        } else {
797            panic!("Expected TokenHealth command");
798        }
799    }
800
801    #[test]
802    fn test_cli_parse_token_health_market_venue_biconomy() {
803        let cli = Cli::try_parse_from([
804            "scope",
805            "token-health",
806            "USDC",
807            "--with-market",
808            "--market-venue",
809            "biconomy",
810        ])
811        .unwrap();
812        if let Commands::TokenHealth(args) = cli.command {
813            assert!(matches!(
814                args.market_venue,
815                crate::market::MarketVenue::Biconomy
816            ));
817        } else {
818            panic!("Expected TokenHealth command");
819        }
820    }
821
822    #[test]
823    fn test_cli_parse_token_health_market_venue_eth() {
824        let cli = Cli::try_parse_from([
825            "scope",
826            "token-health",
827            "USDC",
828            "--with-market",
829            "--market-venue",
830            "eth",
831        ])
832        .unwrap();
833        if let Commands::TokenHealth(args) = cli.command {
834            assert!(matches!(
835                args.market_venue,
836                crate::market::MarketVenue::Ethereum
837            ));
838        } else {
839            panic!("Expected TokenHealth command");
840        }
841    }
842
843    #[test]
844    fn test_cli_parse_token_health_market_venue_solana() {
845        let cli = Cli::try_parse_from([
846            "scope",
847            "token-health",
848            "USDC",
849            "--with-market",
850            "--market-venue",
851            "solana",
852        ])
853        .unwrap();
854        if let Commands::TokenHealth(args) = cli.command {
855            assert!(matches!(
856                args.market_venue,
857                crate::market::MarketVenue::Solana
858            ));
859        } else {
860            panic!("Expected TokenHealth command");
861        }
862    }
863
864    #[test]
865    fn test_cli_parse_ai_flag() {
866        let cli = Cli::try_parse_from([
867            "scope",
868            "--ai",
869            "address",
870            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
871        ])
872        .unwrap();
873        assert!(cli.ai);
874    }
875}