frame-cli 0.3.0

CLI for Frame — five intention-verbs over one application: frame new scaffolds it, frame run serves it, frame test proves it (real browser included), frame check verifies it statically, frame doctor walks the prerequisites
Documentation
//! `frame check` — fast static verification: frame.toml, page types,
//! component types, host types. Seconds, no build artifacts.

use crate::coherence::coherence_violations;
use crate::doctor;
use crate::error::CliError;
use crate::project::{App, ensure_page_toolchain, observe_step};

/// Runs every static check and prints one verdict: frame.toml validation
/// (through the host's own loader), `tsc --noEmit`, `gleam check`, and
/// `cargo check`.
///
/// # Errors
///
/// Refuses on missing prerequisites; returns a failed verdict when any
/// check fails.
pub fn check() -> Result<(), CliError> {
    let app = App::find_from_current_dir()?;
    doctor::require(doctor::CHECK_PREREQUISITES, "frame check")?;
    ensure_page_toolchain(&app)?;

    let mut failed: Vec<String> = Vec::new();

    match app.load_config() {
        Ok(config) => {
            // Static port coherence only bites a fully-stated topology; a
            // portless config is coherent by construction (its ports resolve
            // at boot) and reports no violations.
            let violations = coherence_violations(&config);
            if violations.is_empty() {
                println!("frame.toml   OK");
            } else {
                eprintln!("frame.toml   FAIL: port coherence");
                for violation in &violations {
                    eprintln!("  {violation}");
                }
                failed.push("frame.toml".to_owned());
            }
        }
        Err(error) => {
            report_config_failure(&error);
            failed.push("frame.toml".to_owned());
        }
    }

    let steps: [(&str, &str, &[&str], std::path::PathBuf); 3] = [
        ("page types", "npm", &["run", "typecheck"], app.page_dir()),
        ("component", "gleam", &["check"], app.component_dir()),
        ("host", "cargo", &["check", "--workspace"], app.root.clone()),
    ];
    for (label, program, args, directory) in steps {
        if observe_step(program, args, &directory)? {
            println!("{label:<12} OK");
        } else {
            println!("{label:<12} FAIL");
            failed.push(label.to_owned());
        }
    }

    if failed.is_empty() {
        println!("frame check: PASS");
        Ok(())
    } else {
        Err(CliError::VerdictFailed {
            verb: "frame check",
            failed,
        })
    }
}

/// Prints the config failure with its full cause chain — the loader's own
/// message names the offending key and the fix.
fn report_config_failure(error: &CliError) {
    eprintln!("frame.toml   FAIL: {error}");
    let mut source = std::error::Error::source(error);
    while let Some(cause) = source {
        eprintln!("  caused by: {cause}");
        source = cause.source();
    }
}