Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions packages/provider-core/src/server/maintenanceResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ import {
makeProviderMaintenanceCapabilities,
normalizeCommandPath,
npmGlobalPrefixFromCommandPath,
parseBunReleaseAgeConfig,
parseChocolateyLatestVersion,
parseHomebrewLatestVersion,
resolveLatestProviderVersion,
resolvePackageManagedProviderMaintenance,
resolveProviderMaintenanceCapabilitiesEffect,
selectReleaseAgeEligibleVersion,
type PackageManagedProviderMaintenanceDefinition,
type ProviderMaintenanceCapabilities,
} from "./maintenanceResolver.ts";
Expand Down Expand Up @@ -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();
});
85 changes: 85 additions & 0 deletions packages/provider-core/src/server/maintenanceResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,87 @@ 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<string> } | null {
let inInstall = false;
let seconds: number | null = null;
let excludes: ReadonlyArray<string> = [];
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<Record<string, string>>;
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* (
Expand Down Expand Up @@ -560,13 +641,17 @@ export const resolvePackageManagedProviderMaintenance = Effect.fn(
});
}
if (commandPaths.some(isBunGlobalCommandPath)) {
const services = yield* Effect.context<ProviderMaintenanceResolverServices>();
return makeProviderMaintenanceCapabilities({
provider: definition.provider,
packageName,
updateExecutable: "bun",
updateArgs: ["i", "-g", `${packageName}@latest`],
updateLockKey: "bun-global",
env: context.env,
lookupLatestVersion: resolveBunGlobalLatestVersion(packageName, context.env).pipe(
Effect.provideContext(services),
),
});
}
if (commandPaths.some(isPnpmGlobalCommandPath)) {
Expand Down
Loading