codexa 0.0.3

A Git-native content compiler for building typed public and private knowledge artifacts.
Documentation
use std::{fs, path::PathBuf, process::ExitCode};

use codexa::compiler::{SourceRoot, bundle, compile_roots};

fn main() -> ExitCode {
    match run(std::env::args().skip(1).collect()) {
        Ok(()) => ExitCode::SUCCESS,
        Err(error) => {
            eprintln!("error: {error}");
            ExitCode::from(1)
        }
    }
}

fn run(args: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
    if args.is_empty() {
        print_help();
        return Ok(());
    }
    if matches!(args.as_slice(), [flag] if flag == "--version" || flag == "-V") {
        println!("codexa {}", codexa::VERSION);
        return Ok(());
    }
    if matches!(args.as_slice(), [flag] if flag == "--help" || flag == "-h") {
        print_help();
        return Ok(());
    }

    match args[0].as_str() {
        "validate" => validate_command(&args[1..]),
        "build" => build_command(&args[1..]),
        _ => Err("invalid command".into()),
    }
}

fn validate_command(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
    let (roots, _) = parse_roots_and_output(args, false)?;
    let documents = compile_roots(&roots)?;
    println!(
        "Validated {} documents across {} repositories.",
        documents.len(),
        roots.len()
    );
    Ok(())
}

fn build_command(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
    let (roots, output) = parse_roots_and_output(args, true)?;
    let output = output.ok_or("--output is required")?;
    let documents = compile_roots(&roots)?;
    fs::create_dir_all(&output)?;

    write_json(&output.join("bundle.json"), &bundle(&documents))?;
    codexa::adapter::notion::write_bundle(&documents, &output.join("notion"))?;
    codexa::adapter::web::write_bundle(&documents, &output.join("web"))?;

    println!(
        "Built {} documents from {} repositories.",
        documents.len(),
        roots.len()
    );
    println!("Output: {}", output.display());
    Ok(())
}

fn parse_roots_and_output(
    args: &[String],
    require_output: bool,
) -> Result<(Vec<SourceRoot>, Option<PathBuf>), Box<dyn std::error::Error>> {
    let mut roots = Vec::new();
    let mut output = None;
    let mut index = 0;

    while index < args.len() {
        match args[index].as_str() {
            "--source-root" => {
                let path = args.get(index + 1).ok_or("--source-root requires PATH")?;
                let repository = args
                    .get(index + 2)
                    .ok_or("--source-root requires REPOSITORY after PATH")?;
                roots.push(SourceRoot {
                    path: PathBuf::from(path),
                    repository: repository.clone(),
                });
                index += 3;
            }
            "--output" => {
                let path = args.get(index + 1).ok_or("--output requires DIR")?;
                output = Some(PathBuf::from(path));
                index += 2;
            }
            other => return Err(format!("unexpected argument `{other}`").into()),
        }
    }

    if roots.is_empty() {
        return Err("at least one --source-root PATH REPOSITORY is required".into());
    }
    if require_output && output.is_none() {
        return Err("--output is required".into());
    }
    Ok((roots, output))
}

fn write_json<T: serde::Serialize>(
    path: &std::path::Path,
    value: &T,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut bytes = serde_json::to_vec_pretty(value)?;
    bytes.push(b'\n');
    fs::write(path, bytes)?;
    Ok(())
}

fn print_help() {
    println!(
        "Codexa {}\n\nGit-native multi-endpoint content compiler.\n\nUSAGE:\n    codexa validate --source-root <PATH> <OWNER/REPO> [--source-root ...]\n    codexa build --source-root <PATH> <OWNER/REPO> [--source-root ...] --output <DIR>\n\nSOURCE DOCUMENTS:\n    Markdown with schema: codexa.document@2\n",
        codexa::VERSION
    );
}