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,ignore
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,ignore
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::fs::OpenOptions;
56use std::io::{Read, Write};
57use std::time::{SystemTime, UNIX_EPOCH};
58
59// Command modules - clap-noun-verb v4.0.2 auto-discovery
60pub mod cmds; // clap-noun-verb v4 entry points with #[verb] functions
61pub mod conventions; // File-based routing conventions
62                     // pub mod domain;          // Business logic layer - MOVED TO ggen-domain crate
63#[cfg(feature = "autonomic")]
64pub mod introspection; // AI agent introspection: verb metadata discovery, capability handlers
65pub mod prelude;
66pub mod runtime; // Async/sync bridge utilities
67pub mod runtime_helper; // Sync CLI wrapper utilities for async operations // Common imports for commands
68#[cfg(any(feature = "full", feature = "test-quality"))]
69pub mod validation; // Compile-time validation (Andon Signal Validation Framework)
70
71// Re-export clap-noun-verb for auto-discovery
72pub use clap_noun_verb::{run, CliBuilder, CommandRouter, Result as ClapNounVerbResult};
73use serde_json::json;
74
75fn debug_log(hypothesis_id: &str, location: &str, message: &str, data: serde_json::Value) {
76    let timestamp = SystemTime::now()
77        .duration_since(UNIX_EPOCH)
78        .map(|d| d.as_millis())
79        .unwrap_or(0);
80
81    let payload = json!({
82        "sessionId": "debug-session",
83        "runId": "pre-fix",
84        "hypothesisId": hypothesis_id,
85        "location": location,
86        "message": message,
87        "data": data,
88        "timestamp": timestamp
89    });
90
91    if let Ok(mut file) = OpenOptions::new()
92        .create(true)
93        .append(true)
94        .open("/Users/sac/ggen/.cursor/debug.log")
95    {
96        let _ = writeln!(file, "{}", payload);
97    }
98}
99
100/// Main entry point using clap-noun-verb v4.0.2 auto-discovery
101///
102/// This function handles global introspection flags (--capabilities, --introspect, --graph)
103/// before delegating to clap-noun-verb::run() which automatically discovers
104/// all `\[verb\]` functions in the cmds module and its submodules.
105/// The version flag is handled automatically by clap-noun-verb.
106pub async fn cli_match() -> ggen_utils::error::Result<()> {
107    // Check for introspection flags (must come before clap-noun-verb processing)
108    // These flags are for AI agent discovery and capability planning
109    #[cfg(feature = "autonomic")]
110    {
111        let args: Vec<String> = std::env::args().collect();
112        // #region agent log
113        debug_log(
114            "H1",
115            "lib.rs:cli_match:entry",
116            "cli_match entry with args",
117            json!({ "args": args.clone() }),
118        );
119        // #endregion
120        // Handle --graph flag (export complete command graph)
121        if args.contains(&"--graph".to_string()) {
122            let graph = introspection::build_command_graph();
123            let json = serde_json::to_string_pretty(&graph).map_err(|e| {
124                ggen_utils::error::Error::new(&format!("Failed to serialize command graph: {}", e))
125            })?;
126            println!("{}", json);
127            // #region agent log
128            debug_log(
129                "H2",
130                "lib.rs:cli_match:graph",
131                "handled --graph flag",
132                json!({ "total_verbs": graph.total_verbs, "noun_count": graph.nouns.len() }),
133            );
134            // #endregion
135            return Ok(());
136        }
137
138        // Handle --capabilities noun verb (list verb metadata and arguments)
139        if args.contains(&"--capabilities".to_string()) {
140            if args.len() >= 4 {
141                let noun = &args[args.iter().position(|x| x == "--capabilities").unwrap() + 1];
142                let verb = &args[args.iter().position(|x| x == "--capabilities").unwrap() + 2];
143
144                match introspection::get_verb_metadata(noun, verb) {
145                    Some(metadata) => {
146                        let json = serde_json::to_string_pretty(&metadata).map_err(|e| {
147                            ggen_utils::error::Error::new(&format!(
148                                "Failed to serialize metadata: {}",
149                                e
150                            ))
151                        })?;
152                        println!("{}", json);
153                        // #region agent log
154                        debug_log(
155                            "H3",
156                            "lib.rs:cli_match:capabilities",
157                            "served --capabilities metadata",
158                            json!({ "noun": metadata.noun, "verb": metadata.verb, "arg_count": metadata.arguments.len() }),
159                        );
160                        // #endregion
161                        return Ok(());
162                    }
163                    None => {
164                        eprintln!("Verb not found: {}::{}", noun, verb);
165                        // #region agent log
166                        debug_log(
167                            "H3",
168                            "lib.rs:cli_match:capabilities",
169                            "capabilities verb not found",
170                            json!({ "noun": noun, "verb": verb }),
171                        );
172                        // #endregion
173                        return Err(ggen_utils::error::Error::new(&format!(
174                            "Verb {}::{} not found",
175                            noun, verb
176                        )));
177                    }
178                }
179            } else {
180                // #region agent log
181                debug_log(
182                    "H3",
183                    "lib.rs:cli_match:capabilities",
184                    "capabilities usage error",
185                    json!({ "arg_len": args.len() }),
186                );
187                // #endregion
188                return Err(ggen_utils::error::Error::new(
189                    "Usage: ggen --capabilities <noun> <verb>",
190                ));
191            }
192        }
193
194        // Handle --introspect noun verb (show type information)
195        if args.contains(&"--introspect".to_string()) {
196            if args.len() >= 4 {
197                let noun = &args[args.iter().position(|x| x == "--introspect").unwrap() + 1];
198                let verb = &args[args.iter().position(|x| x == "--introspect").unwrap() + 2];
199
200                match introspection::get_verb_metadata(noun, verb) {
201                    Some(metadata) => {
202                        // Show detailed type information
203                        println!("Verb: {}::{}", metadata.noun, metadata.verb);
204                        println!("Description: {}", metadata.description);
205                        println!("Return Type: {}", metadata.return_type);
206                        println!("JSON Output: {}", metadata.supports_json_output);
207                        println!("\nArguments:");
208                        for arg in &metadata.arguments {
209                            println!(
210                                "  - {} ({}): {}",
211                                arg.name, arg.argument_type, arg.description
212                            );
213                            if let Some(default) = &arg.default_value {
214                                println!("    Default: {}", default);
215                            }
216                            if arg.optional {
217                                println!("    Optional: yes");
218                            } else {
219                                println!("    Required: yes");
220                            }
221                        }
222                        // #region agent log
223                        debug_log(
224                            "H4",
225                            "lib.rs:cli_match:introspect",
226                            "served --introspect metadata",
227                            json!({ "noun": metadata.noun, "verb": metadata.verb, "arg_count": metadata.arguments.len(), "return_type": metadata.return_type }),
228                        );
229                        // #endregion
230                        return Ok(());
231                    }
232                    None => {
233                        eprintln!("Verb not found: {}::{}", noun, verb);
234                        // #region agent log
235                        debug_log(
236                            "H4",
237                            "lib.rs:cli_match:introspect",
238                            "introspect verb not found",
239                            json!({ "noun": noun, "verb": verb }),
240                        );
241                        // #endregion
242                        return Err(ggen_utils::error::Error::new(&format!(
243                            "Verb {}::{} not found",
244                            noun, verb
245                        )));
246                    }
247                }
248            } else {
249                // #region agent log
250                debug_log(
251                    "H4",
252                    "lib.rs:cli_match:introspect",
253                    "introspect usage error",
254                    json!({ "arg_len": args.len() }),
255                );
256                // #endregion
257                return Err(ggen_utils::error::Error::new(
258                    "Usage: ggen --introspect <noun> <verb>",
259                ));
260            }
261        }
262    }
263
264    // Use clap-noun-verb CliBuilder for explicit version configuration
265    // This ensures the correct version (from ggen's Cargo.toml) is displayed
266    // #region agent log
267    debug_log(
268        "H5",
269        "lib.rs:cli_match:router",
270        "delegating to CliBuilder with explicit version",
271        json!({ "version": env!("CARGO_PKG_VERSION") }),
272    );
273    // #endregion
274
275    // IMPORTANT: Don't wrap clap-noun-verb errors. Help/version are returned as errors
276    // with exit code 0, and wrapping them causes "ERROR: CLI execution failed" to appear.
277    // See: docs/howto/setup-help-and-version.md
278    CliBuilder::new()
279        .name("ggen")
280        .about("Language-agnostic, deterministic code generation CLI. Ontologies + RDF → reproducible code projections.")
281        .version(env!("CARGO_PKG_VERSION"))  // Use ggen's version (5.0.0), not clap-noun-verb's
282        .run()
283        .map_err(|e| ggen_utils::error::Error::new(&e.to_string()))?;
284
285    // #region agent log
286    debug_log(
287        "H5",
288        "lib.rs:cli_match:router",
289        "CliBuilder run completed",
290        json!({}),
291    );
292    // #endregion
293    Ok(())
294}
295
296/// Structured result for programmatic CLI execution (used by Node addon)
297#[derive(Debug, Clone)]
298pub struct RunResult {
299    pub code: i32,
300    pub stdout: String,
301    pub stderr: String,
302}
303
304/// Programmatic entrypoint to execute the CLI with provided arguments and capture output.
305/// This avoids spawning a new process and preserves deterministic behavior.
306///
307/// Note: Uses deprecated run_cli() because cli_match() is async and cannot be called
308/// inside spawn_blocking. This is a legitimate architectural constraint.
309#[allow(deprecated)]
310pub async fn run_for_node(args: Vec<String>) -> ggen_utils::error::Result<RunResult> {
311    use std::sync::Arc;
312    use std::sync::Mutex;
313
314    // Prefix with a binary name to satisfy clap-noun-verb semantics
315    let _argv: Vec<String> = std::iter::once("ggen".to_string())
316        .chain(args.into_iter())
317        .collect();
318
319    // Create thread-safe buffers for capturing output
320    let stdout_buffer = Arc::new(Mutex::new(Vec::new()));
321    let stderr_buffer = Arc::new(Mutex::new(Vec::new()));
322
323    let stdout_clone = Arc::clone(&stdout_buffer);
324    let stderr_clone = Arc::clone(&stderr_buffer);
325
326    // Execute in a blocking task to avoid Send issues with gag
327    let result = tokio::task::spawn_blocking(move || {
328        // Capture stdout/stderr using gag buffers
329        let mut captured_stdout = Vec::new();
330        let mut captured_stderr = Vec::new();
331
332        let code = match (gag::BufferRedirect::stdout(), gag::BufferRedirect::stderr()) {
333            (Ok(mut so), Ok(mut se)) => {
334                // Execute using cmds router
335                let code_val = match cmds::run_cli() {
336                    Ok(()) => 0,
337                    Err(err) => {
338                        let _ = writeln!(std::io::stderr(), "{}", err);
339                        1
340                    }
341                };
342
343                let _ = so.read_to_end(&mut captured_stdout);
344                let _ = se.read_to_end(&mut captured_stderr);
345
346                // Store captured output, handle mutex poisoning gracefully
347                match stdout_clone.lock() {
348                    Ok(mut guard) => *guard = captured_stdout,
349                    Err(poisoned) => {
350                        // Recover from poisoned lock
351                        log::warn!("Stdout mutex was poisoned, recovering");
352                        let mut guard = poisoned.into_inner();
353                        *guard = captured_stdout;
354                    }
355                }
356
357                match stderr_clone.lock() {
358                    Ok(mut guard) => *guard = captured_stderr,
359                    Err(poisoned) => {
360                        // Recover from poisoned lock
361                        log::warn!("Stderr mutex was poisoned, recovering");
362                        let mut guard = poisoned.into_inner();
363                        *guard = captured_stderr;
364                    }
365                }
366
367                code_val
368            }
369            _ => {
370                // Fallback: execute without capture
371                match cmds::run_cli() {
372                    Ok(()) => 0,
373                    Err(err) => {
374                        log::error!("{}", err);
375                        1
376                    }
377                }
378            }
379        };
380
381        code
382    })
383    .await
384    .map_err(|e| ggen_utils::error::Error::new(&format!("Failed to execute CLI: {}", e)))?;
385
386    // Retrieve captured output, handle mutex poisoning gracefully
387    let stdout = match stdout_buffer.lock() {
388        Ok(guard) => String::from_utf8_lossy(&guard).to_string(),
389        Err(_poisoned) => {
390            log::warn!("Stdout buffer mutex was poisoned when reading, using empty string");
391            String::new()
392        }
393    };
394
395    let stderr = match stderr_buffer.lock() {
396        Ok(guard) => String::from_utf8_lossy(&guard).to_string(),
397        Err(_poisoned) => {
398            log::warn!("Stderr buffer mutex was poisoned when reading, using empty string");
399            String::new()
400        }
401    };
402
403    Ok(RunResult {
404        code: result,
405        stdout,
406        stderr,
407    })
408}