arcature-cli 2026.1.1

Developer lifecycle CLI for Arcature applications.
Documentation
use std::fs;

use crate::process::{ProcessSpec, run_capture};
use crate::project::ProjectConfig;

use super::artifact::Artifact;
use super::compare;

pub(crate) fn check(project: &ProjectConfig) -> Result<Artifact, String> {
    let backend = backend_artifact(project)?;
    let frontend = frontend_artifact(project)?;
    compare::compare(&backend, &frontend).and_then(|()| frontend_components(project))?;
    typescript_contract_current(&backend, project)?;
    Ok(backend)
}

/// Load the backend page-contract artifact — the single source of truth shared
/// by the Cross-Stack Linker and `arc exposure`. Never prints values; the
/// artifact contains only field names and types, never data.
pub(crate) fn load(project: &ProjectConfig) -> Result<Artifact, String> {
    backend_artifact(project)
}

fn frontend_components(project: &ProjectConfig) -> Result<(), String> {
    run_capture(
        &ProcessSpec::new("node", project.frontend_root()).arg("scripts/verify-contract.mjs"),
    )
    .map(|_| ())
    .map_err(|error| format!("frontend contract component check failed: {error}"))
}

fn backend_artifact(project: &ProjectConfig) -> Result<Artifact, String> {
    let output = run_capture(&ProcessSpec::new("cargo", project.root()).args([
        "run",
        "--quiet",
        "--package",
        &project.backend_package,
        "--bin",
        "arcature-contract",
    ]))
    .map_err(|error| format!("cannot inspect the registered Rust page contracts: {error}"))?;
    serde_json::from_slice(&output)
        .map_err(|error| format!("Rust page-contract binary did not emit valid JSON: {error}"))
}

fn frontend_artifact(project: &ProjectConfig) -> Result<Artifact, String> {
    let path = project.frontend_root().join("arcature.contract.json");
    let source =
        fs::read(&path).map_err(|error| format!("cannot read {}: {error}", path.display()))?;
    serde_json::from_slice(&source).map_err(|error| {
        format!(
            "invalid frontend page-contract artifact {}: {error}",
            path.display()
        )
    })
}

/// Verify the generated TypeScript contract (`src/arcature-contracts.ts`) is
/// current: the TypeScript rendered from the loaded backend artifact must match
/// the committed file. A stale or hand-edited contract is an `arc check`
/// failure. Renders TypeScript from the already-loaded JSON artifact — no
/// second `cargo run` invocation.
fn typescript_contract_current(
    backend: &super::artifact::Artifact,
    project: &ProjectConfig,
) -> Result<(), String> {
    let generated = backend.to_typescript();
    let path = project
        .frontend_root()
        .join("src")
        .join("arcature-contracts.ts");
    let committed =
        fs::read(&path).map_err(|error| format!("cannot read {}: {error}", path.display()))?;
    if generated.as_slice() != committed.as_slice() {
        return Err(format!(
            "stale TypeScript contract at {}: rerun `arc check` to regenerate, or run \
             `cargo run --bin arcature-contract -- --typescript > {}`",
            path.display(),
            path.display(),
        ));
    }
    Ok(())
}