From 2f73416902ba792009b4f9da7791ec8b5650a975 Mon Sep 17 00:00:00 2001 From: Nishant Arora <1895906+whizzzkid@users.noreply.github.com> Date: Thu, 29 Jan 2026 20:25:19 +0000 Subject: [PATCH] feat: add core library foundation with internal abstractions - Rename crate from dev-scope to dx-scope - Add src/internal module with UserInteraction and ProgressReporter traits - Add InquireInteraction implementation in cli/mod.rs - Update build.rs for library support - Update doc tests and binaries to use new crate name This establishes the library-first architecture foundation. Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) --- Cargo.lock | 134 ++++++++++++++++++------------------- Cargo.toml | 2 +- build.rs | 17 ++++- src/bin/scope-intercept.rs | 2 +- src/bin/scope.rs | 4 +- src/cli/mod.rs | 95 ++++++++++++++++++++++++++ src/internal/mod.rs | 82 +++++++++++++++++++++++ src/internal/progress.rs | 102 ++++++++++++++++++++++++++++ src/internal/prompts.rs | 129 +++++++++++++++++++++++++++++++++++ src/lib.rs | 11 +++ src/shared/directories.rs | 6 +- 11 files changed, 509 insertions(+), 75 deletions(-) create mode 100644 src/cli/mod.rs create mode 100644 src/internal/mod.rs create mode 100644 src/internal/progress.rs create mode 100644 src/internal/prompts.rs 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..e01fd40 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/bin/scope-intercept.rs b/src/bin/scope-intercept.rs index faa1572..3ebc43d 100644 --- a/src/bin/scope-intercept.rs +++ b/src/bin/scope-intercept.rs @@ -1,5 +1,5 @@ use clap::Parser; -use dev_scope::prelude::*; +use dx_scope::prelude::*; 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..e2db22e 100644 --- a/src/bin/scope.rs +++ b/src/bin/scope.rs @@ -2,8 +2,8 @@ use anyhow::Result; use clap::CommandFactory; use clap::{Parser, Subcommand}; use colored::Colorize; -use dev_scope::prelude::*; -use dev_scope::report_stdout; +use dx_scope::prelude::*; +use dx_scope::report_stdout; use human_panic::setup_panic; use lazy_static::lazy_static; use regex::Regex; diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..1f38402 --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,95 @@ +//! 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/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..46c9a24 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,14 @@ 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 +pub(crate) mod cli; + pub mod prelude { pub use crate::analyze::prelude::*; pub use crate::doctor::prelude::*; @@ -14,6 +18,13 @@ pub mod prelude { pub use crate::shared::prelude::*; } +// Re-export internal abstractions at crate root for convenience +pub use internal::progress::{NoOpProgress, ProgressReporter}; +pub use internal::prompts::{AutoApprove, DenyAll, UserInteraction}; + +// Re-export CLI implementation for interactive applications +pub use cli::InquireInteraction; + /// Preferred way to output data to users. This macro will write the output to tracing for debugging /// and to stdout using the global stdout writer. Because we use the stdout writer, the calls /// will all be async. diff --git a/src/shared/directories.rs b/src/shared/directories.rs index 9bc13f7..3a9746d 100644 --- a/src/shared/directories.rs +++ b/src/shared/directories.rs @@ -25,7 +25,7 @@ use std::path::PathBuf; /// # Examples /// /// ``` -/// # use dev_scope::shared::directories::home; +/// # use dx_scope::shared::directories::home; /// if let Some(home) = home() { /// println!("Home directory: {}", home.display()); /// } @@ -47,7 +47,7 @@ pub fn home() -> Option { /// # Examples /// /// ``` -/// # use dev_scope::shared::directories::config; +/// # use dx_scope::shared::directories::config; /// if let Some(config_dir) = config() { /// println!("Config directory: {}", config_dir.display()); /// } @@ -79,7 +79,7 @@ pub fn config() -> Option { /// # Examples /// /// ``` -/// # use dev_scope::shared::directories::cache; +/// # use dx_scope::shared::directories::cache; /// if let Some(cache_dir) = cache() { /// println!("Cache directory: {}", cache_dir.display()); /// }