From a1a663d60ebcab22f761cb8e117f491f290e9d00 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 14 Aug 2026 09:09:52 -0400 Subject: [PATCH] refactor(cli): replace updater semver dependency (#42492) --- bun.lock | 4 -- packages/cli/package.json | 2 - packages/cli/src/services/updater-action.ts | 50 +++++++++++++++++++++ packages/cli/src/services/updater.test.ts | 47 ++++++++++++++++++- packages/cli/src/services/updater.ts | 13 +----- 5 files changed, 98 insertions(+), 18 deletions(-) create mode 100644 packages/cli/src/services/updater-action.ts diff --git a/bun.lock b/bun.lock index 92a6e6930c..93361c8394 100644 --- a/bun.lock +++ b/bun.lock @@ -137,7 +137,6 @@ "immer": "11.1.4", "jsonc-parser": "3.3.1", "open": "10.1.2", - "semver": "catalog:", "solid-js": "catalog:", "tree-sitter-bash": "0.25.0", "tree-sitter-powershell": "0.25.10", @@ -166,7 +165,6 @@ "@parcel/watcher-win32-x64": "2.5.1", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", - "@types/semver": "catalog:", "@typescript/native-preview": "catalog:", "@yuuang/ffi-rs-darwin-arm64": "1.3.2", "@yuuang/ffi-rs-linux-arm64-gnu": "1.3.2", @@ -6430,8 +6428,6 @@ "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], - "@opencode-ai/cli/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "@opencode-ai/console-app/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.7", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ=="], "@opencode-ai/console-app/@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], diff --git a/packages/cli/package.json b/packages/cli/package.json index 5f0ea9dddf..e255181f63 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -38,7 +38,6 @@ "immer": "11.1.4", "jsonc-parser": "3.3.1", "open": "10.1.2", - "semver": "catalog:", "solid-js": "catalog:", "tree-sitter-bash": "0.25.0", "tree-sitter-powershell": "0.25.10", @@ -51,7 +50,6 @@ "@opencode-ai/protocol": "workspace:*", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", - "@types/semver": "catalog:", "@typescript/native-preview": "catalog:", "@lydell/node-pty-darwin-arm64": "1.2.0-beta.12", "@lydell/node-pty-darwin-x64": "1.2.0-beta.12", diff --git a/packages/cli/src/services/updater-action.ts b/packages/cli/src/services/updater-action.ts new file mode 100644 index 0000000000..ac597e36e1 --- /dev/null +++ b/packages/cli/src/services/updater-action.ts @@ -0,0 +1,50 @@ +export type Policy = boolean | "notify" +export type Action = "none" | "upgrade" + +const maximumComponent = "9007199254740991" +const versionPattern = + /^v?([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/ + +export function action(current: string, latest: string, policy: Policy): Action { + if (policy === false) return "none" + const currentVersion = parseReleaseVersion(current) + const latestVersion = parseReleaseVersion(latest) + if (!currentVersion || !latestVersion || sameRelease(currentVersion, latestVersion)) return "none" + // Major upgrades are never installed automatically. + if (currentVersion.major !== latestVersion.major) return "none" + return "upgrade" +} + +function parseReleaseVersion(input: string) { + if (input.length > 256) return + const match = input.trim().match(versionPattern) + if (!match) return + if ([match[1], match[2], match[3]].some(invalidComponent)) return + if ( + match[4] + ?.split(".") + .some((identifier) => identifier.length > 1 && identifier.startsWith("0") && /^[0-9]+$/.test(identifier)) + ) + return + return { + major: match[1], + core: `${match[1]}.${match[2]}.${match[3]}`, + prerelease: match[4]?.split(".") ?? [], + } +} + +function sameRelease(current: NonNullable>, latest: typeof current) { + if (current.core !== latest.core || current.prerelease.length !== latest.prerelease.length) return false + return current.prerelease.every((identifier, index) => { + const other = latest.prerelease[index] + if (identifier === other) return true + // semver compares oversized numeric prerelease identifiers after numeric coercion. + return /^[0-9]+$/.test(identifier) && /^[0-9]+$/.test(other) && Number(identifier) === Number(other) + }) +} + +function invalidComponent(value: string) { + if (value.length > 1 && value.startsWith("0")) return true + if (value.length !== maximumComponent.length) return value.length > maximumComponent.length + return value > maximumComponent +} diff --git a/packages/cli/src/services/updater.test.ts b/packages/cli/src/services/updater.test.ts index e11de2a0d9..3977205c64 100644 --- a/packages/cli/src/services/updater.test.ts +++ b/packages/cli/src/services/updater.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test" -import { action, decodePolicy } from "./updater" +import { action } from "./updater-action" +import { decodePolicy } from "./updater" describe("updater", () => { test("reads autoupdate from JSONC", () => { @@ -30,4 +31,48 @@ describe("updater", () => { test("upgrades when latest is lower (rollback)", () => { expect(action("1.2.4", "1.2.3", true)).toBe("upgrade") }) + + test("accepts strict release version variants", () => { + expect(action("v1.2.3", " 1.2.4\n", true)).toBe("upgrade") + expect(action("1.2.3-alpha.1", "1.2.3-alpha.2", true)).toBe("upgrade") + expect(action("0.0.0-next-17403", "0.0.0-next-17403.2", true)).toBe("upgrade") + expect(action("1.2.3+old", "1.2.3+new", true)).toBe("none") + expect(action("v1.2.3+old", "1.2.3", true)).toBe("none") + }) + + test("preserves strict validity", () => { + const invalid = [ + "=1.2.3", + "V1.2.3", + "1.2", + "1.2.3.4", + "01.2.3", + "1.02.3", + "1.2.03", + "1.2.3-01", + "1.2.3-", + "1.2.3+", + "1.2.3-alpha..1", + "1.2.3_alpha", + "9007199254740992.0.0", + "0.9007199254740992.0", + "0.0.9007199254740992", + ] + invalid.forEach((version) => expect(action("1.2.3", version, true), version).toBe("none")) + }) + + test("handles numeric limits without losing precision", () => { + expect(action("9007199254740991.0.0", "9007199254740991.0.1", true)).toBe("upgrade") + expect(action("9007199254740990.0.0", "9007199254740991.0.0", true)).toBe("none") + }) + + test("preserves equality for oversized numeric prerelease identifiers", () => { + expect(action("1.0.0-9007199254740992", "1.0.0-9007199254740993", true)).toBe("none") + expect(action("1.0.0-9007199254740991", "1.0.0-9007199254740992", true)).toBe("upgrade") + }) + + test("rejects versions longer than semver's limit before trimming", () => { + expect(action("1.2.3", `${" ".repeat(251)}1.2.3`, true)).toBe("none") + expect(action("1.2.3", `1.2.4+${"a".repeat(250)}`, true)).toBe("upgrade") + }) }) diff --git a/packages/cli/src/services/updater.ts b/packages/cli/src/services/updater.ts index e7280f4115..1dadc6b67f 100644 --- a/packages/cli/src/services/updater.ts +++ b/packages/cli/src/services/updater.ts @@ -5,12 +5,10 @@ import { Context, Duration, Effect, FileSystem, Layer } from "effect" import { ChildProcess } from "effect/unstable/process" import { parse, type ParseError } from "jsonc-parser" import path from "node:path" -import semver from "semver" +import { action, type Policy } from "./updater-action" declare const OPENCODE_CLI_NAME: string | undefined -export type Policy = boolean | "notify" -export type Action = "none" | "upgrade" type Method = "npm" | "pnpm" | "bun" | "yarn" const packageName = @@ -34,14 +32,6 @@ export function decodePolicy(text: string): Policy | undefined { if (typeof value === "boolean" || value === "notify") return value } -export function action(current: string, latest: string, policy: Policy): Action { - if (policy === false) return "none" - if (!semver.valid(current) || !semver.valid(latest) || semver.eq(latest, current)) return "none" - // Major upgrades are never installed automatically. - if (semver.major(latest) !== semver.major(current)) return "none" - return "upgrade" -} - export const layer = Layer.effect( Service, Effect.gen(function* () { @@ -166,3 +156,4 @@ export const layer = Layer.effect( ) export * as Updater from "./updater" +export { action, type Action, type Policy } from "./updater-action"