Skip to main content

ggen_cli_lib/
lib.rs

1//! # ggen-cli - Command-line interface for ggen code generation
2//!
3//! This crate provides the command-line interface for ggen, using clap-noun-verb
4//! for automatic command discovery and routing. It bridges between user commands
5//! and the domain logic layer (ggen-domain).
6//!
7//! ## Architecture
8//!
9//! - **Command Discovery**: Uses clap-noun-verb v3.4.0 auto-discovery to find
10//!   all `\[verb\]` functions in the `cmds` module
11//! - **Async/Sync Bridge**: Provides runtime utilities to bridge async domain
12//!   functions with synchronous CLI execution
13//! - **Conventions**: File-based routing conventions for template-based command
14//!   generation
15//! - **Node Integration**: Programmatic entry point for Node.js addon integration
16//!
17//! ## Features
18//!
19//! - **Auto-discovery**: Commands are automatically discovered via clap-noun-verb
20//! - **Version handling**: Built-in `--version` flag support
21//! - **Output capture**: Programmatic execution with stdout/stderr capture
22//! - **Async support**: Full async/await support for non-blocking operations
23//!
24//! ## Examples
25//!
26//! ### Basic CLI Execution
27//!
28//! ```rust,no_run
29//! use ggen_cli_lib::cli_match;
30//!
31//! # async fn example() -> ggen_core::utils::error::Result<()> {
32//! // Execute CLI with auto-discovered commands
33//! cli_match().await?;
34//! # Ok(())
35//! # }
36//! ```
37//!
38//! ### Programmatic Execution
39//!
40//! ```rust,ignore
41//! use ggen_cli_lib::run_for_node;
42//!
43//! # async fn example() -> ggen_core::utils::error::Result<()> {
44//! let args = vec!["template".to_string(), "generate".to_string()];
45//! let result = run_for_node(args).await?;
46//! println!("Exit code: {}", result.code);
47//! println!("Output: {}", result.stdout);
48//! # Ok(())
49//! # }
50//! ```
51#![deny(warnings)]
52#![allow(unexpected_cfgs)]
53#![allow(unused_imports)]
54#![allow(dead_code)]
55// Poka-Yoke: Prevent warnings at compile time - compiler enforces correctness
56// Crate-level clippy exceptions for CLI conventions and test code:
57// - expect_used: explicit panic messages improve diagnostics in CLI error paths
58// - unwrap_used: pervasive in conventions/watcher patterns and test code
59// - panic: test assertions in integration tests
60#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
61#![allow(non_upper_case_globals)] // Allow macro-generated static variables from clap-noun-verb
62#![allow(clippy::unused_unit)] // clap-noun-verb #[verb] macro generates unit expressions
63#![allow(
64    clippy::needless_borrows_for_generic_args,
65    clippy::needless_question_mark,
66    clippy::new_without_default,
67    clippy::question_mark,
68    clippy::too_many_arguments,
69    clippy::unnecessary_lazy_evaluations,
70    clippy::unnecessary_map_or,
71    clippy::useless_conversion,
72    clippy::used_underscore_binding  // #[verb] macro generates code referencing underscore-prefixed CLI flag params
73)]
74pub mod config_clap;
75pub mod error;
76pub mod pack_install;
77pub mod prelude;
78pub mod progress;
79pub mod validation_lib;
80pub mod version_checker;
81
82// Note: std::io::Write was used for output capture with gag crate (now disabled)
83
84// Command modules - clap-noun-verb v26.5.19 auto-discovery
85pub mod cmds; // clap-noun-verb v26 entry points with #[verb] functions
86pub mod conventions; // File-based routing conventions
87pub mod receipt_manager; // Cryptographic receipt generation for CLI operations
88pub mod runtime; // Async/sync bridge utilities
89pub mod runtime_helper; // Sync CLI wrapper utilities for async operations // Common imports for commands
90
91// Re-export clap-noun-verb for auto-discovery
92pub use clap_noun_verb::{run, Result as ClapNounVerbResult};
93
94// Re-export Result type for use in cmds
95pub use ggen_core::utils::error::Result;
96
97/// Main entry point using clap-noun-verb v26.5.19 auto-discovery
98///
99/// This function delegates to clap-noun-verb::run() which automatically discovers
100/// all `\[verb\]` functions in the cmds module and its submodules.
101/// The version flag is handled automatically by clap-noun-verb.
102pub async fn cli_match() -> ggen_core::utils::error::Result<()> {
103    version_checker::check_outdated_binary();
104
105    // Find manifest path from CLI args to check if telemetry is configured in ggen.toml
106    let mut manifest_path = "ggen.toml".to_string();
107    let args: Vec<String> = std::env::args().collect();
108    for i in 0..args.len() {
109        if (args[i] == "--manifest" || args[i] == "-m") && i + 1 < args.len() {
110            manifest_path = args[i + 1].clone();
111            break;
112        }
113    }
114
115    let mut telemetry_config = None;
116    if std::path::Path::new(&manifest_path).exists() {
117        if let Ok(content) = std::fs::read_to_string(&manifest_path) {
118            if let Ok(config) = toml::from_str::<ggen_core::config_lib::GgenConfig>(&content) {
119                if let Some(ref tel) = config.telemetry {
120                    telemetry_config = Some(ggen_core::telemetry::TelemetryConfig {
121                        endpoint: tel.endpoint.clone(),
122                        service_name: tel.service_name.clone(),
123                        console_output: tel.console_output,
124                    });
125                }
126            }
127        }
128    }
129
130    // Initialize OTLP telemetry only if configured in ggen.toml
131    let _telemetry_guard = if let Some(cfg) = telemetry_config {
132        ggen_core::telemetry::init_telemetry(cfg).ok()
133    } else {
134        None
135    };
136
137    // Root span so every CLI invocation produces at least one exportable trace
138    let args: Vec<String> = std::env::args().skip(1).collect();
139    let span = tracing::info_span!("ggen.cli", command = %args.join(" "), version = env!("CARGO_PKG_VERSION"));
140    let _enter = span.enter();
141
142    // Handle --version flag before delegating to clap-noun-verb
143    let args: Vec<String> = std::env::args().collect();
144    if args.iter().any(|arg| arg == "--version" || arg == "-V") {
145        println!("ggen {}", env!("CARGO_PKG_VERSION"));
146        return Ok(());
147    }
148
149    // Use clap-noun-verb auto-discovery (handles --version automatically, but we preempted it)
150    clap_noun_verb::run().map_err(|e| {
151        ggen_core::utils::error::Error::new(&format!("CLI execution failed: {}", e))
152    })?;
153    Ok(())
154}
155
156/// Structured result for programmatic CLI execution (used by Node addon)
157#[derive(Debug, Clone)]
158pub struct RunResult {
159    pub code: i32,
160    pub stdout: String,
161    pub stderr: String,
162}
163
164/// Programmatic entrypoint to execute the CLI with provided arguments and capture output.
165/// This avoids spawning a new process and preserves deterministic behavior.
166pub async fn run_for_node(args: Vec<String>) -> ggen_core::utils::error::Result<RunResult> {
167    use std::sync::Arc;
168    use std::sync::Mutex;
169
170    // Prefix with a binary name to satisfy clap-noun-verb semantics
171    let _argv: Vec<String> = std::iter::once("ggen".to_string())
172        .chain(args.into_iter())
173        .collect();
174
175    // Create thread-safe buffers for capturing output
176    let stdout_buffer = Arc::new(Mutex::new(Vec::new()));
177    let stderr_buffer = Arc::new(Mutex::new(Vec::new()));
178
179    let stdout_clone = Arc::clone(&stdout_buffer);
180    let stderr_clone = Arc::clone(&stderr_buffer);
181
182    // Execute in a blocking task
183    // NOTE: Output capture with gag crate is disabled for now
184    let result = tokio::task::spawn_blocking(move || {
185        // Execute without capture (gag crate not available)
186        let code = match cmds::run_cli() {
187            Ok(()) => 0,
188            Err(err) => {
189                log::error!("{}", err);
190                1
191            }
192        };
193
194        // Suppress unused variable warnings
195        let _ = (stdout_clone, stderr_clone);
196
197        code
198    })
199    .await
200    .map_err(|e| ggen_core::utils::error::Error::new(&format!("Failed to execute CLI: {}", e)))?;
201
202    // Retrieve captured output, handle mutex poisoning gracefully
203    let stdout = match stdout_buffer.lock() {
204        Ok(guard) => String::from_utf8_lossy(&guard).to_string(),
205        Err(_poisoned) => {
206            log::warn!("Stdout buffer mutex was poisoned when reading, using empty string");
207            String::new()
208        }
209    };
210
211    let stderr = match stderr_buffer.lock() {
212        Ok(guard) => String::from_utf8_lossy(&guard).to_string(),
213        Err(_poisoned) => {
214            log::warn!("Stderr buffer mutex was poisoned when reading, using empty string");
215            String::new()
216        }
217    };
218
219    Ok(RunResult {
220        code: result,
221        stdout,
222        stderr,
223    })
224}