mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-09 10:59:49 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0552458fba |
@@ -2,9 +2,9 @@ export * as Database from "./database"
|
|||||||
|
|
||||||
import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
||||||
import { layer as sqliteLayer } from "#sqlite"
|
import { layer as sqliteLayer } from "#sqlite"
|
||||||
import { Context, Effect, Layer } from "effect"
|
import { Config, Context, Effect, Layer } from "effect"
|
||||||
import { Global } from "../global"
|
import { Global } from "../global"
|
||||||
import { Flag } from "../flag/flag"
|
import { truthyConfig } from "../flag/flag"
|
||||||
import { isAbsolute, join } from "path"
|
import { isAbsolute, join } from "path"
|
||||||
import { DatabaseMigration } from "./migration"
|
import { DatabaseMigration } from "./migration"
|
||||||
import { InstallationChannel } from "../installation/version"
|
import { InstallationChannel } from "../installation/version"
|
||||||
@@ -40,18 +40,28 @@ export function layerFromPath(filename: string) {
|
|||||||
return layer.pipe(Layer.provide(sqliteLayer({ filename })))
|
return layer.pipe(Layer.provide(sqliteLayer({ filename })))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function path() {
|
function resolvePath(input: { readonly file: string | undefined; readonly disableChannelDb: boolean }) {
|
||||||
if (Flag.OPENCODE_DB) {
|
if (input.file) {
|
||||||
if (Flag.OPENCODE_DB === ":memory:" || isAbsolute(Flag.OPENCODE_DB)) return Flag.OPENCODE_DB
|
if (input.file === ":memory:" || isAbsolute(input.file)) return input.file
|
||||||
return join(Global.Path.data, Flag.OPENCODE_DB)
|
return join(Global.Path.data, input.file)
|
||||||
}
|
}
|
||||||
if (
|
if (["latest", "beta", "prod"].includes(InstallationChannel) || input.disableChannelDb)
|
||||||
["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.db")
|
||||||
return join(Global.Path.data, `opencode-${InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`)
|
return join(Global.Path.data, `opencode-${InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const node = makeGlobalNode({ service: Service, layer: layerFromPath(path()), deps: [] })
|
/**
|
||||||
|
* The database placement every consumer shares, resolved through Effect
|
||||||
|
* Config so tests and tooling can override it with a ConfigProvider. Used by
|
||||||
|
* the layer below and by external tooling such as `opencode db`.
|
||||||
|
*/
|
||||||
|
export const configuredPath = Effect.gen(function* () {
|
||||||
|
const file = yield* Config.string("OPENCODE_DB").pipe(Config.withDefault(undefined))
|
||||||
|
const disableChannelDb = yield* truthyConfig("OPENCODE_DISABLE_CHANNEL_DB")
|
||||||
|
return resolvePath({ file, disableChannelDb })
|
||||||
|
}).pipe(Effect.orDie)
|
||||||
|
|
||||||
|
// Placement is resolved when the layer is built, not at module import.
|
||||||
|
const configuredLayer = Layer.unwrap(Effect.map(configuredPath, layerFromPath))
|
||||||
|
|
||||||
|
export const node = makeGlobalNode({ service: Service, layer: configuredLayer, deps: [] })
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
import { Config } from "effect"
|
import { Config } from "effect"
|
||||||
|
|
||||||
export function truthy(key: string) {
|
function truthyValue(value: string) {
|
||||||
const value = process.env[key]?.toLowerCase()
|
const lower = value.toLowerCase()
|
||||||
return value === "true" || value === "1"
|
return lower === "true" || lower === "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function truthy(key: string) {
|
||||||
|
const value = process.env[key]
|
||||||
|
return value !== undefined && truthyValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `truthy` grammar read through Effect Config, so layer builds can be
|
||||||
|
* steered by a ConfigProvider. Missing or malformed values are false, exactly
|
||||||
|
* like `truthy`; strict boolean decoding would turn a stray env value into a
|
||||||
|
* startup defect.
|
||||||
|
*/
|
||||||
|
export const truthyConfig = (name: string) =>
|
||||||
|
Config.string(name).pipe(Config.map(truthyValue), Config.withDefault(false))
|
||||||
|
|
||||||
const copy = process.env["OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"]
|
const copy = process.env["OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"]
|
||||||
const fff = process.env["OPENCODE_DISABLE_FFF"]
|
const fff = process.env["OPENCODE_DISABLE_FFF"]
|
||||||
|
|
||||||
@@ -39,7 +53,6 @@ export const Flag = {
|
|||||||
copy === undefined ? process.platform === "win32" : truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"),
|
copy === undefined ? process.platform === "win32" : truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT"),
|
||||||
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_WORKSPACE_ID: process.env["OPENCODE_WORKSPACE_ID"],
|
OPENCODE_WORKSPACE_ID: process.env["OPENCODE_WORKSPACE_ID"],
|
||||||
OPENCODE_EXPERIMENTAL_WORKSPACES: enabledByExperimental("OPENCODE_EXPERIMENTAL_WORKSPACES"),
|
OPENCODE_EXPERIMENTAL_WORKSPACES: enabledByExperimental("OPENCODE_EXPERIMENTAL_WORKSPACES"),
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
export * as WebSearchTool from "./websearch"
|
export * as WebSearchTool from "./websearch"
|
||||||
|
|
||||||
import { ToolFailure } from "@opencode-ai/llm"
|
import { ToolFailure } from "@opencode-ai/llm"
|
||||||
import { Context, Duration, Effect, Layer, Schema } from "effect"
|
import { Config, Context, Duration, Effect, Layer, Schema } from "effect"
|
||||||
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||||
import { makeLocationNode } from "../effect/app-node"
|
import { makeLocationNode } from "../effect/app-node"
|
||||||
import { LayerNodePlatform } from "../effect/app-node-platform"
|
import { LayerNodePlatform } from "../effect/app-node-platform"
|
||||||
import { truthy } from "../flag/flag"
|
import { truthyConfig } from "../flag/flag"
|
||||||
import { InstallationVersion } from "../installation/version"
|
import { InstallationVersion } from "../installation/version"
|
||||||
import { PositiveInt } from "../schema"
|
import { PositiveInt } from "../schema"
|
||||||
import { PermissionV2 } from "../permission"
|
import { PermissionV2 } from "../permission"
|
||||||
@@ -69,19 +69,39 @@ export interface Config {
|
|||||||
|
|
||||||
export class ConfigService extends Context.Service<ConfigService, Config>()("@opencode/v2/WebSearchConfig") {}
|
export class ConfigService extends Context.Service<ConfigService, Config>()("@opencode/v2/WebSearchConfig") {}
|
||||||
|
|
||||||
/** Isolates the retained product environment contract from the generic tool implementation. */
|
/**
|
||||||
export const defaultConfigLayer = Layer.sync(ConfigService, () =>
|
* Isolates the retained product environment contract from the generic tool
|
||||||
ConfigService.of({
|
* implementation. Reads through Effect `Config` when the layer is built, so
|
||||||
provider:
|
* tests can override values with a `ConfigProvider` instead of mutating
|
||||||
process.env.OPENCODE_WEBSEARCH_PROVIDER === "exa" || process.env.OPENCODE_WEBSEARCH_PROVIDER === "parallel"
|
* `process.env`. Parsing keeps the legacy lenient grammar: missing or
|
||||||
? process.env.OPENCODE_WEBSEARCH_PROVIDER
|
* malformed values disable rather than fail, because this layer builds at
|
||||||
: undefined,
|
* Location boot where a defect would surface as opaque session failures.
|
||||||
enableExa: truthy("OPENCODE_EXPERIMENTAL") || truthy("OPENCODE_ENABLE_EXA") || truthy("OPENCODE_EXPERIMENTAL_EXA"),
|
*/
|
||||||
enableParallel: truthy("OPENCODE_ENABLE_PARALLEL") || truthy("OPENCODE_EXPERIMENTAL_PARALLEL"),
|
export const defaultConfigLayer = Layer.effect(
|
||||||
exaApiKey: process.env.EXA_API_KEY,
|
ConfigService,
|
||||||
parallelApiKey: process.env.PARALLEL_API_KEY,
|
Effect.gen(function* () {
|
||||||
|
const env = yield* Config.all({
|
||||||
|
provider: Config.string("OPENCODE_WEBSEARCH_PROVIDER").pipe(Config.withDefault(undefined)),
|
||||||
|
experimental: truthyConfig("OPENCODE_EXPERIMENTAL"),
|
||||||
|
enableExa: truthyConfig("OPENCODE_ENABLE_EXA"),
|
||||||
|
experimentalExa: truthyConfig("OPENCODE_EXPERIMENTAL_EXA"),
|
||||||
|
enableParallel: truthyConfig("OPENCODE_ENABLE_PARALLEL"),
|
||||||
|
experimentalParallel: truthyConfig("OPENCODE_EXPERIMENTAL_PARALLEL"),
|
||||||
|
exaApiKey: Config.string("EXA_API_KEY").pipe(Config.withDefault(undefined)),
|
||||||
|
parallelApiKey: Config.string("PARALLEL_API_KEY").pipe(Config.withDefault(undefined)),
|
||||||
|
})
|
||||||
|
const provider = env.provider !== undefined && Schema.is(Provider)(env.provider) ? env.provider : undefined
|
||||||
|
if (env.provider !== undefined && provider === undefined)
|
||||||
|
yield* Effect.logWarning("ignoring invalid OPENCODE_WEBSEARCH_PROVIDER", { value: env.provider })
|
||||||
|
return ConfigService.of({
|
||||||
|
provider,
|
||||||
|
enableExa: env.experimental || env.enableExa || env.experimentalExa,
|
||||||
|
enableParallel: env.enableParallel || env.experimentalParallel,
|
||||||
|
exaApiKey: env.exaApiKey,
|
||||||
|
parallelApiKey: env.parallelApiKey,
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
)
|
).pipe(Layer.orDie)
|
||||||
|
|
||||||
export const configNode = makeLocationNode({ service: ConfigService, layer: defaultConfigLayer, deps: [] })
|
export const configNode = makeLocationNode({ service: ConfigService, layer: defaultConfigLayer, deps: [] })
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import path from "path"
|
||||||
|
import { ConfigProvider, Effect, Layer } from "effect"
|
||||||
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
|
import { Global } from "@opencode-ai/core/global"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
|
|
||||||
|
const resolve = (env: Record<string, string>) =>
|
||||||
|
Effect.runPromise(Effect.provide(Database.configuredPath, ConfigProvider.layer(ConfigProvider.fromUnknown(env))))
|
||||||
|
|
||||||
|
describe("Database placement", () => {
|
||||||
|
test("resolves explicit files, relative names, and the channel default", async () => {
|
||||||
|
expect(await resolve({ OPENCODE_DB: ":memory:" })).toBe(":memory:")
|
||||||
|
expect(await resolve({ OPENCODE_DB: "/tmp/explicit.db" })).toBe("/tmp/explicit.db")
|
||||||
|
expect(await resolve({ OPENCODE_DB: "relative.db" })).toBe(path.join(Global.Path.data, "relative.db"))
|
||||||
|
expect(await resolve({ OPENCODE_DISABLE_CHANNEL_DB: "true" })).toBe(path.join(Global.Path.data, "opencode.db"))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("reads placement from the active ConfigProvider when the layer is built", async () => {
|
||||||
|
await using tmp = await tmpdir()
|
||||||
|
const file = path.join(tmp.path, "config-seam.sqlite")
|
||||||
|
|
||||||
|
// The preload sets OPENCODE_DB=":memory:" in the process environment, so a
|
||||||
|
// database appearing at this path proves the layer reads through the
|
||||||
|
// replaced ConfigProvider rather than the environment snapshot.
|
||||||
|
await Effect.runPromise(
|
||||||
|
Layer.build(
|
||||||
|
LayerNode.compile(LayerNode.group([Database.node])).pipe(
|
||||||
|
Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown({ OPENCODE_DB: file }))),
|
||||||
|
),
|
||||||
|
).pipe(Effect.scoped, Effect.asVoid),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(await Bun.file(file).exists()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { beforeEach, describe, expect, test } from "bun:test"
|
import { beforeEach, describe, expect, test } from "bun:test"
|
||||||
import { Effect, Layer, Schema } from "effect"
|
import { ConfigProvider, Effect, Layer, Schema } from "effect"
|
||||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
@@ -47,6 +47,57 @@ describe("WebSearchTool provider selection", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const readDefaultConfig = (env: Record<string, string>) =>
|
||||||
|
Effect.provide(
|
||||||
|
WebSearchTool.ConfigService,
|
||||||
|
WebSearchTool.defaultConfigLayer.pipe(Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(env)))),
|
||||||
|
)
|
||||||
|
|
||||||
|
describe("WebSearchTool default config", () => {
|
||||||
|
test("decodes an empty environment to defaults", async () => {
|
||||||
|
expect(await Effect.runPromise(readDefaultConfig({}))).toEqual({
|
||||||
|
provider: undefined,
|
||||||
|
enableExa: false,
|
||||||
|
enableParallel: false,
|
||||||
|
exaApiKey: undefined,
|
||||||
|
parallelApiKey: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("decodes provider, truthy flags, and credentials from the active ConfigProvider", async () => {
|
||||||
|
expect(
|
||||||
|
await Effect.runPromise(
|
||||||
|
readDefaultConfig({
|
||||||
|
OPENCODE_WEBSEARCH_PROVIDER: "parallel",
|
||||||
|
OPENCODE_ENABLE_EXA: "1",
|
||||||
|
OPENCODE_EXPERIMENTAL_PARALLEL: "true",
|
||||||
|
EXA_API_KEY: "exa-key",
|
||||||
|
PARALLEL_API_KEY: "parallel-key",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
provider: "parallel",
|
||||||
|
enableExa: true,
|
||||||
|
enableParallel: true,
|
||||||
|
exaApiKey: "exa-key",
|
||||||
|
parallelApiKey: "parallel-key",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keeps the legacy lenient truthiness grammar", async () => {
|
||||||
|
const decoded = await Effect.runPromise(
|
||||||
|
readDefaultConfig({ OPENCODE_ENABLE_EXA: "TRUE", OPENCODE_ENABLE_PARALLEL: "banana" }),
|
||||||
|
)
|
||||||
|
expect(decoded.enableExa).toBe(true)
|
||||||
|
expect(decoded.enableParallel).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("ignores an invalid provider instead of failing the Location layer", async () => {
|
||||||
|
const decoded = await Effect.runPromise(readDefaultConfig({ OPENCODE_WEBSEARCH_PROVIDER: "bing" }))
|
||||||
|
expect(decoded.provider).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("WebSearchTool MCP response parser", () => {
|
describe("WebSearchTool MCP response parser", () => {
|
||||||
test("parses plain JSON-RPC responses", async () => {
|
test("parses plain JSON-RPC responses", async () => {
|
||||||
expect(await Effect.runPromise(WebSearchTool.parseResponse(payload("search results")))).toBe("search results")
|
expect(await Effect.runPromise(WebSearchTool.parseResponse(payload("search results")))).toBe("search results")
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const QueryCommand = effectCmd({
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const child = spawn("sqlite3", [Database.path()], {
|
const child = spawn("sqlite3", [yield* Database.configuredPath], {
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
})
|
})
|
||||||
yield* Effect.promise(() => new Promise((resolve) => child.on("close", resolve)))
|
yield* Effect.promise(() => new Promise((resolve) => child.on("close", resolve)))
|
||||||
@@ -47,7 +47,7 @@ const PathCommand = effectCmd({
|
|||||||
describe: "print the database path",
|
describe: "print the database path",
|
||||||
instance: false,
|
instance: false,
|
||||||
handler: Effect.fn("Cli.db.path")(function* () {
|
handler: Effect.fn("Cli.db.path")(function* () {
|
||||||
console.log(Database.path())
|
console.log(yield* Database.configuredPath)
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { rm } from "fs/promises"
|
import { rm } from "fs/promises"
|
||||||
import { Database } from "@opencode-ai/core/database/database"
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
|
import { Effect } from "effect"
|
||||||
import { disposeAllInstances } from "./fixture"
|
import { disposeAllInstances } from "./fixture"
|
||||||
|
|
||||||
export async function resetDatabase() {
|
export async function resetDatabase() {
|
||||||
await disposeAllInstances().catch(() => undefined)
|
await disposeAllInstances().catch(() => undefined)
|
||||||
const dbPath = Database.path()
|
const dbPath = await Effect.runPromise(Database.configuredPath)
|
||||||
await rm(dbPath, { force: true }).catch(() => undefined)
|
await rm(dbPath, { force: true }).catch(() => undefined)
|
||||||
await rm(`${dbPath}-wal`, { force: true }).catch(() => undefined)
|
await rm(`${dbPath}-wal`, { force: true }).catch(() => undefined)
|
||||||
await rm(`${dbPath}-shm`, { force: true }).catch(() => undefined)
|
await rm(`${dbPath}-shm`, { force: true }).catch(() => undefined)
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ const preserveExerciseDatabase = !!process.env.OPENCODE_HTTPAPI_EXERCISE_DB
|
|||||||
export const exerciseDatabasePath =
|
export const exerciseDatabasePath =
|
||||||
process.env.OPENCODE_HTTPAPI_EXERCISE_DB ??
|
process.env.OPENCODE_HTTPAPI_EXERCISE_DB ??
|
||||||
path.join(process.env.TMPDIR ?? "/tmp", `opencode-httpapi-exercise-${process.pid}.db`)
|
path.join(process.env.TMPDIR ?? "/tmp", `opencode-httpapi-exercise-${process.pid}.db`)
|
||||||
|
// Must run before the first Effect Config read anywhere in the process: the
|
||||||
|
// default ConfigProvider snapshots process.env on first use, so a Config read
|
||||||
|
// during an earlier module import would freeze the wrong database path.
|
||||||
process.env.OPENCODE_DB = exerciseDatabasePath
|
process.env.OPENCODE_DB = exerciseDatabasePath
|
||||||
Flag.OPENCODE_DB = exerciseDatabasePath
|
|
||||||
|
|
||||||
export const original = {
|
export const original = {
|
||||||
OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
|
OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
|
||||||
|
|||||||
@@ -4,17 +4,36 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|||||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||||
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
||||||
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
|
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
|
||||||
import { Context, Effect, Layer, Scope } from "effect"
|
import { ConfigProvider, Context, Effect, Layer, Scope } from "effect"
|
||||||
import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http"
|
import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||||
|
|
||||||
export const create = Effect.fn("OpenCode.create")(function* () {
|
export interface Options {
|
||||||
|
/**
|
||||||
|
* Replaces the ConfigProvider read while this host's process-global layers
|
||||||
|
* are built, for example the OPENCODE_DB database placement. The default
|
||||||
|
* provider snapshots the process environment on first use, so per-host
|
||||||
|
* configuration must come through this seam rather than env mutation.
|
||||||
|
* Location-scoped layers build lazily outside host construction and still
|
||||||
|
* read the process default. Compose with
|
||||||
|
* ConfigProvider.orElse(ConfigProvider.fromEnv()) to keep environment
|
||||||
|
* fallback.
|
||||||
|
*/
|
||||||
|
readonly configProvider?: ConfigProvider.ConfigProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
export const create = Effect.fn("OpenCode.create")(function* (options?: Options) {
|
||||||
const scope = yield* Scope.Scope
|
const scope = yield* Scope.Scope
|
||||||
const memoMap = yield* Layer.makeMemoMap
|
const memoMap = yield* Layer.makeMemoMap
|
||||||
|
const configLayer = options?.configProvider === undefined ? undefined : ConfigProvider.layer(options.configProvider)
|
||||||
|
const withConfig = <A, E, R>(layer: Layer.Layer<A, E, R>) =>
|
||||||
|
configLayer === undefined ? layer : layer.pipe(Layer.provide(configLayer))
|
||||||
const sdkPlugins = SdkPlugins.makeStore()
|
const sdkPlugins = SdkPlugins.makeStore()
|
||||||
const context = yield* Layer.buildWithMemoMap(
|
const context = yield* Layer.buildWithMemoMap(
|
||||||
AppNodeBuilder.build(LayerNode.group([PermissionSaved.node, SdkPlugins.node]), [
|
withConfig(
|
||||||
[SdkPlugins.node, SdkPlugins.layerWithStore(sdkPlugins)],
|
AppNodeBuilder.build(LayerNode.group([PermissionSaved.node, SdkPlugins.node]), [
|
||||||
]),
|
[SdkPlugins.node, SdkPlugins.layerWithStore(sdkPlugins)],
|
||||||
|
]),
|
||||||
|
),
|
||||||
memoMap,
|
memoMap,
|
||||||
scope,
|
scope,
|
||||||
)
|
)
|
||||||
@@ -23,9 +42,11 @@ export const create = Effect.fn("OpenCode.create")(function* () {
|
|||||||
const web = yield* Effect.acquireRelease(
|
const web = yield* Effect.acquireRelease(
|
||||||
Effect.sync(() =>
|
Effect.sync(() =>
|
||||||
HttpRouter.toWebHandler(
|
HttpRouter.toWebHandler(
|
||||||
createEmbeddedRoutes(sdkPlugins).pipe(
|
withConfig(
|
||||||
HttpRouter.provideRequest(Layer.succeed(PermissionSaved.Service, permissions)),
|
createEmbeddedRoutes(sdkPlugins).pipe(
|
||||||
Layer.provide(HttpServer.layerServices),
|
HttpRouter.provideRequest(Layer.succeed(PermissionSaved.Service, permissions)),
|
||||||
|
Layer.provide(HttpServer.layerServices),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
{ disableLogger: true, memoMap },
|
{ disableLogger: true, memoMap },
|
||||||
),
|
),
|
||||||
@@ -56,4 +77,6 @@ export type Interface = Effect.Success<ReturnType<typeof create>>
|
|||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode-ai/sdk-next/OpenCode") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode-ai/sdk-next/OpenCode") {}
|
||||||
|
|
||||||
export const layer = Layer.effect(Service, create())
|
export const layerWith = (options: Options) => Layer.effect(Service, create(options))
|
||||||
|
|
||||||
|
export const layer = layerWith({})
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { expect } from "bun:test"
|
import { expect } from "bun:test"
|
||||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
import { ConfigProvider, Deferred, Effect, Latch, Layer, Option, Schema, Stream } from "effect"
|
||||||
import { Deferred, Effect, Latch, Layer, Option, Schema, Stream } from "effect"
|
import { join } from "node:path"
|
||||||
import { testEffect } from "../../core/test/lib/effect"
|
import { testEffect } from "../../core/test/lib/effect"
|
||||||
import { tmpdir } from "../../core/test/fixture/tmpdir"
|
import { tmpdir } from "../../core/test/fixture/tmpdir"
|
||||||
import type { OpenCodeEvent } from "../src"
|
import type { OpenCodeEvent } from "../src"
|
||||||
|
|
||||||
Flag.OPENCODE_DB = ":memory:"
|
// Database placement resolves through Effect Config when a host's layers are
|
||||||
|
// built. The default ConfigProvider snapshots process.env on first use, so
|
||||||
|
// this must run before the first Config read in the process.
|
||||||
|
process.env.OPENCODE_DB = ":memory:"
|
||||||
|
|
||||||
const it = testEffect(Layer.empty)
|
const it = testEffect(Layer.empty)
|
||||||
type Sdk = typeof import("../src")
|
type Sdk = typeof import("../src")
|
||||||
@@ -204,3 +207,19 @@ it.live("embedded client is available as a Layer service", () =>
|
|||||||
}).pipe(Effect.provide(fixture.sdk.OpenCode.layer))
|
}).pipe(Effect.provide(fixture.sdk.OpenCode.layer))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.live("a host ConfigProvider chooses this host's database placement", () =>
|
||||||
|
withEmbedded("opencode-embedded-config-", (fixture) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
// The process env pins OPENCODE_DB to ":memory:" above, so a database
|
||||||
|
// materializing at this path proves the host read the provider instead.
|
||||||
|
const file = join(fixture.directory, "seam.sqlite")
|
||||||
|
const opencode = yield* fixture.sdk.OpenCode.create({
|
||||||
|
configProvider: ConfigProvider.fromUnknown({ OPENCODE_DB: file }),
|
||||||
|
})
|
||||||
|
const created = yield* opencode.sessions.create({ location: location(fixture) })
|
||||||
|
expect(created.id).toBeTruthy()
|
||||||
|
expect(yield* Effect.promise(() => Bun.file(file).exists())).toBe(true)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user