electron: remove cli and use server code directly
parent
37883a9f3a
commit
759acb60cd
|
|
@ -34,11 +34,6 @@ const getBase = (): Configuration => ({
|
||||||
},
|
},
|
||||||
files: ["out/**/*", "resources/**/*"],
|
files: ["out/**/*", "resources/**/*"],
|
||||||
extraResources: [
|
extraResources: [
|
||||||
{
|
|
||||||
from: "resources/",
|
|
||||||
to: "",
|
|
||||||
filter: ["opencode-cli*"],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
from: "native/",
|
from: "native/",
|
||||||
to: "native/",
|
to: "native/",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { defineConfig } from "electron-vite"
|
import { defineConfig } from "electron-vite"
|
||||||
import appPlugin from "@opencode-ai/app/vite"
|
import appPlugin from "@opencode-ai/app/vite"
|
||||||
|
import * as fs from "node:fs/promises"
|
||||||
|
|
||||||
const channel = (() => {
|
const channel = (() => {
|
||||||
const raw = process.env.OPENCODE_CHANNEL
|
const raw = process.env.OPENCODE_CHANNEL
|
||||||
|
|
@ -7,6 +8,8 @@ const channel = (() => {
|
||||||
return "dev"
|
return "dev"
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
const OPENCODE_SERVER_DIST = "../opencode/dist/node"
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
main: {
|
main: {
|
||||||
define: {
|
define: {
|
||||||
|
|
@ -17,6 +20,25 @@ export default defineConfig({
|
||||||
input: { index: "src/main/index.ts" },
|
input: { index: "src/main/index.ts" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
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",
|
||||||
|
enforce: "post",
|
||||||
|
async closeBundle() {
|
||||||
|
for (const l of await fs.readdir(OPENCODE_SERVER_DIST)) {
|
||||||
|
if (l.endsWith(".js")) continue
|
||||||
|
await fs.writeFile(`./out/main/${l}`, await fs.readFile(`${OPENCODE_SERVER_DIST}/${l}`))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
preload: {
|
preload: {
|
||||||
build: {
|
build: {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
"@solid-primitives/storage": "catalog:",
|
"@solid-primitives/storage": "catalog:",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@solidjs/router": "0.15.4",
|
"@solidjs/router": "0.15.4",
|
||||||
|
"@valibot/to-json-schema": "1.6.0",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
"electron-context-menu": "4.1.2",
|
"electron-context-menu": "4.1.2",
|
||||||
"electron-log": "^5",
|
"electron-log": "^5",
|
||||||
|
|
@ -38,7 +39,9 @@
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"marked": "^15",
|
"marked": "^15",
|
||||||
"solid-js": "catalog:",
|
"solid-js": "catalog:",
|
||||||
"tree-kill": "^1.2.2"
|
"sury": "11.0.0-alpha.4",
|
||||||
|
"tree-kill": "^1.2.2",
|
||||||
|
"zod-openapi": "5.4.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@actions/artifact": "4.0.0",
|
"@actions/artifact": "4.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,5 @@
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
|
|
||||||
import { copyBinaryToSidecarFolder, getCurrentSidecar, windowsify } from "./utils"
|
|
||||||
|
|
||||||
await $`bun ./scripts/copy-icons.ts ${process.env.OPENCODE_CHANNEL ?? "dev"}`
|
await $`bun ./scripts/copy-icons.ts ${process.env.OPENCODE_CHANNEL ?? "dev"}`
|
||||||
|
|
||||||
const RUST_TARGET = Bun.env.RUST_TARGET
|
await $`cd ../opencode && bun script/build-node.ts`
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -5,3 +5,25 @@ interface ImportMetaEnv {
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,6 @@ const { autoUpdater } = pkg
|
||||||
|
|
||||||
import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
|
import type { InitStep, ServerReadyData, SqliteMigrationProgress, WslConfig } from "../preload/types"
|
||||||
import { checkAppExists, resolveAppPath, wslPath } from "./apps"
|
import { checkAppExists, resolveAppPath, wslPath } from "./apps"
|
||||||
import type { CommandChild } from "./cli"
|
|
||||||
import { installCli, syncCli } from "./cli"
|
|
||||||
import { CHANNEL, UPDATER_ENABLED } from "./constants"
|
import { CHANNEL, UPDATER_ENABLED } from "./constants"
|
||||||
import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigrationProgress } from "./ipc"
|
import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigrationProgress } from "./ipc"
|
||||||
import { initLogging } from "./logging"
|
import { initLogging } from "./logging"
|
||||||
|
|
@ -36,12 +34,13 @@ import { parseMarkdown } from "./markdown"
|
||||||
import { createMenu } from "./menu"
|
import { createMenu } from "./menu"
|
||||||
import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server"
|
import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server"
|
||||||
import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows"
|
import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows"
|
||||||
|
import { Server } from "virtual:opencode-server"
|
||||||
|
|
||||||
const initEmitter = new EventEmitter()
|
const initEmitter = new EventEmitter()
|
||||||
let initStep: InitStep = { phase: "server_waiting" }
|
let initStep: InitStep = { phase: "server_waiting" }
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null
|
let mainWindow: BrowserWindow | null = null
|
||||||
let sidecar: CommandChild | null = null
|
let server: Server.Listener | null = null
|
||||||
const loadingComplete = defer<void>()
|
const loadingComplete = defer<void>()
|
||||||
|
|
||||||
const pendingDeepLinks: string[] = []
|
const pendingDeepLinks: string[] = []
|
||||||
|
|
@ -96,11 +95,9 @@ function setupApp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void app.whenReady().then(async () => {
|
void app.whenReady().then(async () => {
|
||||||
// migrate()
|
|
||||||
app.setAsDefaultProtocolClient("opencode")
|
app.setAsDefaultProtocolClient("opencode")
|
||||||
setDockIcon()
|
setDockIcon()
|
||||||
setupAutoUpdater()
|
setupAutoUpdater()
|
||||||
syncCli()
|
|
||||||
await initialize()
|
await initialize()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -134,8 +131,8 @@ async function initialize() {
|
||||||
const password = randomUUID()
|
const password = randomUUID()
|
||||||
|
|
||||||
logger.log("spawning sidecar", { url })
|
logger.log("spawning sidecar", { url })
|
||||||
const { child, health, events } = spawnLocalServer(hostname, port, password)
|
const { listener, health } = await spawnLocalServer(hostname, port, password)
|
||||||
sidecar = child
|
server = listener
|
||||||
serverReady.resolve({
|
serverReady.resolve({
|
||||||
url,
|
url,
|
||||||
username: "opencode",
|
username: "opencode",
|
||||||
|
|
@ -145,7 +142,7 @@ async function initialize() {
|
||||||
const loadingTask = (async () => {
|
const loadingTask = (async () => {
|
||||||
logger.log("sidecar connection started", { url })
|
logger.log("sidecar connection started", { url })
|
||||||
|
|
||||||
events.on("sqlite", (progress: SqliteMigrationProgress) => {
|
initEmitter.on("sqlite", (progress: SqliteMigrationProgress) => {
|
||||||
setInitStep({ phase: "sqlite_waiting" })
|
setInitStep({ phase: "sqlite_waiting" })
|
||||||
if (overlay) sendSqliteMigrationProgress(overlay, progress)
|
if (overlay) sendSqliteMigrationProgress(overlay, progress)
|
||||||
if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress)
|
if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress)
|
||||||
|
|
@ -198,9 +195,6 @@ function wireMenu() {
|
||||||
if (!mainWindow) return
|
if (!mainWindow) return
|
||||||
createMenu({
|
createMenu({
|
||||||
trigger: (id) => mainWindow && sendMenuCommand(mainWindow, id),
|
trigger: (id) => mainWindow && sendMenuCommand(mainWindow, id),
|
||||||
installCli: () => {
|
|
||||||
void installCli()
|
|
||||||
},
|
|
||||||
checkForUpdates: () => {
|
checkForUpdates: () => {
|
||||||
void checkForUpdates(true)
|
void checkForUpdates(true)
|
||||||
},
|
},
|
||||||
|
|
@ -215,7 +209,6 @@ function wireMenu() {
|
||||||
|
|
||||||
registerIpcHandlers({
|
registerIpcHandlers({
|
||||||
killSidecar: () => killSidecar(),
|
killSidecar: () => killSidecar(),
|
||||||
installCli: async () => installCli(),
|
|
||||||
awaitInitialization: async (sendStep) => {
|
awaitInitialization: async (sendStep) => {
|
||||||
sendStep(initStep)
|
sendStep(initStep)
|
||||||
const listener = (step: InitStep) => sendStep(step)
|
const listener = (step: InitStep) => sendStep(step)
|
||||||
|
|
@ -247,16 +240,9 @@ registerIpcHandlers({
|
||||||
})
|
})
|
||||||
|
|
||||||
function killSidecar() {
|
function killSidecar() {
|
||||||
if (!sidecar) return
|
if (!server) return
|
||||||
const pid = sidecar.pid
|
server.stop()
|
||||||
sidecar.kill()
|
server = null
|
||||||
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 {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureLoopbackNoProxy() {
|
function ensureLoopbackNoProxy() {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ const pickerFilters = (ext?: string[]) => {
|
||||||
|
|
||||||
type Deps = {
|
type Deps = {
|
||||||
killSidecar: () => void
|
killSidecar: () => void
|
||||||
installCli: () => Promise<string>
|
|
||||||
awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData>
|
awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData>
|
||||||
getDefaultServerUrl: () => Promise<string | null> | string | null
|
getDefaultServerUrl: () => Promise<string | null> | string | null
|
||||||
setDefaultServerUrl: (url: string | null) => Promise<void> | void
|
setDefaultServerUrl: (url: string | null) => Promise<void> | void
|
||||||
|
|
@ -34,7 +33,6 @@ type Deps = {
|
||||||
|
|
||||||
export function registerIpcHandlers(deps: Deps) {
|
export function registerIpcHandlers(deps: Deps) {
|
||||||
ipcMain.handle("kill-sidecar", () => deps.killSidecar())
|
ipcMain.handle("kill-sidecar", () => deps.killSidecar())
|
||||||
ipcMain.handle("install-cli", () => deps.installCli())
|
|
||||||
ipcMain.handle("await-initialization", (event: IpcMainInvokeEvent) => {
|
ipcMain.handle("await-initialization", (event: IpcMainInvokeEvent) => {
|
||||||
const send = (step: InitStep) => event.sender.send("init-step", step)
|
const send = (step: InitStep) => event.sender.send("init-step", step)
|
||||||
return deps.awaitInitialization(send)
|
return deps.awaitInitialization(send)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import { createMainWindow } from "./windows"
|
||||||
|
|
||||||
type Deps = {
|
type Deps = {
|
||||||
trigger: (id: string) => void
|
trigger: (id: string) => void
|
||||||
installCli: () => void
|
|
||||||
checkForUpdates: () => void
|
checkForUpdates: () => void
|
||||||
reload: () => void
|
reload: () => void
|
||||||
relaunch: () => void
|
relaunch: () => void
|
||||||
|
|
@ -24,10 +23,6 @@ export function createMenu(deps: Deps) {
|
||||||
enabled: UPDATER_ENABLED,
|
enabled: UPDATER_ENABLED,
|
||||||
click: () => deps.checkForUpdates(),
|
click: () => deps.checkForUpdates(),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "Install CLI...",
|
|
||||||
click: () => deps.installCli(),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "Reload Webview",
|
label: "Reload Webview",
|
||||||
click: () => deps.reload(),
|
click: () => deps.reload(),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { serve, type CommandChild } from "./cli"
|
import { Server } from "virtual:opencode-server"
|
||||||
import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants"
|
import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants"
|
||||||
import { store } from "./store"
|
import { store } from "./store"
|
||||||
|
|
||||||
|
|
@ -29,8 +29,13 @@ export function setWslConfig(config: WslConfig) {
|
||||||
store.set(WSL_ENABLED_KEY, config.enabled)
|
store.set(WSL_ENABLED_KEY, config.enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function spawnLocalServer(hostname: string, port: number, password: string) {
|
export async function spawnLocalServer(hostname: string, port: number, password: string) {
|
||||||
const { child, exit, events } = serve(hostname, port, password)
|
const listener = await Server.listen({
|
||||||
|
port,
|
||||||
|
hostname,
|
||||||
|
username: "opencode",
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
|
||||||
const wait = (async () => {
|
const wait = (async () => {
|
||||||
const url = `http://${hostname}:${port}`
|
const url = `http://${hostname}:${port}`
|
||||||
|
|
@ -42,19 +47,10 @@ export function spawnLocalServer(hostname: string, port: number, password: strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const terminated = async () => {
|
await ready()
|
||||||
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()])
|
|
||||||
})()
|
})()
|
||||||
|
|
||||||
return { child, health: { wait }, events }
|
return { listener, health: { wait } }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkHealth(url: string, password?: string | null): Promise<boolean> {
|
export async function checkHealth(url: string, password?: string | null): Promise<boolean> {
|
||||||
|
|
@ -82,5 +78,3 @@ export async function checkHealth(url: string, password?: string | null): Promis
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { CommandChild }
|
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ await $`bun install --linker=${link} --os="*" --cpu="*" @lydell/node-pty@1.2.0-b
|
||||||
await Bun.build({
|
await Bun.build({
|
||||||
target: "node",
|
target: "node",
|
||||||
entrypoints: ["./src/node.ts"],
|
entrypoints: ["./src/node.ts"],
|
||||||
outdir: "./dist",
|
outdir: "./dist/node",
|
||||||
format: "esm",
|
format: "esm",
|
||||||
sourcemap: "linked",
|
sourcemap: "linked",
|
||||||
external: ["jsonc-parser"],
|
external: ["jsonc-parser"],
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { Argv } from "yargs"
|
import type { Argv } from "yargs"
|
||||||
import { spawn } from "child_process"
|
import { spawn } from "child_process"
|
||||||
import { Database } from "../../storage/db"
|
import { Database } from "../../storage/db"
|
||||||
|
import { drizzle } from "drizzle-orm/bun-sqlite"
|
||||||
import { Database as BunDatabase } from "bun:sqlite"
|
import { Database as BunDatabase } from "bun:sqlite"
|
||||||
import { UI } from "../ui"
|
import { UI } from "../ui"
|
||||||
import { cmd } from "./cmd"
|
import { cmd } from "./cmd"
|
||||||
|
|
@ -74,7 +75,7 @@ const MigrateCommand = cmd({
|
||||||
let last = -1
|
let last = -1
|
||||||
if (tty) process.stderr.write("\x1b[?25l")
|
if (tty) process.stderr.write("\x1b[?25l")
|
||||||
try {
|
try {
|
||||||
const stats = await JsonMigration.run(sqlite, {
|
const stats = await JsonMigration.run(drizzle({ client: sqlite }), {
|
||||||
progress: (event) => {
|
progress: (event) => {
|
||||||
const percent = Math.floor((event.current / event.total) * 100)
|
const percent = Math.floor((event.current / event.total) * 100)
|
||||||
if (percent === last) return
|
if (percent === last) return
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { Flag } from "../../flag/flag"
|
||||||
import { bootstrap } from "../bootstrap"
|
import { bootstrap } from "../bootstrap"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { Filesystem } from "../../util/filesystem"
|
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 { Server } from "../../server/server"
|
||||||
import { Provider } from "../../provider/provider"
|
import { Provider } from "../../provider/provider"
|
||||||
import { Agent } from "../../agent/agent"
|
import { Agent } from "../../agent/agent"
|
||||||
|
|
@ -667,7 +667,7 @@ export const RunCommand = cmd({
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
const request = new Request(input, init)
|
const request = new Request(input, init)
|
||||||
return Server.Default().fetch(request)
|
return Server.Default().app.fetch(request)
|
||||||
}) as typeof globalThis.fetch
|
}) as typeof globalThis.fetch
|
||||||
const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn })
|
const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn })
|
||||||
await execute(sdk)
|
await execute(sdk)
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ export const rpc = {
|
||||||
headers,
|
headers,
|
||||||
body: input.body,
|
body: input.body,
|
||||||
})
|
})
|
||||||
const response = await Server.Default().fetch(request)
|
const response = await Server.Default().app.fetch(request)
|
||||||
const body = await response.text()
|
const body = await response.text()
|
||||||
return {
|
return {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import { Database } from "./storage/db"
|
||||||
import { errorMessage } from "./util/error"
|
import { errorMessage } from "./util/error"
|
||||||
import { PluginCommand } from "./cli/cmd/plug"
|
import { PluginCommand } from "./cli/cmd/plug"
|
||||||
import { Heap } from "./cli/heap"
|
import { Heap } from "./cli/heap"
|
||||||
|
import { drizzle } from "drizzle-orm/bun-sqlite"
|
||||||
|
|
||||||
process.on("unhandledRejection", (e) => {
|
process.on("unhandledRejection", (e) => {
|
||||||
Log.Default.error("rejection", {
|
Log.Default.error("rejection", {
|
||||||
|
|
@ -119,7 +120,7 @@ const cli = yargs(args)
|
||||||
let last = -1
|
let last = -1
|
||||||
if (tty) process.stderr.write("\x1b[?25l")
|
if (tty) process.stderr.write("\x1b[?25l")
|
||||||
try {
|
try {
|
||||||
await JsonMigration.run(Database.Client().$client, {
|
await JsonMigration.run(drizzle({ client: Database.Client().$client }), {
|
||||||
progress: (event) => {
|
progress: (event) => {
|
||||||
const percent = Math.floor((event.current / event.total) * 100)
|
const percent = Math.floor((event.current / event.total) * 100)
|
||||||
if (percent === last && event.current !== event.total) return
|
if (percent === last && event.current !== event.total) return
|
||||||
|
|
|
||||||
|
|
@ -1 +1,6 @@
|
||||||
|
export { Config } from "./config/config"
|
||||||
export { Server } from "./server/server"
|
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"
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ export namespace Plugin {
|
||||||
Authorization: `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${Flag.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`,
|
Authorization: `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${Flag.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
fetch: async (...args) => Server.Default().fetch(...args),
|
fetch: async (...args) => Server.Default().app.fetch(...args),
|
||||||
})
|
})
|
||||||
const cfg = yield* config.get()
|
const cfg = yield* config.get()
|
||||||
const input: PluginInput = {
|
const input: PluginInput = {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
// Auto-generated by build.ts - do not edit
|
||||||
|
export declare const snapshot: Record<string, unknown>
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -843,19 +843,17 @@ export const SessionRoutes = lazy(() =>
|
||||||
),
|
),
|
||||||
validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
|
validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
c.status(204)
|
const sessionID = c.req.valid("param").sessionID
|
||||||
c.header("Content-Type", "application/json")
|
const body = c.req.valid("json")
|
||||||
return stream(c, async () => {
|
SessionPrompt.prompt({ ...body, sessionID }).catch((err) => {
|
||||||
const sessionID = c.req.valid("param").sessionID
|
log.error("prompt_async failed", { sessionID, error: err })
|
||||||
const body = c.req.valid("json")
|
Bus.publish(Session.Event.Error, {
|
||||||
SessionPrompt.prompt({ ...body, sessionID }).catch((err) => {
|
sessionID,
|
||||||
log.error("prompt_async failed", { sessionID, error: err })
|
error: new NamedError.Unknown({ message: err instanceof Error ? err.message : String(err) }).toObject(),
|
||||||
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(
|
.post(
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Log } from "../util/log"
|
||||||
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
|
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import { compress } from "hono/compress"
|
import { compress } from "hono/compress"
|
||||||
|
import { createNodeWebSocket } from "@hono/node-ws"
|
||||||
import { cors } from "hono/cors"
|
import { cors } from "hono/cors"
|
||||||
import { basicAuth } from "hono/basic-auth"
|
import { basicAuth } from "hono/basic-auth"
|
||||||
import type { UpgradeWebSocket } from "hono/ws"
|
import type { UpgradeWebSocket } from "hono/ws"
|
||||||
|
|
@ -9,8 +10,6 @@ import z from "zod"
|
||||||
import { Auth } from "../auth"
|
import { Auth } from "../auth"
|
||||||
import { Flag } from "../flag/flag"
|
import { Flag } from "../flag/flag"
|
||||||
import { ProviderID } from "../provider/schema"
|
import { ProviderID } from "../provider/schema"
|
||||||
import { createAdaptorServer, type ServerType } from "@hono/node-server"
|
|
||||||
import { createNodeWebSocket } from "@hono/node-ws"
|
|
||||||
import { WorkspaceRouterMiddleware } from "./router"
|
import { WorkspaceRouterMiddleware } from "./router"
|
||||||
import { errors } from "./error"
|
import { errors } from "./error"
|
||||||
import { GlobalRoutes } from "./routes/global"
|
import { GlobalRoutes } from "./routes/global"
|
||||||
|
|
@ -19,6 +18,7 @@ import { lazy } from "@/util/lazy"
|
||||||
import { errorHandler } from "./middleware"
|
import { errorHandler } from "./middleware"
|
||||||
import { InstanceRoutes } from "./instance"
|
import { InstanceRoutes } from "./instance"
|
||||||
import { initProjectors } from "./projectors"
|
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
|
// @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
|
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||||
|
|
@ -42,7 +42,7 @@ export namespace Server {
|
||||||
return false
|
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 {
|
export function ControlPlaneRoutes(upgrade: UpgradeWebSocket, app = new Hono(), opts?: { cors?: string[] }): Hono {
|
||||||
return app
|
return app
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Database } from "bun:sqlite"
|
import type { SQLiteBunDatabase } from "drizzle-orm/bun-sqlite"
|
||||||
import { drizzle } from "drizzle-orm/bun-sqlite"
|
import type { NodeSQLiteDatabase } from "drizzle-orm/node-sqlite"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { ProjectTable } from "../project/project.sql"
|
import { ProjectTable } from "../project/project.sql"
|
||||||
|
|
@ -23,7 +23,7 @@ export namespace JsonMigration {
|
||||||
progress?: (event: Progress) => void
|
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")
|
const storageDir = path.join(Global.Path.data, "storage")
|
||||||
|
|
||||||
if (!existsSync(storageDir)) {
|
if (!existsSync(storageDir)) {
|
||||||
|
|
@ -43,13 +43,13 @@ export namespace JsonMigration {
|
||||||
log.info("starting json to sqlite migration", { storageDir })
|
log.info("starting json to sqlite migration", { storageDir })
|
||||||
const start = performance.now()
|
const start = performance.now()
|
||||||
|
|
||||||
const db = drizzle({ client: sqlite })
|
// const db = drizzle({ client: sqlite })
|
||||||
|
|
||||||
// Optimize SQLite for bulk inserts
|
// Optimize SQLite for bulk inserts
|
||||||
sqlite.exec("PRAGMA journal_mode = WAL")
|
db.run("PRAGMA journal_mode = WAL")
|
||||||
sqlite.exec("PRAGMA synchronous = OFF")
|
db.run("PRAGMA synchronous = OFF")
|
||||||
sqlite.exec("PRAGMA cache_size = 10000")
|
db.run("PRAGMA cache_size = 10000")
|
||||||
sqlite.exec("PRAGMA temp_store = MEMORY")
|
db.run("PRAGMA temp_store = MEMORY")
|
||||||
const stats = {
|
const stats = {
|
||||||
projects: 0,
|
projects: 0,
|
||||||
sessions: 0,
|
sessions: 0,
|
||||||
|
|
@ -146,7 +146,7 @@ export namespace JsonMigration {
|
||||||
|
|
||||||
progress?.({ current, total, label: "starting" })
|
progress?.({ current, total, label: "starting" })
|
||||||
|
|
||||||
sqlite.exec("BEGIN TRANSACTION")
|
db.run("BEGIN TRANSACTION")
|
||||||
|
|
||||||
// Migrate projects first (no FK deps)
|
// Migrate projects first (no FK deps)
|
||||||
// Derive all IDs from file paths, not JSON content
|
// 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 })
|
log.warn("skipped orphaned session shares", { count: orphans.shares })
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlite.exec("COMMIT")
|
db.run("COMMIT")
|
||||||
|
|
||||||
log.info("json migration complete", {
|
log.info("json migration complete", {
|
||||||
projects: stats.projects,
|
projects: stats.projects,
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ afterEach(async () => {
|
||||||
describe("project.initGit endpoint", () => {
|
describe("project.initGit endpoint", () => {
|
||||||
test("initializes git and reloads immediately", async () => {
|
test("initializes git and reloads immediately", async () => {
|
||||||
await using tmp = await tmpdir()
|
await using tmp = await tmpdir()
|
||||||
const app = Server.Default()
|
const app = Server.Default().app
|
||||||
const seen: { directory?: string; payload: { type: string } }[] = []
|
const seen: { directory?: string; payload: { type: string } }[] = []
|
||||||
const fn = (evt: { directory?: string; payload: { type: string } }) => {
|
const fn = (evt: { directory?: string; payload: { type: string } }) => {
|
||||||
seen.push(evt)
|
seen.push(evt)
|
||||||
|
|
@ -76,7 +76,7 @@ describe("project.initGit endpoint", () => {
|
||||||
|
|
||||||
test("does not reload when the project is already git", async () => {
|
test("does not reload when the project is already git", async () => {
|
||||||
await using tmp = await tmpdir({ git: true })
|
await using tmp = await tmpdir({ git: true })
|
||||||
const app = Server.Default()
|
const app = Server.Default().app
|
||||||
const seen: { directory?: string; payload: { type: string } }[] = []
|
const seen: { directory?: string; payload: { type: string } }[] = []
|
||||||
const fn = (evt: { directory?: string; payload: { type: string } }) => {
|
const fn = (evt: { directory?: string; payload: { type: string } }) => {
|
||||||
seen.push(evt)
|
seen.push(evt)
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ describe("session action routes", () => {
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const session = await Session.create({})
|
const session = await Session.create({})
|
||||||
const cancel = spyOn(SessionPrompt, "cancel").mockResolvedValue()
|
const cancel = spyOn(SessionPrompt, "cancel").mockResolvedValue()
|
||||||
const app = Server.Default()
|
const app = Server.Default().app
|
||||||
|
|
||||||
const res = await app.request(`/session/${session.id}/abort`, {
|
const res = await app.request(`/session/${session.id}/abort`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -66,7 +66,7 @@ describe("session action routes", () => {
|
||||||
const msg = await user(session.id, "hello")
|
const msg = await user(session.id, "hello")
|
||||||
const busy = spyOn(SessionPrompt, "assertNotBusy").mockRejectedValue(new Session.BusyError(session.id))
|
const busy = spyOn(SessionPrompt, "assertNotBusy").mockRejectedValue(new Session.BusyError(session.id))
|
||||||
const remove = spyOn(Session, "removeMessage").mockResolvedValue(msg.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}`, {
|
const res = await app.request(`/session/${session.id}/message/${msg.id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ describe("session messages endpoint", () => {
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const session = await Session.create({})
|
const session = await Session.create({})
|
||||||
const ids = await fill(session.id, 5)
|
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`)
|
const a = await app.request(`/session/${session.id}/message?limit=2`)
|
||||||
expect(a.status).toBe(200)
|
expect(a.status).toBe(200)
|
||||||
|
|
@ -89,7 +89,7 @@ describe("session messages endpoint", () => {
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const session = await Session.create({})
|
const session = await Session.create({})
|
||||||
const ids = await fill(session.id, 3)
|
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`)
|
const res = await app.request(`/session/${session.id}/message`)
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
|
|
@ -109,7 +109,7 @@ describe("session messages endpoint", () => {
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const session = await Session.create({})
|
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`)
|
const bad = await app.request(`/session/${session.id}/message?limit=2&before=bad`)
|
||||||
expect(bad.status).toBe(400)
|
expect(bad.status).toBe(400)
|
||||||
|
|
@ -131,7 +131,7 @@ describe("session messages endpoint", () => {
|
||||||
fn: async () => {
|
fn: async () => {
|
||||||
const session = await Session.create({})
|
const session = await Session.create({})
|
||||||
await fill(session.id, 520)
|
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`)
|
const res = await app.request(`/session/${session.id}/message?limit=510`)
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ describe("tui.selectSession endpoint", () => {
|
||||||
const session = await Session.create({})
|
const session = await Session.create({})
|
||||||
|
|
||||||
// #when
|
// #when
|
||||||
const app = Server.Default()
|
const app = Server.Default().app
|
||||||
const response = await app.request("/tui/select-session", {
|
const response = await app.request("/tui/select-session", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|
@ -47,7 +47,7 @@ describe("tui.selectSession endpoint", () => {
|
||||||
const nonExistentSessionID = "ses_nonexistent123"
|
const nonExistentSessionID = "ses_nonexistent123"
|
||||||
|
|
||||||
// #when
|
// #when
|
||||||
const app = Server.Default()
|
const app = Server.Default().app
|
||||||
const response = await app.request("/tui/select-session", {
|
const response = await app.request("/tui/select-session", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|
@ -69,7 +69,7 @@ describe("tui.selectSession endpoint", () => {
|
||||||
const invalidSessionID = "invalid_session_id"
|
const invalidSessionID = "invalid_session_id"
|
||||||
|
|
||||||
// #when
|
// #when
|
||||||
const app = Server.Default()
|
const app = Server.Default().app
|
||||||
const response = await app.request("/tui/select-session", {
|
const response = await app.request("/tui/select-session", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||||
import { Database } from "bun:sqlite"
|
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 { migrate } from "drizzle-orm/bun-sqlite/migrator"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
|
|
@ -89,18 +89,21 @@ function createTestDb() {
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.timestamp - b.timestamp)
|
.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", () => {
|
describe("JSON to SQLite migration", () => {
|
||||||
let storageDir: string
|
let storageDir: string
|
||||||
let sqlite: Database
|
let sqlite: Database
|
||||||
|
let db: SQLiteBunDatabase
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
storageDir = await setupStorageDir()
|
storageDir = await setupStorageDir()
|
||||||
sqlite = createTestDb()
|
;[sqlite, db] = createTestDb()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
|
@ -118,11 +121,10 @@ describe("JSON to SQLite migration", () => {
|
||||||
sandboxes: ["/test/sandbox"],
|
sandboxes: ["/test/sandbox"],
|
||||||
})
|
})
|
||||||
|
|
||||||
const stats = await JsonMigration.run(sqlite)
|
const stats = await JsonMigration.run(db)
|
||||||
|
|
||||||
expect(stats?.projects).toBe(1)
|
expect(stats?.projects).toBe(1)
|
||||||
|
|
||||||
const db = drizzle({ client: sqlite })
|
|
||||||
const projects = db.select().from(ProjectTable).all()
|
const projects = db.select().from(ProjectTable).all()
|
||||||
expect(projects.length).toBe(1)
|
expect(projects.length).toBe(1)
|
||||||
expect(projects[0].id).toBe(ProjectID.make("proj_test123abc"))
|
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)
|
expect(stats?.projects).toBe(1)
|
||||||
|
|
||||||
const db = drizzle({ client: sqlite })
|
|
||||||
const projects = db.select().from(ProjectTable).all()
|
const projects = db.select().from(ProjectTable).all()
|
||||||
expect(projects.length).toBe(1)
|
expect(projects.length).toBe(1)
|
||||||
expect(projects[0].id).toBe(ProjectID.make("proj_filename")) // Uses filename, not JSON id
|
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" },
|
commands: { start: "npm run dev" },
|
||||||
})
|
})
|
||||||
|
|
||||||
const stats = await JsonMigration.run(sqlite)
|
const stats = await JsonMigration.run(db)
|
||||||
|
|
||||||
expect(stats?.projects).toBe(1)
|
expect(stats?.projects).toBe(1)
|
||||||
|
|
||||||
const db = drizzle({ client: sqlite })
|
|
||||||
const projects = db.select().from(ProjectTable).all()
|
const projects = db.select().from(ProjectTable).all()
|
||||||
expect(projects.length).toBe(1)
|
expect(projects.length).toBe(1)
|
||||||
expect(projects[0].id).toBe(ProjectID.make("proj_with_commands"))
|
expect(projects[0].id).toBe(ProjectID.make("proj_with_commands"))
|
||||||
|
|
@ -185,11 +185,10 @@ describe("JSON to SQLite migration", () => {
|
||||||
sandboxes: [],
|
sandboxes: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
const stats = await JsonMigration.run(sqlite)
|
const stats = await JsonMigration.run(db)
|
||||||
|
|
||||||
expect(stats?.projects).toBe(1)
|
expect(stats?.projects).toBe(1)
|
||||||
|
|
||||||
const db = drizzle({ client: sqlite })
|
|
||||||
const projects = db.select().from(ProjectTable).all()
|
const projects = db.select().from(ProjectTable).all()
|
||||||
expect(projects.length).toBe(1)
|
expect(projects.length).toBe(1)
|
||||||
expect(projects[0].id).toBe(ProjectID.make("proj_no_commands"))
|
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" },
|
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()
|
const sessions = db.select().from(SessionTable).all()
|
||||||
expect(sessions.length).toBe(1)
|
expect(sessions.length).toBe(1)
|
||||||
expect(sessions[0].id).toBe(SessionID.make("ses_test456def"))
|
expect(sessions[0].id).toBe(SessionID.make("ses_test456def"))
|
||||||
|
|
@ -247,12 +245,11 @@ describe("JSON to SQLite migration", () => {
|
||||||
JSON.stringify({ ...fixtures.part }),
|
JSON.stringify({ ...fixtures.part }),
|
||||||
)
|
)
|
||||||
|
|
||||||
const stats = await JsonMigration.run(sqlite)
|
const stats = await JsonMigration.run(db)
|
||||||
|
|
||||||
expect(stats?.messages).toBe(1)
|
expect(stats?.messages).toBe(1)
|
||||||
expect(stats?.parts).toBe(1)
|
expect(stats?.parts).toBe(1)
|
||||||
|
|
||||||
const db = drizzle({ client: sqlite })
|
|
||||||
const messages = db.select().from(MessageTable).all()
|
const messages = db.select().from(MessageTable).all()
|
||||||
expect(messages.length).toBe(1)
|
expect(messages.length).toBe(1)
|
||||||
expect(messages[0].id).toBe(MessageID.make("msg_test789ghi"))
|
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?.messages).toBe(1)
|
||||||
expect(stats?.parts).toBe(1)
|
expect(stats?.parts).toBe(1)
|
||||||
|
|
||||||
const db = drizzle({ client: sqlite })
|
|
||||||
const messages = db.select().from(MessageTable).all()
|
const messages = db.select().from(MessageTable).all()
|
||||||
expect(messages.length).toBe(1)
|
expect(messages.length).toBe(1)
|
||||||
expect(messages[0].id).toBe(MessageID.make("msg_test789ghi"))
|
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)
|
expect(stats?.messages).toBe(1)
|
||||||
|
|
||||||
const db = drizzle({ client: sqlite })
|
|
||||||
const messages = db.select().from(MessageTable).all()
|
const messages = db.select().from(MessageTable).all()
|
||||||
expect(messages.length).toBe(1)
|
expect(messages.length).toBe(1)
|
||||||
expect(messages[0].id).toBe(MessageID.make("msg_from_filename")) // Uses filename, not JSON id
|
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)
|
expect(stats?.parts).toBe(1)
|
||||||
|
|
||||||
const db = drizzle({ client: sqlite })
|
|
||||||
const parts = db.select().from(PartTable).all()
|
const parts = db.select().from(PartTable).all()
|
||||||
expect(parts.length).toBe(1)
|
expect(parts.length).toBe(1)
|
||||||
expect(parts[0].id).toBe(PartID.make("prt_from_filename")) // Uses filename, not JSON id
|
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)
|
expect(stats?.sessions).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
@ -420,11 +414,10 @@ describe("JSON to SQLite migration", () => {
|
||||||
time: { created: 1700000000000, updated: 1700000001000 },
|
time: { created: 1700000000000, updated: 1700000001000 },
|
||||||
})
|
})
|
||||||
|
|
||||||
const stats = await JsonMigration.run(sqlite)
|
const stats = await JsonMigration.run(db)
|
||||||
|
|
||||||
expect(stats?.sessions).toBe(1)
|
expect(stats?.sessions).toBe(1)
|
||||||
|
|
||||||
const db = drizzle({ client: sqlite })
|
|
||||||
const sessions = db.select().from(SessionTable).all()
|
const sessions = db.select().from(SessionTable).all()
|
||||||
expect(sessions.length).toBe(1)
|
expect(sessions.length).toBe(1)
|
||||||
expect(sessions[0].id).toBe(SessionID.make("ses_migrated"))
|
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)
|
expect(stats?.sessions).toBe(1)
|
||||||
|
|
||||||
const db = drizzle({ client: sqlite })
|
|
||||||
const sessions = db.select().from(SessionTable).all()
|
const sessions = db.select().from(SessionTable).all()
|
||||||
expect(sessions.length).toBe(1)
|
expect(sessions.length).toBe(1)
|
||||||
expect(sessions[0].id).toBe(SessionID.make("ses_from_filename")) // Uses filename, not JSON id
|
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: [],
|
sandboxes: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
await JsonMigration.run(sqlite)
|
await JsonMigration.run(db)
|
||||||
await JsonMigration.run(sqlite)
|
await JsonMigration.run(db)
|
||||||
|
|
||||||
const db = drizzle({ client: sqlite })
|
|
||||||
const projects = db.select().from(ProjectTable).all()
|
const projects = db.select().from(ProjectTable).all()
|
||||||
expect(projects.length).toBe(1) // Still only 1 due to onConflictDoNothing
|
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)
|
expect(stats?.todos).toBe(2)
|
||||||
|
|
||||||
const db = drizzle({ client: sqlite })
|
|
||||||
const todos = db.select().from(TodoTable).orderBy(TodoTable.position).all()
|
const todos = db.select().from(TodoTable).orderBy(TodoTable.position).all()
|
||||||
expect(todos.length).toBe(2)
|
expect(todos.length).toBe(2)
|
||||||
expect(todos[0].content).toBe("First todo")
|
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()
|
const todos = db.select().from(TodoTable).orderBy(TodoTable.position).all()
|
||||||
|
|
||||||
expect(todos.length).toBe(3)
|
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))
|
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)
|
expect(stats?.permissions).toBe(1)
|
||||||
|
|
||||||
const db = drizzle({ client: sqlite })
|
|
||||||
const permissions = db.select().from(PermissionTable).all()
|
const permissions = db.select().from(PermissionTable).all()
|
||||||
expect(permissions.length).toBe(1)
|
expect(permissions.length).toBe(1)
|
||||||
expect(permissions[0].project_id).toBe("proj_test123abc")
|
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)
|
expect(stats?.shares).toBe(1)
|
||||||
|
|
||||||
const db = drizzle({ client: sqlite })
|
|
||||||
const shares = db.select().from(SessionShareTable).all()
|
const shares = db.select().from(SessionShareTable).all()
|
||||||
expect(shares.length).toBe(1)
|
expect(shares.length).toBe(1)
|
||||||
expect(shares[0].session_id).toBe("ses_test456def")
|
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 () => {
|
test("returns empty stats when storage directory does not exist", async () => {
|
||||||
await fs.rm(storageDir, { recursive: true, force: true })
|
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.projects).toBe(0)
|
||||||
expect(stats.sessions).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")
|
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.projects).toBe(1)
|
||||||
expect(stats.errors.some((x) => x.includes("failed to read") && x.includes("broken.json"))).toBe(true)
|
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()
|
const projects = db.select().from(ProjectTable).all()
|
||||||
expect(projects.length).toBe(1)
|
expect(projects.length).toBe(1)
|
||||||
expect(projects[0].id).toBe(ProjectID.make("proj_test123abc"))
|
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)
|
expect(stats.todos).toBe(2)
|
||||||
|
|
||||||
const db = drizzle({ client: sqlite })
|
|
||||||
const todos = db.select().from(TodoTable).orderBy(TodoTable.position).all()
|
const todos = db.select().from(TodoTable).orderBy(TodoTable.position).all()
|
||||||
expect(todos.length).toBe(2)
|
expect(todos.length).toBe(2)
|
||||||
expect(todos[0].content).toBe("keep-0")
|
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" }),
|
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.todos).toBe(1)
|
||||||
expect(stats.permissions).toBe(1)
|
expect(stats.permissions).toBe(1)
|
||||||
expect(stats.shares).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(TodoTable).all().length).toBe(1)
|
||||||
expect(db.select().from(PermissionTable).all().length).toBe(1)
|
expect(db.select().from(PermissionTable).all().length).toBe(1)
|
||||||
expect(db.select().from(SessionShareTable).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")
|
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)
|
// Projects: proj_test123abc (valid), proj_missing_id (now derives id from filename)
|
||||||
// Sessions: ses_test456def (valid), ses_missing_project (now uses dir path),
|
// 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.shares).toBe(1)
|
||||||
expect(stats.errors.length).toBeGreaterThanOrEqual(6)
|
expect(stats.errors.length).toBeGreaterThanOrEqual(6)
|
||||||
|
|
||||||
const db = drizzle({ client: sqlite })
|
|
||||||
expect(db.select().from(ProjectTable).all().length).toBe(2)
|
expect(db.select().from(ProjectTable).all().length).toBe(2)
|
||||||
expect(db.select().from(SessionTable).all().length).toBe(3)
|
expect(db.select().from(SessionTable).all().length).toBe(3)
|
||||||
expect(db.select().from(MessageTable).all().length).toBe(1)
|
expect(db.select().from(MessageTable).all().length).toBe(1)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue