diff --git a/packages/cli/src/server-process.ts b/packages/cli/src/server-process.ts
index 6cc4b7e7dd..30504dc03f 100644
--- a/packages/cli/src/server-process.ts
+++ b/packages/cli/src/server-process.ts
@@ -65,6 +65,9 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
port: Option.fromNullishOr(port),
password,
instanceID,
+ database: {
+ path: process.env.OPENCODE_DB,
+ },
service:
serviceOptions === undefined
? undefined
diff --git a/packages/cli/test/service.test.ts b/packages/cli/test/service.test.ts
index 707a3fbdb7..2194bfe20d 100644
--- a/packages/cli/test/service.test.ts
+++ b/packages/cli/test/service.test.ts
@@ -511,7 +511,7 @@ test("a failed service stays registered and owns the selected port until stopped
}, 30_000)
function withDatabase(file: string, effect: Effect.Effect) {
- return Effect.runPromise(effect.pipe(Effect.provide(Database.layerFromPath(file)), Effect.scoped))
+ return Effect.runPromise(effect.pipe(Effect.provide(Database.layer({ path: file })), Effect.scoped))
}
function waitForExecutionStart(file: string, sessionID: SessionV2.ID) {
diff --git a/packages/core/src/database/database.ts b/packages/core/src/database/database.ts
index e6c36d75b8..4693cfa136 100644
--- a/packages/core/src/database/database.ts
+++ b/packages/core/src/database/database.ts
@@ -1,10 +1,9 @@
export * as Database from "./database"
import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
-import { layer } from "#sqlite"
+import { sqliteLayer } from "#sqlite"
import { Context, Effect, Layer } from "effect"
import { Global } from "../global"
-import { Flag } from "../flag/flag"
import { isAbsolute, join } from "path"
import { DatabaseMigration } from "./migration"
import { InstallationChannel } from "../installation/version"
@@ -17,6 +16,10 @@ export interface Interface {
db: DatabaseShape
}
+export interface Options {
+ readonly path?: string
+}
+
export class Service extends Context.Service()("@opencode/v2/storage/Database") {}
const databaseLayer = Layer.effect(
@@ -36,28 +39,25 @@ const databaseLayer = Layer.effect(
}).pipe(Effect.orDie),
)
-export function layerFromPath(filename: string) {
- return databaseLayer.pipe(Layer.provide(layer({ filename })))
+export function layer(options?: Options) {
+ return Layer.suspend(() => {
+ const provide = (filename: string) => databaseLayer.pipe(Layer.provide(sqliteLayer({ filename })))
+ if (options?.path === ":memory:" || (options?.path && isAbsolute(options.path))) return provide(options.path)
+ if (options?.path) return provide(join(Global.Path.data, options.path))
+ if (
+ ["latest", "beta", "prod"].includes(InstallationChannel) ||
+ process.env.OPENCODE_DISABLE_CHANNEL_DB === "1" ||
+ process.env.OPENCODE_DISABLE_CHANNEL_DB === "true"
+ )
+ return provide(join(Global.Path.data, "opencode.db"))
+ return provide(
+ join(Global.Path.data, `opencode-${InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`),
+ )
+ })
}
-export function path() {
- if (Flag.OPENCODE_DB) {
- if (Flag.OPENCODE_DB === ":memory:" || isAbsolute(Flag.OPENCODE_DB)) return Flag.OPENCODE_DB
- return join(Global.Path.data, Flag.OPENCODE_DB)
- }
- if (
- ["latest", "beta", "prod"].includes(InstallationChannel) ||
- process.env.OPENCODE_DISABLE_CHANNEL_DB === "1" ||
- process.env.OPENCODE_DISABLE_CHANNEL_DB === "true"
- )
- return join(Global.Path.data, "opencode.db")
- return join(Global.Path.data, `opencode-${InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`)
-}
-
-// Resolve the database path lazily so tests and embedders that set
-// Flag.OPENCODE_DB after module evaluation still control the storage target.
export const node = makeGlobalNode({
service: Service,
- layer: Layer.suspend(() => layerFromPath(path())),
+ layer: layer(),
deps: [],
})
diff --git a/packages/core/src/database/sqlite.bun.ts b/packages/core/src/database/sqlite.bun.ts
index c1fa349fcd..124b01d84f 100644
--- a/packages/core/src/database/sqlite.bun.ts
+++ b/packages/core/src/database/sqlite.bun.ts
@@ -162,7 +162,7 @@ const nativeLayer = (config: Config) =>
}),
)
-const sqliteLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config))
+const clientLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config))
const drizzleLayer = Layer.effect(
Sqlite.Drizzle,
@@ -171,9 +171,9 @@ const drizzleLayer = Layer.effect(
}),
)
-export const layer = (config: Config) => {
+export const sqliteLayer = (config: Config) => {
const native = nativeLayer(config)
- return Layer.merge(native, Layer.merge(sqliteLayer(config), drizzleLayer).pipe(Layer.provide(native))).pipe(
+ return Layer.merge(native, Layer.merge(clientLayer(config), drizzleLayer).pipe(Layer.provide(native))).pipe(
Layer.provide(Reactivity.layer),
)
}
diff --git a/packages/core/src/database/sqlite.node.ts b/packages/core/src/database/sqlite.node.ts
index a9e0f04660..71b5421e3c 100644
--- a/packages/core/src/database/sqlite.node.ts
+++ b/packages/core/src/database/sqlite.node.ts
@@ -157,7 +157,7 @@ const nativeLayer = (config: Config) =>
}),
)
-const sqliteLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config))
+const clientLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config))
const drizzleLayer = Layer.effect(
Sqlite.Drizzle,
@@ -166,9 +166,9 @@ const drizzleLayer = Layer.effect(
}),
)
-export const layer = (config: Config) => {
+export const sqliteLayer = (config: Config) => {
const native = nativeLayer(config)
- return Layer.merge(native, Layer.merge(sqliteLayer(config), drizzleLayer).pipe(Layer.provide(native))).pipe(
+ return Layer.merge(native, Layer.merge(clientLayer(config), drizzleLayer).pipe(Layer.provide(native))).pipe(
Layer.provide(Reactivity.layer),
)
}
diff --git a/packages/core/test/database-migration.test.ts b/packages/core/test/database-migration.test.ts
index 326855ab51..d4d76c2a23 100644
--- a/packages/core/test/database-migration.test.ts
+++ b/packages/core/test/database-migration.test.ts
@@ -324,7 +324,7 @@ describe("DatabaseMigration", () => {
test("serializes concurrent embedded initialization for one database path", async () => {
await using tmp = await tmpdir()
const filename = path.join(tmp.path, "embedded.sqlite")
- const layers = [Database.layerFromPath(filename), Database.layerFromPath(filename)]
+ const layers = [Database.layer({ path: filename }), Database.layer({ path: filename })]
await Effect.runPromise(
Effect.all(
diff --git a/packages/core/test/session-create.test.ts b/packages/core/test/session-create.test.ts
index 4b43fb5477..e44a50e8dd 100644
--- a/packages/core/test/session-create.test.ts
+++ b/packages/core/test/session-create.test.ts
@@ -460,7 +460,7 @@ describe("SessionV2.create", () => {
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
)
- const targetDatabase = Database.layerFromPath(path.join(tmp.path, "target.sqlite"))
+ const targetDatabase = Database.layer({ path: path.join(tmp.path, "target.sqlite") })
const targetLayer = AppNodeBuilder.build(
LayerNode.group([Database.node, EventV2.node, SessionProjector.node, SessionStore.node]),
[[Database.node, targetDatabase]],
diff --git a/packages/opencode/src/cli/cmd/db.ts b/packages/opencode/src/cli/cmd/db.ts
index 9e7e37e18e..811f7dd137 100644
--- a/packages/opencode/src/cli/cmd/db.ts
+++ b/packages/opencode/src/cli/cmd/db.ts
@@ -1,6 +1,7 @@
import type { Argv } from "yargs"
import { spawn } from "child_process"
import { Database } from "@opencode-ai/core/database/database"
+import { InstallationDatabase } from "@/installation/database"
import { Effect } from "effect"
import { sql } from "drizzle-orm"
import { effectCmd } from "../effect-cmd"
@@ -35,7 +36,7 @@ const QueryCommand = effectCmd({
}
return
}
- const child = spawn("sqlite3", [Database.path()], {
+ const child = spawn("sqlite3", [InstallationDatabase.path()], {
stdio: "inherit",
})
yield* Effect.promise(() => new Promise((resolve) => child.on("close", resolve)))
@@ -47,7 +48,7 @@ const PathCommand = effectCmd({
describe: "print the database path",
instance: false,
handler: Effect.fn("Cli.db.path")(function* () {
- console.log(Database.path())
+ console.log(InstallationDatabase.path())
}),
})
diff --git a/packages/opencode/src/installation/database.ts b/packages/opencode/src/installation/database.ts
new file mode 100644
index 0000000000..b6d00a5cf8
--- /dev/null
+++ b/packages/opencode/src/installation/database.ts
@@ -0,0 +1,20 @@
+export * as InstallationDatabase from "./database"
+
+import { Flag } from "@opencode-ai/core/flag/flag"
+import { Global } from "@opencode-ai/core/global"
+import { InstallationChannel } from "@opencode-ai/core/installation/version"
+import { isAbsolute, join } from "node:path"
+
+export function path() {
+ if (Flag.OPENCODE_DB) {
+ if (Flag.OPENCODE_DB === ":memory:" || isAbsolute(Flag.OPENCODE_DB)) return Flag.OPENCODE_DB
+ return join(Global.Path.data, Flag.OPENCODE_DB)
+ }
+ if (
+ ["latest", "beta", "prod"].includes(InstallationChannel) ||
+ process.env.OPENCODE_DISABLE_CHANNEL_DB === "1" ||
+ process.env.OPENCODE_DISABLE_CHANNEL_DB === "true"
+ )
+ return join(Global.Path.data, "opencode.db")
+ return join(Global.Path.data, `opencode-${InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`)
+}
diff --git a/packages/opencode/test/fixture/db.ts b/packages/opencode/test/fixture/db.ts
index 88f1097f2d..ad55cb2371 100644
--- a/packages/opencode/test/fixture/db.ts
+++ b/packages/opencode/test/fixture/db.ts
@@ -1,10 +1,10 @@
import { rm } from "fs/promises"
-import { Database } from "@opencode-ai/core/database/database"
+import { InstallationDatabase } from "@/installation/database"
import { disposeAllInstances } from "./fixture"
export async function resetDatabase() {
await disposeAllInstances().catch(() => undefined)
- const dbPath = Database.path()
+ const dbPath = InstallationDatabase.path()
await rm(dbPath, { force: true }).catch(() => undefined)
await rm(`${dbPath}-wal`, { force: true }).catch(() => undefined)
await rm(`${dbPath}-shm`, { force: true }).catch(() => undefined)
diff --git a/packages/sdk-next/src/opencode.ts b/packages/sdk-next/src/opencode.ts
index 8ebe230791..6c376beeaf 100644
--- a/packages/sdk-next/src/opencode.ts
+++ b/packages/sdk-next/src/opencode.ts
@@ -1,12 +1,19 @@
import { OpenCode } from "@opencode-ai/client/effect"
+import { Database } from "@opencode-ai/core/database/database"
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
import { Context, Effect, Layer, ManagedRuntime } from "effect"
import { FetchHttpClient, HttpEffect, HttpRouter, HttpServer } from "effect/unstable/http"
-export const create = Effect.fn("OpenCode.create")(function* () {
+export const create = Effect.fn("OpenCode.create")(function* (options: { readonly database?: Database.Options } = {}) {
const runtime = yield* Effect.acquireRelease(
- Effect.sync(() => ManagedRuntime.make(createEmbeddedRoutes().pipe(Layer.provide(HttpServer.layerServices)))),
+ Effect.sync(() =>
+ ManagedRuntime.make(
+ createEmbeddedRoutes({ database: { path: ":memory:", ...options.database } }).pipe(
+ Layer.provide(HttpServer.layerServices),
+ ),
+ ),
+ ),
(runtime) => runtime.disposeEffect,
)
const context = yield* runtime.contextEffect
diff --git a/packages/sdk-next/test/embedded.test.ts b/packages/sdk-next/test/embedded.test.ts
index 2214ebf873..9ce482ff87 100644
--- a/packages/sdk-next/test/embedded.test.ts
+++ b/packages/sdk-next/test/embedded.test.ts
@@ -1,14 +1,11 @@
import fs from "fs/promises"
import path from "path"
import { expect } from "bun:test"
-import { Flag } from "@opencode-ai/core/flag/flag"
import { Deferred, Effect, Latch, Layer, Option, Ref, Schema, Stream } from "effect"
import { testEffect } from "../../core/test/lib/effect"
import { tmpdir } from "../../core/test/fixture/tmpdir"
import type { OpenCodeEvent } from "../src"
-Flag.OPENCODE_DB = ":memory:"
-
const it = testEffect(Layer.empty)
type Sdk = typeof import("../src")
type Fixture = { readonly directory: string; readonly sdk: Sdk }
diff --git a/packages/server/src/process.ts b/packages/server/src/process.ts
index 8fddbe3e2a..3e66ec9965 100644
--- a/packages/server/src/process.ts
+++ b/packages/server/src/process.ts
@@ -1,6 +1,7 @@
export * as ServerProcess from "./process"
import { NodeHttpServer, NodeHttpServerRequest } from "@effect/platform-node"
+import { Database } from "@opencode-ai/core/database/database"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
import { SessionRestart } from "@opencode-ai/core/session/execution/restart"
import { ServiceStatus } from "@opencode-ai/protocol/groups/health"
@@ -20,6 +21,7 @@ export type Options = {
readonly port: Option.Option
readonly password: string
readonly instanceID: string
+ readonly database?: Database.Options
readonly service?: {
readonly onListen: (
address: HttpServer.Address,
@@ -66,11 +68,15 @@ export const start = Effect.fn("ServerProcess.start")(function* (options:
const boot = Effect.gen(function* () {
const context = yield* Layer.buildWithScope(
- createRoutes(options.password, () => {
- const address = bound.server.address()
- if (address === null || typeof address === "string") return []
- const host = address.family === "IPv6" ? `[${address.address}]` : address.address
- return ServerInfo.connectionURLs(`http://${host}:${address.port}`, options.hostname)
+ createRoutes({
+ password: options.password,
+ serviceURLs: () => {
+ const address = bound.server.address()
+ if (address === null || typeof address === "string") return []
+ const host = address.family === "IPv6" ? `[${address.address}]` : address.address
+ return ServerInfo.connectionURLs(`http://${host}:${address.port}`, options.hostname)
+ },
+ database: options.database,
}).pipe(Layer.provide(NodeHttpServer.layerHttpServices)),
applicationScope,
)
diff --git a/packages/server/src/routes.ts b/packages/server/src/routes.ts
index 7a1a1d82be..0b19e2a17f 100644
--- a/packages/server/src/routes.ts
+++ b/packages/server/src/routes.ts
@@ -51,25 +51,32 @@ const applicationServices = LayerNode.group([
SessionRestart.node,
])
-export function createRoutes(password?: string, serviceURLs: () => ReadonlyArray = () => []) {
+export interface Options {
+ readonly password?: string
+ readonly serviceURLs?: () => ReadonlyArray
+ readonly database?: Database.Options
+}
+
+export function createRoutes(options: Options = {}) {
return makeRoutes(
- password
- ? ServerAuth.Config.configLayer({ username: "opencode", password: Option.some(password) })
+ options.password
+ ? ServerAuth.Config.configLayer({ username: "opencode", password: Option.some(options.password) })
: ServerAuth.Config.layer,
- serviceURLs,
+ options,
)
}
-export function createEmbeddedRoutes() {
- return makeRoutes(ServerAuth.Config.configLayer({ username: "opencode", password: Option.none() }))
+export function createEmbeddedRoutes(options: Pick = {}) {
+ return makeRoutes(ServerAuth.Config.configLayer({ username: "opencode", password: Option.none() }), options)
}
function makeRoutes(
auth: Layer.Layer,
- serviceURLs: () => ReadonlyArray = () => [],
+ options: Pick,
) {
const pluginRuntimeCell = PluginRuntime.makeCell()
const replacements: LayerNode.Replacements = [
+ [Database.node, Database.layer(options.database)],
[PluginRuntime.node, PluginRuntime.layerWithCell(pluginRuntimeCell)],
[PluginRuntime.providerNode, PluginRuntime.providerNodeWithCell(pluginRuntimeCell)],
]
@@ -88,7 +95,7 @@ function makeRoutes(
const services = Layer.succeedContext(context)
const requestServices = Layer.merge(
Layer.succeedContext(Context.pick(PermissionSaved.Service, Project.Service, WellKnown.Service)(context)),
- ServerInfo.layer(serviceURLs),
+ ServerInfo.layer(options.serviceURLs ?? (() => [])),
)
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
Layer.provide(handlers.pipe(Layer.provide(services))),