cmduse-core 0.6.7

Shared pure logic for cmduse (CLI) and the Command Code Zed extension: plan table, date/ISO helpers, window math, formatting.
Documentation
// Generates Rust constants from the canonical plans.json so the plan table,
// name rules, and monthly caps have one source across the Rust UIs and the
// TypeScript opencode plugin (which imports the same core/plans.json).
use std::env;
use std::fs;
use std::path::Path;

fn main() {
    let json_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("plans.json");
    println!("cargo:rerun-if-changed={}", json_path.display());
    let src = fs::read_to_string(&json_path).expect("read plans.json");
    let v: serde_json::Value = serde_json::from_str(&src).expect("parse plans.json");

    let rules = v["nameRules"].as_array().expect("nameRules array");
    let caps = v["caps"].as_object().expect("caps object");
    let plans = v["plans"].as_array().expect("plans array");

    let mut out = String::from("// @generated by build.rs from plans.json — do not edit.\n");
    out.push_str("pub static NAME_RULES: &[(&[&str], &str)] = &[\n");
    for r in rules {
        let needles: Vec<String> = r["needles"]
            .as_array()
            .expect("needles array")
            .iter()
            .map(|n| format!("\"{}\"", n.as_str().expect("needle string")))
            .collect();
        out.push_str(&format!(
            "    (&[{}], \"{}\"),\n",
            needles.join(", "),
            r["name"].as_str().expect("rule name")
        ));
    }
    out.push_str("];\n");
    out.push_str(&format!(
        "pub const DEFAULT_NAME: &str = \"{}\";\n",
        v["defaultName"].as_str().expect("defaultName")
    ));

    out.push_str("pub static CAPS: &[(&str, Option<f64>)] = &[\n");
    for (name, cap) in caps {
        let value = if cap.is_null() {
            "None".to_string()
        } else {
            format!("Some({:.1})", cap.as_f64().expect("numeric cap"))
        };
        out.push_str(&format!("    (\"{name}\", {value}),\n"));
    }
    out.push_str("];\n");

    out.push_str("pub static PLANS: &[(&str, &str, &str, &str, &str)] = &[\n");
    for p in plans {
        out.push_str(&format!(
            "    (\"{}\", \"{}\", \"{}\", \"{}\", \"{}\"),\n",
            p["name"].as_str().expect("plan name"),
            p["price"].as_str().expect("plan price"),
            p["monthly"].as_str().expect("plan monthly"),
            p["fiveHour"].as_str().expect("plan fiveHour"),
            p["weekly"].as_str().expect("plan weekly"),
        ));
    }
    out.push_str("];\n");

    // ---- gating.json (shared with the opencode plugin) ----
    let gating_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("gating.json");
    println!("cargo:rerun-if-changed={}", gating_path.display());
    let gsrc = fs::read_to_string(&gating_path).expect("read gating.json");
    let g: serde_json::Value = serde_json::from_str(&gsrc).expect("parse gating.json");

    fn str_slice(a: &serde_json::Value) -> String {
        let items: Vec<String> = a
            .as_array()
            .expect("array")
            .iter()
            .map(|s| format!("\"{}\"", s.as_str().expect("string")))
            .collect();
        format!("&[{}]", items.join(", "))
    }

    out.push_str("pub static GATE_CATEGORIES: &[(&str, &str)] = &[\n");
    for (model, cat) in g["categories"].as_object().expect("categories") {
        out.push_str(&format!(
            "    (\"{model}\", \"{}\"),\n",
            cat.as_str().expect("category")
        ));
    }
    out.push_str("];\n");

    // (plan, allowedCategories, blockedModels)
    out.push_str("pub static GATE_PLANS: &[(&str, &[&str], &[&str])] = &[\n");
    for (plan, rule) in g["plans"].as_object().expect("plans") {
        out.push_str(&format!(
            "    (\"{plan}\", {}, {}),\n",
            str_slice(&rule["allowedCategories"]),
            str_slice(&rule["blockedModels"]),
        ));
    }
    out.push_str("];\n");

    out.push_str("pub static GATE_HARD_BLOCKED: &[(&str, &[&str])] = &[\n");
    for (plan, list) in g["hardBlocked"].as_object().expect("hardBlocked") {
        out.push_str(&format!("    (\"{plan}\", {}),\n", str_slice(list)));
    }
    out.push_str("];\n");

    out.push_str("pub static GATE_KNOWN: &[&str] = ");
    out.push_str(&str_slice(&g["knownModels"]));
    out.push_str(";\n");

    out.push_str(&format!(
        "pub const GATE_EXTRACTED_AT: &str = \"{}\";\n",
        g["extractedAt"].as_str().unwrap_or("")
    ));
    out.push_str(&format!(
        "pub const GATE_CLI_VERSION: &str = \"{}\";\n",
        g["cliVersion"].as_str().unwrap_or("")
    ));

    out.push_str("pub static GATE_ALIASES: &[(&str, &str)] = &[\n");
    for (from, to) in g["aliases"].as_object().expect("aliases") {
        out.push_str(&format!(
            "    (\"{from}\", \"{}\"),\n",
            to.as_str().expect("alias target")
        ));
    }
    out.push_str("];\n");

    let dest = Path::new(&env::var("OUT_DIR").expect("OUT_DIR")).join("plans.rs");
    fs::write(dest, out).expect("write plans.rs");
}