Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
670d2a9c4This file is pinned to the selected release. Download the original to open it in your design tool.
/**
* version-files.mjs — every place the release version is written.
*
* Shared by scripts/release.mjs, which rewrites these when a release is cut,
* and tests-ts/version-consistency.test.ts, which fails CI when they disagree.
* The bump used to be done by hand in eight places, and the README citation
* said 2.3.0 for five releases before anyone noticed.
*
* Each entry names a file and a regular expression whose first group is the
* version. A pattern must match exactly once: a second match would mean the
* anchor is ambiguous and a rewrite could touch the wrong line. Patterns are
* matched against LF-normalised text.
*/
export const VERSION_FILES = [
{
file: "package.json",
what: "package version",
pattern: /^ {2}"version": "([^"]+)",$/m,
},
{
// The only two-space-indented "version" key is the lockfile's own.
file: "package-lock.json",
what: "lockfile version",
pattern: /^ {2}"version": "([^"]+)",$/m,
},
{
file: "package-lock.json",
what: 'lockfile packages[""] version',
pattern: /^ {4}"": \{\n {6}"name": "kicad-mcp",\n {6}"version": "([^"]+)",$/m,
},
{
file: "src/server.ts",
what: "MCP server version",
pattern: /name: "kicad-mcp-server",\s*\n\s*version: "([^"]+)",/,
},
{
file: "python/kicad_interface.py",
what: "Python JSON-RPC serverInfo version",
pattern: /"serverInfo": \{[^}]*?"version": "([^"]+)"/,
},
{
file: "README.md",
what: "README current version",
pattern: /^\*\*Current Version:\*\* (\S+)$/m,
},
{
file: "README.md",
what: "README citation version",
pattern: /^ {2}version = \{([^}]+)\}$/m,
},
{
file: "docs/ROADMAP.md",
what: "roadmap current version",
pattern: /^\*\*Current Version:\*\* (\S+)$/m,
},
{
// Written by scripts/generate-tool-inventory.mjs from package.json.
file: "docs/TOOL_INVENTORY.md",
what: "tool inventory version",
pattern: /^\*\*Version:\*\* (\S+)$/m,
},
];
/** The file's text with CRLF turned into LF, and whether it had CRLF. */
export function normalise(raw) {
return { text: raw.replace(/\r\n/g, "\n"), crlf: raw.includes("\r\n") };
}
/** Put back the line endings `normalise` took out. */
export function restore(text, crlf) {
return crlf ? text.replace(/\n/g, "\r\n") : text;
}
/**
* Every match of an entry's pattern in `text`. The `d` flag records where
* group 1 sits, so a rewrite replaces exactly the version and nothing else.
*/
function matches(text, entry) {
const flags = new Set([...entry.pattern.flags, "g", "d"]);
return [...text.matchAll(new RegExp(entry.pattern.source, [...flags].join("")))];
}
/**
* The version an entry holds in `text`. Throws if the pattern does not match
* exactly once, naming the file and the place.
*/
export function readVersion(text, entry) {
const found = matches(text, entry);
if (found.length !== 1) {
throw new Error(
`${entry.file}: expected one ${entry.what}, found ${found.length} (pattern ${entry.pattern})`,
);
}
return found[0][1];
}
/** `text` with the entry's version replaced by `version`. */
export function writeVersion(text, entry, version) {
readVersion(text, entry); // throws unless the pattern matches exactly once
const [start, end] = matches(text, entry)[0].indices[1];
return text.slice(0, start) + version + text.slice(end);
}
const SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
/** Whether `version` is MAJOR.MINOR.PATCH with an optional -prerelease. */
export function isVersion(version) {
return SEMVER.test(version);
}
/**
* Semantic-version order: negative when a < b, 0 when equal, positive when
* a > b. A prerelease sorts before its release (2.9.0-rc.1 < 2.9.0), and
* prerelease identifiers compare numerically where both are numbers.
*/
export function compareVersions(a, b) {
const pa = SEMVER.exec(a);
const pb = SEMVER.exec(b);
if (!pa || !pb) throw new Error(`not a version: ${!pa ? a : b}`);
for (let i = 1; i <= 3; i++) {
const d = Number(pa[i]) - Number(pb[i]);
if (d !== 0) return d;
}
if (!pa[4] || !pb[4]) return (pa[4] ? -1 : 0) - (pb[4] ? -1 : 0);
const xa = pa[4].split(".");
const xb = pb[4].split(".");
for (let i = 0; i < Math.max(xa.length, xb.length); i++) {
if (xa[i] === undefined) return -1;
if (xb[i] === undefined) return 1;
const na = /^\d+$/.test(xa[i]);
const nb = /^\d+$/.test(xb[i]);
if (na && nb && Number(xa[i]) !== Number(xb[i])) return Number(xa[i]) - Number(xb[i]);
if (na !== nb) return na ? -1 : 1;
if (xa[i] !== xb[i]) return xa[i] < xb[i] ? -1 : 1;
}
return 0;
}