diff --git a/.cursor/rules/conventional_commits.mdc b/.cursor/rules/conventional_commits.mdc index f17453a0c7..1b925f8ebf 100644 --- a/.cursor/rules/conventional_commits.mdc +++ b/.cursor/rules/conventional_commits.mdc @@ -1,3 +1,8 @@ +--- +alwaysApply: false +description: how to write commit and PR titles +--- + ## Conventional Commits (REQUIRED) All commit messages and PR titles MUST follow conventional commit format: @@ -29,4 +34,35 @@ chore(deps): update Rust dependencies ``` ### Common Scopes -`registry`, `aqua`, `cli`, `config`, `backend`, `tool`, `env`, `task`, `api`, `ui`, `core`, `deps`, `schema`, `doctor`, `shim`, `security` \ No newline at end of file +`registry`, `aqua`, `cli`, `config`, `backend`, `tool`, `env`, `task`, `api`, `ui`, `core`, `deps`, `schema`, `doctor`, `shim`, `security`## Conventional Commits (REQUIRED) + +All commit messages and PR titles MUST follow conventional commit format: + +### Format +``` +(): +``` + +### Types +- `feat:` - New features +- `fix:` - Bug fixes +- `refactor:` - Code refactoring +- `doc:` - Documentation +- `style:` - Code style/formatting +- `perf:` - Performance improvements +- `test:` - Testing changes +- `chore:` - Maintenance tasks +- `chore(deps):` - Dependency updates + +### Examples +``` +feat(cli): add new command for tool management +fix(config): resolve parsing issue with nested tables +refactor(backend): simplify plugin loading logic +doc(api): update configuration examples +test(e2e): add tests for tool installation +chore(deps): update Rust dependencies +``` + +### Common Scopes +`registry`, `aqua`, `cli`, `config`, `backend`, `tool`, `env`, `task`, `api`, `ui`, `core`, `deps`, `schema`, `doctor`, `shim`, `security` diff --git a/.cursor/rules/testing.mdc b/.cursor/rules/testing.mdc new file mode 100644 index 0000000000..9ec1f00caa --- /dev/null +++ b/.cursor/rules/testing.mdc @@ -0,0 +1,16 @@ +--- +description: how to test the mise codebase +alwaysApply: false +--- + +Testing and linting commands should be run via `mise run`. + +- `mise run e2e [test_filename]` executes an e2e test +- `mise run test:unit` executes the unit tests +- `mise run lint` runs the linting commands +- `mise run lint-fix` runs the linting commands and fixes the issues +- `mise --cd crates/vfox run test` executes the tests for the vfox crate +- `mise --cd crates/vfox run lint` runs the linting commands for the vfox crate +- `mise --cd crates/vfox run lint-fix` runs the linting commands and fixes the issues for the vfox crate + +other tasks can be found by running `mise task ls` diff --git a/crates/vfox/Cargo.toml b/crates/vfox/Cargo.toml index a1cadee523..18448c2eb7 100644 --- a/crates/vfox/Cargo.toml +++ b/crates/vfox/Cargo.toml @@ -5,8 +5,8 @@ edition = "2021" license = "MIT" description = "Interface to vfox plugins" documentation = "https://docs.rs/vfox" -homepage = "https://github.com/jdx/vfox.rs" -repository = "https://github.com/jdx/vfox.rs" +homepage = "https://github.com/jdx/mise" +repository = "https://github.com/jdx/mise" include = ["src", "lua", "Cargo.toml", "Cargo.lock", "README.md", "LICENSE"] [lib] diff --git a/crates/vfox/src/hooks/backend_exec_env.rs b/crates/vfox/src/hooks/backend_exec_env.rs new file mode 100644 index 0000000000..d8e1ac96c9 --- /dev/null +++ b/crates/vfox/src/hooks/backend_exec_env.rs @@ -0,0 +1,46 @@ +use mlua::{FromLua, IntoLua, Lua, Value, prelude::LuaError}; +use std::path::PathBuf; + +use crate::hooks::env_keys::EnvKey; + +#[derive(Debug, Clone)] +pub struct BackendExecEnvContext { + pub args: Vec, + pub tool: String, + pub version: String, + pub install_path: PathBuf, +} + +#[derive(Debug, Clone)] +pub struct BackendExecEnvResponse { + pub env_vars: Vec, +} + +impl IntoLua for BackendExecEnvContext { + fn into_lua(self, lua: &mlua::Lua) -> mlua::Result { + let table = lua.create_table()?; + table.set("args", self.args)?; + table.set("tool", self.tool)?; + table.set("version", self.version)?; + table.set( + "install_path", + self.install_path.to_string_lossy().to_string(), + )?; + Ok(Value::Table(table)) + } +} + +impl FromLua for BackendExecEnvResponse { + fn from_lua(value: Value, _: &Lua) -> std::result::Result { + match value { + Value::Table(table) => Ok(BackendExecEnvResponse { + env_vars: table.get::>("env_vars")?, + }), + _ => Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "BackendExecEnvResponse".to_string(), + message: Some("Expected table".to_string()), + }), + } + } +} diff --git a/crates/vfox/src/hooks/backend_install.rs b/crates/vfox/src/hooks/backend_install.rs new file mode 100644 index 0000000000..8660e588f4 --- /dev/null +++ b/crates/vfox/src/hooks/backend_install.rs @@ -0,0 +1,41 @@ +use mlua::{FromLua, IntoLua, Lua, Value, prelude::LuaError}; +use std::path::PathBuf; + +#[derive(Debug, Clone)] +pub struct BackendInstallContext { + pub args: Vec, + pub tool: String, + pub version: String, + pub install_path: PathBuf, +} + +#[derive(Debug, Clone)] +pub struct BackendInstallResponse { +} + +impl IntoLua for BackendInstallContext { + fn into_lua(self, lua: &mlua::Lua) -> mlua::Result { + let table = lua.create_table()?; + table.set("args", self.args)?; + table.set("tool", self.tool)?; + table.set("version", self.version)?; + table.set( + "install_path", + self.install_path.to_string_lossy().to_string(), + )?; + Ok(Value::Table(table)) + } +} + +impl FromLua for BackendInstallResponse { + fn from_lua(value: Value, _: &Lua) -> std::result::Result { + match value { + Value::Table(_) => Ok(BackendInstallResponse {}), + _ => Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "BackendInstallResponse".to_string(), + message: Some("Expected table".to_string()), + }), + } + } +} diff --git a/crates/vfox/src/hooks/backend_list_versions.rs b/crates/vfox/src/hooks/backend_list_versions.rs new file mode 100644 index 0000000000..235970cc98 --- /dev/null +++ b/crates/vfox/src/hooks/backend_list_versions.rs @@ -0,0 +1,36 @@ +use mlua::{FromLua, IntoLua, Lua, Value, prelude::LuaError}; + +#[derive(Debug, Clone)] +pub struct BackendListVersionsContext { + pub args: Vec, + pub tool: String, +} + +#[derive(Debug, Clone)] +pub struct BackendListVersionsResponse { + pub versions: Vec, +} + +impl IntoLua for BackendListVersionsContext { + fn into_lua(self, lua: &mlua::Lua) -> mlua::Result { + let table = lua.create_table()?; + table.set("args", self.args)?; + table.set("tool", self.tool)?; + Ok(Value::Table(table)) + } +} + +impl FromLua for BackendListVersionsResponse { + fn from_lua(value: Value, _: &Lua) -> std::result::Result { + match value { + Value::Table(table) => Ok(BackendListVersionsResponse { + versions: table.get::>("versions")?, + }), + _ => Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "BackendListVersionsResponse".to_string(), + message: Some("Expected table".to_string()), + }), + } + } +} diff --git a/crates/vfox/src/hooks/env_keys.rs b/crates/vfox/src/hooks/env_keys.rs index 4a13dd7aa4..755e570838 100644 --- a/crates/vfox/src/hooks/env_keys.rs +++ b/crates/vfox/src/hooks/env_keys.rs @@ -7,7 +7,7 @@ use crate::error::Result; use crate::sdk_info::SdkInfo; use crate::Plugin; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct EnvKey { pub key: String, pub value: String, diff --git a/crates/vfox/src/hooks/mod.rs b/crates/vfox/src/hooks/mod.rs index c8f1fac6ce..165b29619d 100644 --- a/crates/vfox/src/hooks/mod.rs +++ b/crates/vfox/src/hooks/mod.rs @@ -1,4 +1,7 @@ pub mod available; +pub mod backend_exec_env; +pub mod backend_install; +pub mod backend_list_versions; pub mod env_keys; pub mod mise_env; pub mod mise_path; diff --git a/crates/vfox/src/lib.rs b/crates/vfox/src/lib.rs index ea2fcf4886..1e30859013 100644 --- a/crates/vfox/src/lib.rs +++ b/crates/vfox/src/lib.rs @@ -11,6 +11,13 @@ pub use error::VfoxError; pub use plugin::Plugin; pub use vfox::Vfox; +// Backend hooks +pub mod backend_hooks { + pub use crate::hooks::backend_exec_env::{BackendExecEnvContext, BackendExecEnvResponse}; + pub use crate::hooks::backend_install::{BackendInstallContext, BackendInstallResponse}; + pub use crate::hooks::backend_list_versions::{BackendListVersionsContext, BackendListVersionsResponse}; +} + mod config; mod context; mod error; diff --git a/crates/vfox/src/lua_mod/cmd.rs b/crates/vfox/src/lua_mod/cmd.rs new file mode 100644 index 0000000000..730be1dbc7 --- /dev/null +++ b/crates/vfox/src/lua_mod/cmd.rs @@ -0,0 +1,52 @@ +use mlua::prelude::*; +use mlua::Table; + +pub fn mod_cmd(lua: &Lua) -> LuaResult<()> { + let package: Table = lua.globals().get("package")?; + let loaded: Table = package.get("loaded")?; + let cmd = lua.create_table_from(vec![("exec", lua.create_function(exec)?)])?; + loaded.set("cmd", cmd.clone())?; + loaded.set("vfox.cmd", cmd)?; + Ok(()) +} + +fn exec(_lua: &Lua, (command,): (String,)) -> LuaResult { + use std::process::Command; + + let output = Command::new("sh") + .arg("-c") + .arg(&command) + .output() + .map_err(|e| mlua::Error::RuntimeError(format!("Failed to execute command: {}", e)))?; + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + if output.status.success() { + Ok(stdout.to_string()) + } else { + Err(mlua::Error::RuntimeError(format!( + "Command failed with status {}: {}", + output.status, + stderr + ))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cmd() { + let lua = Lua::new(); + mod_cmd(&lua).unwrap(); + lua.load(mlua::chunk! { + local cmd = require("cmd") + local result = cmd.exec("echo hello world") + assert(result == "hello world\n") + }) + .exec() + .unwrap(); + } +} diff --git a/crates/vfox/src/lua_mod/mod.rs b/crates/vfox/src/lua_mod/mod.rs index 58916a4d42..9e40178a18 100644 --- a/crates/vfox/src/lua_mod/mod.rs +++ b/crates/vfox/src/lua_mod/mod.rs @@ -1,4 +1,5 @@ mod archiver; +mod cmd; mod env; mod file; mod hooks; @@ -8,6 +9,7 @@ mod json; mod strings; 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::mod_hooks as hooks; diff --git a/crates/vfox/src/plugin.rs b/crates/vfox/src/plugin.rs index b89d40b5e1..09e712408e 100644 --- a/crates/vfox/src/plugin.rs +++ b/crates/vfox/src/plugin.rs @@ -103,6 +103,65 @@ impl Plugin { Ok(result) } + // Backend plugin methods + pub async fn backend_list_versions( + &self, + ctx: crate::hooks::backend_list_versions::BackendListVersionsContext, + ) -> Result { + debug!("[vfox:{}] backend_list_versions", &self.name); + self.load()?; + // Set the context as a global variable with a unique name + self.set_global("BACKEND_CTX", ctx)?; + let response = self + .eval_async(chunk! { + if PLUGIN.BackendListVersions then + return PLUGIN:BackendListVersions(BACKEND_CTX) + else + return {versions = {}} + end + }) + .await?; + Ok(response) + } + + pub async fn backend_install( + &self, + ctx: crate::hooks::backend_install::BackendInstallContext, + ) -> Result { + debug!("[vfox:{}] backend_install", &self.name); + self.load()?; + self.set_global("BACKEND_CTX", ctx)?; + let response = self + .eval_async(chunk! { + if PLUGIN.BackendInstall then + return PLUGIN:BackendInstall(BACKEND_CTX) + else + return {success = false, message = "Backend install not implemented"} + end + }) + .await?; + Ok(response) + } + + pub async fn backend_exec_env( + &self, + ctx: crate::hooks::backend_exec_env::BackendExecEnvContext, + ) -> Result { + debug!("[vfox:{}] backend_exec_env", &self.name); + self.load()?; + self.set_global("BACKEND_CTX", ctx)?; + let response = self + .eval_async(chunk! { + if PLUGIN.BackendExecEnv then + return PLUGIN:BackendExecEnv(BACKEND_CTX) + else + return {env_vars = {}} + end + }) + .await?; + Ok(response) + } + fn load(&self) -> Result<&Metadata> { self.metadata.get_or_try_init(|| { debug!("Getting metadata for {self}"); @@ -116,6 +175,7 @@ impl Plugin { )?; lua_mod::archiver(&self.lua)?; + lua_mod::cmd(&self.lua)?; lua_mod::file(&self.lua)?; lua_mod::html(&self.lua)?; lua_mod::http(&self.lua)?; diff --git a/e2e/backend/test_vfox_backend_npm b/e2e/backend/test_vfox_backend_npm new file mode 100755 index 0000000000..e526b71528 --- /dev/null +++ b/e2e/backend/test_vfox_backend_npm @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2103 + +# Test vfox backend with npm plugin (plugin:tool format) + +# Create a temporary vfox plugin directory with backend support +temp_plugins_dir=$(mktemp -d) +plugin_name="test-npm" +plugin_dir="$temp_plugins_dir/$plugin_name" + +# Ensure cleanup happens +cleanup() { + rm -rf "$temp_plugins_dir" + # Clean up any test installations + rm -rf "$MISE_DATA_DIR/installs/vfox-test-npm" 2>/dev/null || true + # Clean up plugin link + mise plugins unlink vfox-test-npm 2>/dev/null || true +} +trap cleanup EXIT + +# Create the plugin directory +mkdir -p "$plugin_dir/hooks" + +# Create metadata.lua with backend support that actually uses npm +cat >"$plugin_dir/metadata.lua" <<'EOF' +PLUGIN = { + name = "vfox-test-npm", + version = "1.0.0", + description = "Test npm backend plugin that uses real npm", + author = "Test", + license = "MIT", + + -- Backend methods for plugin:tool format (CamelCase) + BackendListVersions = function(ctx) + local tool = BACKEND_CTX.tool + local versions = {} + + -- Use npm view to get real versions + local cmd = require("cmd") + local result = cmd.exec("npm view " .. tool .. " versions --json 2>/dev/null") + + if result and result ~= "" and not result:match("npm ERR!") then + -- Parse JSON response from npm using built-in json module + local json = require("json") + local success, npm_versions = pcall(json.decode, result) + + if success and npm_versions then + if type(npm_versions) == "table" then + for i = #npm_versions, 1, -1 do + local version = npm_versions[i] + table.insert(versions, version) + end + end + end + end + + if #versions == 0 then + error("Failed to fetch versions for " .. tool .. " from npm registry") + end + + return {versions = versions} + end, + + BackendInstall = function(ctx) + local tool = BACKEND_CTX.tool + local version = BACKEND_CTX.version + local install_path = BACKEND_CTX.install_path + + -- Create install directory + os.execute("mkdir -p " .. install_path) + + -- Install the package directly using npm install + local cmd = require("cmd") + local npm_cmd = "cd " .. install_path .. " && npm install " .. tool .. "@" .. version .. " --no-package-lock --no-save --silent 2>/dev/null" + local result = cmd.exec(npm_cmd) + + -- If we get here, the command succeeded + return {} + end, + + BackendExecEnv = function(ctx) + local install_path = BACKEND_CTX.install_path + if install_path then + -- Add node_modules/.bin to PATH for npm-installed binaries + local bin_path = install_path .. "/node_modules/.bin" + return { + env_vars = { + {key = "PATH", value = bin_path} + } + } + else + return {env_vars = {}} + end + end +} +EOF + +# Initialize the plugin directory as a git repository +cd "$plugin_dir" +git init --quiet +git add . +git commit --quiet -m "Initial commit" +cd - + +# Link the plugin +mise plugins link vfox-test-npm "$plugin_dir" + +# Test plugin:tool format with assertions +latest_version=$(mise latest vfox-test-npm:prettier) +partial_version=$(echo "$latest_version" | cut -d. -f1-2) +assert_contains "mise ls-remote vfox-test-npm:prettier" "$partial_version." +mise install "vfox-test-npm:prettier@$latest_version" +assert "mise use vfox-test-npm:prettier@$latest_version" +assert_contains "mise exec -- prettier --version" "$latest_version" + +# Test uninstall functionality +assert "mise uninstall vfox-test-npm:prettier@$latest_version" +assert_directory_not_exists "$MISE_DATA_DIR/installs/vfox-test-npm/prettier/$latest_version" diff --git a/e2e/backend/test_vfox_cmake b/e2e/backend/test_vfox_cmake old mode 100644 new mode 100755 diff --git a/e2e/backend/test_vfox_go b/e2e/backend/test_vfox_go old mode 100644 new mode 100755 diff --git a/e2e/backend/test_vfox_kotlin_slow b/e2e/backend/test_vfox_kotlin_slow old mode 100644 new mode 100755 diff --git a/e2e/backend/test_vfox_maven_slow b/e2e/backend/test_vfox_maven_slow old mode 100644 new mode 100755 diff --git a/e2e/backend/test_vfox_node_slow b/e2e/backend/test_vfox_node_slow old mode 100644 new mode 100755 diff --git a/e2e/backend/test_vfox_python_slow b/e2e/backend/test_vfox_python_slow old mode 100644 new mode 100755 diff --git a/src/backend/vfox.rs b/src/backend/vfox.rs index a3a78e08f1..c92a3ea811 100644 --- a/src/backend/vfox.rs +++ b/src/backend/vfox.rs @@ -20,6 +20,8 @@ use crate::plugins::vfox_plugin::VfoxPlugin; use crate::plugins::{Plugin, PluginType}; use crate::toolset::{ToolVersion, Toolset}; use crate::ui::multi_progress_report::MultiProgressReport; +// Backend hooks are now available in the current vfox version +use vfox::backend_hooks::{BackendExecEnvContext, BackendInstallContext, BackendListVersionsContext}; #[derive(Debug)] pub struct VfoxBackend { @@ -28,6 +30,7 @@ pub struct VfoxBackend { plugin_enum: PluginEnum, exec_env_cache: RwLock>>, pathname: String, + tool_name: Option, } #[async_trait] @@ -50,6 +53,31 @@ impl Backend for VfoxBackend { || async { let (vfox, _log_rx) = this.plugin.vfox(); this.ensure_plugin_installed(config).await?; + + // Check if this is a plugin:tool format and plugin supports backend methods + if let Some(tool_name) = &this.tool_name { + debug!("Using backend method for tool: {}", tool_name); + // Try to use the new backend method first (CamelCase) + let plugin = vfox.get_sdk(&this.pathname)?; + let ctx = BackendListVersionsContext { + args: vec![], + tool: tool_name.clone(), + }; + + match plugin.backend_list_versions(ctx).await { + Ok(response) => { + return Ok(response.versions.into_iter().rev().collect()); + } + Err(e) => { + debug!("Backend method failed: {}", e); + // For plugin:tool format, don't fall back to traditional methods + // as they don't support tool information + return Ok(vec![]); + } + } + } + + // Use default vfox behavior for traditional plugins let versions = vfox.list_available_versions(&this.pathname).await?; Ok(versions .into_iter() @@ -75,6 +103,29 @@ impl Backend for VfoxBackend { info!("{}", line); } }); + + // Check if this is a plugin:tool format and plugin supports backend methods + if let Some(tool_name) = &self.tool_name { + // Try to use the new backend method first + let plugin = vfox.get_sdk(&self.pathname)?; + let backend_ctx = BackendInstallContext { + args: vec![], + tool: tool_name.clone(), + version: tv.version.clone(), + install_path: tv.install_path(), + }; + + match plugin.backend_install(backend_ctx).await { + Ok(_response) => { + return Ok(tv); + } + Err(e) => { + return Err(eyre::eyre!("Backend install method failed: {}", e)); + } + } + } + + // Use default vfox behavior for traditional plugins vfox.install(&self.pathname, &tv.version, tv.install_path()) .await?; Ok(tv) @@ -116,9 +167,21 @@ impl Backend for VfoxBackend { impl VfoxBackend { pub fn from_arg(ba: BackendArg) -> Self { - let pathname = ba.short.to_kebab_case(); + // For vfox plugins, we need to extract the plugin name from the plugin:tool format + let (plugin_name, pathname, tool_name) = + if let Some((plugin_name, tool_name)) = ba.short.split_once(':') { + // Check if this is a vfox plugin:tool format + if crate::toolset::install_state::get_plugin_type(plugin_name).is_some() { + (plugin_name.to_string(), plugin_name.to_kebab_case(), Some(tool_name.to_string())) + } else { + (ba.short.clone(), ba.short.to_kebab_case(), None) + } + } else { + (ba.short.clone(), ba.short.to_kebab_case(), None) + }; + let plugin_path = dirs::PLUGINS.join(&pathname); - let mut plugin = VfoxPlugin::new(pathname.clone(), plugin_path.clone()); + let mut plugin = VfoxPlugin::new(plugin_name, plugin_path.clone()); plugin.full = Some(ba.full()); let plugin = Arc::new(plugin); Self { @@ -127,6 +190,7 @@ impl VfoxBackend { plugin_enum: PluginEnum::Vfox(plugin), ba: Arc::new(ba), pathname, + tool_name, } } @@ -153,6 +217,48 @@ impl VfoxBackend { .get_or_try_init_async(async || { self.ensure_plugin_installed(config).await?; let (vfox, _log_rx) = self.plugin.vfox(); + + // Check if this is a plugin:tool format and plugin supports backend methods + if let Some(tool_name) = &self.tool_name { + // Try to use the new backend method first + let plugin = vfox.get_sdk(&self.pathname)?; + let backend_ctx = BackendExecEnvContext { + args: vec![], + tool: tool_name.clone(), + version: tv.version.clone(), + install_path: tv.install_path(), + }; + + match plugin.backend_exec_env(backend_ctx).await { + Ok(response) => { + return Ok(response.env_vars + .into_iter() + .fold(BTreeMap::new(), |mut acc, env_key| { + let key = &env_key.key; + if let Some(val) = acc.get(key) { + let mut paths = env::split_paths(val).collect::>(); + paths.push(PathBuf::from(env_key.value)); + acc.insert( + env_key.key, + env::join_paths(paths) + .unwrap() + .to_string_lossy() + .to_string(), + ); + } else { + acc.insert(key.clone(), env_key.value); + } + acc + })); + } + Err(e) => { + debug!("Backend method failed: {}", e); + return Ok(BTreeMap::new()); + } + } + } + + // Use default vfox behavior for traditional plugins Ok(vfox .env_keys(&self.pathname, &tv.version) .await? diff --git a/src/cli/args/backend_arg.rs b/src/cli/args/backend_arg.rs index af80f0db9e..cc8d36c8ec 100644 --- a/src/cli/args/backend_arg.rs +++ b/src/cli/args/backend_arg.rs @@ -107,6 +107,17 @@ impl BackendArg { if let Ok(Some(backend_type)) = install_state::backend_type(&self.short) { return backend_type; } + + // Check if this is a vfox plugin:tool format + if let Some((plugin_name, _tool_name)) = self.short.split_once(':') { + if let Some(plugin_type) = install_state::get_plugin_type(plugin_name) { + return match plugin_type { + PluginType::Vfox => BackendType::Vfox, + PluginType::Asdf => BackendType::Asdf, + }; + } + } + let full = self.full(); let backend = full.split(':').next().unwrap(); if let Ok(backend_type) = backend.parse() { @@ -155,6 +166,16 @@ impl BackendArg { full.clone() } else if let Some(full) = install_state::get_tool_full(short) { full + } else if let Some((plugin_name, _tool_name)) = short.split_once(':') { + // Check if this is a vfox plugin:tool format + if let Some(pt) = install_state::get_plugin_type(plugin_name) { + match pt { + PluginType::Asdf => format!("asdf:{short}"), + PluginType::Vfox => format!("vfox:{short}"), + } + } else { + short.to_string() + } } else if let Some(pt) = install_state::get_plugin_type(short) { match pt { PluginType::Asdf => format!("asdf:{short}"), diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs index 2194b435ae..9de47f4b9f 100644 --- a/src/plugins/mod.rs +++ b/src/plugins/mod.rs @@ -184,11 +184,25 @@ pub static VERSION_REGEX: Lazy = Lazy::new(|| { pub fn get(short: &str) -> Result { let (name, full) = short.split_once(':').unwrap_or((short, short)); - let plugin_type = if let Some(plugin_type) = install_state::list_plugins().get(short) { - *plugin_type + + // For plugin:tool format, look up the plugin by just the plugin name + let plugin_lookup_key = if short.contains(':') { + // Check if the part before the colon is a plugin name + if let Some(_plugin_type) = install_state::list_plugins().get(name) { + name + } else { + short + } } else { - PluginType::from_full(full)? + short }; + + let plugin_type = + if let Some(plugin_type) = install_state::list_plugins().get(plugin_lookup_key) { + *plugin_type + } else { + PluginType::from_full(full)? + }; Ok(plugin_type.plugin(name.to_string())) }