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 cmds::run_cli() which calls clap_noun_verb::run() directly
265 // This properly discovers verbs registered via #[verb] macro using linkme
266 // #region agent log
267 debug_log(
268 "H5",
269 "lib.rs:cli_match:router",
270 "delegating to cmds::run_cli for verb discovery",
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 cmds::run_cli()?;
279
280 // #region agent log
281 debug_log(
282 "H5",
283 "lib.rs:cli_match:router",
284 "cmds::run_cli completed",
285 json!({}),
286 );
287 // #endregion
288 Ok(())
289}
290
291/// Structured result for programmatic CLI execution (used by Node addon)
292#[derive(Debug, Clone)]
293pub struct RunResult {
294 pub code: i32,
295 pub stdout: String,
296 pub stderr: String,
297}
298
299/// Programmatic entrypoint to execute the CLI with provided arguments and capture output.
300/// This avoids spawning a new process and preserves deterministic behavior.
301///
302/// Note: Uses deprecated run_cli() because cli_match() is async and cannot be called
303/// inside spawn_blocking. This is a legitimate architectural constraint.
304#[allow(deprecated)]
305pub async fn run_for_node(args: Vec<String>) -> ggen_utils::error::Result<RunResult> {
306 use std::sync::Arc;
307 use std::sync::Mutex;
308
309 // Prefix with a binary name to satisfy clap-noun-verb semantics
310 let _argv: Vec<String> = std::iter::once("ggen".to_string())
311 .chain(args.into_iter())
312 .collect();
313
314 // Create thread-safe buffers for capturing output
315 let stdout_buffer = Arc::new(Mutex::new(Vec::new()));
316 let stderr_buffer = Arc::new(Mutex::new(Vec::new()));
317
318 let stdout_clone = Arc::clone(&stdout_buffer);
319 let stderr_clone = Arc::clone(&stderr_buffer);
320
321 // Execute in a blocking task to avoid Send issues with gag
322 let result = tokio::task::spawn_blocking(move || {
323 // Capture stdout/stderr using gag buffers
324 let mut captured_stdout = Vec::new();
325 let mut captured_stderr = Vec::new();
326
327 let code = match (gag::BufferRedirect::stdout(), gag::BufferRedirect::stderr()) {
328 (Ok(mut so), Ok(mut se)) => {
329 // Execute using cmds router
330 let code_val = match cmds::run_cli() {
331 Ok(()) => 0,
332 Err(err) => {
333 let _ = writeln!(std::io::stderr(), "{}", err);
334 1
335 }
336 };
337
338 let _ = so.read_to_end(&mut captured_stdout);
339 let _ = se.read_to_end(&mut captured_stderr);
340
341 // Store captured output, handle mutex poisoning gracefully
342 match stdout_clone.lock() {
343 Ok(mut guard) => *guard = captured_stdout,
344 Err(poisoned) => {
345 // Recover from poisoned lock
346 log::warn!("Stdout mutex was poisoned, recovering");
347 let mut guard = poisoned.into_inner();
348 *guard = captured_stdout;
349 }
350 }
351
352 match stderr_clone.lock() {
353 Ok(mut guard) => *guard = captured_stderr,
354 Err(poisoned) => {
355 // Recover from poisoned lock
356 log::warn!("Stderr mutex was poisoned, recovering");
357 let mut guard = poisoned.into_inner();
358 *guard = captured_stderr;
359 }
360 }
361
362 code_val
363 }
364 _ => {
365 // Fallback: execute without capture
366 match cmds::run_cli() {
367 Ok(()) => 0,
368 Err(err) => {
369 log::error!("{}", err);
370 1
371 }
372 }
373 }
374 };
375
376 code
377 })
378 .await
379 .map_err(|e| ggen_utils::error::Error::new(&format!("Failed to execute CLI: {}", e)))?;
380
381 // Retrieve captured output, handle mutex poisoning gracefully
382 let stdout = match stdout_buffer.lock() {
383 Ok(guard) => String::from_utf8_lossy(&guard).to_string(),
384 Err(_poisoned) => {
385 log::warn!("Stdout buffer mutex was poisoned when reading, using empty string");
386 String::new()
387 }
388 };
389
390 let stderr = match stderr_buffer.lock() {
391 Ok(guard) => String::from_utf8_lossy(&guard).to_string(),
392 Err(_poisoned) => {
393 log::warn!("Stderr buffer mutex was poisoned when reading, using empty string");
394 String::new()
395 }
396 };
397
398 Ok(RunResult {
399 code: result,
400 stdout,
401 stderr,
402 })
403}