Просмотр исходного кода

refactor(server): inject runtime options

Dax Raad 3 недель назад
Родитель
Сommit
0409e6884d

+ 6 - 1
packages/cli/src/index.ts

@@ -61,7 +61,12 @@ Effect.logInfo("cli starting", {
   Effect.provide(Config.layer),
   Effect.provide(Updater.layer),
   Effect.provide(LayerNode.compile(LayerNode.group([Global.node, AppProcess.node, Npm.node]))),
-  Effect.provide(Observability.layer),
+  Effect.provide(
+    Observability.layer({
+      endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
+      headers: process.env.OTEL_EXPORTER_OTLP_HEADERS,
+    }),
+  ),
   Effect.provide(NodeServices.layer),
   Effect.scoped,
   Effect.tap(() => Effect.sync(() => process.exit(process.exitCode ?? 0))),

+ 18 - 1
packages/cli/src/server-process.ts

@@ -71,7 +71,20 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
           models: {
             url: process.env.OPENCODE_MODELS_URL,
             file: process.env.OPENCODE_MODELS_PATH,
-            fetch: !["1", "true"].includes(process.env.OPENCODE_DISABLE_MODELS_FETCH?.toLowerCase() ?? ""),
+            fetch: !truthy(process.env.OPENCODE_DISABLE_MODELS_FETCH),
+          },
+          observability: {
+            endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
+            headers: process.env.OTEL_EXPORTER_OTLP_HEADERS,
+          },
+          fs: {
+            filewatcher: !truthy(
+              process.env.OPENCODE_FILEWATCHER_DISABLE ?? process.env.OPENCODE_DISABLE_FILEWATCHER,
+            ),
+            fff:
+              process.env.OPENCODE_DISABLE_FFF === undefined
+                ? process.platform !== "win32"
+                : !truthy(process.env.OPENCODE_DISABLE_FFF),
           },
         },
         serviceOptions === undefined
@@ -177,6 +190,10 @@ function serviceURL(hostname: string, port: number) {
   return `http://${hostname.includes(":") ? `[${hostname}]` : hostname}:${port}`
 }
 
+function truthy(value?: string) {
+  return value === "1" || value?.toLowerCase() === "true"
+}
+
 function addressInUse(error: unknown): boolean {
   if (typeof error !== "object" || error === null) return false
   if ("code" in error && error.code === "EADDRINUSE") return true

+ 5 - 4
packages/core/src/database/database.ts

@@ -2,7 +2,7 @@ export * as Database from "./database"
 
 import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
 import { sqliteLayer } from "#sqlite"
-import { Context, Effect, Layer } from "effect"
+import { Context, Effect, Layer, Schema } from "effect"
 import { Global } from "../global"
 import { isAbsolute, join } from "path"
 import { DatabaseMigration } from "./migration"
@@ -16,9 +16,10 @@ export interface Interface {
   db: DatabaseShape
 }
 
-export interface Options {
-  readonly path?: string
-}
+export const Options = Schema.Struct({
+  path: Schema.optional(Schema.String),
+})
+export type Options = typeof Options.Type
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/storage/Database") {}
 

+ 1 - 1
packages/core/src/effect/runtime.ts

@@ -5,7 +5,7 @@ import { Observability } from "../observability"
 export function makeRuntime<I, S, E>(service: Context.Service<I, S>, layer: Layer.Layer<I, E>) {
   let rt: ManagedRuntime.ManagedRuntime<I, E> | undefined
   const getRuntime = () =>
-    (rt ??= ManagedRuntime.make(Layer.provideMerge(layer, Observability.layer) as Layer.Layer<I, E>, {
+    (rt ??= ManagedRuntime.make(Layer.provideMerge(layer, Observability.layer()) as Layer.Layer<I, E>, {
       memoMap,
     }))
 

+ 14 - 5
packages/core/src/filesystem/search.ts

@@ -2,7 +2,7 @@ export * as FileSystemSearch from "./search"
 
 import { makeLocationNode } from "../effect/app-node"
 import path from "path"
-import { Context, Effect, Layer, Scope } from "effect"
+import { Context, Effect, Layer, Schema, Scope } from "effect"
 import { Fff } from "#fff"
 import fuzzysort from "fuzzysort"
 import { FileSystem } from "../filesystem"
@@ -10,7 +10,6 @@ import { FSUtil } from "../fs-util"
 import { Location } from "../location"
 import { Ripgrep } from "../ripgrep"
 import { RelativePath } from "../schema"
-import { Flag } from "../flag/flag"
 
 export interface Interface {
   readonly find: (input: FileSystem.FindInput) => Effect.Effect<FileSystem.Entry[]>
@@ -18,6 +17,11 @@ export interface Interface {
   readonly grep: (input: FileSystem.GrepInput) => Effect.Effect<readonly FileSystem.Match[]>
 }
 
+export const Options = Schema.Struct({
+  fff: Schema.optional(Schema.Boolean),
+})
+export type Options = typeof Options.Type
+
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileSystem/Search") {}
 
 export const ripgrepLayer = Layer.effect(
@@ -232,13 +236,18 @@ export const fffLayer = Layer.effect(
   }),
 )
 
-const layer = Layer.unwrap(
+export const layer = (options?: Options) => Layer.unwrap(
   Effect.gen(function* () {
-    if (Flag.OPENCODE_DISABLE_FFF || !Fff.available()) return ripgrepLayer
+    if (options?.fff === false || (options?.fff === undefined && process.platform === "win32") || !Fff.available())
+      return ripgrepLayer
     const location = yield* Location.Service
     // Non-VCS locations can contain many repositories, so avoid eagerly content-indexing the entire aggregate tree.
     return location.vcs ? fffLayer : ripgrepLayer
   }),
 )
 
-export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Location.node, Ripgrep.node] })
+export function nodeWith(options?: Options) {
+  return makeLocationNode({ service: Service, layer: layer(options), deps: [FSUtil.node, Location.node, Ripgrep.node] })
+}
+
+export const node = nodeWith()

+ 13 - 5
packages/core/src/filesystem/watcher.ts

@@ -5,9 +5,8 @@ import { createWrapper } from "@parcel/watcher/wrapper"
 import type ParcelWatcher from "@parcel/watcher"
 import { FileSystem } from "@opencode-ai/schema/filesystem"
 import { makeGlobalNode } from "../effect/app-node"
-import { Cause, Context, Effect, Layer, PubSub, Scope, Stream } from "effect"
+import { Cause, Context, Effect, Layer, PubSub, Schema, Scope, Stream } from "effect"
 import { KeyedMutex } from "../effect/keyed-mutex"
-import { Flag } from "../flag/flag"
 import { lazy } from "../util/lazy"
 import { watch as watchFileSystem } from "node:fs"
 import path from "path"
@@ -50,14 +49,19 @@ export interface Interface {
   readonly subscribe: (input: WatchInput) => Stream.Stream<Update>
 }
 
+export const Options = Schema.Struct({
+  enabled: Schema.optional(Schema.Boolean),
+})
+export type Options = typeof Options.Type
+
 export class Service extends Context.Service<Service, Interface>()("@opencode/Watcher") {}
 
-const layer = Layer.effect(
+export const layer = (options?: Options) => Layer.effect(
   Service,
   Effect.gen(function* () {
     const backend = getBackend()
     const native = watcher()
-    if (Flag.OPENCODE_DISABLE_FILEWATCHER) {
+    if (options?.enabled === false) {
       return Service.of({ subscribe: () => Stream.empty })
     }
 
@@ -140,7 +144,11 @@ const layer = Layer.effect(
   }),
 )
 
-export const node = makeGlobalNode({ service: Service, layer, deps: [] })
+export function nodeWith(options?: Options) {
+  return makeGlobalNode({ service: Service, layer: layer(options), deps: [] })
+}
+
+export const node = nodeWith()
 
 function subscribeDirectory(
   native: typeof import("@parcel/watcher") | undefined,

+ 6 - 5
packages/core/src/models-dev.ts

@@ -530,11 +530,12 @@ export interface Interface {
   readonly refresh: (force?: boolean) => Effect.Effect<void>
 }
 
-export interface Options {
-  readonly url?: string
-  readonly file?: string
-  readonly fetch?: boolean
-}
+export const Options = Schema.Struct({
+  url: Schema.optional(Schema.String),
+  file: Schema.optional(Schema.String),
+  fetch: Schema.optional(Schema.Boolean),
+})
+export type Options = typeof Options.Type
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/ModelsDev") {}
 

+ 31 - 19
packages/core/src/observability.ts

@@ -2,29 +2,41 @@ export * as Observability from "./observability"
 
 import { NodeFileSystem } from "@effect/platform-node"
 import { LayerNode } from "./effect/layer-node"
-import { Effect, Layer, Logger, References } from "effect"
+import { Effect, Layer, Logger, References, Schema } from "effect"
 import { FetchHttpClient } from "effect/unstable/http"
 import { OtlpSerialization } from "effect/unstable/observability"
 import { Logging } from "./observability/logging"
 import { Otlp } from "./observability/otlp"
 
-const local = Logger.layer(Logging.loggers(), { mergeWithExisting: false }).pipe(
-  Layer.provide(NodeFileSystem.layer),
-  Layer.orDie,
-  Layer.merge(Layer.succeed(References.MinimumLogLevel, Logging.minimumLogLevel())),
-)
+export const Options = Schema.Struct({
+  endpoint: Schema.optional(Schema.String),
+  headers: Schema.optional(Schema.String),
+})
+export type Options = typeof Options.Type
 
-export const layer = Layer.unwrap(
-  Effect.gen(function* () {
-    const logs = Logger.layer([...Logging.loggers(), ...Otlp.loggers()], { mergeWithExisting: false }).pipe(
-      Layer.provide(NodeFileSystem.layer),
-      Layer.provide(OtlpSerialization.layerJson),
-      Layer.provide(FetchHttpClient.layer),
-      Layer.orDie,
-      Layer.merge(Layer.succeed(References.MinimumLogLevel, Logging.minimumLogLevel())),
-    )
-    return Layer.merge(logs, yield* Effect.promise(Otlp.tracingLayer))
-  }),
-).pipe(Layer.catchCause(() => local))
+export function layer(
+  options: Options = {
+    endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
+    headers: process.env.OTEL_EXPORTER_OTLP_HEADERS,
+  },
+) {
+  const local = Logger.layer(Logging.loggers(), { mergeWithExisting: false }).pipe(
+    Layer.provide(NodeFileSystem.layer),
+    Layer.orDie,
+    Layer.merge(Layer.succeed(References.MinimumLogLevel, Logging.minimumLogLevel())),
+  )
+  return Layer.unwrap(
+    Effect.gen(function* () {
+      const logs = Logger.layer([...Logging.loggers(), ...Otlp.loggers(options)], { mergeWithExisting: false }).pipe(
+        Layer.provide(NodeFileSystem.layer),
+        Layer.provide(OtlpSerialization.layerJson),
+        Layer.provide(FetchHttpClient.layer),
+        Layer.orDie,
+        Layer.merge(Layer.succeed(References.MinimumLogLevel, Logging.minimumLogLevel())),
+      )
+      return Layer.merge(logs, yield* Effect.promise(() => Otlp.tracingLayer(options)))
+    }),
+  ).pipe(Layer.catchCause(() => local))
+}
 
-export const node = LayerNode.make({ name: "observability", layer, deps: [] })
+export const node = LayerNode.make({ name: "observability", layer: layer(), deps: [] })

+ 18 - 11
packages/core/src/observability/otlp.ts

@@ -4,10 +4,14 @@ import { Flag } from "../flag/flag"
 import { InstallationChannel, InstallationVersion } from "../installation/version"
 import { runID } from "./shared"
 
-const endpoint = Flag.OTEL_EXPORTER_OTLP_ENDPOINT
+export interface Options {
+  readonly endpoint?: string
+  readonly headers?: string
+}
 
-const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS
-  ? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce(
+function parseHeaders(value?: string) {
+  return value
+    ? value.split(",").reduce(
       (acc, entry) => {
         const [key, ...value] = entry.split("=")
         acc[key] = value.join("=")
@@ -15,7 +19,8 @@ const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS
       },
       {} as Record<string, string>,
     )
-  : undefined
+    : undefined
+}
 
 function resourceAttributes() {
   const value = process.env.OTEL_RESOURCE_ATTRIBUTES
@@ -47,13 +52,15 @@ export function resource(): { serviceName: string; serviceVersion: string; attri
   }
 }
 
-export function loggers() {
-  if (!endpoint) return []
-  return [OtlpLogger.make({ url: `${endpoint}/v1/logs`, resource: resource(), headers })]
+export function loggers(options?: Options) {
+  if (!options?.endpoint) return []
+  return [
+    OtlpLogger.make({ url: `${options.endpoint}/v1/logs`, resource: resource(), headers: parseHeaders(options.headers) }),
+  ]
 }
 
-export async function tracingLayer() {
-  if (!endpoint) return Layer.empty
+export async function tracingLayer(options?: Options) {
+  if (!options?.endpoint) return Layer.empty
   const NodeSdk = await import("@effect/opentelemetry/NodeSdk")
   const OTLP = await import("@opentelemetry/exporter-trace-otlp-http")
   const SdkBase = await import("@opentelemetry/sdk-trace-base")
@@ -69,8 +76,8 @@ export async function tracingLayer() {
     resource: resource(),
     spanProcessor: new SdkBase.BatchSpanProcessor(
       new OTLP.OTLPTraceExporter({
-        url: `${endpoint}/v1/traces`,
-        headers,
+        url: `${options.endpoint}/v1/traces`,
+        headers: parseHeaders(options.headers),
       }),
     ),
   }))

+ 1 - 1
packages/core/test/effect/observability.test.ts

@@ -66,7 +66,7 @@ test("falls back to local logging when OTLP initialization fails", async () => {
       `
         import { Effect } from "effect"
         import { Observability } from "./src/observability.ts"
-        await Effect.void.pipe(Effect.provide(Observability.layer), Effect.scoped, Effect.runPromise)
+        await Effect.void.pipe(Effect.provide(Observability.layer()), Effect.scoped, Effect.runPromise)
       `,
     ],
     {

+ 1 - 1
packages/opencode/src/effect/app-runtime.ts

@@ -104,7 +104,7 @@ export const AppLayer = AppNodeBuilderV1.build(
     ShareNext.node,
     SessionShare.node,
   ]),
-).pipe(Layer.provideMerge(AppNodeBuilderV1.build(Ripgrep.node)), Layer.provideMerge(Observability.layer))
+).pipe(Layer.provideMerge(AppNodeBuilderV1.build(Ripgrep.node)), Layer.provideMerge(Observability.layer()))
 
 const rt = ManagedRuntime.make(AppLayer, { memoMap })
 type Runtime = Pick<typeof rt, "runSync" | "runPromise" | "runPromiseExit" | "runFork" | "runCallback" | "dispose">

+ 1 - 1
packages/opencode/src/effect/bootstrap-runtime.ts

@@ -14,6 +14,6 @@ import { memoMap } from "@opencode-ai/core/effect/memo-map"
 
 export const BootstrapLayer = AppNodeBuilder.build(
   LayerNode.group([Config.node, Plugin.node, ShareNext.node, Format.node, LSP.node, Vcs.node, Snapshot.node]),
-).pipe(Layer.provide(Observability.layer))
+).pipe(Layer.provide(Observability.layer()))
 
 export const BootstrapRuntime = ManagedRuntime.make(BootstrapLayer, { memoMap })

+ 1 - 1
packages/opencode/src/effect/run-service.ts

@@ -32,7 +32,7 @@ export function attach<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A
 
 export function makeRuntime<I, S, E>(service: Context.Service<I, S>, layer: Layer.Layer<I, E>) {
   let rt: ManagedRuntime.ManagedRuntime<I, E> | undefined
-  const getRuntime = () => (rt ??= ManagedRuntime.make(Layer.provideMerge(layer, Observability.layer), { memoMap }))
+  const getRuntime = () => (rt ??= ManagedRuntime.make(Layer.provideMerge(layer, Observability.layer()), { memoMap }))
 
   return {
     runSync: <A, Err>(fn: (svc: S) => Effect.Effect<A, Err, I>) => getRuntime().runSync(attach(service.use(fn))),

+ 1 - 1
packages/opencode/src/server/routes/instance/httpapi/server.ts

@@ -305,7 +305,7 @@ export function createRoutes(
     Layer.provide(locationServiceMapV2),
 
     Layer.provide(AppNodeBuilderV1.build(app)),
-    Layer.provideMerge(Observability.layer),
+    Layer.provideMerge(Observability.layer()),
   )
 }
 

+ 3 - 7
packages/sdk-next/src/opencode.ts

@@ -1,19 +1,15 @@
 import { OpenCode } from "@opencode-ai/client/effect"
-import { Database } from "@opencode-ai/core/database/database"
 import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
-import { ModelsDev } from "@opencode-ai/core/models-dev"
 import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
+import type { ServerOptions } from "@opencode-ai/server/options"
 import { Context, Effect, Layer, ManagedRuntime } from "effect"
 import { FetchHttpClient, HttpEffect, HttpRouter, HttpServer } from "effect/unstable/http"
 
-export const create = Effect.fn("OpenCode.create")(function* (options: {
-  readonly database?: Database.Options
-  readonly models?: ModelsDev.Options
-} = {}) {
+export const create = Effect.fn("OpenCode.create")(function* (options: ServerOptions = {}) {
   const runtime = yield* Effect.acquireRelease(
     Effect.sync(() =>
       ManagedRuntime.make(
-        createEmbeddedRoutes({ database: { path: ":memory:", ...options.database }, models: options.models }).pipe(
+        createEmbeddedRoutes({ ...options, database: { path: ":memory:", ...options.database } }).pipe(
           Layer.provide(HttpServer.layerServices),
         ),
       ),

+ 21 - 9
packages/server/src/options.ts

@@ -1,10 +1,22 @@
-import type { Database } from "@opencode-ai/core/database/database"
-import type { ModelsDev } from "@opencode-ai/core/models-dev"
+import { Database } from "@opencode-ai/core/database/database"
+import { ModelsDev } from "@opencode-ai/core/models-dev"
+import { Observability } from "@opencode-ai/core/observability"
+import { Schema } from "effect"
 
-export interface ServerOptions {
-  readonly hostname?: string
-  readonly port?: number
-  readonly password?: string
-  readonly database?: Database.Options
-  readonly models?: ModelsDev.Options
-}
+export const ServerOptions = Schema.Struct({
+  hostname: Schema.optional(Schema.String),
+  port: Schema.optional(
+    Schema.Int.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(65_535)),
+  ),
+  password: Schema.optional(Schema.String),
+  database: Schema.optional(Database.Options),
+  models: Schema.optional(ModelsDev.Options),
+  observability: Schema.optional(Observability.Options),
+  fs: Schema.optional(
+    Schema.Struct({
+      filewatcher: Schema.optional(Schema.Boolean),
+      fff: Schema.optional(Schema.Boolean),
+    }),
+  ),
+})
+export type ServerOptions = typeof ServerOptions.Type

+ 5 - 1
packages/server/src/routes.ts

@@ -4,6 +4,7 @@ import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
 import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
 import { EventV2 } from "@opencode-ai/core/event"
 import { EventLogger } from "@opencode-ai/core/event-logger"
+import { FileSystemSearch } from "@opencode-ai/core/filesystem/search"
 import { Observability } from "@opencode-ai/core/observability"
 import { Credential } from "@opencode-ai/core/credential"
 import { PermissionSaved } from "@opencode-ai/core/permission/saved"
@@ -18,6 +19,7 @@ import { PluginRuntime } from "@opencode-ai/core/plugin/runtime"
 import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
 import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
 import { WellKnown } from "@opencode-ai/core/wellknown"
+import { Watcher } from "@opencode-ai/core/filesystem/watcher"
 import { HttpRouter, HttpServer } from "effect/unstable/http"
 import { HttpApiBuilder } from "effect/unstable/httpapi"
 import { Context, Effect, Layer, Option } from "effect"
@@ -76,6 +78,8 @@ function makeRoutes<AuthError, AuthServices>(
   const replacements: LayerNode.Replacements = [
     [Database.node, Database.layer(options.database)],
     [ModelsDev.node, ModelsDev.nodeWith(options.models)],
+    [Watcher.node, Watcher.nodeWith({ enabled: options.fs?.filewatcher })],
+    [FileSystemSearch.node, FileSystemSearch.nodeWith({ fff: options.fs?.fff })],
     [PluginRuntime.node, PluginRuntime.layerWithCell(pluginRuntimeCell)],
     [PluginRuntime.providerNode, PluginRuntime.providerNodeWithCell(pluginRuntimeCell)],
   ]
@@ -104,7 +108,7 @@ function makeRoutes<AuthError, AuthServices>(
         Layer.provide(authorizationLayer),
         Layer.provide(schemaErrorLayer),
         Layer.provide(auth),
-        Layer.provide(Observability.layer),
+        Layer.provide(Observability.layer(options.observability)),
         HttpRouter.provideRequest(requestServices),
         Layer.provideMerge(services),
         Layer.provideMerge(HttpRouter.layer),