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}