Skip to main content

cuenv/
lib.rs

1// Rust 1.92 compiler bug: false positives for thiserror/miette derive macro fields
2// https://github.com/rust-lang/rust/issues/147648
3#![allow(unused_assignments)]
4
5//! cuenv - CUE-powered environment management library
6//!
7//! This crate provides a library-first architecture for cuenv, allowing external
8//! crates to extend functionality by registering custom providers.
9//!
10//! # Architecture
11//!
12//! cuenv uses a unified provider system where providers implement one or more
13//! capability traits:
14//!
15//! - [`SyncCapability`] - Sync files from CUE configuration
16//! - [`RuntimeCapability`] - Execute tasks (future)
17//! - [`SecretCapability`] - Resolve secrets (future)
18//!
19//! # Example: Custom CLI with Additional Providers
20//!
21//! ```ignore
22//! use cuenv::{Cuenv, SyncCapability};
23//!
24//! fn main() -> cuenv::Result<()> {
25//!     Cuenv::builder()
26//!         .with_defaults()
27//!         .with_sync_provider(my_provider::CustomProvider::new())
28//!         .build()
29//!         .run()
30//! }
31//! ```
32//!
33//! # Example: Multi-Capability Provider
34//!
35//! A single provider can implement multiple capabilities:
36//!
37//! ```ignore
38//! use cuenv::{Provider, SyncCapability, RuntimeCapability};
39//!
40//! pub struct DaggerProvider;
41//!
42//! impl Provider for DaggerProvider {
43//!     fn name(&self) -> &'static str { "dagger" }
44//!     fn description(&self) -> &'static str { "Dagger-based sync and execution" }
45//!     fn as_any(&self) -> &dyn std::any::Any { self }
46//!     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
47//! }
48//!
49//! // Implement both SyncCapability and RuntimeCapability...
50//! ```
51
52// expect_used is allowed for infallible operations like writing to strings
53#![allow(clippy::expect_used)]
54
55mod builder;
56/// CLI argument parsing and exit codes.
57pub mod cli;
58/// Command implementations (task, env, sync, etc.).
59pub mod commands;
60/// Shell completion generation.
61pub mod completions;
62/// Multi-process event coordination.
63pub mod coordinator;
64/// Event handling and routing.
65pub mod events;
66/// Performance measurement utilities.
67pub mod performance;
68/// Provider trait definitions.
69pub mod provider;
70/// Built-in provider implementations.
71pub mod providers;
72/// Provider registration and lookup.
73pub mod registry;
74/// Tracing and logging configuration.
75pub mod tracing;
76/// Terminal UI components.
77pub mod tui;
78
79// Re-export public API
80pub use builder::CuenvBuilder;
81pub use cuenv_core::Result;
82pub use provider::{Provider, RuntimeCapability, SecretCapability, SyncCapability};
83pub use registry::ProviderRegistry;
84
85use crate::cli::EXIT_OK;
86
87/// The main cuenv application.
88///
89/// Use [`Cuenv::builder()`] to create a new instance with custom providers,
90/// or [`Cuenv::with_defaults()`] for the standard configuration.
91pub struct Cuenv {
92    /// The provider registry containing all registered providers.
93    pub registry: ProviderRegistry,
94}
95
96impl Cuenv {
97    /// Create a new builder for configuring cuenv.
98    #[must_use]
99    pub fn builder() -> CuenvBuilder {
100        CuenvBuilder::new()
101    }
102
103    /// Create cuenv with default providers (ci, codegen, rules).
104    #[must_use]
105    pub fn with_defaults() -> Self {
106        Self::builder().with_defaults().build()
107    }
108
109    /// Build the `sync` subcommand dynamically from registered providers.
110    ///
111    /// Each sync provider contributes a subcommand via its `build_sync_command()` method.
112    ///
113    /// # Example
114    ///
115    /// ```ignore
116    /// let cuenv = Cuenv::with_defaults();
117    /// let sync_cmd = cuenv.build_sync_command();
118    /// // sync_cmd has subcommands: ci, codegen, rules
119    /// ```
120    #[must_use]
121    pub fn build_sync_command(&self) -> clap::Command {
122        use clap::{Arg, Command};
123
124        let mut sync_cmd = Command::new("sync")
125            .about("Sync generated files from CUE configuration")
126            .arg(
127                Arg::new("path")
128                    .long("path")
129                    .short('p')
130                    .help("Path to directory containing CUE files")
131                    .default_value("."),
132            )
133            .arg(
134                Arg::new("package")
135                    .long("package")
136                    .help("Name of the CUE package to evaluate")
137                    .default_value("cuenv"),
138            )
139            .arg(
140                Arg::new("dry-run")
141                    .long("dry-run")
142                    .help("Show what would be generated without writing files")
143                    .action(clap::ArgAction::SetTrue)
144                    .global(true),
145            )
146            .arg(
147                Arg::new("check")
148                    .long("check")
149                    .help("Check if files are in sync without making changes")
150                    .action(clap::ArgAction::SetTrue)
151                    .global(true),
152            )
153            .arg(
154                Arg::new("all")
155                    .long("all")
156                    .short('A')
157                    .help("Sync all projects in the workspace")
158                    .action(clap::ArgAction::SetTrue)
159                    .global(true),
160            );
161
162        // Add subcommands from registered sync providers
163        for provider in self.registry.sync_providers() {
164            sync_cmd = sync_cmd.subcommand(provider.build_sync_command());
165        }
166
167        sync_cmd
168    }
169
170    /// Run the cuenv CLI (placeholder).
171    ///
172    /// **Note**: This method is a placeholder for future library-driven CLI execution.
173    /// Currently, the full CLI logic remains in the binary (`main.rs`).
174    ///
175    /// For now, use the `cuenv` binary directly or the individual provider APIs
176    /// like [`build_sync_command()`](Self::build_sync_command).
177    ///
178    /// # Errors
179    ///
180    /// Returns an error if command execution fails.
181    #[doc(hidden)]
182    pub fn run(self) -> Result<()> {
183        // Placeholder: In the future, this will parse args using build_sync_command()
184        // and dispatch to providers. For now, return success as the binary handles CLI.
185        let exit_code = run_cli_with_registry(self);
186        if exit_code == EXIT_OK {
187            Ok(())
188        } else {
189            Err(cuenv_core::Error::configuration(format!(
190                "Command failed with exit code {exit_code}"
191            )))
192        }
193    }
194}
195
196/// Run the CLI with the given cuenv instance.
197///
198/// This is a placeholder that will be expanded to use the registry
199/// for dynamic CLI generation in the future.
200fn run_cli_with_registry(_cuenv: Cuenv) -> i32 {
201    // For now, this just returns OK
202    // The actual CLI logic remains in main.rs
203    // Future: Use cuenv.registry to build dynamic CLI
204    EXIT_OK
205}
206
207/// Exit code for SIGINT (128 + signal number 2)
208pub const EXIT_SIGINT: i32 = 130;
209
210/// LLM context content (llms.txt + CUE schemas concatenated at build time)
211pub const LLMS_CONTENT: &str = include_str!(concat!(env!("OUT_DIR"), "/llms-full.txt"));
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_cuenv_builder() {
219        let cuenv = Cuenv::builder().build();
220        // Registry should be empty when no providers are added
221        assert!(cuenv.registry.is_empty());
222    }
223
224    #[test]
225    fn test_cuenv_with_defaults() {
226        let cuenv = Cuenv::with_defaults();
227        // Should have default sync providers registered
228        assert_eq!(cuenv.registry.sync_provider_count(), 3);
229    }
230
231    #[test]
232    fn test_dynamic_sync_command() {
233        let cuenv = Cuenv::with_defaults();
234        let sync_cmd = cuenv.build_sync_command();
235
236        // Should have the expected subcommands from providers
237        let subcommands: Vec<_> = sync_cmd.get_subcommands().map(|c| c.get_name()).collect();
238        assert!(subcommands.contains(&"ci"), "Missing 'ci' subcommand");
239        assert!(
240            subcommands.contains(&"codegen"),
241            "Missing 'codegen' subcommand"
242        );
243        assert!(subcommands.contains(&"rules"), "Missing 'rules' subcommand");
244    }
245
246    #[test]
247    fn test_dynamic_sync_command_empty_registry() {
248        let cuenv = Cuenv::builder().build();
249        let sync_cmd = cuenv.build_sync_command();
250
251        // Should have no subcommands when registry is empty
252        let subcommand_count = sync_cmd.get_subcommands().count();
253        assert_eq!(subcommand_count, 0);
254    }
255
256    #[test]
257    fn test_run_with_empty_registry() {
258        let cuenv = Cuenv::builder().build();
259        let result = cuenv.run();
260        // Should succeed (placeholder returns OK)
261        assert!(result.is_ok());
262    }
263
264    #[test]
265    fn test_run_with_defaults() {
266        let cuenv = Cuenv::with_defaults();
267        let result = cuenv.run();
268        // Should succeed (placeholder returns OK)
269        assert!(result.is_ok());
270    }
271
272    #[test]
273    fn test_exit_sigint_constant() {
274        // SIGINT exit code is 128 + 2 = 130
275        assert_eq!(EXIT_SIGINT, 130);
276    }
277
278    #[test]
279    #[allow(clippy::const_is_empty)]
280    fn test_llms_content_not_empty() {
281        // LLM content should contain some text
282        assert!(!LLMS_CONTENT.is_empty());
283    }
284
285    #[test]
286    fn test_sync_command_has_path_arg() {
287        let cuenv = Cuenv::with_defaults();
288        let sync_cmd = cuenv.build_sync_command();
289
290        let args: Vec<_> = sync_cmd
291            .get_arguments()
292            .map(|a| a.get_id().as_str())
293            .collect();
294        assert!(args.contains(&"path"));
295        assert!(args.contains(&"package"));
296        assert!(args.contains(&"dry-run"));
297        assert!(args.contains(&"check"));
298        assert!(args.contains(&"all"));
299    }
300
301    #[test]
302    fn test_run_cli_with_registry_returns_ok() {
303        let exit_code = run_cli_with_registry(Cuenv::builder().build());
304        assert_eq!(exit_code, EXIT_OK);
305    }
306}