semver range checks and upgrade planning: working versions (tested 2026-10-15)
Use node-semver to check engine ranges, pick the highest satisfying release, classify patch, minor and major upgrades, coerce loose versions and handle prereleases.
passed5 passed · 0 failed2026-10-15 17:11 UTC- Status
- passed
- Last passed
- 2026-10-15 17:11 UTC
- Tests
- 5 / 5
- Language
- TypeScript
- Runtime
- node v22.23.3
pinned versions4
semver@7.8.5@types/node@26.6.5tsx@4.23.15@types/semver@7.8.0
run it
npm install --no-audit --no-fund --ignore-scripts=false npm test
test environment
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38775053802
The code
Exactly the files that passed. Your agent can pull them with get_recipe("semver-range-checks") (install), or see them on GitHub.
package.json
{
"name": "semver-range-checks",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"semver": "7.8.5"
},
"devDependencies": {
"@types/node": "26.6.5",
"tsx": "4.23.15",
"@types/semver": "7.8.0"
}
}
src/versions.ts
import semver from "semver";
export function satisfiesEngine(version: string, range: string): boolean {
const v = semver.coerce(version);
return v !== null && semver.satisfies(v, range);
}
export type UpgradePlan = { current: string; patch: string | null; minor: string | null; major: string | null };
/** Latest stable release within patch, minor and major reach of the current version. */
export function planUpgrades(current: string, available: string[]): UpgradePlan {
const stable = available.filter((v) => semver.valid(v) && !semver.prerelease(v));
const newer = (range: string) => {
const best = semver.maxSatisfying(stable, range);
return best && semver.gt(best, current) ? best : null;
};
const major = semver.maxSatisfying(stable, `>=${current}`);
return {
current,
patch: newer(`~${current}`),
minor: newer(`^${current}`),
major: major && semver.major(major) > semver.major(current) ? major : null,
};
}
export function sortReleases(versions: string[]): string[] {
return semver.sort(versions.filter((v) => semver.valid(v)));
}
export function bump(version: string, kind: semver.ReleaseType, preid?: string): string {
const next = semver.inc(version, kind, preid);
if (!next) throw new Error(`cannot bump ${version}`);
return next;
}
export function describeRange(range: string): { valid: boolean; min: string | null } {
const valid = semver.validRange(range) !== null;
return { valid, min: valid ? semver.minVersion(range)?.version ?? null : null };
}
test/versions.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import semver from "semver";
import { satisfiesEngine, planUpgrades, sortReleases, bump, describeRange } from "../src/versions.ts";
const releases = ["1.2.3", "1.2.9", "1.4.0", "1.5.0-beta.1", "2.0.0", "2.1.0", "3.0.0-rc.1"];
test("engine checks coerce loose versions like v22.11", () => {
assert.equal(satisfiesEngine("v22.11", ">=20 <23"), true);
assert.equal(satisfiesEngine("18.19.0", ">=20"), false);
assert.equal(satisfiesEngine("garbage", ">=1"), false);
});
test("plans patch, minor and major upgrades ignoring prereleases", () => {
assert.deepEqual(planUpgrades("1.2.3", releases), { current: "1.2.3", patch: "1.2.9", minor: "1.4.0", major: "2.1.0" });
assert.deepEqual(planUpgrades("2.1.0", releases), { current: "2.1.0", patch: null, minor: null, major: null });
});
test("sorts and drops invalid versions", () => {
assert.deepEqual(sortReleases(["2.0.0", "nope", "1.10.0", "1.2.0"]), ["1.2.0", "1.10.0", "2.0.0"]);
});
test("bumps including prerelease ids", () => {
assert.equal(bump("1.2.3", "minor"), "1.3.0");
assert.equal(bump("1.2.3", "prerelease", "rc"), "1.2.4-rc.0");
assert.equal(bump("1.2.4-rc.0", "prerelease", "rc"), "1.2.4-rc.1");
});
test("ranges: validity, minimum version and prerelease opt-in", () => {
assert.deepEqual(describeRange("^1.4.0 || ^2.0.0"), { valid: true, min: "1.4.0" });
assert.equal(describeRange("not a range").valid, false);
assert.equal(semver.satisfies("3.0.0-rc.1", ">=2"), false);
assert.equal(semver.satisfies("3.0.0-rc.1", ">=2", { includePrerelease: true }), true);
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.