From 543fb333f72981661a98162a6382529bf15d7ff6 Mon Sep 17 00:00:00 2001 From: Nishant Arora <1895906+whizzzkid@users.noreply.github.com> Date: Thu, 29 Jan 2026 20:02:46 +0000 Subject: [PATCH] refactor: simplify binary by moving logic to cli module - Simplify src/bin/scope.rs to ~110 lines (was ~280) - Add src/bin/cli/mod.rs for CLI module organization - Add src/bin/cli/commands.rs with command routing logic - Update src/bin/scope-intercept.rs for consistency The binary now delegates to the library for core functionality. Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) --- src/bin/cli/commands.rs | 154 +++++++++++++++++++++++++++++++ src/bin/cli/mod.rs | 3 + src/bin/scope-intercept.rs | 7 +- src/bin/scope.rs | 183 +++++++------------------------------ 4 files changed, 195 insertions(+), 152 deletions(-) create mode 100644 src/bin/cli/commands.rs create mode 100644 src/bin/cli/mod.rs diff --git a/src/bin/cli/commands.rs b/src/bin/cli/commands.rs new file mode 100644 index 0000000..e118314 --- /dev/null +++ b/src/bin/cli/commands.rs @@ -0,0 +1,154 @@ +//! Command routing and utility functions for the CLI. +//! +//! This module contains all the command routing logic and helper functions +//! used by the binary, keeping the binary itself thin. + +use anyhow::Result; +use clap::CommandFactory; +use colored::Colorize; +use crate::{Cli, Command, VersionArgs}; +use dx_scope::{ + analyze_root, doctor_root, lint_root, print_details, report_root, + CaptureOpts, FoundConfig, OutputCapture, OutputDestination, + CONFIG_FILE_PATH_ENV, RUN_ID_ENV_VAR, report_stdout, +}; +use lazy_static::lazy_static; +use regex::Regex; +use std::collections::BTreeMap; +use std::ffi::OsString; +use tracing::{debug, info, instrument}; + +lazy_static! { + static ref SCOPE_SUBCOMMAND_REGEX: Regex = Regex::new("^scope-.*").unwrap(); +} + +/// Route a command to its appropriate handler. +pub async fn handle_command(found_config: &FoundConfig, command: &Command) -> Result { + match command { + Command::Doctor(args) => doctor_root(found_config, args).await, + Command::Report(args) => report_root(found_config, args).await, + Command::List => show_config(found_config).await.map(|_| 0), + Command::Version(args) => print_version(args).await, + Command::ExternalSubCommand(args) => exec_sub_command(found_config, args).await, + Command::Analyze(args) => analyze_root(found_config, args).await, + Command::Lint(args) => lint_root(found_config, args).await, + } +} + +/// Execute an external subcommand. +#[instrument("scope external-command", skip_all)] +async fn exec_sub_command(found_config: &FoundConfig, args: &[String]) -> Result { + let mut args = args.to_owned(); + let command = match args.first() { + None => return Err(anyhow::anyhow!("Sub command not provided")), + Some(cmd) => { + format!("scope-{cmd}") + } + }; + let _ = std::mem::replace(&mut args[0], command); + + debug!("Executing {:?}", args); + + let config_file_path = found_config.write_raw_config_to_disk()?; + let capture = OutputCapture::capture_output(CaptureOpts { + working_dir: &found_config.working_dir, + args: &args, + output_dest: OutputDestination::StandardOut, + path: &found_config.bin_path, + env_vars: BTreeMap::from([ + ( + CONFIG_FILE_PATH_ENV.to_string(), + config_file_path.display().to_string(), + ), + (RUN_ID_ENV_VAR.to_string(), found_config.run_id.clone()), + ]), + }) + .await?; + + capture + .exit_code + .ok_or_else(|| anyhow::anyhow!("Unable to exec {}", args.join(" "))) +} + +/// Show configuration and available commands. +#[instrument("scope list", skip_all)] +async fn show_config(found_config: &FoundConfig) -> Result<()> { + info!(target: "user", "Found Resources"); + print_details(&found_config.working_dir, &found_config.raw_config).await; + + info!(target: "user", ""); + info!(target: "user", "Commands"); + print_commands(found_config).await; + Ok(()) +} + +/// Print available commands (both built-in and external). +async fn print_commands(found_config: &FoundConfig) { + if let Ok(commands) = which::which_re_in( + SCOPE_SUBCOMMAND_REGEX.clone(), + Some(OsString::from(&found_config.bin_path)), + ) { + let mut command_map = BTreeMap::new(); + for command in commands { + let command_name = command.file_name().unwrap().to_str().unwrap().to_string(); + let command_name = command_name.replace("scope-", ""); + command_map.entry(command_name.clone()).or_insert_with(|| { + format!("External sub-command, run `scope {command_name}` for help") + }); + } + for command in Cli::command().get_subcommands() { + command_map + .entry(command.get_name().to_string()) + .or_insert_with(|| command.get_about().unwrap_or_default().to_string()); + } + + let mut command_names: Vec<_> = command_map.keys().collect(); + command_names.sort(); + + report_stdout!( + " {:20}{:60}", + "Name".white().bold(), + "Description".white().bold() + ); + for command_name in command_names { + let description = command_map.get(command_name.as_str()).unwrap(); + report_stdout!("- {:20}{:60}", command_name, description); + } + } +} + +/// Print version information. +#[instrument("scope version", skip_all)] +async fn print_version(args: &VersionArgs) -> Result { + if args.short { + report_stdout!("scope {}", env!("CARGO_PKG_VERSION")); + } else { + report_stdout!( + "{}: {:60}", + "Version".white().bold(), + env!("CARGO_PKG_VERSION") + ); + report_stdout!( + "{}: {:60}", + "Build Timestamp".white().bold(), + env!("VERGEN_BUILD_TIMESTAMP") + ); + report_stdout!( + "{}: {:60}", + "Describe".white().bold(), + env!("VERGEN_GIT_DESCRIBE") + ); + report_stdout!( + "{}: {:60}", + "Commit SHA".white().bold(), + env!("VERGEN_GIT_SHA") + ); + report_stdout!( + "{}: {:60}", + "Commit Date".white().bold(), + env!("VERGEN_GIT_COMMIT_DATE") + ); + } + + Ok(0) +} diff --git a/src/bin/cli/mod.rs b/src/bin/cli/mod.rs new file mode 100644 index 0000000..8d65282 --- /dev/null +++ b/src/bin/cli/mod.rs @@ -0,0 +1,3 @@ +//! CLI utilities and command routing for the scope binary. + +pub mod commands; diff --git a/src/bin/scope-intercept.rs b/src/bin/scope-intercept.rs index faa1572..8430e94 100644 --- a/src/bin/scope-intercept.rs +++ b/src/bin/scope-intercept.rs @@ -1,5 +1,10 @@ use clap::Parser; -use dev_scope::prelude::*; +use dx_scope::{ + CaptureOpts, ConfigOptions, DefaultExecutionProvider, + DefaultUnstructuredReportBuilder, FoundConfig, HelpMetadata, + LoggingOpts, OutputCapture, OutputDestination, ReportRenderer, + UnstructuredReportBuilder, +}; use human_panic::setup_panic; use std::env; use std::sync::Arc; diff --git a/src/bin/scope.rs b/src/bin/scope.rs index f2a1666..0b45daa 100644 --- a/src/bin/scope.rs +++ b/src/bin/scope.rs @@ -1,15 +1,16 @@ -use anyhow::Result; -use clap::CommandFactory; +//! Scope CLI binary - thin wrapper around the dx-scope library. +//! +//! This binary provides the command-line interface for scope. Most logic +//! lives in the library or cli module, keeping this file minimal. + +mod cli; + use clap::{Parser, Subcommand}; -use colored::Colorize; -use dev_scope::prelude::*; -use dev_scope::report_stdout; +use dx_scope::{ + AnalyzeArgs, ConfigOptions, DoctorArgs, LintArgs, LoggingOpts, ReportArgs, +}; use human_panic::setup_panic; -use lazy_static::lazy_static; -use regex::Regex; -use std::collections::BTreeMap; -use std::ffi::OsString; -use tracing::{Level, debug, enabled, error, info, instrument}; +use tracing::{Level, enabled, error, info}; /// scope /// @@ -19,25 +20,25 @@ use tracing::{Level, debug, enabled, error, info, instrument}; /// for support's machine is setup correctly. #[derive(Parser)] #[clap(author, version, about)] -struct Cli { +pub(crate) struct Cli { #[clap(flatten)] - logging: LoggingOpts, + pub logging: LoggingOpts, #[clap(flatten)] - config: ConfigOptions, + pub config: ConfigOptions, #[clap(subcommand)] - command: Command, + pub command: Command, } #[derive(Parser, Debug)] -struct VersionArgs { +pub(crate) struct VersionArgs { #[arg(long, action)] pub short: bool, } #[derive(Debug, Subcommand)] -enum Command { +pub(crate) enum Command { /// Run checks that will "checkup" your machine. #[clap(alias("d"))] Doctor(DoctorArgs), @@ -63,18 +64,26 @@ enum Command { #[tokio::main] async fn main() { setup_panic!(); + + // Load environment files dotenvy::dotenv().ok(); let exe_path = std::env::current_exe().unwrap(); let env_path = exe_path.parent().unwrap().join("../etc/scope.env"); dotenvy::from_path(env_path).ok(); + + // Parse CLI arguments let opts = Cli::parse(); + // Setup logging let configured_logger = opts .logging .configure_logging(&opts.config.get_run_id(), "root") .await; - let error_code = run_subcommand(opts).await; + + // Run command + let error_code = run_command(opts).await; + // Show log location on error or debug if error_code != 0 || enabled!(Level::DEBUG) { info!(target: "user", "More detailed logs at {}", configured_logger.log_location); } @@ -83,149 +92,21 @@ async fn main() { std::process::exit(error_code); } -async fn run_subcommand(opts: Cli) -> i32 { - let loaded_config = match opts.config.load_config().await { +async fn run_command(opts: Cli) -> i32 { + // Load configuration + let config = match opts.config.load_config().await { + Ok(c) => c, Err(e) => { error!(target: "user", "Failed to load configuration: {}", e); return 2; } - Ok(c) => c, }; - handle_commands(&loaded_config, &opts.command) + // Route to command handler + cli::commands::handle_command(&config, &opts.command) .await .unwrap_or_else(|e| { error!(target: "user", "Critical Error. {}", e); 1 }) } - -async fn handle_commands(found_config: &FoundConfig, command: &Command) -> Result { - match command { - Command::Doctor(args) => doctor_root(found_config, args).await, - Command::Report(args) => report_root(found_config, args).await, - Command::List => show_config(found_config).await.map(|_| 0), - Command::Version(args) => print_version(args).await, - Command::ExternalSubCommand(args) => exec_sub_command(found_config, args).await, - Command::Analyze(args) => analyze_root(found_config, args).await, - Command::Lint(args) => lint_root(found_config, args).await, - } -} - -#[instrument("scope external-command", skip_all)] -async fn exec_sub_command(found_config: &FoundConfig, args: &[String]) -> Result { - let mut args = args.to_owned(); - let command = match args.first() { - None => return Err(anyhow::anyhow!("Sub command not provided")), - Some(cmd) => { - format!("scope-{cmd}") - } - }; - let _ = std::mem::replace(&mut args[0], command); - - debug!("Executing {:?}", args); - - let config_file_path = found_config.write_raw_config_to_disk()?; - let capture = OutputCapture::capture_output(CaptureOpts { - working_dir: &found_config.working_dir, - args: &args, - output_dest: OutputDestination::StandardOut, - path: &found_config.bin_path, - env_vars: BTreeMap::from([ - ( - CONFIG_FILE_PATH_ENV.to_string(), - config_file_path.display().to_string(), - ), - (RUN_ID_ENV_VAR.to_string(), found_config.run_id.clone()), - ]), - }) - .await?; - - capture - .exit_code - .ok_or_else(|| anyhow::anyhow!("Unable to exec {}", args.join(" "))) -} - -lazy_static! { - static ref SCOPE_SUBCOMMAND_REGEX: Regex = Regex::new("^scope-.*").unwrap(); -} - -#[instrument("scope list", skip_all)] -async fn show_config(found_config: &FoundConfig) -> Result<()> { - info!(target: "user", "Found Resources"); - print_details(&found_config.working_dir, &found_config.raw_config).await; - - info!(target: "user", ""); - info!(target: "user", "Commands"); - print_commands(found_config).await; - Ok(()) -} - -async fn print_commands(found_config: &FoundConfig) { - if let Ok(commands) = which::which_re_in( - SCOPE_SUBCOMMAND_REGEX.clone(), - Some(OsString::from(&found_config.bin_path)), - ) { - let mut command_map = BTreeMap::new(); - for command in commands { - let command_name = command.file_name().unwrap().to_str().unwrap().to_string(); - let command_name = command_name.replace("scope-", ""); - command_map.entry(command_name.clone()).or_insert_with(|| { - format!("External sub-command, run `scope {command_name}` for help") - }); - } - for command in Cli::command().get_subcommands() { - command_map - .entry(command.get_name().to_string()) - .or_insert_with(|| command.get_about().unwrap_or_default().to_string()); - } - - let mut command_names: Vec<_> = command_map.keys().collect(); - command_names.sort(); - - report_stdout!( - " {:20}{:60}", - "Name".white().bold(), - "Description".white().bold() - ); - for command_name in command_names { - let description = command_map.get(command_name.as_str()).unwrap(); - report_stdout!("- {:20}{:60}", command_name, description); - } - } -} - -#[instrument("scope version", skip_all)] -async fn print_version(args: &VersionArgs) -> Result { - if args.short { - report_stdout!("scope {}", env!("CARGO_PKG_VERSION")); - } else { - report_stdout!( - "{}: {:60}", - "Version".white().bold(), - env!("CARGO_PKG_VERSION") - ); - report_stdout!( - "{}: {:60}", - "Build Timestamp".white().bold(), - env!("VERGEN_BUILD_TIMESTAMP") - ); - report_stdout!( - "{}: {:60}", - "Describe".white().bold(), - env!("VERGEN_GIT_DESCRIBE") - ); - report_stdout!( - "{}: {:60}", - "Commit SHA".white().bold(), - env!("VERGEN_GIT_SHA") - ); - report_stdout!( - "{}: {:60}", - "Commit Date".white().bold(), - env!("VERGEN_GIT_COMMIT_DATE") - ); - } - - Ok(0) -}