|
| 1 | +import type { ToolDefinition } from "../types/index.js"; |
| 2 | +import { extractVersion } from "./fp.js"; |
| 3 | + |
| 4 | +/** |
| 5 | + * Parse the numeric MAJOR.MINOR.PATCH tuple from a semver string, |
| 6 | + * stripping pre-release (`-alpha.1`) and build metadata (`+build.456`). |
| 7 | + * |
| 8 | + * Handles formats produced by arcade-mcp's normalize_version(): |
| 9 | + * "3.1.3", "1.2.3-beta.1", "1.2.3+build.456", "1.2.3-rc.1+build.789" |
| 10 | + */ |
| 11 | +const parseNumericVersion = (version: string): number[] => { |
| 12 | + // Strip build metadata (after +) then pre-release (after -) |
| 13 | + const core = version.split("+")[0]?.split("-")[0] ?? version; |
| 14 | + return core.split(".").map((s) => { |
| 15 | + const n = Number(s); |
| 16 | + return Number.isNaN(n) ? 0 : n; |
| 17 | + }); |
| 18 | +}; |
| 19 | + |
| 20 | +/** |
| 21 | + * Compare two semver version strings numerically by MAJOR.MINOR.PATCH. |
| 22 | + * Pre-release and build metadata are ignored for ordering purposes |
| 23 | + * (they are unlikely to appear in Engine API responses, but we handle |
| 24 | + * them defensively since arcade-mcp's semver allows them). |
| 25 | + * |
| 26 | + * Returns a positive number if a > b, negative if a < b, 0 if equal. |
| 27 | + */ |
| 28 | +const compareVersions = (a: string, b: string): number => { |
| 29 | + const aParts = parseNumericVersion(a); |
| 30 | + const bParts = parseNumericVersion(b); |
| 31 | + const len = Math.max(aParts.length, bParts.length); |
| 32 | + for (let i = 0; i < len; i++) { |
| 33 | + const diff = (aParts[i] ?? 0) - (bParts[i] ?? 0); |
| 34 | + if (diff !== 0) return diff; |
| 35 | + } |
| 36 | + return 0; |
| 37 | +}; |
| 38 | + |
| 39 | +/** |
| 40 | + * Find the highest version among all tools in a toolkit. |
| 41 | + * This is the version we keep — stale tools from older releases are dropped. |
| 42 | + */ |
| 43 | +export const getHighestVersion = ( |
| 44 | + tools: readonly ToolDefinition[] |
| 45 | +): string | null => { |
| 46 | + if (tools.length === 0) { |
| 47 | + return null; |
| 48 | + } |
| 49 | + |
| 50 | + let best = ""; |
| 51 | + for (const tool of tools) { |
| 52 | + const version = extractVersion(tool.fullyQualifiedName); |
| 53 | + if (best === "" || compareVersions(version, best) > 0) { |
| 54 | + best = version; |
| 55 | + } |
| 56 | + } |
| 57 | + |
| 58 | + return best || null; |
| 59 | +}; |
| 60 | + |
| 61 | +/** |
| 62 | + * Keep only tools whose @version matches the highest version for |
| 63 | + * their toolkit. If all tools share the same version (the common |
| 64 | + * case), returns the original array unchanged. |
| 65 | + * |
| 66 | + * This drops stale tools from older releases that Engine still serves, |
| 67 | + * while always preserving the newest version — even if it has fewer tools |
| 68 | + * (e.g. tools were removed/consolidated in the new release). |
| 69 | + */ |
| 70 | +export const filterToolsByHighestVersion = ( |
| 71 | + tools: readonly ToolDefinition[] |
| 72 | +): readonly ToolDefinition[] => { |
| 73 | + const highest = getHighestVersion(tools); |
| 74 | + if (highest === null) { |
| 75 | + return tools; |
| 76 | + } |
| 77 | + |
| 78 | + // Fast path: if every tool is already at the highest version, skip filtering |
| 79 | + const allSame = tools.every( |
| 80 | + (t) => extractVersion(t.fullyQualifiedName) === highest |
| 81 | + ); |
| 82 | + if (allSame) { |
| 83 | + return tools; |
| 84 | + } |
| 85 | + |
| 86 | + return tools.filter((t) => extractVersion(t.fullyQualifiedName) === highest); |
| 87 | +}; |
0 commit comments