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
mise-vscode-extensions/
├── metadata.lua
├── mise.plugin.toml
└── hooks/
├── package_installed.lua
├── package_install.lua
├── package_upgrade.lua
└── package_uninstall.luaThe 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.
[package-manager]
requires = ["code"]
supports_version_pins = true
os = ["macos", "linux"]requireslists host binaries the hooks invoke. mise adds its shims and global toolset bin paths toPATH, but does not install these tools automatically; users declare them in[tools]or install them manually.supports_version_pinsdefaults tofalse.osis optional and defaults to every platform. Values use mise platform names such asmacos,linux, andwindows.
Hooks
Hooks are batch-oriented, but each hook receives the batch for its own phase:
PackageInstalledreceives every request in the current invocation. This may be the merged[bootstrap.packages]declarations or an explicit subset named on the command line.PackageInstallreceives only requests mise selected for installation, such as packages reported missing or at a mismatched requested version.PackageUpgradereceives 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.
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" },
},
}
endPackageInstalled 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.
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 {}
endPackageUpgrade 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:
function PLUGIN:PackageUninstall(ctx)
for _, package in ipairs(ctx.packages) do
-- uninstall package.name; package.version is the observed installed version
end
return {}
endDry 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
sudoin any hook. mise never elevates for them. - Version strings are opaque. Compare them with exact equality only; never parse or sort them.
PackageInstalledis 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.
PackageUninstallremoves 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.