From 83f45ff4cfc010fe7253aa51afa9a6ad62ad6603 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Mon, 11 May 2026 16:06:23 +0100 Subject: [PATCH 1/4] feat(opencode): package CodeNomad plugin Replace the copied opencode config template with a versioned npm-packable CodeNomad OpenCode plugin package. The server now builds and packs the plugin artifact, and desktop bundles carry the packaged plugin through the existing server resources. Workspace launches now merge any user or system OPENCODE_CONFIG_CONTENT JSONC with CodeNomad's plugin entry, preserving existing plugin config while ensuring the managed bridge is available. Production uses an explicit npm file alias for the packaged tarball, while dev keeps loading the TypeScript plugin source directly. Validated with plugin build, server and Electron typechecks, targeted plugin config tests, and WSL spawn environment tests. --- package-lock.json | 270 +++++++++++-- package.json | 3 +- packages/electron-app/package.json | 4 +- packages/opencode-config/opencode.jsonc | 3 - packages/opencode-config/package-lock.json | 380 ------------------ packages/opencode-config/package.json | 9 - .../README.md | 18 +- packages/opencode-plugin/package.json | 23 ++ .../plugin/codenomad.ts | 4 +- .../plugin/lib/background-process.ts | 2 +- .../plugin/lib/client.ts | 4 +- .../plugin/lib/request.ts | 0 packages/opencode-plugin/tsconfig.json | 17 + packages/server/package.json | 4 +- .../server/scripts/copy-opencode-config.mjs | 61 --- .../scripts/package-opencode-plugin.mjs | 59 +++ packages/server/src/opencode-config.ts | 31 -- packages/server/src/opencode-plugin.test.ts | 38 ++ packages/server/src/opencode-plugin.ts | 175 ++++++++ .../src/workspaces/__tests__/spawn.test.ts | 16 +- packages/server/src/workspaces/manager.ts | 16 +- packages/server/src/workspaces/spawn.ts | 2 +- 22 files changed, 582 insertions(+), 557 deletions(-) delete mode 100644 packages/opencode-config/opencode.jsonc delete mode 100644 packages/opencode-config/package-lock.json delete mode 100644 packages/opencode-config/package.json rename packages/{opencode-config => opencode-plugin}/README.md (55%) create mode 100644 packages/opencode-plugin/package.json rename packages/{opencode-config => opencode-plugin}/plugin/codenomad.ts (99%) rename packages/{opencode-config => opencode-plugin}/plugin/lib/background-process.ts (99%) rename packages/{opencode-config => opencode-plugin}/plugin/lib/client.ts (98%) rename packages/{opencode-config => opencode-plugin}/plugin/lib/request.ts (100%) create mode 100644 packages/opencode-plugin/tsconfig.json delete mode 100644 packages/server/scripts/copy-opencode-config.mjs create mode 100644 packages/server/scripts/package-opencode-plugin.mjs delete mode 100644 packages/server/src/opencode-config.ts create mode 100644 packages/server/src/opencode-plugin.test.ts create mode 100644 packages/server/src/opencode-plugin.ts diff --git a/package-lock.json b/package-lock.json index 83d6dd284..86db7d010 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.15.0", "license": "MIT", "dependencies": { + "@tauri-apps/cli-darwin-arm64": "^2.11.1", "7zip-bin": "^5.2.0", "google-auth-library": "^10.5.0" }, @@ -29,7 +30,7 @@ "packages/ui", "packages/electron-app", "packages/tauri-app", - "packages/opencode-config" + "packages/opencode-plugin" ] } }, @@ -1575,8 +1576,8 @@ "dev": true, "license": "MIT" }, - "node_modules/@codenomad/opencode-config": { - "resolved": "packages/opencode-config", + "node_modules/@codenomad/codenomad-opencode-plugin": { + "resolved": "packages/opencode-plugin", "link": true }, "node_modules/@codenomad/tauri-app": { @@ -3228,6 +3229,43 @@ "node": ">= 8" } }, + "node_modules/@opencode-ai/plugin": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.3.7.tgz", + "integrity": "sha512-pVBIcYtHiniQ93Gj/KRkhrIz1oIAwGRifb7+dfGWdHRy00gr9DyEHFYmgHcBYgfrBavZrWw2xmqEDJdjdBuC7g==", + "license": "MIT", + "dependencies": { + "@opencode-ai/sdk": "1.3.7", + "zod": "4.1.8" + }, + "peerDependencies": { + "@opentui/core": ">=0.1.92", + "@opentui/solid": ">=0.1.92" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } + } + }, + "node_modules/@opencode-ai/plugin/node_modules/@opencode-ai/sdk": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.3.7.tgz", + "integrity": "sha512-ugkta0v0dMZchN15QGmqHb9zf35k+K1VM9wt3x4ZRJ6GxKAs0XlCmQPQJflgV9YSedNxjkgTud0GCCIWUSiUOg==", + "license": "MIT" + }, + "node_modules/@opencode-ai/plugin/node_modules/zod": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", + "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@opencode-ai/sdk": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.2.6.tgz", @@ -3998,6 +4036,21 @@ "@tauri-apps/cli-win32-x64-msvc": "2.9.4" } }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.1.tgz", + "integrity": "sha512-6eEKMBXsQPCuM1EmvrjT2+aBuxWQuFdKdW8pzNuNQtpq45nEEpBlD5gr8pUeAyOU1DQKlkFaEc/MPBxb/Pfjtg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 OR MIT", + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@tauri-apps/cli-darwin-x64": { "version": "2.9.4", "cpu": [ @@ -4013,6 +4066,157 @@ "node": ">= 10" } }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.9.4.tgz", + "integrity": "sha512-tTWkEPig+2z3Rk0zqZYfjUYcgD+aSm72wdrIhdYobxbQZOBw0zfn50YtWv+av7bm0SHvv75f0l7JuwgZM1HFow==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.9.4.tgz", + "integrity": "sha512-ql6vJ611qoqRYHxkKPnb2vHa27U+YRKRmIpLMMBeZnfFtZ938eao7402AQCH1mO2+/8ioUhbpy9R/ZcLTXVmkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.9.4.tgz", + "integrity": "sha512-vg7yNn7ICTi6hRrcA/6ff2UpZQP7un3xe3SEld5QM0prgridbKAiXGaCKr3BnUBx/rGXegQlD/wiLcWdiiraSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.9.4.tgz", + "integrity": "sha512-l8L+3VxNk6yv5T/Z/gv5ysngmIpsai40B9p6NQQyqYqxImqYX37pqREoEBl1YwG7szGnDibpWhidPrWKR59OJA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.9.4.tgz", + "integrity": "sha512-PepPhCXc/xVvE3foykNho46OmCyx47E/aG676vKTVp+mqin5d+IBqDL6wDKiGNT5OTTxKEyNlCQ81Xs2BQhhqA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.9.4.tgz", + "integrity": "sha512-zcd1QVffh5tZs1u1SCKUV/V7RRynebgYUNWHuV0FsIF1MjnULUChEXhAhug7usCDq4GZReMJOoXa6rukEozWIw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.9.4.tgz", + "integrity": "sha512-/7ZhnP6PY04bEob23q8MH/EoDISdmR1wuNm0k9d5HV7TDMd2GGCDa8dPXA4vJuglJKXIfXqxFmZ4L+J+MO42+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.9.4.tgz", + "integrity": "sha512-1LmAfaC4Cq+3O1Ir1ksdhczhdtFSTIV51tbAGtbV/mr348O+M52A/xwCCXQank0OcdBxy5BctqkMtuZnQvA8uQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@tauri-apps/cli-win32-x64-msvc": { "version": "2.9.4", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.9.4.tgz", @@ -4030,6 +4234,23 @@ "node": ">= 10" } }, + "node_modules/@tauri-apps/cli/node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.9.4.tgz", + "integrity": "sha512-9rHkMVtbMhe0AliVbrGpzMahOBg3rwV46JYRELxR9SN6iu1dvPOaMaiC4cP6M/aD1424ziXnnMdYU06RAH8oIw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@tauri-apps/plugin-dialog": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.6.0.tgz", @@ -13243,49 +13464,16 @@ "dev": true, "license": "MIT" }, - "packages/opencode-config": { - "name": "@codenomad/opencode-config", + "packages/opencode-plugin": { + "name": "@codenomad/codenomad-opencode-plugin", "version": "0.15.0", "license": "MIT", "dependencies": { "@opencode-ai/plugin": "1.3.7" - } - }, - "packages/opencode-config/node_modules/@opencode-ai/plugin": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.3.7.tgz", - "integrity": "sha512-pVBIcYtHiniQ93Gj/KRkhrIz1oIAwGRifb7+dfGWdHRy00gr9DyEHFYmgHcBYgfrBavZrWw2xmqEDJdjdBuC7g==", - "license": "MIT", - "dependencies": { - "@opencode-ai/sdk": "1.3.7", - "zod": "4.1.8" }, - "peerDependencies": { - "@opentui/core": ">=0.1.92", - "@opentui/solid": ">=0.1.92" - }, - "peerDependenciesMeta": { - "@opentui/core": { - "optional": true - }, - "@opentui/solid": { - "optional": true - } - } - }, - "packages/opencode-config/node_modules/@opencode-ai/sdk": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.3.7.tgz", - "integrity": "sha512-ugkta0v0dMZchN15QGmqHb9zf35k+K1VM9wt3x4ZRJ6GxKAs0XlCmQPQJflgV9YSedNxjkgTud0GCCIWUSiUOg==", - "license": "MIT" - }, - "packages/opencode-config/node_modules/zod": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", - "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" + "devDependencies": { + "@types/node": "^22.18.0", + "typescript": "^5.6.3" } }, "packages/server": { diff --git a/package.json b/package.json index 387e35b1b..32907a188 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "packages/ui", "packages/electron-app", "packages/tauri-app", - "packages/opencode-config" + "packages/opencode-plugin" ] }, "scripts": { @@ -26,6 +26,7 @@ "bumpVersion": "node ./scripts/bump-version.js" }, "dependencies": { + "@tauri-apps/cli-darwin-arm64": "^2.11.1", "7zip-bin": "^5.2.0", "google-auth-library": "^10.5.0" }, diff --git a/packages/electron-app/package.json b/packages/electron-app/package.json index 9fca6f98f..460daf6a3 100644 --- a/packages/electron-app/package.json +++ b/packages/electron-app/package.json @@ -79,8 +79,8 @@ ] }, { - "from": "../server/dist/opencode-config", - "to": "opencode-config" + "from": "../server/dist/opencode-plugin", + "to": "opencode-plugin" } ], "mac": { diff --git a/packages/opencode-config/opencode.jsonc b/packages/opencode-config/opencode.jsonc deleted file mode 100644 index c3eb6a56f..000000000 --- a/packages/opencode-config/opencode.jsonc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "$schema": "https://opencode.ai/config.json" -} \ No newline at end of file diff --git a/packages/opencode-config/package-lock.json b/packages/opencode-config/package-lock.json deleted file mode 100644 index 840b67473..000000000 --- a/packages/opencode-config/package-lock.json +++ /dev/null @@ -1,380 +0,0 @@ -{ - "name": "@codenomad/opencode-config", - "version": "0.15.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@codenomad/opencode-config", - "version": "0.15.0", - "license": "MIT", - "dependencies": { - "@opencode-ai/plugin": "1.14.24" - } - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@opencode-ai/plugin": { - "version": "1.14.24", - "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.14.24.tgz", - "integrity": "sha512-upzw2a9KfzIkIvvjYSPJiyV6o85D3HLmhVvAJIwV8mYWxbvi2wP2NA0hJaMp2+GZVuUl/ra8WV8kacD1CWcb4w==", - "license": "MIT", - "dependencies": { - "@opencode-ai/sdk": "1.14.24", - "effect": "4.0.0-beta.48", - "zod": "4.1.8" - }, - "peerDependencies": { - "@opentui/core": ">=0.1.99", - "@opentui/solid": ">=0.1.99" - }, - "peerDependenciesMeta": { - "@opentui/core": { - "optional": true - }, - "@opentui/solid": { - "optional": true - } - } - }, - "node_modules/@opencode-ai/sdk": { - "version": "1.14.24", - "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.14.24.tgz", - "integrity": "sha512-hZWc1jx+gtZBM6Mff9iOMlXM1at9BbAGg0uNrQk8DuXpd8K19fu942emojdInO2zy0jC5/wWggsi7GJu7HMp/w==", - "license": "MIT", - "dependencies": { - "cross-spawn": "7.0.6" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/effect": { - "version": "4.0.0-beta.48", - "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz", - "integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "fast-check": "^4.6.0", - "find-my-way-ts": "^0.1.6", - "ini": "^6.0.0", - "kubernetes-types": "^1.30.0", - "msgpackr": "^1.11.9", - "multipasta": "^0.2.7", - "toml": "^4.1.1", - "uuid": "^13.0.0", - "yaml": "^2.8.3" - } - }, - "node_modules/fast-check": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz", - "integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT", - "dependencies": { - "pure-rand": "^8.0.0" - }, - "engines": { - "node": ">=12.17.0" - } - }, - "node_modules/find-my-way-ts": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", - "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", - "license": "MIT" - }, - "node_modules/ini": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", - "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/kubernetes-types": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", - "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", - "license": "Apache-2.0" - }, - "node_modules/msgpackr": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.10.tgz", - "integrity": "sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==", - "license": "MIT", - "optionalDependencies": { - "msgpackr-extract": "^3.0.2" - } - }, - "node_modules/msgpackr-extract": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", - "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build-optional-packages": "5.2.2" - }, - "bin": { - "download-msgpackr-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" - } - }, - "node_modules/multipasta": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz", - "integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==", - "license": "MIT" - }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", - "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pure-rand": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", - "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/toml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz", - "integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/zod": { - "version": "4.1.8", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/packages/opencode-config/package.json b/packages/opencode-config/package.json deleted file mode 100644 index b7d127b9b..000000000 --- a/packages/opencode-config/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "@codenomad/opencode-config", - "version": "0.15.0", - "private": true, - "license": "MIT", - "dependencies": { - "@opencode-ai/plugin": "1.3.7" - } -} diff --git a/packages/opencode-config/README.md b/packages/opencode-plugin/README.md similarity index 55% rename from packages/opencode-config/README.md rename to packages/opencode-plugin/README.md index 28e60bd2b..16b8a8192 100644 --- a/packages/opencode-config/README.md +++ b/packages/opencode-plugin/README.md @@ -1,16 +1,16 @@ -# opencode-config +# CodeNomad OpenCode Plugin ## TLDR -Template config + plugins injected into every OpenCode instance that CodeNomad launches. It provides a CodeNomad bridge plugin for local event exchange between the CLI server and opencode. +Packaged OpenCode plugin injected into every OpenCode instance that CodeNomad launches. It provides the CodeNomad bridge for local event exchange between the CLI server and OpenCode. ## What it is -A packaged config directory that CodeNomad copies into `~/.config/codenomad/opencode-config` for production builds or uses directly in dev. OpenCode autoloads any `plugin/*.ts` or `plugin/*.js` from this directory. +An npm-packable plugin package. Production builds ship a local `.tgz` and inject it through `OPENCODE_CONFIG_CONTENT`; dev runs reference the TypeScript plugin entry directly with a `file://` URL. ## How it works -- CodeNomad sets `OPENCODE_CONFIG_DIR` when spawning each opencode instance (`packages/server/src/workspaces/manager.ts`). -- This template is synced from `packages/opencode-config` (`packages/server/src/opencode-config.ts`, `packages/server/scripts/copy-opencode-config.mjs`). -- OpenCode autoloads plugins from `plugin/` (`packages/opencode-config/plugin/codenomad.ts`). -- The `CodeNomadPlugin` reads `CODENOMAD_INSTANCE_ID` + `CODENOMAD_BASE_URL`, connects to `GET /workspaces/:id/plugin/events`, and posts to `POST /workspaces/:id/plugin/event` (`packages/opencode-config/plugin/lib/client.ts`). +- CodeNomad sets `OPENCODE_CONFIG_CONTENT` when spawning each OpenCode instance (`packages/server/src/workspaces/manager.ts`). +- The server packs this package during build (`packages/server/scripts/package-opencode-plugin.mjs`). +- OpenCode loads the plugin from `plugin` entries injected into the config content. +- The `CodeNomadPlugin` reads `CODENOMAD_INSTANCE_ID` + `CODENOMAD_BASE_URL`, connects to `GET /workspaces/:id/plugin/events`, and posts to `POST /workspaces/:id/plugin/event` (`packages/opencode-plugin/plugin/lib/client.ts`). - The server exposes the plugin routes and maps events into the UI SSE pipeline (`packages/server/src/server/routes/plugin.ts`, `packages/server/src/plugins/handlers.ts`). ## Expectations @@ -25,8 +25,8 @@ A packaged config directory that CodeNomad copies into `~/.config/codenomad/open - Promote stable event shapes and version tags once the protocol settles. ## Pointers -- Plugin entry: `packages/opencode-config/plugin/codenomad.ts` -- Plugin client: `packages/opencode-config/plugin/lib/client.ts` +- Plugin entry: `packages/opencode-plugin/plugin/codenomad.ts` +- Plugin client: `packages/opencode-plugin/plugin/lib/client.ts` - Plugin server routes: `packages/server/src/server/routes/plugin.ts` - Plugin event handling: `packages/server/src/plugins/handlers.ts` - Workspace env injection: `packages/server/src/workspaces/manager.ts` diff --git a/packages/opencode-plugin/package.json b/packages/opencode-plugin/package.json new file mode 100644 index 000000000..d6d00d34b --- /dev/null +++ b/packages/opencode-plugin/package.json @@ -0,0 +1,23 @@ +{ + "name": "@codenomad/codenomad-opencode-plugin", + "version": "0.15.0", + "private": true, + "license": "MIT", + "type": "module", + "main": "dist/codenomad.js", + "types": "dist/codenomad.d.ts", + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsc -p tsconfig.json" + }, + "dependencies": { + "@opencode-ai/plugin": "1.3.7" + }, + "devDependencies": { + "@types/node": "^22.18.0", + "typescript": "^5.6.3" + } +} diff --git a/packages/opencode-config/plugin/codenomad.ts b/packages/opencode-plugin/plugin/codenomad.ts similarity index 99% rename from packages/opencode-config/plugin/codenomad.ts rename to packages/opencode-plugin/plugin/codenomad.ts index 08515dd8a..4f4229294 100644 --- a/packages/opencode-config/plugin/codenomad.ts +++ b/packages/opencode-plugin/plugin/codenomad.ts @@ -1,6 +1,6 @@ import type { PluginInput } from "@opencode-ai/plugin" -import { createCodeNomadClient, getCodeNomadConfig } from "./lib/client" -import { createBackgroundProcessTools } from "./lib/background-process" +import { createCodeNomadClient, getCodeNomadConfig } from "./lib/client.js" +import { createBackgroundProcessTools } from "./lib/background-process.js" let voiceModeEnabled = false diff --git a/packages/opencode-config/plugin/lib/background-process.ts b/packages/opencode-plugin/plugin/lib/background-process.ts similarity index 99% rename from packages/opencode-config/plugin/lib/background-process.ts rename to packages/opencode-plugin/plugin/lib/background-process.ts index 15198288a..6840737d6 100644 --- a/packages/opencode-config/plugin/lib/background-process.ts +++ b/packages/opencode-plugin/plugin/lib/background-process.ts @@ -1,6 +1,6 @@ import path from "path" import { tool } from "@opencode-ai/plugin/tool" -import { createCodeNomadRequester, type CodeNomadConfig } from "./request" +import { createCodeNomadRequester, type CodeNomadConfig } from "./request.js" type BackgroundProcess = { id: string diff --git a/packages/opencode-config/plugin/lib/client.ts b/packages/opencode-plugin/plugin/lib/client.ts similarity index 98% rename from packages/opencode-config/plugin/lib/client.ts rename to packages/opencode-plugin/plugin/lib/client.ts index fddc49a4f..aee7a15dc 100644 --- a/packages/opencode-config/plugin/lib/client.ts +++ b/packages/opencode-plugin/plugin/lib/client.ts @@ -1,6 +1,6 @@ -import { createCodeNomadRequester, type CodeNomadConfig, type PluginEvent } from "./request" +import { createCodeNomadRequester, type CodeNomadConfig, type PluginEvent } from "./request.js" -export { getCodeNomadConfig, type CodeNomadConfig, type PluginEvent } from "./request" +export { getCodeNomadConfig, type CodeNomadConfig, type PluginEvent } from "./request.js" export function createCodeNomadClient(config: CodeNomadConfig) { const requester = createCodeNomadRequester(config) diff --git a/packages/opencode-config/plugin/lib/request.ts b/packages/opencode-plugin/plugin/lib/request.ts similarity index 100% rename from packages/opencode-config/plugin/lib/request.ts rename to packages/opencode-plugin/plugin/lib/request.ts diff --git a/packages/opencode-plugin/tsconfig.json b/packages/opencode-plugin/tsconfig.json new file mode 100644 index 000000000..e5bcf42cc --- /dev/null +++ b/packages/opencode-plugin/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "outDir": "dist", + "rootDir": "plugin", + "types": ["node"] + }, + "include": ["plugin/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/server/package.json b/packages/server/package.json index b47bdf292..781c8b837 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -17,10 +17,10 @@ "codenomad": "dist/bin.js" }, "scripts": { - "build": "npm run build:ui && npm run prepare-ui && tsc -p tsconfig.json && node ./scripts/copy-auth-pages.mjs && npm run prepare-config", + "build": "npm run build:ui && npm run prepare-ui && tsc -p tsconfig.json && node ./scripts/copy-auth-pages.mjs && npm run prepare-plugin", "build:ui": "npm run build --prefix ../ui", "prepare-ui": "node ./scripts/copy-ui-dist.mjs", - "prepare-config": "node ./scripts/copy-opencode-config.mjs", + "prepare-plugin": "node ./scripts/package-opencode-plugin.mjs", "dev": "cross-env CODENOMAD_DEV=1 CODENOMAD_SERVER_PASSWORD=codenomad-dev CLI_UI_DEV_SERVER=http://localhost:3000 CLI_HTTPS=false CLI_HTTP=true tsx src/index.ts", "typecheck": "tsc --noEmit -p tsconfig.json" }, diff --git a/packages/server/scripts/copy-opencode-config.mjs b/packages/server/scripts/copy-opencode-config.mjs deleted file mode 100644 index 5e7143956..000000000 --- a/packages/server/scripts/copy-opencode-config.mjs +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env node -import { spawnSync } from "child_process" -import { cpSync, existsSync, mkdirSync, rmSync } from "fs" -import path from "path" -import { fileURLToPath } from "url" - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const cliRoot = path.resolve(__dirname, "..") -const sourceDir = path.resolve(cliRoot, "../opencode-config") -const targetDir = path.resolve(cliRoot, "dist/opencode-config") -const nodeModulesDir = path.resolve(sourceDir, "node_modules") -const selfLinkDir = path.resolve(nodeModulesDir, "@codenomad", "opencode-config") -const npmExecPath = process.env.npm_execpath -const npmNodeExecPath = process.env.npm_node_execpath - -if (!existsSync(sourceDir)) { - console.error(`[copy-opencode-config] Missing source directory at ${sourceDir}`) - process.exit(1) -} - -if (!existsSync(nodeModulesDir)) { - console.log(`[copy-opencode-config] Installing opencode-config dependencies in ${sourceDir}`) - - const npmArgs = [ - "install", - "--prefix", - sourceDir, - "--omit=dev", - "--ignore-scripts", - "--fund=false", - "--audit=false", - "--package-lock=false", - "--workspaces=false", - ] - - const env = { ...process.env, npm_config_workspaces: "false" } - - const npmCli = npmExecPath && npmNodeExecPath ? [npmNodeExecPath, [npmExecPath, ...npmArgs]] : null - const result = npmCli - ? spawnSync(npmCli[0], npmCli[1], { cwd: sourceDir, stdio: "inherit", env }) - : spawnSync("npm", npmArgs, { cwd: sourceDir, stdio: "inherit", env, shell: process.platform === "win32" }) - - if (result.status !== 0) { - if (result.error) { - console.error("[copy-opencode-config] npm install failed to start", result.error) - } - console.error("[copy-opencode-config] Failed to install opencode-config dependencies") - process.exit(result.status ?? 1) - } -} - -// npm can create a self-referential link for scoped packages on Windows. -// That link causes recursive copies (ELOOP) during bundling. -rmSync(selfLinkDir, { recursive: true, force: true }) - -rmSync(targetDir, { recursive: true, force: true }) -mkdirSync(path.dirname(targetDir), { recursive: true }) -cpSync(sourceDir, targetDir, { recursive: true }) - -console.log(`[copy-opencode-config] Copied ${sourceDir} -> ${targetDir}`) diff --git a/packages/server/scripts/package-opencode-plugin.mjs b/packages/server/scripts/package-opencode-plugin.mjs new file mode 100644 index 000000000..319905476 --- /dev/null +++ b/packages/server/scripts/package-opencode-plugin.mjs @@ -0,0 +1,59 @@ +#!/usr/bin/env node +import { readdirSync, renameSync, rmSync, mkdirSync } from "fs" +import path from "path" +import { spawnSync } from "child_process" +import { fileURLToPath } from "url" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const serverRoot = path.resolve(__dirname, "..") +const workspaceRoot = path.resolve(serverRoot, "../..") +const pluginRoot = path.resolve(serverRoot, "../opencode-plugin") +const targetDir = path.resolve(serverRoot, "dist/opencode-plugin") +const targetTarballName = "codenomad-opencode-plugin.tgz" +const pluginWorkspace = "@codenomad/codenomad-opencode-plugin" +const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm" + +function run(command, args, options) { + const result = spawnSync(command, args, { + stdio: options?.capture ? ["ignore", "pipe", "inherit"] : "inherit", + shell: process.platform === "win32", + encoding: "utf8", + ...options, + }) + + if (result.error) { + console.error(`[package-opencode-plugin] ${command} failed to start`, result.error) + process.exit(1) + } + + if (result.status !== 0) { + console.error(`[package-opencode-plugin] ${command} exited with code ${result.status ?? 1}`) + process.exit(result.status ?? 1) + } + + return result.stdout ?? "" +} + +rmSync(targetDir, { recursive: true, force: true }) +mkdirSync(targetDir, { recursive: true }) + +console.log(`[package-opencode-plugin] Building ${pluginWorkspace}`) +run(npmCommand, ["run", "build", "--workspace", pluginWorkspace], { cwd: workspaceRoot }) + +console.log(`[package-opencode-plugin] Packing ${pluginWorkspace}`) +run(npmCommand, ["pack", "--pack-destination", targetDir], { cwd: pluginRoot, capture: true }) + +const tarballs = readdirSync(targetDir).filter((name) => name.endsWith(".tgz")) +if (tarballs.length !== 1) { + console.error(`[package-opencode-plugin] Expected exactly one packed plugin tarball in ${targetDir}, found ${tarballs.length}`) + process.exit(1) +} + +const packedTarball = path.join(targetDir, tarballs[0]) +const targetTarball = path.join(targetDir, targetTarballName) +if (packedTarball !== targetTarball) { + renameSync(packedTarball, targetTarball) +} + +console.log(`[package-opencode-plugin] Packed ${targetTarball}`) diff --git a/packages/server/src/opencode-config.ts b/packages/server/src/opencode-config.ts deleted file mode 100644 index f61cb9ca6..000000000 --- a/packages/server/src/opencode-config.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { existsSync } from "fs" -import path from "path" -import { fileURLToPath } from "url" -import { createLogger } from "./logger" - -const log = createLogger({ component: "opencode-config" }) -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const devTemplateDir = path.resolve(__dirname, "../../opencode-config") -const resourcesPath = (process as NodeJS.Process & { resourcesPath?: string }).resourcesPath -const prodTemplateDirs = [ - resourcesPath ? path.resolve(resourcesPath, "opencode-config") : undefined, - path.resolve(__dirname, "opencode-config"), -].filter((dir): dir is string => Boolean(dir)) - -const isDevBuild = Boolean(process.env.CODENOMAD_DEV ?? process.env.CLI_UI_DEV_SERVER) || existsSync(devTemplateDir) -const templateDir = isDevBuild - ? devTemplateDir - : prodTemplateDirs.find((dir) => existsSync(dir)) ?? prodTemplateDirs[0] - -export function getOpencodeConfigDir(): string { - if (!existsSync(templateDir)) { - throw new Error(`CodeNomad Opencode config template missing at ${templateDir}`) - } - - if (isDevBuild) { - log.debug({ templateDir }, "Using Opencode config template directly (dev mode)") - } - - return templateDir -} diff --git a/packages/server/src/opencode-plugin.test.ts b/packages/server/src/opencode-plugin.test.ts new file mode 100644 index 000000000..dda5f9881 --- /dev/null +++ b/packages/server/src/opencode-plugin.test.ts @@ -0,0 +1,38 @@ +import assert from "node:assert/strict" +import { describe, it } from "node:test" + +import { buildOpencodeConfigContent } from "./opencode-plugin" + +describe("buildOpencodeConfigContent", () => { + it("creates config content with the CodeNomad plugin", () => { + const content = buildOpencodeConfigContent(undefined, "file:///plugin.tgz") + + assert.deepEqual(JSON.parse(content), { + "$schema": "https://opencode.ai/config.json", + plugin: ["file:///plugin.tgz"], + }) + }) + + it("merges with existing JSONC content", () => { + const content = buildOpencodeConfigContent( + `{ + // user plugin + "plugin": ["npm:user-plugin",], + "model": "test-model", + }`, + "file:///plugin.tgz", + ) + + assert.deepEqual(JSON.parse(content), { + "$schema": "https://opencode.ai/config.json", + plugin: ["npm:user-plugin", "file:///plugin.tgz"], + model: "test-model", + }) + }) + + it("does not duplicate the CodeNomad plugin", () => { + const content = buildOpencodeConfigContent('{"plugin":["file:///plugin.tgz"]}', "file:///plugin.tgz") + + assert.deepEqual(JSON.parse(content).plugin, ["file:///plugin.tgz"]) + }) +}) diff --git a/packages/server/src/opencode-plugin.ts b/packages/server/src/opencode-plugin.ts new file mode 100644 index 000000000..761292da5 --- /dev/null +++ b/packages/server/src/opencode-plugin.ts @@ -0,0 +1,175 @@ +import { existsSync, readdirSync } from "fs" +import path from "path" +import { fileURLToPath, pathToFileURL } from "url" +import { createLogger } from "./logger" + +const log = createLogger({ component: "opencode-plugin" }) +const pluginPackageName = "@codenomad/codenomad-opencode-plugin" +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const resourcesPath = (process as NodeJS.Process & { resourcesPath?: string }).resourcesPath +const devPluginEntry = path.resolve(__dirname, "../../opencode-plugin/plugin/codenomad.ts") +const prodPluginDirs = [ + resourcesPath ? path.resolve(resourcesPath, "opencode-plugin") : undefined, + resourcesPath ? path.resolve(resourcesPath, "server/dist/opencode-plugin") : undefined, + path.resolve(__dirname, "opencode-plugin"), +].filter((dir): dir is string => Boolean(dir)) + +const isDevBuild = Boolean( + process.env.CODENOMAD_DEV ?? + process.env.CLI_UI_DEV_SERVER ?? + process.env.VITE_DEV_SERVER_URL ?? + process.env.ELECTRON_RENDERER_URL, +) +const isSourceRun = path.basename(__dirname) === "src" && existsSync(devPluginEntry) + +export function getCodeNomadPluginUrl(): string { + if (isDevBuild || isSourceRun) { + if (!existsSync(devPluginEntry)) { + throw new Error(`CodeNomad OpenCode plugin entry missing at ${devPluginEntry}`) + } + + log.debug({ pluginEntry: devPluginEntry }, "Using OpenCode plugin source directly (dev mode)") + return pathToFileURL(devPluginEntry).href + } + + for (const dir of prodPluginDirs) { + const tarball = findPluginTarball(dir) + if (tarball) { + return toNpmFileSpecifier(tarball) + } + } + + throw new Error(`CodeNomad OpenCode plugin package missing in ${prodPluginDirs.join(", ")}`) +} + +export function buildOpencodeConfigContent(existingContent: string | undefined, pluginUrl: string): string { + const config = existingContent?.trim() ? parseJsoncObject(existingContent) : {} + const existingPlugins = normalizePluginEntries(config.plugin) + if (!existingPlugins.includes(pluginUrl)) { + existingPlugins.push(pluginUrl) + } + return JSON.stringify( + { + "$schema": typeof config["$schema"] === "string" ? config["$schema"] : "https://opencode.ai/config.json", + ...config, + plugin: existingPlugins, + }, + null, + 2, + ) +} + +export function resolveExistingOpencodeConfigContent(userEnvironment: Record): string | undefined { + const userValue = normalizeConfigContentValue(userEnvironment.OPENCODE_CONFIG_CONTENT) + if (userValue) { + return userValue + } + return normalizeConfigContentValue(process.env.OPENCODE_CONFIG_CONTENT) +} + +function toNpmFileSpecifier(filePath: string): string { + return `${pluginPackageName}@file:${filePath.replace(/\\/g, "/")}` +} + +function findPluginTarball(dir: string): string | null { + if (!existsSync(dir)) { + return null + } + + const tarballs = readdirSync(dir) + .filter((name) => name.endsWith(".tgz")) + .sort() + return tarballs.length > 0 ? path.resolve(dir, tarballs[tarballs.length - 1]) : null +} + +function normalizeConfigContentValue(value: unknown): string | undefined { + return typeof value === "string" && value.trim().length > 0 ? value : undefined +} + +function parseJsoncObject(content: string): Record { + try { + const parsed = JSON.parse(stripJsonc(content)) + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("OPENCODE_CONFIG_CONTENT must be a JSON object") + } + return parsed as Record + } catch (error) { + const reason = error instanceof Error ? error.message : String(error) + throw new Error(`Failed to parse OPENCODE_CONFIG_CONTENT: ${reason}`) + } +} + +function normalizePluginEntries(value: unknown): string[] { + if (value === undefined) { + return [] + } + if (typeof value === "string") { + return [value] + } + if (Array.isArray(value) && value.every((item) => typeof item === "string")) { + return [...value] + } + throw new Error("OPENCODE_CONFIG_CONTENT plugin field must be a string or string array") +} + +function stripJsonc(input: string): string { + let output = "" + let inString = false + let escape = false + + for (let index = 0; index < input.length; index += 1) { + const char = input[index] + const next = input[index + 1] + + if (escape) { + output += char + escape = false + continue + } + + if (char === "\\" && inString) { + output += char + escape = true + continue + } + + if (char === '"') { + output += char + inString = !inString + continue + } + + if (!inString && char === "/" && next === "/") { + while (index < input.length && input[index] !== "\n") { + index += 1 + } + output += "\n" + continue + } + + if (!inString && char === "/" && next === "*") { + index += 2 + while (index < input.length && !(input[index] === "*" && input[index + 1] === "/")) { + output += input[index] === "\n" ? "\n" : "" + index += 1 + } + index += 1 + continue + } + + if (!inString && char === ",") { + let lookahead = index + 1 + while (lookahead < input.length && /\s/.test(input[lookahead])) { + lookahead += 1 + } + if (input[lookahead] === "}" || input[lookahead] === "]") { + continue + } + } + + output += char + } + + return output +} diff --git a/packages/server/src/workspaces/__tests__/spawn.test.ts b/packages/server/src/workspaces/__tests__/spawn.test.ts index b33aec6a8..0369ae1ff 100644 --- a/packages/server/src/workspaces/__tests__/spawn.test.ts +++ b/packages/server/src/workspaces/__tests__/spawn.test.ts @@ -54,12 +54,12 @@ describe("buildWindowsSpawnSpec", () => { { cwd: String.raw`\\wsl.localhost\Ubuntu\home\dev\workspace`, env: { - OPENCODE_CONFIG_DIR: String.raw`C:\Users\dev\AppData\Roaming\CodeNomad\opencode-config`, + OPENCODE_CONFIG_CONTENT: JSON.stringify({ plugin: ["file:///C:/Users/dev/AppData/Roaming/CodeNomad/plugin.tgz"] }), CODENOMAD_INSTANCE_ID: "workspace-123", OPENCODE_SERVER_BASE_URL: "https://127.0.0.1:4321/workspaces/workspace-123/worktrees/root/instance", OPENCODE_SERVER_PASSWORD: "secret", }, - propagateEnvKeys: ["OPENCODE_CONFIG_DIR", "CODENOMAD_INSTANCE_ID", "OPENCODE_SERVER_BASE_URL", "OPENCODE_SERVER_PASSWORD"], + propagateEnvKeys: ["OPENCODE_CONFIG_CONTENT", "CODENOMAD_INSTANCE_ID", "OPENCODE_SERVER_BASE_URL", "OPENCODE_SERVER_PASSWORD"], }, ) @@ -76,23 +76,23 @@ describe("buildWindowsSpawnSpec", () => { "0", ]) assert.equal(spec.cwd, undefined) - assert.equal(spec.env?.WSLENV, "OPENCODE_CONFIG_DIR/p:CODENOMAD_INSTANCE_ID:OPENCODE_SERVER_BASE_URL:OPENCODE_SERVER_PASSWORD") + assert.equal(spec.env?.WSLENV, "OPENCODE_CONFIG_CONTENT:CODENOMAD_INSTANCE_ID:OPENCODE_SERVER_BASE_URL:OPENCODE_SERVER_PASSWORD") }) - it("upgrades existing WSLENV path entries to include /p", () => { + it("preserves non-path OPENCODE_CONFIG_CONTENT WSLENV entries", () => { const spec = buildWindowsSpawnSpec( String.raw`\\wsl.localhost\Ubuntu\home\dev\.opencode\bin\opencode`, ["serve"], { env: { - OPENCODE_CONFIG_DIR: String.raw`C:\Users\dev\AppData\Roaming\CodeNomad\opencode-config`, - WSLENV: "OPENCODE_CONFIG_DIR:CODENOMAD_INSTANCE_ID/u", + OPENCODE_CONFIG_CONTENT: JSON.stringify({ plugin: ["file:///C:/Users/dev/AppData/Roaming/CodeNomad/plugin.tgz"] }), + WSLENV: "OPENCODE_CONFIG_CONTENT:CODENOMAD_INSTANCE_ID/u", }, - propagateEnvKeys: ["OPENCODE_CONFIG_DIR", "CODENOMAD_INSTANCE_ID"], + propagateEnvKeys: ["OPENCODE_CONFIG_CONTENT", "CODENOMAD_INSTANCE_ID"], }, ) - assert.equal(spec.env?.WSLENV, "OPENCODE_CONFIG_DIR/p:CODENOMAD_INSTANCE_ID/u") + assert.equal(spec.env?.WSLENV, "OPENCODE_CONFIG_CONTENT:CODENOMAD_INSTANCE_ID/u") }) it("propagates inherited known path variables even when they are not explicitly requested", () => { diff --git a/packages/server/src/workspaces/manager.ts b/packages/server/src/workspaces/manager.ts index 063c2cbb9..0cae7fde2 100644 --- a/packages/server/src/workspaces/manager.ts +++ b/packages/server/src/workspaces/manager.ts @@ -10,7 +10,11 @@ import { clearWorkspaceSearchCache } from "../filesystem/search-cache" import { WorkspaceDescriptor, WorkspaceFileResponse, FileSystemEntry } from "../api-types" import { WorkspaceRuntime, ProcessExitInfo } from "./runtime" import { Logger } from "../logger" -import { getOpencodeConfigDir } from "../opencode-config.js" +import { + buildOpencodeConfigContent, + getCodeNomadPluginUrl, + resolveExistingOpencodeConfigContent, +} from "../opencode-plugin.js" import { OPENCODE_SERVER_BASE_URL_ENV, buildOpencodeBasicAuthHeader, @@ -37,12 +41,12 @@ interface WorkspaceRecord extends WorkspaceDescriptor {} export class WorkspaceManager { private readonly workspaces = new Map() private readonly runtime: WorkspaceRuntime - private readonly opencodeConfigDir: string + private readonly codeNomadPluginUrl: string private readonly opencodeAuth = new Map() constructor(private readonly options: WorkspaceManagerOptions) { this.runtime = new WorkspaceRuntime(this.options.eventBus, this.options.logger) - this.opencodeConfigDir = getOpencodeConfigDir() + this.codeNomadPluginUrl = getCodeNomadPluginUrl() } list(): WorkspaceDescriptor[] { @@ -123,6 +127,10 @@ export class WorkspaceManager { const serverConfig = this.options.settings.getOwner("config", "server") const envVars = (serverConfig as any)?.environmentVariables const userEnvironment = envVars && typeof envVars === "object" && !Array.isArray(envVars) ? (envVars as any) : {} + const opencodeConfigContent = buildOpencodeConfigContent( + resolveExistingOpencodeConfigContent(userEnvironment), + this.codeNomadPluginUrl, + ) const serverBaseUrl = this.options.getServerBaseUrl() const normalizedServerBaseUrl = serverBaseUrl.replace(/\/+$/, "") @@ -138,7 +146,7 @@ export class WorkspaceManager { const environment = { ...userEnvironment, - OPENCODE_CONFIG_DIR: this.opencodeConfigDir, + OPENCODE_CONFIG_CONTENT: opencodeConfigContent, CODENOMAD_INSTANCE_ID: id, CODENOMAD_BASE_URL: serverBaseUrl, ...(this.options.nodeExtraCaCertsPath ? { NODE_EXTRA_CA_CERTS: this.options.nodeExtraCaCertsPath } : {}), diff --git a/packages/server/src/workspaces/spawn.ts b/packages/server/src/workspaces/spawn.ts index 8add4aa48..8eec91292 100644 --- a/packages/server/src/workspaces/spawn.ts +++ b/packages/server/src/workspaces/spawn.ts @@ -6,7 +6,7 @@ export const WINDOWS_POWERSHELL_EXTENSIONS = new Set([".ps1"]) const VERSION_REGEX = /([0-9]+\.[0-9]+\.[0-9A-Za-z.-]+)/ const WSL_UNC_PATH_REGEX = /^\\\\wsl(?:\.localhost|\$)\\([^\\/]+)(?:[\\/](.*))?$/i -const WSL_PATH_ENV_KEYS = new Set(["OPENCODE_CONFIG_DIR", "NODE_EXTRA_CA_CERTS"]) +const WSL_PATH_ENV_KEYS = new Set(["NODE_EXTRA_CA_CERTS"]) export interface SpawnSpec { command: string From 7460f9c2698a4fae7a20b9fad63eddab20cabfc5 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Mon, 11 May 2026 16:19:05 +0100 Subject: [PATCH 2/4] fix(opencode): harden plugin packaging paths Remove the accidental darwin-arm64 Tauri CLI root dependency so installs remain cross-platform. For WSL OpenCode launches, rewrite packaged CodeNomad plugin file specs through a WSLENV-translated helper path before exec so the Linux process receives a reachable file path inside OPENCODE_CONFIG_CONTENT. Ensure Tauri prebuild installs the OpenCode plugin workspace build dependencies before invoking the server build, since server packaging now builds the plugin package. Validated with plugin build, server typecheck, targeted OpenCode plugin config and WSL spawn tests, and diff whitespace checks. --- package-lock.json | 16 ----- package.json | 1 - .../src/workspaces/__tests__/spawn.test.ts | 24 ++++++++ packages/server/src/workspaces/spawn.ts | 58 ++++++++++++++++--- packages/tauri-app/scripts/prebuild.js | 22 +++++++ 5 files changed, 95 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 86db7d010..b2fb90cef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.15.0", "license": "MIT", "dependencies": { - "@tauri-apps/cli-darwin-arm64": "^2.11.1", "7zip-bin": "^5.2.0", "google-auth-library": "^10.5.0" }, @@ -4036,21 +4035,6 @@ "@tauri-apps/cli-win32-x64-msvc": "2.9.4" } }, - "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.1.tgz", - "integrity": "sha512-6eEKMBXsQPCuM1EmvrjT2+aBuxWQuFdKdW8pzNuNQtpq45nEEpBlD5gr8pUeAyOU1DQKlkFaEc/MPBxb/Pfjtg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 OR MIT", - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@tauri-apps/cli-darwin-x64": { "version": "2.9.4", "cpu": [ diff --git a/package.json b/package.json index 32907a188..9035ba6c2 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "bumpVersion": "node ./scripts/bump-version.js" }, "dependencies": { - "@tauri-apps/cli-darwin-arm64": "^2.11.1", "7zip-bin": "^5.2.0", "google-auth-library": "^10.5.0" }, diff --git a/packages/server/src/workspaces/__tests__/spawn.test.ts b/packages/server/src/workspaces/__tests__/spawn.test.ts index 0369ae1ff..7b829ac75 100644 --- a/packages/server/src/workspaces/__tests__/spawn.test.ts +++ b/packages/server/src/workspaces/__tests__/spawn.test.ts @@ -95,6 +95,30 @@ describe("buildWindowsSpawnSpec", () => { assert.equal(spec.env?.WSLENV, "OPENCODE_CONFIG_CONTENT:CODENOMAD_INSTANCE_ID/u") }) + it("rewrites packaged plugin paths for WSL before launching", () => { + const spec = buildWindowsSpawnSpec( + String.raw`\\wsl.localhost\Ubuntu\home\dev\.opencode\bin\opencode`, + ["serve"], + { + env: { + OPENCODE_CONFIG_CONTENT: JSON.stringify({ + plugin: [ + "@codenomad/codenomad-opencode-plugin@file:C:/Users/dev/AppData/Roaming/CodeNomad/codenomad-opencode-plugin.tgz", + ], + }), + }, + propagateEnvKeys: ["OPENCODE_CONFIG_CONTENT"], + }, + ) + + assert.equal(spec.command, "wsl.exe") + assert.equal(spec.env?.CODENOMAD_OPENCODE_PLUGIN_WSL_PATH, String.raw`C:\Users\dev\AppData\Roaming\CodeNomad\codenomad-opencode-plugin.tgz`) + assert.match(spec.env?.OPENCODE_CONFIG_CONTENT ?? "", /__CODENOMAD_OPENCODE_PLUGIN_WSL_PATH__/) + assert.equal(spec.env?.WSLENV, "OPENCODE_CONFIG_CONTENT:CODENOMAD_OPENCODE_PLUGIN_WSL_PATH/p") + assert.deepEqual(spec.args.slice(0, 4), ["--distribution", "Ubuntu", "--exec", "sh"]) + assert.match(spec.args[5] ?? "", /CODENOMAD_OPENCODE_PLUGIN_WSL_PATH/) + }) + it("propagates inherited known path variables even when they are not explicitly requested", () => { const spec = buildWindowsSpawnSpec( String.raw`\\wsl.localhost\Ubuntu\home\dev\.opencode\bin\opencode`, diff --git a/packages/server/src/workspaces/spawn.ts b/packages/server/src/workspaces/spawn.ts index 8eec91292..f40dcdb02 100644 --- a/packages/server/src/workspaces/spawn.ts +++ b/packages/server/src/workspaces/spawn.ts @@ -6,7 +6,13 @@ export const WINDOWS_POWERSHELL_EXTENSIONS = new Set([".ps1"]) const VERSION_REGEX = /([0-9]+\.[0-9]+\.[0-9A-Za-z.-]+)/ const WSL_UNC_PATH_REGEX = /^\\\\wsl(?:\.localhost|\$)\\([^\\/]+)(?:[\\/](.*))?$/i -const WSL_PATH_ENV_KEYS = new Set(["NODE_EXTRA_CA_CERTS"]) +const CODENOMAD_PLUGIN_PACKAGE_NAME = "@codenomad/codenomad-opencode-plugin" +const WSL_PLUGIN_PATH_ENV = "CODENOMAD_OPENCODE_PLUGIN_WSL_PATH" +const WSL_PLUGIN_PATH_PLACEHOLDER = "__CODENOMAD_OPENCODE_PLUGIN_WSL_PATH__" +const CODENOMAD_PLUGIN_FILE_SPEC_REGEX = new RegExp( + `(${escapeRegex(CODENOMAD_PLUGIN_PACKAGE_NAME)}@file:)([A-Za-z]:[^"\\r\\n]+?\\.tgz)`, +) +const WSL_PATH_ENV_KEYS = new Set(["NODE_EXTRA_CA_CERTS", WSL_PLUGIN_PATH_ENV]) export interface SpawnSpec { command: string @@ -187,6 +193,8 @@ export function probeBinaryVersion(binaryPath: string): { function buildWslSpawnSpec(wslPath: WslPath, args: string[], options: BuildSpawnSpecOptions): SpawnSpec { const workingDirectory = options.cwd ? resolveWslWorkingDirectory(options.cwd, wslPath.distro) : undefined + const env = buildWslEnvironment(options.env, options.propagateEnvKeys) + const shouldTranslatePluginPath = Boolean(env?.[WSL_PLUGIN_PATH_ENV]) if (options.cwd && !workingDirectory) { throw new Error( `Unable to translate workspace folder for WSL binary in distro "${wslPath.distro}": ${options.cwd}`, @@ -194,14 +202,14 @@ function buildWslSpawnSpec(wslPath: WslPath, args: string[], options: BuildSpawn } const wslArgs = ["--distribution", wslPath.distro] - const shouldWrapWithShell = Boolean(options.wslPidMarker) || workingDirectory?.kind === "windows" + const shouldWrapWithShell = Boolean(options.wslPidMarker) || workingDirectory?.kind === "windows" || shouldTranslatePluginPath if (!shouldWrapWithShell && workingDirectory?.kind === "linux") { wslArgs.push("--cd", workingDirectory.path) } if (shouldWrapWithShell) { - const launchScript = buildWslLaunchScript(workingDirectory ?? undefined, options.wslPidMarker) + const launchScript = buildWslLaunchScript(workingDirectory ?? undefined, options.wslPidMarker, shouldTranslatePluginPath) wslArgs.push( "--exec", "sh", @@ -224,12 +232,16 @@ function buildWslSpawnSpec(wslPath: WslPath, args: string[], options: BuildSpawn command: "wsl.exe", args: wslArgs, options: {}, - env: buildWslEnvironment(options.env, options.propagateEnvKeys), + env, wsl: { distro: wslPath.distro, pidMarker: options.wslPidMarker }, } } -function buildWslLaunchScript(workingDirectory: WslWorkingDirectory | undefined, pidMarker: string | undefined): string { +function buildWslLaunchScript( + workingDirectory: WslWorkingDirectory | undefined, + pidMarker: string | undefined, + translatePluginPath: boolean, +): string { const steps: string[] = [] if (pidMarker) { @@ -244,6 +256,12 @@ function buildWslLaunchScript(workingDirectory: WslWorkingDirectory | undefined, steps.push("shift") } + if (translatePluginPath) { + steps.push( + `if [ -n "$${WSL_PLUGIN_PATH_ENV}" ] && [ -n "$OPENCODE_CONFIG_CONTENT" ]; then escaped_plugin_path=$(printf '%s' "$${WSL_PLUGIN_PATH_ENV}" | sed 's/[\\&|]/\\\\&/g'); OPENCODE_CONFIG_CONTENT=$(printf '%s' "$OPENCODE_CONFIG_CONTENT" | sed "s|${WSL_PLUGIN_PATH_PLACEHOLDER}|$escaped_plugin_path|g"); export OPENCODE_CONFIG_CONTENT; unset ${WSL_PLUGIN_PATH_ENV}; fi`, + ) + } + steps.push('exec "$@"') return steps.join(" && ") } @@ -266,17 +284,19 @@ function buildWslEnvironment(env: NodeJS.ProcessEnv | undefined, propagateEnvKey return env } + const next = { ...env } + rewriteOpencodePluginPathForWsl(next) + const keysToPropagate = Array.from( new Set([ - ...(propagateEnvKeys ?? []).filter((key) => env[key] !== undefined), - ...Array.from(WSL_PATH_ENV_KEYS).filter((key) => env[key] !== undefined), + ...(propagateEnvKeys ?? []).filter((key) => next[key] !== undefined), + ...Array.from(WSL_PATH_ENV_KEYS).filter((key) => next[key] !== undefined), ]), ) if (keysToPropagate.length === 0) { - return env + return next } - const next = { ...env } const entries = (next.WSLENV ?? "").split(":").filter((entry) => entry.length > 0) const byName = new Map(entries.map((entry) => [entry.split("/")[0] ?? entry, entry])) @@ -293,6 +313,22 @@ function buildWslEnvironment(env: NodeJS.ProcessEnv | undefined, propagateEnvKey return next } +function rewriteOpencodePluginPathForWsl(env: NodeJS.ProcessEnv) { + const content = env.OPENCODE_CONFIG_CONTENT + if (!content) { + return + } + + const match = content.match(CODENOMAD_PLUGIN_FILE_SPEC_REGEX) + const hostPath = match?.[2] + if (!hostPath) { + return + } + + env.OPENCODE_CONFIG_CONTENT = content.replace(hostPath, WSL_PLUGIN_PATH_PLACEHOLDER) + env[WSL_PLUGIN_PATH_ENV] = path.win32.normalize(hostPath) +} + function ensureWslenvEntry(entry: string, requiresPathTranslation: boolean): string { if (!requiresPathTranslation) { return entry @@ -305,3 +341,7 @@ function ensureWslenvEntry(entry: string, requiresPathTranslation: boolean): str return rawFlags.length > 0 ? `${name}/${rawFlags}p` : `${name}/p` } + +function escapeRegex(input: string): string { + return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") +} diff --git a/packages/tauri-app/scripts/prebuild.js b/packages/tauri-app/scripts/prebuild.js index ba2a119c0..62c3daaea 100644 --- a/packages/tauri-app/scripts/prebuild.js +++ b/packages/tauri-app/scripts/prebuild.js @@ -20,6 +20,8 @@ const serverInstallCommand = "npm install --omit=dev --ignore-scripts --workspaces=false --package-lock=false --install-strategy=shallow --fund=false --audit=false" const serverDevInstallCommand = "npm install --workspace @neuralnomads/codenomad --include-workspace-root=false --install-strategy=nested --fund=false --audit=false" +const pluginDevInstallCommand = + "npm install --workspace @codenomad/codenomad-opencode-plugin --include-workspace-root=false --install-strategy=nested --fund=false --audit=false" const uiDevInstallCommand = "npm install --workspace @codenomad/ui --include-workspace-root=false --install-strategy=nested --fund=false --audit=false" const serverPrepareUiCommand = "npm run prepare-ui --workspace @neuralnomads/codenomad" @@ -45,6 +47,12 @@ const serverBuildDependencyPaths = [ path.join(serverRoot, "node_modules", "@types", "yauzl", "package.json"), ] +const pluginRoot = path.resolve(root, "..", "opencode-plugin") +const pluginBuildDependencyPaths = [ + path.join(pluginRoot, "node_modules", "typescript", "package.json"), + path.join(pluginRoot, "node_modules", "@types", "node", "package.json"), +] + const viteBinPath = path.join(uiRoot, "node_modules", ".bin", "vite") async function ensureMonacoAssets() { @@ -118,6 +126,19 @@ function ensureServerDevDependencies() { }) } +function ensurePluginDevDependencies() { + if (pluginBuildDependencyPaths.every((filePath) => fs.existsSync(filePath))) { + return + } + + console.log("[prebuild] ensuring OpenCode plugin build dependencies...") + execSync(pluginDevInstallCommand, { + cwd: workspaceRoot, + stdio: "inherit", + env: envWithRootBin, + }) +} + function ensureServerDependencies() { if (fs.existsSync(braceExpansionPath)) { return @@ -302,6 +323,7 @@ function copyUiLoadingAssets() { ;(async () => { ensureServerDevDependencies() + ensurePluginDevDependencies() ensureUiDevDependencies() await ensureMonacoAssets() ensureRollupPlatformBinary() From 6f68526e6ac3e5787255d0aa56f6754182a30bd4 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Mon, 11 May 2026 16:23:42 +0100 Subject: [PATCH 3/4] fix(opencode): avoid plugin declaration portability errors Disable declaration output for the packaged OpenCode plugin and clean dist before builds so stale .d.ts files are not packed. The plugin is consumed as runtime JavaScript by OpenCode, and declaration generation can infer non-portable nested zod paths from @opencode-ai/plugin. Add explicit hook return annotations on the plugin entry to keep exported runtime shapes clear while avoiding declaration generation in CI builds. Validated with plugin build, server prepare-plugin packaging, server typecheck, and targeted plugin/WSL tests. --- packages/opencode-plugin/package.json | 3 +-- packages/opencode-plugin/plugin/codenomad.ts | 13 ++++++++++++- packages/opencode-plugin/tsconfig.json | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/opencode-plugin/package.json b/packages/opencode-plugin/package.json index d6d00d34b..fe7e16c31 100644 --- a/packages/opencode-plugin/package.json +++ b/packages/opencode-plugin/package.json @@ -5,13 +5,12 @@ "license": "MIT", "type": "module", "main": "dist/codenomad.js", - "types": "dist/codenomad.d.ts", "files": [ "dist", "README.md" ], "scripts": { - "build": "tsc -p tsconfig.json" + "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json" }, "dependencies": { "@opencode-ai/plugin": "1.3.7" diff --git a/packages/opencode-plugin/plugin/codenomad.ts b/packages/opencode-plugin/plugin/codenomad.ts index 4f4229294..61d1827f0 100644 --- a/packages/opencode-plugin/plugin/codenomad.ts +++ b/packages/opencode-plugin/plugin/codenomad.ts @@ -4,7 +4,11 @@ import { createBackgroundProcessTools } from "./lib/background-process.js" let voiceModeEnabled = false -export async function CodeNomadPlugin(input: PluginInput) { +export async function CodeNomadPlugin(input: PluginInput): Promise<{ + tool: ReturnType + "chat.message": CodeNomadChatMessageHook + event: CodeNomadEventHook +}> { const config = getCodeNomadConfig() const client = createCodeNomadClient(config) const backgroundProcessTools = createBackgroundProcessTools(config, { baseDir: input.directory }) @@ -45,6 +49,13 @@ export async function CodeNomadPlugin(input: PluginInput) { } } +type CodeNomadChatMessageHook = ( + _input: { sessionID: string }, + output: { message: { system?: string } }, +) => Promise + +type CodeNomadEventHook = (input: { event: any }) => Promise + function buildVoiceModePrompt(): string { return [ "Voice conversation mode is enabled.", diff --git a/packages/opencode-plugin/tsconfig.json b/packages/opencode-plugin/tsconfig.json index e5bcf42cc..09a866276 100644 --- a/packages/opencode-plugin/tsconfig.json +++ b/packages/opencode-plugin/tsconfig.json @@ -7,7 +7,7 @@ "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "declaration": true, + "declaration": false, "outDir": "dist", "rootDir": "plugin", "types": ["node"] From 8be3433402e0e889a01da2523e6ba9bf8884e7a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Tue, 12 May 2026 08:58:32 +0200 Subject: [PATCH 4/4] fix(electron): avoid shell for resource prep (#436) ## Summary - Avoid routing the Electron resource-prep Node invocation through the Windows shell. - Fix local `build:win` when Node is installed under `C:\Program Files\nodejs\node.exe`. ## Validation - `npm run build:win --workspace @neuralnomads/codenomad-electron-app` ## Notes - This targets `opencode-config-merge` so it can be merged into #433. --- packages/electron-app/scripts/build.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/electron-app/scripts/build.js b/packages/electron-app/scripts/build.js index 1621ce1ac..f308a2ad5 100644 --- a/packages/electron-app/scripts/build.js +++ b/packages/electron-app/scripts/build.js @@ -137,6 +137,7 @@ async function build(platform) { console.log(`\n📦 Preparing resources for ${job.nodeTarget}...\n`) await run(process.execPath, [join(appDir, "scripts", "prepare-resources.js")], { cwd: workspaceRoot, + shell: false, env: { NODE_PATH: workspaceNodeModulesPath, CODENOMAD_NODE_TARGET: job.nodeTarget }, })