From 8b2ef0186ed9898bf0040db0769c2e2bfc46086b Mon Sep 17 00:00:00 2001 From: Lex Date: Wed, 19 Aug 2026 10:30:19 +0800 Subject: [PATCH] fix(opencode): auto-update notifies only; latest() tracks fork release feed Closes #351 --- .specgit.yaml | 7 ++ packages/core/src/config.ts | 2 +- packages/opencode/src/cli/upgrade.ts | 51 ++------ packages/opencode/src/installation/index.ts | 71 ++--------- .../instance/httpapi/handlers/global.ts | 2 +- .../test/installation/installation.test.ts | 112 ++---------------- 6 files changed, 41 insertions(+), 204 deletions(-) create mode 100644 .specgit.yaml diff --git a/.specgit.yaml b/.specgit.yaml new file mode 100644 index 0000000000..741ce82e68 --- /dev/null +++ b/.specgit.yaml @@ -0,0 +1,7 @@ +version: 1 +delivery: fix-opencode-auto +context: + kind: branch + branch: feat/351-fix-opencode-auto +issues: + - 351 diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 1f97194ad5..003a8c2936 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -41,7 +41,7 @@ export class Info extends Schema.Class("Config.Info")({ autoupdate: Schema.Union([Schema.Boolean, Schema.Literal("notify")]) .pipe(Schema.optional) .annotate({ - description: "Automatically update or notify when a new version is available", + description: "Notify when a new fork version is available on GitHub releases. Automatic updates are disabled; set to false to disable the notification", }), share: Schema.Literals(["manual", "auto", "disabled"]).pipe(Schema.optional).annotate({ description: "Control whether sessions may be shared manually, automatically, or not at all", diff --git a/packages/opencode/src/cli/upgrade.ts b/packages/opencode/src/cli/upgrade.ts index 62b230a633..5f03394493 100644 --- a/packages/opencode/src/cli/upgrade.ts +++ b/packages/opencode/src/cli/upgrade.ts @@ -5,49 +5,22 @@ import { Installation } from "@/installation" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { GlobalBus } from "@/bus/global" +// This fork never auto-updates: it only checks the fork's GitHub releases and +// notifies. `autoupdate: false` (or OPENCODE_DISABLE_AUTOUPDATE) silences the +// notification entirely. export async function upgrade() { const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.getGlobal())) if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) return - const method = await Installation.method() - const latest = await Installation.latest(method).catch(() => {}) + const latest = await Installation.latest().catch(() => {}) if (!latest) return - if (Flag.OPENCODE_ALWAYS_NOTIFY_UPDATE) { - GlobalBus.emit("event", { - directory: "global", - payload: { - type: Installation.Event.UpdateAvailable.type, - properties: { version: latest }, - }, - }) - return - } + if (!Flag.OPENCODE_ALWAYS_NOTIFY_UPDATE && InstallationVersion === latest) return - if (InstallationVersion === latest) return - - const kind = Installation.getReleaseType(InstallationVersion, latest) - - if (config.autoupdate === "notify" || kind !== "patch") { - GlobalBus.emit("event", { - directory: "global", - payload: { - type: Installation.Event.UpdateAvailable.type, - properties: { version: latest }, - }, - }) - return - } - - if (method === "unknown") return - await Installation.upgrade(method, latest) - .then(() => - GlobalBus.emit("event", { - directory: "global", - payload: { - type: Installation.Event.Updated.type, - properties: { version: latest }, - }, - }), - ) - .catch(() => {}) + GlobalBus.emit("event", { + directory: "global", + payload: { + type: Installation.Event.UpdateAvailable.type, + properties: { version: latest }, + }, + }) } diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index b4b888ed78..03b98c1372 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -11,7 +11,6 @@ import path from "path" import { makeRuntime } from "@opencode-ai/core/effect/runtime" import semver from "semver" import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version" -import { NpmConfig } from "@opencode-ai/core/npm-config" import { InstallationEvent } from "@opencode-ai/schema/installation-event" export type Method = "curl" | "npm" | "yarn" | "pnpm" | "bun" | "brew" | "scoop" | "choco" | "unknown" @@ -59,22 +58,18 @@ export class UpgradeFailedError extends Schema.TaggedErrorClass Effect.Effect readonly method: () => Effect.Effect - readonly latest: (method?: Method) => Effect.Effect + readonly latest: () => Effect.Effect readonly upgrade: (method: Method, target: string) => Effect.Effect } @@ -204,62 +199,14 @@ export const layer: Layer.Layer diff --git a/packages/opencode/test/installation/installation.test.ts b/packages/opencode/test/installation/installation.test.ts index aaf2a9ea02..cc64031e89 100644 --- a/packages/opencode/test/installation/installation.test.ts +++ b/packages/opencode/test/installation/installation.test.ts @@ -3,7 +3,6 @@ import { Effect, Layer, Stream } from "effect" import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { Installation } from "../../src/installation" -import { InstallationChannel } from "@opencode-ai/core/installation/version" import { AppProcess } from "@opencode-ai/core/process" import { testEffect } from "../lib/effect" @@ -58,117 +57,28 @@ function testLayer( describe("installation", () => { describe("latest", () => { - testEffect(testLayer(() => jsonResponse({ tag_name: "v1.2.3" }))).effect( - "reads release version from GitHub releases", - () => - Effect.gen(function* () { - const result = yield* Installation.use.latest("unknown") - expect(result).toBe("1.2.3") - }), - ) - - testEffect(testLayer(() => jsonResponse({ tag_name: "v4.0.0-beta.1" }))).effect( - "strips v prefix from GitHub release tag", - () => - Effect.gen(function* () { - const result = yield* Installation.use.latest("curl") - expect(result).toBe("4.0.0-beta.1") - }), - ) - - const npmCalls: string[] = [] - testEffect( - testLayer((request) => { - npmCalls.push(request.url) - return jsonResponse({ version: "1.5.0" }) - }), - ).effect("reads npm versions via registry", () => - Effect.gen(function* () { - const result = yield* Installation.use.latest("npm") - expect(result).toBe("1.5.0") - expect(npmCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`) - }), - ) - - const bunCalls: string[] = [] - testEffect( - testLayer((request) => { - bunCalls.push(request.url) - return jsonResponse({ version: "1.6.0" }) - }), - ).effect("reads bun versions via registry", () => - Effect.gen(function* () { - const result = yield* Installation.use.latest("bun") - expect(result).toBe("1.6.0") - expect(bunCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`) - }), - ) - - const pnpmCalls: string[] = [] + const urls: string[] = [] testEffect( testLayer((request) => { - pnpmCalls.push(request.url) - return jsonResponse({ version: "1.7.0" }) + urls.push(request.url) + return jsonResponse({ tag_name: "graphagent-v1.2.3" }) }), - ).effect("reads pnpm versions via registry", () => + ).effect("reads release version from the fork GitHub releases", () => Effect.gen(function* () { - const result = yield* Installation.use.latest("pnpm") - expect(result).toBe("1.7.0") - expect(pnpmCalls).toContain(`https://registry.npmjs.org/opencode-ai/${InstallationChannel}`) + const result = yield* Installation.use.latest() + expect(result).toBe("1.2.3") + expect(urls).toContain("https://api.github.com/repos/LeXwDeX/OpenCode-GraphAgent/releases/latest") }), ) - testEffect(testLayer(() => jsonResponse({ version: "2.3.4" }))).effect("reads scoop manifest versions", () => - Effect.gen(function* () { - const result = yield* Installation.use.latest("scoop") - expect(result).toBe("2.3.4") - }), - ) - - testEffect(testLayer(() => jsonResponse({ d: { results: [{ Version: "3.4.5" }] } }))).effect( - "reads chocolatey feed versions", + testEffect(testLayer(() => jsonResponse({ tag_name: "graphagent-v4.0.0-beta.1" }))).effect( + "strips the graphagent-v prefix from release tags", () => Effect.gen(function* () { - const result = yield* Installation.use.latest("choco") - expect(result).toBe("3.4.5") + const result = yield* Installation.use.latest() + expect(result).toBe("4.0.0-beta.1") }), ) - - testEffect( - testLayer( - () => jsonResponse({ versions: { stable: "2.0.0" } }), - (cmd, args) => { - // getBrewFormula: return core formula (no tap) - if (cmd === "brew" && args.includes("--formula") && args.includes("anomalyco/tap/opencode")) return "" - if (cmd === "brew" && args.includes("--formula") && args.includes("opencode")) return "opencode" - return "" - }, - ), - ).effect("reads brew formulae API versions", () => - Effect.gen(function* () { - const result = yield* Installation.use.latest("brew") - expect(result).toBe("2.0.0") - }), - ) - - const brewInfoJson = JSON.stringify({ - formulae: [{ versions: { stable: "2.1.0" } }], - }) - testEffect( - testLayer( - () => jsonResponse({}), // HTTP not used for tap formula - (cmd, args) => { - if (cmd === "brew" && args.includes("anomalyco/tap/opencode") && args.includes("--formula")) return "opencode" - if (cmd === "brew" && args.includes("--json=v2")) return brewInfoJson - return "" - }, - ), - ).effect("reads brew tap info JSON via CLI", () => - Effect.gen(function* () { - const result = yield* Installation.use.latest("brew") - expect(result).toBe("2.1.0") - }), - ) }) describe("upgrade", () => {