leviath-cli 0.3.8

Command-line interface for Leviath agent framework
Documentation
//! Build script for the `lev` CLI. Two jobs:
//!
//! 1. Embed a build identifier (`LEVIATH_BUILD`) so a running daemon can tell
//!    whether the installed CLI is a newer build than itself and restart
//!    cleanly.
//! 2. Embed the workspace's `agents/` blueprints, so a released binary can
//!    install them without a git checkout.
//!
//! The build id is the short git commit hash. When the working tree is dirty it
//! also carries a short hash of the uncommitted changes, so **every edit
//! produces a distinct id** - that's what lets a dev-iteration reinstall (same
//! commit, changed code) be detected as stale and reload the daemon. Falls back
//! to the package version when git is unavailable (e.g. a packaged crate).
//!
//! The script re-runs on every build (via a sentinel `rerun-if-changed` path
//! that never exists) so the dirty hash tracks source edits, not just commits;
//! when the computed id is unchanged this is free (Cargo won't recompile). That
//! same always-rerun also keeps the embedded blueprints in step with the files
//! on disk without a second watch list.

use std::collections::hash_map::DefaultHasher;
use std::fmt::Write as _;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::process::Command;

/// Run a git command and return its stdout bytes, or `None` on any failure.
fn git(args: &[&str]) -> Option<Vec<u8>> {
    Command::new("git")
        .args(args)
        .output()
        .ok()
        .filter(|o| o.status.success())
        .map(|o| o.stdout)
}

/// A short hex hash of the working tree's uncommitted state (the tracked diff
/// plus the porcelain status, which surfaces new/removed files), so the id moves
/// whenever the code does. `None` when the tree is clean.
fn dirty_hash() -> Option<String> {
    let status = git(&["status", "--porcelain"])?;
    if status.is_empty() {
        return None; // clean tree
    }
    let diff = git(&["diff", "HEAD"]).unwrap_or_default();
    let mut hasher = DefaultHasher::new();
    status.hash(&mut hasher);
    diff.hash(&mut hasher);
    Some(format!("{:08x}", hasher.finish() & 0xffff_ffff))
}

// ─── Bundled agent blueprints ────────────────────────────────────────────────

/// Every file under one agent directory, sorted, as `(relative path, absolute
/// path)`. Relative paths use `/` on every platform because they are written
/// verbatim into the generated source and later joined onto an install dir.
fn agent_files(dir: &Path) -> Vec<(String, PathBuf)> {
    let mut out = Vec::new();
    collect_files(dir, dir, &mut out);
    out.sort_by(|a, b| a.0.cmp(&b.0));
    out
}

fn collect_files(root: &Path, dir: &Path, out: &mut Vec<(String, PathBuf)>) {
    let Ok(entries) = std::fs::read_dir(dir) else {
        return;
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            collect_files(root, &path, out);
        } else if let Ok(rel) = path.strip_prefix(root) {
            let rel = rel
                .components()
                .map(|c| c.as_os_str().to_string_lossy().into_owned())
                .collect::<Vec<_>>()
                .join("/");
            out.push((rel, path));
        }
    }
}

/// Read `version = "..."` out of a blueprint manifest.
///
/// A deliberately dumb line scan rather than a TOML parse: the build script has
/// no dependencies, and `version` is a top-level key on the first few lines of
/// every manifest. A blueprint whose version can't be read is a build failure,
/// not a silent empty string - the wizard drives install/update decisions off
/// this value, so an unreadable one would quietly present as "no update".
fn manifest_version(manifest: &str, path: &Path) -> String {
    manifest
        .lines()
        .find_map(|line| {
            let line = line.trim();
            let rest = line.strip_prefix("version")?.trim_start();
            let rest = rest.strip_prefix('=')?.trim();
            rest.strip_prefix('"')?.split('"').next().map(str::to_owned)
        })
        .unwrap_or_else(|| {
            panic!(
                "no top-level `version = \"...\"` in {} -- every bundled \
                 blueprint needs one for `lev setup` to plan installs",
                path.display()
            )
        })
}

/// Generate `bundled_agents.rs`: a `BUNDLED_AGENTS` table whose file contents
/// are `include_str!`s of the real files, so the blueprints ride along in the
/// binary and a released `lev` can install them with no git checkout.
///
/// A missing `agents/` directory yields an empty table rather than a build
/// error, so an exotic build without the directory still compiles and
/// `lev setup` degrades to "nothing to install". The directory lives inside
/// this crate, so both git checkouts and the published crates.io package
/// normally have it.
fn write_bundled_agents(out_dir: &Path, agents_dir: &Path) {
    let mut src = String::from(
        "// @generated by build.rs -- do not edit.\n\
         /// One blueprint embedded in the binary.\n\
         #[derive(Debug)]\n\
         pub struct BundledAgent {\n\
         \x20   /// Blueprint name, from the manifest's `[agent] name`.\n\
         \x20   pub name: &'static str,\n\
         \x20   /// Manifest `version`, used to plan install vs. update.\n\
         \x20   pub version: &'static str,\n\
         \x20   /// `(path relative to the agent dir, file contents)`, sorted.\n\
         \x20   pub files: &'static [(&'static str, &'static str)],\n\
         }\n\n\
         /// Every blueprint shipped with this binary, sorted by name.\n\
         pub static BUNDLED_AGENTS: &[BundledAgent] = &[\n",
    );

    let mut dirs: Vec<PathBuf> = std::fs::read_dir(agents_dir)
        .into_iter()
        .flatten()
        .flatten()
        .map(|e| e.path())
        .filter(|p| p.is_dir())
        .collect();
    dirs.sort();

    for dir in dirs {
        let manifest_path = dir.join("agent.leviath");
        let Ok(manifest) = std::fs::read_to_string(&manifest_path) else {
            // A directory under `agents/` with no manifest isn't a blueprint.
            continue;
        };
        let name = dir
            .file_name()
            .and_then(|n| n.to_str())
            .expect("agent directory names are valid UTF-8")
            .to_string();
        let version = manifest_version(&manifest, &manifest_path);

        writeln!(
            src,
            "    BundledAgent {{\n        name: {name:?},\n        version: {version:?},\n        files: &[",
        )
        .expect("writing to a String cannot fail");
        for (rel, abs) in agent_files(&dir) {
            writeln!(
                src,
                "            ({rel:?}, include_str!({:?})),",
                abs.display().to_string()
            )
            .expect("writing to a String cannot fail");
        }
        src.push_str("        ],\n    },\n");
    }

    src.push_str("];\n");
    std::fs::write(out_dir.join("bundled_agents.rs"), src)
        .expect("failed to write the generated bundled_agents.rs");
}

fn main() {
    let hash = git(&["rev-parse", "--short", "HEAD"])
        .and_then(|o| String::from_utf8(o).ok())
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty());

    let build = match hash {
        Some(hash) => match dirty_hash() {
            Some(dirty) => format!("{hash}-dirty-{dirty}"),
            None => hash,
        },
        None => format!("v{}", env!("CARGO_PKG_VERSION")),
    };

    println!("cargo:rustc-env=LEVIATH_BUILD={build}");

    // `agents/` lives inside this crate so it ships in the published package
    // and a `cargo install leviath-cli` binary carries the same blueprints as
    // a release build.
    let manifest_dir =
        PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("cargo sets CARGO_MANIFEST_DIR"));
    let agents_dir = manifest_dir.join("agents");
    let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("cargo sets OUT_DIR"));
    write_bundled_agents(&out_dir, &agents_dir);

    // Re-run on every build so the dirty hash reflects the current source, not
    // just the last commit, and the embedded blueprints track the files on
    // disk. A path that never exists is always "changed", which forces the
    // re-run; when nothing it emits has changed Cargo skips recompiling.
    println!("cargo:rerun-if-changed=.leviath-build-always-rerun");
}