فهرست منبع

Remove CLI from electron app (#17803)

Co-authored-by: LukeParkerDev <[email protected]>
Brendan Allan 1 هفته پیش
والد
کامیت
ee23043d64
38فایلهای تغییر یافته به همراه283 افزوده شده و 521 حذف شده
  1. 29 11
      bun.lock
  2. 2 1
      package.json
  3. 1 0
      packages/app/src/components/terminal.tsx
  4. 0 5
      packages/desktop-electron/electron-builder.config.ts
  5. 31 0
      packages/desktop-electron/electron.vite.config.ts
  6. 22 11
      packages/desktop-electron/package.json
  7. 9 0
      packages/desktop-electron/scripts/prebuild.ts
  8. 1 13
      packages/desktop-electron/scripts/predev.ts
  9. 1 17
      packages/desktop-electron/scripts/prepare.ts
  10. 0 283
      packages/desktop-electron/src/main/cli.ts
  11. 22 0
      packages/desktop-electron/src/main/env.d.ts
  12. 10 22
      packages/desktop-electron/src/main/index.ts
  13. 0 2
      packages/desktop-electron/src/main/ipc.ts
  14. 0 5
      packages/desktop-electron/src/main/menu.ts
  15. 30 16
      packages/desktop-electron/src/main/server.ts
  16. 13 13
      packages/desktop-electron/src/main/shell-env.ts
  17. 1 1
      packages/opencode/package.json
  18. 5 16
      packages/opencode/script/build-node.ts
  19. 2 1
      packages/opencode/src/cli/cmd/db.ts
  20. 2 2
      packages/opencode/src/cli/cmd/run.ts
  21. 1 1
      packages/opencode/src/cli/cmd/tui/worker.ts
  22. 2 1
      packages/opencode/src/index.ts
  23. 5 0
      packages/opencode/src/node.ts
  24. 1 1
      packages/opencode/src/plugin/index.ts
  25. 2 0
      packages/opencode/src/provider/models-snapshot.d.ts
  26. 2 0
      packages/opencode/src/provider/models-snapshot.js
  27. 8 5
      packages/opencode/src/server/instance.ts
  28. 11 8
      packages/opencode/src/server/proxy.ts
  29. 1 1
      packages/opencode/src/server/router.ts
  30. 9 11
      packages/opencode/src/server/routes/session.ts
  31. 6 3
      packages/opencode/src/server/server.ts
  32. 3 3
      packages/opencode/src/shell/shell.ts
  33. 10 10
      packages/opencode/src/storage/json-migration.ts
  34. 2 2
      packages/opencode/test/server/project-init-git.test.ts
  35. 2 2
      packages/opencode/test/server/session-actions.test.ts
  36. 4 4
      packages/opencode/test/server/session-messages.test.ts
  37. 3 3
      packages/opencode/test/server/session-select.test.ts
  38. 30 47
      packages/opencode/test/storage/json-migration.test.ts

+ 29 - 11
bun.lock

@@ -225,12 +225,6 @@
       "name": "@opencode-ai/desktop-electron",
       "version": "1.4.0",
       "dependencies": {
-        "@opencode-ai/app": "workspace:*",
-        "@opencode-ai/ui": "workspace:*",
-        "@solid-primitives/i18n": "2.2.1",
-        "@solid-primitives/storage": "catalog:",
-        "@solidjs/meta": "catalog:",
-        "@solidjs/router": "0.15.4",
         "effect": "catalog:",
         "electron-context-menu": "4.1.2",
         "electron-log": "^5",
@@ -238,19 +232,36 @@
         "electron-updater": "^6",
         "electron-window-state": "^5.0.3",
         "marked": "^15",
-        "solid-js": "catalog:",
-        "tree-kill": "^1.2.2",
       },
       "devDependencies": {
         "@actions/artifact": "4.0.0",
+        "@lydell/node-pty": "catalog:",
+        "@opencode-ai/app": "workspace:*",
+        "@opencode-ai/ui": "workspace:*",
+        "@solid-primitives/i18n": "2.2.1",
+        "@solid-primitives/storage": "catalog:",
+        "@solidjs/meta": "catalog:",
+        "@solidjs/router": "0.15.4",
         "@types/bun": "catalog:",
         "@types/node": "catalog:",
         "@typescript/native-preview": "catalog:",
+        "@valibot/to-json-schema": "1.6.0",
         "electron": "40.4.1",
         "electron-builder": "^26",
         "electron-vite": "^5",
+        "solid-js": "catalog:",
+        "sury": "11.0.0-alpha.4",
         "typescript": "~5.6.2",
         "vite": "catalog:",
+        "zod-openapi": "5.4.6",
+      },
+      "optionalDependencies": {
+        "@lydell/node-pty-darwin-arm64": "1.2.0-beta.10",
+        "@lydell/node-pty-darwin-x64": "1.2.0-beta.10",
+        "@lydell/node-pty-linux-arm64": "1.2.0-beta.10",
+        "@lydell/node-pty-linux-x64": "1.2.0-beta.10",
+        "@lydell/node-pty-win32-arm64": "1.2.0-beta.10",
+        "@lydell/node-pty-win32-x64": "1.2.0-beta.10",
       },
     },
     "packages/enterprise": {
@@ -336,7 +347,7 @@
         "@hono/node-ws": "1.3.0",
         "@hono/standard-validator": "0.1.5",
         "@hono/zod-validator": "catalog:",
-        "@lydell/node-pty": "1.2.0-beta.10",
+        "@lydell/node-pty": "catalog:",
         "@modelcontextprotocol/sdk": "1.27.1",
         "@npmcli/arborist": "9.4.0",
         "@octokit/graphql": "9.0.2",
@@ -633,6 +644,7 @@
     "@effect/platform-node": "4.0.0-beta.43",
     "@hono/zod-validator": "0.4.2",
     "@kobalte/core": "0.13.11",
+    "@lydell/node-pty": "1.2.0-beta.10",
     "@octokit/rest": "22.0.0",
     "@openauthjs/openauth": "0.0.0-20250322224806",
     "@pierre/diffs": "1.1.0-beta.18",
@@ -2313,6 +2325,8 @@
 
     "@ungap/structured-clone": ["@ungap/[email protected]", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
 
+    "@valibot/to-json-schema": ["@valibot/[email protected]", "", { "peerDependencies": { "valibot": "^1.3.0" } }, "sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A=="],
+
     "@vercel/oidc": ["@vercel/[email protected]", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
 
     "@vitejs/plugin-react": ["@vitejs/[email protected]", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
@@ -4577,6 +4591,8 @@
 
     "supports-preserve-symlinks-flag": ["[email protected]", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
 
+    "sury": ["[email protected]", "", { "peerDependencies": { "rescript": "12.x" }, "optionalPeers": ["rescript"] }, "sha512-oeG/GJWZvQCKtGPpLbu0yCZudfr5LxycDo5kh7SJmKHDPCsEPJssIZL2Eb4Tl7g9aPEvIDuRrkS+L0pybsMEMA=="],
+
     "system-architecture": ["[email protected]", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="],
 
     "tailwindcss": ["[email protected]", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
@@ -4655,8 +4671,6 @@
 
     "traverse": ["[email protected]", "", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="],
 
-    "tree-kill": ["[email protected]", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
-
     "tree-sitter-bash": ["[email protected]", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-gZtlj9+qFS81qKxpLfD6H0UssQ3QBc/F0nKkPsiFDyfQF2YBqYvglFJUzchrPpVhZe9kLZTrJ9n2J6lmka69Vg=="],
 
     "tree-sitter-powershell": ["[email protected]", "", { "dependencies": { "node-addon-api": "^7.1.0", "node-gyp-build": "^4.8.0" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-bEt8QoySpGFnU3aa8WedQyNMaN6aTwy/WUbvIVt0JSKF+BbJoSHNHu+wCbhj7xLMsfB0AuffmiJm+B8gzva8Lg=="],
@@ -4811,6 +4825,8 @@
 
     "uuid": ["[email protected]", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],
 
+    "valibot": ["[email protected]", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg=="],
+
     "validate-npm-package-name": ["[email protected]", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="],
 
     "vary": ["[email protected]", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
@@ -4967,6 +4983,8 @@
 
     "zod": ["[email protected]", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
 
+    "zod-openapi": ["[email protected]", "", { "peerDependencies": { "zod": "^3.25.74 || ^4.0.0" } }, "sha512-P2jsOOBAq/6hCwUsMCjUATZ8szkMsV5VAwZENfyxp2Hc/XPJQpVwAgevWZc65xZauCwWB9LAn7zYeiCJFAEL+A=="],
+
     "zod-to-json-schema": ["[email protected]", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
 
     "zod-to-ts": ["[email protected]", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="],

+ 2 - 1
package.json

@@ -71,7 +71,8 @@
       "@solidjs/router": "0.15.4",
       "@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
       "solid-js": "1.9.10",
-      "vite-plugin-solid": "2.11.10"
+      "vite-plugin-solid": "2.11.10",
+      "@lydell/node-pty": "1.2.0-beta.10"
     }
   },
   "devDependencies": {

+ 1 - 0
packages/app/src/components/terminal.tsx

@@ -521,6 +521,7 @@ export const Terminal = (props: TerminalProps) => {
         next.searchParams.set("cursor", String(seek))
         next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
         if (!sameOrigin && password) {
+          next.searchParams.set("auth_token", btoa(`${username}:${password}`))
           // For same-origin requests, let the browser reuse the page's existing auth.
           next.username = username
           next.password = password

+ 0 - 5
packages/desktop-electron/electron-builder.config.ts

@@ -34,11 +34,6 @@ const getBase = (): Configuration => ({
   },
   files: ["out/**/*", "resources/**/*"],
   extraResources: [
-    {
-      from: "resources/",
-      to: "",
-      filter: ["opencode-cli*"],
-    },
     {
       from: "native/",
       to: "native/",

+ 31 - 0
packages/desktop-electron/electron.vite.config.ts

@@ -1,5 +1,6 @@
 import { defineConfig } from "electron-vite"
 import appPlugin from "@opencode-ai/app/vite"
+import * as fs from "node:fs/promises"
 
 const channel = (() => {
   const raw = process.env.OPENCODE_CHANNEL
@@ -7,6 +8,10 @@ const channel = (() => {
   return "dev"
 })()
 
+const OPENCODE_SERVER_DIST = "../opencode/dist/node"
+
+const nodePtyPkg = `@lydell/node-pty-${process.platform}-${process.arch}`
+
 export default defineConfig({
   main: {
     define: {
@@ -16,7 +21,33 @@ export default defineConfig({
       rollupOptions: {
         input: { index: "src/main/index.ts" },
       },
+      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}`))
+          }
+        },
+      },
+    ],
   },
   preload: {
     build: {

+ 22 - 11
packages/desktop-electron/package.json

@@ -13,7 +13,7 @@
     "typecheck": "tsgo -b",
     "predev": "bun ./scripts/predev.ts",
     "dev": "electron-vite dev",
-    "prebuild": "bun ./scripts/copy-icons.ts",
+    "prebuild": "bun ./scripts/prebuild.ts",
     "build": "electron-vite build",
     "preview": "electron-vite preview",
     "package": "electron-builder --config electron-builder.config.ts",
@@ -24,31 +24,42 @@
   },
   "main": "./out/main/index.js",
   "dependencies": {
-    "@opencode-ai/app": "workspace:*",
-    "@opencode-ai/ui": "workspace:*",
-    "@solid-primitives/i18n": "2.2.1",
-    "@solid-primitives/storage": "catalog:",
-    "@solidjs/meta": "catalog:",
-    "@solidjs/router": "0.15.4",
     "effect": "catalog:",
     "electron-context-menu": "4.1.2",
     "electron-log": "^5",
     "electron-store": "^10",
     "electron-updater": "^6",
     "electron-window-state": "^5.0.3",
-    "marked": "^15",
-    "solid-js": "catalog:",
-    "tree-kill": "^1.2.2"
+    "marked": "^15"
   },
   "devDependencies": {
     "@actions/artifact": "4.0.0",
+    "@lydell/node-pty": "catalog:",
+    "@opencode-ai/app": "workspace:*",
+    "@opencode-ai/ui": "workspace:*",
+    "@solid-primitives/i18n": "2.2.1",
+    "@solid-primitives/storage": "catalog:",
+    "@solidjs/meta": "catalog:",
+    "@solidjs/router": "0.15.4",
     "@types/bun": "catalog:",
     "@types/node": "catalog:",
     "@typescript/native-preview": "catalog:",
+    "@valibot/to-json-schema": "1.6.0",
     "electron": "40.4.1",
     "electron-builder": "^26",
     "electron-vite": "^5",
+    "solid-js": "catalog:",
+    "sury": "11.0.0-alpha.4",
     "typescript": "~5.6.2",
-    "vite": "catalog:"
+    "vite": "catalog:",
+    "zod-openapi": "5.4.6"
+  },
+  "optionalDependencies": {
+    "@lydell/node-pty-darwin-arm64": "1.2.0-beta.10",
+    "@lydell/node-pty-darwin-x64": "1.2.0-beta.10",
+    "@lydell/node-pty-linux-arm64": "1.2.0-beta.10",
+    "@lydell/node-pty-linux-x64": "1.2.0-beta.10",
+    "@lydell/node-pty-win32-arm64": "1.2.0-beta.10",
+    "@lydell/node-pty-win32-x64": "1.2.0-beta.10"
   }
 }

+ 9 - 0
packages/desktop-electron/scripts/prebuild.ts

@@ -0,0 +1,9 @@
+#!/usr/bin/env bun
+import { $ } from "bun"
+
+import { resolveChannel } from "./utils"
+
+const channel = resolveChannel()
+await $`bun ./scripts/copy-icons.ts ${channel}`
+
+await $`cd ../opencode && bun script/build-node.ts`

+ 1 - 13
packages/desktop-electron/scripts/predev.ts

@@ -1,17 +1,5 @@
 import { $ } from "bun"
 
-import { copyBinaryToSidecarFolder, getCurrentSidecar, windowsify } from "./utils"
-
 await $`bun ./scripts/copy-icons.ts ${process.env.OPENCODE_CHANNEL ?? "dev"}`
 
-const RUST_TARGET = Bun.env.RUST_TARGET
-
-const sidecarConfig = getCurrentSidecar(RUST_TARGET)
-
-const binaryPath = windowsify(`../opencode/dist/${sidecarConfig.ocBinary}/bin/opencode`)
-
-await (sidecarConfig.ocBinary.includes("-baseline")
-  ? $`cd ../opencode && bun run build --single --baseline`
-  : $`cd ../opencode && bun run build --single`)
-
-await copyBinaryToSidecarFolder(binaryPath, RUST_TARGET)
+await $`cd ../opencode && bun script/build-node.ts`

+ 1 - 17
packages/desktop-electron/scripts/prepare.ts

@@ -1,25 +1,9 @@
 #!/usr/bin/env bun
-import { $ } from "bun"
-
 import { Script } from "@opencode-ai/script"
-import { copyBinaryToSidecarFolder, getCurrentSidecar, resolveChannel, windowsify } from "./utils"
 
-const channel = resolveChannel()
-await $`bun ./scripts/copy-icons.ts ${channel}`
+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")
 console.log(`Updated package.json version to ${Script.version}`)
-
-const sidecarConfig = getCurrentSidecar()
-const artifact = process.env.OPENCODE_CLI_ARTIFACT ?? "opencode-cli"
-
-const dir = "resources/opencode-binaries"
-
-await $`mkdir -p ${dir}`
-await $`gh run download ${process.env.GITHUB_RUN_ID} -n ${artifact}`.cwd(dir)
-
-await copyBinaryToSidecarFolder(windowsify(`${dir}/${sidecarConfig.ocBinary}/bin/opencode`))
-
-await $`rm -rf ${dir}`

+ 0 - 283
packages/desktop-electron/src/main/cli.ts

@@ -1,283 +0,0 @@
-import { execFileSync, spawn } from "node:child_process"
-import { EventEmitter } from "node:events"
-import { chmodSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"
-import { tmpdir } from "node:os"
-import { dirname, join } from "node:path"
-import readline from "node:readline"
-import { fileURLToPath } from "node:url"
-import { app } from "electron"
-import treeKill from "tree-kill"
-
-import { WSL_ENABLED_KEY } from "./constants"
-import { getUserShell, loadShellEnv, mergeShellEnv } from "./shell-env"
-import { store } from "./store"
-
-const CLI_INSTALL_DIR = ".opencode/bin"
-const CLI_BINARY_NAME = "opencode"
-
-export type ServerConfig = {
-  hostname?: string
-  port?: number
-}
-
-export type Config = {
-  server?: ServerConfig
-}
-
-export type TerminatedPayload = { code: number | null; signal: number | null }
-
-export type CommandEvent =
-  | { type: "stdout"; value: string }
-  | { type: "stderr"; value: string }
-  | { type: "error"; value: string }
-  | { type: "terminated"; value: TerminatedPayload }
-  | { type: "sqlite"; value: SqliteMigrationProgress }
-
-export type SqliteMigrationProgress = { type: "InProgress"; value: number } | { type: "Done" }
-
-export type CommandChild = {
-  pid: number | undefined
-  kill: () => void
-}
-
-const root = dirname(fileURLToPath(import.meta.url))
-
-export function getSidecarPath() {
-  const suffix = process.platform === "win32" ? ".exe" : ""
-  const path = app.isPackaged
-    ? join(process.resourcesPath, `opencode-cli${suffix}`)
-    : join(root, "../../resources", `opencode-cli${suffix}`)
-  console.log(`[cli] Sidecar path resolved: ${path} (isPackaged: ${app.isPackaged})`)
-  return path
-}
-
-export async function getConfig(): Promise<Config | null> {
-  const { events } = spawnCommand("debug config", {})
-  let output = ""
-
-  await new Promise<void>((resolve) => {
-    events.on("stdout", (line: string) => {
-      output += line
-    })
-    events.on("stderr", (line: string) => {
-      output += line
-    })
-    events.on("terminated", () => resolve())
-    events.on("error", () => resolve())
-  })
-
-  try {
-    return JSON.parse(output) as Config
-  } catch {
-    return null
-  }
-}
-
-export async function installCli(): Promise<string> {
-  if (process.platform === "win32") {
-    throw new Error("CLI installation is only supported on macOS & Linux")
-  }
-
-  const sidecar = getSidecarPath()
-  const scriptPath = join(app.getAppPath(), "install")
-  const script = readFileSync(scriptPath, "utf8")
-  const tempScript = join(tmpdir(), "opencode-install.sh")
-
-  writeFileSync(tempScript, script, "utf8")
-  chmodSync(tempScript, 0o755)
-
-  const cmd = spawn(tempScript, ["--binary", sidecar], { stdio: "pipe" })
-  return await new Promise<string>((resolve, reject) => {
-    cmd.on("exit", (code: number | null) => {
-      try {
-        unlinkSync(tempScript)
-      } catch {}
-      if (code === 0) {
-        const installPath = getCliInstallPath()
-        if (installPath) return resolve(installPath)
-        return reject(new Error("Could not determine install path"))
-      }
-      reject(new Error("Install script failed"))
-    })
-  })
-}
-
-export function syncCli() {
-  if (!app.isPackaged) return
-  const installPath = getCliInstallPath()
-  if (!installPath) return
-
-  let version = ""
-  try {
-    version = execFileSync(installPath, ["--version"], { windowsHide: true }).toString().trim()
-  } catch {
-    return
-  }
-
-  const cli = parseVersion(version)
-  const appVersion = parseVersion(app.getVersion())
-  if (!cli || !appVersion) return
-  if (compareVersions(cli, appVersion) >= 0) return
-  void installCli().catch(() => undefined)
-}
-
-export function serve(hostname: string, port: number, password: string) {
-  const args = `--print-logs --log-level WARN serve --hostname ${hostname} --port ${port}`
-  const env = {
-    OPENCODE_SERVER_USERNAME: "opencode",
-    OPENCODE_SERVER_PASSWORD: password,
-  }
-
-  return spawnCommand(args, env)
-}
-
-export function spawnCommand(args: string, extraEnv: Record<string, string>) {
-  console.log(`[cli] Spawning command with args: ${args}`)
-  const base = Object.fromEntries(
-    Object.entries(process.env).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
-  )
-  const env = {
-    ...base,
-    OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true",
-    OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
-    OPENCODE_CLIENT: "desktop",
-    XDG_STATE_HOME: app.getPath("userData"),
-    ...extraEnv,
-  }
-  const shell = process.platform === "win32" ? null : getUserShell()
-  const envs = shell ? mergeShellEnv(loadShellEnv(shell), env) : env
-
-  const { cmd, cmdArgs } = buildCommand(args, envs, shell)
-  console.log(`[cli] Executing: ${cmd} ${cmdArgs.join(" ")}`)
-  const child = spawn(cmd, cmdArgs, {
-    env: envs,
-    detached: process.platform !== "win32",
-    windowsHide: true,
-    stdio: ["ignore", "pipe", "pipe"],
-  })
-  console.log(`[cli] Spawned process with PID: ${child.pid}`)
-
-  const events = new EventEmitter()
-  const exit = new Promise<TerminatedPayload>((resolve) => {
-    child.on("exit", (code: number | null, signal: NodeJS.Signals | null) => {
-      console.log(`[cli] Process exited with code: ${code}, signal: ${signal}`)
-      resolve({ code: code ?? null, signal: null })
-    })
-    child.on("error", (error: Error) => {
-      console.error(`[cli] Process error: ${error.message}`)
-      events.emit("error", error.message)
-    })
-  })
-
-  const stdout = child.stdout
-  const stderr = child.stderr
-
-  if (stdout) {
-    readline.createInterface({ input: stdout }).on("line", (line: string) => {
-      if (handleSqliteProgress(events, line)) return
-      events.emit("stdout", `${line}\n`)
-    })
-  }
-
-  if (stderr) {
-    readline.createInterface({ input: stderr }).on("line", (line: string) => {
-      if (handleSqliteProgress(events, line)) return
-      events.emit("stderr", `${line}\n`)
-    })
-  }
-
-  exit.then((payload) => {
-    events.emit("terminated", payload)
-  })
-
-  const kill = () => {
-    if (!child.pid) return
-    treeKill(child.pid)
-  }
-
-  return { events, child: { pid: child.pid, kill }, exit }
-}
-
-function handleSqliteProgress(events: EventEmitter, line: string) {
-  const stripped = line.startsWith("sqlite-migration:") ? line.slice("sqlite-migration:".length).trim() : null
-  if (!stripped) return false
-  if (stripped === "done") {
-    events.emit("sqlite", { type: "Done" })
-    return true
-  }
-  const value = Number.parseInt(stripped, 10)
-  if (!Number.isNaN(value)) {
-    events.emit("sqlite", { type: "InProgress", value })
-    return true
-  }
-  return false
-}
-
-function buildCommand(args: string, env: Record<string, string>, shell: string | null) {
-  if (process.platform === "win32" && isWslEnabled()) {
-    console.log(`[cli] Using WSL mode`)
-    const version = app.getVersion()
-    const script = [
-      "set -e",
-      'BIN="$HOME/.opencode/bin/opencode"',
-      'if [ ! -x "$BIN" ]; then',
-      `  curl -fsSL https://opencode.ai/install | bash -s -- --version ${shellEscape(version)} --no-modify-path`,
-      "fi",
-      `${envPrefix(env)} exec "$BIN" ${args}`,
-    ].join("\n")
-
-    return { cmd: "wsl", cmdArgs: ["-e", "bash", "-lc", script] }
-  }
-
-  if (process.platform === "win32") {
-    const sidecar = getSidecarPath()
-    console.log(`[cli] Windows direct mode, sidecar: ${sidecar}`)
-    return { cmd: sidecar, cmdArgs: args.split(" ") }
-  }
-
-  const sidecar = getSidecarPath()
-  const user = shell || getUserShell()
-  const line = user.endsWith("/nu") ? `^\"${sidecar}\" ${args}` : `\"${sidecar}\" ${args}`
-  console.log(`[cli] Unix mode, shell: ${user}, command: ${line}`)
-  return { cmd: user, cmdArgs: ["-l", "-c", line] }
-}
-
-function envPrefix(env: Record<string, string>) {
-  const entries = Object.entries(env).map(([key, value]) => `${key}=${shellEscape(value)}`)
-  return entries.join(" ")
-}
-
-function shellEscape(input: string) {
-  if (!input) return "''"
-  return `'${input.replace(/'/g, `'"'"'`)}'`
-}
-
-function getCliInstallPath() {
-  const home = process.env.HOME
-  if (!home) return null
-  return join(home, CLI_INSTALL_DIR, CLI_BINARY_NAME)
-}
-
-function isWslEnabled() {
-  return store.get(WSL_ENABLED_KEY) === true
-}
-
-function parseVersion(value: string) {
-  const parts = value
-    .replace(/^v/, "")
-    .split(".")
-    .map((part) => Number.parseInt(part, 10))
-  if (parts.some((part) => Number.isNaN(part))) return null
-  return parts
-}
-
-function compareVersions(a: number[], b: number[]) {
-  const len = Math.max(a.length, b.length)
-  for (let i = 0; i < len; i += 1) {
-    const left = a[i] ?? 0
-    const right = b[i] ?? 0
-    if (left > right) return 1
-    if (left < right) return -1
-  }
-  return 0
-}

+ 22 - 0
packages/desktop-electron/src/main/env.d.ts

@@ -5,3 +5,25 @@ 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 namespace Log {
+    export const init: typeof import("../../../opencode/dist/types/src/node").Log.init
+  }
+  export namespace Database {
+    export const Path: typeof import("../../../opencode/dist/types/src/node").Database.Path
+    export const Client: typeof import("../../../opencode/dist/types/src/node").Database.Client
+  }
+  export namespace JsonMigration {
+    export type Progress = import("../../../opencode/dist/types/src/node").JsonMigration.Progress
+    export const run: typeof import("../../../opencode/dist/types/src/node").JsonMigration.run
+  }
+  export const bootstrap: typeof import("../../../opencode/dist/types/src/node").bootstrap
+}

+ 10 - 22
packages/desktop-electron/src/main/index.ts

@@ -11,6 +11,8 @@ import pkg from "electron-updater"
 import contextMenu from "electron-context-menu"
 contextMenu({ showSaveImageAs: true, showLookUpSelection: false, showSearchWithGoogle: false })
 
+process.env.OPENCODE_DISABLE_EMBEDDED_WEB_UI = "true"
+
 const APP_NAMES: Record<string, string> = {
   dev: "OpenCode Dev",
   beta: "OpenCode Beta",
@@ -27,8 +29,6 @@ const { autoUpdater } = pkg
 
 import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
 import { checkAppExists, resolveAppPath, wslPath } from "./apps"
-import type { CommandChild } from "./cli"
-import { installCli, syncCli } from "./cli"
 import { CHANNEL, UPDATER_ENABLED } from "./constants"
 import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigrationProgress } from "./ipc"
 import { initLogging } from "./logging"
@@ -36,12 +36,13 @@ import { parseMarkdown } from "./markdown"
 import { createMenu } from "./menu"
 import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server"
 import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows"
+import type { Server } from "virtual:opencode-server"
 
 const initEmitter = new EventEmitter()
 let initStep: InitStep = { phase: "server_waiting" }
 
 let mainWindow: BrowserWindow | null = null
-let sidecar: CommandChild | null = null
+let server: Server.Listener | null = null
 const loadingComplete = defer<void>()
 
 const pendingDeepLinks: string[] = []
@@ -96,11 +97,9 @@ function setupApp() {
   }
 
   void app.whenReady().then(async () => {
-    // migrate()
     app.setAsDefaultProtocolClient("opencode")
     setDockIcon()
     setupAutoUpdater()
-    syncCli()
     await initialize()
   })
 }
@@ -134,8 +133,8 @@ async function initialize() {
   const password = randomUUID()
 
   logger.log("spawning sidecar", { url })
-  const { child, health, events } = spawnLocalServer(hostname, port, password)
-  sidecar = child
+  const { listener, health } = await spawnLocalServer(hostname, port, password)
+  server = listener
   serverReady.resolve({
     url,
     username: "opencode",
@@ -145,7 +144,7 @@ async function initialize() {
   const loadingTask = (async () => {
     logger.log("sidecar connection started", { url })
 
-    events.on("sqlite", (progress: SqliteMigrationProgress) => {
+    initEmitter.on("sqlite", (progress: SqliteMigrationProgress) => {
       setInitStep({ phase: "sqlite_waiting" })
       if (overlay) sendSqliteMigrationProgress(overlay, progress)
       if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress)
@@ -198,9 +197,6 @@ function wireMenu() {
   if (!mainWindow) return
   createMenu({
     trigger: (id) => mainWindow && sendMenuCommand(mainWindow, id),
-    installCli: () => {
-      void installCli()
-    },
     checkForUpdates: () => {
       void checkForUpdates(true)
     },
@@ -215,7 +211,6 @@ function wireMenu() {
 
 registerIpcHandlers({
   killSidecar: () => killSidecar(),
-  installCli: async () => installCli(),
   awaitInitialization: async (sendStep) => {
     sendStep(initStep)
     const listener = (step: InitStep) => sendStep(step)
@@ -247,16 +242,9 @@ registerIpcHandlers({
 })
 
 function killSidecar() {
-  if (!sidecar) return
-  const pid = sidecar.pid
-  sidecar.kill()
-  sidecar = null
-  // tree-kill is async; also send process group signal as immediate fallback
-  if (pid && process.platform !== "win32") {
-    try {
-      process.kill(-pid, "SIGTERM")
-    } catch {}
-  }
+  if (!server) return
+  server.stop()
+  server = null
 }
 
 function ensureLoopbackNoProxy() {

+ 0 - 2
packages/desktop-electron/src/main/ipc.ts

@@ -13,7 +13,6 @@ const pickerFilters = (ext?: string[]) => {
 
 type Deps = {
   killSidecar: () => void
-  installCli: () => Promise<string>
   awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData>
   getDefaultServerUrl: () => Promise<string | null> | string | null
   setDefaultServerUrl: (url: string | null) => Promise<void> | void
@@ -34,7 +33,6 @@ type Deps = {
 
 export function registerIpcHandlers(deps: Deps) {
   ipcMain.handle("kill-sidecar", () => deps.killSidecar())
-  ipcMain.handle("install-cli", () => deps.installCli())
   ipcMain.handle("await-initialization", (event: IpcMainInvokeEvent) => {
     const send = (step: InitStep) => event.sender.send("init-step", step)
     return deps.awaitInitialization(send)

+ 0 - 5
packages/desktop-electron/src/main/menu.ts

@@ -5,7 +5,6 @@ import { createMainWindow } from "./windows"
 
 type Deps = {
   trigger: (id: string) => void
-  installCli: () => void
   checkForUpdates: () => void
   reload: () => void
   relaunch: () => void
@@ -24,10 +23,6 @@ export function createMenu(deps: Deps) {
           enabled: UPDATER_ENABLED,
           click: () => deps.checkForUpdates(),
         },
-        {
-          label: "Install CLI...",
-          click: () => deps.installCli(),
-        },
         {
           label: "Reload Webview",
           click: () => deps.reload(),

+ 30 - 16
packages/desktop-electron/src/main/server.ts

@@ -1,5 +1,6 @@
-import { serve, type CommandChild } from "./cli"
+import { app } from "electron"
 import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants"
+import { getUserShell, loadShellEnv } from "./shell-env"
 import { store } from "./store"
 
 export type WslConfig = { enabled: boolean }
@@ -29,8 +30,16 @@ export function setWslConfig(config: WslConfig) {
   store.set(WSL_ENABLED_KEY, config.enabled)
 }
 
-export function spawnLocalServer(hostname: string, port: number, password: string) {
-  const { child, exit, events } = serve(hostname, port, password)
+export async function spawnLocalServer(hostname: string, port: number, password: string) {
+  prepareServerEnv(password)
+  const { Log, Server } = await import("virtual:opencode-server")
+  await Log.init({ level: "WARN" })
+  const listener = await Server.listen({
+    port,
+    hostname,
+    username: "opencode",
+    password,
+  })
 
   const wait = (async () => {
     const url = `http://${hostname}:${port}`
@@ -42,19 +51,26 @@ export function spawnLocalServer(hostname: string, port: number, password: strin
       }
     }
 
-    const terminated = async () => {
-      const payload = await exit
-      throw new Error(
-        `Sidecar terminated before becoming healthy (code=${payload.code ?? "unknown"} signal=${
-          payload.signal ?? "unknown"
-        })`,
-      )
-    }
-
-    await Promise.race([ready(), terminated()])
+    await ready()
   })()
 
-  return { child, health: { wait }, events }
+  return { listener, health: { wait } }
+}
+
+function prepareServerEnv(password: string) {
+  const shell = process.platform === "win32" ? null : getUserShell()
+  const shellEnv = shell ? (loadShellEnv(shell) ?? {}) : {}
+  const env = {
+    ...process.env,
+    ...shellEnv,
+    OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true",
+    OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
+    OPENCODE_CLIENT: "desktop",
+    OPENCODE_SERVER_USERNAME: "opencode",
+    OPENCODE_SERVER_PASSWORD: password,
+    XDG_STATE_HOME: app.getPath("userData"),
+  }
+  Object.assign(process.env, env)
 }
 
 export async function checkHealth(url: string, password?: string | null): Promise<boolean> {
@@ -82,5 +98,3 @@ export async function checkHealth(url: string, password?: string | null): Promis
     return false
   }
 }
-
-export type { CommandChild }

+ 13 - 13
packages/desktop-electron/src/main/shell-env.ts

@@ -1,7 +1,7 @@
 import { spawnSync } from "node:child_process"
 import { basename } from "node:path"
 
-const SHELL_ENV_TIMEOUT = 5_000
+const TIMEOUT = 5_000
 
 type Probe = { type: "Loaded"; value: Record<string, string> } | { type: "Timeout" } | { type: "Unavailable" }
 
@@ -20,28 +20,28 @@ export function parseShellEnv(out: Buffer) {
   return env
 }
 
-function probeShellEnv(shell: string, mode: "-il" | "-l"): Probe {
+function probe(shell: string, mode: "-il" | "-l"): Probe {
   const out = spawnSync(shell, [mode, "-c", "env -0"], {
     stdio: ["ignore", "pipe", "ignore"],
-    timeout: SHELL_ENV_TIMEOUT,
+    timeout: TIMEOUT,
     windowsHide: true,
   })
 
   const err = out.error as NodeJS.ErrnoException | undefined
   if (err) {
     if (err.code === "ETIMEDOUT") return { type: "Timeout" }
-    console.log(`[cli] Shell env probe failed for ${shell} ${mode}: ${err.message}`)
+    console.log(`[server] Shell env probe failed for ${shell} ${mode}: ${err.message}`)
     return { type: "Unavailable" }
   }
 
   if (out.status !== 0) {
-    console.log(`[cli] Shell env probe exited with non-zero status for ${shell} ${mode}`)
+    console.log(`[server] Shell env probe exited with non-zero status for ${shell} ${mode}`)
     return { type: "Unavailable" }
   }
 
   const env = parseShellEnv(out.stdout)
   if (Object.keys(env).length === 0) {
-    console.log(`[cli] Shell env probe returned empty env for ${shell} ${mode}`)
+    console.log(`[server] Shell env probe returned empty env for ${shell} ${mode}`)
     return { type: "Unavailable" }
   }
 
@@ -56,27 +56,27 @@ export function isNushell(shell: string) {
 
 export function loadShellEnv(shell: string) {
   if (isNushell(shell)) {
-    console.log(`[cli] Skipping shell env probe for nushell: ${shell}`)
+    console.log(`[server] Skipping shell env probe for nushell: ${shell}`)
     return null
   }
 
-  const interactive = probeShellEnv(shell, "-il")
+  const interactive = probe(shell, "-il")
   if (interactive.type === "Loaded") {
-    console.log(`[cli] Loaded shell environment with -il (${Object.keys(interactive.value).length} vars)`)
+    console.log(`[server] Loaded shell environment with -il (${Object.keys(interactive.value).length} vars)`)
     return interactive.value
   }
   if (interactive.type === "Timeout") {
-    console.warn(`[cli] Interactive shell env probe timed out: ${shell}`)
+    console.warn(`[server] Interactive shell env probe timed out: ${shell}`)
     return null
   }
 
-  const login = probeShellEnv(shell, "-l")
+  const login = probe(shell, "-l")
   if (login.type === "Loaded") {
-    console.log(`[cli] Loaded shell environment with -l (${Object.keys(login.value).length} vars)`)
+    console.log(`[server] Loaded shell environment with -l (${Object.keys(login.value).length} vars)`)
     return login.value
   }
 
-  console.warn(`[cli] Falling back to app environment: ${shell}`)
+  console.warn(`[server] Falling back to app environment: ${shell}`)
   return null
 }
 

+ 1 - 1
packages/opencode/package.json

@@ -106,7 +106,7 @@
     "@hono/node-ws": "1.3.0",
     "@hono/standard-validator": "0.1.5",
     "@hono/zod-validator": "catalog:",
-    "@lydell/node-pty": "1.2.0-beta.10",
+    "@lydell/node-pty": "catalog:",
     "@modelcontextprotocol/sdk": "1.27.1",
     "@npmcli/arborist": "9.4.0",
     "@octokit/graphql": "9.0.2",

+ 5 - 16
packages/opencode/script/build-node.ts

@@ -1,6 +1,5 @@
 #!/usr/bin/env bun
 
-import { $ } from "bun"
 import { Script } from "@opencode-ai/script"
 import fs from "fs"
 import path from "path"
@@ -9,15 +8,6 @@ import { fileURLToPath } from "url"
 const __filename = fileURLToPath(import.meta.url)
 const __dirname = path.dirname(__filename)
 const dir = path.resolve(__dirname, "..")
-const root = path.resolve(dir, "../..")
-
-function linker(): "hoisted" | "isolated" {
-  // jsonc-parser is only declared in packages/opencode, so its install location
-  // tells us whether Bun used a hoisted or isolated workspace layout.
-  if (fs.existsSync(path.join(dir, "node_modules", "jsonc-parser"))) return "isolated"
-  if (fs.existsSync(path.join(root, "node_modules", "jsonc-parser"))) return "hoisted"
-  throw new Error("Could not detect Bun linker from jsonc-parser")
-}
 
 process.chdir(dir)
 
@@ -51,21 +41,20 @@ const migrations = await Promise.all(
 )
 console.log(`Loaded ${migrations.length} migrations`)
 
-const link = linker()
-
-await $`bun install --linker=${link} --os="*" --cpu="*" @lydell/[email protected]`
-
 await Bun.build({
   target: "node",
   entrypoints: ["./src/node.ts"],
-  outdir: "./dist",
+  outdir: "./dist/node",
   format: "esm",
   sourcemap: "linked",
-  external: ["jsonc-parser"],
+  external: ["jsonc-parser", "@lydell/node-pty"],
   define: {
     OPENCODE_MIGRATIONS: JSON.stringify(migrations),
     OPENCODE_CHANNEL: `'${Script.channel}'`,
   },
+  files: {
+    "opencode-web-ui.gen.ts": "",
+  },
 })
 
 console.log("Build complete")

+ 2 - 1
packages/opencode/src/cli/cmd/db.ts

@@ -1,6 +1,7 @@
 import type { Argv } from "yargs"
 import { spawn } from "child_process"
 import { Database } from "../../storage/db"
+import { drizzle } from "drizzle-orm/bun-sqlite"
 import { Database as BunDatabase } from "bun:sqlite"
 import { UI } from "../ui"
 import { cmd } from "./cmd"
@@ -74,7 +75,7 @@ const MigrateCommand = cmd({
     let last = -1
     if (tty) process.stderr.write("\x1b[?25l")
     try {
-      const stats = await JsonMigration.run(sqlite, {
+      const stats = await JsonMigration.run(drizzle({ client: sqlite }), {
         progress: (event) => {
           const percent = Math.floor((event.current / event.total) * 100)
           if (percent === last) return

+ 2 - 2
packages/opencode/src/cli/cmd/run.ts

@@ -7,7 +7,7 @@ import { Flag } from "../../flag/flag"
 import { bootstrap } from "../bootstrap"
 import { EOL } from "os"
 import { Filesystem } from "../../util/filesystem"
-import { createOpencodeClient, type Message, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
+import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
 import { Server } from "../../server/server"
 import { Provider } from "../../provider/provider"
 import { Agent } from "../../agent/agent"
@@ -680,7 +680,7 @@ export const RunCommand = cmd({
     await bootstrap(process.cwd(), async () => {
       const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
         const request = new Request(input, init)
-        return Server.Default().fetch(request)
+        return Server.Default().app.fetch(request)
       }) as typeof globalThis.fetch
       const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn })
       await execute(sdk)

+ 1 - 1
packages/opencode/src/cli/cmd/tui/worker.ts

@@ -138,7 +138,7 @@ export const rpc = {
       headers,
       body: input.body,
     })
-    const response = await Server.Default().fetch(request)
+    const response = await Server.Default().app.fetch(request)
     const body = await response.text()
     return {
       status: response.status,

+ 2 - 1
packages/opencode/src/index.ts

@@ -36,6 +36,7 @@ import { Database } from "./storage/db"
 import { errorMessage } from "./util/error"
 import { PluginCommand } from "./cli/cmd/plug"
 import { Heap } from "./cli/heap"
+import { drizzle } from "drizzle-orm/bun-sqlite"
 
 process.on("unhandledRejection", (e) => {
   Log.Default.error("rejection", {
@@ -119,7 +120,7 @@ const cli = yargs(args)
       let last = -1
       if (tty) process.stderr.write("\x1b[?25l")
       try {
-        await JsonMigration.run(Database.Client().$client, {
+        await JsonMigration.run(drizzle({ client: Database.Client().$client }), {
           progress: (event) => {
             const percent = Math.floor((event.current / event.total) * 100)
             if (percent === last && event.current !== event.total) return

+ 5 - 0
packages/opencode/src/node.ts

@@ -1 +1,6 @@
+export { Config } from "./config/config"
 export { Server } from "./server/server"
+export { bootstrap } from "./cli/bootstrap"
+export { Log } from "./util/log"
+export { Database } from "./storage/db"
+export { JsonMigration } from "./storage/json-migration"

+ 1 - 1
packages/opencode/src/plugin/index.ts

@@ -119,7 +119,7 @@ export namespace Plugin {
                   Authorization: `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${Flag.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`,
                 }
               : undefined,
-            fetch: async (...args) => Server.Default().fetch(...args),
+            fetch: async (...args) => Server.Default().app.fetch(...args),
           })
           const cfg = yield* config.get()
           const input: PluginInput = {

+ 2 - 0
packages/opencode/src/provider/models-snapshot.d.ts

@@ -0,0 +1,2 @@
+// Auto-generated by build.ts - do not edit
+export declare const snapshot: Record<string, unknown>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 0
packages/opencode/src/provider/models-snapshot.js


+ 8 - 5
packages/opencode/src/server/instance.ts

@@ -4,6 +4,7 @@ import { proxy } from "hono/proxy"
 import type { UpgradeWebSocket } from "hono/ws"
 import z from "zod"
 import { createHash } from "node:crypto"
+import * as fs from "node:fs/promises"
 import { Log } from "../util/log"
 import { Format } from "../format"
 import { TuiRoutes } from "./routes/tui"
@@ -28,6 +29,7 @@ import { ExperimentalRoutes } from "./routes/experimental"
 import { ProviderRoutes } from "./routes/provider"
 import { EventRoutes } from "./routes/event"
 import { errorHandler } from "./middleware"
+import { getMimeType } from "hono/utils/mime"
 
 const log = Log.create({ service: "server" })
 
@@ -285,13 +287,14 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket, app: Hono = new Hono()
       if (embeddedWebUI) {
         const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
         if (!match) return c.json({ error: "Not Found" }, 404)
-        const file = Bun.file(match)
-        if (await file.exists()) {
-          c.header("Content-Type", file.type)
-          if (file.type.startsWith("text/html")) {
+
+        if (await fs.exists(match)) {
+          const mime = getMimeType(match) ?? "text/plain"
+          c.header("Content-Type", mime)
+          if (mime.startsWith("text/html")) {
             c.header("Content-Security-Policy", DEFAULT_CSP)
           }
-          return c.body(await file.arrayBuffer())
+          return c.body(new Uint8Array(await fs.readFile(match)))
         } else {
           return c.json({ error: "Not Found" }, 404)
         }

+ 11 - 8
packages/opencode/src/server/proxy.ts

@@ -1,7 +1,6 @@
 import type { Target } from "@/control-plane/types"
-import { lazy } from "@/util/lazy"
 import { Hono } from "hono"
-import { upgradeWebSocket } from "hono/bun"
+import type { UpgradeWebSocket } from "hono/ws"
 
 const hop = new Set([
   "connection",
@@ -53,10 +52,10 @@ function send(ws: { send(data: string | ArrayBuffer | Uint8Array): void }, data:
   return ws.send(data)
 }
 
-const app = lazy(() =>
+const app = (upgrade: UpgradeWebSocket) =>
   new Hono().get(
     "/__workspace_ws",
-    upgradeWebSocket((c) => {
+    upgrade((c) => {
       const url = c.req.header("x-opencode-proxy-url")
       const queue: Msg[] = []
       let remote: WebSocket | undefined
@@ -96,8 +95,7 @@ const app = lazy(() =>
         },
       }
     }),
-  ),
-)
+  )
 
 export namespace ServerProxy {
   export function http(target: Extract<Target, { type: "remote" }>, req: Request) {
@@ -112,13 +110,18 @@ export namespace ServerProxy {
     )
   }
 
-  export function websocket(target: Extract<Target, { type: "remote" }>, req: Request, env: unknown) {
+  export function websocket(
+    upgrade: UpgradeWebSocket,
+    target: Extract<Target, { type: "remote" }>,
+    req: Request,
+    env: unknown,
+  ) {
     const url = new URL(req.url)
     url.pathname = "/__workspace_ws"
     url.search = ""
     const next = new Headers(req.headers)
     next.set("x-opencode-proxy-url", socket(target.url))
-    return app().fetch(
+    return app(upgrade).fetch(
       new Request(url, {
         method: req.method,
         headers: next,

+ 1 - 1
packages/opencode/src/server/router.ts

@@ -89,7 +89,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
     }
 
     if (c.req.header("upgrade")?.toLowerCase() === "websocket") {
-      return ServerProxy.websocket(target, c.req.raw, c.env)
+      return ServerProxy.websocket(upgrade, target, c.req.raw, c.env)
     }
 
     const headers = new Headers(c.req.raw.headers)

+ 9 - 11
packages/opencode/src/server/routes/session.ts

@@ -843,19 +843,17 @@ export const SessionRoutes = lazy(() =>
       ),
       validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
       async (c) => {
-        c.status(204)
-        c.header("Content-Type", "application/json")
-        return stream(c, async () => {
-          const sessionID = c.req.valid("param").sessionID
-          const body = c.req.valid("json")
-          SessionPrompt.prompt({ ...body, sessionID }).catch((err) => {
-            log.error("prompt_async failed", { sessionID, error: err })
-            Bus.publish(Session.Event.Error, {
-              sessionID,
-              error: new NamedError.Unknown({ message: err instanceof Error ? err.message : String(err) }).toObject(),
-            })
+        const sessionID = c.req.valid("param").sessionID
+        const body = c.req.valid("json")
+        SessionPrompt.prompt({ ...body, sessionID }).catch((err) => {
+          log.error("prompt_async failed", { sessionID, error: err })
+          Bus.publish(Session.Event.Error, {
+            sessionID,
+            error: new NamedError.Unknown({ message: err instanceof Error ? err.message : String(err) }).toObject(),
           })
         })
+
+        return c.body(null, 204)
       },
     )
     .post(

+ 6 - 3
packages/opencode/src/server/server.ts

@@ -2,6 +2,7 @@ import { Log } from "../util/log"
 import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
 import { Hono } from "hono"
 import { compress } from "hono/compress"
+import { createNodeWebSocket } from "@hono/node-ws"
 import { cors } from "hono/cors"
 import { basicAuth } from "hono/basic-auth"
 import type { UpgradeWebSocket } from "hono/ws"
@@ -9,8 +10,6 @@ import z from "zod"
 import { Auth } from "../auth"
 import { Flag } from "../flag/flag"
 import { ProviderID } from "../provider/schema"
-import { createAdaptorServer, type ServerType } from "@hono/node-server"
-import { createNodeWebSocket } from "@hono/node-ws"
 import { WorkspaceRouterMiddleware } from "./router"
 import { errors } from "./error"
 import { GlobalRoutes } from "./routes/global"
@@ -19,6 +18,7 @@ import { lazy } from "@/util/lazy"
 import { errorHandler } from "./middleware"
 import { InstanceRoutes } from "./instance"
 import { initProjectors } from "./projectors"
+import { createAdaptorServer, type ServerType } from "@hono/node-server"
 
 // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
 globalThis.AI_SDK_LOG_WARNINGS = false
@@ -42,7 +42,7 @@ export namespace Server {
     return false
   }
 
-  export const Default = lazy(() => create({}).app)
+  export const Default = lazy(() => create({}))
 
   export function ControlPlaneRoutes(upgrade: UpgradeWebSocket, app = new Hono(), opts?: { cors?: string[] }): Hono {
     return app
@@ -54,6 +54,9 @@ export namespace Server {
         const password = Flag.OPENCODE_SERVER_PASSWORD
         if (!password) return next()
         const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
+
+        if (c.req.query("auth_token")) c.req.raw.headers.set("authorization", `Basic ${c.req.query("auth_token")}`)
+
         return basicAuth({ username, password })(c, next)
       })
       .use(async (c, next) => {

+ 3 - 3
packages/opencode/src/shell/shell.ts

@@ -51,13 +51,13 @@ export namespace Shell {
       if (shell.startsWith("/") && name(shell) === "bash") return gitbash() || shell
       return shell
     }
-    return Bun.which(shell) || shell
+    return which(shell) || shell
   }
 
   function pick() {
-    const pwsh = Bun.which("pwsh")
+    const pwsh = which("pwsh.exe")
     if (pwsh) return pwsh
-    const powershell = Bun.which("powershell")
+    const powershell = which("powershell.exe")
     if (powershell) return powershell
   }
 

+ 10 - 10
packages/opencode/src/storage/json-migration.ts

@@ -1,5 +1,5 @@
-import { Database } from "bun:sqlite"
-import { drizzle } from "drizzle-orm/bun-sqlite"
+import type { SQLiteBunDatabase } from "drizzle-orm/bun-sqlite"
+import type { NodeSQLiteDatabase } from "drizzle-orm/node-sqlite"
 import { Global } from "../global"
 import { Log } from "../util/log"
 import { ProjectTable } from "../project/project.sql"
@@ -23,7 +23,7 @@ export namespace JsonMigration {
     progress?: (event: Progress) => void
   }
 
-  export async function run(sqlite: Database, options?: Options) {
+  export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<any, any>, options?: Options) {
     const storageDir = path.join(Global.Path.data, "storage")
 
     if (!existsSync(storageDir)) {
@@ -43,13 +43,13 @@ export namespace JsonMigration {
     log.info("starting json to sqlite migration", { storageDir })
     const start = performance.now()
 
-    const db = drizzle({ client: sqlite })
+    // const db = drizzle({ client: sqlite })
 
     // Optimize SQLite for bulk inserts
-    sqlite.exec("PRAGMA journal_mode = WAL")
-    sqlite.exec("PRAGMA synchronous = OFF")
-    sqlite.exec("PRAGMA cache_size = 10000")
-    sqlite.exec("PRAGMA temp_store = MEMORY")
+    db.run("PRAGMA journal_mode = WAL")
+    db.run("PRAGMA synchronous = OFF")
+    db.run("PRAGMA cache_size = 10000")
+    db.run("PRAGMA temp_store = MEMORY")
     const stats = {
       projects: 0,
       sessions: 0,
@@ -146,7 +146,7 @@ export namespace JsonMigration {
 
     progress?.({ current, total, label: "starting" })
 
-    sqlite.exec("BEGIN TRANSACTION")
+    db.run("BEGIN TRANSACTION")
 
     // Migrate projects first (no FK deps)
     // Derive all IDs from file paths, not JSON content
@@ -400,7 +400,7 @@ export namespace JsonMigration {
       log.warn("skipped orphaned session shares", { count: orphans.shares })
     }
 
-    sqlite.exec("COMMIT")
+    db.run("COMMIT")
 
     log.info("json migration complete", {
       projects: stats.projects,

+ 2 - 2
packages/opencode/test/server/project-init-git.test.ts

@@ -19,7 +19,7 @@ afterEach(async () => {
 describe("project.initGit endpoint", () => {
   test("initializes git and reloads immediately", async () => {
     await using tmp = await tmpdir()
-    const app = Server.Default()
+    const app = Server.Default().app
     const seen: { directory?: string; payload: { type: string } }[] = []
     const fn = (evt: { directory?: string; payload: { type: string } }) => {
       seen.push(evt)
@@ -76,7 +76,7 @@ describe("project.initGit endpoint", () => {
 
   test("does not reload when the project is already git", async () => {
     await using tmp = await tmpdir({ git: true })
-    const app = Server.Default()
+    const app = Server.Default().app
     const seen: { directory?: string; payload: { type: string } }[] = []
     const fn = (evt: { directory?: string; payload: { type: string } }) => {
       seen.push(evt)

+ 2 - 2
packages/opencode/test/server/session-actions.test.ts

@@ -42,7 +42,7 @@ describe("session action routes", () => {
       fn: async () => {
         const session = await Session.create({})
         const cancel = spyOn(SessionPrompt, "cancel").mockResolvedValue()
-        const app = Server.Default()
+        const app = Server.Default().app
 
         const res = await app.request(`/session/${session.id}/abort`, {
           method: "POST",
@@ -66,7 +66,7 @@ describe("session action routes", () => {
         const msg = await user(session.id, "hello")
         const busy = spyOn(SessionPrompt, "assertNotBusy").mockRejectedValue(new Session.BusyError(session.id))
         const remove = spyOn(Session, "removeMessage").mockResolvedValue(msg.id)
-        const app = Server.Default()
+        const app = Server.Default().app
 
         const res = await app.request(`/session/${session.id}/message/${msg.id}`, {
           method: "DELETE",

+ 4 - 4
packages/opencode/test/server/session-messages.test.ts

@@ -60,7 +60,7 @@ describe("session messages endpoint", () => {
         fn: async () => {
           const session = await Session.create({})
           const ids = await fill(session.id, 5)
-          const app = Server.Default()
+          const app = Server.Default().app
 
           const a = await app.request(`/session/${session.id}/message?limit=2`)
           expect(a.status).toBe(200)
@@ -89,7 +89,7 @@ describe("session messages endpoint", () => {
         fn: async () => {
           const session = await Session.create({})
           const ids = await fill(session.id, 3)
-          const app = Server.Default()
+          const app = Server.Default().app
 
           const res = await app.request(`/session/${session.id}/message`)
           expect(res.status).toBe(200)
@@ -109,7 +109,7 @@ describe("session messages endpoint", () => {
         directory: tmp.path,
         fn: async () => {
           const session = await Session.create({})
-          const app = Server.Default()
+          const app = Server.Default().app
 
           const bad = await app.request(`/session/${session.id}/message?limit=2&before=bad`)
           expect(bad.status).toBe(400)
@@ -131,7 +131,7 @@ describe("session messages endpoint", () => {
         fn: async () => {
           const session = await Session.create({})
           await fill(session.id, 520)
-          const app = Server.Default()
+          const app = Server.Default().app
 
           const res = await app.request(`/session/${session.id}/message?limit=510`)
           expect(res.status).toBe(200)

+ 3 - 3
packages/opencode/test/server/session-select.test.ts

@@ -21,7 +21,7 @@ describe("tui.selectSession endpoint", () => {
         const session = await Session.create({})
 
         // #when
-        const app = Server.Default()
+        const app = Server.Default().app
         const response = await app.request("/tui/select-session", {
           method: "POST",
           headers: { "Content-Type": "application/json" },
@@ -47,7 +47,7 @@ describe("tui.selectSession endpoint", () => {
         const nonExistentSessionID = "ses_nonexistent123"
 
         // #when
-        const app = Server.Default()
+        const app = Server.Default().app
         const response = await app.request("/tui/select-session", {
           method: "POST",
           headers: { "Content-Type": "application/json" },
@@ -69,7 +69,7 @@ describe("tui.selectSession endpoint", () => {
         const invalidSessionID = "invalid_session_id"
 
         // #when
-        const app = Server.Default()
+        const app = Server.Default().app
         const response = await app.request("/tui/select-session", {
           method: "POST",
           headers: { "Content-Type": "application/json" },

+ 30 - 47
packages/opencode/test/storage/json-migration.test.ts

@@ -1,6 +1,6 @@
 import { describe, test, expect, beforeEach, afterEach } from "bun:test"
 import { Database } from "bun:sqlite"
-import { drizzle } from "drizzle-orm/bun-sqlite"
+import { drizzle, SQLiteBunDatabase } from "drizzle-orm/bun-sqlite"
 import { migrate } from "drizzle-orm/bun-sqlite/migrator"
 import path from "path"
 import fs from "fs/promises"
@@ -89,18 +89,21 @@ function createTestDb() {
       name: entry.name,
     }))
     .sort((a, b) => a.timestamp - b.timestamp)
-  migrate(drizzle({ client: sqlite }), migrations)
 
-  return sqlite
+  const db = drizzle({ client: sqlite })
+  migrate(db, migrations)
+
+  return [sqlite, db] as const
 }
 
 describe("JSON to SQLite migration", () => {
   let storageDir: string
   let sqlite: Database
+  let db: SQLiteBunDatabase
 
   beforeEach(async () => {
     storageDir = await setupStorageDir()
-    sqlite = createTestDb()
+    ;[sqlite, db] = createTestDb()
   })
 
   afterEach(async () => {
@@ -118,11 +121,10 @@ describe("JSON to SQLite migration", () => {
       sandboxes: ["/test/sandbox"],
     })
 
-    const stats = await JsonMigration.run(sqlite)
+    const stats = await JsonMigration.run(db)
 
     expect(stats?.projects).toBe(1)
 
-    const db = drizzle({ client: sqlite })
     const projects = db.select().from(ProjectTable).all()
     expect(projects.length).toBe(1)
     expect(projects[0].id).toBe(ProjectID.make("proj_test123abc"))
@@ -143,11 +145,10 @@ describe("JSON to SQLite migration", () => {
       }),
     )
 
-    const stats = await JsonMigration.run(sqlite)
+    const stats = await JsonMigration.run(db)
 
     expect(stats?.projects).toBe(1)
 
-    const db = drizzle({ client: sqlite })
     const projects = db.select().from(ProjectTable).all()
     expect(projects.length).toBe(1)
     expect(projects[0].id).toBe(ProjectID.make("proj_filename")) // Uses filename, not JSON id
@@ -164,11 +165,10 @@ describe("JSON to SQLite migration", () => {
       commands: { start: "npm run dev" },
     })
 
-    const stats = await JsonMigration.run(sqlite)
+    const stats = await JsonMigration.run(db)
 
     expect(stats?.projects).toBe(1)
 
-    const db = drizzle({ client: sqlite })
     const projects = db.select().from(ProjectTable).all()
     expect(projects.length).toBe(1)
     expect(projects[0].id).toBe(ProjectID.make("proj_with_commands"))
@@ -185,11 +185,10 @@ describe("JSON to SQLite migration", () => {
       sandboxes: [],
     })
 
-    const stats = await JsonMigration.run(sqlite)
+    const stats = await JsonMigration.run(db)
 
     expect(stats?.projects).toBe(1)
 
-    const db = drizzle({ client: sqlite })
     const projects = db.select().from(ProjectTable).all()
     expect(projects.length).toBe(1)
     expect(projects[0].id).toBe(ProjectID.make("proj_no_commands"))
@@ -216,9 +215,8 @@ describe("JSON to SQLite migration", () => {
       share: { url: "https://example.com/share" },
     })
 
-    await JsonMigration.run(sqlite)
+    await JsonMigration.run(db)
 
-    const db = drizzle({ client: sqlite })
     const sessions = db.select().from(SessionTable).all()
     expect(sessions.length).toBe(1)
     expect(sessions[0].id).toBe(SessionID.make("ses_test456def"))
@@ -247,12 +245,11 @@ describe("JSON to SQLite migration", () => {
       JSON.stringify({ ...fixtures.part }),
     )
 
-    const stats = await JsonMigration.run(sqlite)
+    const stats = await JsonMigration.run(db)
 
     expect(stats?.messages).toBe(1)
     expect(stats?.parts).toBe(1)
 
-    const db = drizzle({ client: sqlite })
     const messages = db.select().from(MessageTable).all()
     expect(messages.length).toBe(1)
     expect(messages[0].id).toBe(MessageID.make("msg_test789ghi"))
@@ -287,12 +284,11 @@ describe("JSON to SQLite migration", () => {
       }),
     )
 
-    const stats = await JsonMigration.run(sqlite)
+    const stats = await JsonMigration.run(db)
 
     expect(stats?.messages).toBe(1)
     expect(stats?.parts).toBe(1)
 
-    const db = drizzle({ client: sqlite })
     const messages = db.select().from(MessageTable).all()
     expect(messages.length).toBe(1)
     expect(messages[0].id).toBe(MessageID.make("msg_test789ghi"))
@@ -329,11 +325,10 @@ describe("JSON to SQLite migration", () => {
       }),
     )
 
-    const stats = await JsonMigration.run(sqlite)
+    const stats = await JsonMigration.run(db)
 
     expect(stats?.messages).toBe(1)
 
-    const db = drizzle({ client: sqlite })
     const messages = db.select().from(MessageTable).all()
     expect(messages.length).toBe(1)
     expect(messages[0].id).toBe(MessageID.make("msg_from_filename")) // Uses filename, not JSON id
@@ -367,11 +362,10 @@ describe("JSON to SQLite migration", () => {
       }),
     )
 
-    const stats = await JsonMigration.run(sqlite)
+    const stats = await JsonMigration.run(db)
 
     expect(stats?.parts).toBe(1)
 
-    const db = drizzle({ client: sqlite })
     const parts = db.select().from(PartTable).all()
     expect(parts.length).toBe(1)
     expect(parts[0].id).toBe(PartID.make("prt_from_filename")) // Uses filename, not JSON id
@@ -392,7 +386,7 @@ describe("JSON to SQLite migration", () => {
       }),
     )
 
-    const stats = await JsonMigration.run(sqlite)
+    const stats = await JsonMigration.run(db)
 
     expect(stats?.sessions).toBe(0)
   })
@@ -420,11 +414,10 @@ describe("JSON to SQLite migration", () => {
       time: { created: 1700000000000, updated: 1700000001000 },
     })
 
-    const stats = await JsonMigration.run(sqlite)
+    const stats = await JsonMigration.run(db)
 
     expect(stats?.sessions).toBe(1)
 
-    const db = drizzle({ client: sqlite })
     const sessions = db.select().from(SessionTable).all()
     expect(sessions.length).toBe(1)
     expect(sessions[0].id).toBe(SessionID.make("ses_migrated"))
@@ -452,11 +445,10 @@ describe("JSON to SQLite migration", () => {
       }),
     )
 
-    const stats = await JsonMigration.run(sqlite)
+    const stats = await JsonMigration.run(db)
 
     expect(stats?.sessions).toBe(1)
 
-    const db = drizzle({ client: sqlite })
     const sessions = db.select().from(SessionTable).all()
     expect(sessions.length).toBe(1)
     expect(sessions[0].id).toBe(SessionID.make("ses_from_filename")) // Uses filename, not JSON id
@@ -471,10 +463,9 @@ describe("JSON to SQLite migration", () => {
       sandboxes: [],
     })
 
-    await JsonMigration.run(sqlite)
-    await JsonMigration.run(sqlite)
+    await JsonMigration.run(db)
+    await JsonMigration.run(db)
 
-    const db = drizzle({ client: sqlite })
     const projects = db.select().from(ProjectTable).all()
     expect(projects.length).toBe(1) // Still only 1 due to onConflictDoNothing
   })
@@ -507,11 +498,10 @@ describe("JSON to SQLite migration", () => {
       ]),
     )
 
-    const stats = await JsonMigration.run(sqlite)
+    const stats = await JsonMigration.run(db)
 
     expect(stats?.todos).toBe(2)
 
-    const db = drizzle({ client: sqlite })
     const todos = db.select().from(TodoTable).orderBy(TodoTable.position).all()
     expect(todos.length).toBe(2)
     expect(todos[0].content).toBe("First todo")
@@ -540,9 +530,8 @@ describe("JSON to SQLite migration", () => {
       ]),
     )
 
-    await JsonMigration.run(sqlite)
+    await JsonMigration.run(db)
 
-    const db = drizzle({ client: sqlite })
     const todos = db.select().from(TodoTable).orderBy(TodoTable.position).all()
 
     expect(todos.length).toBe(3)
@@ -570,11 +559,10 @@ describe("JSON to SQLite migration", () => {
     ]
     await Bun.write(path.join(storageDir, "permission", "proj_test123abc.json"), JSON.stringify(permissionData))
 
-    const stats = await JsonMigration.run(sqlite)
+    const stats = await JsonMigration.run(db)
 
     expect(stats?.permissions).toBe(1)
 
-    const db = drizzle({ client: sqlite })
     const permissions = db.select().from(PermissionTable).all()
     expect(permissions.length).toBe(1)
     expect(permissions[0].project_id).toBe("proj_test123abc")
@@ -600,11 +588,10 @@ describe("JSON to SQLite migration", () => {
       }),
     )
 
-    const stats = await JsonMigration.run(sqlite)
+    const stats = await JsonMigration.run(db)
 
     expect(stats?.shares).toBe(1)
 
-    const db = drizzle({ client: sqlite })
     const shares = db.select().from(SessionShareTable).all()
     expect(shares.length).toBe(1)
     expect(shares[0].session_id).toBe("ses_test456def")
@@ -616,7 +603,7 @@ describe("JSON to SQLite migration", () => {
   test("returns empty stats when storage directory does not exist", async () => {
     await fs.rm(storageDir, { recursive: true, force: true })
 
-    const stats = await JsonMigration.run(sqlite)
+    const stats = await JsonMigration.run(db)
 
     expect(stats.projects).toBe(0)
     expect(stats.sessions).toBe(0)
@@ -637,12 +624,11 @@ describe("JSON to SQLite migration", () => {
     })
     await Bun.write(path.join(storageDir, "project", "broken.json"), "{ invalid json")
 
-    const stats = await JsonMigration.run(sqlite)
+    const stats = await JsonMigration.run(db)
 
     expect(stats.projects).toBe(1)
     expect(stats.errors.some((x) => x.includes("failed to read") && x.includes("broken.json"))).toBe(true)
 
-    const db = drizzle({ client: sqlite })
     const projects = db.select().from(ProjectTable).all()
     expect(projects.length).toBe(1)
     expect(projects[0].id).toBe(ProjectID.make("proj_test123abc"))
@@ -666,10 +652,9 @@ describe("JSON to SQLite migration", () => {
       ]),
     )
 
-    const stats = await JsonMigration.run(sqlite)
+    const stats = await JsonMigration.run(db)
     expect(stats.todos).toBe(2)
 
-    const db = drizzle({ client: sqlite })
     const todos = db.select().from(TodoTable).orderBy(TodoTable.position).all()
     expect(todos.length).toBe(2)
     expect(todos[0].content).toBe("keep-0")
@@ -714,13 +699,12 @@ describe("JSON to SQLite migration", () => {
       JSON.stringify({ id: "share_missing", secret: "secret", url: "https://missing.example.com" }),
     )
 
-    const stats = await JsonMigration.run(sqlite)
+    const stats = await JsonMigration.run(db)
 
     expect(stats.todos).toBe(1)
     expect(stats.permissions).toBe(1)
     expect(stats.shares).toBe(1)
 
-    const db = drizzle({ client: sqlite })
     expect(db.select().from(TodoTable).all().length).toBe(1)
     expect(db.select().from(PermissionTable).all().length).toBe(1)
     expect(db.select().from(SessionShareTable).all().length).toBe(1)
@@ -823,7 +807,7 @@ describe("JSON to SQLite migration", () => {
     )
     await Bun.write(path.join(storageDir, "session_share", "ses_broken.json"), "{ nope")
 
-    const stats = await JsonMigration.run(sqlite)
+    const stats = await JsonMigration.run(db)
 
     // Projects: proj_test123abc (valid), proj_missing_id (now derives id from filename)
     // Sessions: ses_test456def (valid), ses_missing_project (now uses dir path),
@@ -837,7 +821,6 @@ describe("JSON to SQLite migration", () => {
     expect(stats.shares).toBe(1)
     expect(stats.errors.length).toBeGreaterThanOrEqual(6)
 
-    const db = drizzle({ client: sqlite })
     expect(db.select().from(ProjectTable).all().length).toBe(2)
     expect(db.select().from(SessionTable).all().length).toBe(3)
     expect(db.select().from(MessageTable).all().length).toBe(1)

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است