From ec2c45b0dadaebcffebdc39ab51699bc8c9f8f85 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 17 Dec 2025 21:49:56 +0000 Subject: [PATCH 01/16] feat(vfox): embed vfox plugin Lua code in binary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Embed vfox plugin Lua code directly into the mise binary at build time for tools where vfox is the first/default backend. This eliminates git clone operations for 8 tools. Embedded plugins: - vfox-aapt2 - vfox-ag - vfox-android-sdk - vfox-ant - vfox-bfs - vfox-bpkg - vfox-chicken - vfox-vlang Implementation: - Add git submodules for plugin repos - Build-time code generation via build.rs using include_str!() - Plugin struct supports both filesystem and embedded sources - Embedded plugins skip git clone and are always "installed" - Updates to embedded plugins ship with mise releases 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .gitmodules | 24 +++ crates/vfox/Cargo.toml | 3 +- crates/vfox/build.rs | 192 ++++++++++++++++++ crates/vfox/embedded-plugins/vfox-aapt2 | 1 + crates/vfox/embedded-plugins/vfox-ag | 1 + crates/vfox/embedded-plugins/vfox-android-sdk | 1 + crates/vfox/embedded-plugins/vfox-ant | 1 + crates/vfox/embedded-plugins/vfox-bfs | 1 + crates/vfox/embedded-plugins/vfox-bpkg | 1 + crates/vfox/embedded-plugins/vfox-chicken | 1 + crates/vfox/embedded-plugins/vfox-vlang | 1 + crates/vfox/src/embedded_plugins.rs | 4 + crates/vfox/src/lib.rs | 1 + crates/vfox/src/lua_mod/hooks.rs | 36 +++- crates/vfox/src/lua_mod/mod.rs | 1 + crates/vfox/src/plugin.rs | 116 +++++++++-- crates/vfox/src/vfox.rs | 7 +- src/plugins/vfox_plugin.rs | 23 ++- 18 files changed, 390 insertions(+), 25 deletions(-) create mode 100644 .gitmodules create mode 100644 crates/vfox/build.rs create mode 160000 crates/vfox/embedded-plugins/vfox-aapt2 create mode 160000 crates/vfox/embedded-plugins/vfox-ag create mode 160000 crates/vfox/embedded-plugins/vfox-android-sdk create mode 160000 crates/vfox/embedded-plugins/vfox-ant create mode 160000 crates/vfox/embedded-plugins/vfox-bfs create mode 160000 crates/vfox/embedded-plugins/vfox-bpkg create mode 160000 crates/vfox/embedded-plugins/vfox-chicken create mode 160000 crates/vfox/embedded-plugins/vfox-vlang create mode 100644 crates/vfox/src/embedded_plugins.rs diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..4f3fbc88f7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,24 @@ +[submodule "crates/vfox/embedded-plugins/vfox-aapt2"] + path = crates/vfox/embedded-plugins/vfox-aapt2 + url = https://github.com/mise-plugins/vfox-aapt2.git +[submodule "crates/vfox/embedded-plugins/vfox-ag"] + path = crates/vfox/embedded-plugins/vfox-ag + url = https://github.com/mise-plugins/vfox-ag.git +[submodule "crates/vfox/embedded-plugins/vfox-android-sdk"] + path = crates/vfox/embedded-plugins/vfox-android-sdk + url = https://github.com/mise-plugins/vfox-android-sdk.git +[submodule "crates/vfox/embedded-plugins/vfox-ant"] + path = crates/vfox/embedded-plugins/vfox-ant + url = https://github.com/mise-plugins/vfox-ant.git +[submodule "crates/vfox/embedded-plugins/vfox-bfs"] + path = crates/vfox/embedded-plugins/vfox-bfs + url = https://github.com/mise-plugins/vfox-bfs.git +[submodule "crates/vfox/embedded-plugins/vfox-bpkg"] + path = crates/vfox/embedded-plugins/vfox-bpkg + url = https://github.com/mise-plugins/vfox-bpkg.git +[submodule "crates/vfox/embedded-plugins/vfox-chicken"] + path = crates/vfox/embedded-plugins/vfox-chicken + url = https://github.com/mise-plugins/vfox-chicken.git +[submodule "crates/vfox/embedded-plugins/vfox-vlang"] + path = crates/vfox/embedded-plugins/vfox-vlang + url = https://github.com/mise-plugins/vfox-vlang.git diff --git a/crates/vfox/Cargo.toml b/crates/vfox/Cargo.toml index ce16c1cb2b..fb6fb42fb5 100644 --- a/crates/vfox/Cargo.toml +++ b/crates/vfox/Cargo.toml @@ -7,7 +7,8 @@ description = "Interface to vfox plugins" documentation = "https://docs.rs/vfox" homepage = "https://github.com/jdx/mise" repository = "https://github.com/jdx/mise" -include = ["src", "lua", "Cargo.toml", "Cargo.lock", "README.md", "LICENSE"] +include = ["src", "lua", "embedded-plugins", "build.rs", "Cargo.toml", "Cargo.lock", "README.md", "LICENSE"] +build = "build.rs" [lib] name = "vfox" diff --git a/crates/vfox/build.rs b/crates/vfox/build.rs new file mode 100644 index 0000000000..c7349beb82 --- /dev/null +++ b/crates/vfox/build.rs @@ -0,0 +1,192 @@ +use std::collections::BTreeMap; +use std::env; +use std::fs; +use std::path::Path; + +fn main() { + codegen_embedded_plugins(); +} + +fn codegen_embedded_plugins() { + let out_dir = env::var_os("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("embedded_plugins.rs"); + + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let embedded_dir = Path::new(&manifest_dir).join("embedded-plugins"); + + // Tell Cargo to re-run if any embedded plugin files change + println!("cargo:rerun-if-changed=embedded-plugins"); + + if !embedded_dir.exists() { + // Generate empty implementation if no embedded plugins + let code = r#" +#[derive(Debug)] +pub struct EmbeddedPlugin { + pub metadata: &'static str, + pub hooks: &'static [(&'static str, &'static str)], + pub lib: &'static [(&'static str, &'static str)], +} + +pub fn get_embedded_plugin(_name: &str) -> Option<&'static EmbeddedPlugin> { + None +} + +pub fn list_embedded_plugins() -> &'static [&'static str] { + &[] +} +"#; + fs::write(&dest_path, code).unwrap(); + return; + } + + let mut plugins: BTreeMap = BTreeMap::new(); + + // Scan for plugin directories + for entry in fs::read_dir(&embedded_dir).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if !path.is_dir() { + continue; + } + + let dir_name = path.file_name().unwrap().to_string_lossy().to_string(); + if !dir_name.starts_with("vfox-") { + continue; + } + + // Tell Cargo to re-run if this plugin directory changes + println!("cargo:rerun-if-changed={}", path.display()); + + let plugin = collect_plugin_files(&path); + plugins.insert(dir_name, plugin); + } + + // Generate Rust code + let mut code = String::new(); + + // Struct definition + code.push_str( + r#" +#[derive(Debug)] +pub struct EmbeddedPlugin { + pub metadata: &'static str, + pub hooks: &'static [(&'static str, &'static str)], + pub lib: &'static [(&'static str, &'static str)], +} + +"#, + ); + + // Generate static instances for each plugin + for (name, files) in &plugins { + let var_name = name.replace('-', "_").to_uppercase(); + code.push_str(&format!( + "static {var_name}: EmbeddedPlugin = EmbeddedPlugin {{\n" + )); + + // Metadata - use absolute path + let metadata_path = embedded_dir.join(name).join("metadata.lua"); + code.push_str(&format!( + " metadata: include_str!(\"{}\"),\n", + metadata_path.display() + )); + + // Hooks + code.push_str(" hooks: &[\n"); + for hook in &files.hooks { + let hook_path = embedded_dir + .join(name) + .join("hooks") + .join(format!("{}.lua", hook)); + code.push_str(&format!( + " (\"{}\", include_str!(\"{}\")),\n", + hook, + hook_path.display() + )); + } + code.push_str(" ],\n"); + + // Lib files + code.push_str(" lib: &[\n"); + for lib in &files.lib { + let lib_path = embedded_dir + .join(name) + .join("lib") + .join(format!("{}.lua", lib)); + code.push_str(&format!( + " (\"{}\", include_str!(\"{}\")),\n", + lib, + lib_path.display() + )); + } + code.push_str(" ],\n"); + + code.push_str("};\n\n"); + } + + // Generate lookup function + code.push_str("pub fn get_embedded_plugin(name: &str) -> Option<&'static EmbeddedPlugin> {\n"); + code.push_str(" match name {\n"); + for name in plugins.keys() { + let var_name = name.replace('-', "_").to_uppercase(); + let short_name = name.strip_prefix("vfox-").unwrap_or(name); + code.push_str(&format!( + " \"{}\" | \"{}\" => Some(&{}),\n", + name, short_name, var_name + )); + } + code.push_str(" _ => None,\n"); + code.push_str(" }\n"); + code.push_str("}\n\n"); + + // Generate list function + code.push_str("pub fn list_embedded_plugins() -> &'static [&'static str] {\n"); + code.push_str(" &[\n"); + for name in plugins.keys() { + code.push_str(&format!(" \"{}\",\n", name)); + } + code.push_str(" ]\n"); + code.push_str("}\n"); + + fs::write(&dest_path, code).unwrap(); +} + +struct PluginFiles { + hooks: Vec, + lib: Vec, +} + +fn collect_plugin_files(plugin_dir: &Path) -> PluginFiles { + let mut hooks = Vec::new(); + let mut lib = Vec::new(); + + // Collect hooks + let hooks_dir = plugin_dir.join("hooks"); + if hooks_dir.exists() { + for entry in fs::read_dir(&hooks_dir).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.extension().is_some_and(|ext| ext == "lua") { + let name = path.file_stem().unwrap().to_string_lossy().to_string(); + hooks.push(name); + } + } + } + hooks.sort(); + + // Collect lib files + let lib_dir = plugin_dir.join("lib"); + if lib_dir.exists() { + for entry in fs::read_dir(&lib_dir).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.extension().is_some_and(|ext| ext == "lua") { + let name = path.file_stem().unwrap().to_string_lossy().to_string(); + lib.push(name); + } + } + } + lib.sort(); + + PluginFiles { hooks, lib } +} diff --git a/crates/vfox/embedded-plugins/vfox-aapt2 b/crates/vfox/embedded-plugins/vfox-aapt2 new file mode 160000 index 0000000000..144a5eafe7 --- /dev/null +++ b/crates/vfox/embedded-plugins/vfox-aapt2 @@ -0,0 +1 @@ +Subproject commit 144a5eafe7a76e954e91e8a9ca7aae99b895deda diff --git a/crates/vfox/embedded-plugins/vfox-ag b/crates/vfox/embedded-plugins/vfox-ag new file mode 160000 index 0000000000..1ca4f81221 --- /dev/null +++ b/crates/vfox/embedded-plugins/vfox-ag @@ -0,0 +1 @@ +Subproject commit 1ca4f81221b51a0d68de1bb958dede5762d6c08f diff --git a/crates/vfox/embedded-plugins/vfox-android-sdk b/crates/vfox/embedded-plugins/vfox-android-sdk new file mode 160000 index 0000000000..5f8b78d944 --- /dev/null +++ b/crates/vfox/embedded-plugins/vfox-android-sdk @@ -0,0 +1 @@ +Subproject commit 5f8b78d944f63f0c78775916aad0808a524148ae diff --git a/crates/vfox/embedded-plugins/vfox-ant b/crates/vfox/embedded-plugins/vfox-ant new file mode 160000 index 0000000000..0254ebce47 --- /dev/null +++ b/crates/vfox/embedded-plugins/vfox-ant @@ -0,0 +1 @@ +Subproject commit 0254ebce476d03b703604bd876d69f5026447c1a diff --git a/crates/vfox/embedded-plugins/vfox-bfs b/crates/vfox/embedded-plugins/vfox-bfs new file mode 160000 index 0000000000..165106df1d --- /dev/null +++ b/crates/vfox/embedded-plugins/vfox-bfs @@ -0,0 +1 @@ +Subproject commit 165106df1da197070e2d56edf0661f1cb2e2699d diff --git a/crates/vfox/embedded-plugins/vfox-bpkg b/crates/vfox/embedded-plugins/vfox-bpkg new file mode 160000 index 0000000000..365ac71fbd --- /dev/null +++ b/crates/vfox/embedded-plugins/vfox-bpkg @@ -0,0 +1 @@ +Subproject commit 365ac71fbdfdd9372dcd42796a9bdb1b67171370 diff --git a/crates/vfox/embedded-plugins/vfox-chicken b/crates/vfox/embedded-plugins/vfox-chicken new file mode 160000 index 0000000000..85ce8f79ed --- /dev/null +++ b/crates/vfox/embedded-plugins/vfox-chicken @@ -0,0 +1 @@ +Subproject commit 85ce8f79ed99057a3415627c94af28e49781feec diff --git a/crates/vfox/embedded-plugins/vfox-vlang b/crates/vfox/embedded-plugins/vfox-vlang new file mode 160000 index 0000000000..61dee71718 --- /dev/null +++ b/crates/vfox/embedded-plugins/vfox-vlang @@ -0,0 +1 @@ +Subproject commit 61dee71718b40c42314220cf03a34977c341d18c diff --git a/crates/vfox/src/embedded_plugins.rs b/crates/vfox/src/embedded_plugins.rs new file mode 100644 index 0000000000..5a7b180f41 --- /dev/null +++ b/crates/vfox/src/embedded_plugins.rs @@ -0,0 +1,4 @@ +// This module provides access to embedded vfox plugin Lua code. +// The actual code is generated at build time by build.rs + +include!(concat!(env!("OUT_DIR"), "/embedded_plugins.rs")); diff --git a/crates/vfox/src/lib.rs b/crates/vfox/src/lib.rs index ea2fcf4886..19f659c117 100644 --- a/crates/vfox/src/lib.rs +++ b/crates/vfox/src/lib.rs @@ -13,6 +13,7 @@ pub use vfox::Vfox; mod config; mod context; +pub mod embedded_plugins; mod error; mod hooks; mod http; diff --git a/crates/vfox/src/lua_mod/hooks.rs b/crates/vfox/src/lua_mod/hooks.rs index aef1f5ae37..573067c98f 100644 --- a/crates/vfox/src/lua_mod/hooks.rs +++ b/crates/vfox/src/lua_mod/hooks.rs @@ -1,3 +1,4 @@ +use crate::embedded_plugins::EmbeddedPlugin; use crate::error::Result; use mlua::Lua; use std::collections::BTreeSet; @@ -5,11 +6,11 @@ use std::path::Path; pub struct HookFunc { _name: &'static str, - filename: &'static str, + pub filename: &'static str, } #[rustfmt::skip] -const HOOK_FUNCS: [HookFunc; 12] = [ +pub const HOOK_FUNCS: [HookFunc; 12] = [ HookFunc { _name: "Available", filename: "available" }, HookFunc { _name: "PreInstall", filename: "pre_install" }, HookFunc { _name: "EnvKeys", filename: "env_keys" }, @@ -22,7 +23,7 @@ const HOOK_FUNCS: [HookFunc; 12] = [ HookFunc { _name: "BackendListVersions", filename: "backend_list_versions" }, HookFunc { _name: "BackendInstall", filename: "backend_install" }, HookFunc { _name: "BackendExecEnv", filename: "backend_exec_env" }, - + // mise HookFunc { _name: "MiseEnv", filename: "mise_env" }, HookFunc { _name: "MisePath", filename: "mise_path" }, @@ -39,3 +40,32 @@ pub fn mod_hooks(lua: &Lua, root: &Path) -> Result> { } Ok(hooks) } + +pub fn hooks_embedded(lua: &Lua, embedded: &EmbeddedPlugin) -> Result> { + let mut hooks = BTreeSet::new(); + + // Get package.loaded table to preload hooks + let package: mlua::Table = lua.globals().get("package")?; + let loaded: mlua::Table = package.get("loaded")?; + + for (hook_name, hook_code) in embedded.hooks { + // Execute the hook code to define the function + lua.load(*hook_code).exec()?; + + // Also preload into package.loaded so require("hooks/") works + // The hook code typically defines a function on the PLUGIN table + // We need to register a module that can be required + let module_name = format!("hooks/{}", hook_name); + // Create a simple module that returns true (the hook code has already been executed) + loaded.set(module_name, true)?; + + // Find the matching hook filename from HOOK_FUNCS + for hook in &HOOK_FUNCS { + if hook.filename == *hook_name { + hooks.insert(hook.filename); + break; + } + } + } + Ok(hooks) +} diff --git a/crates/vfox/src/lua_mod/mod.rs b/crates/vfox/src/lua_mod/mod.rs index 9e40178a18..e0f75ac75d 100644 --- a/crates/vfox/src/lua_mod/mod.rs +++ b/crates/vfox/src/lua_mod/mod.rs @@ -12,6 +12,7 @@ pub use archiver::mod_archiver as archiver; pub use cmd::mod_cmd as cmd; pub use env::mod_env as env; pub use file::mod_file as file; +pub use hooks::hooks_embedded; pub use hooks::mod_hooks as hooks; pub use html::mod_html as html; pub use http::mod_http as http; diff --git a/crates/vfox/src/plugin.rs b/crates/vfox/src/plugin.rs index 80e5378323..4f8c13bb41 100644 --- a/crates/vfox/src/plugin.rs +++ b/crates/vfox/src/plugin.rs @@ -7,16 +7,24 @@ use once_cell::sync::OnceCell; use crate::config::Config; use crate::context::Context; +use crate::embedded_plugins::{self, EmbeddedPlugin}; use crate::error::Result; use crate::metadata::Metadata; use crate::runtime::Runtime; use crate::sdk_info::SdkInfo; use crate::{VfoxError, config, error, lua_mod}; +#[derive(Debug)] +pub enum PluginSource { + Filesystem(PathBuf), + Embedded(&'static EmbeddedPlugin), +} + #[derive(Debug)] pub struct Plugin { pub name: String, pub dir: PathBuf, + source: PluginSource, lua: Lua, metadata: OnceCell, } @@ -31,16 +39,48 @@ impl Plugin { Ok(Self { name: dir.file_name().unwrap().to_string_lossy().to_string(), dir: dir.to_path_buf(), + source: PluginSource::Filesystem(dir.to_path_buf()), + lua, + metadata: OnceCell::new(), + }) + } + + pub fn from_embedded(name: &str, embedded: &'static EmbeddedPlugin) -> Result { + let lua = Lua::new(); + // Use a dummy path for embedded plugins + let dummy_dir = PathBuf::from(format!("embedded:{}", name)); + lua.set_named_registry_value("plugin_dir", dummy_dir.clone())?; + lua.set_named_registry_value("embedded_plugin", true)?; + Ok(Self { + name: name.to_string(), + dir: dummy_dir, + source: PluginSource::Embedded(embedded), lua, metadata: OnceCell::new(), }) } pub fn from_name(name: &str) -> Result { + // Check for embedded plugin first + if let Some(embedded) = embedded_plugins::get_embedded_plugin(name) { + return Self::from_embedded(name, embedded); + } let dir = Config::get().plugin_dir.join(name); Self::from_dir(&dir) } + pub fn from_name_or_dir(name: &str, dir: &Path) -> Result { + // Check for embedded plugin first + if let Some(embedded) = embedded_plugins::get_embedded_plugin(name) { + return Self::from_embedded(name, embedded); + } + Self::from_dir(dir) + } + + pub fn is_embedded(&self) -> bool { + matches!(self.source, PluginSource::Embedded(_)) + } + pub fn list() -> Result> { let config = Config::get(); if !config.plugin_dir.exists() { @@ -107,15 +147,21 @@ impl Plugin { fn load(&self) -> Result<&Metadata> { self.metadata.get_or_try_init(|| { debug!("[vfox] Getting metadata for {self}"); - set_paths( - &self.lua, - &[ - self.dir.join("?.lua"), //xx - self.dir.join("hooks/?.lua"), - self.dir.join("lib/?.lua"), - ], - )?; + // For filesystem plugins, set Lua package paths + if let PluginSource::Filesystem(dir) = &self.source { + set_paths( + &self.lua, + &[ + dir.join("?.lua"), + dir.join("hooks/?.lua"), + dir.join("lib/?.lua"), + ], + )?; + } + + // Load standard Lua modules (http, json, etc.) FIRST + // These must be available before loading embedded lib files lua_mod::archiver(&self.lua)?; lua_mod::cmd(&self.lua)?; lua_mod::file(&self.lua)?; @@ -125,6 +171,12 @@ impl Plugin { lua_mod::strings(&self.lua)?; lua_mod::env(&self.lua)?; + // For embedded plugins, load lib modules AFTER standard modules + // (lib files may require http, json, etc.) + if let PluginSource::Embedded(embedded) = &self.source { + self.load_embedded_libs(embedded)?; + } + let metadata = self.load_metadata()?; self.set_global("PLUGIN", metadata.clone())?; self.set_global("RUNTIME", Runtime::get(self.dir.clone()))?; @@ -133,12 +185,28 @@ impl Plugin { let mut metadata: Metadata = metadata.try_into()?; - metadata.hooks = lua_mod::hooks(&self.lua, &self.dir)?; + metadata.hooks = match &self.source { + PluginSource::Filesystem(dir) => lua_mod::hooks(&self.lua, dir)?, + PluginSource::Embedded(embedded) => lua_mod::hooks_embedded(&self.lua, embedded)?, + }; Ok(metadata) }) } + fn load_embedded_libs(&self, embedded: &EmbeddedPlugin) -> Result<()> { + let package: Table = self.lua.globals().get("package")?; + let loaded: Table = package.get("loaded")?; + + // Load lib modules + for (name, code) in embedded.lib { + let module: Table = self.lua.load(*code).eval()?; + loaded.set(*name, module)?; + } + + Ok(()) + } + fn set_global(&self, name: &str, value: V) -> Result<()> where V: IntoLua, @@ -148,16 +216,26 @@ impl Plugin { } fn load_metadata(&self) -> Result { - let metadata = self - .lua - .load( - r#" - require "metadata" - return PLUGIN - "#, - ) - .eval()?; - Ok(metadata) + match &self.source { + PluginSource::Filesystem(_) => { + let metadata = self + .lua + .load( + r#" + require "metadata" + return PLUGIN + "#, + ) + .eval()?; + Ok(metadata) + } + PluginSource::Embedded(embedded) => { + // Load metadata from embedded string + self.lua.load(embedded.metadata).exec()?; + let metadata = self.lua.globals().get("PLUGIN")?; + Ok(metadata) + } + } } } diff --git a/crates/vfox/src/vfox.rs b/crates/vfox/src/vfox.rs index 283cbd0e2e..ace9cf0e30 100644 --- a/crates/vfox/src/vfox.rs +++ b/crates/vfox/src/vfox.rs @@ -99,10 +99,15 @@ impl Vfox { } pub fn get_sdk(&self, name: &str) -> Result { - Plugin::from_dir(&self.plugin_dir.join(name)) + Plugin::from_name_or_dir(name, &self.plugin_dir.join(name)) } pub fn install_plugin(&self, sdk: &str) -> Result { + // Check for embedded plugin first - no installation needed + if let Some(embedded) = crate::embedded_plugins::get_embedded_plugin(sdk) { + return Plugin::from_embedded(sdk, embedded); + } + let plugin_dir = self.plugin_dir.join(sdk); if !plugin_dir.exists() { let url = registry::sdk_url(sdk).ok_or_else(|| format!("Unknown SDK: {sdk}"))?; diff --git a/src/plugins/vfox_plugin.rs b/src/plugins/vfox_plugin.rs index 7535e07962..feb6cb8c86 100644 --- a/src/plugins/vfox_plugin.rs +++ b/src/plugins/vfox_plugin.rs @@ -15,6 +15,7 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, MutexGuard, mpsc}; use url::Url; use vfox::Vfox; +use vfox::embedded_plugins; use xx::regex; #[derive(Debug)] @@ -108,6 +109,10 @@ impl VfoxPlugin { )?; Ok(()) } + + pub fn is_embedded(&self) -> bool { + embedded_plugins::get_embedded_plugin(&self.name).is_some() + } } #[async_trait] @@ -144,7 +149,8 @@ impl Plugin for VfoxPlugin { } fn is_installed(&self) -> bool { - self.plugin_path.exists() + // Embedded plugins are always "installed" + self.is_embedded() || self.plugin_path.exists() } fn is_installed_err(&self) -> eyre::Result<()> { @@ -162,6 +168,11 @@ impl Plugin for VfoxPlugin { _force: bool, dry_run: bool, ) -> Result<()> { + // Skip installation for embedded plugins + if self.is_embedded() { + return Ok(()); + } + if !self.plugin_path.exists() { let url = self.get_repo_url(config)?; trace!("Cloning vfox plugin: {url}"); @@ -175,6 +186,16 @@ impl Plugin for VfoxPlugin { } async fn update(&self, pr: &dyn SingleReport, gitref: Option) -> Result<()> { + // Embedded plugins cannot be updated - they're bundled with mise + if self.is_embedded() { + warn!( + "plugin:{} is embedded in mise, not updating", + style(&self.name).blue().for_stderr() + ); + pr.finish_with_message("embedded plugin".into()); + return Ok(()); + } + let plugin_path = self.plugin_path.to_path_buf(); if plugin_path.is_symlink() { warn!( From dd9649957dfa9c6d63203dafdfda1478733e9f14 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 17 Dec 2025 21:57:57 +0000 Subject: [PATCH 02/16] ci: add submodules checkout for embedded vfox plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update GitHub Actions workflows to checkout git submodules, which are needed for the embedded vfox plugins feature. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/release.yml | 6 ++++++ .github/workflows/test.yml | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b44ab406c6..837107f477 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,6 +45,8 @@ jobs: target: armv7-unknown-linux-musleabi steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - name: Install cross uses: taiki-e/install-action@0aa4f22591557b744fe31e55dbfcdfea74a073f7 # v2 with: @@ -96,6 +98,8 @@ jobs: p12-file-base64: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12 }} p12-password: ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTS_P12_PASS }} - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - name: cache crates id: cache-crates uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 @@ -138,6 +142,8 @@ jobs: target: x86_64-pc-windows-msvc steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - run: rustup target add ${{matrix.target}} - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9d68831402..c579be2145 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,6 +29,8 @@ jobs: timeout-minutes: 60 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 with: shared-key: build @@ -54,6 +56,8 @@ jobs: timeout-minutes: 60 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 with: shared-key: build @@ -82,6 +86,8 @@ jobs: MISE_CACHE_DIR: ~/.cache/mise steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 with: shared-key: build @@ -118,6 +124,7 @@ jobs: with: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.head_ref }} + submodules: true - uses: taiki-e/install-action@0aa4f22591557b744fe31e55dbfcdfea74a073f7 # v2 with: tool: cargo-deny,cargo-msrv,cargo-machete @@ -155,6 +162,7 @@ jobs: with: repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.head_ref }} + submodules: true - run: rustup default nightly - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 with: @@ -185,6 +193,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 + submodules: true - name: Install build and test dependencies run: | sudo apt-get update @@ -234,6 +243,8 @@ jobs: MISE_CACHE_DIR: ~/.cache/mise steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 with: shared-key: unit @@ -253,6 +264,8 @@ jobs: MISE_CACHE_DIR: ~/.cache/mise steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: mise-windows-latest From ac83d2b16f9434b044cbffe6007b081e3f1184ba Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 17 Dec 2025 22:02:58 +0000 Subject: [PATCH 03/16] fix(vfox): use forward slashes in include_str! paths for Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows, path.display() outputs backslashes which are escape characters in Rust string literals, causing include_str! to fail. Convert all paths to forward slashes for cross-platform compatibility. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crates/vfox/build.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/vfox/build.rs b/crates/vfox/build.rs index c7349beb82..7d551ce570 100644 --- a/crates/vfox/build.rs +++ b/crates/vfox/build.rs @@ -7,6 +7,11 @@ fn main() { codegen_embedded_plugins(); } +/// Convert a path to a string with forward slashes (required for include_str! on Windows) +fn path_to_forward_slashes(path: &Path) -> String { + path.to_string_lossy().replace('\\', "/") +} + fn codegen_embedded_plugins() { let out_dir = env::var_os("OUT_DIR").unwrap(); let dest_path = Path::new(&out_dir).join("embedded_plugins.rs"); @@ -84,11 +89,11 @@ pub struct EmbeddedPlugin { "static {var_name}: EmbeddedPlugin = EmbeddedPlugin {{\n" )); - // Metadata - use absolute path + // Metadata - use absolute path with forward slashes for cross-platform include_str! let metadata_path = embedded_dir.join(name).join("metadata.lua"); code.push_str(&format!( " metadata: include_str!(\"{}\"),\n", - metadata_path.display() + path_to_forward_slashes(&metadata_path) )); // Hooks @@ -101,7 +106,7 @@ pub struct EmbeddedPlugin { code.push_str(&format!( " (\"{}\", include_str!(\"{}\")),\n", hook, - hook_path.display() + path_to_forward_slashes(&hook_path) )); } code.push_str(" ],\n"); @@ -116,7 +121,7 @@ pub struct EmbeddedPlugin { code.push_str(&format!( " (\"{}\", include_str!(\"{}\")),\n", lib, - lib_path.display() + path_to_forward_slashes(&lib_path) )); } code.push_str(" ],\n"); From c2b8fd3eb0f2bb20b86515183a3194fcd8303a0b Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 17 Dec 2025 22:10:07 +0000 Subject: [PATCH 04/16] fix(vfox): add embedded plugin check to uninstall method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Embedded plugins cannot be uninstalled since they're bundled with mise. Now shows a warning like update() and ensure_installed() do, instead of silently succeeding while the plugin continues to work. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/plugins/vfox_plugin.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/plugins/vfox_plugin.rs b/src/plugins/vfox_plugin.rs index feb6cb8c86..520c4828b0 100644 --- a/src/plugins/vfox_plugin.rs +++ b/src/plugins/vfox_plugin.rs @@ -227,6 +227,15 @@ impl Plugin for VfoxPlugin { if !self.is_installed() { return Ok(()); } + // Embedded plugins cannot be uninstalled - they're bundled with mise + if self.is_embedded() { + warn!( + "plugin:{} is embedded in mise, cannot uninstall", + style(&self.name).blue().for_stderr() + ); + pr.finish_with_message("embedded plugin".into()); + return Ok(()); + } pr.set_message("uninstall".into()); let rmdir = |dir: &Path| { From 5b749c0c31193b6b0fcd00e09e146fb036abe1b9 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 17 Dec 2025 22:47:17 +0000 Subject: [PATCH 05/16] feat(vfox): allow installed plugins to override embedded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Filesystem plugins now take priority over embedded plugins - Updated from_name_or_dir() to check filesystem first - uninstall() only warns when no filesystem plugin exists - Added e2e test for embedded plugin override behavior 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crates/vfox/src/plugin.rs | 6 +++++- e2e/backend/test_vfox_embedded_override_slow | 21 ++++++++++++++++++++ src/plugins/vfox_plugin.rs | 4 ++-- 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 e2e/backend/test_vfox_embedded_override_slow diff --git a/crates/vfox/src/plugin.rs b/crates/vfox/src/plugin.rs index 4f8c13bb41..15f3a6f73e 100644 --- a/crates/vfox/src/plugin.rs +++ b/crates/vfox/src/plugin.rs @@ -70,7 +70,11 @@ impl Plugin { } pub fn from_name_or_dir(name: &str, dir: &Path) -> Result { - // Check for embedded plugin first + // Check filesystem first - allows user to override embedded plugins + if dir.exists() { + return Self::from_dir(dir); + } + // Fall back to embedded plugin if available if let Some(embedded) = embedded_plugins::get_embedded_plugin(name) { return Self::from_embedded(name, embedded); } diff --git a/e2e/backend/test_vfox_embedded_override_slow b/e2e/backend/test_vfox_embedded_override_slow new file mode 100644 index 0000000000..249eb3bf32 --- /dev/null +++ b/e2e/backend/test_vfox_embedded_override_slow @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +# Test that embedded vfox plugins can be overridden by installing a plugin + +# Verify embedded plugin works without installation - use the short "bfs" name +# which goes through the versions host cache (doesn't require network to the plugin) +assert_contains "mise ls-remote bfs" "3.4" + +# Install a different plugin with the same name to override +# Using mise-tiny as a stand-in since it's a simple test plugin +mise plugin install vfox-bfs https://github.com/mise-plugins/mise-tiny + +# Verify the installed plugin is used (not embedded) +# mise-tiny returns different output than the real bfs plugin +assert "mise x vfox:bfs@latest -- rtx-tiny" "rtx-tiny: v3.1.0 args:" + +# Uninstall the override +mise plugin uninstall vfox-bfs + +# Verify it falls back - use short name again +assert_contains "mise ls-remote bfs" "3.4" diff --git a/src/plugins/vfox_plugin.rs b/src/plugins/vfox_plugin.rs index 520c4828b0..f24c3b91fd 100644 --- a/src/plugins/vfox_plugin.rs +++ b/src/plugins/vfox_plugin.rs @@ -227,8 +227,8 @@ impl Plugin for VfoxPlugin { if !self.is_installed() { return Ok(()); } - // Embedded plugins cannot be uninstalled - they're bundled with mise - if self.is_embedded() { + // If only embedded (no filesystem plugin), warn that it can't be uninstalled + if self.is_embedded() && !self.plugin_path.exists() { warn!( "plugin:{} is embedded in mise, cannot uninstall", style(&self.name).blue().for_stderr() From b69bfecd4f5a1e204a07c780ad4c544fd8ac1aca Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 17 Dec 2025 22:54:58 +0000 Subject: [PATCH 06/16] chore(vfox): ignore embedded-plugins in prettier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The embedded plugin submodules have their own formatting conventions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crates/vfox/.prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/vfox/.prettierignore b/crates/vfox/.prettierignore index 29fde18168..44839fa761 100644 --- a/crates/vfox/.prettierignore +++ b/crates/vfox/.prettierignore @@ -1 +1,2 @@ plugins/nodejs +embedded-plugins From ed00dea1c7067cafcd7004dddd6e605096e11652 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 17 Dec 2025 23:00:19 +0000 Subject: [PATCH 07/16] ci: add submodules checkout to autofix and test-vfox workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/autofix.yml | 2 ++ .github/workflows/test-vfox.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index c1d21fd672..e10603291d 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -28,6 +28,8 @@ jobs: timeout-minutes: 10 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 with: shared-key: autofix diff --git a/.github/workflows/test-vfox.yml b/.github/workflows/test-vfox.yml index 8a6d99f016..587976de9b 100644 --- a/.github/workflows/test-vfox.yml +++ b/.github/workflows/test-vfox.yml @@ -24,6 +24,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 - run: | cargo build --all-features From 02aa695befa1244737eef43702b8c67fb3fc9ca3 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 17 Dec 2025 23:03:38 +0000 Subject: [PATCH 08/16] fix(vfox): properly handle filesystem overrides of embedded plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - install_plugin: check filesystem first, then embedded, then registry - update: only warn when no filesystem plugin exists - current_abbrev_ref/current_sha_short: check plugin_path.exists() This ensures users can override embedded plugins by installing their own version, and those overrides can be updated/uninstalled properly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crates/vfox/src/vfox.rs | 17 ++++++++++------- src/plugins/vfox_plugin.rs | 10 ++++++---- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/crates/vfox/src/vfox.rs b/crates/vfox/src/vfox.rs index ace9cf0e30..79e0623b49 100644 --- a/crates/vfox/src/vfox.rs +++ b/crates/vfox/src/vfox.rs @@ -103,17 +103,20 @@ impl Vfox { } pub fn install_plugin(&self, sdk: &str) -> Result { - // Check for embedded plugin first - no installation needed + // Check filesystem first - allows user to override embedded plugins + let plugin_dir = self.plugin_dir.join(sdk); + if plugin_dir.exists() { + return Plugin::from_dir(&plugin_dir); + } + + // Fall back to embedded plugin if available if let Some(embedded) = crate::embedded_plugins::get_embedded_plugin(sdk) { return Plugin::from_embedded(sdk, embedded); } - let plugin_dir = self.plugin_dir.join(sdk); - if !plugin_dir.exists() { - let url = registry::sdk_url(sdk).ok_or_else(|| format!("Unknown SDK: {sdk}"))?; - return self.install_plugin_from_url(url); - } - Plugin::from_dir(&plugin_dir) + // Otherwise install from registry + let url = registry::sdk_url(sdk).ok_or_else(|| format!("Unknown SDK: {sdk}"))?; + self.install_plugin_from_url(url) } pub fn install_plugin_from_url(&self, url: &Url) -> Result { diff --git a/src/plugins/vfox_plugin.rs b/src/plugins/vfox_plugin.rs index f24c3b91fd..e3b820cc86 100644 --- a/src/plugins/vfox_plugin.rs +++ b/src/plugins/vfox_plugin.rs @@ -135,14 +135,16 @@ impl Plugin for VfoxPlugin { } fn current_abbrev_ref(&self) -> eyre::Result> { - if !self.is_installed() { + // No git ref for embedded plugins or if plugin_path doesn't exist + if !self.plugin_path.exists() { return Ok(None); } self.repo().current_abbrev_ref().map(Some) } fn current_sha_short(&self) -> eyre::Result> { - if !self.is_installed() { + // No git sha for embedded plugins or if plugin_path doesn't exist + if !self.plugin_path.exists() { return Ok(None); } self.repo().current_sha_short().map(Some) @@ -186,8 +188,8 @@ impl Plugin for VfoxPlugin { } async fn update(&self, pr: &dyn SingleReport, gitref: Option) -> Result<()> { - // Embedded plugins cannot be updated - they're bundled with mise - if self.is_embedded() { + // If only embedded (no filesystem plugin), warn that it can't be updated + if self.is_embedded() && !self.plugin_path.exists() { warn!( "plugin:{} is embedded in mise, not updating", style(&self.name).blue().for_stderr() From ce162df63e2123398928ef79fa6a041495212086 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 17 Dec 2025 23:11:06 +0000 Subject: [PATCH 09/16] chore(release): auto-sync vfox embedded plugins from registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modify release-plz to automatically add/remove vfox plugin submodules based on `mise registry` output. Tools where vfox is the first (default) backend will have their plugins embedded at build time. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- xtasks/release-plz | 56 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/xtasks/release-plz b/xtasks/release-plz index 331dfda20f..4d8b302dc9 100755 --- a/xtasks/release-plz +++ b/xtasks/release-plz @@ -160,14 +160,70 @@ changelog="$(echo "$changelog" | tail -n +3)" mise up mise lock + +# Update embedded vfox plugin submodules based on mise registry +# Tools where vfox is the first backend should have embedded plugins +EMBEDDED_PLUGINS_DIR="crates/vfox/embedded-plugins" + +# Get current embedded plugins (submodule directory names) +CURRENT_EMBEDDED="" +if [[ -d "$EMBEDDED_PLUGINS_DIR" ]]; then + CURRENT_EMBEDDED="$(find "$EMBEDDED_PLUGINS_DIR" -maxdepth 1 -type d -name 'vfox-*' -exec basename {} \; | sort)" +fi + +# Get tools where vfox is the FIRST (default) backend using mise registry command +# Output format: "toolname backend1 backend2 ..." +# We only want tools where the first backend starts with "vfox:" +VFOX_REGISTRY="$(mise registry | awk '$2 ~ /^vfox:/ {print}')" + +# Extract just the plugin directory names (e.g., vfox-aapt2) for comparison +DESIRED_EMBEDDED="$(echo "$VFOX_REGISTRY" | awk '{print $2}' | sed 's|vfox:||' | sed 's|.*/||' | sort | uniq)" + +# Find plugins to add (in desired but not in current) +PLUGINS_TO_ADD="$(comm -23 <(echo "$DESIRED_EMBEDDED") <(echo "$CURRENT_EMBEDDED"))" + +# Find plugins to remove (in current but not in desired) +PLUGINS_TO_REMOVE="$(comm -13 <(echo "$DESIRED_EMBEDDED") <(echo "$CURRENT_EMBEDDED"))" + +# Add new submodules +if [[ -n "$PLUGINS_TO_ADD" ]]; then + echo "Adding embedded vfox plugins: $PLUGINS_TO_ADD" + while IFS= read -r plugin; do + [[ -z "$plugin" ]] && continue + # Extract the org/repo from mise registry output for this plugin + repo_path="$(echo "$VFOX_REGISTRY" | grep "/$plugin\b" | awk '{print $2}' | sed 's|vfox:||' | head -1)" + if [[ -n "$repo_path" ]]; then + echo "Adding submodule for $plugin from https://github.com/$repo_path" + git submodule add "https://github.com/$repo_path.git" "$EMBEDDED_PLUGINS_DIR/$plugin" || true + fi + done <<< "$PLUGINS_TO_ADD" +fi + +# Remove old submodules +if [[ -n "$PLUGINS_TO_REMOVE" ]]; then + echo "Removing embedded vfox plugins: $PLUGINS_TO_REMOVE" + while IFS= read -r plugin; do + [[ -z "$plugin" ]] && continue + echo "Removing submodule for $plugin" + git submodule deinit -f "$EMBEDDED_PLUGINS_DIR/$plugin" || true + git rm -f "$EMBEDDED_PLUGINS_DIR/$plugin" || true + rm -rf ".git/modules/$EMBEDDED_PLUGINS_DIR/$plugin" || true + done <<< "$PLUGINS_TO_REMOVE" +fi + +# Update existing submodules to latest +git submodule update --remote --merge "$EMBEDDED_PLUGINS_DIR" || true + git status # cargo update git add \ + .gitmodules \ Cargo.lock \ Cargo.toml \ CHANGELOG.md \ README.md \ crates/aqua-registry/aqua-registry \ + crates/vfox/embedded-plugins \ default.nix \ snapcraft.yaml \ docs/.vitepress/stars.data.ts \ From 4a0870b21330643dcb762c179450cc8a78dcc8b2 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 17 Dec 2025 23:12:19 +0000 Subject: [PATCH 10/16] fix(vfox): accept any Lua type from embedded lib modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lib modules can legitimately return non-table values (functions, strings, nil, etc.). Changed load_embedded_libs to use Value type instead of Table to handle all valid Lua module return types. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crates/vfox/src/plugin.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/vfox/src/plugin.rs b/crates/vfox/src/plugin.rs index 15f3a6f73e..0446aa6c86 100644 --- a/crates/vfox/src/plugin.rs +++ b/crates/vfox/src/plugin.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; use std::fmt::Display; use std::path::{Path, PathBuf}; -use mlua::{AsChunk, FromLuaMulti, IntoLua, Lua, Table}; +use mlua::{AsChunk, FromLuaMulti, IntoLua, Lua, Table, Value}; use once_cell::sync::OnceCell; use crate::config::Config; @@ -202,9 +202,9 @@ impl Plugin { let package: Table = self.lua.globals().get("package")?; let loaded: Table = package.get("loaded")?; - // Load lib modules + // Load lib modules - use Value since modules can return any Lua type for (name, code) in embedded.lib { - let module: Table = self.lua.load(*code).eval()?; + let module: Value = self.lua.load(*code).eval()?; loaded.set(*name, module)?; } From 3af2503747756a707dcd2b76843c569ba57a9137 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 17 Dec 2025 23:12:58 +0000 Subject: [PATCH 11/16] fix(vfox): consistent filesystem-first priority in from_name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The from_name method was checking embedded plugins before filesystem, while from_name_or_dir and install_plugin check filesystem first. This inconsistency prevented user overrides when code paths used from_name. Now all methods consistently check filesystem first, allowing users to override embedded plugins by installing their own version. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crates/vfox/src/plugin.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/vfox/src/plugin.rs b/crates/vfox/src/plugin.rs index 0446aa6c86..be11d2efe1 100644 --- a/crates/vfox/src/plugin.rs +++ b/crates/vfox/src/plugin.rs @@ -61,11 +61,15 @@ impl Plugin { } pub fn from_name(name: &str) -> Result { - // Check for embedded plugin first + // Check filesystem first - allows user to override embedded plugins + let dir = Config::get().plugin_dir.join(name); + if dir.exists() { + return Self::from_dir(&dir); + } + // Fall back to embedded plugin if available if let Some(embedded) = embedded_plugins::get_embedded_plugin(name) { return Self::from_embedded(name, embedded); } - let dir = Config::get().plugin_dir.join(name); Self::from_dir(&dir) } From a14456e49be097004f966b05b543db46d9ea9b22 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 17 Dec 2025 23:27:59 +0000 Subject: [PATCH 12/16] fix(ci): add submodules checkout to remaining workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add submodules: true to workflows that build Rust code: - hyperfine.yml (benchmark) - ppa-publish.yml - registry.yml - release-fig.yml - release-plz.yml 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/hyperfine.yml | 1 + .github/workflows/ppa-publish.yml | 1 + .github/workflows/registry.yml | 2 ++ .github/workflows/release-fig.yml | 1 + .github/workflows/release-plz.yml | 1 + 5 files changed, 6 insertions(+) diff --git a/.github/workflows/hyperfine.yml b/.github/workflows/hyperfine.yml index c59a0d2403..55f0142f71 100644 --- a/.github/workflows/hyperfine.yml +++ b/.github/workflows/hyperfine.yml @@ -27,6 +27,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 + submodules: true - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 - run: curl https://mise.run | MISE_INSTALL_PATH="$HOME/bin/mise-release" sh - run: echo "$HOME/bin" >> "$GITHUB_PATH" diff --git a/.github/workflows/ppa-publish.yml b/.github/workflows/ppa-publish.yml index 75268626c1..52bff84a3c 100644 --- a/.github/workflows/ppa-publish.yml +++ b/.github/workflows/ppa-publish.yml @@ -35,6 +35,7 @@ jobs: uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 + submodules: true - name: Set up environment variables run: | diff --git a/.github/workflows/registry.yml b/.github/workflows/registry.yml index 60e90aefdb..140be1e0e5 100644 --- a/.github/workflows/registry.yml +++ b/.github/workflows/registry.yml @@ -30,6 +30,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: true - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 with: shared-key: build diff --git a/.github/workflows/release-fig.yml b/.github/workflows/release-fig.yml index 21d576135f..69d3cce461 100644 --- a/.github/workflows/release-fig.yml +++ b/.github/workflows/release-fig.yml @@ -14,6 +14,7 @@ jobs: with: fetch-depth: 0 token: ${{ secrets.MISE_GH_TOKEN }} + submodules: true - uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2 with: shared-key: build diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index 05e42a507a..87c5d55d02 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -32,6 +32,7 @@ jobs: with: fetch-depth: 0 token: ${{ secrets.MISE_GH_TOKEN }} + submodules: true - uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6 with: gpg_private_key: ${{ secrets.MISE_GPG_KEY }} From 744ab55ef8a7479fc26e7d16e928cd75a8d15697 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 17 Dec 2025 23:29:21 +0000 Subject: [PATCH 13/16] fix(vfox): multiple embedded plugin fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Fix e2e test to use vfox-compatible plugin override instead of asdf plugin (mise-tiny). Now tests with the actual vfox-bfs plugin. 2. Fix lib-to-lib require dependencies by using package.preload instead of package.loaded. This allows lib files to require each other regardless of alphabetical ordering. 3. Fix build script to track hooks/ and lib/ subdirectories and individual .lua files for proper rebuild detection. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crates/vfox/build.rs | 26 +++++++++++++++++++- crates/vfox/src/plugin.rs | 14 ++++++++--- e2e/backend/test_vfox_embedded_override_slow | 24 ++++++++++++------ 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/crates/vfox/build.rs b/crates/vfox/build.rs index 7d551ce570..6199bb23b4 100644 --- a/crates/vfox/build.rs +++ b/crates/vfox/build.rs @@ -59,9 +59,33 @@ pub fn list_embedded_plugins() -> &'static [&'static str] { continue; } - // Tell Cargo to re-run if this plugin directory changes + // Tell Cargo to re-run if this plugin directory or any Lua files change println!("cargo:rerun-if-changed={}", path.display()); + // Also track subdirectories and individual Lua files + let hooks_dir = path.join("hooks"); + if hooks_dir.exists() { + println!("cargo:rerun-if-changed={}", hooks_dir.display()); + for entry in fs::read_dir(&hooks_dir).unwrap().flatten() { + if entry.path().extension().is_some_and(|ext| ext == "lua") { + println!("cargo:rerun-if-changed={}", entry.path().display()); + } + } + } + let lib_dir = path.join("lib"); + if lib_dir.exists() { + println!("cargo:rerun-if-changed={}", lib_dir.display()); + for entry in fs::read_dir(&lib_dir).unwrap().flatten() { + if entry.path().extension().is_some_and(|ext| ext == "lua") { + println!("cargo:rerun-if-changed={}", entry.path().display()); + } + } + } + let metadata_file = path.join("metadata.lua"); + if metadata_file.exists() { + println!("cargo:rerun-if-changed={}", metadata_file.display()); + } + let plugin = collect_plugin_files(&path); plugins.insert(dir_name, plugin); } diff --git a/crates/vfox/src/plugin.rs b/crates/vfox/src/plugin.rs index be11d2efe1..81d46630e1 100644 --- a/crates/vfox/src/plugin.rs +++ b/crates/vfox/src/plugin.rs @@ -204,12 +204,18 @@ impl Plugin { fn load_embedded_libs(&self, embedded: &EmbeddedPlugin) -> Result<()> { let package: Table = self.lua.globals().get("package")?; - let loaded: Table = package.get("loaded")?; + let preload: Table = package.get("preload")?; - // Load lib modules - use Value since modules can return any Lua type + // Register lib modules in package.preload so require() works regardless of load order + // This allows lib files to require each other without alphabetical ordering issues for (name, code) in embedded.lib { - let module: Value = self.lua.load(*code).eval()?; - loaded.set(*name, module)?; + let lua = self.lua.clone(); + let code = *code; + let loader = lua.create_function(move |lua, _: ()| { + let module: Value = lua.load(code).eval()?; + Ok(module) + })?; + preload.set(*name, loader)?; } Ok(()) diff --git a/e2e/backend/test_vfox_embedded_override_slow b/e2e/backend/test_vfox_embedded_override_slow index 249eb3bf32..7eb73b0adf 100644 --- a/e2e/backend/test_vfox_embedded_override_slow +++ b/e2e/backend/test_vfox_embedded_override_slow @@ -1,21 +1,29 @@ #!/usr/bin/env bash -# Test that embedded vfox plugins can be overridden by installing a plugin +# Test that embedded vfox plugins can be overridden by installing a filesystem plugin # Verify embedded plugin works without installation - use the short "bfs" name # which goes through the versions host cache (doesn't require network to the plugin) assert_contains "mise ls-remote bfs" "3.4" -# Install a different plugin with the same name to override -# Using mise-tiny as a stand-in since it's a simple test plugin -mise plugin install vfox-bfs https://github.com/mise-plugins/mise-tiny +# Embedded plugins don't show git info in plugins list +assert_not_contains "mise plugins --urls" "vfox-bfs" -# Verify the installed plugin is used (not embedded) -# mise-tiny returns different output than the real bfs plugin -assert "mise x vfox:bfs@latest -- rtx-tiny" "rtx-tiny: v3.1.0 args:" +# Install the same vfox plugin as a filesystem override +mise plugin install vfox-bfs https://github.com/mise-plugins/vfox-bfs + +# Verify the filesystem plugin is now shown with git URL (overrides embedded) +assert_contains "mise plugins --urls" "vfox-bfs" +assert_contains "mise plugins --urls" "github.com/mise-plugins/vfox-bfs" + +# Verify it still works (now using filesystem version) +assert_contains "mise ls-remote bfs" "3.4" # Uninstall the override mise plugin uninstall vfox-bfs -# Verify it falls back - use short name again +# Verify it falls back to embedded (no longer in plugins list with URL) +assert_not_contains "mise plugins --urls" "vfox-bfs" + +# Verify embedded still works after uninstall assert_contains "mise ls-remote bfs" "3.4" From 849a8718ac4d0ea395676cff42cf9bd9d87e5320 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 17 Dec 2025 23:34:53 +0000 Subject: [PATCH 14/16] fix(lint): ignore embedded-plugins in markdownlint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The submodule READMEs have their own formatting that we shouldn't modify. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .markdownlintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.markdownlintignore b/.markdownlintignore index dfe34bff89..8bc02f5a91 100644 --- a/.markdownlintignore +++ b/.markdownlintignore @@ -1,6 +1,7 @@ /registry/ /target/ CHANGELOG.md +crates/vfox/embedded-plugins/ docs/node_modules/ docs/cli/watch.md node_modules/ From 2bae46960dfcdc3301d8b2f7ab84c9ba5ae1fc05 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 23:42:07 +0000 Subject: [PATCH 15/16] [autofix.ci] apply automated fixes --- crates/vfox/Cargo.toml | 11 ++++++++++- xtasks/release-plz | 16 ++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/crates/vfox/Cargo.toml b/crates/vfox/Cargo.toml index fb6fb42fb5..54ba06b39a 100644 --- a/crates/vfox/Cargo.toml +++ b/crates/vfox/Cargo.toml @@ -7,7 +7,16 @@ description = "Interface to vfox plugins" documentation = "https://docs.rs/vfox" homepage = "https://github.com/jdx/mise" repository = "https://github.com/jdx/mise" -include = ["src", "lua", "embedded-plugins", "build.rs", "Cargo.toml", "Cargo.lock", "README.md", "LICENSE"] +include = [ + "src", + "lua", + "embedded-plugins", + "build.rs", + "Cargo.toml", + "Cargo.lock", + "README.md", + "LICENSE", +] build = "build.rs" [lib] diff --git a/xtasks/release-plz b/xtasks/release-plz index 4d8b302dc9..648202d014 100755 --- a/xtasks/release-plz +++ b/xtasks/release-plz @@ -167,7 +167,7 @@ EMBEDDED_PLUGINS_DIR="crates/vfox/embedded-plugins" # Get current embedded plugins (submodule directory names) CURRENT_EMBEDDED="" -if [[ -d "$EMBEDDED_PLUGINS_DIR" ]]; then +if [[ -d $EMBEDDED_PLUGINS_DIR ]]; then CURRENT_EMBEDDED="$(find "$EMBEDDED_PLUGINS_DIR" -maxdepth 1 -type d -name 'vfox-*' -exec basename {} \; | sort)" fi @@ -186,29 +186,29 @@ PLUGINS_TO_ADD="$(comm -23 <(echo "$DESIRED_EMBEDDED") <(echo "$CURRENT_EMBEDDED PLUGINS_TO_REMOVE="$(comm -13 <(echo "$DESIRED_EMBEDDED") <(echo "$CURRENT_EMBEDDED"))" # Add new submodules -if [[ -n "$PLUGINS_TO_ADD" ]]; then +if [[ -n $PLUGINS_TO_ADD ]]; then echo "Adding embedded vfox plugins: $PLUGINS_TO_ADD" while IFS= read -r plugin; do - [[ -z "$plugin" ]] && continue + [[ -z $plugin ]] && continue # Extract the org/repo from mise registry output for this plugin repo_path="$(echo "$VFOX_REGISTRY" | grep "/$plugin\b" | awk '{print $2}' | sed 's|vfox:||' | head -1)" - if [[ -n "$repo_path" ]]; then + if [[ -n $repo_path ]]; then echo "Adding submodule for $plugin from https://github.com/$repo_path" git submodule add "https://github.com/$repo_path.git" "$EMBEDDED_PLUGINS_DIR/$plugin" || true fi - done <<< "$PLUGINS_TO_ADD" + done <<<"$PLUGINS_TO_ADD" fi # Remove old submodules -if [[ -n "$PLUGINS_TO_REMOVE" ]]; then +if [[ -n $PLUGINS_TO_REMOVE ]]; then echo "Removing embedded vfox plugins: $PLUGINS_TO_REMOVE" while IFS= read -r plugin; do - [[ -z "$plugin" ]] && continue + [[ -z $plugin ]] && continue echo "Removing submodule for $plugin" git submodule deinit -f "$EMBEDDED_PLUGINS_DIR/$plugin" || true git rm -f "$EMBEDDED_PLUGINS_DIR/$plugin" || true rm -rf ".git/modules/$EMBEDDED_PLUGINS_DIR/$plugin" || true - done <<< "$PLUGINS_TO_REMOVE" + done <<<"$PLUGINS_TO_REMOVE" fi # Update existing submodules to latest From 8b36694f853ffeb2a1417f68fda0859c7126977d Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 17 Dec 2025 23:44:07 +0000 Subject: [PATCH 16/16] fix(release): use precise grep pattern for plugin matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The \b word boundary incorrectly matches plugins with similar prefixes (e.g., vfox-android would match vfox-android-sdk). Use explicit pattern that matches plugin name followed by space or end of line. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- xtasks/release-plz | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xtasks/release-plz b/xtasks/release-plz index 648202d014..a2ca0ba5e6 100755 --- a/xtasks/release-plz +++ b/xtasks/release-plz @@ -191,7 +191,8 @@ if [[ -n $PLUGINS_TO_ADD ]]; then while IFS= read -r plugin; do [[ -z $plugin ]] && continue # Extract the org/repo from mise registry output for this plugin - repo_path="$(echo "$VFOX_REGISTRY" | grep "/$plugin\b" | awk '{print $2}' | sed 's|vfox:||' | head -1)" + # Use pattern that matches plugin name at end of URL (followed by space or EOL) + repo_path="$(echo "$VFOX_REGISTRY" | grep -E "/$plugin( |\$)" | awk '{print $2}' | sed 's|vfox:||' | head -1)" if [[ -n $repo_path ]]; then echo "Adding submodule for $plugin from https://github.com/$repo_path" git submodule add "https://github.com/$repo_path.git" "$EMBEDDED_PLUGINS_DIR/$plugin" || true