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