use anyhow::Result;
use clap::Parser;
use scope::Config;
use scope::config::OutputFormat;
use scope::error::ScopeError;
use scope_cli::cli::errors::display_error;
use scope_cli::cli::output::Output;
use scope_cli::cli::prompt;
use scope_cli::cli::{Cli, Commands};
use scope_cli::note;
use std::io::{self, IsTerminal};
use tracing_subscriber::EnvFilter;
const BANNER: &str = include_str!("../assets/banner.txt");
fn fail(out: &Output, message: String, code: i32) -> ! {
let _ = display_error(&ScopeError::Other(message), out);
std::process::exit(code);
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let out = Output::stdio();
init_logging(cli.verbose);
if cli.no_color {
scope::display::theme::set_color_override(Some(false));
}
if matches!(cli.command, Commands::Interactive(_)) {
let term = std::env::var("TERM").ok();
if let Err(message) = scope_cli::cli::tui::tui_terminal_ok(
io::stdin().is_terminal(),
io::stdout().is_terminal(),
term.as_deref(),
) {
fail(&out, message, 2);
}
}
if cli.verbose > 0 {
note!(out, "{}", BANNER);
}
tracing::debug!("Scope v{} starting", scope::VERSION);
let is_setup_command = matches!(cli.command, Commands::Setup(_));
let mut config = Config::load(cli.config.as_deref()).unwrap_or_else(|e| {
note!(
out,
"⚠ Could not load config, using defaults (use -v for details)"
);
tracing::debug!("Failed to load config: {}", e);
Config::default()
});
if cli.ai {
config.output.format = OutputFormat::Markdown;
}
if let Commands::Web(ref web_args) = cli.command {
let addr: std::net::SocketAddr = format!("{}:{}", web_args.bind, web_args.port)
.parse()
.unwrap_or_else(|_| {
let message = format!("Invalid bind address: {}:{}", web_args.bind, web_args.port);
fail(&out, message, 1)
});
if web_args.stop {
if let Err(e) = scope_web::stop_daemon(&out) {
let _ = display_error(&e, &out);
std::process::exit(1);
}
return Ok(());
}
if scope_web::is_daemon_child() || !web_args.daemon {
return scope_web::start_server(addr, config, &out)
.await
.map_err(|e| anyhow::anyhow!(e));
}
if let Err(e) = scope_web::start_daemon(addr, config, &out) {
let _ = display_error(&e, &out);
std::process::exit(1);
}
return Ok(());
}
if !is_setup_command && !config_file_exists(&cli) && prompt_for_setup(&out) {
let setup_args = scope_cli::cli::setup::SetupArgs {
status: false,
key: None,
reset: false,
};
if let Err(e) = scope_cli::cli::setup::run(setup_args, &config, &out).await {
let _ = display_error(&e, &out);
}
let mut config = Config::load(cli.config.as_deref()).unwrap_or_default();
if cli.ai {
config.output.format = OutputFormat::Markdown;
}
return dispatch(cli.command, &config, &out).await;
}
dispatch(cli.command, &config, &out).await
}
fn config_file_exists(cli: &Cli) -> bool {
if let Some(ref path) = cli.config {
return path.exists();
}
Config::config_path().map(|p| p.exists()).unwrap_or(false)
}
fn prompt_for_setup(out: &Output) -> bool {
note!(out);
note!(out, "Welcome to Scope Blockchain Analysis!");
note!(out);
note!(
out,
"No configuration file found. Would you like to run the setup wizard"
);
note!(out, "to configure API keys and preferences?");
note!(out);
let Ok(input) = prompt::ask_stdin(out, "Run setup now? [Y/n]: ") else {
return false;
};
let response = input.trim().to_lowercase();
response.is_empty() || response == "y" || response == "yes"
}
async fn dispatch(command: Commands, config: &Config, out: &Output) -> Result<()> {
if !command.is_interactive() {
note!(out, "Scope v{}", scope::VERSION);
}
match scope_cli::cli::dispatch::dispatch(command, config, out).await {
Ok(0) => Ok(()),
Ok(code) => std::process::exit(code),
Err(e) => {
tracing::debug!("Command failed: {}", e);
let _ = display_error(&e, out);
std::process::exit(1);
}
}
}
fn init_logging(verbosity: u8) {
let level = match verbosity {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
};
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(format!("scope={},warn", level)));
let builder = tracing_subscriber::fmt()
.with_writer(|| scope::diag::Stderr)
.with_env_filter(filter)
.with_target(false)
.with_thread_ids(false);
if verbosity < 2 {
builder.without_time().with_level(false).init();
} else {
builder.init();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verbosity_levels() {
assert_eq!(
match 0u8 {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
},
"warn"
);
assert_eq!(
match 1u8 {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
},
"info"
);
assert_eq!(
match 2u8 {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
},
"debug"
);
assert_eq!(
match 3u8 {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
},
"trace"
);
}
#[test]
fn test_cli_parsing() {
let result = Cli::try_parse_from([
"scope",
"address",
"0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
]);
assert!(result.is_ok());
}
#[test]
fn test_cli_help_flag() {
let result = Cli::try_parse_from(["scope", "--help"]);
assert!(result.is_err());
}
#[test]
fn test_cli_version_flag() {
let result = Cli::try_parse_from(["scope", "--version"]);
assert!(result.is_err());
}
#[test]
fn test_config_file_exists_with_explicit_path() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("test_config.toml");
std::fs::write(&config_path, "").unwrap();
let cli = Cli::try_parse_from([
"scope",
"--config",
config_path.to_str().unwrap(),
"venues",
"list",
])
.unwrap();
assert!(config_file_exists(&cli));
}
#[test]
fn test_config_file_exists_nonexistent() {
let cli = Cli::try_parse_from([
"scope",
"--config",
"/tmp/nonexistent_scope_config_test.toml",
"venues",
"list",
])
.unwrap();
assert!(!config_file_exists(&cli));
}
#[test]
fn test_cli_completions_parsing() {
let result = Cli::try_parse_from(["scope", "completions", "bash"]);
assert!(result.is_ok());
}
}