feat(desktop): compose V2 sidecar
This commit is contained in:
parent
f147ff64f1
commit
850721c3e9
28 changed files with 577 additions and 161 deletions
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
|
|
@ -308,6 +308,7 @@ jobs:
|
|||
- host: "windows-2025"
|
||||
target: aarch64-pc-windows-msvc
|
||||
platform_flag: --win --arm64
|
||||
bun_install_flags: --os=win32 --cpu=arm64
|
||||
- host: "blacksmith-4vcpu-windows-2025"
|
||||
target: x86_64-pc-windows-msvc
|
||||
platform_flag: --win
|
||||
|
|
@ -388,6 +389,7 @@ jobs:
|
|||
working-directory: packages/desktop
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
RUST_TARGET: ${{ matrix.settings.target }}
|
||||
OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
||||
|
|
|
|||
63
bun.lock
63
bun.lock
|
|
@ -174,12 +174,14 @@
|
|||
"dependencies": {
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"ws": "8.21.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/httpapi-codegen": "workspace:*",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"effect": "catalog:",
|
||||
},
|
||||
|
|
@ -431,10 +433,28 @@
|
|||
"devDependencies": {
|
||||
"@actions/artifact": "4.0.0",
|
||||
"@lydell/node-pty": "catalog:",
|
||||
"@lydell/node-pty-darwin-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-darwin-x64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-linux-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-linux-x64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-win32-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-win32-x64": "1.2.0-beta.12",
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/sdk-next": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1",
|
||||
"@sentry/solid": "catalog:",
|
||||
"@sentry/vite-plugin": "catalog:",
|
||||
"@silvia-odwyer/photon-node": "0.3.4",
|
||||
"@solid-primitives/i18n": "2.2.1",
|
||||
"@solid-primitives/storage": "catalog:",
|
||||
"@solidjs/meta": "catalog:",
|
||||
|
|
@ -452,22 +472,6 @@
|
|||
"vite": "catalog:",
|
||||
"zod-openapi": "5.4.6",
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@lydell/node-pty-darwin-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-darwin-x64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-linux-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-linux-x64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-win32-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-win32-x64": "1.2.0-beta.12",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1",
|
||||
},
|
||||
},
|
||||
"packages/effect-drizzle-sqlite": {
|
||||
"name": "@opencode-ai/effect-drizzle-sqlite",
|
||||
|
|
@ -650,6 +654,7 @@
|
|||
"packages/sdk-next": {
|
||||
"name": "@opencode-ai/sdk-next",
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
|
|
@ -663,6 +668,8 @@
|
|||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"ws": "8.21.0",
|
||||
},
|
||||
},
|
||||
"packages/server": {
|
||||
|
|
@ -1039,23 +1046,23 @@
|
|||
},
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"esbuild",
|
||||
"protobufjs",
|
||||
"electron",
|
||||
"web-tree-sitter",
|
||||
"esbuild",
|
||||
"electron",
|
||||
"protobufjs",
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||
"effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch",
|
||||
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch",
|
||||
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||
"solid-js@1.9.10": "patches/solid-js@1.9.10.patch",
|
||||
"@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch",
|
||||
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
|
||||
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
|
||||
"@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch",
|
||||
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
|
||||
"@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch",
|
||||
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
|
||||
"effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch",
|
||||
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
|
||||
"@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@opentui/core": "catalog:",
|
||||
|
|
|
|||
1
packages/desktop/.gitignore
vendored
1
packages/desktop/.gitignore
vendored
|
|
@ -25,5 +25,6 @@ dist-ssr
|
|||
out/
|
||||
|
||||
resources/opencode-cli*
|
||||
resources/server-assets
|
||||
resources/icons
|
||||
resources/*.metainfo.xml
|
||||
|
|
|
|||
|
|
@ -46,3 +46,20 @@ test("keeps a hidden prod launcher for old Linux pins", async () => {
|
|||
expect(desktop).toContain("StartupWMClass=ai.opencode.desktop")
|
||||
expect(desktop).toContain("NoDisplay=true")
|
||||
})
|
||||
|
||||
test("packages server native assets outside ASAR", async () => {
|
||||
const module = await import("./electron-builder.config")
|
||||
const config = module.default
|
||||
const resources = Array.isArray(config.extraResources) ? config.extraResources : []
|
||||
|
||||
expect(config.files).toContain("!resources/server-assets/**/*")
|
||||
expect(resources).toContainEqual({ from: "resources/server-assets/", to: "server-assets/" })
|
||||
})
|
||||
|
||||
test("keeps target native variants out of packaged production dependencies", async () => {
|
||||
const pkg = await Bun.file("package.json").json()
|
||||
|
||||
expect(pkg.optionalDependencies).toBeUndefined()
|
||||
expect(pkg.devDependencies["@lydell/node-pty-win32-arm64"]).toBe("1.2.0-beta.12")
|
||||
expect(pkg.devDependencies["@parcel/watcher-win32-arm64"]).toBe("2.5.1")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -52,13 +52,17 @@ const getBase = (appId: string): Configuration => ({
|
|||
extraMetadata: {
|
||||
desktopName: `${appId}.desktop`,
|
||||
},
|
||||
files: ["out/**/*", "resources/**/*"],
|
||||
files: ["out/**/*", "resources/**/*", "!resources/server-assets/**/*"],
|
||||
extraResources: [
|
||||
{
|
||||
from: "native/",
|
||||
to: "native/",
|
||||
filter: ["index.js", "index.d.ts", "build/Release/mac_window.node", "swift-build/**"],
|
||||
},
|
||||
{
|
||||
from: "resources/server-assets/",
|
||||
to: "server-assets/",
|
||||
},
|
||||
],
|
||||
mac: {
|
||||
category: "public.app-category.developer-tools",
|
||||
|
|
|
|||
12
packages/desktop/electron.vite.config.test.ts
Normal file
12
packages/desktop/electron.vite.config.test.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import config from "./electron.vite.config"
|
||||
|
||||
test("bundles the Node client into the packaged main process", () => {
|
||||
expect(config.main?.build?.externalizeDeps).toEqual({ exclude: ["@opencode-ai/client"] })
|
||||
})
|
||||
|
||||
test("keeps the bundled Node client out of packaged production dependencies", async () => {
|
||||
const pkg = await Bun.file("package.json").json()
|
||||
expect(pkg.dependencies["@opencode-ai/client"]).toBeUndefined()
|
||||
expect(pkg.devDependencies["@opencode-ai/client"]).toBe("workspace:*")
|
||||
})
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
import { sentryVitePlugin } from "@sentry/vite-plugin"
|
||||
import { defineConfig } from "electron-vite"
|
||||
import appPlugin from "@opencode-ai/app/vite"
|
||||
import * as fs from "node:fs/promises"
|
||||
|
||||
const OPENCODE_SERVER_DIST = "../opencode/dist/node"
|
||||
import { serverVite } from "@opencode-ai/sdk-next/vite"
|
||||
import { getServerTarget } from "./scripts/target"
|
||||
|
||||
const channel = (() => {
|
||||
const raw = process.env.OPENCODE_CHANNEL
|
||||
|
|
@ -12,7 +11,8 @@ const channel = (() => {
|
|||
return "dev"
|
||||
})()
|
||||
|
||||
const nodePtyPkg = `@lydell/node-pty-${process.platform}-${process.arch}`
|
||||
const server = serverVite()
|
||||
const target = getServerTarget()
|
||||
|
||||
const sentry =
|
||||
process.env.SENTRY_AUTH_TOKEN && process.env.SENTRY_ORG && process.env.SENTRY_PROJECT
|
||||
|
|
@ -33,12 +33,18 @@ const sentry =
|
|||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
resolve: server.resolve,
|
||||
define: {
|
||||
"import.meta.env.OPENCODE_CHANNEL": JSON.stringify(channel),
|
||||
OPENCODE_LIBC: target.platform === "linux" ? JSON.stringify("glibc") : "undefined",
|
||||
FFF_LIBC: target.platform === "linux" ? JSON.stringify("gnu") : "undefined",
|
||||
},
|
||||
build: {
|
||||
target: "node24",
|
||||
externalizeDeps: { exclude: ["@opencode-ai/client"] },
|
||||
rollupOptions: {
|
||||
input: { index: "src/main/index.ts", sidecar: "src/main/sidecar.ts" },
|
||||
external: [/^@opencode-ai\/simulation(?:\/|$)/],
|
||||
// Keep this identical to electron-vite's Node 20.11+ shim. Its regex insertion can
|
||||
// corrupt bundled TypeScript, while a Rollup banner places the shim safely.
|
||||
output: {
|
||||
|
|
@ -51,33 +57,8 @@ const require = __cjs_mod__.createRequire(import.meta.url);
|
|||
`,
|
||||
},
|
||||
},
|
||||
externalizeDeps: { include: [nodePtyPkg] },
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: "opencode:node-pty-narrower",
|
||||
enforce: "pre",
|
||||
resolveId(s) {
|
||||
if (s === "@lydell/node-pty") return nodePtyPkg
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "opencode:virtual-server-module",
|
||||
enforce: "pre",
|
||||
resolveId(id) {
|
||||
if (id === "virtual:opencode-server") return this.resolve(`${OPENCODE_SERVER_DIST}/node.js`)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "opencode:copy-server-assets",
|
||||
async writeBundle() {
|
||||
for (const l of await fs.readdir(OPENCODE_SERVER_DIST)) {
|
||||
if (!l.endsWith(".wasm")) continue
|
||||
await fs.writeFile(`./out/main/chunks/${l}`, await fs.readFile(`${OPENCODE_SERVER_DIST}/${l}`))
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
plugins: server.plugins,
|
||||
},
|
||||
preload: {
|
||||
build: {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsgo -b",
|
||||
"test": "bun test --only-failures",
|
||||
"predev": "bun ./scripts/predev.ts",
|
||||
"dev": "electron-vite dev",
|
||||
"prebuild": "bun ./scripts/prebuild.ts",
|
||||
|
|
@ -36,8 +37,26 @@
|
|||
"devDependencies": {
|
||||
"@actions/artifact": "4.0.0",
|
||||
"@lydell/node-pty": "catalog:",
|
||||
"@lydell/node-pty-darwin-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-darwin-x64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-linux-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-linux-x64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-win32-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-win32-x64": "1.2.0-beta.12",
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/schema": "workspace:*",
|
||||
"@opencode-ai/sdk-next": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1",
|
||||
"@silvia-odwyer/photon-node": "0.3.4",
|
||||
"@sentry/solid": "catalog:",
|
||||
"@sentry/vite-plugin": "catalog:",
|
||||
"@solid-primitives/i18n": "2.2.1",
|
||||
|
|
@ -56,21 +75,5 @@
|
|||
"typescript": "~5.6.2",
|
||||
"vite": "catalog:",
|
||||
"zod-openapi": "5.4.6"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@lydell/node-pty-darwin-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-darwin-x64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-linux-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-linux-x64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-win32-arm64": "1.2.0-beta.12",
|
||||
"@lydell/node-pty-win32-x64": "1.2.0-beta.12",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,4 +7,4 @@ const channel = resolveChannel()
|
|||
await $`bun ./scripts/copy-icons.ts ${channel}`
|
||||
await $`bun ./scripts/copy-metainfo.ts ${channel}`
|
||||
|
||||
await $`cd ../opencode && bun script/build-node.ts`
|
||||
await import("./stage-server-assets")
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@ import { $ } from "bun"
|
|||
|
||||
await $`bun ./scripts/copy-icons.ts ${process.env.OPENCODE_CHANNEL ?? "dev"}`
|
||||
|
||||
await $`cd ../opencode && bun script/build-node.ts`
|
||||
await import("./stage-server-assets")
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
#!/usr/bin/env bun
|
||||
import { Script } from "@opencode-ai/script"
|
||||
|
||||
await import("./prebuild")
|
||||
|
||||
const pkg = await Bun.file("./package.json").json()
|
||||
pkg.version = Script.version
|
||||
await Bun.write("./package.json", JSON.stringify(pkg, null, 2) + "\n")
|
||||
|
|
|
|||
36
packages/desktop/scripts/stage-server-assets.ts
Normal file
36
packages/desktop/scripts/stage-server-assets.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { chmod, cp, mkdir, rm, writeFile } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
import { getServerTarget } from "./target"
|
||||
|
||||
const target = getServerTarget()
|
||||
const root = path.resolve(import.meta.dirname, "../resources/server-assets")
|
||||
const nodePtyPackage = `@lydell/node-pty-${target.platform}-${target.arch}`
|
||||
const parcelWatcherPackage = `@parcel/watcher-${target.platform}-${target.arch}${target.platform === "linux" ? "-glibc" : ""}`
|
||||
const photonWasm = "@silvia-odwyer/photon-node/photon_rs_bg.wasm"
|
||||
const nodePtyEntry = fileURLToPath(import.meta.resolve(nodePtyPackage))
|
||||
const nodePtyRoot = path.resolve(path.dirname(nodePtyEntry), "..")
|
||||
|
||||
await rm(root, { recursive: true, force: true })
|
||||
await mkdir(root, { recursive: true })
|
||||
await cp(nodePtyRoot, path.join(root, nodePtyPackage), {
|
||||
recursive: true,
|
||||
filter: (source) => !source.endsWith(".map") && !source.endsWith(".pdb"),
|
||||
})
|
||||
await mkdir(path.join(root, parcelWatcherPackage), { recursive: true })
|
||||
await cp(
|
||||
fileURLToPath(import.meta.resolve(parcelWatcherPackage)),
|
||||
path.join(root, parcelWatcherPackage, "watcher.node"),
|
||||
)
|
||||
await mkdir(path.join(root, path.dirname(photonWasm)), { recursive: true })
|
||||
await cp(fileURLToPath(import.meta.resolve(photonWasm)), path.join(root, photonWasm))
|
||||
|
||||
if (target.platform === "darwin") {
|
||||
await chmod(path.join(root, nodePtyPackage, "prebuilds", `darwin-${target.arch}`, "spawn-helper"), 0o755)
|
||||
}
|
||||
|
||||
await writeFile(
|
||||
path.join(root, "target.json"),
|
||||
`${JSON.stringify({ ...target, nodePtyPackage, parcelWatcherPackage }, null, 2)}\n`,
|
||||
)
|
||||
72
packages/desktop/scripts/target.ts
Normal file
72
packages/desktop/scripts/target.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
export const SIDECAR_BINARIES: Array<{
|
||||
rustTarget: string
|
||||
ocBinary: string
|
||||
assetExt: string
|
||||
platform: "darwin" | "linux" | "win32"
|
||||
arch: "arm64" | "x64"
|
||||
}> = [
|
||||
{
|
||||
rustTarget: "aarch64-apple-darwin",
|
||||
ocBinary: "opencode-darwin-arm64",
|
||||
assetExt: "zip",
|
||||
platform: "darwin",
|
||||
arch: "arm64",
|
||||
},
|
||||
{
|
||||
rustTarget: "x86_64-apple-darwin",
|
||||
ocBinary: "opencode-darwin-x64-baseline",
|
||||
assetExt: "zip",
|
||||
platform: "darwin",
|
||||
arch: "x64",
|
||||
},
|
||||
{
|
||||
rustTarget: "aarch64-pc-windows-msvc",
|
||||
ocBinary: "opencode-windows-arm64",
|
||||
assetExt: "zip",
|
||||
platform: "win32",
|
||||
arch: "arm64",
|
||||
},
|
||||
{
|
||||
rustTarget: "x86_64-pc-windows-msvc",
|
||||
ocBinary: "opencode-windows-x64-baseline",
|
||||
assetExt: "zip",
|
||||
platform: "win32",
|
||||
arch: "x64",
|
||||
},
|
||||
{
|
||||
rustTarget: "x86_64-unknown-linux-gnu",
|
||||
ocBinary: "opencode-linux-x64-baseline",
|
||||
assetExt: "tar.gz",
|
||||
platform: "linux",
|
||||
arch: "x64",
|
||||
},
|
||||
{
|
||||
rustTarget: "aarch64-unknown-linux-gnu",
|
||||
ocBinary: "opencode-linux-arm64",
|
||||
assetExt: "tar.gz",
|
||||
platform: "linux",
|
||||
arch: "arm64",
|
||||
},
|
||||
]
|
||||
|
||||
export const RUST_TARGET = process.env.RUST_TARGET
|
||||
|
||||
function nativeTarget() {
|
||||
if (process.platform === "darwin") return process.arch === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin"
|
||||
if (process.platform === "win32")
|
||||
return process.arch === "arm64" ? "aarch64-pc-windows-msvc" : "x86_64-pc-windows-msvc"
|
||||
if (process.platform === "linux")
|
||||
return process.arch === "arm64" ? "aarch64-unknown-linux-gnu" : "x86_64-unknown-linux-gnu"
|
||||
throw new Error(`Unsupported platform: ${process.platform}/${process.arch}`)
|
||||
}
|
||||
|
||||
export function getCurrentSidecar(target = RUST_TARGET ?? nativeTarget()) {
|
||||
const binaryConfig = SIDECAR_BINARIES.find((item) => item.rustTarget === target)
|
||||
if (!binaryConfig) throw new Error(`Sidecar configuration not available for Rust target '${target}'`)
|
||||
return binaryConfig
|
||||
}
|
||||
|
||||
export function getServerTarget(target = RUST_TARGET ?? nativeTarget()) {
|
||||
const current = getCurrentSidecar(target)
|
||||
return { rustTarget: current.rustTarget, platform: current.platform, arch: current.arch }
|
||||
}
|
||||
28
packages/desktop/scripts/utils.test.ts
Normal file
28
packages/desktop/scripts/utils.test.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { expect, test } from "bun:test"
|
||||
|
||||
import { SIDECAR_BINARIES, getServerTarget } from "./target"
|
||||
import packageManifest from "../package.json"
|
||||
|
||||
test("derives the Windows ARM64 server assets from the release target", () => {
|
||||
expect(getServerTarget("aarch64-pc-windows-msvc")).toEqual({
|
||||
rustTarget: "aarch64-pc-windows-msvc",
|
||||
platform: "win32",
|
||||
arch: "arm64",
|
||||
})
|
||||
})
|
||||
|
||||
test("derives each packaged server architecture independently of the runner", () => {
|
||||
expect(getServerTarget("x86_64-apple-darwin")).toMatchObject({ platform: "darwin", arch: "x64" })
|
||||
expect(getServerTarget("aarch64-unknown-linux-gnu")).toMatchObject({ platform: "linux", arch: "arm64" })
|
||||
})
|
||||
|
||||
test("declares every target-native server dependency directly", () => {
|
||||
const dependencies = packageManifest.devDependencies
|
||||
for (const target of SIDECAR_BINARIES) {
|
||||
expect(dependencies[`@lydell/node-pty-${target.platform}-${target.arch}`]).toBeDefined()
|
||||
const watcher = `@parcel/watcher-${target.platform}-${target.arch}${target.platform === "linux" ? "-glibc" : ""}`
|
||||
expect(dependencies[watcher]).toBeDefined()
|
||||
}
|
||||
expect(dependencies["@parcel/watcher-darwin-x64"]).toBe("2.5.1")
|
||||
expect(dependencies["@silvia-odwyer/photon-node"]).toBe("0.3.4")
|
||||
})
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import { $ } from "bun"
|
||||
|
||||
export { getCurrentSidecar, getServerTarget, RUST_TARGET, SIDECAR_BINARIES } from "./target"
|
||||
|
||||
export type Channel = "dev" | "beta" | "prod"
|
||||
|
||||
export function resolveChannel(): Channel {
|
||||
|
|
@ -8,56 +10,6 @@ export function resolveChannel(): Channel {
|
|||
return "dev"
|
||||
}
|
||||
|
||||
export const SIDECAR_BINARIES: Array<{ rustTarget: string; ocBinary: string; assetExt: string }> = [
|
||||
{
|
||||
rustTarget: "aarch64-apple-darwin",
|
||||
ocBinary: "opencode-darwin-arm64",
|
||||
assetExt: "zip",
|
||||
},
|
||||
{
|
||||
rustTarget: "x86_64-apple-darwin",
|
||||
ocBinary: "opencode-darwin-x64-baseline",
|
||||
assetExt: "zip",
|
||||
},
|
||||
{
|
||||
rustTarget: "aarch64-pc-windows-msvc",
|
||||
ocBinary: "opencode-windows-arm64",
|
||||
assetExt: "zip",
|
||||
},
|
||||
{
|
||||
rustTarget: "x86_64-pc-windows-msvc",
|
||||
ocBinary: "opencode-windows-x64-baseline",
|
||||
assetExt: "zip",
|
||||
},
|
||||
{
|
||||
rustTarget: "x86_64-unknown-linux-gnu",
|
||||
ocBinary: "opencode-linux-x64-baseline",
|
||||
assetExt: "tar.gz",
|
||||
},
|
||||
{
|
||||
rustTarget: "aarch64-unknown-linux-gnu",
|
||||
ocBinary: "opencode-linux-arm64",
|
||||
assetExt: "tar.gz",
|
||||
},
|
||||
]
|
||||
|
||||
export const RUST_TARGET = Bun.env.RUST_TARGET
|
||||
|
||||
function nativeTarget() {
|
||||
const { platform, arch } = process
|
||||
if (platform === "darwin") return arch === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin"
|
||||
if (platform === "win32") return arch === "arm64" ? "aarch64-pc-windows-msvc" : "x86_64-pc-windows-msvc"
|
||||
if (platform === "linux") return arch === "arm64" ? "aarch64-unknown-linux-gnu" : "x86_64-unknown-linux-gnu"
|
||||
throw new Error(`Unsupported platform: ${platform}/${arch}`)
|
||||
}
|
||||
|
||||
export function getCurrentSidecar(target = RUST_TARGET ?? nativeTarget()) {
|
||||
const binaryConfig = SIDECAR_BINARIES.find((b) => b.rustTarget === target)
|
||||
if (!binaryConfig) throw new Error(`Sidecar configuration not available for Rust target '${target}'`)
|
||||
|
||||
return binaryConfig
|
||||
}
|
||||
|
||||
export async function copyBinaryToSidecarFolder(source: string) {
|
||||
const dir = `resources`
|
||||
await $`mkdir -p ${dir}`
|
||||
|
|
|
|||
12
packages/desktop/src/main/env.d.ts
vendored
12
packages/desktop/src/main/env.d.ts
vendored
|
|
@ -5,15 +5,3 @@ interface ImportMetaEnv {
|
|||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
|
||||
declare module "virtual:opencode-server" {
|
||||
export namespace Server {
|
||||
export const listen: typeof import("../../../opencode/dist/types/src/node").Server.listen
|
||||
export type Listener = import("../../../opencode/dist/types/src/node").Server.Listener
|
||||
}
|
||||
export namespace Config {
|
||||
export const get: typeof import("../../../opencode/dist/types/src/node").Config.get
|
||||
export type Info = import("../../../opencode/dist/types/src/node").Config.Info
|
||||
}
|
||||
export const bootstrap: typeof import("../../../opencode/dist/types/src/node").bootstrap
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ export function preferAppEnv(userDataPath: string) {
|
|||
...(shell ? loadShellEnv(shell, getLogger()) : null),
|
||||
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true",
|
||||
OPENCODE_CLIENT: "desktop",
|
||||
OPENCODE_NODE_PTY_PATH:
|
||||
process.env.OPENCODE_NODE_PTY_PATH ??
|
||||
join(serverAssetsPath(), `@lydell/node-pty-${process.platform}-${process.arch}`, "lib/index.js"),
|
||||
XDG_STATE_HOME: process.env.XDG_STATE_HOME ?? userDataPath,
|
||||
})
|
||||
}
|
||||
|
|
@ -132,6 +135,9 @@ export async function spawnLocalServer(
|
|||
port,
|
||||
password,
|
||||
userDataPath: options.userDataPath,
|
||||
version: app.getVersion(),
|
||||
channel: import.meta.env.OPENCODE_CHANNEL,
|
||||
serverAssetsPath: serverAssetsPath(),
|
||||
})
|
||||
}).catch((error) => {
|
||||
if (!exited) child.kill()
|
||||
|
|
@ -166,13 +172,12 @@ export async function spawnLocalServer(
|
|||
stop: () => {
|
||||
if (stopping) return stopping
|
||||
if (exited) return Promise.resolve()
|
||||
child.postMessage({ type: "stop" })
|
||||
stopping = Promise.race([
|
||||
exit.promise.then(() => undefined),
|
||||
delay(SIDECAR_STOP_TIMEOUT).then(() => {
|
||||
if (!exited) child.kill()
|
||||
}),
|
||||
])
|
||||
stopping = (async () => {
|
||||
child.postMessage({ type: "stop" })
|
||||
if (await settlesWithin(exit.promise, SIDECAR_STOP_TIMEOUT)) return
|
||||
if (!exited) child.kill()
|
||||
await exit.promise
|
||||
})()
|
||||
return stopping
|
||||
},
|
||||
},
|
||||
|
|
@ -180,6 +185,12 @@ export async function spawnLocalServer(
|
|||
}
|
||||
}
|
||||
|
||||
function serverAssetsPath() {
|
||||
return app.isPackaged
|
||||
? join(process.resourcesPath, "server-assets")
|
||||
: join(app.getAppPath(), "resources/server-assets")
|
||||
}
|
||||
|
||||
export async function checkHealth(url: string, password?: string | null): Promise<boolean> {
|
||||
let healthUrls: URL[]
|
||||
try {
|
||||
|
|
@ -216,8 +227,15 @@ function createSidecarEnv(): Record<string, string> {
|
|||
return env
|
||||
}
|
||||
|
||||
function delay(ms: number) {
|
||||
return new Promise<void>((resolve) => setTimeout(resolve, ms))
|
||||
function settlesWithin(promise: Promise<unknown>, ms: number) {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const timeout = setTimeout(() => resolve(false), ms)
|
||||
timeout.unref()
|
||||
void promise.then(() => {
|
||||
clearTimeout(timeout)
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function serializeError(error: unknown) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import * as http from "node:http"
|
||||
import * as tls from "node:tls"
|
||||
import path from "node:path"
|
||||
|
||||
type NodeHttpWithEnvProxy = typeof http & {
|
||||
setGlobalProxyFromEnv: () => void
|
||||
|
|
@ -16,6 +17,9 @@ type StartCommand = {
|
|||
port: number
|
||||
password: string
|
||||
userDataPath: string
|
||||
version: string
|
||||
channel: string
|
||||
serverAssetsPath: string
|
||||
}
|
||||
|
||||
type StopCommand = { type: "stop" }
|
||||
|
|
@ -50,18 +54,17 @@ parentPort.on("message", (event) => {
|
|||
|
||||
async function start(command: StartCommand) {
|
||||
try {
|
||||
prepareSidecarEnv(command.password, command.userDataPath)
|
||||
prepareSidecarEnv(command)
|
||||
ensureLoopbackNoProxy()
|
||||
useSystemCertificates()
|
||||
useEnvProxy()
|
||||
const { Server } = await import("virtual:opencode-server")
|
||||
const { Server } = await import("@opencode-ai/sdk-next/server")
|
||||
|
||||
listener = await Server.listen({
|
||||
app: { name: "desktop", version: command.version, channel: command.channel },
|
||||
port: command.port,
|
||||
hostname: command.hostname,
|
||||
username: "opencode",
|
||||
password: command.password,
|
||||
cors: ["oc://renderer"],
|
||||
})
|
||||
parentPort.postMessage({ type: "ready" })
|
||||
} catch (error) {
|
||||
|
|
@ -80,11 +83,17 @@ async function stop() {
|
|||
}
|
||||
}
|
||||
|
||||
function prepareSidecarEnv(password: string, userDataPath: string) {
|
||||
function prepareSidecarEnv(command: StartCommand) {
|
||||
const nodePtyPackage = `@lydell/node-pty-${process.platform}-${process.arch}`
|
||||
const parcelWatcherPackage = `@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? "-glibc" : ""}`
|
||||
Object.assign(process.env, {
|
||||
OPENCODE_SERVER_USERNAME: "opencode",
|
||||
OPENCODE_SERVER_PASSWORD: password,
|
||||
XDG_STATE_HOME: process.env.XDG_STATE_HOME ?? userDataPath,
|
||||
OPENCODE_SERVER_PASSWORD: command.password,
|
||||
XDG_STATE_HOME: process.env.XDG_STATE_HOME ?? command.userDataPath,
|
||||
OPENCODE_NODE_ASSETS_DIR: command.serverAssetsPath,
|
||||
OPENCODE_NODE_PTY_PATH: path.join(command.serverAssetsPath, nodePtyPackage, "lib/index.js"),
|
||||
OPENCODE_PARCEL_WATCHER_PATH: path.join(command.serverAssetsPath, parcelWatcherPackage, "watcher.node"),
|
||||
OPENCODE_PHOTON_WASM_PATH: path.join(command.serverAssetsPath, "@silvia-odwyer/photon-node/photon_rs_bg.wasm"),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -136,12 +145,18 @@ function parseCommand(value: unknown): SidecarCommand | undefined {
|
|||
if (typeof command.port !== "number") return
|
||||
if (typeof command.password !== "string") return
|
||||
if (typeof command.userDataPath !== "string") return
|
||||
if (typeof command.version !== "string") return
|
||||
if (typeof command.channel !== "string") return
|
||||
if (typeof command.serverAssetsPath !== "string") return
|
||||
return {
|
||||
type: "start",
|
||||
hostname: command.hostname,
|
||||
port: command.port,
|
||||
password: command.password,
|
||||
userDataPath: command.userDataPath,
|
||||
version: command.version,
|
||||
channel: command.channel,
|
||||
serverAssetsPath: command.serverAssetsPath,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { spawn } from "node:child_process"
|
||||
import { existsSync } from "node:fs"
|
||||
import { createRequire } from "node:module"
|
||||
import { join } from "node:path"
|
||||
import * as pty from "@lydell/node-pty"
|
||||
import type { WslDistroProbe, WslInstalledDistro, WslOnlineDistro, WslRuntimeCheck } from "../../preload/types"
|
||||
import { wslTerminalArgs } from "./policy"
|
||||
|
||||
|
|
@ -32,6 +32,7 @@ export type RunWslOptions = {
|
|||
|
||||
const DEFAULT_WSL_TIMEOUT_MS = 20_000
|
||||
const DEFAULT_WSL_INSTALL_TIMEOUT_MS = 15 * 60_000
|
||||
let nodePty: typeof import("@lydell/node-pty") | undefined
|
||||
|
||||
export function wslArgs(args: string[], distro?: string | null, user?: string | null) {
|
||||
return [...(distro ? ["-d", distro] : []), ...(user ? ["--user", user] : []), "--", ...args]
|
||||
|
|
@ -112,7 +113,7 @@ function runCommand(command: string, args: string[], opts: RunWslOptions = {}) {
|
|||
|
||||
function runInteractiveCommand(command: string, args: string[], opts: RunWslOptions = {}, defaultTimeoutMs: number) {
|
||||
return new Promise<WslCommandResult>((resolve, reject) => {
|
||||
const child = pty.spawn(command, args, {
|
||||
const child = loadNodePty().spawn(command, args, {
|
||||
name: "xterm-color",
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
|
|
@ -172,6 +173,15 @@ function runInteractiveCommand(command: string, args: string[], opts: RunWslOpti
|
|||
})
|
||||
}
|
||||
|
||||
function loadNodePty() {
|
||||
if (nodePty) return nodePty
|
||||
const loaded: typeof import("@lydell/node-pty") = createRequire(import.meta.url)(
|
||||
process.env.OPENCODE_NODE_PTY_PATH ?? "@lydell/node-pty",
|
||||
)
|
||||
nodePty = loaded
|
||||
return loaded
|
||||
}
|
||||
|
||||
function createOutputDecoder() {
|
||||
let decoder: TextDecoder | undefined
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -5,13 +5,19 @@
|
|||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
".": "./src/index.ts",
|
||||
"./server": {
|
||||
"types": "./src/server-interface.ts",
|
||||
"default": "./src/server.ts"
|
||||
},
|
||||
"./vite": "./src/vite.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "bun test --timeout 5000",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/client": "workspace:*",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/plugin": "workspace:*",
|
||||
|
|
@ -24,6 +30,8 @@
|
|||
"@opencode-ai/protocol": "workspace:*",
|
||||
"@tsconfig/bun": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
"@typescript/native-preview": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"ws": "8.21.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
packages/sdk-next/src/plugin-runtime.effect.ts
Normal file
28
packages/sdk-next/src/plugin-runtime.effect.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import {
|
||||
Agent,
|
||||
Command,
|
||||
Connection,
|
||||
Credential,
|
||||
Integration,
|
||||
Model,
|
||||
Plugin,
|
||||
Provider,
|
||||
Reference,
|
||||
Skill,
|
||||
} from "@opencode-ai/plugin/effect"
|
||||
import { Tool } from "@opencode-ai/schema/tool"
|
||||
|
||||
const key = Symbol.for("opencode.plugin.v2.effect")
|
||||
;(globalThis as typeof globalThis & { [key]?: unknown })[key] = {
|
||||
Agent,
|
||||
Command,
|
||||
Connection,
|
||||
Credential,
|
||||
Integration,
|
||||
Model,
|
||||
Plugin,
|
||||
Provider,
|
||||
Reference,
|
||||
Skill,
|
||||
Tool: { Error: Tool.Error },
|
||||
}
|
||||
26
packages/sdk-next/src/plugin-runtime.promise.ts
Normal file
26
packages/sdk-next/src/plugin-runtime.promise.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import {
|
||||
Agent,
|
||||
Command,
|
||||
Connection,
|
||||
Credential,
|
||||
Integration,
|
||||
Model,
|
||||
Plugin,
|
||||
Provider,
|
||||
Reference,
|
||||
Skill,
|
||||
} from "@opencode-ai/plugin"
|
||||
|
||||
const key = Symbol.for("opencode.plugin.v2.promise")
|
||||
;(globalThis as typeof globalThis & { [key]?: unknown })[key] = {
|
||||
Agent,
|
||||
Command,
|
||||
Connection,
|
||||
Credential,
|
||||
Integration,
|
||||
Model,
|
||||
Plugin,
|
||||
Provider,
|
||||
Reference,
|
||||
Skill,
|
||||
}
|
||||
61
packages/sdk-next/src/plugin-runtime.ts
Normal file
61
packages/sdk-next/src/plugin-runtime.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import "./plugin-runtime.promise"
|
||||
import "./plugin-runtime.effect"
|
||||
|
||||
import { registerHooks } from "node:module"
|
||||
|
||||
const key = Symbol.for("opencode.sdk-next.plugin-runtime")
|
||||
const globals = globalThis as typeof globalThis & Record<symbol, unknown>
|
||||
|
||||
if (!globals[key]) {
|
||||
globals[key] = true
|
||||
const promiseModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.promise")]
|
||||
if (!sdk) throw new Error("OpenCode Promise plugin SDK is unavailable")
|
||||
export const Agent = sdk.Agent
|
||||
export const Command = sdk.Command
|
||||
export const Connection = sdk.Connection
|
||||
export const Credential = sdk.Credential
|
||||
export const Integration = sdk.Integration
|
||||
export const Model = sdk.Model
|
||||
export const Plugin = sdk.Plugin
|
||||
export const Provider = sdk.Provider
|
||||
export const Reference = sdk.Reference
|
||||
export const Skill = sdk.Skill`
|
||||
const effectModule = promiseModule
|
||||
.replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect")
|
||||
.replace("Promise plugin", "Effect plugin")
|
||||
const promisePluginModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.promise")]
|
||||
if (!sdk) throw new Error("OpenCode Promise plugin SDK is unavailable")
|
||||
export const define = sdk.Plugin.define`
|
||||
const effectPluginModule = promisePluginModule
|
||||
.replace("opencode.plugin.v2.promise", "opencode.plugin.v2.effect")
|
||||
.replace("Promise plugin", "Effect plugin")
|
||||
const effectToolModule = `const sdk = globalThis[Symbol.for("opencode.plugin.v2.effect")]
|
||||
if (!sdk) throw new Error("OpenCode Effect plugin SDK is unavailable")
|
||||
export const Error = sdk.Tool.Error`
|
||||
const modules: Record<string, string> = {
|
||||
"@opencode-ai/plugin": "opencode:plugin-v2",
|
||||
"@opencode-ai/plugin/promise/plugin": "opencode:plugin-promise-plugin",
|
||||
"@opencode-ai/plugin/promise/tool": "opencode:plugin-promise-tool",
|
||||
"@opencode-ai/plugin/effect": "opencode:plugin-v2-effect",
|
||||
"@opencode-ai/plugin/effect/plugin": "opencode:plugin-v2-effect-plugin",
|
||||
"@opencode-ai/plugin/effect/tool": "opencode:plugin-v2-effect-tool",
|
||||
}
|
||||
const sources: Record<string, string> = {
|
||||
"opencode:plugin-v2": promiseModule,
|
||||
"opencode:plugin-promise-plugin": promisePluginModule,
|
||||
"opencode:plugin-promise-tool": "export {}",
|
||||
"opencode:plugin-v2-effect": effectModule,
|
||||
"opencode:plugin-v2-effect-plugin": effectPluginModule,
|
||||
"opencode:plugin-v2-effect-tool": effectToolModule,
|
||||
}
|
||||
registerHooks({
|
||||
resolve(specifier, context, nextResolve) {
|
||||
const url = modules[specifier]
|
||||
return url ? { url, shortCircuit: true } : nextResolve(specifier, context)
|
||||
},
|
||||
load(url, context, nextLoad) {
|
||||
const source = sources[url]
|
||||
return source ? { format: "module", source, shortCircuit: true } : nextLoad(url, context)
|
||||
},
|
||||
})
|
||||
}
|
||||
16
packages/sdk-next/src/server-interface.ts
Normal file
16
packages/sdk-next/src/server-interface.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export type ListenOptions = {
|
||||
readonly app: {
|
||||
readonly name: string
|
||||
readonly version: string
|
||||
readonly channel: string
|
||||
}
|
||||
readonly hostname: string
|
||||
readonly port: number
|
||||
readonly password: string
|
||||
}
|
||||
|
||||
export type Listener = { readonly stop: () => Promise<void> }
|
||||
export type Listen = (options: ListenOptions) => Promise<Listener>
|
||||
|
||||
export declare const listen: Listen
|
||||
export declare const Server: { readonly listen: Listen }
|
||||
79
packages/sdk-next/src/server.ts
Normal file
79
packages/sdk-next/src/server.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import "./plugin-runtime"
|
||||
|
||||
import { NodeServices } from "@effect/platform-node"
|
||||
import { ServerProcess } from "@opencode-ai/server/process"
|
||||
import { Effect, Exit, ManagedRuntime, Scope } from "effect"
|
||||
import { randomUUID } from "node:crypto"
|
||||
import type { Listen, ListenOptions } from "./server-interface"
|
||||
|
||||
export const listen: Listen = async (options) => {
|
||||
const runtime = ManagedRuntime.make(NodeServices.layer)
|
||||
const scope = await runtime.runPromise(Scope.make())
|
||||
await runtime
|
||||
.runPromise(
|
||||
ServerProcess.start(fromEnvironment(options), {
|
||||
instanceID: randomUUID(),
|
||||
onListen: () => Effect.succeed(Effect.void),
|
||||
}).pipe(Effect.provideService(Scope.Scope, scope)),
|
||||
)
|
||||
.catch(async (error) => {
|
||||
await runtime
|
||||
.runPromise(Scope.close(scope, Exit.fail(error)))
|
||||
.finally(() => Effect.runPromise(runtime.disposeEffect))
|
||||
throw error
|
||||
})
|
||||
let stopped = false
|
||||
return {
|
||||
stop: async () => {
|
||||
if (stopped) return
|
||||
stopped = true
|
||||
await runtime.runPromise(Scope.close(scope, Exit.void)).finally(() => Effect.runPromise(runtime.disposeEffect))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function fromEnvironment(options: ListenOptions, environment = process.env) {
|
||||
const channel = options.app.channel
|
||||
return {
|
||||
...options,
|
||||
simulation: truthy(environment.OPENCODE_SIMULATE),
|
||||
database: {
|
||||
path:
|
||||
environment.OPENCODE_DB ??
|
||||
(["latest", "beta", "prod"].includes(channel) || truthy(environment.OPENCODE_DISABLE_CHANNEL_DB)
|
||||
? "opencode.db"
|
||||
: `opencode-${channel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`),
|
||||
},
|
||||
models: {
|
||||
url: environment.OPENCODE_MODELS_URL,
|
||||
file: environment.OPENCODE_MODELS_PATH,
|
||||
fetch: !truthy(environment.OPENCODE_DISABLE_MODELS_FETCH),
|
||||
},
|
||||
observability: {
|
||||
endpoint: environment.OTEL_EXPORTER_OTLP_ENDPOINT,
|
||||
headers: environment.OTEL_EXPORTER_OTLP_HEADERS,
|
||||
},
|
||||
config: {
|
||||
directory: environment.OPENCODE_CONFIG_DIR,
|
||||
project: !truthy(environment.OPENCODE_CONFIG_PROJECT_DISABLE ?? environment.OPENCODE_DISABLE_PROJECT_CONFIG),
|
||||
file: environment.OPENCODE_CONFIG,
|
||||
content: environment.OPENCODE_CONFIG_CONTENT,
|
||||
},
|
||||
windows: {
|
||||
gitbash: environment.OPENCODE_GIT_BASH_PATH,
|
||||
},
|
||||
fs: {
|
||||
filewatcher: !truthy(environment.OPENCODE_FILEWATCHER_DISABLE ?? environment.OPENCODE_DISABLE_FILEWATCHER),
|
||||
fff:
|
||||
environment.OPENCODE_DISABLE_FFF === undefined
|
||||
? process.platform !== "win32"
|
||||
: !truthy(environment.OPENCODE_DISABLE_FFF),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function truthy(value?: string) {
|
||||
return value === "1" || value?.toLowerCase() === "true"
|
||||
}
|
||||
|
||||
export const Server = { listen }
|
||||
43
packages/sdk-next/src/vite.ts
Normal file
43
packages/sdk-next/src/vite.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { readFile } from "node:fs/promises"
|
||||
import { createRequire } from "node:module"
|
||||
import path from "node:path"
|
||||
import type { Plugin } from "vite"
|
||||
|
||||
export function serverVite() {
|
||||
const plugins: Plugin[] = [
|
||||
{
|
||||
name: "opencode:raw-text",
|
||||
async load(id) {
|
||||
if (!id.endsWith(".md")) return
|
||||
return `export default ${JSON.stringify(await readFile(id, "utf8"))}`
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "opencode:runtime-require",
|
||||
enforce: "pre",
|
||||
transform(code, id) {
|
||||
if (!id.endsWith("turndown/lib/turndown.es.js")) return
|
||||
const transformed = code.replace(" var domino = require('@mixmark-io/domino');", "")
|
||||
if (transformed === code) this.error("Failed to rewrite Turndown's Domino require")
|
||||
return `import domino from "@mixmark-io/domino"\n${transformed}`
|
||||
},
|
||||
},
|
||||
]
|
||||
return {
|
||||
plugins,
|
||||
resolve: {
|
||||
alias: [
|
||||
{ find: /^solid-js\/store$/, replacement: "solid-js/store/dist/store.js" },
|
||||
{ find: /^solid-js$/, replacement: "solid-js/dist/solid.js" },
|
||||
{
|
||||
find: /^ws$/,
|
||||
replacement: path.join(
|
||||
path.dirname(createRequire(import.meta.url).resolve("ws/package.json")),
|
||||
"wrapper.mjs",
|
||||
),
|
||||
},
|
||||
],
|
||||
conditions: ["node"],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -8,19 +8,26 @@ const core = resolve(import.meta.dir, "../../core")
|
|||
const server = resolve(import.meta.dir, "../../server")
|
||||
|
||||
test("bundles the client and in-memory host", async () => {
|
||||
const inputs = await bundleInputs()
|
||||
const inputs = await bundleInputs("@opencode-ai/sdk-next")
|
||||
|
||||
expect(within(inputs, client).length).toBeGreaterThan(0)
|
||||
expect(within(inputs, core).length).toBeGreaterThan(0)
|
||||
expect(within(inputs, server).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
async function bundleInputs() {
|
||||
test("declares the Node server composition entrypoint", async () => {
|
||||
const inputs = await bundleInputs("@opencode-ai/sdk-next/server")
|
||||
|
||||
expect(within(inputs, core).length).toBeGreaterThan(0)
|
||||
expect(within(inputs, server).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
async function bundleInputs(specifier: string) {
|
||||
const temporary = await mkdtemp(join(import.meta.dir, ".import-boundary-"))
|
||||
const entrypoint = join(temporary, "index.ts")
|
||||
const metafile = join(temporary, "meta.json")
|
||||
try {
|
||||
await Bun.write(entrypoint, 'export * from "@opencode-ai/sdk-next"')
|
||||
await Bun.write(entrypoint, `export * from ${JSON.stringify(specifier)}`)
|
||||
const child = Bun.spawn(
|
||||
[
|
||||
process.execPath,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@
|
|||
"dependsOn": ["^build"],
|
||||
"outputs": []
|
||||
},
|
||||
"@opencode-ai/desktop#test": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": []
|
||||
},
|
||||
"@opencode-ai/ui#test": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue