docs/backend-plugin-development.md
::: tip The mise-backend-plugin-template provides a ready-to-use starting point with LuaCATS type definitions, stylua formatting, and hk linting pre-configured. :::
Backend plugins in mise use enhanced backend methods to manage multiple tools using the plugin:tool format. These plugins are perfect for package managers, tool families, and custom installations that need to manage multiple related tools.
Backend plugins extend the standard vfox plugin system with enhanced backend methods. They support:
vfox-npm is the plugin which could install different types of tools like prettier, eslint, and other npm packagesBackend plugins are generally a git repository but can also be a directory (via mise link).
Backend plugins are implemented in Lua (version 5.1 at the moment). They use three main backend methods implemented as individual files:
hooks/backend_list_versions.lua - Lists available versions for a toolhooks/backend_install.lua - Installs a specific version of a toolhooks/backend_exec_env.lua - Sets up environment variables for a toolLists available versions for a tool:
function PLUGIN:BackendListVersions(ctx)
local tool = ctx.tool
local options = ctx.options
local versions = {}
-- Your logic to fetch versions for the tool
-- Example: query an API, parse a registry, etc.
-- Access custom options via options["key"] or options.key
return {versions = versions}
end
[!WARNING] Version sorting: The versions returned by
BackendListVersionsshould be in ascending order (oldest to newest), sorted semantically (version3.10.0should not come before3.2.0). Mise does not apply any additional sorting to the versions returned by this method.
Installs a specific version of a tool:
function PLUGIN:BackendInstall(ctx)
local tool = ctx.tool
local version = ctx.version
local install_path = ctx.install_path
local download_path = ctx.download_path
local options = ctx.options
-- Your logic to install the tool
-- Example: download files, extract archives, etc.
-- Access custom options via options["key"] or options.key
return {}
end
Sets up environment variables for a tool:
function PLUGIN:BackendExecEnv(ctx)
local install_path = ctx.install_path
local options = ctx.options
-- Your logic to set up environment variables
-- Example: add bin directories to PATH
-- Access custom options via options["key"] or options.key
return {
env_vars = {
{key = "PATH", value = install_path .. "/bin"}
}
}
end
Use the dedicated mise-backend-plugin-template for creating backend plugins:
# Option 1: Use GitHub's template feature (recommended)
# Visit https://github.com/jdx/mise-backend-plugin-template
# Click "Use this template" to create your repository
# Option 2: Clone and modify
git clone https://github.com/jdx/mise-backend-plugin-template my-backend-plugin
cd my-backend-plugin
rm -rf .git
git init
The template includes:
Create a directory with this structure:
my-backend-plugin/
├── metadata.lua # Plugin metadata
├── hooks/
│ ├── backend_list_versions.lua # BackendListVersions hook
│ ├── backend_install.lua # BackendInstall hook
│ └── backend_exec_env.lua # BackendExecEnv hook
└── Injection.lua # Runtime injection (auto-generated)
PLUGIN = {
name = "vfox-npm",
version = "1.0.0",
description = "Backend plugin for npm packages",
author = "Your Name"
}
Here's the complete implementation of the vfox-npm plugin that manages npm packages:
PLUGIN = {
name = "vfox-npm",
version = "1.0.0",
description = "Backend plugin for npm packages",
author = "jdx"
}
function PLUGIN:BackendListVersions(ctx)
local cmd = require("cmd")
local json = require("json")
local result = cmd.exec("npm view " .. ctx.tool .. " versions --json")
local versions = json.decode(result)
return {versions = versions}
end
function PLUGIN:BackendInstall(ctx)
local tool = ctx.tool
local version = ctx.version
local install_path = ctx.install_path
-- Install the package directly using npm install
local cmd = require("cmd")
local npm_cmd = "npm install " .. tool .. "@" .. version .. " --no-package-lock --no-save --silent"
local result = cmd.exec(npm_cmd, {cwd = install_path})
-- If we get here, the command succeeded
return {}
end
function PLUGIN:BackendExecEnv(ctx)
local file = require("file")
return {
env_vars = {
{key = "PATH", value = file.join_path(ctx.install_path, "node_modules", ".bin")}
}
}
end
The plugin name doesn't have to match the repository name. The backend prefix will match whatever name the backend plugin was installed as.
# Install the plugin
mise plugin install vfox-npm https://github.com/jdx/vfox-npm
# List available versions
mise ls-remote vfox-npm:prettier
# Install a specific version
mise install vfox-npm:[email protected]
# Use in a project
mise use vfox-npm:prettier@latest
# Execute the tool
mise exec -- prettier --help
Tip: This naming flexibility could potentially be used to have a very complex plugin backend that would behave differently based on what it was named. For example, you could install the same plugin with different names to configure different behaviors or access different tool registries.
Backend plugins receive context through the ctx parameter passed to each hook function:
| Variable | Description | Example |
|---|---|---|
ctx.tool | The tool name | "prettier" |
ctx.options | Tool options from mise.toml | {channels = {"a", "b"}} |
| Variable | Description | Example |
|---|---|---|
ctx.tool | The tool name | "prettier" |
ctx.version | The requested version | "3.0.0" |
ctx.install_path | Installation directory | "/home/user/.local/share/mise/installs/vfox-npm-prettier/3.0.0" |
ctx.download_path | Download directory | "/home/user/.local/share/mise/downloads/vfox-npm-prettier/3.0.0" |
ctx.options | Tool options from mise.toml | {exe = "rg"} |
| Variable | Description | Example |
|---|---|---|
ctx.tool | The tool name | "prettier" |
ctx.version | The requested version | "3.0.0" |
ctx.install_path | Installation directory | "/home/user/.local/share/mise/installs/vfox-npm-prettier/3.0.0" |
ctx.options | Tool options from mise.toml | {exe = "rg"} |
[!TIP] Option values preserve their TOML types as native Lua equivalents. Strings remain strings, arrays become Lua sequence tables, and nested tables become Lua map tables. For example,
channels = ["conda-forge", "robostack"]inmise.tomlbecomes a Lua table you can iterate withipairs(ctx.options.channels).
# Link your plugin for development
mise plugin link my-plugin /path/to/my-plugin
# Test listing versions
mise ls-remote my-plugin:some-tool
# Test installation
mise use my-plugin:[email protected]
# Test execution
mise exec -- some-tool --version
Use debug mode to see detailed plugin execution:
mise --debug install my-plugin:[email protected]
Provide more meaningful error messages:
function PLUGIN:BackendListVersions(ctx)
local tool = ctx.tool
-- Validate tool name
if not tool or tool == "" then
error("Tool name cannot be empty")
end
-- Execute command with error checking
local cmd = require("cmd")
local result = cmd.exec("npm view " .. tool .. " versions --json 2>/dev/null")
if not result or result:match("npm ERR!") then
error("Failed to fetch versions for " .. tool .. ": " .. (result or "no output"))
end
-- Parse JSON response
local json = require("json")
local success, npm_versions = pcall(json.decode, result)
if not success or not npm_versions then
error("Failed to parse versions for " .. tool)
end
-- Return versions or error if none found
local versions = {}
if type(npm_versions) == "table" then
for i = #npm_versions, 1, -1 do
table.insert(versions, npm_versions[i])
end
end
if #versions == 0 then
error("No versions found for " .. tool)
end
return {versions = versions}
end
Parse versions with regex:
local function parse_version(version_string)
-- Remove prefixes like 'v' or 'release-'
return version_string:gsub("^v", ""):gsub("^release%-", "")
end
Use cross-platform path handling:
local function join_path(...)
local sep = package.config:sub(1,1) -- Get OS path separator
return table.concat({...}, sep)
end
local bin_path = join_path(install_path, "bin")
Handle different operating systems:
local function create_dir(path)
local cmd = RUNTIME.osType == "windows" and "mkdir" or "mkdir -p"
os.execute(cmd .. " " .. path)
end
Different installation logic based on tool or version:
function PLUGIN:BackendInstall(ctx)
local tool = ctx.tool
local version = ctx.version
local install_path = ctx.install_path
-- Create install directory
os.execute("mkdir -p " .. install_path)
if tool == "special-tool" then
-- Special installation logic
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 result:match("npm ERR!") then
error("Failed to install " .. tool .. "@" .. version)
end
else
-- Default installation logic
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 result:match("npm ERR!") then
error("Failed to install " .. tool .. "@" .. version)
end
end
return {}
end
vfox automatically injects runtime information into your plugin:
function PLUGIN:BackendInstall(ctx)
-- Platform-specific installation using injected RUNTIME object
if RUNTIME.osType == "darwin" then
-- macOS installation logic
elseif RUNTIME.osType == "linux" then
-- Linux installation logic
elseif RUNTIME.osType == "windows" then
-- Windows installation logic
end
return {}
end
The RUNTIME object provides:
RUNTIME.osType: Operating system type ("windows", "linux", "darwin")RUNTIME.archType: Architecture ("amd64", "arm64", "x86", etc.)RUNTIME.envType: libc environment type ("gnu" on glibc Linux, "musl" on musl Linux, nil on Windows/macOS and undetected systems)RUNTIME.version: vfox runtime versionRUNTIME.pluginDirPath: Plugin directory pathSet multiple environment variables:
function PLUGIN:BackendExecEnv(ctx)
-- Add node_modules/.bin to PATH for npm-installed binaries
local bin_path = ctx.install_path .. "/node_modules/.bin"
return {
env_vars = {
{key = "PATH", value = bin_path},
{key = ctx.tool:upper() .. "_HOME", value = ctx.install_path},
{key = ctx.tool:upper() .. "_VERSION", value = ctx.version}
}
}
end
TODO: We need caching support for Shared Lua modules.