diff --git a/crates/typedialog-core/src/nickel/roundtrip.rs b/crates/typedialog-core/src/nickel/roundtrip.rs index 15412b0..2b44999 100644 --- a/crates/typedialog-core/src/nickel/roundtrip.rs +++ b/crates/typedialog-core/src/nickel/roundtrip.rs @@ -55,6 +55,15 @@ pub struct RoundtripResult { /// Validation result (if enabled) pub validation_passed: Option, + + /// Original input Nickel code (for diff) + pub input_nickel: String, + + /// Path to output file + pub output_path: PathBuf, + + /// Initial values loaded from input (for change detection) + pub initial_values: std::collections::HashMap, } impl RoundtripConfig { @@ -118,12 +127,26 @@ impl RoundtripConfig { ); } - // Step 2: Execute form to get results + // Step 2: Load defaults from input .ncl (if fields have nickel_path) + let initial_values = + Self::load_defaults_from_input(&self.input_ncl, &self.form_path, self.verbose)?; + + if self.verbose && !initial_values.is_empty() { + eprintln!( + "[roundtrip] Loaded {} default values from input", + initial_values.len() + ); + } + + // Step 3: Execute form to get results (with defaults pre-populated) if self.verbose { eprintln!("[roundtrip] Executing form: {}", self.form_path.display()); } - let form_results = Self::execute_form_with_backend(&self.form_path, backend).await?; + let initial_values_backup = initial_values.clone(); + let form_results = + Self::execute_form_with_backend_and_defaults(&self.form_path, backend, initial_values) + .await?; if self.verbose { eprintln!( @@ -197,8 +220,11 @@ impl RoundtripConfig { Ok(RoundtripResult { input_contracts, form_results, - output_nickel, + output_nickel: output_nickel.clone(), validation_passed, + input_nickel: input_source, + output_path: self.output_ncl, + initial_values: initial_values_backup, }) } @@ -230,12 +256,24 @@ impl RoundtripConfig { ); } - // Step 2: Execute form to get results + // Step 2: Load defaults from input .ncl (if fields have nickel_path) + let initial_values = + Self::load_defaults_from_input(&self.input_ncl, &self.form_path, self.verbose)?; + + if self.verbose && !initial_values.is_empty() { + eprintln!( + "[roundtrip] Loaded {} default values from input", + initial_values.len() + ); + } + + // Step 3: Execute form to get results (with defaults pre-populated) if self.verbose { eprintln!("[roundtrip] Executing form: {}", self.form_path.display()); } - let form_results = Self::execute_form(&self.form_path)?; + let initial_values_backup = initial_values.clone(); + let form_results = Self::execute_form_with_defaults(&self.form_path, initial_values)?; if self.verbose { eprintln!( @@ -309,15 +347,19 @@ impl RoundtripConfig { Ok(RoundtripResult { input_contracts, form_results, - output_nickel, + output_nickel: output_nickel.clone(), validation_passed, + input_nickel: input_source, + output_path: self.output_ncl, + initial_values: initial_values_backup, }) } - /// Execute a form with a specific backend and return results - async fn execute_form_with_backend( + /// Execute a form with a specific backend and return results (with defaults) + async fn execute_form_with_backend_and_defaults( form_path: &Path, backend: &mut dyn FormBackend, + initial_values: HashMap, ) -> Result> { // Read form definition let form_content = fs::read_to_string(form_path).map_err(|e| { @@ -330,24 +372,160 @@ impl RoundtripConfig { // Migrate to unified elements format if needed form.migrate_to_elements(); + // NOTE: We don't apply defaults here because execute_with_backend_two_phase_with_defaults + // will call build_element_list which reloads fragments from disk, losing any modifications. + // Instead, we pass initial_values to execute_with_backend_two_phase_with_defaults + // which will apply them after fragment expansion. + // Extract base directory for resolving relative paths (includes, fragments) let base_dir = form_path.parent().unwrap_or_else(|| Path::new(".")); - // Execute form using provided backend (TUI, Web, or CLI) - form_parser::execute_with_backend_two_phase(form, backend, None, base_dir).await + // Execute form using provided backend (TUI, Web, or CLI) with defaults + form_parser::execute_with_backend_two_phase_with_defaults( + form, + backend, + None, + base_dir, + Some(initial_values), + ) + .await } - /// Execute a form and return results (CLI backend only) - fn execute_form(form_path: &Path) -> Result> { - // Load form definition from file (resolves constraint interpolations + includes) - let form = form_parser::load_from_file(form_path)?; + /// Execute a form and return results (CLI backend only, with defaults) + fn execute_form_with_defaults( + form_path: &Path, + initial_values: HashMap, + ) -> Result> { + // Read form definition + let form_content = fs::read_to_string(form_path).map_err(|e| { + crate::error::ErrorWrapper::new(format!("Failed to read form file: {}", e)) + })?; + + // Parse TOML form definition + let mut form = form_parser::parse_toml(&form_content)?; + + // Migrate to unified elements format + form.migrate_to_elements(); + + // Merge initial values into form defaults (unified elements format) + for element in &mut form.elements { + if let form_parser::FormElement::Field(field) = element { + if let Some(value) = initial_values.get(&field.name) { + // Set as default if field doesn't already have one + if field.default.is_none() { + field.default = Some(form_parser::value_to_string(value)); + } + } + } + } // Extract base directory for resolving relative paths (includes, fragments) let base_dir = form_path.parent().unwrap_or_else(|| Path::new(".")); // Execute form using CLI backend (interactive prompts) + // This will expand includes from groups during execution form_parser::execute_with_base_dir(form, base_dir) } + + /// Load defaults from input Nickel file using form field nickel_path + fn load_defaults_from_input( + input_path: &Path, + form_path: &Path, + verbose: bool, + ) -> Result> { + // Export input .ncl to JSON + let json_value = match NickelCli::export(input_path) { + Ok(val) => val, + Err(_) => { + // If export fails (e.g., file is empty or has syntax errors), + // return empty defaults rather than failing the whole roundtrip + if verbose { + eprintln!( + "[roundtrip] Warning: Could not export input .ncl, proceeding without defaults" + ); + } + return Ok(HashMap::new()); + } + }; + + // Load form to get field definitions with nickel_path + let form_content = fs::read_to_string(form_path).map_err(|e| { + crate::error::ErrorWrapper::new(format!("Failed to read form file: {}", e)) + })?; + + let mut form = form_parser::parse_toml(&form_content)?; + form.migrate_to_elements(); + + // Extract base directory for resolving fragment includes + let base_dir = form_path.parent().unwrap_or_else(|| Path::new(".")); + + // Expand fragments to get ALL fields (including those in conditional groups) + // Uses expand_includes to process group elements with includes + let expanded_form = form_parser::expand_includes(form, base_dir)?; + + // Extract field definitions that have nickel_path + let fields_with_paths: Vec<_> = expanded_form + .elements + .iter() + .filter_map(|elem| { + if let form_parser::FormElement::Field(field) = elem { + if field.nickel_path.is_some() { + Some(field.clone()) + } else { + None + } + } else { + None + } + }) + .collect(); + + if fields_with_paths.is_empty() { + // No fields with nickel_path, return empty + return Ok(HashMap::new()); + } + + // Extract values using nickel_path + let mut defaults = HashMap::new(); + + if let Value::Object(obj) = json_value { + for field in &fields_with_paths { + if let Some(nickel_path) = &field.nickel_path { + if let Some(value) = + Self::extract_value_by_path(&Value::Object(obj.clone()), nickel_path) + { + defaults.insert(field.name.clone(), value); + } + } + } + } + + if verbose && !defaults.is_empty() { + eprintln!( + "[roundtrip] Extracted {} default values from {} fields with nickel_path", + defaults.len(), + fields_with_paths.len() + ); + } + + Ok(defaults) + } + + /// Extract a value from nested JSON using a path + fn extract_value_by_path(json: &Value, path: &[String]) -> Option { + let mut current = json; + + for key in path { + match current { + Value::Object(map) => { + current = map.get(key)?; + } + _ => return None, + } + } + + Some(current.clone()) + } } #[cfg(test)] @@ -367,4 +545,39 @@ mod tests { assert_eq!(config.output_ncl, PathBuf::from("output.ncl")); assert!(config.validate); } + + #[test] + fn test_extract_value_by_path() { + use serde_json::json; + + let json = json!({ + "ci": { + "project": { + "name": "test-project" + } + } + }); + + let path = vec!["ci".to_string(), "project".to_string(), "name".to_string()]; + let result = RoundtripConfig::extract_value_by_path(&json, &path); + + assert!(result.is_some()); + assert_eq!(result.unwrap(), json!("test-project")); + } + + #[test] + fn test_extract_value_by_path_missing() { + use serde_json::json; + + let json = json!({ + "ci": { + "project": {} + } + }); + + let path = vec!["ci".to_string(), "project".to_string(), "name".to_string()]; + let result = RoundtripConfig::extract_value_by_path(&json, &path); + + assert!(result.is_none()); + } } diff --git a/crates/typedialog-tui/src/commands/nickel.rs b/crates/typedialog-tui/src/commands/nickel.rs index c3e532c..0f21781 100644 --- a/crates/typedialog-tui/src/commands/nickel.rs +++ b/crates/typedialog-tui/src/commands/nickel.rs @@ -181,23 +181,33 @@ pub async fn nickel_roundtrip( eprintln!("[roundtrip] Generated {} bytes", result.output_nickel.len()); } - // Print summary - println!("✓ Roundtrip completed successfully (TUI backend)"); - println!(" Input fields: {}", result.form_results.len()); - println!( - " Imports preserved: {}", - result.input_contracts.imports.len() - ); - println!( - " Contracts preserved: {}", - result.input_contracts.field_contracts.len() + // Create and print terminal summary + use typedialog_core::nickel::summary::RoundtripSummary; + + let summary = RoundtripSummary::from_values( + &result.initial_values, + &result.form_results, + result.validation_passed, + result.output_path.display().to_string(), ); + // Print terminal summary + print!("{}", summary.render_terminal(verbose)); + + println!( + "\n✅ Configuration saved to: {}\n", + result.output_path.display() + ); + println!("Next steps:"); + println!( + " - Review the configuration: cat {}", + result.output_path.display() + ); + println!(" - Apply CI tools: (run your CI setup command)"); + println!(" - Re-run this script anytime to update your configuration\n"); + if let Some(passed) = result.validation_passed { - if passed { - println!(" ✓ Validation: PASSED"); - } else { - println!(" ✗ Validation: FAILED"); + if !passed { return Err(Error::validation_failed( "Nickel typecheck failed on output", )); diff --git a/crates/typedialog-web/src/main.rs b/crates/typedialog-web/src/main.rs index 6f69278..6d6c825 100644 --- a/crates/typedialog-web/src/main.rs +++ b/crates/typedialog-web/src/main.rs @@ -589,11 +589,32 @@ async fn nickel_roundtrip_cmd( let port = 8080; let mut backend = BackendFactory::create(BackendType::Web { port })?; + // Set roundtrip context for web backend so it can show summary page + if let Some(init_vals) = &initial_values { + use typedialog_core::backends::web::RoundtripContext; + + // Downcast to WebBackend to access state + let web_backend = backend + .as_any() + .downcast_ref::() + .ok_or_else(|| Error::validation_failed("Expected WebBackend"))?; + + if let Some(state) = web_backend.get_state() { + let context = RoundtripContext { + initial_values: init_vals.clone(), + output_path: output.clone(), + input_nickel: input_source.clone(), + }; + state.set_roundtrip_context(context).await; + } + } + println!("Starting interactive form on http://localhost:{}", port); println!("Complete the form and submit to continue...\n"); // USE THE SAME EXECUTION PATH AS NORMAL WEB BACKEND // This respects display_mode and calls execute_form_complete() when display_mode = "complete" + let initial_values_backup = initial_values.clone(); let form_results = form_parser::execute_with_backend_i18n_with_defaults( form, backend.as_mut(), @@ -669,24 +690,29 @@ async fn nickel_roundtrip_cmd( true }; - // Print summary - println!("✓ Roundtrip completed successfully (Web backend - interactive)"); - println!(" Input fields: {}", form_results.len()); - println!(" Imports preserved: {}", input_contracts.imports.len()); - println!( - " Contracts preserved: {}", - input_contracts.field_contracts.len() + // Create and print terminal summary + use typedialog_core::nickel::summary::RoundtripSummary; + + let summary = RoundtripSummary::from_values( + &initial_values_backup.unwrap_or_default(), + &form_results, + Some(validation_passed), + output.display().to_string(), ); - if validate { - if validation_passed { - println!(" ✓ Validation: PASSED"); - } else { - println!(" ✗ Validation: FAILED"); - return Err(Error::validation_failed( - "Nickel typecheck failed on output", - )); - } + // Print terminal summary + print!("{}", summary.render_terminal(false)); + + println!("\n✅ Configuration saved to: {}\n", output.display()); + println!("Next steps:"); + println!(" - Review the configuration: cat {}", output.display()); + println!(" - Apply CI tools: (run your CI setup command)"); + println!(" - Re-run this script anytime to update: .typedialog/ci/ci-configure.sh\n"); + + if !validation_passed { + return Err(Error::validation_failed( + "Nickel typecheck failed on output", + )); } Ok(()) diff --git a/crates/typedialog/src/commands/nickel.rs b/crates/typedialog/src/commands/nickel.rs index 4e00cc6..a26a543 100644 --- a/crates/typedialog/src/commands/nickel.rs +++ b/crates/typedialog/src/commands/nickel.rs @@ -175,22 +175,33 @@ pub fn nickel_roundtrip( eprintln!("[roundtrip] Generated {} bytes", result.output_nickel.len()); } - println!("✓ Roundtrip completed successfully"); - println!(" Input fields: {}", result.form_results.len()); - println!( - " Imports preserved: {}", - result.input_contracts.imports.len() - ); - println!( - " Contracts preserved: {}", - result.input_contracts.field_contracts.len() + // Create and print terminal summary + use typedialog_core::nickel::summary::RoundtripSummary; + + let summary = RoundtripSummary::from_values( + &result.initial_values, + &result.form_results, + result.validation_passed, + result.output_path.display().to_string(), ); + // Print terminal summary + print!("{}", summary.render_terminal(verbose)); + + println!( + "\n✅ Configuration saved to: {}\n", + result.output_path.display() + ); + println!("Next steps:"); + println!( + " - Review the configuration: cat {}", + result.output_path.display() + ); + println!(" - Apply CI tools: (run your CI setup command)"); + println!(" - Re-run this script anytime to update your configuration\n"); + if let Some(passed) = result.validation_passed { - if passed { - println!(" ✓ Validation: PASSED"); - } else { - println!(" ✗ Validation: FAILED"); + if !passed { return Err(Error::validation_failed( "Nickel typecheck failed on output", ));