# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
from __future__ import annotations
import re
from extra_platforms import ALL_PLATFORMS
from ..capabilities import search_capabilities
from ..manager import PackageManager
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Iterator
from ..package import Package
[docs]
class Mise(PackageManager):
"""mise (formerly rtx) installs and switches between versions of developer
tools like Node, Python, Ruby and any tool reachable through its plugin
backends.
```{note}
`mpm` is system-scoped, so this wrapper reports every tool version
present on disk regardless of which `mise.toml` (global or project)
requested it. Project-pinned versions are not surfaced as a separate
scope.
```
```{note}
Backend-prefixed tool IDs (`pipx:ruff`, `cargo:ubi-cli`,
`asdf:mise-plugins/mise-poetry`) round-trip as-is. The colon is part
of the package ID; `mpm install pipx:ruff` resolves the backend
through `mise` itself.
```
```{caution}
`mise outdated --json` only reports tools tracked in a `mise.toml`
(global or project). A tool installed bare with `mise install <tool>`
and never pinned with `mise use` will not appear in the outdated
list, so `mpm outdated --mise` understates the upgrade surface for
those entries.
```
"""
name = "mise"
homepage_url = "https://mise.jdx.dev"
# No `logo`: Simple Icons carries no mise mark yet. Their request sits in the
# `permissions in review` stage, where they check with the trademark owner
# before drawing anything:
# https://github.com/simple-icons/simple-icons/issues/14747
platforms = ALL_PLATFORMS
requirement = ">=2025.5.10"
"""`mise search` shipped in `2025.5.10`, the binding floor for the
feature set this wrapper depends on. Earlier releases also miss the
`outdated --json` fix from `2025.2.8` that emits valid JSON when no
tool is outdated.
"""
version_regexes = (r"^(?P<version>\d+\.\d+\.\d+)",)
"""`mise` uses CalVer (`YYYY.M.P`), not SemVer.
```{code-block} shell-session
$ mise --version
2026.6.3 macos-arm64 (2026-06-13)
```
"""
_SEARCH_REGEXP = re.compile(
r"^(?P<package_id>\S+)\s{2,}(?P<description>.+)$",
re.MULTILINE,
)
@property
def installed(self) -> Iterator[Package]:
"""Fetch installed packages.
Emits one {class}`meta_package_manager.package.Package` per
`(tool, installed_version)` pair, so a tool installed at multiple
versions yields multiple entries sharing the same ID.
```{code-block} shell-session
$ mise ls --installed --json
{
"node": [
{
"version": "20.10.0",
"install_path": "~/.local/share/mise/installs/node/20.10.0",
"source": {"type": "mise.toml",
"path": "~/.config/mise/config.toml"}
}
],
"pipx:ruff": [
{
"version": "0.6.9",
"install_path": "~/.local/share/mise/installs/pipx-ruff/0.6.9"
}
]
}
```
"""
output = self.run_cli("ls", "--installed", "--json", must_succeed=True)
data = self.parse_json(output)
if not data:
return
for tool_id, entries in data.items():
for entry in entries:
yield self.package(
id=tool_id,
installed_version=entry["version"],
)
@property
def outdated(self) -> Iterator[Package]:
"""Fetch outdated packages.
```{code-block} shell-session
$ mise outdated --json
{
"node": {
"requested": "20",
"current": "20.0.0",
"latest": "20.10.0"
}
}
```
"""
output = self.run_cli("outdated", "--json", must_succeed=True)
data = self.parse_json(output)
if not data:
return
for tool_id, info in data.items():
yield self.package(
id=tool_id,
installed_version=info["current"],
latest_version=info["latest"],
)
[docs]
@search_capabilities(extended_support=True, exact_support=False)
def search(self, query: str, extended: bool, exact: bool) -> Iterator[Package]:
"""Fetch matching packages.
`mise search` returns a two-column `Tool Description` table.
`--match-type contains` keeps the candidate set wide; the framework's
{meth}`meta_package_manager.manager.PackageManager.refiltered_search`
narrows it down to honor `extended` and `exact` flags.
```{code-block} shell-session
$ mise search --no-header --match-type contains node
node Node.js
node-build Compile and install Node.js
nodejs alias for node
```
"""
output = self.run_cli(
"search",
"--no-header",
"--match-type",
"contains",
query,
)
for match in self._SEARCH_REGEXP.finditer(output):
yield self.package(
id=match.group("package_id"),
description=match.group("description").strip(),
)
[docs]
def install(self, package_id: str, version: str | None = None) -> str:
"""Install one package.
`mise install <tool>` resolves to the latest version compatible with
the active config; `mise install <tool>@<version>` pins it
explicitly. Neither variant writes to `mise.toml`: the dedicated
`mise use` command is the config-mutating verb and is deliberately
avoided here.
```{code-block} shell-session
$ mise install node@20
```
"""
spec = f"{package_id}@{version}" if version else package_id
return self.run_cli("install", spec)
[docs]
def upgrade_all_cli(self) -> tuple[str, ...]:
"""Generates the CLI to upgrade all packages.
```{code-block} shell-session
$ mise upgrade
```
"""
return self.build_cli("upgrade")
[docs]
def upgrade_one_cli(
self,
package_id: str,
version: str | None = None,
) -> tuple[str, ...]:
"""Generates the CLI to upgrade one package.
```{code-block} shell-session
$ mise upgrade node
```
"""
spec = f"{package_id}@{version}" if version else package_id
return self.build_cli("upgrade", spec)
[docs]
def remove(self, package_id: str) -> str:
"""Remove one package.
`mise uninstall <tool>` errors when more than one version of the
tool is installed. `--all` removes every installed version
unconditionally, which matches `mpm`'s "remove this package"
contract.
```{code-block} shell-session
$ mise uninstall --all node
```
"""
return self.run_cli("uninstall", "--all", package_id)
[docs]
def sync(self) -> None:
"""Refresh plugin metadata.
`mise` resolves tool listings and version catalogues through its
plugins, so updating the plugins is the closest equivalent to the
package-list refresh other managers perform during sync.
```{code-block} shell-session
$ mise plugins update
```
"""
self.run_cli("plugins", "update")
[docs]
def cleanup_cache(self) -> None:
"""Clear `mise`'s download and metadata caches.
```{code-block} shell-session
$ mise cache clear
```
"""
self.run_cli("cache", "clear")
[docs]
def doctor_cli(self) -> tuple[str, ...]:
"""Generates the CLI running the native self-diagnosis.
```{code-block} shell-session
$ mise doctor
```
"""
return self.build_cli("doctor")