Skip to main content

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