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 diff --git a/scripts/opencode2-adapter-canary.mjs b/scripts/opencode2-adapter-canary.mjs new file mode 100644 index 0000000..fec7606 --- /dev/null +++ b/scripts/opencode2-adapter-canary.mjs @@ -0,0 +1,102 @@ +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 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 } = {}) { + 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 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 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`, + "utf8", + ) + await writeFile(path.join(project, "README.md"), "# OpenCode Loop V2 adapter canary\n", "utf8") + + 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 }) + run("git", ["add", "."], { cwd: project, env }) + run("git", ["commit", "-q", "-m", "initialize adapter canary"], { cwd: project, env }) + + try { + const request = run( + opencode2Command, + ["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 { + run(opencode2Command, ["service", "stop"], { cwd: project, env, timeout: 15_000 }) + await rm(temp, { recursive: true, force: true }) + } +} + +main().catch((error) => { + console.error(error?.stack || error) + process.exitCode = 1 +}) 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") diff --git a/src/source/opencode2/experimental.js b/src/source/opencode2/experimental.js index cf9c31d..71af851 100644 --- a/src/source/opencode2/experimental.js +++ b/src/source/opencode2/experimental.js @@ -1,9 +1,9 @@ -import { define } from "@opencode-ai/plugin/v2/promise" +import { Plugin } from "@opencode-ai/plugin" import { inspectOpenCode2Context } from "./capabilities.js" 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)