From 4179eebcf255b06a5554e2a99d36683da2b83f90 Mon Sep 17 00:00:00 2001 From: Panagiotis Theologou Date: Wed, 16 Sep 2026 11:03:53 +0000 Subject: [PATCH 1/2] fix(ci): verify the packed schema export and make check-compatibility cwd-independent - The packed-artifact step now asserts options.schema.json is in the tarball and that the opencode2-tps/options.schema.json export resolves (with the JSON import attribute) and carries the expected title. - scripts/check-compatibility.mjs resolves node_modules/.bin against the script location instead of cwd, matching its other path resolutions. Co-authored-by: Panagiotis Theologou --- .github/workflows/ci.yml | 2 ++ scripts/check-compatibility.mjs | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc62b0b..8c13d64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,9 @@ jobs: run: | mkdir -p "$RUNNER_TEMP/tps-pack" "$RUNNER_TEMP/tps-install" npm pack --ignore-scripts --pack-destination "$RUNNER_TEMP/tps-pack" + tar -tzf "$RUNNER_TEMP"/tps-pack/opencode2-tps-*.tgz | grep -qx 'package/options.schema.json' || { echo "options.schema.json missing from the tarball"; exit 1; } opentui=$(node -p 'require("./node_modules/@opentui/solid/package.json").version') solid=$(node -p 'require("./node_modules/solid-js/package.json").version') npm install --ignore-scripts --prefix "$RUNNER_TEMP/tps-install" "$RUNNER_TEMP"/tps-pack/opencode2-tps-*.tgz "@opentui/solid@${opentui}" "solid-js@${solid}" node --input-type=module -e 'const m = await import(process.argv[1]); if (m.default?.id !== "opencode2.tps") { console.error("unexpected entrypoint export:", m.default); process.exit(1) }' "file://$RUNNER_TEMP/tps-install/node_modules/opencode2-tps/dist/tui.js" + (cd "$RUNNER_TEMP/tps-install" && node --input-type=module -e 'const s = await import("opencode2-tps/options.schema.json", { with: { type: "json" } }); if (s.default.title !== "opencode2-tps plugin options") { console.error("unexpected schema export:", s.default.title); process.exit(1) }') diff --git a/scripts/check-compatibility.mjs b/scripts/check-compatibility.mjs index 97d7fa3..26646b6 100644 --- a/scripts/check-compatibility.mjs +++ b/scripts/check-compatibility.mjs @@ -4,6 +4,7 @@ import { execFileSync } from "node:child_process" import { readFileSync } from "node:fs" import { resolve } from "node:path" +import { fileURLToPath } from "node:url" const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) @@ -33,7 +34,10 @@ if (Number(floor.replace("0.0.0-beta-", "")) > Number(version.replace("0.0.0-bet throw new Error(`README floor ${floor} is newer than the pinned compatibility target ${version}`) } -const executable = resolve("node_modules", ".bin", process.platform === "win32" ? "opencode2.cmd" : "opencode2") +// Resolved against this script's location, not cwd, so the check works from any directory. +const root = fileURLToPath(new URL("..", import.meta.url)) + +const executable = resolve(root, "node_modules", ".bin", process.platform === "win32" ? "opencode2.cmd" : "opencode2") const reported = execFileSync(executable, ["--version"], { encoding: "utf8" }).trim() From 8ae345857b9bb5a83a0c2045fa08bc167be17cd9 Mon Sep 17 00:00:00 2001 From: Panagiotis Theologou Date: Wed, 16 Sep 2026 12:31:53 +0000 Subject: [PATCH 2/2] fix(ci): exec the native CLI binary and baseline CI on Node 26.4 - check-compatibility runs @opencode/cli's real bin target (bin/opencode2.exe) instead of npm's .bin wrapper: .cmd shims need a shell on Windows, while the native binary executes unshelled on every platform; also drops the process.platform branch. - CI's node-version moves 24 -> 26.4.0, @opentui/core's documented Node floor, and @types/node follows (^26.0.0, lock regenerated). - The two setInterval test doubles cast to typeof globalThis.setInterval: Node 26 types add a conditional rest-args overload no two-parameter double can satisfy. Co-authored-by: Panagiotis Theologou --- .github/workflows/ci.yml | 3 ++- docs/development.md | 2 ++ package-lock.json | 16 ++++++++-------- package.json | 2 +- scripts/check-compatibility.mjs | 6 +++++- tests/entrypoint.test.tsx | 8 ++++++-- tests/plugin.test.ts | 8 ++++++-- 7 files changed, 30 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c13d64..40124bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,9 +14,10 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + # 26.4.0 is @opentui/core's documented Node floor; the non-bun toolchain runs on it. - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24 + node-version: 26.4.0 cache: npm # tsc, oxlint and scripts/build.mjs run on node; the test script is `bun test`. diff --git a/docs/development.md b/docs/development.md index 5933bf7..f2ba676 100644 --- a/docs/development.md +++ b/docs/development.md @@ -14,6 +14,8 @@ Tests run through `bun test`. `bunfig.toml` preloads `@opentui/solid/preload` so `scripts/*.mjs` run under plain node and stay outside the `tsconfig.json` typecheck — they are exercised by CI and the entrypoint test instead. +CI runs the Node-side toolchain on Node 26.4, the `@opentui/core` documented floor; tests run under Bun either way. + ## Run from source Point a path entry in `cli.json` at this repository's directory. The loader resolves `/tui.tsx`, which re-exports the plugin definition from `src/plugin.tsx`, transforms the source, and watches it — saving a file under `src/` reloads the plugin without a restart. diff --git a/package-lock.json b/package-lock.json index 39616b4..1ce21d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@opentui/solid": "^0.5.10", "@oxlint/plugins": "^1.78.0", "@types/bun": "^1.3.14", - "@types/node": "^24.0.0", + "@types/node": "^26.0.0", "babel-preset-solid": "^1.9.12", "oxlint": "^1.78.0", "solid-js": "^1.9.0", @@ -3793,13 +3793,13 @@ } }, "node_modules/@types/node": { - "version": "24.13.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", - "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "version": "26.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.1.tgz", + "integrity": "sha512-VqGJBMCtdhqkBUCcBLvywI0NJ+KLuVzgNnlBUNFOQjqVxzo2lxLUNg1DSey8+u2u6ktswSAxg+s68QLzWHNOuA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": "~8.9.0" } }, "node_modules/@types/ws": { @@ -6739,9 +6739,9 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 41a862d..be671b9 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@opentui/solid": "^0.5.10", "@oxlint/plugins": "^1.78.0", "@types/bun": "^1.3.14", - "@types/node": "^24.0.0", + "@types/node": "^26.0.0", "babel-preset-solid": "^1.9.12", "oxlint": "^1.78.0", "solid-js": "^1.9.0", diff --git a/scripts/check-compatibility.mjs b/scripts/check-compatibility.mjs index 26646b6..cd2cd25 100644 --- a/scripts/check-compatibility.mjs +++ b/scripts/check-compatibility.mjs @@ -37,7 +37,11 @@ if (Number(floor.replace("0.0.0-beta-", "")) > Number(version.replace("0.0.0-bet // Resolved against this script's location, not cwd, so the check works from any directory. const root = fileURLToPath(new URL("..", import.meta.url)) -const executable = resolve(root, "node_modules", ".bin", process.platform === "win32" ? "opencode2.cmd" : "opencode2") +// The package's own bin target, not npm's `.bin` wrapper: a .cmd shim cannot be +// execFileSync'd on Windows without a shell, while the native binary runs +// unshelled on every platform despite the .exe name. A normal install's +// postinstall (or CI's prepare step with --ignore-scripts) puts it there. +const executable = resolve(root, "node_modules", "@opencode", "cli", "bin", "opencode2.exe") const reported = execFileSync(executable, ["--version"], { encoding: "utf8" }).trim() diff --git a/tests/entrypoint.test.tsx b/tests/entrypoint.test.tsx index 5ab9678..a00d1bf 100644 --- a/tests/entrypoint.test.tsx +++ b/tests/entrypoint.test.tsx @@ -106,7 +106,11 @@ function createHarness(options: TpsOptionsInput = {}): Harness { const realClearInterval = globalThis.clearInterval // Rendering is throttled by the plugin's own interval; capture the callback // so the test can flush on demand instead of waiting on wall-clock time. - globalThis.setInterval = (callback: () => void, _ms?: number) => { + // SAFETY: Node 26's setInterval type has a conditional rest-args overload no + // two-parameter double can satisfy; the double ignores extra arguments by + // design, so the assignment is narrowed in one step — a test-double + // limitation, not a production cast. + globalThis.setInterval = ((callback: () => void, _ms?: number) => { flush = callback // A real (immediately cancelled) handle keeps the host's return type honest // without leaving a live interval behind. @@ -114,7 +118,7 @@ function createHarness(options: TpsOptionsInput = {}): Harness { realClearInterval(handle) return handle - } + }) as typeof globalThis.setInterval globalThis.clearInterval = () => { flush = undefined diff --git a/tests/plugin.test.ts b/tests/plugin.test.ts index a094577..ee70360 100644 --- a/tests/plugin.test.ts +++ b/tests/plugin.test.ts @@ -78,7 +78,11 @@ function createHarness(options: TpsOptionsInput = {}) { const realSetInterval = globalThis.setInterval const realClearInterval = globalThis.clearInterval - globalThis.setInterval = (fn: () => void, ms?: number) => { + // SAFETY: Node 26's setInterval type has a conditional rest-args overload no + // two-parameter double can satisfy; the double ignores extra arguments by + // design, so the assignment is narrowed in one step — a test-double + // limitation, not a production cast. + globalThis.setInterval = ((fn: () => void, ms?: number) => { timer.callback = fn timer.intervalMs = ms ?? 0 timer.created += 1 @@ -88,7 +92,7 @@ function createHarness(options: TpsOptionsInput = {}) { realClearInterval(handle) return handle - } + }) as typeof globalThis.setInterval globalThis.clearInterval = () => { timer.cleared += 1