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