From 8b45ffee63f3e78f0e5246b7dd990584654c7f52 Mon Sep 17 00:00:00 2001 From: Atulya Date: Wed, 4 Nov 2026 10:10:43 +0530 Subject: [PATCH 1/3] fix(provider): bun global updates advertise a version bun will install --- .../src/server/maintenanceResolver.test.ts | 41 +++++++++ .../src/server/maintenanceResolver.ts | 83 +++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/packages/provider-core/src/server/maintenanceResolver.test.ts b/packages/provider-core/src/server/maintenanceResolver.test.ts index 9447742ca354b..dd360ed613438 100644 --- a/packages/provider-core/src/server/maintenanceResolver.test.ts +++ b/packages/provider-core/src/server/maintenanceResolver.test.ts @@ -31,11 +31,13 @@ import { makeProviderMaintenanceCapabilities, normalizeCommandPath, npmGlobalPrefixFromCommandPath, + parseBunReleaseAgeConfig, parseChocolateyLatestVersion, parseHomebrewLatestVersion, resolveLatestProviderVersion, resolvePackageManagedProviderMaintenance, resolveProviderMaintenanceCapabilitiesEffect, + selectReleaseAgeEligibleVersion, type PackageManagedProviderMaintenanceDefinition, type ProviderMaintenanceCapabilities, } from "./maintenanceResolver.ts"; @@ -1660,3 +1662,42 @@ it("pins only owned package-manager installs and preserves their execution conte } expect(makeTargetedProviderUpdateAction(capabilities, "2.0.0; rm -rf /")).toBeNull(); }); + +it("reads bun's release-age limit and excludes from the install table only", () => { + expect( + parseBunReleaseAgeConfig( + [ + "[run]", + "minimumReleaseAge = 1", + "[install]", + "minimumReleaseAge = 604800 # 7 days", + 'minimumReleaseAgeExcludes = ["@openai/codex", "typescript"]', + ].join("\n"), + ), + ).toEqual({ seconds: 604800, excludes: ["@openai/codex", "typescript"] }); + expect(parseBunReleaseAgeConfig('[install]\nregistry = "https://example.test"\n')).toBeNull(); +}); + +it("picks the newest stable version published before the release-age cutoff", () => { + const nowMs = Date.parse("2026-11-04T04:20:00Z"); + const times = { + created: "2026-10-01T00:00:00Z", + modified: "2026-11-03T00:00:00Z", + "2.2.0": "2026-10-27T08:59:34Z", + "2.2.1": "2026-10-29T05:12:41Z", + "2.2.5": "2026-10-30T23:14:07Z", + "2.3.0-beta.1": "2026-10-01T00:00:00Z", + "0.0.0-dev-565": "2026-11-04T04:11:56Z", + }; + expect( + selectReleaseAgeEligibleVersion({ times, nowMs, minimumAgeSeconds: 604_800 }), + ).toBe("2.2.0"); + expect(selectReleaseAgeEligibleVersion({ times, nowMs, minimumAgeSeconds: 0 })).toBe("2.2.5"); + expect( + selectReleaseAgeEligibleVersion({ + times: { "1.0.0": "2026-11-03T00:00:00Z" }, + nowMs, + minimumAgeSeconds: 604_800, + }), + ).toBeNull(); +}); diff --git a/packages/provider-core/src/server/maintenanceResolver.ts b/packages/provider-core/src/server/maintenanceResolver.ts index 364b4cee9bf42..18f1346ce784a 100644 --- a/packages/provider-core/src/server/maintenanceResolver.ts +++ b/packages/provider-core/src/server/maintenanceResolver.ts @@ -463,6 +463,85 @@ const resolvePnpmGlobalLatestVersion = Effect.fn("resolvePnpmGlobalLatestVersion return nonEmptyString(listedEntries?.[0]?.dependencies?.[packageName]?.version); }); +const decodeNpmViewTime = Schema.decodeUnknownOption( + Schema.fromJsonString(Schema.Record(Schema.String, Schema.String)), +); + +/** Reads `[install] minimumReleaseAge` and `minimumReleaseAgeExcludes` from bunfig.toml. */ +export function parseBunReleaseAgeConfig( + bunfig: string, +): { readonly seconds: number; readonly excludes: ReadonlyArray } | null { + let inInstall = false; + let seconds: number | null = null; + let excludes: ReadonlyArray = []; + for (const rawLine of bunfig.split("\n")) { + const line = rawLine.replace(/#.*$/, "").trim(); + const section = /^\[([^\]]+)\]$/.exec(line); + if (section) { + inInstall = section[1]!.trim() === "install"; + continue; + } + if (!inInstall) continue; + const age = /^minimumReleaseAge\s*=\s*(\d+)$/.exec(line); + if (age) seconds = Number(age[1]); + const list = /^minimumReleaseAgeExcludes\s*=\s*\[(.*)\]$/.exec(line); + if (list) { + excludes = [...list[1]!.matchAll(/"([^"]*)"|'([^']*)'/g)].map((match) => match[1] ?? match[2]!); + } + } + return seconds === null ? null : { seconds, excludes }; +} + +/** + * Newest stable version whose publish time is at least `minimumAgeSeconds` old, + * matching bun's rule for skipping recently published releases. + */ +export function selectReleaseAgeEligibleVersion(input: { + readonly times: Readonly>; + readonly nowMs: number; + readonly minimumAgeSeconds: number; +}): string | null { + const cutoffMs = input.nowMs - input.minimumAgeSeconds * 1_000; + let best: string | null = null; + for (const [version, published] of Object.entries(input.times)) { + if (!/^\d+\.\d+\.\d+$/.test(version)) continue; + const publishedMs = Date.parse(published); + if (!Number.isFinite(publishedMs) || publishedMs > cutoffMs) continue; + if (best === null || compareSemverVersions(version, best) > 0) best = version; + } + return best; +} + +// Bun skips releases newer than `minimumReleaseAge` when it installs `@latest`, but the +// npm `latest` tag does not. Advertising that tag would offer an update bun keeps refusing. +// Null defers to the npm lookup, which is the right answer when no age limit is set. +const resolveBunGlobalLatestVersion = Effect.fn("resolveBunGlobalLatestVersion")(function* ( + packageName: string, + env: NodeJS.ProcessEnv, +) { + const resolvedEnv = { ...(yield* HostProcess.Environment), ...env }; + const home = resolvedEnv.HOME ?? resolvedEnv.USERPROFILE; + if (!home) return null; + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const bunfig = yield* fileSystem + .readFileString(path.join(home, ".bunfig.toml")) + .pipe(Effect.orElseSucceed(() => null)); + const releaseAge = bunfig === null ? null : parseBunReleaseAgeConfig(bunfig); + if (!releaseAge || releaseAge.excludes.includes(packageName)) return null; + const npmPath = yield* resolveCommandPath("npm", { env }); + if (!npmPath) return null; + const stdout = yield* runProbe(npmPath, ["view", "--json", packageName, "time"], env); + const times = stdout ? Option.getOrNull(decodeNpmViewTime(stdout)) : null; + if (!times) return null; + const nowMs = DateTime.toEpochMillis(yield* DateTime.now); + return selectReleaseAgeEligibleVersion({ + times, + nowMs, + minimumAgeSeconds: releaseAge.seconds, + }); +}); + export const resolvePackageManagedProviderMaintenance = Effect.fn( "resolvePackageManagedProviderMaintenance", )(function* ( @@ -560,6 +639,7 @@ export const resolvePackageManagedProviderMaintenance = Effect.fn( }); } if (commandPaths.some(isBunGlobalCommandPath)) { + const services = yield* Effect.context(); return makeProviderMaintenanceCapabilities({ provider: definition.provider, packageName, @@ -567,6 +647,9 @@ export const resolvePackageManagedProviderMaintenance = Effect.fn( updateArgs: ["i", "-g", `${packageName}@latest`], updateLockKey: "bun-global", env: context.env, + lookupLatestVersion: resolveBunGlobalLatestVersion(packageName, context.env).pipe( + Effect.provideContext(services), + ), }); } if (commandPaths.some(isPnpmGlobalCommandPath)) { From fc7ce2024fdf0dfdc48e58dff89b714af7813679 Mon Sep 17 00:00:00 2001 From: Atulya Date: Thu, 5 Nov 2026 10:46:20 +0530 Subject: [PATCH 2/3] style(provider): wrap bunfig excludes parsing to satisfy formatter --- packages/provider-core/src/server/maintenanceResolver.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/provider-core/src/server/maintenanceResolver.ts b/packages/provider-core/src/server/maintenanceResolver.ts index 18f1346ce784a..ce849518936af 100644 --- a/packages/provider-core/src/server/maintenanceResolver.ts +++ b/packages/provider-core/src/server/maintenanceResolver.ts @@ -486,7 +486,9 @@ export function parseBunReleaseAgeConfig( if (age) seconds = Number(age[1]); const list = /^minimumReleaseAgeExcludes\s*=\s*\[(.*)\]$/.exec(line); if (list) { - excludes = [...list[1]!.matchAll(/"([^"]*)"|'([^']*)'/g)].map((match) => match[1] ?? match[2]!); + excludes = [...list[1]!.matchAll(/"([^"]*)"|'([^']*)'/g)].map( + (match) => match[1] ?? match[2]!, + ); } } return seconds === null ? null : { seconds, excludes }; From 699044c996ec9004003757975a1f0ca64a30b19d Mon Sep 17 00:00:00 2001 From: Atulya Date: Thu, 5 Nov 2026 10:54:47 +0530 Subject: [PATCH 3/3] style(provider): format release-age test with vp fmt --- .../provider-core/src/server/maintenanceResolver.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/provider-core/src/server/maintenanceResolver.test.ts b/packages/provider-core/src/server/maintenanceResolver.test.ts index dd360ed613438..9b2ca612ea55e 100644 --- a/packages/provider-core/src/server/maintenanceResolver.test.ts +++ b/packages/provider-core/src/server/maintenanceResolver.test.ts @@ -1689,9 +1689,9 @@ it("picks the newest stable version published before the release-age cutoff", () "2.3.0-beta.1": "2026-10-01T00:00:00Z", "0.0.0-dev-565": "2026-11-04T04:11:56Z", }; - expect( - selectReleaseAgeEligibleVersion({ times, nowMs, minimumAgeSeconds: 604_800 }), - ).toBe("2.2.0"); + expect(selectReleaseAgeEligibleVersion({ times, nowMs, minimumAgeSeconds: 604_800 })).toBe( + "2.2.0", + ); expect(selectReleaseAgeEligibleVersion({ times, nowMs, minimumAgeSeconds: 0 })).toBe("2.2.5"); expect( selectReleaseAgeEligibleVersion({