diff --git a/docs/cli/ls-remote.md b/docs/cli/ls-remote.md index 70cf6b7291..41744275cc 100644 --- a/docs/cli/ls-remote.md +++ b/docs/cli/ls-remote.md @@ -36,9 +36,9 @@ Disable checking the mise-versions host ### `--prerelease` Include pre-release versions in the output for backends that report -an upstream prerelease flag (currently github + aqua). Equivalent to -setting `MISE_PRERELEASES=1` or the `prereleases` setting for the -duration of this command. +upstream prerelease metadata or opt in to regex-based prerelease +detection. Equivalent to setting `MISE_PRERELEASES=1` or the +`prereleases` setting for the duration of this command. ### `--strict-metadata` diff --git a/man/man1/mise.1 b/man/man1/mise.1 index b9dbb0d845..9f0f2df8f3 100644 --- a/man/man1/mise.1 +++ b/man/man1/mise.1 @@ -1517,9 +1517,9 @@ Disable checking the mise\-versions host .TP \fB\-\-prerelease\fR Include pre\-release versions in the output for backends that report -an upstream prerelease flag (currently github + aqua). Equivalent to -setting `MISE_PRERELEASES=1` or the `prereleases` setting for the -duration of this command. +upstream prerelease metadata or opt in to regex\-based prerelease +detection. Equivalent to setting `MISE_PRERELEASES=1` or the +`prereleases` setting for the duration of this command. .TP \fB\-\-strict\-metadata\fR Fail if release metadata fetches fail diff --git a/mise.usage.kdl b/mise.usage.kdl index e442794557..573ed8dcbe 100644 --- a/mise.usage.kdl +++ b/mise.usage.kdl @@ -596,7 +596,7 @@ cmd ls-remote help="List runtime versions available for install." { flag --all help="Show all installed plugins and versions" flag "-J --json" help="Output in JSON format (includes version metadata like created_at timestamps when available)" flag --no-versions-host help="Disable checking the mise-versions host" - flag --prerelease help="Include pre-release versions in the output for backends that report\nan upstream prerelease flag (currently github + aqua). Equivalent to\nsetting `MISE_PRERELEASES=1` or the `prereleases` setting for the\nduration of this command." + flag --prerelease help="Include pre-release versions in the output for backends that report\nupstream prerelease metadata or opt in to regex-based prerelease\ndetection. Equivalent to setting `MISE_PRERELEASES=1` or the\n`prereleases` setting for the duration of this command." flag --strict-metadata help="Fail if release metadata fetches fail" { long_help "Fail if release metadata fetches fail\n\nRequires --json and --no-versions-host.\n\nThis prevents metadata consumers from accepting empty fallback results\nwhen a backend's metadata-producing upstream request fails." } diff --git a/src/backend/asdf.rs b/src/backend/asdf.rs index 29a0b9b7ae..a2c2b1d15f 100644 --- a/src/backend/asdf.rs +++ b/src/backend/asdf.rs @@ -248,6 +248,10 @@ impl Backend for AsdfBackend { Some(PluginType::Asdf) } + fn mark_prereleases_from_version_pattern(&self) -> bool { + true + } + /// ASDF plugins handle their own downloads through plugin scripts. /// Lockfile URLs are not applicable since installation is delegated to plugin scripts. fn supports_lockfile_url(&self) -> bool { diff --git a/src/backend/cargo.rs b/src/backend/cargo.rs index 54063be403..6d73a2509d 100644 --- a/src/backend/cargo.rs +++ b/src/backend/cargo.rs @@ -50,6 +50,10 @@ impl Backend for CargoBackend { false } + fn mark_prereleases_from_version_pattern(&self) -> bool { + true + } + async fn _list_remote_versions(&self, _config: &Arc) -> eyre::Result> { if self.git_url().is_some() { // TODO: maybe fetch tags/branches from git? diff --git a/src/backend/conda.rs b/src/backend/conda.rs index b2797d043e..c5977b055d 100644 --- a/src/backend/conda.rs +++ b/src/backend/conda.rs @@ -1,6 +1,8 @@ -use crate::backend::VersionInfo; use crate::backend::backend_type::BackendType; use crate::backend::platform_target::PlatformTarget; +use crate::backend::{ + VersionInfo, filter_cached_prereleases, include_prereleases, mark_prerelease, +}; use crate::cli::args::BackendArg; use crate::config::Config; use crate::config::Settings; @@ -662,7 +664,18 @@ impl Backend for CondaBackend { &self, config: &Arc, ) -> Result> { - self._list_remote_versions(config).await + let opts = config + .get_tool_opts(&self.ba) + .await? + .unwrap_or_else(|| self.ba.opts()); + let want_prereleases = include_prereleases(&opts); + let versions = self + ._list_remote_versions(config) + .await? + .into_iter() + .map(mark_prerelease) + .collect(); + Ok(filter_cached_prereleases(versions, want_prereleases)) } async fn install_version_( diff --git a/src/backend/dotnet.rs b/src/backend/dotnet.rs index 7a342c107d..2ee62a374c 100644 --- a/src/backend/dotnet.rs +++ b/src/backend/dotnet.rs @@ -32,6 +32,10 @@ impl Backend for DotnetBackend { Ok(vec!["dotnet"]) } + fn mark_prereleases_from_version_pattern(&self) -> bool { + true + } + async fn _list_remote_versions(&self, _config: &Arc) -> eyre::Result> { let feed_url = self.get_search_url().await?; diff --git a/src/backend/gem.rs b/src/backend/gem.rs index 4701c3f98c..e94e208d20 100644 --- a/src/backend/gem.rs +++ b/src/backend/gem.rs @@ -39,6 +39,10 @@ impl Backend for GemBackend { Ok(vec!["ruby"]) } + fn mark_prereleases_from_version_pattern(&self) -> bool { + true + } + async fn _list_remote_versions(&self, config: &Arc) -> eyre::Result> { // Get the gem source URL using the mise-managed Ruby environment let source_url = self.get_gem_source(config).await; diff --git a/src/backend/go.rs b/src/backend/go.rs index fe96305c75..42703a841c 100644 --- a/src/backend/go.rs +++ b/src/backend/go.rs @@ -47,6 +47,10 @@ impl Backend for GoBackend { false } + fn mark_prereleases_from_version_pattern(&self) -> bool { + true + } + async fn _list_remote_versions(&self, config: &Arc) -> eyre::Result> { // Check if go is available self.warn_if_dependency_missing( diff --git a/src/backend/http.rs b/src/backend/http.rs index 25f96c69ee..7b3bf1fa79 100644 --- a/src/backend/http.rs +++ b/src/backend/http.rs @@ -607,6 +607,10 @@ impl Backend for HttpBackend { &self.ba } + fn mark_prereleases_from_version_pattern(&self) -> bool { + true + } + async fn install_operation_count(&self, tv: &ToolVersion, _ctx: &InstallContext) -> usize { let opts = tv.request.options(); super::http_install_operation_count( diff --git a/src/backend/mod.rs b/src/backend/mod.rs index b002e22158..e1386a3612 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -117,10 +117,10 @@ pub struct VersionInfo { /// Checksum of the release asset, used to detect changes in rolling releases #[serde(skip_serializing_if = "Option::is_none", default)] pub checksum: Option, - /// Whether the upstream flagged this as a pre-release. Backends that have a - /// reliable signal (e.g. GitHub releases' `prerelease: true`) populate this - /// so the shared remote-versions cache can store the superset and apply the - /// `prerelease` tool option as a read-time filter. + /// Whether this is a pre-release. Backends with a reliable upstream signal + /// (e.g. GitHub releases' `prerelease: true`) populate this directly. + /// Metadata-free listing backends can opt in to stamping this from mise's + /// legacy pre-release pattern before caching. #[serde(default, skip_serializing_if = "is_false")] pub prerelease: bool, } @@ -479,10 +479,8 @@ mod tests { #[test] fn test_filter_cached_prereleases_leaves_unflagged_backends_alone() { - // Backends that don't populate `VersionInfo.prerelease` (e.g. node, - // ruby, aqua's `github_tag` source) keep the legacy regex-based filter - // in `fuzzy_match_versions` for pre-release-shaped strings. The - // cache-layer filter must not strip those entries on its own. + // The cache-layer filter only trusts the metadata bit. Regex-shaped + // versions are stamped before they enter the cache. let cached = vec![ VersionInfo { version: "1.0.0".into(), @@ -498,6 +496,28 @@ mod tests { assert_eq!(versions, vec!["1.0.0", "1.1.0-rc1"]); } + #[test] + fn test_mark_prerelease_flags_regex_matches() { + let stable = mark_prerelease(VersionInfo { + version: "1.0.0".into(), + ..Default::default() + }); + assert!(!stable.prerelease); + + let rc = mark_prerelease(VersionInfo { + version: "1.1.0-rc1".into(), + ..Default::default() + }); + assert!(rc.prerelease); + + let already_flagged = mark_prerelease(VersionInfo { + version: "2.0.0".into(), + prerelease: true, + ..Default::default() + }); + assert!(already_flagged.prerelease); + } + #[test] fn test_include_prereleases_accepts_bool_and_string_values() { use crate::toolset::ToolVersionOptions; @@ -623,6 +643,12 @@ pub trait Backend: Debug + Send + Sync { fn get_dependencies(&self) -> Result> { Ok(vec![]) } + + /// Whether this backend's version source lacks an upstream prerelease flag + /// and should mark regex-shaped versions as prereleases before caching. + fn mark_prereleases_from_version_pattern(&self) -> bool { + false + } /// dependencies which wait for install but do not warn, like cargo-binstall fn get_optional_dependencies(&self) -> Result> { Ok(vec![]) @@ -816,6 +842,10 @@ pub trait Backend: Debug + Send + Sync { ._list_remote_versions(config) .await? .into_iter() + .map(|v| match self.mark_prereleases_from_version_pattern() { + true => mark_prerelease(v), + false => v, + }) .filter(|v| match v.version.parse::() { Ok(ToolVersionType::Version(_)) => true, _ => { @@ -2328,11 +2358,10 @@ fn find_match_in_list(list: &[String], query: &str) -> Option { } /// Apply the read-time `prerelease` filter to the cached remote-versions -/// superset. Backends that honor `prerelease` (github, aqua) cache the full -/// list and stamp `VersionInfo.prerelease` per entry; this helper drops -/// pre-release entries when the current tool opts don't opt in. Backends that -/// don't populate the flag are unaffected because their entries default to -/// `prerelease = false`. +/// superset. Backends cache the full list and stamp `VersionInfo.prerelease` +/// either from upstream metadata or, for metadata-free listing backends, mise's +/// legacy pre-release pattern. This helper drops pre-release entries when the +/// current tool opts don't opt in. pub(crate) fn filter_cached_prereleases( versions: Vec, want_prereleases: bool, @@ -2344,6 +2373,13 @@ pub(crate) fn filter_cached_prereleases( } } +pub(crate) fn mark_prerelease(mut version: VersionInfo) -> VersionInfo { + if !version.prerelease && VERSION_REGEX.is_match(&version.version) { + version.prerelease = true; + } + version +} + /// Whether pre-release versions should be included for the current tool. /// /// Returns true if either the global `prereleases` setting (`MISE_PRERELEASES=1`, diff --git a/src/backend/npm.rs b/src/backend/npm.rs index d2690b3ba5..737f40c1ee 100644 --- a/src/backend/npm.rs +++ b/src/backend/npm.rs @@ -52,6 +52,10 @@ impl Backend for NPMBackend { &self.ba } + fn mark_prereleases_from_version_pattern(&self) -> bool { + true + } + fn get_dependencies(&self) -> eyre::Result> { // npm CLI is always needed for version queries (npm view), plus the configured // package manager for installation. We avoid listing all package managers to diff --git a/src/backend/pipx.rs b/src/backend/pipx.rs index fb33790a5a..6a8f3c660b 100644 --- a/src/backend/pipx.rs +++ b/src/backend/pipx.rs @@ -52,6 +52,10 @@ impl Backend for PIPXBackend { Ok(vec!["uv"]) } + fn mark_prereleases_from_version_pattern(&self) -> bool { + true + } + /// Pipx installs packages from PyPI or Git using version specs (e.g., black==24.3.0). /// It doesn't support installing from direct URLs, so lockfile URLs are not applicable. fn supports_lockfile_url(&self) -> bool { diff --git a/src/backend/s3.rs b/src/backend/s3.rs index b50e1a4dc1..333d8b4ee2 100644 --- a/src/backend/s3.rs +++ b/src/backend/s3.rs @@ -418,6 +418,10 @@ impl Backend for S3Backend { &self.ba } + fn mark_prereleases_from_version_pattern(&self) -> bool { + true + } + async fn install_operation_count(&self, tv: &ToolVersion, _ctx: &InstallContext) -> usize { let opts = tv.request.options(); super::http_install_operation_count( diff --git a/src/backend/spm.rs b/src/backend/spm.rs index d4bc4f64c6..80816f4b95 100644 --- a/src/backend/spm.rs +++ b/src/backend/spm.rs @@ -44,6 +44,10 @@ impl Backend for SPMBackend { Ok(vec!["swift"]) } + fn mark_prereleases_from_version_pattern(&self) -> bool { + true + } + async fn _list_remote_versions(&self, _config: &Arc) -> eyre::Result> { let provider = GitProvider::from_ba(&self.ba); let repo = SwiftPackageRepo::new(&self.tool_name(), &provider)?; diff --git a/src/backend/ubi.rs b/src/backend/ubi.rs index c3695e1d1c..eef77748e0 100644 --- a/src/backend/ubi.rs +++ b/src/backend/ubi.rs @@ -39,6 +39,10 @@ impl Backend for UbiBackend { &self.ba } + fn mark_prereleases_from_version_pattern(&self) -> bool { + true + } + async fn _list_remote_versions(&self, _config: &Arc) -> eyre::Result> { deprecated_at!( "2026.4.0", diff --git a/src/backend/vfox.rs b/src/backend/vfox.rs index 249cffe0c9..e97c7b95e1 100644 --- a/src/backend/vfox.rs +++ b/src/backend/vfox.rs @@ -63,6 +63,10 @@ impl Backend for VfoxBackend { Ok(deps.iter().map(|s| s.as_str()).collect()) } + fn mark_prereleases_from_version_pattern(&self) -> bool { + true + } + fn supports_lockfile_url(&self) -> bool { // TODO: expose a plugin hook (e.g. BackendLockInfo) so custom Lua backends // can surface a download URL + checksum, and flip this back on for them. diff --git a/src/cli/ls_remote.rs b/src/cli/ls_remote.rs index 05e2018e61..82dd9f43a7 100644 --- a/src/cli/ls_remote.rs +++ b/src/cli/ls_remote.rs @@ -17,9 +17,8 @@ struct VersionOutputAll { version: String, #[serde(skip_serializing_if = "Option::is_none")] created_at: Option, - /// Upstream pre-release flag, sourced from the versions host or the - /// backend (currently github + aqua report this). Always emitted so - /// JSON consumers can rely on its presence. + /// Pre-release flag, sourced from upstream metadata or backend opt-in + /// detection. Always emitted so JSON consumers can rely on its presence. prerelease: bool, } @@ -52,9 +51,9 @@ pub struct LsRemote { pub no_versions_host: bool, /// Include pre-release versions in the output for backends that report - /// an upstream prerelease flag (currently github + aqua). Equivalent to - /// setting `MISE_PRERELEASES=1` or the `prereleases` setting for the - /// duration of this command. + /// upstream prerelease metadata or opt in to regex-based prerelease + /// detection. Equivalent to setting `MISE_PRERELEASES=1` or the + /// `prereleases` setting for the duration of this command. #[clap(long, verbatim_doc_comment)] pub prerelease: bool, diff --git a/src/plugins/core/java.rs b/src/plugins/core/java.rs index d918d2dba0..7c8965eacf 100644 --- a/src/plugins/core/java.rs +++ b/src/plugins/core/java.rs @@ -350,6 +350,7 @@ impl Backend for JavaPlugin { .map(|(v, m)| VersionInfo { version: v.clone(), created_at: m.created_at.clone(), + prerelease: VERSION_REGEX.is_match(v), ..Default::default() }) .unique_by(|v| v.version.clone()) diff --git a/src/versions_host.rs b/src/versions_host.rs index 0f81ba685b..96bc81f5fe 100644 --- a/src/versions_host.rs +++ b/src/versions_host.rs @@ -55,12 +55,11 @@ struct VersionEntry { created_at: toml::value::Datetime, #[serde(default)] release_url: Option, - /// Upstream pre-release flag, when the producing source can distinguish - /// it (currently github + aqua releases). Defaults to false so old - /// host data — and entries from sources that don't track prereleases — - /// stay correct without any schema upgrade. Old mise clients that don't - /// know about this field ignore it (toml-rs accepts unknown fields by - /// default), so populating it in mise-versions is forward-compatible. + /// Pre-release flag, when the producing source can distinguish it. Defaults + /// to false so old host data — and entries from sources that don't track + /// prereleases — stay correct without any schema upgrade. Old mise clients + /// that don't know about this field ignore it (toml-rs accepts unknown + /// fields by default), so populating it in mise-versions is forward-compatible. #[serde(default)] prerelease: bool, } diff --git a/xtasks/fig/src/mise.ts b/xtasks/fig/src/mise.ts index 5d024a0b8c..ec1d5e3709 100644 --- a/xtasks/fig/src/mise.ts +++ b/xtasks/fig/src/mise.ts @@ -1757,7 +1757,7 @@ const completionSpec: Fig.Spec = { { name: "--prerelease", description: - "Include pre-release versions in the output for backends that report\nan upstream prerelease flag (currently github + aqua). Equivalent to\nsetting `MISE_PRERELEASES=1` or the `prereleases` setting for the\nduration of this command.", + "Include pre-release versions in the output for backends that report\nupstream prerelease metadata or opt in to regex-based prerelease\ndetection. Equivalent to setting `MISE_PRERELEASES=1` or the\n`prereleases` setting for the duration of this command.", isRepeatable: false, }, {