Back to Mise

Package Plugin Development

docs/package-plugin-development.md

2026.8.125.4 KB
Original Source

Package Plugin Development

A package plugin is a Lua-based vfox plugin that implements a machine-global manager for [bootstrap.packages]. It wraps state owned by a host tool rather than installing versioned tools under mise's data directory.

Layout

text
mise-vscode-extensions/
├── metadata.lua
├── mise.plugin.toml
└── hooks/
    ├── package_installed.lua
    ├── package_install.lua
    ├── package_upgrade.lua
    └── package_uninstall.lua

The required hooks/package_installed.lua and hooks/package_install.lua pair identifies the repository as a package plugin. A repository with only one of these hooks remains a regular vfox plugin. If hooks/backend_install.lua is also present, mise treats the repository as a tool backend instead; package and tool-backend plugins must be separate repositories.

toml
[package-manager]
requires = ["code"]
supports_version_pins = true
os = ["macos", "linux"]
  • requires lists host binaries the hooks invoke. mise adds its shims and global toolset bin paths to PATH, but does not install these tools automatically; users declare them in [tools] or install them manually.
  • supports_version_pins defaults to false.
  • os is optional and defaults to every platform. Values use mise platform names such as macos, linux, and windows.

Hooks

Hooks are batch-oriented, but each hook receives the batch for its own phase:

  • PackageInstalled receives every request in the current invocation. This may be the merged [bootstrap.packages] declarations or an explicit subset named on the command line.
  • PackageInstall receives only requests mise selected for installation, such as packages reported missing or at a mismatched requested version.
  • PackageUpgrade receives the actionable requests reported as present, including packages that are already current so the manager can no-op them. Requests reported missing or unavailable and unsupported version pins are omitted.

mise does not call an action hook when its action batch is empty.

lua
function PLUGIN:PackageInstalled(ctx)
  -- ctx.packages: {{ name = "diff", version = "1.3.4" | nil }, ...}
  return {
    packages = {
      { name = "diff", state = "installed", version = "1.3.4" },
      { name = "s3", state = "missing" },
    },
  }
end

PackageInstalled must be side-effect free, fast, non-interactive, and never elevate. It must return one installed or missing entry for every request. mise computes a version mismatch when a requested pin is not exactly equal to the returned version.

lua
function PLUGIN:PackageInstall(ctx)
  -- ctx.dry_run: print intended actions and do nothing
  -- ctx.update: refresh manager metadata first when applicable
  for _, package in ipairs(ctx.packages) do
    -- install package.name, optionally at package.version
  end
  return {}
end

PackageUpgrade has the same context and response. It is optional; mise calls PackageInstall when the upgrade hook is absent.

An action batch is not a complete desired-state snapshot. An explicit command may target only a subset, and removing the final declaration for a manager produces no batch for that manager. A plugin must not infer that an identity should be removed merely because it is absent from ctx.packages.

PackageUninstall is optional and is used only by the explicit destructive command mise bootstrap packages prune --manager <plugin>. mise passes the concrete, approved removal batch after protecting packages declared by the current config and trusted, loadable tracked configs:

lua
function PLUGIN:PackageUninstall(ctx)
  for _, package in ipairs(ctx.packages) do
    -- uninstall package.name; package.version is the observed installed version
  end
  return {}
end

Dry runs do not invoke this hook. mise records ownership only when a package reported missing before PackageInstall is present afterwards. Packages that were already installed, including installations made before ownership tracking was introduced, are never claimed or sent to PackageUninstall. The ownership ledger persists across plugin removal and reinstallation. Explicit prune still works when the desired set is empty, including after the final declaration is removed. After the hook returns or fails, mise calls PackageInstalled to verify each removal when possible and retains ownership for anything still present. After confirmation, mise reloads the complete desired set before invoking the hook; newly declared packages are removed from the approved batch, and new removal candidates are never added without another confirmation.

Hard contracts

  • Package plugins must never invoke sudo in any hook. mise never elevates for them.
  • Version strings are opaque. Compare them with exact equality only; never parse or sort them.
  • PackageInstalled is side-effect free, non-interactive, never elevates, and should be fast.
  • Hooks operate on phase-specific batches and must not treat absence from a batch as an uninstall request.
  • PackageUninstall removes only the identities provided by mise and must not perform manager-wide orphan cleanup.
  • Declare every required host binary in requires.

For a VS Code implementation, PackageInstalled can parse code --list-extensions --show-versions, PackageInstall can run code --install-extension name[@version], and PackageUpgrade can run code --update-extensions or reinstall the requested extensions.