Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/opencode2-adapter-canary.yml
Original file line number Diff line number Diff line change
@@ -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
102 changes: 102 additions & 0 deletions scripts/opencode2-adapter-canary.mjs
Original file line number Diff line number Diff line change
@@ -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
})
15 changes: 14 additions & 1 deletion scripts/v2-plugin-sentinel-test.mjs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions src/source/opencode2/experimental.js
Original file line number Diff line number Diff line change
@@ -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)
Expand Down