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 doctor` — the prerequisite walkthrough, mechanized.

use crate::doctor::{self, CHROME_INSTALL};
use crate::error::CliError;
use crate::project::App;

/// Walks every prerequisite tool with install links, then — when run inside
/// an application — reports the application's own health: config validity,
/// page toolchain, page freshness.
///
/// # Errors
///
/// Returns [`CliError::DoctorProblems`] when any tool is missing or the
/// application's config is invalid, so scripts get a real exit code.
pub fn doctor() -> Result<(), CliError> {
    let mut problems = 0_usize;

    println!("toolchain:");
    for tool in doctor::ALL_TOOLS {
        match doctor::probe(tool) {
            Ok(version) => {
                println!("  {:<24} ok       {version}", label(tool));
            }
            Err(failure) => {
                problems += 1;
                println!("  {:<24} MISSING  {failure}", label(tool));
                println!("      {} — install: {}", tool.purpose, tool.install);
            }
        }
    }
    match doctor::require_chrome() {
        Ok(path) => println!(
            "  {:<24} ok       {}",
            "Chrome (browser proof)",
            path.display()
        ),
        Err(_reason) => {
            problems += 1;
            println!(
                "  {:<24} MISSING  no installed browser found",
                "Chrome (browser proof)"
            );
            println!(
                "      drives frame test's real-browser proof — install: {CHROME_INSTALL} (or set CHROME_BIN)"
            );
        }
    }

    println!();
    match App::find_from_current_dir() {
        Ok(app) => {
            println!("application at {}:", app.root.display());
            match app.load_config() {
                Ok(_config) => println!("  frame.toml               ok"),
                Err(error) => {
                    problems += 1;
                    println!("  frame.toml               INVALID  {error}");
                    let mut source = std::error::Error::source(&error);
                    while let Some(cause) = source {
                        println!("      caused by: {cause}");
                        source = cause.source();
                    }
                }
            }
            if app.page_toolchain_installed() {
                println!("  page toolchain           ok       page/node_modules present");
            } else {
                println!(
                    "  page toolchain           not installed — frame check and frame test run \
                     `npm install` for you (network, once); frame run does not need it"
                );
            }
            match app.stale_page_sources() {
                Ok(stale) if stale.is_empty() => {
                    println!("  compiled page            ok       current with page/src");
                }
                Ok(stale) => {
                    println!(
                        "  compiled page            stale    {} changed source(s) — frame run \
                         and frame build recompile it",
                        stale.len()
                    );
                }
                Err(error) => {
                    problems += 1;
                    println!("  compiled page            UNREADABLE  {error}");
                }
            }
        }
        Err(CliError::NotAnApplication { .. }) => {
            println!("no application here (no frame.toml in this directory or any parent);");
            println!("toolchain checks above are the whole report. `frame new my_app` starts one.");
        }
        Err(other) => return Err(other),
    }

    if problems == 0 {
        println!();
        println!("frame doctor: everything present");
        Ok(())
    } else {
        Err(CliError::DoctorProblems { problems })
    }
}

fn label(tool: &doctor::Tool) -> String {
    format!("{} ({})", tool.name, tool.command)
}