use anyhow::Result;
use clap::Parser;
use scope::Config;
use scope::error::ScopeError;
use scope_cli::cli::dispatch::ConfigSource;
use scope_cli::cli::errors::display_error;
use scope_cli::cli::ghola_offer;
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);
}
const MAIN_STACK_BYTES: usize = 8 * 1024 * 1024;
fn main() -> Result<()> {
std::thread::Builder::new()
.name("main".into())
.stack_size(MAIN_STACK_BYTES)
.spawn(|| {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("tokio runtime")
.block_on(run())
})
.expect("main thread")
.join()
.unwrap_or_else(|panic| std::panic::resume_unwind(panic))
}
async fn run() -> 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));
}
let opens_tui = match &cli.command {
Commands::Interactive(_) => true,
Commands::Monitor(args) => !args.watch.dashboard,
_ => false,
};
if opens_tui {
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(_));
match scope::paths::migration::migrate() {
Ok(report) if report.copied > 0 => note!(
out,
"Moved your scope files to {} (copied {} files; the old files are unchanged).",
scope::paths::home().display(),
report.copied
),
Ok(_) => {}
Err(e) => fail(&out, e.to_string(), 1),
}
let source = ConfigSource {
path: cli.config.clone(),
ai: cli.ai,
};
let config = source.load().unwrap_or_else(|e| {
note!(
out,
"⚠ Could not load config, using defaults (use -v for details)"
);
tracing::debug!("Failed to load config: {}", e);
let mut config = Config::default();
source.apply(&mut config);
config
});
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 {
let served = scope_web::start_server(addr, config, &out).await;
ghola_offer::finish_sidecar(&out, false);
return served.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(());
}
let first_run = !is_setup_command && !config_file_exists(&cli);
let can_ask = first_run
&& ghola_offer::may_ask(&out)
&& !ghola_offer::scripted(&ghola_offer::process_args(), &config);
if first_run && !can_ask {
note!(
out,
"No configuration file found; using defaults. Run 'scope setup' to configure."
);
}
if can_ask && 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 config = source.load().unwrap_or_else(|_| {
let mut config = Config::default();
source.apply(&mut config);
config
});
return dispatch(cli.command, &source, &config, &out).await;
}
dispatch(cli.command, &source, &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,
source: &ConfigSource,
config: &Config,
out: &Output,
) -> Result<()> {
if !command.is_interactive() {
note!(out, "Scope v{}", scope::VERSION);
}
let ask =
ghola_offer::may_ask(out) && !ghola_offer::scripted(&ghola_offer::process_args(), config);
match scope_cli::cli::dispatch::dispatch(command, source, config, out).await {
Ok(0) => {
ghola_offer::finish_sidecar(out, ask);
Ok(())
}
Ok(code) => {
ghola_offer::finish_sidecar(out, false);
std::process::exit(code)
}
Err(e) => {
tracing::debug!("Command failed: {}", e);
let _ = display_error(&e, out);
ghola_offer::finish_sidecar(out, ask);
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());
}
}