Replace color-eyre with miette for error reporting
miette provides richer diagnostics — source spans, labels, help text,
and severity levels — which will be useful as frork's error messages
mature. FrorkError now derives miette::Diagnostic alongside thiserror.
The io::Error uses in the satisfy prompt needed explicit .map_err
wrapping since miette::Report does not impl From<io::Error> the way
eyre::Report does.
Assisted-by: Claude Opus 4.6 via pi
diff --git a/AGENTS.md b/AGENTS.md
index aa117f5..8af6bf3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -50,7 +50,7 @@ just
## Conventions
- **Rust edition 2024**, resolver v3 workspace.
-- Error handling: `color-eyre` in the binary, `thiserror` in library code.
+- Error handling: `miette` in the binary, `thiserror` in library code. Library errors derive `miette::Diagnostic`.
- Logging uses `tracing` with `tracing-subscriber`. Use `tracing::info`, `tracing::debug`, etc. — not `println!` for diagnostic output.
- Lua integration via `mlua` with vendored Lua 5.4. Fennel compiler is vendored as a Lua source file.
- All CI checks must pass: `cargo fmt --all --check`, `cargo clippy`, `cargo test`.
diff --git a/Cargo.lock b/Cargo.lock
index 8532086..7e0b94c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -112,6 +112,15 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "backtrace-ext"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50"
+dependencies = [
+ "backtrace",
+]
+
[[package]]
name = "bitflags"
version = "2.10.0"
@@ -212,33 +221,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
-[[package]]
-name = "color-eyre"
-version = "0.6.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d"
-dependencies = [
- "backtrace",
- "color-spantrace",
- "eyre",
- "indenter",
- "once_cell",
- "owo-colors",
- "tracing-error",
-]
-
-[[package]]
-name = "color-spantrace"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427"
-dependencies = [
- "once_cell",
- "owo-colors",
- "tracing-core",
- "tracing-error",
-]
-
[[package]]
name = "colorchoice"
version = "1.0.4"
@@ -299,16 +281,6 @@ dependencies = [
"windows-sys 0.61.2",
]
-[[package]]
-name = "eyre"
-version = "0.6.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
-dependencies = [
- "indenter",
- "once_cell",
-]
-
[[package]]
name = "fastrand"
version = "2.3.0"
@@ -343,10 +315,10 @@ version = "0.1.0"
dependencies = [
"assert_cmd",
"clap",
- "color-eyre",
"frork-lib",
"fs-err",
"hegeltest",
+ "miette",
"mlua",
"predicates",
"regex",
@@ -431,10 +403,10 @@ dependencies = [
]
[[package]]
-name = "indenter"
-version = "0.3.4"
+name = "is_ci"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
+checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
[[package]]
name = "is_terminal_polyfill"
@@ -509,6 +481,36 @@ version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+[[package]]
+name = "miette"
+version = "7.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
+dependencies = [
+ "backtrace",
+ "backtrace-ext",
+ "cfg-if",
+ "miette-derive",
+ "owo-colors",
+ "supports-color",
+ "supports-hyperlinks",
+ "supports-unicode",
+ "terminal_size",
+ "textwrap",
+ "unicode-width 0.1.14",
+]
+
+[[package]]
+name = "miette-derive"
+version = "7.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -858,6 +860,27 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+[[package]]
+name = "supports-color"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6"
+dependencies = [
+ "is_ci",
+]
+
+[[package]]
+name = "supports-hyperlinks"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91"
+
+[[package]]
+name = "supports-unicode"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
+
[[package]]
name = "syn"
version = "2.0.108"
@@ -882,12 +905,32 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "terminal_size"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
+dependencies = [
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "termtree"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
+[[package]]
+name = "textwrap"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
+dependencies = [
+ "unicode-linebreak",
+ "unicode-width 0.2.2",
+]
+
[[package]]
name = "thiserror"
version = "2.0.17"
@@ -949,16 +992,6 @@ dependencies = [
"valuable",
]
-[[package]]
-name = "tracing-error"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db"
-dependencies = [
- "tracing",
- "tracing-subscriber",
-]
-
[[package]]
name = "tracing-log"
version = "0.2.0"
@@ -1000,6 +1033,24 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
+[[package]]
+name = "unicode-linebreak"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
[[package]]
name = "utf8parse"
version = "0.2.2"
diff --git a/frork-cli/Cargo.toml b/frork-cli/Cargo.toml
index 6d2e416..0f47c93 100644
--- a/frork-cli/Cargo.toml
+++ b/frork-cli/Cargo.toml
@@ -9,7 +9,7 @@ path = "src/main.rs"
[dependencies]
frork-lib = { path = "../frork-lib" }
-color-eyre = "*"
+miette = { version = "*", features = ["fancy"] }
fs-err = "*"
clap = { version = "*", features = ["derive", "env"] }
mlua = { version = "*", features = ["lua54", "serde", "vendored"] }
diff --git a/frork-cli/src/assertions.rs b/frork-cli/src/assertions.rs
index ea908bf..5dfaa0c 100644
--- a/frork-cli/src/assertions.rs
+++ b/frork-cli/src/assertions.rs
@@ -1,5 +1,5 @@
-use color_eyre::{Result, eyre::eyre};
use fs_err as fs;
+use miette::{Result, miette};
use mlua::prelude::*;
use serde::Deserialize;
use std::path::Path;
@@ -33,7 +33,7 @@ where
fn create(&self, lua: &Lua, args: LuaMultiValue) -> Result<Box<dyn AssertionType>> {
T::from_lua_multi(args, lua)
.map(|t| Box::new(t) as Box<dyn AssertionType>)
- .map_err(|e| eyre!("Failed to create assertion type: {}", e))
+ .map_err(|e| miette!("Failed to create assertion type: {}", e))
}
}
@@ -178,7 +178,7 @@ impl AssertionType for Symlink {
fn install(&self) -> Result<()> {
use std::os::unix::fs;
fs::symlink(&self.source, &self.target)
- .map_err(|e| eyre!("Failed to create symlink: {}", e))?;
+ .map_err(|e| miette!("Failed to create symlink: {}", e))?;
debug!("created: {}", self);
Ok(())
}
@@ -225,7 +225,7 @@ impl AssertionType for Directory {
}
fn install(&self) -> Result<()> {
- fs::create_dir_all(&self.path).map_err(|e| eyre!("Failed to create directory: {}", e))?;
+ fs::create_dir_all(&self.path).map_err(|e| miette!("Failed to create directory: {}", e))?;
debug!("created: {}", self);
Ok(())
}
@@ -267,7 +267,7 @@ impl AssertionType for Debug {
if let Some(ref status_fn) = self.status_fn {
let result = status_fn
.call::<Status>(LuaMultiValue::new())
- .map_err(|e| eyre!("Debug status function failed: {}", e))?;
+ .map_err(|e| miette!("Debug status function failed: {}", e))?;
Ok(result)
} else {
Ok(Status::Ok)
@@ -279,10 +279,10 @@ impl AssertionType for Debug {
let install_fn = self
.install_fn
.as_ref()
- .ok_or_else(|| eyre!("Install not implemented for debug assertion"))?;
+ .ok_or_else(|| miette!("Install not implemented for debug assertion"))?;
install_fn
.call::<()>(LuaMultiValue::new())
- .map_err(|e| eyre!("Debug install function failed: {}", e))?;
+ .map_err(|e| miette!("Debug install function failed: {}", e))?;
Ok(())
}
@@ -335,7 +335,7 @@ impl AssertionType for Git {
// Check if directory is empty (only . and ..)
let mut entries =
- fs::read_dir(dir_path).map_err(|e| eyre!("Failed to read directory: {}", e))?;
+ fs::read_dir(dir_path).map_err(|e| miette!("Failed to read directory: {}", e))?;
if entries.next().is_none() {
return Ok(Status::Missing);
}
@@ -371,7 +371,7 @@ impl AssertionType for Git {
let (_output, exit_code) = Utils::sh("git", &["clone", &self.remote_url, &self.dir])?;
if exit_code != 0 {
- return Err(eyre!("Failed to clone git repository"));
+ return Err(miette!("Failed to clone git repository"));
}
debug!("created: {}", self);
@@ -497,7 +497,7 @@ impl AssertionType for Brew {
)?;
if exit_code != 0 {
- return Err(eyre!("Failed to install Homebrew: {}", output));
+ return Err(miette!("Failed to install Homebrew: {}", output));
}
debug!("installed: {}", self);
@@ -533,7 +533,7 @@ impl std::fmt::Display for BrewBundle {
impl AssertionType for BrewBundle {
#[cfg(not(target_os = "macos"))]
fn status(&self) -> Result<Status> {
- Err(eyre!("brew-bundle only supported on Darwin/macOS"))
+ Err(miette!("brew-bundle only supported on Darwin/macOS"))
}
#[cfg(target_os = "macos")]
@@ -572,7 +572,7 @@ impl AssertionType for BrewBundle {
#[cfg(not(target_os = "macos"))]
fn install(&self) -> Result<()> {
- Err(eyre!("brew-bundle only supported on Darwin/macOS"))
+ Err(miette!("brew-bundle only supported on Darwin/macOS"))
}
#[cfg(target_os = "macos")]
@@ -583,7 +583,7 @@ impl AssertionType for BrewBundle {
let (_output, exit_code) = Utils::sh("brew", &["bundle", "install", &file_arg])?;
if exit_code != 0 {
- return Err(eyre!("Failed to install brew bundle"));
+ return Err(miette!("Failed to install brew bundle"));
}
debug!("installed: {}", self);
diff --git a/frork-cli/src/errors.rs b/frork-cli/src/errors.rs
index 4043b67..ee1fbeb 100644
--- a/frork-cli/src/errors.rs
+++ b/frork-cli/src/errors.rs
@@ -1,7 +1,8 @@
+use miette::Diagnostic;
use mlua::prelude::*;
use thiserror::Error;
-#[derive(Error, Debug, Clone, PartialEq)]
+#[derive(Error, Diagnostic, Debug, Clone, PartialEq)]
pub enum FrorkError {
#[error("No operation specified")]
NoOperation,
diff --git a/frork-cli/src/main.rs b/frork-cli/src/main.rs
index 02fe4da..6a22411 100644
--- a/frork-cli/src/main.rs
+++ b/frork-cli/src/main.rs
@@ -20,9 +20,9 @@ use assertions::Symlink;
use assertions::TypedFactory;
use clap::Parser;
use clap::Subcommand;
-use color_eyre::Result;
-use color_eyre::eyre::eyre;
use errors::FrorkError;
+use miette::Result;
+use miette::miette;
use mlua::prelude::*;
use tracing::info;
use utils::Utils;
@@ -188,19 +188,19 @@ fn setup_lua(
let fennel_module: LuaTable = lua
.load(fennel_code)
.eval()
- .map_err(|e| eyre!("Failed to load Fennel: {}", e))?;
+ .map_err(|e| miette!("Failed to load Fennel: {}", e))?;
lua.register_module("fennel", &fennel_module)
- .map_err(|e| eyre!("Failed to register fennel module: {}", e))?;
+ .map_err(|e| miette!("Failed to register fennel module: {}", e))?;
let frork_table = match Frork::new(handle_status, lua.clone())
.into_lua(&lua)
- .map_err(|e| eyre!("Failed to create frork table: {}", e))?
+ .map_err(|e| miette!("Failed to create frork table: {}", e))?
{
LuaValue::Table(table) => table,
_ => unreachable!(),
};
lua.register_module("frork", &frork_table)
- .map_err(|e| eyre!("Failed to register frork module: {}", e))?;
+ .map_err(|e| miette!("Failed to register frork module: {}", e))?;
Ok((lua, frork_table, fennel_module))
}
@@ -213,18 +213,18 @@ fn run_code(
let ok_fn: LuaFunction = frork_module
.get("ok")
- .map_err(|e| eyre!("Failed to get frork.ok: {}", e))?;
+ .map_err(|e| miette!("Failed to get frork.ok: {}", e))?;
lua.globals()
.set("ok", ok_fn)
- .map_err(|e| eyre!("Failed to set ok global: {}", e))?;
+ .map_err(|e| miette!("Failed to set ok global: {}", e))?;
let eval_fn: LuaFunction = fennel_module
.get("eval")
- .map_err(|e| eyre!("Failed to get fennel.eval: {}", e))?;
+ .map_err(|e| miette!("Failed to get fennel.eval: {}", e))?;
eval_fn
.call::<()>(code)
- .map_err(|e| eyre!("Failed to execute fennel code: {}", e))?;
+ .map_err(|e| miette!("Failed to execute fennel code: {}", e))?;
Ok(())
}
@@ -243,19 +243,19 @@ fn run_script(
let package_table: LuaTable = lua
.globals()
.get("package")
- .map_err(|e| eyre!("Failed to get package table: {}", e))?;
+ .map_err(|e| miette!("Failed to get package table: {}", e))?;
// Add to Lua search path
let current_path: String = package_table
.get("path")
- .map_err(|e| eyre!("Failed to get current Lua path: {}", e))?;
+ .map_err(|e| miette!("Failed to get current Lua path: {}", e))?;
let new_path = format!(
"{};{}/?.lua;{}/?/init.lua",
current_path, script_dir_str, script_dir_str
);
package_table
.set("path", new_path)
- .map_err(|e| eyre!("Failed to set Lua path: {}", e))?;
+ .map_err(|e| miette!("Failed to set Lua path: {}", e))?;
// Add to Fennel search path
let current_fennel_path: String = fennel_module
@@ -267,7 +267,7 @@ fn run_script(
);
fennel_module
.set("path", new_fennel_path)
- .map_err(|e| eyre!("Failed to set Fennel path: {}", e))?;
+ .map_err(|e| miette!("Failed to set Fennel path: {}", e))?;
}
lua.load(format!(
@@ -275,7 +275,7 @@ fn run_script(
script
))
.exec()
- .map_err(|e| eyre!("Failed to execute script: {}", e))?;
+ .map_err(|e| miette!("Failed to execute script: {}", e))?;
Ok(())
}
@@ -307,12 +307,16 @@ fn satisfy(status: &Status, assertion: &dyn AssertionType) -> Result<()> {
println!(" expected: {}", conflict.expected);
println!(" actual: {}", conflict.actual);
- use std::io::{self, Write};
+ use std::io::Write as _;
print!("Upgrade? [y/N]: ");
- io::stdout().flush()?;
+ std::io::stdout()
+ .flush()
+ .map_err(|e| miette!("Failed to flush stdout: {}", e))?;
let mut input = String::new();
- io::stdin().read_line(&mut input)?;
+ std::io::stdin()
+ .read_line(&mut input)
+ .map_err(|e| miette!("Failed to read input: {}", e))?;
match input.trim().to_lowercase().as_str() {
"y" | "yes" => {
@@ -338,7 +342,6 @@ fn run(command: &Commands) -> Result<()> {
}
fn main() -> Result<()> {
- color_eyre::install()?;
tracing_subscriber::fmt::init();
let cli = Cli::parse();
diff --git a/frork-cli/src/utils.rs b/frork-cli/src/utils.rs
index c08ae68..282aefe 100644
--- a/frork-cli/src/utils.rs
+++ b/frork-cli/src/utils.rs
@@ -1,4 +1,4 @@
-use color_eyre::{Result, eyre::eyre};
+use miette::{Result, miette};
use mlua::prelude::*;
use regex::Regex;
use std::env;
@@ -35,7 +35,7 @@ impl From<ExpandedPath> for String {
}
impl TryFrom<String> for ExpandedPath {
- type Error = color_eyre::eyre::Error;
+ type Error = miette::Report;
fn try_from(path: String) -> Result<Self> {
Self::new(&path)
@@ -43,7 +43,7 @@ impl TryFrom<String> for ExpandedPath {
}
impl TryFrom<&str> for ExpandedPath {
- type Error = color_eyre::eyre::Error;
+ type Error = miette::Report;
fn try_from(path: &str) -> Result<Self> {
Self::new(path)
@@ -110,7 +110,8 @@ impl Utils {
// Expand tilde
if expanded.starts_with('~') {
- let home = env::var("HOME").map_err(|_| eyre!("HOME environment variable not set"))?;
+ let home =
+ env::var("HOME").map_err(|_| miette!("HOME environment variable not set"))?;
expanded = expanded.replacen('~', &home, 1);
}
@@ -130,7 +131,7 @@ impl Utils {
.to_string();
if !missing.is_empty() {
- return Err(eyre!(
+ return Err(miette!(
"Environment variables not found: {}",
missing.join(", ")
));
@@ -167,13 +168,13 @@ impl Utils {
let output = command
.output()
- .map_err(|e| eyre!("Failed to execute command '{}': {}", cmd, e))?;
+ .map_err(|e| miette!("Failed to execute command '{}': {}", cmd, e))?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let status = output
.status
.code()
- .ok_or_else(|| eyre!("Command '{}' terminated by signal", cmd))?;
+ .ok_or_else(|| miette!("Command '{}' terminated by signal", cmd))?;
debug!(
"Command '{}' completed with status {}, stdout: {}",
cmd,
@@ -191,7 +192,7 @@ impl Utils {
debug!("Binary '{}' found in PATH", bin_name);
Ok(())
} else {
- Err(eyre!("Required binary '{}' not found in PATH", bin_name))
+ Err(miette!("Required binary '{}' not found in PATH", bin_name))
}
}
}