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