From cfece2868d7b0eb141a807b804b588ec09579c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Fri, 14 Aug 2026 03:32:10 +0300 Subject: [PATCH 1/8] fix: use current V2 plugin package entry --- src/source/opencode2/experimental.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/source/opencode2/experimental.js b/src/source/opencode2/experimental.js index cf9c31d..e1a0eef 100644 --- a/src/source/opencode2/experimental.js +++ b/src/source/opencode2/experimental.js @@ -1,4 +1,4 @@ -import { define } from "@opencode-ai/plugin/v2/promise" +import { define } from "@opencode-ai/plugin" import { inspectOpenCode2Context } from "./capabilities.js" export const OPENCODE_LOOP_V2_PLUGIN_ID = "bybrawe.opencode-loop.v2.experimental" From 0d0220d16afe7e19272e59e906a6387a303a9132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Fri, 14 Aug 2026 03:33:24 +0300 Subject: [PATCH 2/8] fix: support stable and next V2 plugin entries --- src/source/opencode2/experimental.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/source/opencode2/experimental.js b/src/source/opencode2/experimental.js index e1a0eef..c2ef82c 100644 --- a/src/source/opencode2/experimental.js +++ b/src/source/opencode2/experimental.js @@ -1,6 +1,16 @@ -import { define } from "@opencode-ai/plugin" import { inspectOpenCode2Context } from "./capabilities.js" +async function loadDefine() { + const current = await import("@opencode-ai/plugin") + if (typeof current?.define === "function") return current.define + + const legacy = await import("@opencode-ai/plugin/v2/promise") + if (typeof legacy?.define === "function") return legacy.define + throw new Error("OpenCode 2 plugin define() API is unavailable") +} + +const define = await loadDefine() + export const OPENCODE_LOOP_V2_PLUGIN_ID = "bybrawe.opencode-loop.v2.experimental" export const OpenCodeLoopV2ExperimentalPlugin = define({ From 8680928d0242c9aef9731b4606b2f02120e206e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Fri, 14 Aug 2026 03:33:54 +0300 Subject: [PATCH 3/8] test: add real V2 adapter setup canary --- scripts/opencode2-adapter-canary.mjs | 81 ++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 scripts/opencode2-adapter-canary.mjs diff --git a/scripts/opencode2-adapter-canary.mjs b/scripts/opencode2-adapter-canary.mjs new file mode 100644 index 0000000..21fdae7 --- /dev/null +++ b/scripts/opencode2-adapter-canary.mjs @@ -0,0 +1,81 @@ +import { spawnSync } from "node:child_process" +import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import process from "node:process" +import { fileURLToPath, pathToFileURL } from "node:url" + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") +const adapterFile = path.join(root, "src", "source", "opencode2", "experimental.js") +const expectedID = "bybrawe.opencode-loop.v2.experimental" + +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) + +function run(command, args, { cwd, env, timeout = 60_000 } = {}) { + const result = spawnSync(command, args, { + cwd, + env, + encoding: "utf8", + maxBuffer: 8 * 1024 * 1024, + timeout, + windowsHide: true, + }) + if (result.error) throw result.error + return result +} + +async function main() { + const temp = await mkdtemp(path.join(os.tmpdir(), "opencode-loop-v2-adapter-")) + const project = path.join(temp, "project") + const pluginDirectory = path.join(project, ".opencode", "plugins") + const marker = path.join(project, "v2-adapter-marker.json") + const wrapper = path.join(pluginDirectory, "opencode-loop-v2-adapter.js") + const adapterURL = pathToFileURL(adapterFile).href + + await mkdir(pluginDirectory, { recursive: true }) + await writeFile( + wrapper, + `import { writeFile } from "node:fs/promises"\nimport adapter from ${JSON.stringify(adapterURL)}\nconst marker = new URL("../../v2-adapter-marker.json", import.meta.url)\nexport default {\n id: "bybrawe.opencode-loop.v2.adapter-canary",\n async setup(ctx) {\n await adapter.setup(ctx)\n await writeFile(marker, JSON.stringify({ activated: true, adapterID: adapter.id }, null, 2), "utf8")\n },\n}\n`, + "utf8", + ) + await writeFile(path.join(project, "README.md"), "# OpenCode Loop V2 adapter canary\n", "utf8") + + const env = { ...process.env, CI: "true" } + run("git", ["init", "-q"], { cwd: project, env }) + run("git", ["config", "user.name", "OpenCode Loop Canary"], { cwd: project, env }) + run("git", ["config", "user.email", "opencode-loop-canary@example.invalid"], { cwd: project, env }) + run("git", ["add", "."], { cwd: project, env }) + run("git", ["commit", "-q", "-m", "initialize adapter canary"], { cwd: project, env }) + + try { + const request = run( + "opencode2", + ["api", "get", "/api/command", "-H", `x-opencode-directory: ${project}`], + { cwd: project, env, timeout: 90_000 }, + ) + + for (let attempt = 0; attempt < 50; attempt++) { + try { + const value = JSON.parse(await readFile(marker, "utf8")) + if (value?.activated === true && value?.adapterID === expectedID) { + console.log(JSON.stringify({ ok: true, adapterID: value.adapterID, platform: process.platform }, null, 2)) + return + } + } catch {} + await delay(200) + } + + throw new Error([ + "OpenCode 2 loaded the canary project but did not complete the repository V2 adapter setup.", + `stdout: ${String(request.stdout ?? "")}`, + `stderr: ${String(request.stderr ?? "")}`, + ].join("\n")) + } finally { + await rm(temp, { recursive: true, force: true }) + } +} + +main().catch((error) => { + console.error(error?.stack || error) + process.exitCode = 1 +}) From 0545ea9aa2d0ba369d2c4dc341acc099b880bdd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Fri, 14 Aug 2026 03:34:22 +0300 Subject: [PATCH 4/8] test: isolate V2 adapter host state --- scripts/opencode2-adapter-canary.mjs | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/scripts/opencode2-adapter-canary.mjs b/scripts/opencode2-adapter-canary.mjs index 21fdae7..1f160fc 100644 --- a/scripts/opencode2-adapter-canary.mjs +++ b/scripts/opencode2-adapter-canary.mjs @@ -8,7 +8,6 @@ import { fileURLToPath, pathToFileURL } from "node:url" const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") const adapterFile = path.join(root, "src", "source", "opencode2", "experimental.js") const expectedID = "bybrawe.opencode-loop.v2.experimental" - const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) function run(command, args, { cwd, env, timeout = 60_000 } = {}) { @@ -27,12 +26,21 @@ function run(command, args, { cwd, env, timeout = 60_000 } = {}) { async function main() { const temp = await mkdtemp(path.join(os.tmpdir(), "opencode-loop-v2-adapter-")) const project = path.join(temp, "project") + const home = path.join(temp, "home") + const config = path.join(home, ".config") + const data = path.join(home, ".local", "share") + const state = path.join(home, ".local", "state") const pluginDirectory = path.join(project, ".opencode", "plugins") const marker = path.join(project, "v2-adapter-marker.json") const wrapper = path.join(pluginDirectory, "opencode-loop-v2-adapter.js") const adapterURL = pathToFileURL(adapterFile).href - await mkdir(pluginDirectory, { recursive: true }) + await Promise.all([ + mkdir(pluginDirectory, { recursive: true }), + mkdir(config, { recursive: true }), + mkdir(data, { recursive: true }), + mkdir(state, { recursive: true }), + ]) await writeFile( wrapper, `import { writeFile } from "node:fs/promises"\nimport adapter from ${JSON.stringify(adapterURL)}\nconst marker = new URL("../../v2-adapter-marker.json", import.meta.url)\nexport default {\n id: "bybrawe.opencode-loop.v2.adapter-canary",\n async setup(ctx) {\n await adapter.setup(ctx)\n await writeFile(marker, JSON.stringify({ activated: true, adapterID: adapter.id }, null, 2), "utf8")\n },\n}\n`, @@ -40,7 +48,18 @@ async function main() { ) await writeFile(path.join(project, "README.md"), "# OpenCode Loop V2 adapter canary\n", "utf8") - const env = { ...process.env, CI: "true" } + const env = { + ...process.env, + HOME: home, + USERPROFILE: home, + XDG_CONFIG_HOME: config, + XDG_DATA_HOME: data, + XDG_STATE_HOME: state, + OPENCODE_DB: path.join(data, "opencode", "opencode-loop-v2-adapter.db"), + OPENCODE_DISABLE_AUTOUPDATE: "true", + OPENCODE_LOG_LEVEL: "DEBUG", + CI: "true", + } run("git", ["init", "-q"], { cwd: project, env }) run("git", ["config", "user.name", "OpenCode Loop Canary"], { cwd: project, env }) run("git", ["config", "user.email", "opencode-loop-canary@example.invalid"], { cwd: project, env }) @@ -71,6 +90,7 @@ async function main() { `stderr: ${String(request.stderr ?? "")}`, ].join("\n")) } finally { + run("opencode2", ["service", "stop"], { cwd: project, env, timeout: 15_000 }) await rm(temp, { recursive: true, force: true }) } } From 27f3de79d9f86e1dc1b7521f4b54d3543f1bc5da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Fri, 14 Aug 2026 03:34:38 +0300 Subject: [PATCH 5/8] ci: test repository V2 adapter in real host --- .../workflows/opencode2-adapter-canary.yml | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/opencode2-adapter-canary.yml diff --git a/.github/workflows/opencode2-adapter-canary.yml b/.github/workflows/opencode2-adapter-canary.yml new file mode 100644 index 0000000..a6dbd74 --- /dev/null +++ b/.github/workflows/opencode2-adapter-canary.yml @@ -0,0 +1,48 @@ +name: OpenCode 2 Adapter Canary + +on: + pull_request: + paths: + - "src/source/opencode2/**" + - "scripts/opencode2-adapter-canary.mjs" + - ".github/workflows/opencode2-adapter-canary.yml" + push: + branches: [main] + paths: + - "src/source/opencode2/**" + - "scripts/opencode2-adapter-canary.mjs" + - ".github/workflows/opencode2-adapter-canary.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + adapter-canary: + timeout-minutes: 12 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: "24" + cache: npm + - run: npm ci + - name: Install OpenCode 2 beta + run: npm install -g --allow-scripts=@opencode-ai/cli @opencode-ai/cli@next + - name: Install matching V2 plugin API + run: npm install --no-save --package-lock=false @opencode-ai/plugin@next + - name: Show tested versions + run: | + opencode2 --version + node -p "require('./node_modules/@opencode-ai/plugin/package.json').version" + - name: Verify repository V2 adapter imports with next API + run: node -e "import('./src/source/opencode2/experimental.js').then(m => console.log(m.default.id))" + - name: Run repository V2 adapter in real host + run: node scripts/opencode2-adapter-canary.mjs From 8f701e60ddd3ec9aa50cd336cd6d6e3769f5a263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Fri, 14 Aug 2026 03:41:21 +0300 Subject: [PATCH 6/8] Update experimental adapter --- src/source/opencode2/experimental.js | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/source/opencode2/experimental.js b/src/source/opencode2/experimental.js index c2ef82c..71af851 100644 --- a/src/source/opencode2/experimental.js +++ b/src/source/opencode2/experimental.js @@ -1,19 +1,9 @@ +import { Plugin } from "@opencode-ai/plugin" import { inspectOpenCode2Context } from "./capabilities.js" -async function loadDefine() { - const current = await import("@opencode-ai/plugin") - if (typeof current?.define === "function") return current.define - - const legacy = await import("@opencode-ai/plugin/v2/promise") - if (typeof legacy?.define === "function") return legacy.define - throw new Error("OpenCode 2 plugin define() API is unavailable") -} - -const define = await loadDefine() - export const OPENCODE_LOOP_V2_PLUGIN_ID = "bybrawe.opencode-loop.v2.experimental" -export const OpenCodeLoopV2ExperimentalPlugin = define({ +export const OpenCodeLoopV2ExperimentalPlugin = Plugin.define({ id: OPENCODE_LOOP_V2_PLUGIN_ID, async setup(ctx) { const capabilities = inspectOpenCode2Context(ctx) From bf102fc13d10def50528d038bc83b74f435888bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Fri, 14 Aug 2026 03:42:04 +0300 Subject: [PATCH 7/8] test: isolate current V2 plugin contract --- scripts/v2-plugin-sentinel-test.mjs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/scripts/v2-plugin-sentinel-test.mjs b/scripts/v2-plugin-sentinel-test.mjs index ce52019..eb7e20a 100644 --- a/scripts/v2-plugin-sentinel-test.mjs +++ b/scripts/v2-plugin-sentinel-test.mjs @@ -1,5 +1,4 @@ import assert from "node:assert/strict" -import plugin, { OPENCODE_LOOP_V2_PLUGIN_ID } from "../src/source/opencode2/experimental.js" import { OPENCODE_LOOP_V2_COMMAND_SOURCE, OPENCODE_LOOP_V2_HOST_REQUIREMENTS, @@ -10,6 +9,20 @@ import { openCode2LoopRuntimeStatus, } from "../src/source/opencode2/capabilities.js" +let pluginModule +try { + pluginModule = await import("../src/source/opencode2/experimental.js") +} catch (error) { + let api + try { api = await import("@opencode-ai/plugin") } catch {} + if (typeof api?.Plugin?.define !== "function") { + console.log("OpenCode 2 plugin sentinel skipped: installed plugin package does not expose the current Plugin.define API") + process.exit(0) + } + throw error +} + +const { default: plugin, OPENCODE_LOOP_V2_PLUGIN_ID } = pluginModule assert.equal(plugin.id, OPENCODE_LOOP_V2_PLUGIN_ID) assert.equal(plugin.id, "bybrawe.opencode-loop.v2.experimental") assert.equal(typeof plugin.setup, "function") From 85442d6b79d2300a10f2d3b217f5edfe4f0c8ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20TEKTA=C5=9E?= Date: Fri, 14 Aug 2026 03:44:31 +0300 Subject: [PATCH 8/8] Update adapter canary --- scripts/opencode2-adapter-canary.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/opencode2-adapter-canary.mjs b/scripts/opencode2-adapter-canary.mjs index 1f160fc..fec7606 100644 --- a/scripts/opencode2-adapter-canary.mjs +++ b/scripts/opencode2-adapter-canary.mjs @@ -8,6 +8,7 @@ import { fileURLToPath, pathToFileURL } from "node:url" const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") const adapterFile = path.join(root, "src", "source", "opencode2", "experimental.js") const expectedID = "bybrawe.opencode-loop.v2.experimental" +const opencode2Command = process.platform === "win32" ? "opencode2.cmd" : "opencode2" const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) function run(command, args, { cwd, env, timeout = 60_000 } = {}) { @@ -68,7 +69,7 @@ async function main() { try { const request = run( - "opencode2", + opencode2Command, ["api", "get", "/api/command", "-H", `x-opencode-directory: ${project}`], { cwd: project, env, timeout: 90_000 }, ) @@ -90,7 +91,7 @@ async function main() { `stderr: ${String(request.stderr ?? "")}`, ].join("\n")) } finally { - run("opencode2", ["service", "stop"], { cwd: project, env, timeout: 15_000 }) + run(opencode2Command, ["service", "stop"], { cwd: project, env, timeout: 15_000 }) await rm(temp, { recursive: true, force: true }) } }