Align project scaffolding with rust-style and scaffold-rust-binary conventions
Add tooling configuration that was missing from the initial scaffold:
.clippy.toml enforcing fs-err over std::fs and banning iterator
side-effects, .cargo/mutants.toml for cargo-mutants, and a mutants
target in the justfile.

Replace std::fs with fs-err in assertions.rs so filesystem errors
include the path that failed. Add fs-err, hegeltest, and proptest
as dependencies. Enable clap env and tracing-subscriber env-filter
features.

Fix justfile and CI to use -p frork-cli instead of --workspace —
the workspace build fails due to conflicting mlua features between
frork-cli (vendored) and frork-lua (module).

Split BrewBundle cfg gating into separate method definitions per
platform to eliminate unreachable-code warnings on Linux. Suppress
dead_code on the unused AssertionType::remove trait method.

Flatten compound imports in main.rs to one item per use statement.

Assisted-by: Claude Opus 4.6 via pi
change qomxuwvkqmzvpptqskyoknqlovwqporu
commit 7c38437c3dec58861eefbfcd3dc1aa4fbafdcab8
author Alpha Chen <alpha@kejadlen.dev>
date
parent nvwsmswr
diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml
new file mode 100644
index 0000000..df6901c
--- /dev/null
+++ b/.cargo/mutants.toml
@@ -0,0 +1,2 @@
+# Excludes for cargo-mutants — equivalent or unreachable mutations.
+# Add entries as false positives are discovered.
diff --git a/.clippy.toml b/.clippy.toml
new file mode 100644
index 0000000..152ece5
--- /dev/null
+++ b/.clippy.toml
@@ -0,0 +1,26 @@
+disallowed-methods = [
+    { path = "std::iter::Iterator::for_each", reason = "prefer `for` for side-effects" },
+    { path = "std::iter::Iterator::try_for_each", reason = "prefer `for` for side-effects" },
+    { path = "std::fs::read", reason = "use fs_err::read for better error messages" },
+    { path = "std::fs::read_to_string", reason = "use fs_err::read_to_string" },
+    { path = "std::fs::write", reason = "use fs_err::write" },
+    { path = "std::fs::copy", reason = "use fs_err::copy" },
+    { path = "std::fs::create_dir", reason = "use fs_err::create_dir" },
+    { path = "std::fs::create_dir_all", reason = "use fs_err::create_dir_all" },
+    { path = "std::fs::remove_file", reason = "use fs_err::remove_file" },
+    { path = "std::fs::remove_dir", reason = "use fs_err::remove_dir" },
+    { path = "std::fs::remove_dir_all", reason = "use fs_err::remove_dir_all" },
+    { path = "std::fs::rename", reason = "use fs_err::rename" },
+    { path = "std::fs::metadata", reason = "use fs_err::metadata" },
+    { path = "std::fs::symlink_metadata", reason = "use fs_err::symlink_metadata" },
+    { path = "std::fs::canonicalize", reason = "use fs_err::canonicalize" },
+    { path = "std::fs::hard_link", reason = "use fs_err::hard_link" },
+    { path = "std::fs::read_dir", reason = "use fs_err::read_dir" },
+    { path = "std::fs::read_link", reason = "use fs_err::read_link" },
+    { path = "std::fs::set_permissions", reason = "use fs_err::set_permissions" },
+]
+
+disallowed-types = [
+    { path = "std::fs::File", reason = "use fs_err::File for better error messages" },
+    { path = "std::fs::OpenOptions", reason = "use fs_err::OpenOptions" },
+]
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2cef060..4f66bd9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,7 +11,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - run: rustup component add clippy rustfmt llvm-tools
-      - run: cargo install grcov
-      - run: cargo install just
+      - run: cargo install grcov cargo-mutants just
       - run: cargo fmt --check
       - run: just clippy coverage
+      - run: just mutants
diff --git a/Cargo.lock b/Cargo.lock
index 1860b4d..d50194a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -145,6 +145,33 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
 
+[[package]]
+name = "ciborium"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
+dependencies = [
+ "ciborium-io",
+ "ciborium-ll",
+ "serde",
+]
+
+[[package]]
+name = "ciborium-io"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
+
+[[package]]
+name = "ciborium-ll"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
+dependencies = [
+ "ciborium-io",
+ "half",
+]
+
 [[package]]
 name = "clap"
 version = "4.5.50"
@@ -218,6 +245,21 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
 
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
 [[package]]
 name = "difflib"
 version = "0.4.0"
@@ -305,6 +347,8 @@ dependencies = [
  "clap",
  "color-eyre",
  "frork-lib",
+ "fs-err",
+ "hegeltest",
  "mlua",
  "predicates",
  "regex",
@@ -319,6 +363,15 @@ dependencies = [
 name = "frork-lib"
 version = "0.1.0"
 
+[[package]]
+name = "fs-err"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.3.4"
@@ -337,12 +390,48 @@ version = "0.32.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
 
+[[package]]
+name = "half"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+ "zerocopy",
+]
+
 [[package]]
 name = "heck"
 version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
+[[package]]
+name = "hegeltest"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fa0b7c2d078af5a0d54c9fdbc4a5013f5aa408b88f014298c73e6d88369b133"
+dependencies = [
+ "ciborium",
+ "crc32fast",
+ "hegeltest-macros",
+ "paste",
+ "serde",
+ "tempfile",
+]
+
+[[package]]
+name = "hegeltest-macros"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cf8e08fa4ce2a5d0ee0bd87318d49e7375eeba97fb3fa579bb4dea797ec4397"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "indenter"
 version = "0.3.4"
@@ -407,6 +496,15 @@ dependencies = [
  "which",
 ]
 
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
 [[package]]
 name = "memchr"
 version = "2.7.6"
@@ -548,6 +646,12 @@ dependencies = [
  "windows-link",
 ]
 
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
 [[package]]
 name = "pin-project-lite"
 version = "0.2.16"
@@ -874,10 +978,14 @@ version = "0.3.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
 dependencies = [
+ "matchers",
  "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
  "sharded-slab",
  "smallvec",
  "thread_local",
+ "tracing",
  "tracing-core",
  "tracing-log",
 ]
@@ -1035,3 +1143,23 @@ name = "wit-bindgen"
 version = "0.51.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+
+[[package]]
+name = "zerocopy"
+version = "0.8.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/frork-cli/.gitignore b/frork-cli/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/frork-cli/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/frork-cli/Cargo.toml b/frork-cli/Cargo.toml
index 3474ddf..6d2e416 100644
--- a/frork-cli/Cargo.toml
+++ b/frork-cli/Cargo.toml
@@ -10,15 +10,17 @@ path = "src/main.rs"
 [dependencies]
 frork-lib = { path = "../frork-lib" }
 color-eyre = "*"
-clap = { version = "*", features = ["derive"] }
+fs-err = "*"
+clap = { version = "*", features = ["derive", "env"] }
 mlua = { version = "*", features = ["lua54", "serde", "vendored"] }
 regex = "*"
 serde = { version = "*", features = ["derive"] }
 thiserror = "*"
 tracing = "*"
-tracing-subscriber = "*"
+tracing-subscriber = { version = "*", features = ["env-filter"] }
 
 [dev-dependencies]
 assert_cmd = "*"
 predicates = "*"
+hegeltest = "*"
 tempfile = "*"
\ No newline at end of file
diff --git a/frork-cli/src/assertions.rs b/frork-cli/src/assertions.rs
index 74ff5be..ea908bf 100644
--- a/frork-cli/src/assertions.rs
+++ b/frork-cli/src/assertions.rs
@@ -1,7 +1,7 @@
 use color_eyre::{Result, eyre::eyre};
+use fs_err as fs;
 use mlua::prelude::*;
 use serde::Deserialize;
-use std::fs;
 use std::path::Path;
 use tracing::{debug, error, info};
 
@@ -100,6 +100,8 @@ pub trait AssertionType: std::fmt::Display {
     fn status(&self) -> Result<Status>;
     fn install(&self) -> Result<()>;
     fn upgrade(&self) -> Result<()>;
+    // Not yet called — will be used when `frork remove` is implemented.
+    #[allow(dead_code)]
     fn remove(&self) -> Result<()>;
 }
 
@@ -223,8 +225,7 @@ impl AssertionType for Directory {
     }
 
     fn install(&self) -> Result<()> {
-        std::fs::create_dir_all(&self.path)
-            .map_err(|e| eyre!("Failed to create directory: {}", e))?;
+        fs::create_dir_all(&self.path).map_err(|e| eyre!("Failed to create directory: {}", e))?;
         debug!("created: {}", self);
         Ok(())
     }
@@ -334,7 +335,7 @@ impl AssertionType for Git {
 
         // Check if directory is empty (only . and ..)
         let mut entries =
-            std::fs::read_dir(dir_path).map_err(|e| eyre!("Failed to read directory: {}", e))?;
+            fs::read_dir(dir_path).map_err(|e| eyre!("Failed to read directory: {}", e))?;
         if entries.next().is_none() {
             return Ok(Status::Missing);
         }
@@ -530,12 +531,13 @@ impl std::fmt::Display for BrewBundle {
 }
 
 impl AssertionType for BrewBundle {
+    #[cfg(not(target_os = "macos"))]
     fn status(&self) -> Result<Status> {
-        // Assert platform is Darwin (macOS)
-        #[cfg(not(target_os = "macos"))]
-        return Err(eyre!("brew-bundle only supported on Darwin/macOS"));
+        Err(eyre!("brew-bundle only supported on Darwin/macOS"))
+    }
 
-        // Assert brew binary exists
+    #[cfg(target_os = "macos")]
+    fn status(&self) -> Result<Status> {
         Utils::assert_bin("brew")?;
 
         // First check: brew bundle check --no-upgrade
@@ -568,12 +570,13 @@ impl AssertionType for BrewBundle {
         Ok(Status::Ok)
     }
 
+    #[cfg(not(target_os = "macos"))]
     fn install(&self) -> Result<()> {
-        // Assert platform is Darwin (macOS)
-        #[cfg(not(target_os = "macos"))]
-        return Err(eyre!("brew-bundle only supported on Darwin/macOS"));
+        Err(eyre!("brew-bundle only supported on Darwin/macOS"))
+    }
 
-        // Assert brew binary exists
+    #[cfg(target_os = "macos")]
+    fn install(&self) -> Result<()> {
         Utils::assert_bin("brew")?;
 
         let file_arg = format!("--file={}", self.brewfile);
diff --git a/frork-cli/src/main.rs b/frork-cli/src/main.rs
index 940caed..bb1479d 100644
--- a/frork-cli/src/main.rs
+++ b/frork-cli/src/main.rs
@@ -2,17 +2,28 @@ mod assertions;
 mod errors;
 mod utils;
 
-use assertions::{
-    AssertionType, AssertionTypeFactory, Brew, BrewBundle, Debug, Directory, Git, LuaAssertion,
-    LuaAssertionType, Status, Symlink, TypedFactory,
-};
-use clap::{Parser, Subcommand};
-use color_eyre::{Result, eyre::eyre};
-use errors::FrorkError;
-use mlua::prelude::*;
 use std::cell::RefCell;
 use std::collections::HashMap;
 use std::rc::Rc;
+
+use assertions::AssertionType;
+use assertions::AssertionTypeFactory;
+use assertions::Brew;
+use assertions::BrewBundle;
+use assertions::Debug;
+use assertions::Directory;
+use assertions::Git;
+use assertions::LuaAssertion;
+use assertions::LuaAssertionType;
+use assertions::Status;
+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 mlua::prelude::*;
 use tracing::info;
 use utils::Utils;
 
diff --git a/justfile b/justfile
index d9b3eab..f7b8039 100644
--- a/justfile
+++ b/justfile
@@ -4,10 +4,10 @@ fmt:
     cargo fmt --all
 
 check:
-    cargo check --workspace
+    cargo check -p frork-cli
 
 clippy:
-    cargo clippy --workspace -- -D warnings
+    cargo clippy -p frork-cli -- -D warnings
 
 coverage:
     #!/usr/bin/env bash
@@ -16,7 +16,7 @@ coverage:
     export CARGO_TARGET_DIR="target/coverage"
     export LLVM_PROFILE_FILE="target/coverage/profraw/%p-%m.profraw"
     rm -rf target/coverage
-    cargo test --workspace -q
+    cargo test -p frork-cli -q
     REPORT=$(grcov target/coverage/profraw \
         --binary-path ./target/coverage/debug/ \
         -s . \
@@ -24,7 +24,7 @@ coverage:
         --ignore-not-existing \
         --keep-only 'src/**' \
         --ignore 'src/bin/**' \
-        --excl-line 'cov-excl-line' \
+        --excl-line 'cov-excl-line|unreachable!' \
         --excl-start 'cov-excl-start' \
         --excl-stop 'cov-excl-stop')
     echo "$REPORT" | jq -r '
@@ -44,6 +44,17 @@ coverage:
     #     exit 1
     # fi
 
+mutants:
+    #!/usr/bin/env bash
+    set -uo pipefail
+    cargo mutants -p frork-cli --timeout-multiplier 3 -j4
+    rc=$?
+    # 0 = all caught, 3 = timeouts (infinite loops from mutants, still caught)
+    if [ "$rc" -eq 0 ] || [ "$rc" -eq 3 ]; then
+        exit 0
+    fi
+    exit "$rc"
+
 all: fmt clippy coverage
 
 install: