浏览代码

refactor(flags): move channel db flag to runtime flags (#27615)

Shoubhit Dash 3 月之前
父节点
当前提交
fc34c74567

+ 0 - 1
packages/core/src/flag/flag.ts

@@ -62,7 +62,6 @@ export const Flag = {
   OPENCODE_MODELS_URL: process.env["OPENCODE_MODELS_URL"],
   OPENCODE_MODELS_URL: process.env["OPENCODE_MODELS_URL"],
   OPENCODE_MODELS_PATH: process.env["OPENCODE_MODELS_PATH"],
   OPENCODE_MODELS_PATH: process.env["OPENCODE_MODELS_PATH"],
   OPENCODE_DB: process.env["OPENCODE_DB"],
   OPENCODE_DB: process.env["OPENCODE_DB"],
-  OPENCODE_DISABLE_CHANNEL_DB: truthy("OPENCODE_DISABLE_CHANNEL_DB"),
   OPENCODE_SKIP_MIGRATIONS: truthy("OPENCODE_SKIP_MIGRATIONS"),
   OPENCODE_SKIP_MIGRATIONS: truthy("OPENCODE_SKIP_MIGRATIONS"),
   OPENCODE_STRICT_CONFIG_DEPS: truthy("OPENCODE_STRICT_CONFIG_DEPS"),
   OPENCODE_STRICT_CONFIG_DEPS: truthy("OPENCODE_STRICT_CONFIG_DEPS"),
 
 

+ 1 - 1
packages/desktop/src/main/env.d.ts

@@ -19,7 +19,7 @@ declare module "virtual:opencode-server" {
     export const init: typeof import("../../../opencode/dist/types/src/node").Log.init
     export const init: typeof import("../../../opencode/dist/types/src/node").Log.init
   }
   }
   export namespace Database {
   export namespace Database {
-    export const Path: typeof import("../../../opencode/dist/types/src/node").Database.Path
+    export const getPath: typeof import("../../../opencode/dist/types/src/node").Database.getPath
     export const Client: typeof import("../../../opencode/dist/types/src/node").Database.Client
     export const Client: typeof import("../../../opencode/dist/types/src/node").Database.Client
   }
   }
   export namespace JsonMigration {
   export namespace JsonMigration {

+ 4 - 4
packages/opencode/src/cli/cmd/db.ts

@@ -28,7 +28,7 @@ const QueryCommand = cmd({
   handler: async (args: { query?: string; format: string }) => {
   handler: async (args: { query?: string; format: string }) => {
     const query = args.query as string | undefined
     const query = args.query as string | undefined
     if (query) {
     if (query) {
-      const db = new BunDatabase(Database.Path, { readonly: true })
+      const db = new BunDatabase(Database.getPath(), { readonly: true })
       try {
       try {
         const result = db.query(query).all() as Record<string, unknown>[]
         const result = db.query(query).all() as Record<string, unknown>[]
         if (args.format === "json") {
         if (args.format === "json") {
@@ -47,7 +47,7 @@ const QueryCommand = cmd({
       db.close()
       db.close()
       return
       return
     }
     }
-    const child = spawn("sqlite3", [Database.Path], {
+    const child = spawn("sqlite3", [Database.getPath()], {
       stdio: "inherit",
       stdio: "inherit",
     })
     })
     await new Promise((resolve) => child.on("close", resolve))
     await new Promise((resolve) => child.on("close", resolve))
@@ -58,7 +58,7 @@ const PathCommand = cmd({
   command: "path",
   command: "path",
   describe: "print the database path",
   describe: "print the database path",
   handler: () => {
   handler: () => {
-    console.log(Database.Path)
+    console.log(Database.getPath())
   },
   },
 })
 })
 
 
@@ -66,7 +66,7 @@ const MigrateCommand = cmd({
   command: "migrate",
   command: "migrate",
   describe: "migrate JSON data to SQLite (merges with existing data)",
   describe: "migrate JSON data to SQLite (merges with existing data)",
   handler: async () => {
   handler: async () => {
-    const sqlite = new BunDatabase(Database.Path)
+    const sqlite = new BunDatabase(Database.getPath())
     const tty = process.stderr.isTTY
     const tty = process.stderr.isTTY
     const width = 36
     const width = 36
     const orange = "\x1b[38;5;214m"
     const orange = "\x1b[38;5;214m"

+ 1 - 0
packages/opencode/src/effect/runtime-flags.ts

@@ -15,6 +15,7 @@ export class Service extends ConfigService.Service<Service>()("@opencode/Runtime
   autoShare: bool("OPENCODE_AUTO_SHARE"),
   autoShare: bool("OPENCODE_AUTO_SHARE"),
   pure: bool("OPENCODE_PURE"),
   pure: bool("OPENCODE_PURE"),
   disableDefaultPlugins: bool("OPENCODE_DISABLE_DEFAULT_PLUGINS"),
   disableDefaultPlugins: bool("OPENCODE_DISABLE_DEFAULT_PLUGINS"),
+  disableChannelDb: bool("OPENCODE_DISABLE_CHANNEL_DB"),
   disableEmbeddedWebUi: bool("OPENCODE_DISABLE_EMBEDDED_WEB_UI"),
   disableEmbeddedWebUi: bool("OPENCODE_DISABLE_EMBEDDED_WEB_UI"),
   disableClaudeCodeSkills: Config.all({
   disableClaudeCodeSkills: Config.all({
     broad: bool("OPENCODE_DISABLE_CLAUDE_CODE"),
     broad: bool("OPENCODE_DISABLE_CLAUDE_CODE"),

+ 59 - 38
packages/opencode/src/storage/db.ts

@@ -2,8 +2,8 @@ import { type SQLiteBunDatabase } from "drizzle-orm/bun-sqlite"
 import { migrate } from "drizzle-orm/bun-sqlite/migrator"
 import { migrate } from "drizzle-orm/bun-sqlite/migrator"
 import { type SQLiteTransaction } from "drizzle-orm/sqlite-core"
 import { type SQLiteTransaction } from "drizzle-orm/sqlite-core"
 export * from "drizzle-orm"
 export * from "drizzle-orm"
+import { RuntimeFlags } from "@/effect/runtime-flags"
 import { LocalContext } from "@/util/local-context"
 import { LocalContext } from "@/util/local-context"
-import { lazy } from "../util/lazy"
 import { Global } from "@opencode-ai/core/global"
 import { Global } from "@opencode-ai/core/global"
 import * as Log from "@opencode-ai/core/util/log"
 import * as Log from "@opencode-ai/core/util/log"
 import { NamedError } from "@opencode-ai/core/util/error"
 import { NamedError } from "@opencode-ai/core/util/error"
@@ -12,9 +12,8 @@ import { readFileSync, readdirSync, existsSync } from "fs"
 import { Flag } from "@opencode-ai/core/flag/flag"
 import { Flag } from "@opencode-ai/core/flag/flag"
 import { InstallationChannel } from "@opencode-ai/core/installation/version"
 import { InstallationChannel } from "@opencode-ai/core/installation/version"
 import { InstanceState } from "@/effect/instance-state"
 import { InstanceState } from "@/effect/instance-state"
-import { iife } from "@/util/iife"
 import { init } from "#db"
 import { init } from "#db"
-import { Schema } from "effect"
+import { Effect, Schema } from "effect"
 
 
 declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number; name: string }[] | undefined
 declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number; name: string }[] | undefined
 
 
@@ -24,24 +23,29 @@ export const NotFoundError = NamedError.create("NotFoundError", {
 
 
 const log = Log.create({ service: "db" })
 const log = Log.create({ service: "db" })
 
 
-export function getChannelPath() {
-  if (["latest", "beta", "prod"].includes(InstallationChannel) || Flag.OPENCODE_DISABLE_CHANNEL_DB)
+type ChannelDbFlags = Pick<RuntimeFlags.Info, "disableChannelDb">
+
+const readRuntimeFlags = () =>
+  Effect.runSync(RuntimeFlags.Service.useSync((flags) => flags).pipe(Effect.provide(RuntimeFlags.defaultLayer)))
+
+export function getChannelPath(flags: ChannelDbFlags = readRuntimeFlags()) {
+  if (["latest", "beta", "prod"].includes(InstallationChannel) || flags.disableChannelDb)
     return path.join(Global.Path.data, "opencode.db")
     return path.join(Global.Path.data, "opencode.db")
   const safe = InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")
   const safe = InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")
   return path.join(Global.Path.data, `opencode-${safe}.db`)
   return path.join(Global.Path.data, `opencode-${safe}.db`)
 }
 }
 
 
-export const Path = iife(() => {
+export const getPath = (flags?: ChannelDbFlags) => {
   if (Flag.OPENCODE_DB) {
   if (Flag.OPENCODE_DB) {
     if (Flag.OPENCODE_DB === ":memory:" || path.isAbsolute(Flag.OPENCODE_DB)) return Flag.OPENCODE_DB
     if (Flag.OPENCODE_DB === ":memory:" || path.isAbsolute(Flag.OPENCODE_DB)) return Flag.OPENCODE_DB
     return path.join(Global.Path.data, Flag.OPENCODE_DB)
     return path.join(Global.Path.data, Flag.OPENCODE_DB)
   }
   }
-  return getChannelPath()
-})
+  return getChannelPath(flags)
+}
 
 
 export type Transaction = SQLiteTransaction<"sync", void>
 export type Transaction = SQLiteTransaction<"sync", void>
 
 
-type Client = SQLiteBunDatabase
+type Client = ReturnType<typeof init>
 
 
 type Journal = { sql: string; timestamp: number; name: string }[]
 type Journal = { sql: string; timestamp: number; name: string }[]
 
 
@@ -85,38 +89,55 @@ function migrations(dir: string): Journal {
   return sql.sort((a, b) => a.timestamp - b.timestamp)
   return sql.sort((a, b) => a.timestamp - b.timestamp)
 }
 }
 
 
-export const Client = lazy(() => {
-  log.info("opening database", { path: Path })
-
-  const db = init(Path)
-
-  db.run("PRAGMA journal_mode = WAL")
-  db.run("PRAGMA synchronous = NORMAL")
-  db.run("PRAGMA busy_timeout = 5000")
-  db.run("PRAGMA cache_size = -64000")
-  db.run("PRAGMA foreign_keys = ON")
-  db.run("PRAGMA wal_checkpoint(PASSIVE)")
-
-  // Apply schema migrations
-  const entries =
-    typeof OPENCODE_MIGRATIONS !== "undefined"
-      ? OPENCODE_MIGRATIONS
-      : migrations(path.join(import.meta.dirname, "../../migration"))
-  if (entries.length > 0) {
-    log.info("applying migrations", {
-      count: entries.length,
-      mode: typeof OPENCODE_MIGRATIONS !== "undefined" ? "bundled" : "dev",
-    })
-    if (Flag.OPENCODE_SKIP_MIGRATIONS) {
-      for (const item of entries) {
-        item.sql = "select 1;"
+let client: Client | undefined
+let loaded = false
+
+export const Client = Object.assign(
+  (flags?: ChannelDbFlags): Client => {
+    if (loaded) return client as Client
+
+    const dbPath = getPath(flags)
+    log.info("opening database", { path: dbPath })
+
+    const db = init(dbPath)
+
+    db.run("PRAGMA journal_mode = WAL")
+    db.run("PRAGMA synchronous = NORMAL")
+    db.run("PRAGMA busy_timeout = 5000")
+    db.run("PRAGMA cache_size = -64000")
+    db.run("PRAGMA foreign_keys = ON")
+    db.run("PRAGMA wal_checkpoint(PASSIVE)")
+
+    // Apply schema migrations
+    const entries =
+      typeof OPENCODE_MIGRATIONS !== "undefined"
+        ? OPENCODE_MIGRATIONS
+        : migrations(path.join(import.meta.dirname, "../../migration"))
+    if (entries.length > 0) {
+      log.info("applying migrations", {
+        count: entries.length,
+        mode: typeof OPENCODE_MIGRATIONS !== "undefined" ? "bundled" : "dev",
+      })
+      if (Flag.OPENCODE_SKIP_MIGRATIONS) {
+        for (const item of entries) {
+          item.sql = "select 1;"
+        }
       }
       }
+      applyMigrations(db, entries)
     }
     }
-    applyMigrations(db, entries)
-  }
 
 
-  return db
-})
+    client = db
+    loaded = true
+    return db
+  },
+  {
+    reset: () => {
+      loaded = false
+      client = undefined
+    },
+    loaded: () => loaded,
+  },
+)
 
 
 export function close() {
 export function close() {
   if (!Client.loaded()) return
   if (!Client.loaded()) return

+ 4 - 0
packages/opencode/test/effect/runtime-flags.test.ts

@@ -24,6 +24,7 @@ describe("RuntimeFlags", () => {
           fromConfig({
           fromConfig({
             OPENCODE_PURE: "true",
             OPENCODE_PURE: "true",
             OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
             OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
+            OPENCODE_DISABLE_CHANNEL_DB: "true",
             OPENCODE_AUTO_SHARE: "true",
             OPENCODE_AUTO_SHARE: "true",
             OPENCODE_DISABLE_EMBEDDED_WEB_UI: "true",
             OPENCODE_DISABLE_EMBEDDED_WEB_UI: "true",
             OPENCODE_EXPERIMENTAL: "true",
             OPENCODE_EXPERIMENTAL: "true",
@@ -39,6 +40,7 @@ describe("RuntimeFlags", () => {
       expect(flags.pure).toBe(true)
       expect(flags.pure).toBe(true)
       expect(flags.autoShare).toBe(true)
       expect(flags.autoShare).toBe(true)
       expect(flags.disableDefaultPlugins).toBe(true)
       expect(flags.disableDefaultPlugins).toBe(true)
+      expect(flags.disableChannelDb).toBe(true)
       expect(flags.disableEmbeddedWebUi).toBe(true)
       expect(flags.disableEmbeddedWebUi).toBe(true)
       expect(flags.enableExa).toBe(true)
       expect(flags.enableExa).toBe(true)
       expect(flags.enableParallel).toBe(true)
       expect(flags.enableParallel).toBe(true)
@@ -80,6 +82,7 @@ describe("RuntimeFlags", () => {
       expect(flags.pure).toBe(false)
       expect(flags.pure).toBe(false)
       expect(flags.autoShare).toBe(false)
       expect(flags.autoShare).toBe(false)
       expect(flags.disableDefaultPlugins).toBe(true)
       expect(flags.disableDefaultPlugins).toBe(true)
+      expect(flags.disableChannelDb).toBe(false)
       expect(flags.disableEmbeddedWebUi).toBe(false)
       expect(flags.disableEmbeddedWebUi).toBe(false)
       expect(flags.disableClaudeCodeSkills).toBe(false)
       expect(flags.disableClaudeCodeSkills).toBe(false)
       expect(flags.enableExa).toBe(false)
       expect(flags.enableExa).toBe(false)
@@ -200,6 +203,7 @@ describe("RuntimeFlags", () => {
 
 
       expect(flags.pure).toBe(false)
       expect(flags.pure).toBe(false)
       expect(flags.disableDefaultPlugins).toBe(false)
       expect(flags.disableDefaultPlugins).toBe(false)
+      expect(flags.disableChannelDb).toBe(false)
       expect(flags.disableEmbeddedWebUi).toBe(false)
       expect(flags.disableEmbeddedWebUi).toBe(false)
       expect(flags.disableClaudeCodeSkills).toBe(false)
       expect(flags.disableClaudeCodeSkills).toBe(false)
       expect(flags.enableExa).toBe(false)
       expect(flags.enableExa).toBe(false)

+ 4 - 3
packages/opencode/test/fixture/db.ts

@@ -5,7 +5,8 @@ import { disposeAllInstances } from "./fixture"
 export async function resetDatabase() {
 export async function resetDatabase() {
   await disposeAllInstances().catch(() => undefined)
   await disposeAllInstances().catch(() => undefined)
   Database.close()
   Database.close()
-  await rm(Database.Path, { force: true }).catch(() => undefined)
-  await rm(`${Database.Path}-wal`, { force: true }).catch(() => undefined)
-  await rm(`${Database.Path}-shm`, { force: true }).catch(() => undefined)
+  const dbPath = Database.getPath()
+  await rm(dbPath, { force: true }).catch(() => undefined)
+  await rm(`${dbPath}-wal`, { force: true }).catch(() => undefined)
+  await rm(`${dbPath}-shm`, { force: true }).catch(() => undefined)
 }
 }

+ 23 - 8
packages/opencode/test/storage/db.test.ts

@@ -1,14 +1,29 @@
-import { describe, expect, test } from "bun:test"
+import { describe, expect } from "bun:test"
 import path from "path"
 import path from "path"
+import { Effect } from "effect"
 import { Global } from "@opencode-ai/core/global"
 import { Global } from "@opencode-ai/core/global"
 import { InstallationChannel } from "@opencode-ai/core/installation/version"
 import { InstallationChannel } from "@opencode-ai/core/installation/version"
+import { RuntimeFlags } from "@/effect/runtime-flags"
 import { Database } from "@/storage/db"
 import { Database } from "@/storage/db"
+import { it } from "../lib/effect"
 
 
-describe("Database.Path", () => {
-  test("returns database path for the current channel", () => {
-    const expected = ["latest", "beta"].includes(InstallationChannel)
-      ? path.join(Global.Path.data, "opencode.db")
-      : path.join(Global.Path.data, `opencode-${InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`)
-    expect(Database.getChannelPath()).toBe(expected)
-  })
+describe("Database.getChannelPath", () => {
+  it.effect("returns database path for the current channel", () =>
+    Effect.gen(function* () {
+      const flags = yield* RuntimeFlags.Service
+      const expected = ["latest", "beta", "prod"].includes(InstallationChannel)
+        ? path.join(Global.Path.data, "opencode.db")
+        : path.join(Global.Path.data, `opencode-${InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`)
+
+      expect(Database.getChannelPath(flags)).toBe(expected)
+    }).pipe(Effect.provide(RuntimeFlags.layer())),
+  )
+
+  it.effect("uses the shared database path when channel databases are disabled", () =>
+    Effect.gen(function* () {
+      const flags = yield* RuntimeFlags.Service
+
+      expect(Database.getChannelPath(flags)).toBe(path.join(Global.Path.data, "opencode.db"))
+    }).pipe(Effect.provide(RuntimeFlags.layer({ disableChannelDb: true }))),
+  )
 })
 })