Tool Plugin Development
Tool plugins use a hook-based architecture to manage individual tools. They are compatible with the standard vfox ecosystem and are perfect for tools that need complex installation logic, environment configuration, or legacy file parsing.
What are Tool Plugins?
Tool plugins use traditional hook functions to manage a single tool. They provide:
- Standard vfox Compatibility: Works with both mise and vfox
- Complex Installation Logic: Handle source compilation, custom builds, and complex setups
- Environment Configuration: Set up complex environment variables beyond just PATH
- Legacy File Support: Parse version files from other tools (
.nvmrc
,.tool-version
, etc.) - Cross-Platform Support: Works on Windows, macOS, and Linux
Plugin Architecture
Tool plugins use a hook-based architecture with specific functions for different lifecycle events:
Hook Functions
Required Hooks
These hooks must be implemented for a functional plugin:
Available Hook
Lists all available versions of the tool:
-- hooks/available.lua
function PLUGIN:Available(ctx)
local args = ctx.args -- User arguments
-- Return array of available versions
return {
{
version = "20.0.0",
note = "Latest"
},
{
version = "18.18.0",
note = "LTS",
addition = {
{
name = "npm",
version = "9.8.1"
}
}
}
}
end
PreInstall Hook
Handles pre-installation logic and returns download information:
-- hooks/pre_install.lua
function PLUGIN:PreInstall(ctx)
local version = ctx.version
local runtimeVersion = ctx.runtimeVersion
-- Determine download URL and checksums
local url = "https://nodejs.org/dist/v" .. version .. "/node-v" .. version .. "-linux-x64.tar.gz"
return {
version = version,
url = url,
sha256 = "abc123...", -- Optional checksum
note = "Installing Node.js " .. version,
-- Additional files can be specified
addition = {
{
name = "npm",
url = "https://registry.npmjs.org/npm/-/npm-" .. npm_version .. ".tgz"
}
}
}
end
EnvKeys Hook
Configures environment variables for the installed tool:
-- hooks/env_keys.lua
function PLUGIN:EnvKeys(ctx)
local mainPath = ctx.path
local runtimeVersion = ctx.runtimeVersion
local sdkInfo = ctx.sdkInfo['nodejs']
local path = sdkInfo.path
local version = sdkInfo.version
local name = sdkInfo.name
return {
{
key = "NODE_HOME",
value = mainPath
},
{
key = "PATH",
value = mainPath .. "/bin"
},
-- Multiple PATH entries are automatically merged
{
key = "PATH",
value = mainPath .. "/lib/node_modules/.bin"
}
}
end
Optional Hooks
These hooks provide additional functionality:
PostInstall Hook
Performs additional setup after installation:
-- hooks/post_install.lua
function PLUGIN:PostInstall(ctx)
local rootPath = ctx.rootPath
local runtimeVersion = ctx.runtimeVersion
local sdkInfo = ctx.sdkInfo['nodejs']
local path = sdkInfo.path
local version = sdkInfo.version
-- Compile native modules, set permissions, etc.
local result = os.execute("chmod +x " .. path .. "/bin/*")
if result ~= 0 then
error("Failed to set permissions")
end
-- No return value needed
end
PreUse Hook
Modifies version before use:
-- hooks/pre_use.lua
function PLUGIN:PreUse(ctx)
local version = ctx.version
local previousVersion = ctx.previousVersion
local installedSdks = ctx.installedSdks
local cwd = ctx.cwd
local scope = ctx.scope -- global/project/session
-- Optionally modify the version
if version == "latest" then
version = "20.0.0" -- Resolve to specific version
end
return {
version = version
}
end
ParseLegacyFile Hook
Parses version files from other tools:
-- hooks/parse_legacy_file.lua
function PLUGIN:ParseLegacyFile(ctx)
local filename = ctx.filename
local filepath = ctx.filepath
local versions = ctx:getInstalledVersions()
-- Read and parse the file
local file = require("file")
local content = file.read(filepath)
local version = content:match("v?([%d%.]+)")
return {
version = version
}
end
Creating a Tool Plugin
1. Plugin Structure
Create a directory with this structure:
nodejs-plugin/
├── metadata.lua # Plugin metadata and configuration
├── hooks/ # Hook functions directory
│ ├── available.lua # List available versions [required]
│ ├── pre_install.lua # Pre-installation hook [required]
│ ├── env_keys.lua # Environment configuration [required]
│ ├── post_install.lua # Post-installation hook [optional]
│ ├── pre_use.lua # Pre-use hook [optional]
│ └── parse_legacy_file.lua # Legacy file parser [optional]
├── lib/ # Shared library code [optional]
│ └── helper.lua # Helper functions
└── test/ # Test scripts [optional]
└── test.sh
2. metadata.lua
Configure plugin metadata and legacy file support:
-- metadata.lua
PLUGIN = {
name = "nodejs",
version = "1.0.0",
description = "Node.js runtime environment",
author = "Plugin Author",
-- Legacy version files this plugin can parse
legacyFilenames = {
'.nvmrc',
'.node-version'
}
}
3. Helper Libraries
Create shared functions in the lib/
directory:
-- lib/helper.lua
local M = {}
function M.get_arch()
local arch = os.getenv("PROCESSOR_ARCHITECTURE") or os.capture("uname -m")
if arch:match("x86_64") or arch:match("AMD64") then
return "x64"
elseif arch:match("i386") or arch:match("i686") then
return "x86"
elseif arch:match("arm64") or arch:match("aarch64") then
return "arm64"
else
return "x64" -- default
end
end
function M.get_os()
if package.config:sub(1,1) == '\\' then
return "win"
else
local os_name = os.capture("uname"):lower()
if os_name:find("darwin") then
return "darwin"
else
return "linux"
end
end
end
function M.get_platform()
return M.get_os() .. "-" .. M.get_arch()
end
return M
Real-World Example: vfox-nodejs
Here's a complete example based on the vfox-nodejs plugin that demonstrates all the concepts:
Available Hook Example
-- hooks/available.lua
function PLUGIN:Available(ctx)
local http = require("http")
local json = require("json")
-- Fetch versions from Node.js API
local resp, err = http.get({
url = "https://nodejs.org/dist/index.json"
})
if err ~= nil then
error("Failed to fetch versions: " .. err)
end
local versions = json.decode(resp.body)
local result = {}
for i, v in ipairs(versions) do
local version = v.version:gsub("^v", "") -- Remove 'v' prefix
local note = nil
if v.lts then
note = "LTS"
end
table.insert(result, {
version = version,
note = note,
addition = {
{
name = "npm",
version = v.npm
}
}
})
end
return result
end
PreInstall Hook Example
-- hooks/pre_install.lua
function PLUGIN:PreInstall(ctx)
local version = ctx.version
local helper = require("lib/helper")
-- Determine platform
local platform = helper.get_platform()
local extension = platform:match("win") and "zip" or "tar.gz"
-- Build download URL
local filename = "node-v" .. version .. "-" .. platform .. "." .. extension
local url = "https://nodejs.org/dist/v" .. version .. "/" .. filename
-- Fetch checksum
local http = require("http")
local shasums_url = "https://nodejs.org/dist/v" .. version .. "/SHASUMS256.txt"
local resp, err = http.get({ url = shasums_url })
local sha256 = nil
if err == nil then
-- Extract SHA256 for our file
for line in resp.body:gmatch("[^\n]+") do
if line:match(filename) then
sha256 = line:match("^(%w+)")
break
end
end
end
return {
version = version,
url = url,
sha256 = sha256,
note = "Installing Node.js " .. version .. " (" .. platform .. ")"
}
end
EnvKeys Hook Example
-- hooks/env_keys.lua
function PLUGIN:EnvKeys(ctx)
local mainPath = ctx.path
local helper = require("lib/helper")
local os_type = helper.get_os()
local env_vars = {
{
key = "NODE_HOME",
value = mainPath
},
{
key = "PATH",
value = mainPath .. "/bin"
}
}
-- Add npm global modules to PATH
local npm_global_path = mainPath .. "/lib/node_modules/.bin"
if os_type == "win" then
npm_global_path = mainPath .. "/node_modules/.bin"
end
table.insert(env_vars, {
key = "PATH",
value = npm_global_path
})
return env_vars
end
PostInstall Hook Example
-- hooks/post_install.lua
function PLUGIN:PostInstall(ctx)
local sdkInfo = ctx.sdkInfo['nodejs']
local path = sdkInfo.path
local helper = require("lib/helper")
-- Set executable permissions on Unix systems
if helper.get_os() ~= "win" then
os.execute("chmod +x " .. path .. "/bin/*")
end
-- Create npm cache directory
local npm_cache_dir = path .. "/.npm"
os.execute("mkdir -p " .. npm_cache_dir)
-- Configure npm to use local cache
local npm_cmd = path .. "/bin/npm"
if helper.get_os() == "win" then
npm_cmd = path .. "/npm.cmd"
end
os.execute(npm_cmd .. " config set cache " .. npm_cache_dir)
os.execute(npm_cmd .. " config set prefix " .. path)
end
Legacy File Support
-- hooks/parse_legacy_file.lua
function PLUGIN:ParseLegacyFile(ctx)
local filename = ctx.filename
local filepath = ctx.filepath
local file = require("file")
-- Read file content
local content = file.read(filepath)
if not content then
error("Failed to read " .. filepath)
end
-- Parse version from different file formats
local version = nil
if filename == ".nvmrc" then
-- .nvmrc can contain version with or without 'v' prefix
version = content:match("v?([%d%.]+)")
elseif filename == ".node-version" then
-- .node-version typically contains just the version number
version = content:match("([%d%.]+)")
end
-- Remove any whitespace
if version then
version = version:gsub("%s+", "")
end
return {
version = version
}
end
Testing Your Plugin
Local Development
# Link your plugin for development
mise plugin link nodejs /path/to/nodejs-plugin
# Test listing versions
mise ls-remote nodejs
# Test installation
mise install [email protected]
# Test environment setup
mise use [email protected]
node --version
# Test legacy file parsing
echo "18.18.0" > .nvmrc
mise use nodejs
Debug Mode
Use debug mode to see detailed plugin execution:
mise --debug install [email protected]
Plugin Test Script
Create a comprehensive test script:
#!/bin/bash
# test/test.sh
set -e
echo "Testing nodejs plugin..."
# Install the plugin
mise plugin install nodejs .
# Test basic functionality
mise install [email protected]
mise use [email protected]
# Verify installation
node --version | grep "18.18.0"
npm --version
# Test legacy file support
echo "20.0.0" > .nvmrc
mise use nodejs
node --version | grep "20.0.0"
# Clean up
rm -f .nvmrc
mise plugin remove nodejs
echo "All tests passed!"
Best Practices
Error Handling
Always provide meaningful error messages:
function PLUGIN:Available(ctx)
local http = require("http")
local resp, err = http.get({
url = "https://api.example.com/versions"
})
if err ~= nil then
error("Failed to fetch versions from API: " .. err)
end
if resp.status_code ~= 200 then
error("API returned status " .. resp.status_code .. ": " .. resp.body)
end
-- Process response...
end
Platform Detection
Handle different operating systems properly:
-- lib/platform.lua
local M = {}
function M.is_windows()
return package.config:sub(1,1) == '\\'
end
function M.get_exe_extension()
return M.is_windows() and ".exe" or ""
end
function M.get_path_separator()
return M.is_windows() and "\\" or "/"
end
return M
Version Normalization
Normalize versions consistently:
local function normalize_version(version)
-- Remove 'v' prefix if present
version = version:gsub("^v", "")
-- Remove pre-release suffixes
version = version:gsub("%-.*", "")
return version
end
Caching
Cache expensive operations:
-- Cache versions for 12 hours
local cache = {}
local cache_ttl = 12 * 60 * 60 -- 12 hours in seconds
function PLUGIN:Available(ctx)
local now = os.time()
-- Check cache first
if cache.versions and cache.timestamp and (now - cache.timestamp) < cache_ttl then
return cache.versions
end
-- Fetch fresh data
local versions = fetch_versions_from_api()
-- Update cache
cache.versions = versions
cache.timestamp = now
return versions
end
Advanced Features
Conditional Installation
Different installation logic based on platform or version:
function PLUGIN:PreInstall(ctx)
local version = ctx.version
local helper = require("lib/helper")
local platform = helper.get_platform()
-- Different logic for different platforms
if platform:match("win") then
-- Windows-specific installation
return install_windows(version)
elseif platform:match("darwin") then
-- macOS-specific installation
return install_macos(version)
else
-- Linux installation
return install_linux(version)
end
end
Source Compilation
For plugins that need to compile from source:
-- hooks/post_install.lua
function PLUGIN:PostInstall(ctx)
local sdkInfo = ctx.sdkInfo['tool-name']
local path = sdkInfo.path
local version = sdkInfo.version
-- Change to source directory
local build_dir = path .. "/src"
-- Configure build
local configure_result = os.execute("cd " .. build_dir .. " && ./configure --prefix=" .. path)
if configure_result ~= 0 then
error("Configure failed")
end
-- Compile
local make_result = os.execute("cd " .. build_dir .. " && make -j$(nproc)")
if make_result ~= 0 then
error("Compilation failed")
end
-- Install
local install_result = os.execute("cd " .. build_dir .. " && make install")
if install_result ~= 0 then
error("Installation failed")
end
end
Environment Configuration
Complex environment variable setup:
function PLUGIN:EnvKeys(ctx)
local mainPath = ctx.path
local version = ctx.sdkInfo['tool-name'].version
local env_vars = {
-- Standard environment variables
{
key = "TOOL_HOME",
value = mainPath
},
{
key = "TOOL_VERSION",
value = version
},
-- PATH entries
{
key = "PATH",
value = mainPath .. "/bin"
},
{
key = "PATH",
value = mainPath .. "/scripts"
},
-- Library paths
{
key = "LD_LIBRARY_PATH",
value = mainPath .. "/lib"
},
{
key = "PKG_CONFIG_PATH",
value = mainPath .. "/lib/pkgconfig"
}
}
-- Platform-specific additions
local helper = require("lib/helper")
if helper.get_os() == "darwin" then
table.insert(env_vars, {
key = "DYLD_LIBRARY_PATH",
value = mainPath .. "/lib"
})
end
return env_vars
end