Skip to main content

kaizen/core/
migrate_home.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! One-shot, idempotent migration: `<workspace>/.kaizen/` → `~/.kaizen/projects/<slug>/`.
3
4use anyhow::Result;
5use std::path::Path;
6
7pub enum MigrationOutcome {
8    Skipped,
9    AlreadyMigrated,
10    Conflict,
11    Migrated,
12}
13
14/// Moves `workspace/.kaizen/` → `target/` (one-shot, idempotent).
15///
16/// - Old absent → `Skipped`
17/// - Old has only `MIGRATED.txt` → `AlreadyMigrated`
18/// - Both non-empty → `Conflict` (warn, don't merge)
19/// - Otherwise: move all entries, write `MIGRATED.txt`, return `Migrated`
20pub fn migrate_legacy_in_repo(workspace: &Path, target: &Path) -> Result<MigrationOutcome> {
21    let old = workspace.join(".kaizen");
22    if !old.exists() {
23        return Ok(MigrationOutcome::Skipped);
24    }
25    let entries: Vec<_> = std::fs::read_dir(&old)?.filter_map(|e| e.ok()).collect();
26    if entries.len() == 1 && entries[0].file_name() == "MIGRATED.txt" {
27        return Ok(MigrationOutcome::AlreadyMigrated);
28    }
29    if target.exists() {
30        let n = std::fs::read_dir(target)?.filter_map(|e| e.ok()).count();
31        if n > 0 {
32            return Ok(MigrationOutcome::Conflict);
33        }
34    }
35    std::fs::create_dir_all(target)?;
36    for entry in &entries {
37        if entry.file_name() == "MIGRATED.txt" {
38            continue;
39        }
40        let src = entry.path();
41        let dst = target.join(entry.file_name());
42        if std::fs::rename(&src, &dst).is_err() {
43            copy_recursive(&src, &dst)?;
44            std::fs::remove_dir_all(&src).or_else(|_| std::fs::remove_file(&src))?;
45        }
46    }
47    write_marker(&old, target)?;
48    Ok(MigrationOutcome::Migrated)
49}
50
51fn write_marker(old: &Path, target: &Path) -> Result<()> {
52    let secs = std::time::SystemTime::now()
53        .duration_since(std::time::UNIX_EPOCH)
54        .map(|d| d.as_secs())
55        .unwrap_or(0);
56    std::fs::write(
57        old.join("MIGRATED.txt"),
58        format!(
59            "migrated to: {}\nat: {secs} (unix secs)\nsafe to delete this folder\n",
60            target.display()
61        ),
62    )?;
63    Ok(())
64}
65
66fn copy_recursive(src: &Path, dst: &Path) -> Result<()> {
67    if src.is_dir() {
68        std::fs::create_dir_all(dst)?;
69        for entry in std::fs::read_dir(src)?.filter_map(|e| e.ok()) {
70            copy_recursive(&entry.path(), &dst.join(entry.file_name()))?;
71        }
72    } else {
73        std::fs::copy(src, dst)?;
74    }
75    Ok(())
76}