Skip to content

Commit a1a663d

Browse files
authored
refactor(cli): replace updater semver dependency (#42492)
1 parent 4a899bb commit a1a663d

5 files changed

Lines changed: 98 additions & 18 deletions

File tree

‎bun.lock‎

Lines changed: 0 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/cli/package.json‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
"immer": "11.1.4",
3939
"jsonc-parser": "3.3.1",
4040
"open": "10.1.2",
41-
"semver": "catalog:",
4241
"solid-js": "catalog:",
4342
"tree-sitter-bash": "0.25.0",
4443
"tree-sitter-powershell": "0.25.10",
@@ -51,7 +50,6 @@
5150
"@opencode-ai/protocol": "workspace:*",
5251
"@tsconfig/bun": "catalog:",
5352
"@types/bun": "catalog:",
54-
"@types/semver": "catalog:",
5553
"@typescript/native-preview": "catalog:",
5654
"@lydell/node-pty-darwin-arm64": "1.2.0-beta.12",
5755
"@lydell/node-pty-darwin-x64": "1.2.0-beta.12",
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
export type Policy = boolean | "notify"
2+
export type Action = "none" | "upgrade"
3+
4+
const maximumComponent = "9007199254740991"
5+
const versionPattern =
6+
/^v?([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/
7+
8+
export function action(current: string, latest: string, policy: Policy): Action {
9+
if (policy === false) return "none"
10+
const currentVersion = parseReleaseVersion(current)
11+
const latestVersion = parseReleaseVersion(latest)
12+
if (!currentVersion || !latestVersion || sameRelease(currentVersion, latestVersion)) return "none"
13+
// Major upgrades are never installed automatically.
14+
if (currentVersion.major !== latestVersion.major) return "none"
15+
return "upgrade"
16+
}
17+
18+
function parseReleaseVersion(input: string) {
19+
if (input.length > 256) return
20+
const match = input.trim().match(versionPattern)
21+
if (!match) return
22+
if ([match[1], match[2], match[3]].some(invalidComponent)) return
23+
if (
24+
match[4]
25+
?.split(".")
26+
.some((identifier) => identifier.length > 1 && identifier.startsWith("0") && /^[0-9]+$/.test(identifier))
27+
)
28+
return
29+
return {
30+
major: match[1],
31+
core: `${match[1]}.${match[2]}.${match[3]}`,
32+
prerelease: match[4]?.split(".") ?? [],
33+
}
34+
}
35+
36+
function sameRelease(current: NonNullable<ReturnType<typeof parseReleaseVersion>>, latest: typeof current) {
37+
if (current.core !== latest.core || current.prerelease.length !== latest.prerelease.length) return false
38+
return current.prerelease.every((identifier, index) => {
39+
const other = latest.prerelease[index]
40+
if (identifier === other) return true
41+
// semver compares oversized numeric prerelease identifiers after numeric coercion.
42+
return /^[0-9]+$/.test(identifier) && /^[0-9]+$/.test(other) && Number(identifier) === Number(other)
43+
})
44+
}
45+
46+
function invalidComponent(value: string) {
47+
if (value.length > 1 && value.startsWith("0")) return true
48+
if (value.length !== maximumComponent.length) return value.length > maximumComponent.length
49+
return value > maximumComponent
50+
}

‎packages/cli/src/services/updater.test.ts‎

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, test } from "bun:test"
2-
import { action, decodePolicy } from "./updater"
2+
import { action } from "./updater-action"
3+
import { decodePolicy } from "./updater"
34

45
describe("updater", () => {
56
test("reads autoupdate from JSONC", () => {
@@ -30,4 +31,48 @@ describe("updater", () => {
3031
test("upgrades when latest is lower (rollback)", () => {
3132
expect(action("1.2.4", "1.2.3", true)).toBe("upgrade")
3233
})
34+
35+
test("accepts strict release version variants", () => {
36+
expect(action("v1.2.3", " 1.2.4\n", true)).toBe("upgrade")
37+
expect(action("1.2.3-alpha.1", "1.2.3-alpha.2", true)).toBe("upgrade")
38+
expect(action("0.0.0-next-17403", "0.0.0-next-17403.2", true)).toBe("upgrade")
39+
expect(action("1.2.3+old", "1.2.3+new", true)).toBe("none")
40+
expect(action("v1.2.3+old", "1.2.3", true)).toBe("none")
41+
})
42+
43+
test("preserves strict validity", () => {
44+
const invalid = [
45+
"=1.2.3",
46+
"V1.2.3",
47+
"1.2",
48+
"1.2.3.4",
49+
"01.2.3",
50+
"1.02.3",
51+
"1.2.03",
52+
"1.2.3-01",
53+
"1.2.3-",
54+
"1.2.3+",
55+
"1.2.3-alpha..1",
56+
"1.2.3_alpha",
57+
"9007199254740992.0.0",
58+
"0.9007199254740992.0",
59+
"0.0.9007199254740992",
60+
]
61+
invalid.forEach((version) => expect(action("1.2.3", version, true), version).toBe("none"))
62+
})
63+
64+
test("handles numeric limits without losing precision", () => {
65+
expect(action("9007199254740991.0.0", "9007199254740991.0.1", true)).toBe("upgrade")
66+
expect(action("9007199254740990.0.0", "9007199254740991.0.0", true)).toBe("none")
67+
})
68+
69+
test("preserves equality for oversized numeric prerelease identifiers", () => {
70+
expect(action("1.0.0-9007199254740992", "1.0.0-9007199254740993", true)).toBe("none")
71+
expect(action("1.0.0-9007199254740991", "1.0.0-9007199254740992", true)).toBe("upgrade")
72+
})
73+
74+
test("rejects versions longer than semver's limit before trimming", () => {
75+
expect(action("1.2.3", `${" ".repeat(251)}1.2.3`, true)).toBe("none")
76+
expect(action("1.2.3", `1.2.4+${"a".repeat(250)}`, true)).toBe("upgrade")
77+
})
3378
})

‎packages/cli/src/services/updater.ts‎

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@ import { Context, Duration, Effect, FileSystem, Layer } from "effect"
55
import { ChildProcess } from "effect/unstable/process"
66
import { parse, type ParseError } from "jsonc-parser"
77
import path from "node:path"
8-
import semver from "semver"
8+
import { action, type Policy } from "./updater-action"
99

1010
declare const OPENCODE_CLI_NAME: string | undefined
1111

12-
export type Policy = boolean | "notify"
13-
export type Action = "none" | "upgrade"
1412
type Method = "npm" | "pnpm" | "bun" | "yarn"
1513

1614
const packageName =
@@ -34,14 +32,6 @@ export function decodePolicy(text: string): Policy | undefined {
3432
if (typeof value === "boolean" || value === "notify") return value
3533
}
3634

37-
export function action(current: string, latest: string, policy: Policy): Action {
38-
if (policy === false) return "none"
39-
if (!semver.valid(current) || !semver.valid(latest) || semver.eq(latest, current)) return "none"
40-
// Major upgrades are never installed automatically.
41-
if (semver.major(latest) !== semver.major(current)) return "none"
42-
return "upgrade"
43-
}
44-
4535
export const layer = Layer.effect(
4636
Service,
4737
Effect.gen(function* () {
@@ -166,3 +156,4 @@ export const layer = Layer.effect(
166156
)
167157

168158
export * as Updater from "./updater"
159+
export { action, type Action, type Policy } from "./updater-action"

0 commit comments

Comments
 (0)