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