Compare commits

...

1 Commits

Author SHA1 Message Date
Kit Langton a1fce85b76 refactor(cli): replace updater semver dependency 2026-08-13 23:09:52 -04:00
5 changed files with 98 additions and 18 deletions
-4
View File
@@ -137,7 +137,6 @@
"immer": "11.1.4",
"jsonc-parser": "3.3.1",
"open": "10.1.2",
"semver": "catalog:",
"solid-js": "catalog:",
"uqr": "0.1.3",
"ws": "8.21.0",
@@ -163,7 +162,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",
@@ -6429,8 +6427,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=="],
-2
View File
@@ -38,7 +38,6 @@
"immer": "11.1.4",
"jsonc-parser": "3.3.1",
"open": "10.1.2",
"semver": "catalog:",
"solid-js": "catalog:",
"uqr": "0.1.3",
"ws": "8.21.0"
@@ -48,7 +47,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",
@@ -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<ReturnType<typeof parseReleaseVersion>>, 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
}
+46 -1
View File
@@ -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")
})
})
+2 -11
View File
@@ -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"