mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-07-10 00:14:27 +00:00
Package CodeNomad OpenCode plugin (#433)
## Summary - Rename the OpenCode config template into a versioned npm-packable CodeNomad plugin package. - Build and package the plugin through the server bundle, with Electron/Tauri carrying it via existing server resources. - Replace OPENCODE_CONFIG_DIR injection with JSONC-aware OPENCODE_CONFIG_CONTENT merging that appends the CodeNomad plugin while preserving user config. ## Validation - npm run build --workspace @codenomad/codenomad-opencode-plugin - npm run prepare-plugin --workspace @neuralnomads/codenomad - npm run typecheck --workspace @neuralnomads/codenomad - npm run typecheck --workspace @neuralnomads/codenomad-electron-app - node --import tsx --test \"src/opencode-plugin.test.ts\" \"src/workspaces/__tests__/spawn.test.ts\" ## Notes - Production plugin loading uses an explicit npm file alias for the packaged tarball. - Dev loading still references the TypeScript plugin entry directly. --------- Co-authored-by: Pascal André <pascalandr@gmail.com>
This commit is contained in:
parent
9165299027
commit
cf88dc06ef
24 changed files with 671 additions and 566 deletions
254
package-lock.json
generated
254
package-lock.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
"packages/ui",
|
||||
"packages/electron-app",
|
||||
"packages/tauri-app",
|
||||
"packages/opencode-config"
|
||||
"packages/opencode-plugin"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -79,8 +79,8 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"from": "../server/dist/opencode-config",
|
||||
"to": "opencode-config"
|
||||
"from": "../server/dist/opencode-plugin",
|
||||
"to": "opencode-plugin"
|
||||
}
|
||||
],
|
||||
"mac": {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"$schema": "https://opencode.ai/config.json"
|
||||
}
|
||||
380
packages/opencode-config/package-lock.json
generated
380
packages/opencode-config/package-lock.json
generated
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"name": "@codenomad/opencode-config",
|
||||
"version": "0.15.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@opencode-ai/plugin": "1.3.7"
|
||||
}
|
||||
}
|
||||
|
|
@ -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`
|
||||
22
packages/opencode-plugin/package.json
Normal file
22
packages/opencode-plugin/package.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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<typeof createBackgroundProcessTools>
|
||||
"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<void>
|
||||
|
||||
type CodeNomadEventHook = (input: { event: any }) => Promise<void>
|
||||
|
||||
function buildVoiceModePrompt(): string {
|
||||
return [
|
||||
"Voice conversation mode is enabled.",
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
17
packages/opencode-plugin/tsconfig.json
Normal file
17
packages/opencode-plugin/tsconfig.json
Normal file
|
|
@ -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"]
|
||||
}
|
||||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
59
packages/server/scripts/package-opencode-plugin.mjs
Normal file
59
packages/server/scripts/package-opencode-plugin.mjs
Normal file
|
|
@ -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}`)
|
||||
|
|
@ -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
|
||||
}
|
||||
38
packages/server/src/opencode-plugin.test.ts
Normal file
38
packages/server/src/opencode-plugin.test.ts
Normal file
|
|
@ -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"])
|
||||
})
|
||||
})
|
||||
175
packages/server/src/opencode-plugin.ts
Normal file
175
packages/server/src/opencode-plugin.ts
Normal file
|
|
@ -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, unknown>): 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<string, unknown> {
|
||||
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<string, unknown>
|
||||
} 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
|
||||
}
|
||||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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<string, WorkspaceRecord>()
|
||||
private readonly runtime: WorkspaceRuntime
|
||||
private readonly opencodeConfigDir: string
|
||||
private readonly codeNomadPluginUrl: string
|
||||
private readonly opencodeAuth = new Map<string, { username: string; password: string; authorization: string }>()
|
||||
|
||||
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 } : {}),
|
||||
|
|
|
|||
|
|
@ -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, "\\$&")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue