mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-13 15:03:43 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8baa69b899 |
@@ -189,14 +189,12 @@
|
||||
"dependencies": {
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"semver": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/httpapi-codegen": "workspace:*",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/semver": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"effect": "catalog:",
|
||||
},
|
||||
@@ -6469,8 +6467,6 @@
|
||||
|
||||
"@opencode-ai/cli/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"@opencode-ai/client/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=="],
|
||||
|
||||
@@ -103,9 +103,7 @@ export const options = Effect.fnUntraced(function* (input: { readonly checkVersi
|
||||
yield* Effect.forEach(legacyRegistrationFiles, (legacy) => migrateRegistration(legacy, file))
|
||||
return {
|
||||
file,
|
||||
version: input.checkVersion
|
||||
? (version: string) => Service.isServiceVersionCompatible(version, OPENCODE_VERSION)
|
||||
: undefined,
|
||||
version: input.checkVersion ? OPENCODE_VERSION : undefined,
|
||||
command: [...selfCommand(), "serve", "--service"],
|
||||
}
|
||||
})
|
||||
|
||||
@@ -64,11 +64,7 @@ test("service options only require a matching version when requested", async ()
|
||||
|
||||
try {
|
||||
expect((await runPromise(ServiceConfig.options())).version).toBeUndefined()
|
||||
const version = (await runPromise(ServiceConfig.options({ checkVersion: true }))).version
|
||||
expect(version).toBeFunction()
|
||||
if (typeof version !== "function") throw new Error("Expected a service version predicate")
|
||||
expect(version(OPENCODE_VERSION)).toBe(true)
|
||||
expect(version("999.0.0")).toBe(true)
|
||||
expect((await runPromise(ServiceConfig.options({ checkVersion: true }))).version).toBe(OPENCODE_VERSION)
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
@@ -33,8 +33,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
"semver": "catalog:"
|
||||
"@opencode-ai/protocol": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"effect": "4.0.0-beta.101"
|
||||
@@ -49,7 +48,6 @@
|
||||
"@opencode-ai/httpapi-codegen": "workspace:*",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/semver": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"effect": "catalog:"
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
spawnServiceContender,
|
||||
} from "../service-contender.js"
|
||||
import { defaultEnsureTiming, ensureTiming, type EnsureTiming } from "../service-timing.js"
|
||||
import { isServiceVersionCompatible, matchesVersion } from "../service-version.js"
|
||||
import { matchesVersion } from "../service-version.js"
|
||||
|
||||
export * from "../service.js"
|
||||
/** Contents of the local service registration file. */
|
||||
@@ -325,4 +325,4 @@ const requestStop = Effect.fnUntraced(function* (service: LocalService, timeout
|
||||
})
|
||||
|
||||
/** Effect-based local service lifecycle operations. */
|
||||
export const Service = { discover, incumbent, ensure, stop, headers, isServiceVersionCompatible, Info }
|
||||
export const Service = { discover, incumbent, ensure, stop, headers, Info }
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
spawnServiceContender,
|
||||
} from "../service-contender.js"
|
||||
import { defaultEnsureTiming, ensureTiming, type EnsureTiming } from "../service-timing.js"
|
||||
import { isServiceVersionCompatible, matchesVersion } from "../service-version.js"
|
||||
import type { ServiceHealth, ServiceStopResponse } from "./generated/types.js"
|
||||
import { matchesVersion } from "../service-version.js"
|
||||
import type { ServiceStopResponse } from "./generated/types.js"
|
||||
|
||||
export * from "../service.js"
|
||||
|
||||
@@ -130,7 +130,15 @@ async function read(file?: string) {
|
||||
const text = await readFile(file ?? fallback(), "utf8").catch(() => undefined)
|
||||
if (text === undefined) return undefined
|
||||
try {
|
||||
return JSON.parse(text) as Info
|
||||
const value: unknown = JSON.parse(text)
|
||||
if (typeof value !== "object" || value === null) return undefined
|
||||
if (!("url" in value) || typeof value.url !== "string") return undefined
|
||||
if (!("pid" in value) || !Number.isInteger(value.pid) || typeof value.pid !== "number" || value.pid <= 0)
|
||||
return undefined
|
||||
if ("id" in value && value.id !== undefined && typeof value.id !== "string") return undefined
|
||||
if ("version" in value && value.version !== undefined && typeof value.version !== "string") return undefined
|
||||
if ("password" in value && value.password !== undefined && typeof value.password !== "string") return undefined
|
||||
return value as Info
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
@@ -163,7 +171,7 @@ async function probeResult(info: Info, allowLegacy = false, timeout = defaultEns
|
||||
})
|
||||
.then(async (response) => ({
|
||||
response,
|
||||
body: (await response.json()) as ServiceHealth | { readonly healthy: true },
|
||||
body: (await response.json()) as unknown,
|
||||
}))
|
||||
.then(
|
||||
(value) => ({ value }),
|
||||
@@ -172,7 +180,18 @@ async function probeResult(info: Info, allowLegacy = false, timeout = defaultEns
|
||||
if ("cause" in result) return { service: undefined, timedOut: signal.aborted }
|
||||
const response = result.value.response
|
||||
const body = result.value.body
|
||||
if (body !== undefined && "version" in body && "pid" in body) {
|
||||
if (
|
||||
typeof body === "object" &&
|
||||
body !== null &&
|
||||
"healthy" in body &&
|
||||
body.healthy === true &&
|
||||
"version" in body &&
|
||||
typeof body.version === "string" &&
|
||||
"pid" in body &&
|
||||
typeof body.pid === "number" &&
|
||||
Number.isInteger(body.pid) &&
|
||||
body.pid > 0
|
||||
) {
|
||||
if (body.pid !== info.pid) return { service: undefined, timedOut: false }
|
||||
if (info.version !== undefined && body.version !== info.version) return { service: undefined, timedOut: false }
|
||||
return {
|
||||
@@ -186,7 +205,16 @@ async function probeResult(info: Info, allowLegacy = false, timeout = defaultEns
|
||||
timedOut: false,
|
||||
}
|
||||
}
|
||||
if (!allowLegacy || body?.healthy !== true) return { service: undefined, timedOut: false }
|
||||
if (
|
||||
!allowLegacy ||
|
||||
typeof body !== "object" ||
|
||||
body === null ||
|
||||
!("healthy" in body) ||
|
||||
body.healthy !== true ||
|
||||
"version" in body ||
|
||||
"pid" in body
|
||||
)
|
||||
return { service: undefined, timedOut: false }
|
||||
return {
|
||||
service: { info, endpoint, state: "ready", legacy: true } satisfies LocalService,
|
||||
timedOut: false,
|
||||
@@ -278,4 +306,4 @@ function delay(milliseconds: number) {
|
||||
}
|
||||
|
||||
/** Promise-based local service lifecycle operations. */
|
||||
export const Service = { discover, ensure, stop, headers, isServiceVersionCompatible }
|
||||
export const Service = { discover, ensure, stop, headers }
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { DiscoverOptions } from "./service.js"
|
||||
import semver from "semver"
|
||||
|
||||
export function matchesVersion(version: string | undefined, options: DiscoverOptions) {
|
||||
if (options.version === undefined) return true
|
||||
@@ -7,12 +6,3 @@ export function matchesVersion(version: string | undefined, options: DiscoverOpt
|
||||
if (typeof options.version === "function") return options.version(version)
|
||||
return version === options.version
|
||||
}
|
||||
|
||||
/** Whether a service is at least as new as its client. */
|
||||
export function isServiceVersionCompatible(serverVersion: string, clientVersion: string) {
|
||||
if (serverVersion === clientVersion) return true
|
||||
const server = serverVersion.replace(/^(0\.0\.0-.+)-(\d+(?:\.\d+)?)$/, "$1.$2")
|
||||
const client = clientVersion.replace(/^(0\.0\.0-.+)-(\d+(?:\.\d+)?)$/, "$1.$2")
|
||||
if (!semver.valid(server) || !semver.valid(client)) return true
|
||||
return semver.gte(server, client)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export { isServiceVersionCompatible } from "./service-version.js"
|
||||
|
||||
/** Connection details for a local OpenCode service. */
|
||||
export type Endpoint = {
|
||||
/** Base URL of the service. */
|
||||
|
||||
@@ -31,7 +31,6 @@ let version = "test"
|
||||
if (mode === "old" || mode === "reject-stop") version = "old"
|
||||
if (mode === "incompatible") version = "1.9.0"
|
||||
if (mode === "compatible" || mode === "delayed-compatible") version = "2.1.0-next.1"
|
||||
if (mode === "newer") version = "0.0.0-next-17272"
|
||||
const id = crypto.randomUUID()
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
|
||||
@@ -38,6 +38,50 @@ test("discovers a compatible registered service", async () => {
|
||||
expect(await Service.discover({ file: registration, version: (version) => version.startsWith("3.") })).toBeUndefined()
|
||||
})
|
||||
|
||||
test("rejects malformed registrations without probing or signaling", async () => {
|
||||
const directory = await temp()
|
||||
const registration = join(directory, "service.json")
|
||||
const malformed = [
|
||||
null,
|
||||
[],
|
||||
{},
|
||||
{ url: "http://127.0.0.1:1" },
|
||||
{ url: "http://127.0.0.1:1", pid: 0 },
|
||||
{ url: "http://127.0.0.1:1", pid: -1 },
|
||||
{ url: "http://127.0.0.1:1", pid: 1.5 },
|
||||
{ url: "http://127.0.0.1:1", pid: "1" },
|
||||
{ url: "http://127.0.0.1:1", pid: 1, id: 1 },
|
||||
]
|
||||
|
||||
for (const value of malformed) {
|
||||
await Bun.write(registration, JSON.stringify(value))
|
||||
expect(await Service.discover({ file: registration })).toBeUndefined()
|
||||
}
|
||||
})
|
||||
|
||||
test("rejects primitive and partial modern health responses", async () => {
|
||||
const directory = await temp()
|
||||
const registration = join(directory, "service.json")
|
||||
const bodies = [
|
||||
null,
|
||||
1,
|
||||
"healthy",
|
||||
[],
|
||||
{},
|
||||
{ healthy: false, version: "test", pid: process.pid },
|
||||
{ healthy: true, version: null, pid: process.pid },
|
||||
{ healthy: true, version: "test", pid: "1" },
|
||||
{ healthy: true, version: "test" },
|
||||
{ healthy: true, pid: process.pid },
|
||||
]
|
||||
|
||||
for (const body of bodies) {
|
||||
using server = Bun.serve({ port: 0, fetch: () => Response.json(body) })
|
||||
await Bun.write(registration, JSON.stringify({ url: server.url.toString(), pid: process.pid }))
|
||||
expect(await Service.discover({ file: registration })).toBeUndefined()
|
||||
}
|
||||
})
|
||||
|
||||
test("ensures a missing service with native promises", async () => {
|
||||
const directory = await temp()
|
||||
const registration = join(directory, "service.json")
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { isServiceVersionCompatible } from "../src/service"
|
||||
|
||||
test("accepts the same or a newer service version", () => {
|
||||
expect(isServiceVersionCompatible("0.0.0-next-17272", "0.0.0-next-17272")).toBe(true)
|
||||
expect(isServiceVersionCompatible("0.0.0-next-17272", "0.0.0-next-17271")).toBe(true)
|
||||
expect(isServiceVersionCompatible("0.0.0-next-17271", "0.0.0-next-17272")).toBe(false)
|
||||
expect(isServiceVersionCompatible("0.0.0-next-15000", "0.0.0-next-9999")).toBe(true)
|
||||
})
|
||||
|
||||
test("accepts incomparable development versions", () => {
|
||||
expect(isServiceVersionCompatible("development-a", "development-b")).toBe(true)
|
||||
})
|
||||
@@ -68,30 +68,6 @@ test("reuses a compatible registered service", async () => {
|
||||
expect(existing.exitCode).toBe(null)
|
||||
})
|
||||
|
||||
test("a stale client reuses a newer registered service", async () => {
|
||||
const directory = await temp()
|
||||
const registration = join(directory, "service.json")
|
||||
const existing = spawn(registration, "newer")
|
||||
await waitForFile(registration)
|
||||
const original = await Bun.file(registration).json()
|
||||
|
||||
const starts: EnsureReason[] = []
|
||||
const endpoint = await run(
|
||||
ensure({
|
||||
file: registration,
|
||||
version: (version) => Service.isServiceVersionCompatible(version, "0.0.0-next-17271"),
|
||||
command: [process.execPath, fixture, registration, "record-start"],
|
||||
onStart: (reason) => starts.push(reason),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(endpoint.url).toBe(original.url)
|
||||
expect(starts).toEqual([])
|
||||
expect(existing.exitCode).toBe(null)
|
||||
expect(await Bun.file(registration).json()).toEqual(original)
|
||||
expect(await Bun.file(registration + ".started").exists()).toBe(false)
|
||||
})
|
||||
|
||||
test("replaces an incompatible registered service", async () => {
|
||||
const directory = await temp()
|
||||
const registration = join(directory, "service.json")
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function startBackgroundCli(logger: Logger) {
|
||||
isolated && process.env.OPENCODE_DESKTOP_SERVER_CHANNEL === "local"
|
||||
? join(app.getPath("userData"), "opencode", "service-local.json")
|
||||
: undefined,
|
||||
version: (serverVersion) => Service.isServiceVersionCompatible(serverVersion, version),
|
||||
version,
|
||||
command: [binary, "serve", "--service"],
|
||||
onStart: (reason, previousVersion) => logger.log("v2 CLI background service starting", { reason, previousVersion }),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user