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    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}