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 clippy::unused_async_trait_impl,
74 clippy::needless_pass_by_ref_mut
75)]
76pub mod config_clap;
77pub mod error;
78pub mod pack_install;
79pub mod prelude;
80pub mod progress;
81pub mod validation_lib;
82pub mod version_checker;
83
84// Note: std::io::Write was used for output capture with gag crate (now disabled)
85
86// Command modules - clap-noun-verb v26.5.19 auto-discovery
87pub mod cmds; // clap-noun-verb v26 entry points with #[verb] functions
88pub mod conventions; // File-based routing conventions
89pub mod receipt_manager; // Cryptographic receipt generation for CLI operations
90pub mod runtime; // Async/sync bridge utilities
91pub mod runtime_helper; // Sync CLI wrapper utilities for async operations // Common imports for commands
92
93// Re-export clap-noun-verb for auto-discovery
94pub use clap_noun_verb::{run, Result as ClapNounVerbResult};
95
96// Re-export Result type for use in cmds
97pub use ggen_core::utils::error::Result;
98
99/// Main entry point using clap-noun-verb v26.5.19 auto-discovery
100///
101/// This function delegates to clap-noun-verb::run() which automatically discovers
102/// all `\[verb\]` functions in the cmds module and its submodules.
103/// The version flag is handled automatically by clap-noun-verb.
104pub async fn cli_match() -> ggen_core::utils::error::Result<()> {
105 version_checker::check_outdated_binary();
106
107 // Find manifest path from CLI args to check if telemetry is configured in ggen.toml
108 let mut manifest_path = "ggen.toml".to_string();
109 let args: Vec<String> = std::env::args().collect();
110 for i in 0..args.len() {
111 if (args[i] == "--manifest" || args[i] == "-m") && i + 1 < args.len() {
112 manifest_path = args[i + 1].clone();
113 break;
114 }
115 }
116
117 let mut telemetry_config = None;
118 if std::path::Path::new(&manifest_path).exists() {
119 if let Ok(content) = std::fs::read_to_string(&manifest_path) {
120 if let Ok(config) = toml::from_str::<ggen_core::config_lib::GgenConfig>(&content) {
121 if let Some(ref tel) = config.telemetry {
122 telemetry_config = Some(ggen_core::telemetry::TelemetryConfig {
123 endpoint: tel.endpoint.clone(),
124 service_name: tel.service_name.clone(),
125 console_output: tel.console_output,
126 });
127 }
128 }
129 }
130 }
131
132 // Initialize OTLP telemetry only if configured in ggen.toml
133 let _telemetry_guard = if let Some(cfg) = telemetry_config {
134 ggen_core::telemetry::init_telemetry(cfg).ok()
135 } else {
136 None
137 };
138
139 // Root span so every CLI invocation produces at least one exportable trace
140 let args: Vec<String> = std::env::args().skip(1).collect();
141 let span = tracing::info_span!("ggen.cli", command = %args.join(" "), version = env!("CARGO_PKG_VERSION"));
142 let _enter = span.enter();
143
144 // Handle --version flag before delegating to clap-noun-verb
145 let args: Vec<String> = std::env::args().collect();
146 if args.iter().any(|arg| arg == "--version" || arg == "-V") {
147 println!("ggen {}", env!("CARGO_PKG_VERSION"));
148 return Ok(());
149 }
150
151 // Use clap-noun-verb auto-discovery (handles --version automatically, but we preempted it)
152 clap_noun_verb::run().map_err(|e| {
153 ggen_core::utils::error::Error::new(&format!("CLI execution failed: {}", e))
154 })?;
155 Ok(())
156}
157
158/// Structured result for programmatic CLI execution (used by Node addon)
159#[derive(Debug, Clone)]
160pub struct RunResult {
161 pub code: i32,
162 pub stdout: String,
163 pub stderr: String,
164}
165
166/// Programmatic entrypoint to execute the CLI with provided arguments and capture output.
167/// This avoids spawning a new process and preserves deterministic behavior.
168pub async fn run_for_node(args: Vec<String>) -> ggen_core::utils::error::Result<RunResult> {
169 use std::sync::Arc;
170 use std::sync::Mutex;
171
172 // Known top-level subcommands (nouns) registered via clap-noun-verb
173 const KNOWN_NOUNS: &[&str] = &[
174 "sync",
175 "init",
176 "doctor",
177 "pack",
178 "agent",
179 "packs",
180 "capability",
181 "graph",
182 "receipt",
183 "utils",
184 "policy",
185 "market",
186 "lifecycle",
187 "a2a",
188 "ci",
189 "framework",
190 "git-hooks",
191 "lsp",
192 "mcp",
193 "sigma",
194 "template",
195 "wizard",
196 // Global flags that are always valid
197 "--help",
198 "-h",
199 "--version",
200 "-V",
201 "--format",
202 "--select",
203 "--introspect",
204 "--structured-errors",
205 "--autonomic",
206 "help",
207 // Allow empty args (shows help)
208 ];
209
210 // Validate the first argument — if it's not a known noun or flag, return code 1 immediately
211 // This is necessary because run_cli() reads std::env::args() and cannot be passed our args.
212 let early_exit_code: Option<i32> = if let Some(first) = args.first() {
213 if KNOWN_NOUNS.contains(&first.as_str()) {
214 None // known — proceed
215 } else {
216 // Unknown subcommand — report error and return non-zero
217 log::error!("error: unrecognized subcommand '{}'", first);
218 Some(1)
219 }
220 } else {
221 None // no args — proceed (shows help)
222 };
223
224 if let Some(code) = early_exit_code {
225 return Ok(RunResult {
226 code,
227 stdout: String::new(),
228 stderr: format!(
229 "error: unrecognized subcommand '{}'",
230 args.first().unwrap_or(&String::new())
231 ),
232 });
233 }
234
235 // Prefix with a binary name to satisfy clap-noun-verb semantics
236 let _argv: Vec<String> = std::iter::once("ggen".to_string())
237 .chain(args.into_iter())
238 .collect();
239
240 // Create thread-safe buffers for capturing output
241 let stdout_buffer = Arc::new(Mutex::new(Vec::new()));
242 let stderr_buffer = Arc::new(Mutex::new(Vec::new()));
243
244 let stdout_clone = Arc::clone(&stdout_buffer);
245 let stderr_clone = Arc::clone(&stderr_buffer);
246
247 // Execute in a blocking task
248 // NOTE: Output capture with gag crate is disabled for now
249 let result = tokio::task::spawn_blocking(move || {
250 // Execute without capture (gag crate not available)
251 let code = match cmds::run_cli() {
252 Ok(()) => 0,
253 Err(err) => {
254 log::error!("{}", err);
255 1
256 }
257 };
258
259 // Suppress unused variable warnings
260 let _ = (stdout_clone, stderr_clone);
261
262 code
263 })
264 .await
265 .map_err(|e| ggen_core::utils::error::Error::new(&format!("Failed to execute CLI: {}", e)))?;
266
267 // Retrieve captured output, handle mutex poisoning gracefully
268 let stdout = match stdout_buffer.lock() {
269 Ok(guard) => String::from_utf8_lossy(&guard).to_string(),
270 Err(_poisoned) => {
271 log::warn!("Stdout buffer mutex was poisoned when reading, using empty string");
272 String::new()
273 }
274 };
275
276 let stderr = match stderr_buffer.lock() {
277 Ok(guard) => String::from_utf8_lossy(&guard).to_string(),
278 Err(_poisoned) => {
279 log::warn!("Stderr buffer mutex was poisoned when reading, using empty string");
280 String::new()
281 }
282 };
283
284 Ok(RunResult {
285 code: result,
286 stdout,
287 stderr,
288 })
289}