docs/package-plugin-development.md
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.
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.
[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 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.
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.
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:
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.
sudo in any hook. mise never elevates for
them.PackageInstalled is side-effect free, non-interactive, never elevates, and
should be fast.PackageUninstall removes only the identities provided by mise and must not
perform manager-wide orphan cleanup.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.