Skip to main content

frame_cli/
error.rs

1//! Typed failures for every `frame` verb — each one loud, each one carrying
2//! its fix.
3
4use std::io;
5use std::net::SocketAddr;
6use std::path::PathBuf;
7use std::time::Duration;
8
9use thiserror::Error;
10
11use crate::doctor::MissingTools;
12use crate::scaffold::NewError;
13
14/// Typed refusal or failure from any `frame` verb.
15#[derive(Debug, Error)]
16pub enum CliError {
17    /// The dev loop refused to start or died with a typed report (F-7b).
18    #[error("frame dev: {detail}")]
19    Dev {
20        /// The typed failure, rendered.
21        detail: String,
22    },
23
24    /// Scaffold generation refused or failed.
25    #[error(transparent)]
26    New(#[from] NewError),
27
28    /// Initialising the new application's git repository failed (a git command
29    /// that ran refused — never a missing-git or already-in-a-repo condition,
30    /// which `frame new` reports and continues past).
31    #[error(transparent)]
32    Git(#[from] crate::scaffold::GitError),
33
34    /// A verb's prerequisite tools are missing (with install links).
35    #[error(transparent)]
36    MissingTools(#[from] MissingTools),
37
38    /// The real-browser proof has no browser to drive.
39    #[error("{reason}")]
40    MissingChrome {
41        /// The refusal, naming the probed path and the install link.
42        reason: String,
43    },
44
45    /// The verb was run outside a Frame application.
46    #[error(
47        "not inside a Frame application: no frame.toml in `{start}` or any parent directory; \
48         run this inside an application created by `frame new` (or create one: `frame new my_app`)"
49    )]
50    NotAnApplication {
51        /// Directory the search started from.
52        start: PathBuf,
53    },
54
55    /// The application's `frame.toml` failed to load or validate.
56    #[error("`{path}` is not a valid frame.toml")]
57    Config {
58        /// The config file that failed.
59        path: PathBuf,
60        /// The host's own typed load failure (boxed: the host error is
61        /// large, and `Result` should stay lean on the Ok path).
62        #[source]
63        source: Box<frame_host::HostError>,
64    },
65
66    /// `frame host` failed to boot or serve.
67    #[error("frame host failed")]
68    Host {
69        /// The host's own typed failure (boxed as above).
70        #[source]
71        source: Box<frame_host::HostError>,
72    },
73
74    /// A build or verification command exited unsuccessfully.
75    #[error("`{command}` failed with {status}; its output above names the failure")]
76    CommandFailed {
77        /// The command line that failed.
78        command: String,
79        /// Its exit status.
80        status: String,
81    },
82
83    /// A command could not be spawned at all (present prerequisites were
84    /// already checked, so this is an unexpected process failure, not a
85    /// missing tool).
86    #[error("`{command}` could not be started: {source}")]
87    CommandSpawn {
88        /// The command line that failed to start.
89        command: String,
90        /// The underlying process error.
91        #[source]
92        source: io::Error,
93    },
94
95    /// Page sources changed but the toolchain that recompiles them is not
96    /// installed, and this verb never installs anything.
97    #[error(
98        "{changed} page source file(s) changed since the page was last compiled, and the page \
99         toolchain is not installed; run `npm --prefix page install` once (network), or run \
100         `frame check` or `frame test`, which install it for you"
101    )]
102    PageToolchainMissing {
103        /// How many sources are newer than their compiled modules.
104        changed: usize,
105    },
106
107    /// A named filesystem operation failed.
108    #[error("{operation} `{path}` failed: {source}")]
109    Io {
110        /// Operation that failed.
111        operation: &'static str,
112        /// File or directory involved.
113        path: PathBuf,
114        /// Underlying filesystem error.
115        #[source]
116        source: io::Error,
117    },
118
119    /// The current directory could not be read.
120    #[error("reading the current directory failed: {source}")]
121    CurrentDir {
122        /// Underlying filesystem error.
123        #[source]
124        source: io::Error,
125    },
126
127    /// The async runtime backing `frame run` could not be built.
128    #[error("starting frame's process supervisor failed: {source}")]
129    Runtime {
130        /// Underlying runtime construction error.
131        #[source]
132        source: io::Error,
133    },
134
135    /// An EXPLICITLY-stated frame.toml socket is already taken — named before
136    /// the host even tries to bind it, with the frame.toml key that holds it
137    /// and the coherence rule for changing it. Only stated addresses are
138    /// probed; a portless config (no `[frame].bind`, no `[bus]`) has nothing to
139    /// probe because the host prefers-and-walks / OS-assigns those ports.
140    #[error(
141        "cannot start the {role}: the port named by frame.toml `{key}` ({addr}) is unavailable \
142         ({source}). {coherence}"
143    )]
144    PortUnavailable {
145        /// Which socket role is blocked (page server / bus / bus health /
146        /// websocket).
147        role: &'static str,
148        /// The frame.toml key that holds this socket's address.
149        key: &'static str,
150        /// The address the host would have bound.
151        addr: SocketAddr,
152        /// The coherence rule to honour when moving this port.
153        coherence: &'static str,
154        /// The underlying bind failure (address-in-use, permission, …).
155        #[source]
156        source: io::Error,
157    },
158
159    /// The booted application never started accepting connections.
160    #[error(
161        "the application booted but `{bind}` never accepted a connection within {deadline:?}; \
162         its output above names what went wrong"
163    )]
164    NeverReady {
165        /// The configured page-server address.
166        bind: String,
167        /// How long frame waited.
168        deadline: Duration,
169    },
170
171    /// The application ignored a stop request past the teardown deadline.
172    #[error(
173        "the application did not exit within {deadline:?} of SIGTERM and was killed; \
174         this is a bug in the application's shutdown path"
175    )]
176    TeardownTimeout {
177        /// How long frame waited before killing it.
178        deadline: Duration,
179    },
180
181    /// One or more scopes of a multi-part verdict failed.
182    #[error("{verb}: FAIL ({})", failed.join(", "))]
183    VerdictFailed {
184        /// The verb whose verdict failed.
185        verb: &'static str,
186        /// The scopes that failed.
187        failed: Vec<String>,
188    },
189
190    /// The doctor found problems.
191    #[error(
192        "frame doctor found {problems} problem(s); each line above carries its fix and install link"
193    )]
194    DoctorProblems {
195        /// How many problems the walkthrough found.
196        problems: usize,
197    },
198}