frame-cli 0.4.0

CLI for Frame — six intention-verbs over one application: frame new scaffolds it, frame run serves it, frame dev hot-reloads it against the running node, frame test proves it (real browser included), frame check verifies it statically, frame doctor walks the prerequisites
Documentation
//! Typed failures for every `frame` verb — each one loud, each one carrying
//! its fix.

use std::io;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::time::Duration;

use thiserror::Error;

use crate::doctor::MissingTools;
use crate::scaffold::NewError;

/// Typed refusal or failure from any `frame` verb.
#[derive(Debug, Error)]
pub enum CliError {
    /// The dev loop refused to start or died with a typed report (F-7b).
    #[error("frame dev: {detail}")]
    Dev {
        /// The typed failure, rendered.
        detail: String,
    },

    /// Scaffold generation refused or failed.
    #[error(transparent)]
    New(#[from] NewError),

    /// Initialising the new application's git repository failed (a git command
    /// that ran refused — never a missing-git or already-in-a-repo condition,
    /// which `frame new` reports and continues past).
    #[error(transparent)]
    Git(#[from] crate::scaffold::GitError),

    /// A verb's prerequisite tools are missing (with install links).
    #[error(transparent)]
    MissingTools(#[from] MissingTools),

    /// The real-browser proof has no browser to drive.
    #[error("{reason}")]
    MissingChrome {
        /// The refusal, naming the probed path and the install link.
        reason: String,
    },

    /// The verb was run outside a Frame application.
    #[error(
        "not inside a Frame application: no frame.toml in `{start}` or any parent directory; \
         run this inside an application created by `frame new` (or create one: `frame new my_app`)"
    )]
    NotAnApplication {
        /// Directory the search started from.
        start: PathBuf,
    },

    /// The application's `frame.toml` failed to load or validate.
    #[error("`{path}` is not a valid frame.toml")]
    Config {
        /// The config file that failed.
        path: PathBuf,
        /// The host's own typed load failure (boxed: the host error is
        /// large, and `Result` should stay lean on the Ok path).
        #[source]
        source: Box<frame_host::HostError>,
    },

    /// `frame host` failed to boot or serve.
    #[error("frame host failed")]
    Host {
        /// The host's own typed failure (boxed as above).
        #[source]
        source: Box<frame_host::HostError>,
    },

    /// A build or verification command exited unsuccessfully.
    #[error("`{command}` failed with {status}; its output above names the failure")]
    CommandFailed {
        /// The command line that failed.
        command: String,
        /// Its exit status.
        status: String,
    },

    /// A command could not be spawned at all (present prerequisites were
    /// already checked, so this is an unexpected process failure, not a
    /// missing tool).
    #[error("`{command}` could not be started: {source}")]
    CommandSpawn {
        /// The command line that failed to start.
        command: String,
        /// The underlying process error.
        #[source]
        source: io::Error,
    },

    /// Page sources changed but the toolchain that recompiles them is not
    /// installed, and this verb never installs anything.
    #[error(
        "{changed} page source file(s) changed since the page was last compiled, and the page \
         toolchain is not installed; run `npm --prefix page install` once (network), or run \
         `frame check` or `frame test`, which install it for you"
    )]
    PageToolchainMissing {
        /// How many sources are newer than their compiled modules.
        changed: usize,
    },

    /// A named filesystem operation failed.
    #[error("{operation} `{path}` failed: {source}")]
    Io {
        /// Operation that failed.
        operation: &'static str,
        /// File or directory involved.
        path: PathBuf,
        /// Underlying filesystem error.
        #[source]
        source: io::Error,
    },

    /// The current directory could not be read.
    #[error("reading the current directory failed: {source}")]
    CurrentDir {
        /// Underlying filesystem error.
        #[source]
        source: io::Error,
    },

    /// The async runtime backing `frame run` could not be built.
    #[error("starting frame's process supervisor failed: {source}")]
    Runtime {
        /// Underlying runtime construction error.
        #[source]
        source: io::Error,
    },

    /// An EXPLICITLY-stated frame.toml socket is already taken — named before
    /// the host even tries to bind it, with the frame.toml key that holds it
    /// and the coherence rule for changing it. Only stated addresses are
    /// probed; a portless config (no `[frame].bind`, no `[bus]`) has nothing to
    /// probe because the host prefers-and-walks / OS-assigns those ports.
    #[error(
        "cannot start the {role}: the port named by frame.toml `{key}` ({addr}) is unavailable \
         ({source}). {coherence}"
    )]
    PortUnavailable {
        /// Which socket role is blocked (page server / bus / bus health /
        /// websocket).
        role: &'static str,
        /// The frame.toml key that holds this socket's address.
        key: &'static str,
        /// The address the host would have bound.
        addr: SocketAddr,
        /// The coherence rule to honour when moving this port.
        coherence: &'static str,
        /// The underlying bind failure (address-in-use, permission, …).
        #[source]
        source: io::Error,
    },

    /// The booted application never started accepting connections.
    #[error(
        "the application booted but `{bind}` never accepted a connection within {deadline:?}; \
         its output above names what went wrong"
    )]
    NeverReady {
        /// The configured page-server address.
        bind: String,
        /// How long frame waited.
        deadline: Duration,
    },

    /// The application ignored a stop request past the teardown deadline.
    #[error(
        "the application did not exit within {deadline:?} of SIGTERM and was killed; \
         this is a bug in the application's shutdown path"
    )]
    TeardownTimeout {
        /// How long frame waited before killing it.
        deadline: Duration,
    },

    /// One or more scopes of a multi-part verdict failed.
    #[error("{verb}: FAIL ({})", failed.join(", "))]
    VerdictFailed {
        /// The verb whose verdict failed.
        verb: &'static str,
        /// The scopes that failed.
        failed: Vec<String>,
    },

    /// The doctor found problems.
    #[error(
        "frame doctor found {problems} problem(s); each line above carries its fix and install link"
    )]
    DoctorProblems {
        /// How many problems the walkthrough found.
        problems: usize,
    },
}