navi-core 0.3.7

Local agentic engine and terminal-first coding agent.
Documentation
//! Build script for navi-core.
//!
//! Embeds the provider registry snapshot (`registry-snapshot/`) into the
//! compiled binary so NAVI works offline and has a fallback when the SQLite
//! cache and remote fetch both fail.

use std::env;
use std::fs;
use std::path::PathBuf;

fn main() {
    let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
    let manifest_dir =
        PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));
    let snapshot_dir = manifest_dir.join("registry-snapshot");
    let providers_dir = snapshot_dir.join("providers");
    let transcription_providers_dir = snapshot_dir.join("transcription-providers");

    let embedded_dir = out_dir.join("embedded_registry");
    fs::create_dir_all(&embedded_dir).expect("failed to create embedded_registry output dir");
    let embedded_providers_dir = embedded_dir.join("providers");
    fs::create_dir_all(&embedded_providers_dir).expect("failed to create embedded providers dir");
    let embedded_transcription_dir = embedded_dir.join("transcription-providers");
    fs::create_dir_all(&embedded_transcription_dir)
        .expect("failed to create embedded transcription-providers dir");

    // Copy manifest.json
    let manifest_src = snapshot_dir.join("manifest.json");
    let manifest_dst = embedded_dir.join("manifest.json");
    fs::copy(&manifest_src, &manifest_dst).expect("failed to copy embedded manifest");
    println!("cargo:rerun-if-changed={}", manifest_src.display());

    // Collect and copy LLM provider files, sorted for deterministic output.
    let mut provider_files: Vec<_> = fs::read_dir(&providers_dir)
        .expect("failed to read registry-snapshot/providers")
        .filter_map(|e| {
            let e = e.expect("dir entry");
            let path = e.path();
            if path.extension().is_some_and(|ext| ext == "json") {
                Some(path)
            } else {
                None
            }
        })
        .collect();
    provider_files.sort();

    let mut entries = Vec::new();
    for path in &provider_files {
        let id = path
            .file_stem()
            .expect("provider file has no stem")
            .to_str()
            .expect("provider file name is not valid UTF-8");
        let dst = embedded_providers_dir.join(format!("{id}.json"));
        fs::copy(path, &dst).expect("failed to copy embedded provider");
        println!("cargo:rerun-if-changed={}", path.display());
        entries.push((
            id.to_string(),
            dst.to_str().expect("path is not valid UTF-8").to_string(),
        ));
    }

    // Collect and copy transcription / dictation provider files.
    let mut transcription_entries = Vec::new();
    if transcription_providers_dir.is_dir() {
        let mut files: Vec<_> = fs::read_dir(&transcription_providers_dir)
            .expect("failed to read registry-snapshot/transcription-providers")
            .filter_map(|e| {
                let e = e.expect("dir entry");
                let path = e.path();
                if path.extension().is_some_and(|ext| ext == "json") {
                    Some(path)
                } else {
                    None
                }
            })
            .collect();
        files.sort();
        for path in &files {
            let id = path
                .file_stem()
                .expect("transcription provider file has no stem")
                .to_str()
                .expect("transcription provider file name is not valid UTF-8");
            let dst = embedded_transcription_dir.join(format!("{id}.json"));
            fs::copy(path, &dst).expect("failed to copy embedded transcription provider");
            println!("cargo:rerun-if-changed={}", path.display());
            transcription_entries.push((
                id.to_string(),
                dst.to_str().expect("path is not valid UTF-8").to_string(),
            ));
        }
    }

    // Collect and copy canonical model catalog files.
    let models_dir = snapshot_dir.join("models");
    let mut model_catalog_entries = Vec::new();
    if models_dir.is_dir() {
        let embedded_models_dir = embedded_dir.join("models");
        fs::create_dir_all(&embedded_models_dir).expect("failed to create embedded models dir");
        let mut model_files: Vec<_> = fs::read_dir(&models_dir)
            .expect("failed to read registry-snapshot/models")
            .filter_map(|e| {
                let e = e.expect("dir entry");
                let path = e.path();
                if path.extension().is_some_and(|ext| ext == "json") {
                    Some(path)
                } else {
                    None
                }
            })
            .collect();
        model_files.sort();
        for path in &model_files {
            // Filenames must stay Windows-safe (no `:`). Model ids may contain
            // `:` (Ollama tags like `gemma3:12b`); encode as `__` on disk and
            // restore when embedding the catalog id.
            let stem = path
                .file_stem()
                .expect("model file has no stem")
                .to_str()
                .expect("model file name is not valid UTF-8");
            let id = stem.replace("__", ":");
            let safe_name = format!("{stem}.json");
            let dst = embedded_models_dir.join(&safe_name);
            fs::copy(path, &dst).expect("failed to copy embedded canonical model");
            println!("cargo:rerun-if-changed={}", path.display());
            model_catalog_entries.push((
                id,
                dst.to_str().expect("path is not valid UTF-8").to_string(),
            ));
        }
    }

    // Collect and copy provider base definitions (for `extends`).
    let bases_dir = snapshot_dir.join("bases");
    let mut base_entries = Vec::new();
    if bases_dir.is_dir() {
        let embedded_bases_dir = embedded_dir.join("bases");
        fs::create_dir_all(&embedded_bases_dir).expect("failed to create embedded bases dir");
        let mut base_files: Vec<_> = fs::read_dir(&bases_dir)
            .expect("failed to read registry-snapshot/bases")
            .filter_map(|e| {
                let e = e.expect("dir entry");
                let path = e.path();
                if path.extension().is_some_and(|ext| ext == "json") {
                    Some(path)
                } else {
                    None
                }
            })
            .collect();
        base_files.sort();
        for path in &base_files {
            let id = path
                .file_stem()
                .expect("base file has no stem")
                .to_str()
                .expect("base file name is not valid UTF-8");
            let dst = embedded_bases_dir.join(format!("{id}.json"));
            fs::copy(path, &dst).expect("failed to copy embedded base");
            println!("cargo:rerun-if-changed={}", path.display());
            base_entries.push((
                id.to_string(),
                dst.to_str().expect("path is not valid UTF-8").to_string(),
            ));
        }
    }

    let schema_src = snapshot_dir.join("schemas");
    if schema_src.is_dir() {
        let embedded_schema_dir = embedded_dir.join("schemas");
        fs::create_dir_all(&embedded_schema_dir).expect("failed to create embedded schemas dir");
        for entry in fs::read_dir(&schema_src).expect("failed to read schemas dir") {
            let entry = entry.expect("schema dir entry");
            let path = entry.path();
            if path.is_file() {
                let dst = embedded_schema_dir.join(path.file_name().unwrap());
                fs::copy(&path, &dst).expect("failed to copy schema");
                println!("cargo:rerun-if-changed={}", path.display());
            }
        }
    }

    // Generate the Rust source that includes the embedded files.
    let mut src = String::new();
    src.push_str("// Auto-generated by build.rs. Do not edit.\n");
    src.push_str("// Embeds the registry snapshot into the binary.\n\n");
    src.push_str("pub const MANIFEST_JSON: &str = include_str!(\"manifest.json\");\n\n");
    src.push_str("pub const PROVIDER_FILES: &[(&str, &str)] = &[\n");
    for (id, path) in &entries {
        src.push_str(&format!("    ({id:?}, include_str!({path:?})),\n"));
    }
    src.push_str("];\n\n");
    src.push_str("pub const TRANSCRIPTION_PROVIDER_FILES: &[(&str, &str)] = &[\n");
    for (id, path) in &transcription_entries {
        src.push_str(&format!("    ({id:?}, include_str!({path:?})),\n"));
    }
    src.push_str("];\n\n");
    src.push_str("pub const BASE_FILES: &[(&str, &str)] = &[\n");
    for (id, path) in &base_entries {
        src.push_str(&format!("    ({id:?}, include_str!({path:?})),\n"));
    }
    src.push_str("];\n\n");
    src.push_str("pub const MODEL_CATALOG_FILES: &[(&str, &str)] = &[\n");
    for (id, path) in &model_catalog_entries {
        src.push_str(&format!("    ({id:?}, include_str!({path:?})),\n"));
    }
    src.push_str("];\n");

    let out_file = embedded_dir.join("embedded.rs");
    fs::write(&out_file, src).expect("failed to write embedded.rs");
    println!("cargo:rerun-if-changed={}", out_file.display());

    // Tell cargo to look in the embedded dir for include_str! paths.
    println!(
        "cargo:rustc-env=NAVI_EMBEDDED_REGISTRY_DIR={}",
        embedded_dir.display()
    );
}