diff --git a/Cargo.lock b/Cargo.lock index ae12841..cf9381a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -716,73 +716,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" -[[package]] -name = "dev-scope" -version = "0.0.0-dev" -dependencies = [ - "anyhow", - "assert_cmd", - "assert_fs", - "async-trait", - "chrono", - "clap", - "colored", - "derivative", - "derive_builder", - "directories", - "dotenvy", - "educe", - "escargot", - "fake", - "gethostname", - "glob", - "human-panic", - "ignore", - "indicatif", - "inquire", - "itertools", - "json", - "jsonschema", - "jsonwebtoken", - "lazy_static", - "minijinja", - "mockall", - "nanoid", - "normpath", - "octocrab", - "opentelemetry", - "opentelemetry-otlp", - "opentelemetry_sdk", - "path-clean", - "pathdiff", - "petgraph", - "predicates", - "regex", - "reqwest", - "schemars", - "secrecy 0.8.0", - "serde", - "serde_json", - "serde_yaml", - "sha256", - "shellexpand", - "strip-ansi-escapes", - "strum", - "tempfile", - "thiserror 1.0.69", - "time", - "tokio", - "tonic", - "tracing", - "tracing-appender", - "tracing-indicatif", - "tracing-opentelemetry", - "tracing-subscriber", - "url", - "vergen", - "which", -] - [[package]] name = "difflib" version = "0.4.0" @@ -860,6 +793,73 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" +[[package]] +name = "dx-scope" +version = "0.0.0-dev" +dependencies = [ + "anyhow", + "assert_cmd", + "assert_fs", + "async-trait", + "chrono", + "clap", + "colored", + "derivative", + "derive_builder", + "directories", + "dotenvy", + "educe", + "escargot", + "fake", + "gethostname", + "glob", + "human-panic", + "ignore", + "indicatif", + "inquire", + "itertools", + "json", + "jsonschema", + "jsonwebtoken", + "lazy_static", + "minijinja", + "mockall", + "nanoid", + "normpath", + "octocrab", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", + "path-clean", + "pathdiff", + "petgraph", + "predicates", + "regex", + "reqwest", + "schemars", + "secrecy 0.8.0", + "serde", + "serde_json", + "serde_yaml", + "sha256", + "shellexpand", + "strip-ansi-escapes", + "strum", + "tempfile", + "thiserror 1.0.69", + "time", + "tokio", + "tonic", + "tracing", + "tracing-appender", + "tracing-indicatif", + "tracing-opentelemetry", + "tracing-subscriber", + "url", + "vergen", + "which", +] + [[package]] name = "dyn-clone" version = "1.0.17" diff --git a/Cargo.toml b/Cargo.toml index 753c3fe..30b10e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "dev-scope" +name = "dx-scope" version = "0.0.0-dev" edition = "2024" default-run = "scope" diff --git a/build.rs b/build.rs index 5f35616..75c3566 100644 --- a/build.rs +++ b/build.rs @@ -2,7 +2,22 @@ use anyhow::Result; use vergen::EmitBuilder; pub fn main() -> Result<()> { - EmitBuilder::builder().all_build().all_git().emit()?; - + let mut builder = EmitBuilder::builder(); + + // Build info always works + builder.all_build(); + + // Git info only available when building from git repo + // This allows the crate to be built from crates.io where .git doesn't exist + if std::path::Path::new(".git").exists() { + builder.all_git(); + } else { + // Provide default values when not in a git repo + println!("cargo:rustc-env=VERGEN_GIT_DESCRIBE=unknown"); + println!("cargo:rustc-env=VERGEN_GIT_SHA=unknown"); + println!("cargo:rustc-env=VERGEN_GIT_COMMIT_DATE=unknown"); + } + + builder.emit()?; Ok(()) } diff --git a/src/analyze/options.rs b/src/analyze/options.rs new file mode 100644 index 0000000..62b1a61 --- /dev/null +++ b/src/analyze/options.rs @@ -0,0 +1,148 @@ +//! CLI-independent options types for the analyze module. +//! +//! This module provides options types that can be used to configure +//! analyze operations without depending on CLI argument parsing (clap). +//! +//! # Overview +//! +//! The analyze functionality detects known errors in command output or log files +//! and can optionally run fixes for detected errors. +//! +//! # Examples +//! +//! ## Basic Configuration +//! +//! ```rust +//! use dx_scope::AnalyzeOptions; +//! use std::collections::BTreeMap; +//! use std::path::PathBuf; +//! +//! let options = AnalyzeOptions { +//! known_errors: BTreeMap::new(), +//! working_dir: PathBuf::from("/path/to/project"), +//! }; +//! assert!(options.known_errors.is_empty()); +//! ``` +//! +//! ## Different Input Sources +//! +//! ```rust,no_run +//! use dx_scope::analyze::options::AnalyzeInput; +//! +//! // From a file +//! let input = AnalyzeInput::from_file("/var/log/build.log"); +//! +//! // From stdin +//! let input = AnalyzeInput::Stdin; +//! +//! // From in-memory lines (useful for testing or library usage) +//! let input = AnalyzeInput::from_lines(vec![ +//! "Building project...".to_string(), +//! "error: missing dependency".to_string(), +//! ]); +//! ``` + +use crate::shared::prelude::KnownError; +use std::collections::BTreeMap; +use std::path::PathBuf; + +/// Options for running analyze operations. +/// +/// This struct contains all the configuration needed to run analysis, +/// without any CLI-specific dependencies like clap. +/// +/// # Example +/// +/// ```rust +/// use dx_scope::AnalyzeOptions; +/// use std::collections::BTreeMap; +/// use std::path::PathBuf; +/// +/// let options = AnalyzeOptions { +/// known_errors: BTreeMap::new(), +/// working_dir: PathBuf::from("/path/to/project"), +/// }; +/// assert!(options.known_errors.is_empty()); +/// ``` +#[derive(Debug, Clone)] +pub struct AnalyzeOptions { + /// Map of known errors to detect and potentially fix + pub known_errors: BTreeMap, + /// Working directory for running fix commands + pub working_dir: PathBuf, +} + +impl AnalyzeOptions { + /// Create new analyze options with the given parameters. + pub fn new(known_errors: BTreeMap, working_dir: PathBuf) -> Self { + Self { + known_errors, + working_dir, + } + } +} + +impl Default for AnalyzeOptions { + fn default() -> Self { + Self { + known_errors: BTreeMap::new(), + working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), + } + } +} + +/// Specifies the input source for analysis. +/// +/// The analyze functionality can process input from various sources. +/// This enum allows callers to specify where the input should come from. +#[derive(Debug, Clone)] +pub enum AnalyzeInput { + /// Read from a file at the given path + File(PathBuf), + /// Read from standard input + Stdin, + /// Process the given lines directly (useful for library usage) + Lines(Vec), +} + +impl AnalyzeInput { + /// Create input from a file path. + pub fn from_file(path: impl Into) -> Self { + Self::File(path.into()) + } + + /// Create input from a vector of strings. + pub fn from_lines(lines: Vec) -> Self { + Self::Lines(lines) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_analyze_options_default() { + let options = AnalyzeOptions::default(); + assert!(options.known_errors.is_empty()); + } + + #[test] + fn test_analyze_input_from_file() { + let input = AnalyzeInput::from_file("/path/to/file"); + match input { + AnalyzeInput::File(path) => assert_eq!(path, PathBuf::from("/path/to/file")), + _ => panic!("Expected File variant"), + } + } + + #[test] + fn test_analyze_input_from_lines() { + let lines = vec!["line1".to_string(), "line2".to_string()]; + let input = AnalyzeInput::from_lines(lines.clone()); + match input { + AnalyzeInput::Lines(l) => assert_eq!(l, lines), + _ => panic!("Expected Lines variant"), + } + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..da85c52 --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,96 @@ +//! CLI-specific implementations for interactive usage. +//! +//! This module provides implementations of the library traits that are +//! suitable for command-line interface usage, including: +//! +//! - Interactive prompts using the `inquire` crate +//! - Visual progress reporting using `tracing-indicatif` +//! +//! # When to Use +//! +//! Use the implementations in this module when building CLI applications +//! that need interactive user prompts. For library usage, automated +//! environments, or testing, use the implementations in [`crate::internal`]. +//! +//! # Examples +//! +//! ## Interactive CLI Application +//! +//! ```rust +//! use dx_scope::InquireInteraction; +//! use dx_scope::internal::prompts::UserInteraction; +//! +//! let interaction = InquireInteraction; +//! +//! // This will show an interactive prompt in the terminal +//! // In non-TTY environments (like this doc test), it returns false +//! let result = interaction.confirm("Apply this fix?", Some("This will modify files")); +//! // result is false in doc test environment (no TTY) +//! ``` +//! +//! # TTY Detection +//! +//! `InquireInteraction` automatically detects when stdin is not a TTY +//! (e.g., when running in a pipe or CI environment) and returns `false` +//! instead of crashing. For explicit control in non-interactive environments, +//! use [`AutoApprove`](crate::AutoApprove) or [`DenyAll`](crate::DenyAll). + + +use crate::internal::prompts::UserInteraction; +use inquire::InquireError; +use tracing::warn; + +/// CLI user interaction using the `inquire` crate. +/// +/// This implementation provides interactive prompts suitable for terminal usage. +/// It handles TTY detection and gracefully falls back to denial when running +/// in non-interactive environments. +/// +/// # Example +/// +/// ```rust +/// use dx_scope::InquireInteraction; +/// use dx_scope::internal::prompts::UserInteraction; +/// +/// let interaction = InquireInteraction; +/// // In non-TTY environments, confirm() returns false gracefully +/// let confirmed = interaction.confirm("Apply fix?", Some("This will modify files")); +/// ``` +#[derive(Debug, Clone, Copy, Default)] +pub struct InquireInteraction; + +impl UserInteraction for InquireInteraction { + fn confirm(&self, prompt: &str, help_text: Option<&str>) -> bool { + tracing_indicatif::suspend_tracing_indicatif(|| { + let base_prompt = inquire::Confirm::new(prompt).with_default(false); + let prompt = match help_text { + Some(text) => base_prompt.with_help_message(text), + None => base_prompt, + }; + + match prompt.prompt() { + Ok(result) => result, + Err(InquireError::NotTTY) => { + warn!(target: "user", "Prompting user, but input device is not a TTY. Skipping."); + false + } + Err(_) => false, + } + }) + } + + fn notify(&self, message: &str) { + println!("{}", message); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_inquire_interaction_is_send_sync() { + fn assert_send_sync() {} + assert_send_sync::(); + } +} diff --git a/src/doctor/options.rs b/src/doctor/options.rs new file mode 100644 index 0000000..0c5b05b --- /dev/null +++ b/src/doctor/options.rs @@ -0,0 +1,152 @@ +//! CLI-independent options types for the doctor module. +//! +//! This module provides options types that can be used to configure +//! doctor operations without depending on CLI argument parsing (clap). +//! +//! # Overview +//! +//! The doctor functionality runs health checks on your development environment +//! and can automatically apply fixes when issues are detected. +//! +//! # Examples +//! +//! ## Common Configurations +//! +//! ```rust +//! use dx_scope::doctor::options::DoctorRunOptions; +//! +//! // CI mode - run checks without fixes +//! let ci_options = DoctorRunOptions::ci_mode(); +//! +//! // Interactive mode - prompt for fixes +//! let interactive_options = DoctorRunOptions::with_fixes(); +//! +//! // Run specific groups only +//! let targeted_options = DoctorRunOptions::for_groups(vec![ +//! "rust".to_string(), +//! "docker".to_string(), +//! ]); +//! ``` +//! +//! ## Full Customization +//! +//! ```rust +//! use dx_scope::doctor::options::DoctorRunOptions; +//! use std::path::PathBuf; +//! +//! let options = DoctorRunOptions { +//! only_groups: Some(vec!["build".to_string()]), +//! run_fix: true, +//! cache_dir: Some(PathBuf::from("/tmp/my-cache")), +//! no_cache: false, +//! auto_publish_report: true, +//! }; +//! ``` +//! +//! # Caching +//! +//! The doctor module supports caching to avoid re-running checks when +//! source files haven't changed. Control caching with: +//! +//! - `cache_dir`: Custom cache location (default: system cache directory) +//! - `no_cache`: Disable caching entirely (useful for debugging) + +use std::path::PathBuf; + +/// Options for running doctor operations. +/// +/// This struct contains all the configuration needed to run doctor checks, +/// without any CLI-specific dependencies like clap. +/// +/// # Example +/// +/// ```rust,no_run +/// use dx_scope::DoctorRunOptions; +/// +/// // Run all default groups with fixes enabled +/// let options = DoctorRunOptions::default(); +/// +/// // Run only specific groups +/// let options = DoctorRunOptions { +/// only_groups: Some(vec!["build".to_string(), "test".to_string()]), +/// ..Default::default() +/// }; +/// +/// // Run in CI mode (no interactive fixes) +/// let options = DoctorRunOptions::ci_mode(); +/// ``` +#[derive(Debug, Clone, Default)] +pub struct DoctorRunOptions { + /// Run only these groups (None = run all default groups) + pub only_groups: Option>, + /// Whether to run fixes when checks fail + pub run_fix: bool, + /// Custom cache directory path + pub cache_dir: Option, + /// Disable caching + pub no_cache: bool, + /// Automatically publish report on failure + pub auto_publish_report: bool, +} + +impl DoctorRunOptions { + /// Create new options with default values but fixes enabled. + pub fn with_fixes() -> Self { + Self { + run_fix: true, + ..Default::default() + } + } + + /// Create new options for CI/non-interactive mode (no fixes). + pub fn ci_mode() -> Self { + Self { + run_fix: false, + ..Default::default() + } + } + + /// Create options to run specific groups only. + pub fn for_groups(groups: Vec) -> Self { + Self { + only_groups: Some(groups), + run_fix: true, + ..Default::default() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_options() { + let options = DoctorRunOptions::default(); + assert!(options.only_groups.is_none()); + assert!(!options.run_fix); + assert!(options.cache_dir.is_none()); + assert!(!options.no_cache); + assert!(!options.auto_publish_report); + } + + #[test] + fn test_with_fixes() { + let options = DoctorRunOptions::with_fixes(); + assert!(options.run_fix); + } + + #[test] + fn test_ci_mode() { + let options = DoctorRunOptions::ci_mode(); + assert!(!options.run_fix); + } + + #[test] + fn test_for_groups() { + let groups = vec!["group1".to_string(), "group2".to_string()]; + let options = DoctorRunOptions::for_groups(groups.clone()); + assert_eq!(options.only_groups, Some(groups)); + assert!(options.run_fix); + } +} diff --git a/src/internal/mod.rs b/src/internal/mod.rs new file mode 100644 index 0000000..f5c327c --- /dev/null +++ b/src/internal/mod.rs @@ -0,0 +1,82 @@ +//! Internal abstractions for library-first design. +//! +//! This module contains traits and implementations that allow the scope library +//! to be used both as a CLI tool and as a programmatic library. These abstractions +//! decouple the core logic from specific UI implementations. +//! +//! # Overview +//! +//! The internal module provides: +//! - [`prompts`] - User interaction abstractions (confirmations, notifications) +//! - [`progress`] - Progress reporting abstractions +//! +//! # Choosing an Implementation +//! +//! | Use Case | UserInteraction | ProgressReporter | +//! |----------|-----------------|------------------| +//! | CLI/Interactive | `InquireInteraction` | (use tracing-indicatif) | +//! | CI/Automated | `AutoApprove` | `NoOpProgress` | +//! | Dry-run/Testing | `DenyAll` | `NoOpProgress` | +//! | Custom | Implement trait | Implement trait | +//! +//! # Examples +//! +//! ## Automated Environment (CI) +//! +//! ```rust +//! use dx_scope::internal::prompts::{UserInteraction, AutoApprove}; +//! use dx_scope::internal::progress::{ProgressReporter, NoOpProgress}; +//! +//! let interaction = AutoApprove; +//! let progress = NoOpProgress; +//! +//! // All prompts will be automatically approved +//! assert!(interaction.confirm("Apply fix?", None)); +//! +//! // Progress calls are silent +//! progress.start_group("build", 5); +//! progress.finish_group(); +//! ``` +//! +//! ## Dry-Run Mode +//! +//! ```rust +//! use dx_scope::internal::prompts::{UserInteraction, DenyAll}; +//! +//! let interaction = DenyAll; +//! +//! // All prompts will be denied - no changes made +//! assert!(!interaction.confirm("Apply fix?", None)); +//! ``` +//! +//! ## Custom Implementation +//! +//! ```rust +//! use dx_scope::internal::prompts::UserInteraction; +//! +//! struct AlwaysAskUser; +//! +//! impl UserInteraction for AlwaysAskUser { +//! fn confirm(&self, prompt: &str, _help: Option<&str>) -> bool { +//! // Custom logic - maybe read from a config file +//! println!("Would prompt: {}", prompt); +//! false +//! } +//! +//! fn notify(&self, message: &str) { +//! println!("[INFO] {}", message); +//! } +//! } +//! ``` +//! +//! # Thread Safety +//! +//! All provided implementations are `Send + Sync`, making them safe to use +//! across async tasks and threads. + +pub mod progress; +pub mod prompts; + +// Re-export commonly used types at the module level +pub use progress::{NoOpProgress, ProgressReporter}; +pub use prompts::{AutoApprove, DenyAll, UserInteraction}; diff --git a/src/internal/progress.rs b/src/internal/progress.rs new file mode 100644 index 0000000..d1fff7b --- /dev/null +++ b/src/internal/progress.rs @@ -0,0 +1,102 @@ +//! Progress reporting abstractions for library-first design. +//! +//! This module provides traits and implementations for progress reporting, +//! allowing the library to be used both with visual progress indicators (CLI) +//! and silently (library/testing). + +/// Trait for progress reporting during long-running operations. +/// +/// This trait abstracts away the progress visualization mechanism, allowing +/// the library to be used in different contexts: +/// - CLI applications can use `IndicatifProgress` (in the cli module) +/// - Library consumers can use `NoOpProgress` +/// - Tests can use mock implementations to verify progress calls +/// +/// # Example +/// +/// ```rust +/// use dx_scope::ProgressReporter; +/// +/// fn run_checks(progress: &P, groups: &[&str]) { +/// for group in groups { +/// progress.start_group(group, 3); +/// // Run actions... +/// progress.advance_action("action1", "Checking dependencies"); +/// progress.finish_group(); +/// } +/// } +/// ``` +pub trait ProgressReporter: Send + Sync { + /// Start a new group of actions. + /// + /// # Arguments + /// * `name` - The name of the group being processed + /// * `total_actions` - The total number of actions in this group + fn start_group(&self, name: &str, total_actions: usize); + + /// Advance to the next action within the current group. + /// + /// # Arguments + /// * `name` - The name of the action + /// * `description` - A description of what the action does + fn advance_action(&self, name: &str, description: &str); + + /// Finish the current group. + fn finish_group(&self); +} + +/// No-op progress reporter for library use. +/// +/// This implementation does nothing for all progress methods. +/// Useful for: +/// - Library usage where visual progress isn't needed +/// - Testing scenarios where progress output should be suppressed +/// - Automated/CI environments +/// +/// # Example +/// +/// ```rust +/// use dx_scope::{ProgressReporter, NoOpProgress}; +/// +/// let progress = NoOpProgress; +/// progress.start_group("test", 5); // Does nothing +/// progress.advance_action("action", "description"); // Does nothing +/// progress.finish_group(); // Does nothing +/// ``` +#[derive(Debug, Clone, Copy, Default)] +pub struct NoOpProgress; + +impl ProgressReporter for NoOpProgress { + fn start_group(&self, _name: &str, _total_actions: usize) { + // No-op + } + + fn advance_action(&self, _name: &str, _description: &str) { + // No-op + } + + fn finish_group(&self) { + // No-op + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_noop_progress_does_not_panic() { + let progress = NoOpProgress; + progress.start_group("test-group", 10); + progress.advance_action("action1", "Test action"); + progress.advance_action("action2", "Another action"); + progress.finish_group(); + // Should complete without panicking + } + + #[test] + fn test_noop_progress_is_send_sync() { + fn assert_send_sync() {} + assert_send_sync::(); + } +} diff --git a/src/internal/prompts.rs b/src/internal/prompts.rs new file mode 100644 index 0000000..8f3ef99 --- /dev/null +++ b/src/internal/prompts.rs @@ -0,0 +1,129 @@ +//! User interaction abstractions for library-first design. +//! +//! This module provides traits and implementations for user interaction, +//! allowing the library to be used both interactively (CLI) and programmatically. + +/// Trait for user interaction (prompts, confirmations). +/// +/// This trait abstracts away the interactive prompting mechanism, allowing +/// the library to be used in different contexts: +/// - CLI applications can use `InquireInteraction` (in the cli module) +/// - Library consumers can use `AutoApprove` or `DenyAll` +/// - Tests can use mock implementations +/// +/// # Example +/// +/// ```rust +/// use dx_scope::UserInteraction; +/// +/// fn run_with_interaction(interaction: &U) { +/// if interaction.confirm("Apply fix?", Some("This will modify files")) { +/// // Apply the fix +/// } +/// } +/// ``` +pub trait UserInteraction: Send + Sync { + /// Prompt user for yes/no confirmation. + /// + /// # Arguments + /// * `prompt` - The question to ask the user + /// * `help_text` - Optional additional context or help text + /// + /// # Returns + /// `true` if the user confirms, `false` otherwise + fn confirm(&self, prompt: &str, help_text: Option<&str>) -> bool; + + /// Notify the user with a message (non-blocking). + /// + /// This is used for informational messages that don't require user input. + fn notify(&self, message: &str); +} + +/// Auto-approve all prompts. +/// +/// This implementation automatically approves all confirmation prompts. +/// Useful for: +/// - Automated/CI environments where human interaction isn't available +/// - Testing scenarios where you want fixes to run automatically +/// - Library usage where the caller has pre-approved all operations +/// +/// # Example +/// +/// ```rust +/// use dx_scope::{UserInteraction, AutoApprove}; +/// +/// let interaction = AutoApprove; +/// assert!(interaction.confirm("Apply fix?", None)); // Always returns true +/// ``` +#[derive(Debug, Clone, Copy, Default)] +pub struct AutoApprove; + +impl UserInteraction for AutoApprove { + fn confirm(&self, _prompt: &str, _help_text: Option<&str>) -> bool { + true + } + + fn notify(&self, _message: &str) { + // No-op: auto-approve mode doesn't display notifications + } +} + +/// Deny all prompts. +/// +/// This implementation automatically denies all confirmation prompts. +/// Useful for: +/// - Non-interactive environments where no changes should be made +/// - Testing scenarios where you want to verify denial handling +/// - Dry-run modes where operations should be skipped +/// +/// # Example +/// +/// ```rust +/// use dx_scope::{UserInteraction, DenyAll}; +/// +/// let interaction = DenyAll; +/// assert!(!interaction.confirm("Apply fix?", None)); // Always returns false +/// ``` +#[derive(Debug, Clone, Copy, Default)] +pub struct DenyAll; + +impl UserInteraction for DenyAll { + fn confirm(&self, _prompt: &str, _help_text: Option<&str>) -> bool { + false + } + + fn notify(&self, _message: &str) { + // No-op: deny-all mode doesn't display notifications + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_auto_approve_always_returns_true() { + let interaction = AutoApprove; + assert!(interaction.confirm("Test prompt?", None)); + assert!(interaction.confirm("Another prompt?", Some("With help text"))); + } + + #[test] + fn test_deny_all_always_returns_false() { + let interaction = DenyAll; + assert!(!interaction.confirm("Test prompt?", None)); + assert!(!interaction.confirm("Another prompt?", Some("With help text"))); + } + + #[test] + fn test_auto_approve_notify_does_not_panic() { + let interaction = AutoApprove; + interaction.notify("Test notification"); // Should not panic + } + + #[test] + fn test_deny_all_notify_does_not_panic() { + let interaction = DenyAll; + interaction.notify("Test notification"); // Should not panic + } +} diff --git a/src/lib.rs b/src/lib.rs index 7268367..2b372a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,135 @@ +//! # dev-scope Library +//! +//! This library provides the core functionality for the `scope` tool, +//! designed for library-first usage with a thin CLI wrapper. +//! +//! ## Key Features +//! +//! - **Analyze**: Detect known errors in command output and log files +//! - **Doctor**: Run health checks with automatic fixes +//! - **Config**: Load and manage scope configuration files +//! +//! ## Library Usage +//! +//! The library can be used programmatically without CLI dependencies: +//! +//! ```rust +//! use dx_scope::{ +//! AnalyzeOptions, AnalyzeInput, AutoApprove, +//! DoctorRunOptions, FoundConfig, +//! }; +//! +//! // Create configuration +//! let working_dir = std::env::current_dir().unwrap(); +//! let config = FoundConfig::empty(working_dir); +//! +//! // Configure analyze options +//! let analyze_options = AnalyzeOptions::default(); +//! let input = AnalyzeInput::from_lines(vec!["test".to_string()]); +//! // Use dx_scope::analyze::process_input(&analyze_options, input, &AutoApprove).await +//! +//! // Configure doctor options for CI mode +//! let doctor_options = DoctorRunOptions::ci_mode(); +//! // Use dx_scope::doctor::run(&config, doctor_options).await +//! ``` +//! +//! ## Modules +//! +//! - [`analyze`] - Log and output analysis for known errors +//! - [`doctor`] - Health checks and automatic fixes +//! - [`internal`] - Abstraction traits (UserInteraction, ProgressReporter) +//! - [`shared`] - Shared utilities and configuration loading +//! - [`models`] - Data model definitions +//! +//! ## CLI Module +//! +//! The `cli` module contains CLI-specific implementations and is not exported +//! as part of the public library API. However, `InquireInteraction` is re-exported +//! at the crate root for convenience when building CLI applications. + pub mod analyze; pub mod doctor; +pub mod internal; pub mod lint; pub mod models; pub mod report; pub mod shared; +// CLI module is internal - not part of public library API +// Only InquireInteraction is re-exported for CLI usage +pub(crate) mod cli; + +// Re-export key types at crate root for convenience +// Analyze module +pub use analyze::{AnalyzeInput, AnalyzeOptions, AnalyzeStatus}; + +// Doctor module +pub use doctor::{DoctorRunOptions, PathRunResult}; + +// Config module +pub use shared::config::ConfigLoadOptions; +pub use shared::prelude::FoundConfig; + +// Internal abstractions (for library implementors) +pub use internal::progress::{NoOpProgress, ProgressReporter}; +pub use internal::prompts::{AutoApprove, DenyAll, UserInteraction}; + +// CLI implementations +pub use cli::InquireInteraction; + +// Capture module (for CLI tools that intercept commands) +pub use shared::prelude::{ + CaptureOpts, DefaultExecutionProvider, OutputCapture, OutputDestination, +}; + +// Logging module (for CLI tools) +pub use shared::prelude::LoggingOpts; + +// Config loading (for CLI tools) +pub use shared::prelude::ConfigOptions; + +// Report builders (for CLI tools) +pub use shared::prelude::{DefaultGroupedReportBuilder, DefaultUnstructuredReportBuilder}; + +// Report traits (for CLI tools that need to render reports) +pub use shared::prelude::{ReportRenderer, UnstructuredReportBuilder}; + +// Model traits (for accessing metadata on config models) +pub use models::HelpMetadata; + +// CLI argument types (for CLI binaries) +pub use analyze::prelude::{AnalyzeArgs, analyze_root}; +pub use doctor::prelude::{DoctorArgs, doctor_root}; +pub use lint::cli::LintArgs; +pub use lint::commands::lint_root; +pub use report::prelude::{ReportArgs, report_root}; + +// Shared utilities (for CLI binaries) +pub use shared::{print_details, CONFIG_FILE_PATH_ENV, RUN_ID_ENV_VAR}; + +/// Prelude module for convenient glob imports. +/// +/// **DEPRECATED**: This module will be removed in a future version. +/// For new code, use explicit imports from the crate root or specific modules instead. +/// +/// # Migration +/// +/// Instead of: +/// ```rust +/// # #[allow(deprecated)] +/// use dx_scope::prelude::*; +/// ``` +/// +/// Use explicit imports: +/// ```rust +/// use dx_scope::{DoctorRunOptions, AnalyzeOptions, FoundConfig}; +/// use dx_scope::doctor; +/// use dx_scope::analyze; +/// ``` +#[deprecated( + since = "2026.1.13", + note = "Use explicit imports from crate root or specific modules instead of prelude" +)] pub mod prelude { pub use crate::analyze::prelude::*; pub use crate::doctor::prelude::*; diff --git a/src/shared/config/options.rs b/src/shared/config/options.rs new file mode 100644 index 0000000..5d7a37e --- /dev/null +++ b/src/shared/config/options.rs @@ -0,0 +1,210 @@ +//! CLI-independent configuration loading options. +//! +//! This module provides options types that can be used to configure +//! configuration loading without depending on CLI argument parsing (clap). +//! +//! # Overview +//! +//! Scope configuration is loaded from YAML files in `.scope` directories. +//! By default, scope searches: +//! +//! 1. Ancestor directories from the working directory (`.scope/`) +//! 2. User home directory (`~/.scope/`) +//! 3. System config directory +//! +//! # Examples +//! +//! ## Default Discovery +//! +//! ```rust +//! use dx_scope::shared::config::ConfigLoadOptions; +//! +//! // Uses default discovery from current directory +//! let options = ConfigLoadOptions::default(); +//! ``` +//! +//! ## Custom Working Directory +//! +//! ```rust +//! use dx_scope::shared::config::ConfigLoadOptions; +//! use std::path::PathBuf; +//! +//! // Search from a specific directory +//! let options = ConfigLoadOptions::with_working_dir( +//! PathBuf::from("/path/to/my/project") +//! ); +//! ``` +//! +//! ## Additional Config Paths +//! +//! ```rust +//! use dx_scope::shared::config::ConfigLoadOptions; +//! use std::path::PathBuf; +//! +//! // Add extra config directories to the search +//! let options = ConfigLoadOptions::with_extra_config(vec![ +//! PathBuf::from("/shared/team/config"), +//! PathBuf::from("/company/global/scope"), +//! ]); +//! ``` +//! +//! ## Explicit Paths Only +//! +//! ```rust +//! use dx_scope::shared::config::ConfigLoadOptions; +//! use std::path::PathBuf; +//! +//! // Disable default discovery, use only specified paths +//! let options = ConfigLoadOptions::explicit_only(vec![ +//! PathBuf::from("/my/config/only"), +//! ]); +//! ``` +//! +//! # Configuration Precedence +//! +//! When the same configuration item appears in multiple files, the first +//! occurrence wins. Files are processed in order: +//! +//! 1. Most specific directory (closest to working dir) +//! 2. Parent directories (ascending) +//! 3. Home directory +//! 4. System config +//! 5. Extra config paths (in order specified) + +use std::path::PathBuf; + +/// Options for loading scope configuration. +/// +/// This struct contains all the configuration needed to find and load +/// scope configuration files, without any CLI-specific dependencies like clap. +/// +/// # Example +/// +/// ```rust,no_run +/// use dx_scope::shared::config::ConfigLoadOptions; +/// use std::path::PathBuf; +/// +/// // Use default configuration discovery +/// let options = ConfigLoadOptions::default(); +/// +/// // Load from specific directories +/// let options = ConfigLoadOptions { +/// extra_config: vec![PathBuf::from("/custom/config/path")], +/// ..Default::default() +/// }; +/// +/// // Disable default config discovery (only use explicit paths) +/// let options = ConfigLoadOptions { +/// disable_default_config: true, +/// extra_config: vec![PathBuf::from("/my/config")], +/// ..Default::default() +/// }; +/// ``` +#[derive(Debug, Clone, Default)] +pub struct ConfigLoadOptions { + /// Additional paths to search for configuration files. + /// These are added to the default discovery paths. + pub extra_config: Vec, + + /// When true, skip default config discovery (ancestor .scope directories, + /// home directory, system config). Only explicitly specified paths are used. + pub disable_default_config: bool, + + /// Override the working directory for config discovery and command execution. + /// If None, uses the current working directory. + pub working_dir: Option, + + /// Custom run ID for this execution. + /// If None, a unique ID will be generated. + pub run_id: Option, +} + +impl ConfigLoadOptions { + /// Create options that only use explicitly specified config paths. + pub fn explicit_only(paths: Vec) -> Self { + Self { + extra_config: paths, + disable_default_config: true, + ..Default::default() + } + } + + /// Create options with a custom working directory. + pub fn with_working_dir(working_dir: PathBuf) -> Self { + Self { + working_dir: Some(working_dir), + ..Default::default() + } + } + + /// Create options with additional config paths. + pub fn with_extra_config(paths: Vec) -> Self { + Self { + extra_config: paths, + ..Default::default() + } + } + + /// Get the working directory, falling back to current directory. + pub fn get_working_dir(&self) -> std::io::Result { + match &self.working_dir { + Some(dir) => Ok(dir.clone()), + None => std::env::current_dir(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_options() { + let options = ConfigLoadOptions::default(); + assert!(options.extra_config.is_empty()); + assert!(!options.disable_default_config); + assert!(options.working_dir.is_none()); + assert!(options.run_id.is_none()); + } + + #[test] + fn test_explicit_only() { + let paths = vec![PathBuf::from("/path1"), PathBuf::from("/path2")]; + let options = ConfigLoadOptions::explicit_only(paths.clone()); + assert_eq!(options.extra_config, paths); + assert!(options.disable_default_config); + } + + #[test] + fn test_with_working_dir() { + let dir = PathBuf::from("/custom/dir"); + let options = ConfigLoadOptions::with_working_dir(dir.clone()); + assert_eq!(options.working_dir, Some(dir)); + } + + #[test] + fn test_with_extra_config() { + let paths = vec![PathBuf::from("/extra/config")]; + let options = ConfigLoadOptions::with_extra_config(paths.clone()); + assert_eq!(options.extra_config, paths); + assert!(!options.disable_default_config); + } + + #[test] + fn test_get_working_dir_with_override() { + let dir = PathBuf::from("/override/dir"); + let options = ConfigLoadOptions { + working_dir: Some(dir.clone()), + ..Default::default() + }; + assert_eq!(options.get_working_dir().unwrap(), dir); + } + + #[test] + fn test_get_working_dir_without_override() { + let options = ConfigLoadOptions::default(); + // Should return the current directory + let result = options.get_working_dir(); + assert!(result.is_ok()); + } +}