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}