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