//! export/import: back up the configuration and installation state. use super::*; use crate::config::{Config, Settings}; use crate::state::StateFile; use crate::output::print_json; use anyhow::{anyhow, Context, Result}; use serde::{Deserialize, Serialize}; use std::path::Path; #[derive(Serialize, Deserialize)] struct ExportFile { tool: String, version: String, generated_at: String, config: Config, state: StateFile, } pub fn export_cmd(app: &App, output: &Path) -> Result { let export = ExportFile { tool: "agent-manager".to_string(), version: env!("CARGO_PKG_VERSION").to_string(), generated_at: crate::installers::now_rfc3339(), config: app.config.clone(), state: app.state.load()?, }; if app.dry_run() { app.log .dry(format!("would write {}", output.display())); return Ok(0); } let text = serde_json::to_string_pretty(&export)?; std::fs::write(output, text) .with_context(|| format!("cannot write {}", output.display()))?; app.log .success(&format!("exported config + state to {}", output.display())); Ok(0) } pub fn import_cmd(app: &App, file: &Path) -> Result { let text = std::fs::read_to_string(file) .with_context(|| format!("cannot read {}", file.display()))?; let export: ExportFile = serde_json::from_str(&text) .with_context(|| format!("{} is not a valid agent-manager export", file.display()))?; if app.dry_run() { app.log .dry(format!("would import from {}", file.display())); return Ok(0); } // State: merge entries (existing ones win). let mut sf = app.state.load()?; let mut imported_state = 0; for (name, entry) in export.state.installed { if !sf.installed.contains_key(&name) { sf.installed.insert(name, entry); imported_state += 1; } } app.state.save(&sf)?; // Config: write the exported agents to a file and include it. let dir = app .paths .config_dir .clone() .ok_or_else(|| anyhow!("cannot determine the user config directory (no HOME set)"))?; std::fs::create_dir_all(&dir)?; let ts = chrono::Local::now().format("%Y%m%d-%H%M%S"); let fname = format!("imported-{ts}.yaml"); let dest = dir.join(&fname); let cfg = Config { version: "1.0".to_string(), settings: Settings::default(), aliases: Default::default(), groups: Default::default(), include: vec![], agents: export.config.agents.clone(), projects: Default::default(), profiles: export.config.profiles.clone(), }; std::fs::write(&dest, serde_yaml::to_string(&cfg)?) .with_context(|| format!("cannot write {}", dest.display()))?; let cfg_path = dir.join(crate::config::CONFIG_FILE_NAME); if !cfg_path.exists() { std::fs::write( &cfg_path, format!("version: \"1.0\"\ninclude:\n - {fname}\n"), )?; } else { let text = std::fs::read_to_string(&cfg_path)?; let mut value: serde_yaml::Value = serde_yaml::from_str(&text) .unwrap_or_else(|_| serde_yaml::Value::Null); if let serde_yaml::Value::Mapping(map) = &mut value { let key = serde_yaml::Value::String("include".to_string()); match map.get_mut(&key) { Some(serde_yaml::Value::Sequence(seq)) => { let entry = serde_yaml::Value::String(fname.clone()); if !seq.contains(&entry) { seq.push(entry); } } _ => { map.insert( key, serde_yaml::Value::Sequence(vec![serde_yaml::Value::String( fname.clone(), )]), ); } } std::fs::write(&cfg_path, serde_yaml::to_string(&value)?)?; } } if app.json() { print_json(&serde_json::json!({ "agents_imported": export.config.agents.len(), "state_entries_imported": imported_state, "config_file": dest.display().to_string(), })); } else { app.log.success(&format!( "imported {} agent definition(s) and {} state entrie(s) from {}", export.config.agents.len(), imported_state, file.display() )); app.log.info(&format!( "definitions written to {}", dest.display() )); } Ok(0) }