ggen_cli_lib/cmds/sync.rs
1//! Sync Command - The ONLY command in ggen v5
2//!
3//! `ggen sync` is the unified code synchronization pipeline that replaces ALL
4//! previous ggen commands. It transforms domain ontologies through inference
5//! rules into typed code via Tera templates.
6
7#![allow(clippy::unused_unit)] // clap-noun-verb macro generates this
8//!
9//! ## Architecture: Three-Layer Pattern
10//!
11//! - **Layer 3 (CLI)**: Input validation, output formatting, thin routing
12//! - **Layer 2 (Integration)**: Async execution, error handling
13//! - **Layer 1 (Domain)**: Pure generation logic from ggen_core::codegen
14//!
15//! ## Exit Codes
16//!
17//! | Code | Meaning |
18//! |------|---------|
19//! | 0 | Success |
20//! | 1 | Manifest validation error |
21//! | 2 | Ontology load error |
22//! | 3 | SPARQL query error |
23//! | 4 | Template rendering error |
24//! | 5 | File I/O error |
25//! | 6 | Timeout exceeded |
26
27use clap_noun_verb::Result as VerbResult;
28use clap_noun_verb_macros::verb;
29use ggen_core::codegen::{OutputFormat, SyncExecutor, SyncOptions, SyncResult};
30use serde::Serialize;
31use std::path::PathBuf;
32
33// ============================================================================
34// Output Types (re-exported for CLI compatibility)
35// ============================================================================
36
37/// Output for the `ggen sync` command
38#[derive(Debug, Clone, Serialize)]
39pub struct SyncOutput {
40 /// Overall status: "success" or "error"
41 pub status: String,
42
43 /// Number of files synced
44 pub files_synced: usize,
45
46 /// Total duration in milliseconds
47 pub duration_ms: u64,
48
49 /// Generated files with details
50 pub files: Vec<SyncedFile>,
51
52 /// Number of inference rules executed
53 pub inference_rules_executed: usize,
54
55 /// Number of generation rules executed
56 pub generation_rules_executed: usize,
57
58 /// Audit trail path (if enabled)
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub audit_trail: Option<String>,
61
62 /// Error message (if failed)
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub error: Option<String>,
65}
66
67/// Individual file sync result
68#[derive(Debug, Clone, Serialize)]
69pub struct SyncedFile {
70 /// File path relative to output directory
71 pub path: String,
72
73 /// File size in bytes
74 pub size_bytes: usize,
75
76 /// Action taken: "created", "updated", "unchanged"
77 pub action: String,
78}
79
80impl From<SyncResult> for SyncOutput {
81 fn from(result: SyncResult) -> Self {
82 Self {
83 status: result.status,
84 files_synced: result.files_synced,
85 duration_ms: result.duration_ms,
86 files: result
87 .files
88 .into_iter()
89 .map(|f| SyncedFile {
90 path: f.path,
91 size_bytes: f.size_bytes,
92 action: f.action,
93 })
94 .collect(),
95 inference_rules_executed: result.inference_rules_executed,
96 generation_rules_executed: result.generation_rules_executed,
97 audit_trail: result.audit_trail,
98 error: result.error,
99 }
100 }
101}
102
103// ============================================================================
104// The ONLY Command: ggen sync
105// ============================================================================
106
107/// Execute the complete code synchronization pipeline from a ggen.toml manifest.
108///
109/// This is THE ONLY command in ggen v5. It replaces all previous commands
110/// (`ggen generate`, `ggen validate`, `ggen template`, etc.) with a single
111/// unified pipeline.
112///
113/// ## Pipeline Flow
114///
115/// ```text
116/// ggen.toml → ontology → CONSTRUCT inference → SELECT → Template → Code
117/// ```
118///
119/// ## Examples
120///
121/// ```bash
122/// # Basic sync (the primary workflow)
123/// ggen sync
124///
125/// # Sync from specific manifest
126/// ggen sync --manifest project/ggen.toml
127///
128/// # Dry-run to preview changes
129/// ggen sync --dry-run
130///
131/// # Sync specific rule only
132/// ggen sync --rule structs
133///
134/// # Force overwrite with audit trail
135/// ggen sync --force --audit
136///
137/// # Watch mode for development
138/// ggen sync --watch --verbose
139///
140/// # Validate without generating
141/// ggen sync --validate-only
142///
143/// # JSON output for CI/CD
144/// ggen sync --format json
145/// ```
146#[allow(clippy::unused_unit, clippy::too_many_arguments)]
147#[verb("sync", "root")]
148pub fn sync(
149 manifest: Option<String>, output_dir: Option<String>, dry_run: Option<bool>,
150 force: Option<bool>, audit: Option<bool>, rule: Option<String>, verbose: Option<bool>,
151 watch: Option<bool>, validate_only: Option<bool>, format: Option<String>, timeout: Option<u64>,
152) -> VerbResult<SyncOutput> {
153 // Build options from CLI args
154 let options = build_sync_options(
155 manifest,
156 output_dir,
157 dry_run,
158 force,
159 audit,
160 rule,
161 verbose,
162 watch,
163 validate_only,
164 format,
165 timeout,
166 );
167
168 // Execute via domain executor
169 let result = SyncExecutor::new(options)
170 .execute()
171 .map_err(|e| clap_noun_verb::NounVerbError::execution_error(e.to_string()))?;
172
173 Ok(SyncOutput::from(result))
174}
175
176/// Build SyncOptions from CLI arguments
177///
178/// This helper keeps the verb function thin by extracting option building.
179#[allow(clippy::too_many_arguments)]
180fn build_sync_options(
181 manifest: Option<String>, output_dir: Option<String>, dry_run: Option<bool>,
182 force: Option<bool>, audit: Option<bool>, rule: Option<String>, verbose: Option<bool>,
183 watch: Option<bool>, validate_only: Option<bool>, format: Option<String>, timeout: Option<u64>,
184) -> SyncOptions {
185 let mut options = SyncOptions::new();
186
187 // Set manifest path
188 options.manifest_path = PathBuf::from(manifest.unwrap_or_else(|| "ggen.toml".to_string()));
189
190 // Set optional output directory
191 if let Some(dir) = output_dir {
192 options.output_dir = Some(PathBuf::from(dir));
193 }
194
195 // Set boolean flags
196 options.dry_run = dry_run.unwrap_or(false);
197 options.force = force.unwrap_or(false);
198 options.audit = audit.unwrap_or(false);
199 options.verbose = verbose.unwrap_or(false);
200 options.watch = watch.unwrap_or(false);
201 options.validate_only = validate_only.unwrap_or(false);
202
203 // Set selected rules
204 if let Some(r) = rule {
205 options.selected_rules = Some(vec![r]);
206 }
207
208 // Set output format
209 if let Some(fmt) = format {
210 options.output_format = fmt.parse().unwrap_or(OutputFormat::Text);
211 }
212
213 // Set timeout
214 if let Some(t) = timeout {
215 options.timeout_ms = t;
216 }
217
218 options
219}