refactor: Align frork-cli with rust-style and rust-binary conventions
Reorder assertions.rs to lead with the central AssertionType trait
(M-ITEM-TOC), split compound imports (M-SINGLE-USE), and move
Lua custom types to the end. In main.rs, place callers before
callees (M-CALLER-CALLEE) and group all Frork inherent impls
before the IntoLua trait impl (M-TYPE-ASSOC).
Rename errors.rs to error.rs (singular, per rust-binary skill).
Add clap_complete with a --completions flag for bash/zsh/fish.
Running frork with no subcommand now prints help instead of
erroring. Add setup-uv to CI for hegeltest's Hypothesis backend.
Assisted-by: Claude Opus 4.6 via pi
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7d6fa3d..3b7e8c0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,6 +12,7 @@ jobs:
- uses: actions/checkout@v4
- run: rustup component add clippy rustfmt llvm-tools
- run: cargo install grcov just
+ - uses: astral-sh/setup-uv@v6
- run: cargo fmt --check
- run: just clippy coverage
# - run: just mutants
diff --git a/AGENTS.md b/AGENTS.md
index 8af6bf3..b4f8b01 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -16,7 +16,7 @@ frork-cli/
main.rs # Entrypoint, clap CLI, Lua/Fennel setup
lib.rs # Library root — re-exports modules
assertions.rs # Assertion types (symlink, directory, git, brew, lua)
- errors.rs # thiserror enum (FrorkError)
+ error.rs # thiserror enum (FrorkError)
utils.rs # Shell helpers, path expansion, Lua bindings
tests/
cli.rs # Integration tests via assert_cmd
diff --git a/Cargo.lock b/Cargo.lock
index 7e0b94c..219b98d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -203,6 +203,15 @@ dependencies = [
"strsim",
]
+[[package]]
+name = "clap_complete"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb"
+dependencies = [
+ "clap",
+]
+
[[package]]
name = "clap_derive"
version = "4.5.49"
@@ -315,6 +324,7 @@ version = "0.1.0"
dependencies = [
"assert_cmd",
"clap",
+ "clap_complete",
"frork-lib",
"fs-err",
"hegeltest",
diff --git a/frork-cli/Cargo.toml b/frork-cli/Cargo.toml
index 0f47c93..ee02148 100644
--- a/frork-cli/Cargo.toml
+++ b/frork-cli/Cargo.toml
@@ -12,6 +12,7 @@ frork-lib = { path = "../frork-lib" }
miette = { version = "*", features = ["fancy"] }
fs-err = "*"
clap = { version = "*", features = ["derive", "env"] }
+clap_complete = "*"
mlua = { version = "*", features = ["lua54", "serde", "vendored"] }
regex = "*"
serde = { version = "*", features = ["derive"] }
diff --git a/frork-cli/src/assertions.rs b/frork-cli/src/assertions.rs
index 1c4382c..c4f983c 100644
--- a/frork-cli/src/assertions.rs
+++ b/frork-cli/src/assertions.rs
@@ -5,50 +5,21 @@ use miette::miette;
use mlua::prelude::*;
use serde::Deserialize;
use std::path::Path;
-use tracing::{debug, error, info};
+use tracing::debug;
+use tracing::error;
+use tracing::info;
-use crate::errors::FrorkError;
-use crate::utils::{ExpandedPath, Utils};
+use crate::error::FrorkError;
+use crate::utils::ExpandedPath;
+use crate::utils::Utils;
-pub trait AssertionTypeFactory {
- fn create(&self, lua: &Lua, args: LuaMultiValue) -> Result<Box<dyn AssertionType>>;
-}
-
-pub struct TypedFactory<T>(std::marker::PhantomData<T>);
-
-impl<T> TypedFactory<T> {
- pub fn new() -> Self {
- Self(std::marker::PhantomData)
- }
-}
-
-impl<T> Default for TypedFactory<T> {
- fn default() -> Self {
- Self::new()
- }
-}
-
-impl<T> AssertionTypeFactory for TypedFactory<T>
-where
- T: AssertionType + FromLuaMulti + 'static,
-{
- 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| miette!("Failed to create assertion type: {e}"))
- }
-}
-
-#[derive(Debug, Deserialize)]
-pub struct Conflict {
- pub expected: String,
- pub actual: String,
-}
-
-impl FromLua for Conflict {
- fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
- lua.from_value(value)
- }
+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<()>;
}
#[derive(Debug)]
@@ -60,7 +31,7 @@ pub enum Status {
impl FromLuaMulti for Status {
fn from_lua_multi(values: LuaMultiValue, lua: &Lua) -> LuaResult<Self> {
- // Try two values: string and conflict for conflict-upgrade
+ // Try two values: string and conflict for conflict-upgrade.
if let Ok((status_str, conflict)) =
<(String, Conflict)>::from_lua_multi(values.clone(), lua)
{
@@ -77,7 +48,7 @@ impl FromLuaMulti for Status {
};
}
- // Try single string
+ // Try single string.
if let Ok(status_str) = String::from_lua_multi(values, lua) {
return match status_str.as_str() {
"ok" => Ok(Status::Ok),
@@ -98,38 +69,49 @@ impl FromLuaMulti for Status {
}
}
-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<()>;
+#[derive(Debug, Deserialize)]
+pub struct Conflict {
+ pub expected: String,
+ pub actual: String,
}
-#[derive(Clone)]
-pub struct LuaAssertionType {
- pub display_fn: Option<LuaFunction>,
- pub status_fn: LuaFunction,
- pub install_fn: LuaFunction,
+impl FromLua for Conflict {
+ fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
+ lua.from_value(value)
+ }
}
-impl FromLua for LuaAssertionType {
- fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
- let table = LuaTable::from_lua(value, lua)?;
+pub trait AssertionTypeFactory {
+ fn create(&self, lua: &Lua, args: LuaMultiValue) -> Result<Box<dyn AssertionType>>;
+}
- let display_fn: Option<LuaFunction> = table.get("display").ok();
- let status_fn: LuaFunction = table.get("status")?;
- let install_fn: LuaFunction = table.get("install")?;
+pub struct TypedFactory<T>(std::marker::PhantomData<T>);
- Ok(Self {
- display_fn,
- status_fn,
- install_fn,
- })
+impl<T> TypedFactory<T> {
+ pub fn new() -> Self {
+ Self(std::marker::PhantomData)
}
}
+impl<T> Default for TypedFactory<T> {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl<T> AssertionTypeFactory for TypedFactory<T>
+where
+ T: AssertionType + FromLuaMulti + 'static,
+{
+ 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| miette!("Failed to create assertion type: {e}"))
+ }
+}
+
+// --- Built-in assertion types ---
+
pub struct Symlink {
pub target: ExpandedPath,
pub source: ExpandedPath,
@@ -241,62 +223,6 @@ impl AssertionType for Directory {
}
}
-pub struct Debug {
- pub status_fn: Option<LuaFunction>,
- pub install_fn: Option<LuaFunction>,
-}
-
-impl FromLuaMulti for Debug {
- fn from_lua_multi(args: LuaMultiValue, lua: &Lua) -> LuaResult<Self> {
- let table = LuaTable::from_lua_multi(args, lua)?;
- let status_fn: Option<LuaFunction> = table.get("status").ok();
- let install_fn: Option<LuaFunction> = table.get("install").ok();
- Ok(Self {
- status_fn,
- install_fn,
- })
- }
-}
-
-impl std::fmt::Display for Debug {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "debug")
- }
-}
-
-impl AssertionType for Debug {
- fn status(&self) -> Result<Status> {
- if let Some(ref status_fn) = self.status_fn {
- let result = status_fn
- .call::<Status>(LuaMultiValue::new())
- .map_err(|e| miette!("Debug status function failed: {e}"))?;
- Ok(result)
- } else {
- Ok(Status::Ok)
- }
- }
-
- fn install(&self) -> Result<()> {
- info!("debug: installing");
- let install_fn = self
- .install_fn
- .as_ref()
- .ok_or_else(|| miette!("Install not implemented for debug assertion"))?;
- install_fn
- .call::<()>(LuaMultiValue::new())
- .map_err(|e| miette!("Debug install function failed: {e}"))?;
- Ok(())
- }
-
- fn upgrade(&self) -> Result<()> {
- todo!()
- }
-
- fn remove(&self) -> Result<()> {
- todo!()
- }
-}
-
pub struct Git {
pub dir: ExpandedPath,
pub remote_url: String,
@@ -317,17 +243,14 @@ impl std::fmt::Display for Git {
impl AssertionType for Git {
fn status(&self) -> Result<Status> {
- // First, ensure git binary is available
Utils::assert_bin("git")?;
let dir_path = Path::new(&self.dir);
- // Check if directory exists
if !dir_path.exists() {
return Ok(Status::Missing);
}
- // Check if it's a directory
if !dir_path.is_dir() {
return Ok(Status::ConflictUpgrade(Conflict {
expected: "directory".to_string(),
@@ -335,13 +258,12 @@ impl AssertionType for Git {
}));
}
- // Check if directory is empty (only . and ..)
+ // Check if the directory is empty.
let mut entries = fs::read_dir(dir_path).into_diagnostic()?;
if entries.next().is_none() {
return Ok(Status::Missing);
}
- // Check if we can get the git remote
let (remote_output, exit_code) = Utils::sh(
"git",
&["-C", &self.dir, "config", "--get", "remote.origin.url"],
@@ -366,7 +288,6 @@ impl AssertionType for Git {
}
fn install(&self) -> Result<()> {
- // Ensure git binary is available before attempting clone
Utils::assert_bin("git")?;
let (_output, exit_code) = Utils::sh("git", &["clone", &self.remote_url, &self.dir])?;
@@ -388,76 +309,6 @@ impl AssertionType for Git {
}
}
-pub struct LuaAssertion {
- pub name: String,
- pub args: LuaMultiValue,
- pub assertion_type: LuaAssertionType,
-}
-
-impl LuaAssertion {
- pub fn new(name: &str, args: LuaMultiValue, assertion_type: LuaAssertionType) -> Self {
- Self {
- name: name.to_string(),
- args,
- assertion_type,
- }
- }
-}
-
-impl std::fmt::Display for LuaAssertion {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let default_display = || {
- let args_str = self
- .args
- .iter()
- .map(|v| v.to_string().unwrap_or_else(|_| "?".to_string()))
- .collect::<Vec<_>>()
- .join(" ");
- format!("{} {}", self.name, args_str)
- };
-
- if let Some(ref display_fn) = self.assertion_type.display_fn {
- let result = display_fn
- .call::<String>(self.args.clone())
- .unwrap_or_else(|err| {
- error!("Display function failed for {}: {}", self.name, err);
- default_display()
- });
- write!(f, "{}", result)
- } else {
- write!(f, "{}", default_display())
- }
- }
-}
-
-impl AssertionType for LuaAssertion {
- fn status(&self) -> Result<Status> {
- let result = self
- .assertion_type
- .status_fn
- .call::<Status>(self.args.clone())
- .map_err(FrorkError::from)?;
- Ok(result)
- }
-
- fn install(&self) -> Result<()> {
- self.assertion_type
- .install_fn
- .call::<()>(self.args.clone())
- .map_err(FrorkError::from)?;
- debug!("installed: {}", self);
- Ok(())
- }
-
- fn upgrade(&self) -> Result<()> {
- todo!()
- }
-
- fn remove(&self) -> Result<()> {
- todo!()
- }
-}
-
pub struct Brew;
impl FromLuaMulti for Brew {
@@ -474,7 +325,7 @@ impl std::fmt::Display for Brew {
impl AssertionType for Brew {
fn status(&self) -> Result<Status> {
- // Early return if brew command is not in PATH
+ // Early return if brew command is not in PATH.
if Utils::assert_bin("brew").is_err() {
return Ok(Status::Missing);
}
@@ -487,7 +338,7 @@ impl AssertionType for Brew {
}
}
- // This needs sudo - figure out how to make this work through frork
+ // This needs sudo — figure out how to make this work through frork.
fn install(&self) -> Result<()> {
let (output, exit_code) = Utils::sh(
"bash",
@@ -541,7 +392,7 @@ impl AssertionType for BrewBundle {
fn status(&self) -> Result<Status> {
Utils::assert_bin("brew")?;
- // First check: brew bundle check --no-upgrade
+ // First check: brew bundle check --no-upgrade.
let file_arg = format!("--file={}", self.brewfile);
let (_output, exit_code) = Utils::sh_with_envs(
"brew",
@@ -553,7 +404,7 @@ impl AssertionType for BrewBundle {
return Ok(Status::Missing);
}
- // Second check: brew bundle check (without --no-upgrade)
+ // Second check: brew bundle check (without --no-upgrade).
let file_arg = format!("--file={}", self.brewfile);
let (_output, exit_code) = Utils::sh_with_envs(
"brew",
@@ -599,3 +450,154 @@ impl AssertionType for BrewBundle {
todo!()
}
}
+
+pub struct Debug {
+ pub status_fn: Option<LuaFunction>,
+ pub install_fn: Option<LuaFunction>,
+}
+
+impl FromLuaMulti for Debug {
+ fn from_lua_multi(args: LuaMultiValue, lua: &Lua) -> LuaResult<Self> {
+ let table = LuaTable::from_lua_multi(args, lua)?;
+ let status_fn: Option<LuaFunction> = table.get("status").ok();
+ let install_fn: Option<LuaFunction> = table.get("install").ok();
+ Ok(Self {
+ status_fn,
+ install_fn,
+ })
+ }
+}
+
+impl std::fmt::Display for Debug {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "debug")
+ }
+}
+
+impl AssertionType for Debug {
+ fn status(&self) -> Result<Status> {
+ if let Some(ref status_fn) = self.status_fn {
+ let result = status_fn
+ .call::<Status>(LuaMultiValue::new())
+ .map_err(|e| miette!("Debug status function failed: {e}"))?;
+ Ok(result)
+ } else {
+ Ok(Status::Ok)
+ }
+ }
+
+ fn install(&self) -> Result<()> {
+ info!("debug: installing");
+ let install_fn = self
+ .install_fn
+ .as_ref()
+ .ok_or_else(|| miette!("Install not implemented for debug assertion"))?;
+ install_fn
+ .call::<()>(LuaMultiValue::new())
+ .map_err(|e| miette!("Debug install function failed: {e}"))?;
+ Ok(())
+ }
+
+ fn upgrade(&self) -> Result<()> {
+ todo!()
+ }
+
+ fn remove(&self) -> Result<()> {
+ todo!()
+ }
+}
+
+// --- Lua custom assertion types ---
+
+#[derive(Clone)]
+pub struct LuaAssertionType {
+ pub display_fn: Option<LuaFunction>,
+ pub status_fn: LuaFunction,
+ pub install_fn: LuaFunction,
+}
+
+impl FromLua for LuaAssertionType {
+ fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
+ let table = LuaTable::from_lua(value, lua)?;
+
+ let display_fn: Option<LuaFunction> = table.get("display").ok();
+ let status_fn: LuaFunction = table.get("status")?;
+ let install_fn: LuaFunction = table.get("install")?;
+
+ Ok(Self {
+ display_fn,
+ status_fn,
+ install_fn,
+ })
+ }
+}
+
+pub struct LuaAssertion {
+ pub name: String,
+ pub args: LuaMultiValue,
+ pub assertion_type: LuaAssertionType,
+}
+
+impl LuaAssertion {
+ pub fn new(name: &str, args: LuaMultiValue, assertion_type: LuaAssertionType) -> Self {
+ Self {
+ name: name.to_string(),
+ args,
+ assertion_type,
+ }
+ }
+}
+
+impl std::fmt::Display for LuaAssertion {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let default_display = || {
+ let args_str = self
+ .args
+ .iter()
+ .map(|v| v.to_string().unwrap_or_else(|_| "?".to_string()))
+ .collect::<Vec<_>>()
+ .join(" ");
+ format!("{} {}", self.name, args_str)
+ };
+
+ if let Some(ref display_fn) = self.assertion_type.display_fn {
+ let result = display_fn
+ .call::<String>(self.args.clone())
+ .unwrap_or_else(|err| {
+ error!("Display function failed for {}: {}", self.name, err);
+ default_display()
+ });
+ write!(f, "{}", result)
+ } else {
+ write!(f, "{}", default_display())
+ }
+ }
+}
+
+impl AssertionType for LuaAssertion {
+ fn status(&self) -> Result<Status> {
+ let result = self
+ .assertion_type
+ .status_fn
+ .call::<Status>(self.args.clone())
+ .map_err(FrorkError::from)?;
+ Ok(result)
+ }
+
+ fn install(&self) -> Result<()> {
+ self.assertion_type
+ .install_fn
+ .call::<()>(self.args.clone())
+ .map_err(FrorkError::from)?;
+ debug!("installed: {}", self);
+ Ok(())
+ }
+
+ fn upgrade(&self) -> Result<()> {
+ todo!()
+ }
+
+ fn remove(&self) -> Result<()> {
+ todo!()
+ }
+}
diff --git a/frork-cli/src/errors.rs b/frork-cli/src/error.rs
similarity index 100%
rename from frork-cli/src/errors.rs
rename to frork-cli/src/error.rs
diff --git a/frork-cli/src/lib.rs b/frork-cli/src/lib.rs
index 08a4294..2c6ff14 100644
--- a/frork-cli/src/lib.rs
+++ b/frork-cli/src/lib.rs
@@ -1,3 +1,5 @@
+#![warn(clippy::self_named_module_files)]
+
pub mod assertions;
-pub mod errors;
+pub mod error;
pub mod utils;
diff --git a/frork-cli/src/main.rs b/frork-cli/src/main.rs
index cde9f24..aa4052c 100644
--- a/frork-cli/src/main.rs
+++ b/frork-cli/src/main.rs
@@ -1,5 +1,5 @@
mod assertions;
-mod errors;
+mod error;
mod utils;
use std::cell::RefCell;
@@ -18,9 +18,12 @@ use assertions::LuaAssertionType;
use assertions::Status;
use assertions::Symlink;
use assertions::TypedFactory;
+use clap::CommandFactory;
use clap::Parser;
use clap::Subcommand;
-use errors::FrorkError;
+use clap_complete::Shell;
+use error::FrorkError;
+use miette::IntoDiagnostic as _;
use miette::Result;
use miette::miette;
use mlua::prelude::*;
@@ -31,8 +34,12 @@ use utils::Utils;
#[command(name = "frork", version)]
#[command(about = "A Fennel-based configuration management tool")]
struct Cli {
+ /// Generate shell completions and exit.
+ #[arg(long, value_enum)]
+ completions: Option<Shell>,
+
#[command(subcommand)]
- command: Commands,
+ command: Option<Commands>,
}
#[derive(Subcommand)]
@@ -43,166 +50,83 @@ enum Commands {
Satisfy { script: String },
}
-struct LuaAssertionFactory {
- assertion_type: String,
- lua_assertion_type: LuaAssertionType,
-}
-
-impl AssertionTypeFactory for LuaAssertionFactory {
- fn create(&self, _lua: &Lua, args: LuaMultiValue) -> Result<Box<dyn AssertionType>> {
- Ok(Box::new(LuaAssertion::new(
- &self.assertion_type,
- args,
- self.lua_assertion_type.clone(),
- )))
- }
-}
+fn main() -> Result<()> {
+ tracing_subscriber::fmt::init();
-#[derive(Default)]
-struct Registry {
- lua_assertion_types: HashMap<String, LuaAssertionType>,
-}
+ let cli = Cli::parse();
-impl Registry {
- fn register(&mut self, name: &str, lua_assertion_type: LuaAssertionType) {
- self.lua_assertion_types
- .insert(name.to_string(), lua_assertion_type);
+ if let Some(shell) = cli.completions {
+ clap_complete::generate(shell, &mut Cli::command(), "frork", &mut std::io::stdout());
+ return Ok(());
}
- fn get_factory(&self, assertion_type: &str) -> Result<Box<dyn AssertionTypeFactory>> {
- // Check Lua assertions first
- if let Some(lua_assertion) = self.lua_assertion_types.get(assertion_type) {
- return Ok(Box::new(LuaAssertionFactory {
- assertion_type: assertion_type.to_string(),
- lua_assertion_type: lua_assertion.clone(),
- }));
- }
-
- // Return factory for built-in types
- match assertion_type {
- "brew" => Ok(Box::new(TypedFactory::<Brew>::new())),
- "brew-bundle" => Ok(Box::new(TypedFactory::<BrewBundle>::new())),
- "debug" => Ok(Box::new(TypedFactory::<Debug>::new())),
- "directory" => Ok(Box::new(TypedFactory::<Directory>::new())),
- "git" => Ok(Box::new(TypedFactory::<Git>::new())),
- "symlink" => Ok(Box::new(TypedFactory::<Symlink>::new())),
- _ => Err(FrorkError::UnknownAssertionType {
- assertion_type: assertion_type.to_string(),
- }
- .into()),
- }
- }
-}
+ let Some(command) = cli.command else {
+ Cli::command().print_help().into_diagnostic()?;
+ return Ok(());
+ };
-struct Frork<F> {
- // RefCell needed for interior mutability - register() method needs to add
- // new assertion types at runtime when called from Lua/Fennel code
- registry: RefCell<Registry>,
- handle_status: F,
- lua: Lua,
+ run(&command)
}
-impl<F> Frork<F> {
- fn new(handle_status: F, lua: Lua) -> Self {
- Self {
- registry: RefCell::new(Registry::default()),
- handle_status,
- lua,
- }
+fn run(command: &Commands) -> Result<()> {
+ match command {
+ Commands::Check { code } => run_code(code, status),
+ Commands::Do { code } => run_code(code, satisfy),
+ Commands::Status { script } => run_script(script, status),
+ Commands::Satisfy { script } => run_script(script, satisfy),
}
}
-impl<F> IntoLua for Frork<F>
-where
- F: Fn(&Status, &dyn AssertionType) -> Result<()> + Clone + 'static,
-{
- fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> {
- let frork_table = lua.create_table()?;
- let frork = Rc::new(self);
-
- let frork_clone = frork.clone();
- let register_fn = lua.create_function(
- move |_lua, (name, lua_assertion_type): (String, LuaAssertionType)| {
- frork_clone.register(&name, lua_assertion_type)
- },
- )?;
-
- frork_table.set("register", register_fn)?;
-
- let frork_clone = frork.clone();
- let ok_fn = lua.create_function(move |_lua, args: LuaMultiValue| frork_clone.ok(args))?;
- frork_table.set("ok", ok_fn)?;
-
- frork_table.set("utils", Utils {})?;
-
- Ok(LuaValue::Table(frork_table))
+fn status(status: &Status, assertion: &dyn AssertionType) -> Result<()> {
+ match status {
+ Status::Ok => println!("ok: {}", assertion),
+ Status::Missing => println!("missing: {}", assertion),
+ // TODO: show a nicer diff?
+ Status::ConflictUpgrade(conflict) => {
+ println!("conflict (upgradable): {}", assertion);
+ println!(" expected: {}", conflict.expected);
+ println!(" actual: {}", conflict.actual);
+ }
}
+ Ok(())
}
-impl<F> Frork<F>
-where
- F: Fn(&Status, &dyn AssertionType) -> Result<()> + Clone + 'static,
-{
- fn register(&self, name: &str, lua_assertion_type: LuaAssertionType) -> LuaResult<()> {
- self.registry
- .borrow_mut()
- .register(name, lua_assertion_type);
- info!("Registered assertion type: {}", name);
- Ok(())
- }
-
- fn ok(&self, args: LuaMultiValue) -> LuaResult<()> {
- if args.is_empty() {
- return Err(LuaError::external(FrorkError::NoOperation));
+fn satisfy(status: &Status, assertion: &dyn AssertionType) -> Result<()> {
+ match status {
+ Status::Ok => println!("ok: {}", assertion),
+ Status::Missing => {
+ println!("missing: {}", assertion);
+ assertion.install()?;
+ println!("ok: {}", assertion);
}
+ Status::ConflictUpgrade(conflict) => {
+ println!("conflict (upgradable): {}", assertion);
+ println!(" expected: {}", conflict.expected);
+ println!(" actual: {}", conflict.actual);
- let mut args_iter = args.into_iter();
- let assertion_type = args_iter
- .next()
- .and_then(|v| v.to_string().ok())
- .ok_or_else(|| LuaError::external(FrorkError::MissingAssertionType))?;
-
- let assertion_args: LuaMultiValue = args_iter.collect();
+ use std::io::Write as _;
+ print!("Upgrade? [y/N]: ");
+ std::io::stdout()
+ .flush()
+ .map_err(|e| miette!("Failed to flush stdout: {e}"))?;
- let factory = self
- .registry
- .borrow()
- .get_factory(&assertion_type)
- .map_err(LuaError::external)?;
- let assertion = factory
- .create(&self.lua, assertion_args)
- .map_err(LuaError::external)?;
- let status = assertion.status().map_err(LuaError::external)?;
+ let mut input = String::new();
+ std::io::stdin()
+ .read_line(&mut input)
+ .map_err(|e| miette!("Failed to read input: {e}"))?;
- (self.handle_status)(&status, assertion.as_ref()).map_err(LuaError::external)?;
- Ok(())
+ match input.trim().to_lowercase().as_str() {
+ "y" | "yes" => {
+ assertion.upgrade()?;
+ println!("ok: {}", assertion);
+ }
+ _ => {
+ println!("skipped: {}", assertion);
+ }
+ }
+ }
}
-}
-
-fn setup_lua(
- handle_status: impl Fn(&Status, &dyn AssertionType) -> Result<()> + Clone + 'static,
-) -> Result<(Lua, LuaTable, LuaTable)> {
- let lua = Lua::new();
-
- let fennel_code = include_str!("../fennel-1.6.0.lua");
- let fennel_module: LuaTable = lua
- .load(fennel_code)
- .eval()
- .map_err(|e| miette!("Failed to load Fennel: {e}"))?;
- lua.register_module("fennel", &fennel_module)
- .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| miette!("Failed to create frork table: {e}"))?
- {
- LuaValue::Table(table) => table,
- _ => unreachable!(),
- };
- lua.register_module("frork", &frork_table)
- .map_err(|e| miette!("Failed to register frork module: {e}"))?;
-
- Ok((lua, frork_table, fennel_module))
+ Ok(())
}
fn run_code(
@@ -235,7 +159,7 @@ fn run_script(
) -> Result<()> {
let (lua, _frork_module, fennel_module) = setup_lua(handle_status)?;
- // Add script directory to Lua and Fennel search paths
+ // Add script directory to Lua and Fennel search paths.
let script_path = std::path::Path::new(script);
if let Some(script_dir) = script_path.parent()
&& let Some(script_dir_str) = script_dir.to_str()
@@ -245,7 +169,7 @@ fn run_script(
.get("package")
.map_err(|e| miette!("Failed to get package table: {e}"))?;
- // Add to Lua search path
+ // Add to Lua search path.
let current_path: String = package_table
.get("path")
.map_err(|e| miette!("Failed to get current Lua path: {e}"))?;
@@ -257,7 +181,7 @@ fn run_script(
.set("path", new_path)
.map_err(|e| miette!("Failed to set Lua path: {e}"))?;
- // Add to Fennel search path
+ // Add to Fennel search path.
let current_fennel_path: String = fennel_module
.get("path")
.unwrap_or_else(|_| "./?.fnl;./?/init.fnl".to_string());
@@ -280,72 +204,164 @@ fn run_script(
Ok(())
}
-fn status(status: &Status, assertion: &dyn AssertionType) -> Result<()> {
- match status {
- Status::Ok => println!("ok: {}", assertion),
- Status::Missing => println!("missing: {}", assertion),
- // TODO show a nicer diff?
- Status::ConflictUpgrade(conflict) => {
- println!("conflict (upgradable): {}", assertion);
- println!(" expected: {}", conflict.expected);
- println!(" actual: {}", conflict.actual);
+fn setup_lua(
+ handle_status: impl Fn(&Status, &dyn AssertionType) -> Result<()> + Clone + 'static,
+) -> Result<(Lua, LuaTable, LuaTable)> {
+ let lua = Lua::new();
+
+ let fennel_code = include_str!("../fennel-1.6.0.lua");
+ let fennel_module: LuaTable = lua
+ .load(fennel_code)
+ .eval()
+ .map_err(|e| miette!("Failed to load Fennel: {e}"))?;
+ lua.register_module("fennel", &fennel_module)
+ .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| miette!("Failed to create frork table: {e}"))?
+ {
+ LuaValue::Table(table) => table,
+ _ => unreachable!(),
+ };
+ lua.register_module("frork", &frork_table)
+ .map_err(|e| miette!("Failed to register frork module: {e}"))?;
+
+ Ok((lua, frork_table, fennel_module))
+}
+
+struct Frork<F> {
+ // RefCell needed for interior mutability — register() adds new assertion
+ // types at runtime when called from Lua/Fennel code.
+ registry: RefCell<Registry>,
+ handle_status: F,
+ lua: Lua,
+}
+
+impl<F> Frork<F> {
+ fn new(handle_status: F, lua: Lua) -> Self {
+ Self {
+ registry: RefCell::new(Registry::default()),
+ handle_status,
+ lua,
}
}
- Ok(())
}
-fn satisfy(status: &Status, assertion: &dyn AssertionType) -> Result<()> {
- match status {
- Status::Ok => println!("ok: {}", assertion),
- Status::Missing => {
- println!("missing: {}", assertion);
- assertion.install()?;
- println!("ok: {}", assertion);
+impl<F> Frork<F>
+where
+ F: Fn(&Status, &dyn AssertionType) -> Result<()> + Clone + 'static,
+{
+ fn register(&self, name: &str, lua_assertion_type: LuaAssertionType) -> LuaResult<()> {
+ self.registry
+ .borrow_mut()
+ .register(name, lua_assertion_type);
+ info!("Registered assertion type: {}", name);
+ Ok(())
+ }
+
+ fn ok(&self, args: LuaMultiValue) -> LuaResult<()> {
+ if args.is_empty() {
+ return Err(LuaError::external(FrorkError::NoOperation));
}
- Status::ConflictUpgrade(conflict) => {
- println!("conflict (upgradable): {}", assertion);
- println!(" expected: {}", conflict.expected);
- println!(" actual: {}", conflict.actual);
- use std::io::Write as _;
- print!("Upgrade? [y/N]: ");
- std::io::stdout()
- .flush()
- .map_err(|e| miette!("Failed to flush stdout: {e}"))?;
+ let mut args_iter = args.into_iter();
+ let assertion_type = args_iter
+ .next()
+ .and_then(|v| v.to_string().ok())
+ .ok_or_else(|| LuaError::external(FrorkError::MissingAssertionType))?;
- let mut input = String::new();
- std::io::stdin()
- .read_line(&mut input)
- .map_err(|e| miette!("Failed to read input: {e}"))?;
+ let assertion_args: LuaMultiValue = args_iter.collect();
- match input.trim().to_lowercase().as_str() {
- "y" | "yes" => {
- assertion.upgrade()?;
- println!("ok: {}", assertion);
- }
- _ => {
- println!("skipped: {}", assertion);
- }
- }
- }
+ let factory = self
+ .registry
+ .borrow()
+ .get_factory(&assertion_type)
+ .map_err(LuaError::external)?;
+ let assertion = factory
+ .create(&self.lua, assertion_args)
+ .map_err(LuaError::external)?;
+ let status = assertion.status().map_err(LuaError::external)?;
+
+ (self.handle_status)(&status, assertion.as_ref()).map_err(LuaError::external)?;
+ Ok(())
}
- Ok(())
}
-fn run(command: &Commands) -> Result<()> {
- match command {
- Commands::Check { code } => run_code(code, status),
- Commands::Do { code } => run_code(code, satisfy),
- Commands::Status { script } => run_script(script, status),
- Commands::Satisfy { script } => run_script(script, satisfy),
+impl<F> IntoLua for Frork<F>
+where
+ F: Fn(&Status, &dyn AssertionType) -> Result<()> + Clone + 'static,
+{
+ fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> {
+ let frork_table = lua.create_table()?;
+ let frork = Rc::new(self);
+
+ let frork_clone = frork.clone();
+ let register_fn = lua.create_function(
+ move |_lua, (name, lua_assertion_type): (String, LuaAssertionType)| {
+ frork_clone.register(&name, lua_assertion_type)
+ },
+ )?;
+
+ frork_table.set("register", register_fn)?;
+
+ let frork_clone = frork.clone();
+ let ok_fn = lua.create_function(move |_lua, args: LuaMultiValue| frork_clone.ok(args))?;
+ frork_table.set("ok", ok_fn)?;
+
+ frork_table.set("utils", Utils {})?;
+
+ Ok(LuaValue::Table(frork_table))
}
}
-fn main() -> Result<()> {
- tracing_subscriber::fmt::init();
+#[derive(Default)]
+struct Registry {
+ lua_assertion_types: HashMap<String, LuaAssertionType>,
+}
- let cli = Cli::parse();
- run(&cli.command)?;
+impl Registry {
+ fn register(&mut self, name: &str, lua_assertion_type: LuaAssertionType) {
+ self.lua_assertion_types
+ .insert(name.to_string(), lua_assertion_type);
+ }
- Ok(())
+ fn get_factory(&self, assertion_type: &str) -> Result<Box<dyn AssertionTypeFactory>> {
+ // Check Lua assertions first.
+ if let Some(lua_assertion) = self.lua_assertion_types.get(assertion_type) {
+ return Ok(Box::new(LuaAssertionFactory {
+ assertion_type: assertion_type.to_string(),
+ lua_assertion_type: lua_assertion.clone(),
+ }));
+ }
+
+ // Return factory for built-in types.
+ match assertion_type {
+ "brew" => Ok(Box::new(TypedFactory::<Brew>::new())),
+ "brew-bundle" => Ok(Box::new(TypedFactory::<BrewBundle>::new())),
+ "debug" => Ok(Box::new(TypedFactory::<Debug>::new())),
+ "directory" => Ok(Box::new(TypedFactory::<Directory>::new())),
+ "git" => Ok(Box::new(TypedFactory::<Git>::new())),
+ "symlink" => Ok(Box::new(TypedFactory::<Symlink>::new())),
+ _ => Err(FrorkError::UnknownAssertionType {
+ assertion_type: assertion_type.to_string(),
+ }
+ .into()),
+ }
+ }
+}
+
+struct LuaAssertionFactory {
+ assertion_type: String,
+ lua_assertion_type: LuaAssertionType,
+}
+
+impl AssertionTypeFactory for LuaAssertionFactory {
+ fn create(&self, _lua: &Lua, args: LuaMultiValue) -> Result<Box<dyn AssertionType>> {
+ Ok(Box::new(LuaAssertion::new(
+ &self.assertion_type,
+ args,
+ self.lua_assertion_type.clone(),
+ )))
+ }
}
diff --git a/frork-cli/src/utils.rs b/frork-cli/src/utils.rs
index 2d88a38..95ca14b 100644
--- a/frork-cli/src/utils.rs
+++ b/frork-cli/src/utils.rs
@@ -10,7 +10,7 @@ use std::process::Command;
use std::sync::LazyLock;
use tracing::debug;
-use crate::errors::FrorkError;
+use crate::error::FrorkError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExpandedPath(String);
diff --git a/frork-cli/tests/cli.rs b/frork-cli/tests/cli.rs
index 32810cc..86fcfbd 100644
--- a/frork-cli/tests/cli.rs
+++ b/frork-cli/tests/cli.rs
@@ -1,5 +1,6 @@
use assert_cmd::Command;
use assert_cmd::cargo::cargo_bin_cmd;
+use predicates::str::contains;
fn cmd() -> Command {
Command::from(cargo_bin_cmd!("frork"))
@@ -9,3 +10,26 @@ fn cmd() -> Command {
fn shows_help() {
cmd().arg("--help").assert().success();
}
+
+#[test]
+fn no_args_shows_help() {
+ cmd().assert().success().stdout(contains("Usage"));
+}
+
+#[test]
+fn generates_bash_completions() {
+ cmd()
+ .args(["--completions", "bash"])
+ .assert()
+ .success()
+ .stdout(contains("frork"));
+}
+
+#[test]
+fn generates_zsh_completions() {
+ cmd()
+ .args(["--completions", "zsh"])
+ .assert()
+ .success()
+ .stdout(contains("frork"));
+}