arcature-cli 2026.2.0

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

use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub(crate) struct Artifact {
    pub(crate) format: String,
    pub(crate) pages: BTreeMap<String, Page>,
}

impl Artifact {
    /// Render this artifact as deterministic TypeScript interface declarations.
    ///
    /// Mirrors `arcature_inertia::ContractArtifact::to_typescript` so the CLI
    /// can verify a committed TypeScript contract is current from the same JSON
    /// artifact the Cross-Stack Linker loads — no second `cargo run` invocation.
    /// Output is stable: pages and fields are emitted in `BTreeMap` order.
    pub(crate) fn to_typescript(&self) -> Vec<u8> {
        let mut out = String::new();
        out.push_str("// Arcature Rust-to-TypeScript client contract.\n");
        out.push_str("// Generated by `arcature-contract` from the registered page contracts.\n");
        out.push_str("// Do not edit by hand; run `arc check` to regenerate and verify.\n");
        for (page, schema) in &self.pages {
            out.push_str(&format!("export interface {page}Props {{\n"));
            for (field, prop) in &schema.props.fields {
                let optional = if prop.required { "" } else { "?" };
                out.push_str(&format!(
                    "  {field}{optional}: {};\n",
                    render_type(&prop.ty)
                ));
            }
            out.push_str("}\n\n");
        }
        out.into_bytes()
    }
}

fn render_type(ty: &Type) -> String {
    match ty {
        Type::Boolean => "boolean".to_owned(),
        Type::Number => "number".to_owned(),
        Type::String => "string".to_owned(),
        Type::Array { item } => format!("{}[]", render_type(item)),
        Type::Nullable { item } => format!("{} | null", render_type(item)),
        Type::Object { fields } => {
            let mut out = String::from("{ ");
            for (index, (name, prop)) in fields.iter().enumerate() {
                if index > 0 {
                    out.push_str("; ");
                }
                let optional = if prop.required { "" } else { "?" };
                out.push_str(&format!("{name}{optional}: {}", render_type(&prop.ty)));
            }
            out.push_str(" }");
            out
        }
    }
}

#[derive(Debug, Deserialize)]
pub(crate) struct Page {
    pub(crate) props: Props,
}

#[derive(Debug, Deserialize)]
pub(crate) struct Props {
    pub(crate) fields: BTreeMap<String, Prop>,
}

#[derive(Debug, Deserialize, PartialEq, Eq)]
pub(crate) struct Prop {
    pub(crate) required: bool,
    #[serde(rename = "type")]
    pub(crate) ty: Type,
}

#[derive(Debug, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub(crate) enum Type {
    Boolean,
    Number,
    String,
    Array { item: Box<Self> },
    Object { fields: BTreeMap<String, Prop> },
    Nullable { item: Box<Self> },
}