kaizen-cli 0.1.45

Distributable agent observability: real-time-tailable sessions, agile-style retros, and repo-level improvement (Cursor, Claude Code, Codex). SQLite, redact before any sync you enable.
Documentation
use anyhow::Result;
use rusqlite::{Connection, params};
use std::path::{Path, PathBuf};

use crate::core::paths::{canonical, kaizen_dir};

use super::{name_for_path, now_ms, sql};

const LEGACY_WORKSPACES_JSON: &str = "workspaces.json";

pub(super) fn migrate(conn: &Connection) -> Result<()> {
    let Some((home, legacy)) = legacy_paths() else {
        return Ok(());
    };
    let seen_at = now_ms();
    read_rows(&legacy)
        .into_iter()
        .filter(|path| path.exists())
        .for_each(|path| upsert(conn, &path, seen_at));
    archive(&home, &legacy);
    Ok(())
}

pub(super) fn read_paths() -> Vec<PathBuf> {
    let Some((_, legacy)) = legacy_paths() else {
        return Vec::new();
    };
    read_rows(&legacy)
        .into_iter()
        .filter(|path| path.exists())
        .map(|path| canonical(&path))
        .collect()
}

fn legacy_paths() -> Option<(PathBuf, PathBuf)> {
    let home = kaizen_dir()?;
    let legacy = home.join(LEGACY_WORKSPACES_JSON);
    legacy.exists().then_some((home, legacy))
}

fn read_rows(path: &Path) -> Vec<PathBuf> {
    let text = std::fs::read_to_string(path).unwrap_or_default();
    serde_json::from_str::<Vec<String>>(&text)
        .unwrap_or_default()
        .into_iter()
        .map(PathBuf::from)
        .collect()
}

fn upsert(conn: &Connection, path: &Path, seen_at: i64) {
    let canonical = canonical(path);
    let name = name_for_path(&canonical);
    let path = canonical.to_string_lossy();
    let values = params![path.as_ref(), &name, seen_at, seen_at];
    let _ = conn.execute(sql::IMPORT_LEGACY, values);
}

fn archive(home: &Path, legacy: &Path) {
    let migrated = home.join("workspaces.json.migrated");
    if std::fs::rename(legacy, migrated).is_err() {
        let _ = std::fs::remove_file(legacy);
    }
}