use std::io::{self, Write};
use std::path::PathBuf;
use anyhow::{Context, Result};
use zeptoclaw::config::Config;
use zeptoclaw::migrate::{self, MigrationReport};
use super::common::read_line;
pub(crate) async fn cmd_migrate(from: Option<String>, yes: bool, dry_run: bool) -> Result<()> {
if dry_run {
println!("(dry-run mode — no files will be written)");
println!();
}
let openclaw_dir = match from {
Some(ref path) => {
let p = PathBuf::from(path);
if !p.is_dir() {
anyhow::bail!("Specified path does not exist: {}", p.display());
}
p
}
None => {
println!("Searching for OpenClaw installation...");
match migrate::detect_openclaw_dir() {
Some(dir) => {
println!(" Found: {}", dir.display());
dir
}
None => {
println!("No OpenClaw installation detected.");
println!();
println!("Checked: ~/.openclaw, ~/.clawdbot, ~/.moldbot, $OPENCLAW_STATE_DIR");
println!();
println!("Use --from <path> to specify the OpenClaw directory manually.");
return Ok(());
}
}
}
};
println!();
println!("Loading OpenClaw config from: {}", openclaw_dir.display());
let openclaw_config = match migrate::load_openclaw_config(&openclaw_dir) {
Ok(c) => {
println!(" Config loaded successfully.");
c
}
Err(e) => {
println!(" Warning: Could not load config: {}", e);
println!(" Proceeding with skill migration only.");
serde_json::json!({})
}
};
let provider_count = openclaw_config
.get("models")
.and_then(|m| m.get("providers"))
.and_then(|p| p.as_object())
.map(|o| o.len())
.unwrap_or(0);
let channel_count = openclaw_config
.get("channels")
.and_then(|c| c.as_object())
.map(|o| o.len())
.unwrap_or(0);
let skill_dirs = migrate::skills::find_skill_dirs(&openclaw_dir, &openclaw_config);
let skill_count: usize = skill_dirs
.iter()
.filter_map(|d| std::fs::read_dir(d).ok())
.flat_map(|entries| entries.flatten())
.filter(|e| e.path().is_dir() && e.path().join("SKILL.md").is_file())
.count();
println!();
println!(" Providers found: {}", provider_count);
println!(" Channels found: {}", channel_count);
println!(" Skills found: {}", skill_count);
let mut report = MigrationReport::new(openclaw_dir.clone());
let mut config = Config::load().unwrap_or_default();
println!();
let do_config = if yes {
true
} else {
print!("Migrate config settings? [Y/n]: ");
io::stdout().flush()?;
let answer = read_line()?.to_ascii_lowercase();
answer.is_empty() || answer == "y" || answer == "yes"
};
if do_config {
let config_result = migrate::config::convert_config(&openclaw_config, &mut config);
report.config_migrated = config_result.migrated;
report.config_skipped = config_result.skipped;
report.not_portable = config_result.not_portable;
if !dry_run {
let config_path = Config::path();
if config_path.exists() {
let backup_path = config_path.with_extension("json.bak");
std::fs::copy(&config_path, &backup_path).with_context(|| {
format!("Failed to back up config to {}", backup_path.display())
})?;
println!(" Backed up existing config to: {}", backup_path.display());
}
config
.save()
.with_context(|| "Failed to save migrated config")?;
println!(" Config saved to: {}", Config::path().display());
} else {
println!(
" (dry-run) Would save config to: {}",
Config::path().display()
);
}
} else {
println!(" Skipping config migration.");
}
println!();
let do_skills = if skill_dirs.is_empty() || skill_count == 0 {
println!(" No skills found to copy.");
false
} else if yes {
true
} else {
print!("Copy {} skills to ZeptoClaw? [Y/n]: ", skill_count);
io::stdout().flush()?;
let answer = read_line()?.to_ascii_lowercase();
answer.is_empty() || answer == "y" || answer == "yes"
};
if do_skills {
let dest_dir = Config::dir().join("skills");
if !dry_run {
let (copied, skipped) = migrate::skills::copy_skills(&skill_dirs, &dest_dir)?;
report.skills_copied = copied;
report.skills_skipped = skipped;
println!(
" Copied {} skills to: {}",
report.skills_copied.len(),
dest_dir.display()
);
} else {
println!(
" (dry-run) Would copy {} skills to: {}",
skill_count,
dest_dir.display()
);
}
}
if do_config && !dry_run {
println!();
println!("Validating migrated config...");
let config_path = Config::path();
if config_path.exists() {
if let Ok(content) = std::fs::read_to_string(&config_path) {
if let Ok(raw) = serde_json::from_str::<serde_json::Value>(&content) {
let diagnostics = zeptoclaw::config::validate::validate_config(&raw);
if diagnostics.is_empty() {
println!(" Config is valid.");
} else {
for diag in &diagnostics {
println!(" {}", diag);
}
}
}
}
}
}
report.print_summary();
if !report.not_portable.is_empty() {
println!("For details on features that can't be migrated, see:");
println!(" https://zeptoclaw.pages.dev/docs/guides/migration/");
println!();
}
Ok(())
}