Skip to content
Merged
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
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -44,7 +45,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) }')
2 changes: 2 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<directory>/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.
Expand Down
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 9 additions & 1 deletion scripts/check-compatibility.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))

Expand Down Expand Up @@ -33,7 +34,14 @@ 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))

// 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()

Expand Down
8 changes: 6 additions & 2 deletions tests/entrypoint.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,19 @@ 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.
const handle = realSetInterval(() => {}, 60_000)
realClearInterval(handle)

return handle
}
}) as typeof globalThis.setInterval

globalThis.clearInterval = () => {
flush = undefined
Expand Down
8 changes: 6 additions & 2 deletions tests/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -88,7 +92,7 @@ function createHarness(options: TpsOptionsInput = {}) {
realClearInterval(handle)

return handle
}
}) as typeof globalThis.setInterval

globalThis.clearInterval = () => {
timer.cleared += 1
Expand Down