use crate::core::store::convert::{analyze_conversion, ConversionSignals};
use crate::core::store::convert_exec;
use std::path::Path;
pub(crate) fn cmd_convert(
file: &Path,
reproducible: bool,
apply: bool,
json: bool,
) -> Result<(), String> {
if !reproducible {
return Err("use --reproducible flag to enable conversion".to_string());
}
let signals = extract_signals(file)?;
let report = analyze_conversion(&signals);
if apply {
let result = convert_exec::apply_conversion(file, &report)?;
if json {
let j = serde_json::json!({
"changes_applied": result.changes_applied,
"backup_path": result.backup_path.display().to_string(),
"new_purity": format!("{:?}", result.new_purity),
"lock_pins_generated": result.lock_pins_generated,
});
println!(
"{}",
serde_json::to_string_pretty(&j).unwrap_or_else(|_| "{}".to_string())
);
} else {
println!("Conversion applied to {}:", file.display());
println!(" Changes: {}", result.changes_applied);
println!(" Backup: {}", result.backup_path.display());
println!(" New purity: {:?}", result.new_purity);
println!(" Lock pins: {}", result.lock_pins_generated);
}
return Ok(());
}
if json {
let j = serde_json::to_string_pretty(&report).unwrap_or_else(|_| "{}".to_string());
println!("{j}");
} else {
println!("Conversion report for {}:", file.display());
println!(
" Current purity: {:?} → Projected: {:?}",
report.current_purity, report.projected_purity
);
println!(
" Auto changes: {} | Manual changes: {}",
report.auto_change_count, report.manual_change_count
);
println!();
for res in &report.resources {
println!(" {}:", res.name);
println!(" {:?} → {:?}", res.current_purity, res.target_purity);
for c in &res.auto_changes {
println!(" [auto] {}", c.description);
}
for m in &res.manual_changes {
println!(" [manual] {m}");
}
}
if report.auto_change_count > 0 {
println!(
"\n Use --apply to apply {} changes",
report.auto_change_count
);
}
}
Ok(())
}
fn extract_signals(file: &Path) -> Result<Vec<ConversionSignals>, String> {
let content =
std::fs::read_to_string(file).map_err(|e| format!("read {}: {e}", file.display()))?;
let doc: serde_yaml_ng::Value =
serde_yaml_ng::from_str(&content).map_err(|e| format!("parse {}: {e}", file.display()))?;
let resources = doc
.get("resources")
.and_then(|r| r.as_mapping())
.ok_or_else(|| "no resources section found".to_string())?;
let mut signals = Vec::new();
for (key, val) in resources {
let name = key.as_str().unwrap_or("").to_string();
let provider = val
.get("provider")
.and_then(|v| v.as_str())
.unwrap_or("file")
.to_string();
let has_version = val.get("version").is_some();
let has_store = val.get("store").and_then(|v| v.as_bool()).unwrap_or(false);
let has_sandbox = val.get("sandbox").is_some();
let has_curl_pipe = detect_curl_pipe(val);
let current_version = val
.get("version")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
signals.push(ConversionSignals {
name,
has_version,
has_store,
has_sandbox,
has_curl_pipe,
provider,
current_version,
});
}
signals.sort_by(|a, b| a.name.cmp(&b.name));
Ok(signals)
}
fn detect_curl_pipe(val: &serde_yaml_ng::Value) -> bool {
let s = serde_yaml_ng::to_string(val).unwrap_or_default();
s.contains("curl") && s.contains("bash") || s.contains("wget") && s.contains("sh")
}