diff --git a/package-lock.json b/package-lock.json index 83d6dd284..b2fb90cef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "packages/ui", "packages/electron-app", "packages/tauri-app", - "packages/opencode-config" + "packages/opencode-plugin" ] } }, @@ -1575,8 +1575,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 +3228,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", @@ -4013,6 +4050,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 +4218,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 +13448,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..9035ba6c2 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": { 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/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 }, }) 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..fe7e16c31 --- /dev/null +++ b/packages/opencode-plugin/package.json @@ -0,0 +1,22 @@ +{ + "name": "@codenomad/codenomad-opencode-plugin", + "version": "0.15.0", + "private": true, + "license": "MIT", + "type": "module", + "main": "dist/codenomad.js", + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && 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 86% rename from packages/opencode-config/plugin/codenomad.ts rename to packages/opencode-plugin/plugin/codenomad.ts index 08515dd8a..61d1827f0 100644 --- a/packages/opencode-config/plugin/codenomad.ts +++ b/packages/opencode-plugin/plugin/codenomad.ts @@ -1,10 +1,14 @@ 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 -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-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..09a866276 --- /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": false, + "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..7b829ac75 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,47 @@ 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("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", () => { diff --git a/packages/server/src/workspaces/manager.ts b/packages/server/src/workspaces/manager.ts index 3ce25069b..cd1933b2f 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[] { @@ -125,6 +129,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(/\/+$/, "") @@ -140,7 +148,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..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(["OPENCODE_CONFIG_DIR", "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()