omne-cli 0.2.0

CLI for managing omne volumes: init, upgrade, and validate kernel and distro releases
Documentation
//! Volume directory scaffolding.
//!
//! Creates the v2 `.omne/` directory structure — `lib/` for static
//! config + docs, `var/` for runtime state, `wt/` for per-run
//! worktrees — writes the `CLAUDE.md` bootloader with its single-hop
//! `@.omne/dist/AGENTS.md` import, seeds the docs baseline, and
//! appends `.gitignore` entries for the runtime paths.
//!
//! Idempotency contract:
//! - `create_volume_dirs` uses `create_dir_all` (idempotent).
//! - `write_docs_baseline` skips `index.md` and any existing
//!   `.gitkeep`.
//! - `write_gitignore` appends only missing entries under an `# omne`
//!   marker.
//! - `write_omne_readme` and `write_bootloader` **overwrite
//!   unconditionally**: re-running `init` always re-stamps these
//!   files. Volume metadata in `omne.md` (versions, sources) is
//!   re-derived from the freshly-extracted tarballs, so the prior
//!   content is by definition stale.

// Items below are first used when Unit 8a wires this module into
// `init::run`. Until then, only the inline test module uses them.
#![allow(dead_code)]

use std::io;
use std::path::Path;

use crate::volume;

/// Bootloader written to `CLAUDE.md` at the volume root.
///
/// Uses Claude Code `@path` import syntax so `dist/AGENTS.md` (and the
/// distro's transitive imports) auto-load into context. Exactly one
/// `@import` hop — the v2 design doc collapsed the pre-v1 chain
/// (`CLAUDE.md → @MANIFEST.md → @SYSTEM.md → ...`) into a single
/// reference so agents see a predictable boot surface.
const BOOTLOADER_CONTENT: &str = "\
# CLAUDE.md

> Bootloader — loads the distro's agent briefing.

@.omne/dist/AGENTS.md
";

/// Baseline content for `.omne/lib/docs/index.md`. Stamped only when
/// the file is absent so a user's hand-authored index is preserved
/// across re-inits.
const DOCS_INDEX_BASELINE: &str = "\
# Volume docs

Index for this volume's knowledge base. Populate `raw/` with verbatim
source material, `inter/` with intermediate synthesis, and `wiki/`
with curated long-form notes.
";

/// `.gitignore` fragment covering the v2 runtime paths. Embedded at
/// compile time from `templates/gitignore-template` so the source of
/// truth lives beside the rest of the shipped templates.
const GITIGNORE_TEMPLATE: &str = include_str!("../templates/gitignore-template");

/// Create the v2 `.omne/` directory skeleton under `root`.
///
/// Creates `.omne/{lib/cfg, lib/docs/{raw, inter, wiki}, var, wt}`.
/// `.omne/core/` and `.omne/dist/` are populated by tarball extraction
/// downstream and are not created here.
pub fn create_volume_dirs(root: &Path) -> io::Result<()> {
    std::fs::create_dir_all(volume::cfg_dir(root))?;
    let docs = volume::docs_dir(root);
    std::fs::create_dir_all(docs.join("raw"))?;
    std::fs::create_dir_all(docs.join("inter"))?;
    std::fs::create_dir_all(docs.join("wiki"))?;
    std::fs::create_dir_all(volume::var_dir(root))?;
    std::fs::create_dir_all(volume::wt_dir(root))?;
    Ok(())
}

/// Seed the docs baseline: `index.md` + `.gitkeep` files in each
/// bucket. Idempotent — a pre-existing `index.md` is preserved and
/// existing `.gitkeep` files are left alone.
pub fn write_docs_baseline(root: &Path) -> io::Result<()> {
    let index = volume::docs_baseline(root);
    if !index.exists() {
        std::fs::write(&index, DOCS_INDEX_BASELINE)?;
    }
    for subdir in ["raw", "inter", "wiki"] {
        let keep = volume::docs_dir(root).join(subdir).join(".gitkeep");
        if !keep.exists() {
            std::fs::write(&keep, "")?;
        }
    }
    Ok(())
}

/// Write the `CLAUDE.md` bootloader to the volume root.
pub fn write_bootloader(root: &Path) -> io::Result<()> {
    std::fs::write(root.join("CLAUDE.md"), BOOTLOADER_CONTENT)
}

/// Write stamped README content to `.omne/omne.md`.
///
/// Per the v2 design, `omne.md` is a **non-loaded** README — the boot
/// chain skips it and goes straight from `CLAUDE.md` to
/// `@.omne/dist/AGENTS.md`. `omne upgrade` and `omne validate` still
/// read this file for identity metadata (volume, distro versions,
/// kernel-source, distro-source frontmatter).
pub fn write_omne_readme(root: &Path, content: &str) -> io::Result<()> {
    std::fs::write(root.join(".omne").join("omne.md"), content)
}

/// Ensure the volume's `.gitignore` carries the runtime entries.
///
/// Writes the template verbatim when `.gitignore` is absent; when it
/// exists, appends any missing non-comment lines under an `# omne`
/// section header. Comments in the template are only emitted when a
/// fresh write occurs so repeat invocations don't accrete marker
/// blocks.
pub fn write_gitignore(root: &Path) -> io::Result<()> {
    let path = root.join(".gitignore");
    if !path.exists() {
        return std::fs::write(&path, GITIGNORE_TEMPLATE);
    }

    let existing = std::fs::read_to_string(&path)?;
    let required: Vec<&str> = GITIGNORE_TEMPLATE
        .lines()
        .map(str::trim)
        .filter(|l| !l.is_empty() && !l.starts_with('#'))
        .collect();
    let missing: Vec<&str> = required
        .into_iter()
        .filter(|needle| !existing.lines().any(|l| l.trim() == *needle))
        .collect();
    if missing.is_empty() {
        return Ok(());
    }

    let mut out = existing;
    if !out.ends_with('\n') {
        out.push('\n');
    }
    if !out.contains("# omne") {
        out.push_str("\n# omne\n");
    }
    for line in missing {
        out.push_str(line);
        out.push('\n');
    }
    std::fs::write(&path, out)
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;

    // -- create_volume_dirs --

    #[test]
    fn creates_lib_and_runtime_dirs() {
        let tmp = TempDir::new().unwrap();
        create_volume_dirs(tmp.path()).unwrap();
        let omne = tmp.path().join(".omne");
        assert!(omne.join("lib").join("cfg").is_dir());
        assert!(omne.join("lib").join("docs").join("raw").is_dir());
        assert!(omne.join("lib").join("docs").join("inter").is_dir());
        assert!(omne.join("lib").join("docs").join("wiki").is_dir());
        assert!(omne.join("var").is_dir());
        assert!(omne.join("wt").is_dir());
    }

    #[test]
    fn does_not_create_legacy_cfg_or_log_at_root() {
        let tmp = TempDir::new().unwrap();
        create_volume_dirs(tmp.path()).unwrap();
        assert!(!tmp.path().join(".omne/cfg").exists());
        assert!(!tmp.path().join(".omne/log").exists());
    }

    #[test]
    fn idempotent() {
        let tmp = TempDir::new().unwrap();
        create_volume_dirs(tmp.path()).unwrap();
        create_volume_dirs(tmp.path()).unwrap();
        assert!(tmp.path().join(".omne/var").is_dir());
    }

    // -- write_docs_baseline --

    #[test]
    fn stamps_index_and_gitkeeps() {
        let tmp = TempDir::new().unwrap();
        create_volume_dirs(tmp.path()).unwrap();
        write_docs_baseline(tmp.path()).unwrap();
        let docs = tmp.path().join(".omne/lib/docs");
        assert!(docs.join("index.md").is_file());
        assert!(docs.join("raw/.gitkeep").is_file());
        assert!(docs.join("inter/.gitkeep").is_file());
        assert!(docs.join("wiki/.gitkeep").is_file());
    }

    #[test]
    fn docs_baseline_preserves_existing_index() {
        let tmp = TempDir::new().unwrap();
        create_volume_dirs(tmp.path()).unwrap();
        let index = tmp.path().join(".omne/lib/docs/index.md");
        std::fs::write(&index, "user authored\n").unwrap();
        write_docs_baseline(tmp.path()).unwrap();
        assert_eq!(std::fs::read_to_string(&index).unwrap(), "user authored\n");
    }

    // -- write_bootloader --

    #[test]
    fn bootloader_imports_dist_agents() {
        let tmp = TempDir::new().unwrap();
        write_bootloader(tmp.path()).unwrap();
        let content = std::fs::read_to_string(tmp.path().join("CLAUDE.md")).unwrap();
        assert!(
            content.contains("@.omne/dist/AGENTS.md"),
            "bootloader must import dist/AGENTS.md: {content}"
        );
        assert!(
            !content.contains("MANIFEST.md"),
            "bootloader must not retain legacy MANIFEST.md hop: {content}"
        );
    }

    // -- write_omne_readme --

    #[test]
    fn writes_omne_readme_file() {
        let tmp = TempDir::new().unwrap();
        std::fs::create_dir_all(tmp.path().join(".omne")).unwrap();
        write_omne_readme(tmp.path(), "test content").unwrap();
        let path = tmp.path().join(".omne").join("omne.md");
        assert!(path.is_file());
        assert_eq!(std::fs::read_to_string(path).unwrap(), "test content");
    }

    // -- write_gitignore --

    #[test]
    fn writes_fresh_gitignore_when_absent() {
        let tmp = TempDir::new().unwrap();
        write_gitignore(tmp.path()).unwrap();
        let content = std::fs::read_to_string(tmp.path().join(".gitignore")).unwrap();
        assert!(content.contains(".omne/wt/"));
        assert!(content.contains(".omne/var/runs/*/nodes/"));
        assert!(content.contains(".omne/var/.ulid-last"));
    }

    #[test]
    fn appends_missing_entries_to_existing_gitignore() {
        let tmp = TempDir::new().unwrap();
        let path = tmp.path().join(".gitignore");
        std::fs::write(&path, "target/\nnode_modules/\n").unwrap();
        write_gitignore(tmp.path()).unwrap();
        let content = std::fs::read_to_string(&path).unwrap();
        assert!(content.contains("target/"), "existing entries preserved");
        assert!(content.contains(".omne/wt/"), "new entries appended");
        assert!(content.contains("# omne"), "section marker added");
    }

    #[test]
    fn gitignore_is_idempotent() {
        let tmp = TempDir::new().unwrap();
        write_gitignore(tmp.path()).unwrap();
        let first = std::fs::read_to_string(tmp.path().join(".gitignore")).unwrap();
        write_gitignore(tmp.path()).unwrap();
        let second = std::fs::read_to_string(tmp.path().join(".gitignore")).unwrap();
        assert_eq!(first, second, "repeat invocations must not mutate");
    }
}