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