# Backend Plugin Development

::: tip
The [mise-backend-plugin-template](https://github.com/jdx/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.

## What are Backend Plugins?

Backend plugins extend the standard vfox plugin system with enhanced backend methods. They support:

- **Multiple Tools**: One plugin can manage multiple tools. For example, `vfox-npm` is the plugin which could install different types of tools like `prettier`, `eslint`, and other npm packages
- **Cross-Platform Support**: Works on Windows, macOS, and Linux
- **Flexible Architecture**: Modern plugin system with dedicated backend methods for enhanced functionality

## Plugin Architecture

Backend 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 tool
- `hooks/backend_install.lua` - Installs a specific version of a tool
- `hooks/backend_exec_env.lua` - Sets up environment variables for a tool

## Backend Methods

### BackendListVersions

Lists available versions for a tool:

```lua
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 `BackendListVersions` should be in ascending order (oldest to newest), sorted semantically (version `3.10.0` should not come before `3.2.0`). Mise does not apply any additional sorting to the versions returned by this method.

### BackendInstall

Installs a specific version of a tool:

```lua
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
```

### BackendExecEnv

Sets up environment variables for a tool:

```lua
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
```

## Creating a Backend Plugin

### Using the Template Repository

Use the dedicated [mise-backend-plugin-template](https://github.com/jdx/mise-backend-plugin-template) for creating backend plugins:

```bash
# 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:

- Complete backend plugin structure with all required hooks
- Modern development tooling (hk, stylua, luacheck, actionlint)
- Comprehensive documentation and examples
- CI/CD setup with GitHub Actions
- Multiple implementation patterns for different backend types

### 1. Plugin Structure

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)
```

### 2. Basic metadata.lua

```lua
PLUGIN = {
    name = "vfox-npm",
    version = "1.0.0",
    description = "Backend plugin for npm packages",
    author = "Your Name"
}
```

## Real-World Example: vfox-npm

Here's the complete implementation of the vfox-npm plugin that manages npm packages:

### metadata.lua

```lua
PLUGIN = {
    name = "vfox-npm",
    version = "1.0.0",
    description = "Backend plugin for npm packages",
    author = "jdx"
}
```

### hooks/backend_list_versions.lua

```lua
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
```

### hooks/backend_install.lua

```lua
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
```

### hooks/backend_exec_env.lua

```lua
function PLUGIN:BackendExecEnv(ctx)
    local file = require("file")
    return {
        env_vars = {
            {key = "PATH", value = file.join_path(ctx.install_path, "node_modules", ".bin")}
        }
    }
end
```

## Usage Example

The plugin name doesn't have to match the repository name. The backend prefix will match whatever name the backend plugin was installed as.

```bash
# 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:prettier@3.0.0

# 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.

## Context Variables

Backend plugins receive context through the `ctx` parameter passed to each hook function:

### BackendListVersions Context

| Variable      | Description                 | Example                   |
| ------------- | --------------------------- | ------------------------- |
| `ctx.tool`    | The tool name               | `"prettier"`              |
| `ctx.options` | Tool options from mise.toml | `{channels = {"a", "b"}}` |

### BackendInstall Context

| 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"}`                                                     |

### BackendExecEnv Context

| 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"]` in `mise.toml` becomes a Lua table you can
> iterate with `ipairs(ctx.options.channels)`.

## Testing Your Plugin

### Local Development

```bash
# 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:some-tool@1.0.0

# Test execution
mise exec -- some-tool --version
```

### Debug Mode

Use debug mode to see detailed plugin execution:

```bash
mise --debug install my-plugin:some-tool@1.0.0
```

## Best Practices

### Error Handling

Provide more meaningful error messages:

```lua
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
```

### Regex Parsing

Parse versions with regex:

```lua
local function parse_version(version_string)
    -- Remove prefixes like 'v' or 'release-'
    return version_string:gsub("^v", ""):gsub("^release%-", "")
end
```

### Path Handling

Use cross-platform path handling:

```lua
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")
```

### Cross-Platform Commands

Handle different operating systems:

```lua
local function create_dir(path)
    local cmd = RUNTIME.osType == "windows" and "mkdir" or "mkdir -p"
    os.execute(cmd .. " " .. path)
end
```

## Advanced Features

### Conditional Installation

Different installation logic based on tool or version:

```lua
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
```

### Environment Detection

vfox automatically injects runtime information into your plugin:

```lua
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 version
- `RUNTIME.pluginDirPath`: Plugin directory path

### Multiple Environment Variables

Set multiple environment variables:

```lua
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
```

## Performance Optimization

### Caching

TODO: We need caching support for [Shared Lua modules](plugin-lua-modules.md).

## Next Steps

- [Start with the backend plugin template](https://github.com/jdx/mise-backend-plugin-template)
- [Learn about Tool Plugin Development](tool-plugin-development.md)
- [Explore available Lua modules](plugin-lua-modules.md)
- [Publishing your plugin](plugin-publishing.md)
- [View the vfox-npm plugin source](https://github.com/jdx/vfox-npm)
