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::cli_match;
30//!
31//! # async fn example() -> ggen_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,no_run
41//! use ggen_cli::run_for_node;
42//!
43//! # async fn example() -> ggen_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
52#![deny(warnings)] // Poka-Yoke: Prevent warnings at compile time - compiler enforces correctness
53#![allow(non_upper_case_globals)] // Allow macro-generated static variables from clap-noun-verb
54
55use std::io::{Read, Write};
56
57// Command modules - clap-noun-verb v4.0.2 auto-discovery
58pub mod cmds; // clap-noun-verb v4 entry points with #[verb] functions
59pub mod conventions; // File-based routing conventions
60                     // pub mod domain;          // Business logic layer - MOVED TO ggen-domain crate
61#[cfg(feature = "autonomic")]
62pub mod introspection; // AI agent introspection: verb metadata discovery, capability handlers
63pub mod prelude;
64pub mod runtime; // Async/sync bridge utilities
65pub mod runtime_helper; // Sync CLI wrapper utilities for async operations // Common imports for commands
66
67// Re-export clap-noun-verb for auto-discovery
68pub use clap_noun_verb::{run, CommandRouter, Result as ClapNounVerbResult};
69
70/// Main entry point using clap-noun-verb v4.0.2 auto-discovery
71///
72/// This function handles global introspection flags (--capabilities, --introspect, --graph)
73/// before delegating to clap-noun-verb::run() which automatically discovers
74/// all `\[verb\]` functions in the cmds module and its submodules.
75/// The version flag is handled automatically by clap-noun-verb.
76pub async fn cli_match() -> ggen_utils::error::Result<()> {
77    // Check for introspection flags (must come before clap-noun-verb processing)
78    // These flags are for AI agent discovery and capability planning
79    #[cfg(feature = "autonomic")]
80    {
81        let args: Vec<String> = std::env::args().collect();
82        // Handle --graph flag (export complete command graph)
83        if args.contains(&"--graph".to_string()) {
84            let graph = introspection::build_command_graph();
85            let json = serde_json::to_string_pretty(&graph).map_err(|e| {
86                ggen_utils::error::Error::new(&format!("Failed to serialize command graph: {}", e))
87            })?;
88            println!("{}", json);
89            return Ok(());
90        }
91
92        // Handle --capabilities noun verb (list verb metadata and arguments)
93        if args.contains(&"--capabilities".to_string()) {
94            if args.len() >= 4 {
95                let noun = &args[args.iter().position(|x| x == "--capabilities").unwrap() + 1];
96                let verb = &args[args.iter().position(|x| x == "--capabilities").unwrap() + 2];
97
98                match introspection::get_verb_metadata(noun, verb) {
99                    Some(metadata) => {
100                        let json = serde_json::to_string_pretty(&metadata).map_err(|e| {
101                            ggen_utils::error::Error::new(&format!(
102                                "Failed to serialize metadata: {}",
103                                e
104                            ))
105                        })?;
106                        println!("{}", json);
107                        return Ok(());
108                    }
109                    None => {
110                        eprintln!("Verb not found: {}::{}", noun, verb);
111                        return Err(ggen_utils::error::Error::new(&format!(
112                            "Verb {}::{} not found",
113                            noun, verb
114                        )));
115                    }
116                }
117            } else {
118                return Err(ggen_utils::error::Error::new(
119                    "Usage: ggen --capabilities <noun> <verb>",
120                ));
121            }
122        }
123
124        // Handle --introspect noun verb (show type information)
125        if args.contains(&"--introspect".to_string()) {
126            if args.len() >= 4 {
127                let noun = &args[args.iter().position(|x| x == "--introspect").unwrap() + 1];
128                let verb = &args[args.iter().position(|x| x == "--introspect").unwrap() + 2];
129
130                match introspection::get_verb_metadata(noun, verb) {
131                    Some(metadata) => {
132                        // Show detailed type information
133                        println!("Verb: {}::{}", metadata.noun, metadata.verb);
134                        println!("Description: {}", metadata.description);
135                        println!("Return Type: {}", metadata.return_type);
136                        println!("JSON Output: {}", metadata.supports_json_output);
137                        println!("\nArguments:");
138                        for arg in metadata.arguments {
139                            println!(
140                                "  - {} ({}): {}",
141                                arg.name, arg.argument_type, arg.description
142                            );
143                            if let Some(default) = arg.default_value {
144                                println!("    Default: {}", default);
145                            }
146                            if arg.optional {
147                                println!("    Optional: yes");
148                            } else {
149                                println!("    Required: yes");
150                            }
151                        }
152                        return Ok(());
153                    }
154                    None => {
155                        eprintln!("Verb not found: {}::{}", noun, verb);
156                        return Err(ggen_utils::error::Error::new(&format!(
157                            "Verb {}::{} not found",
158                            noun, verb
159                        )));
160                    }
161                }
162            } else {
163                return Err(ggen_utils::error::Error::new(
164                    "Usage: ggen --introspect <noun> <verb>",
165                ));
166            }
167        }
168    }
169
170    // Use clap-noun-verb auto-discovery (handles --version automatically)
171    clap_noun_verb::run()
172        .map_err(|e| ggen_utils::error::Error::new(&format!("CLI execution failed: {}", e)))?;
173    Ok(())
174}
175
176/// Structured result for programmatic CLI execution (used by Node addon)
177#[derive(Debug, Clone)]
178pub struct RunResult {
179    pub code: i32,
180    pub stdout: String,
181    pub stderr: String,
182}
183
184/// Programmatic entrypoint to execute the CLI with provided arguments and capture output.
185/// This avoids spawning a new process and preserves deterministic behavior.
186pub async fn run_for_node(args: Vec<String>) -> ggen_utils::error::Result<RunResult> {
187    use std::sync::Arc;
188    use std::sync::Mutex;
189
190    // Prefix with a binary name to satisfy clap-noun-verb semantics
191    let _argv: Vec<String> = std::iter::once("ggen".to_string())
192        .chain(args.into_iter())
193        .collect();
194
195    // Create thread-safe buffers for capturing output
196    let stdout_buffer = Arc::new(Mutex::new(Vec::new()));
197    let stderr_buffer = Arc::new(Mutex::new(Vec::new()));
198
199    let stdout_clone = Arc::clone(&stdout_buffer);
200    let stderr_clone = Arc::clone(&stderr_buffer);
201
202    // Execute in a blocking task to avoid Send issues with gag
203    let result = tokio::task::spawn_blocking(move || {
204        // Capture stdout/stderr using gag buffers
205        let mut captured_stdout = Vec::new();
206        let mut captured_stderr = Vec::new();
207
208        let code = match (gag::BufferRedirect::stdout(), gag::BufferRedirect::stderr()) {
209            (Ok(mut so), Ok(mut se)) => {
210                // Execute using cmds router
211                let code_val = match cmds::run_cli() {
212                    Ok(()) => 0,
213                    Err(err) => {
214                        let _ = writeln!(std::io::stderr(), "{}", err);
215                        1
216                    }
217                };
218
219                let _ = so.read_to_end(&mut captured_stdout);
220                let _ = se.read_to_end(&mut captured_stderr);
221
222                // Store captured output, handle mutex poisoning gracefully
223                match stdout_clone.lock() {
224                    Ok(mut guard) => *guard = captured_stdout,
225                    Err(poisoned) => {
226                        // Recover from poisoned lock
227                        log::warn!("Stdout mutex was poisoned, recovering");
228                        let mut guard = poisoned.into_inner();
229                        *guard = captured_stdout;
230                    }
231                }
232
233                match stderr_clone.lock() {
234                    Ok(mut guard) => *guard = captured_stderr,
235                    Err(poisoned) => {
236                        // Recover from poisoned lock
237                        log::warn!("Stderr mutex was poisoned, recovering");
238                        let mut guard = poisoned.into_inner();
239                        *guard = captured_stderr;
240                    }
241                }
242
243                code_val
244            }
245            _ => {
246                // Fallback: execute without capture
247                match cmds::run_cli() {
248                    Ok(()) => 0,
249                    Err(err) => {
250                        log::error!("{}", err);
251                        1
252                    }
253                }
254            }
255        };
256
257        code
258    })
259    .await
260    .map_err(|e| ggen_utils::error::Error::new(&format!("Failed to execute CLI: {}", e)))?;
261
262    // Retrieve captured output, handle mutex poisoning gracefully
263    let stdout = match stdout_buffer.lock() {
264        Ok(guard) => String::from_utf8_lossy(&*guard).to_string(),
265        Err(_poisoned) => {
266            log::warn!("Stdout buffer mutex was poisoned when reading, using empty string");
267            String::new()
268        }
269    };
270
271    let stderr = match stderr_buffer.lock() {
272        Ok(guard) => String::from_utf8_lossy(&*guard).to_string(),
273        Err(_poisoned) => {
274            log::warn!("Stderr buffer mutex was poisoned when reading, using empty string");
275            String::new()
276        }
277    };
278
279    Ok(RunResult {
280        code: result,
281        stdout,
282        stderr,
283    })
284}