فهرست منبع

effect: add RuntimeFlags service (#27181)

Kit Langton 3 ماه پیش
والد
کامیت
b7c6fa611f

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

@@ -0,0 +1,27 @@
+import { Config, ConfigProvider, Context, Effect, Layer } from "effect"
+import { ConfigService } from "@/effect/config-service"
+
+export class Service extends ConfigService.Service<Service>()("@opencode/RuntimeFlags", {
+  pure: Config.boolean("OPENCODE_PURE").pipe(Config.withDefault(false)),
+  disableDefaultPlugins: Config.boolean("OPENCODE_DISABLE_DEFAULT_PLUGINS").pipe(Config.withDefault(false)),
+}) {}
+
+export type Info = Context.Service.Shape<typeof Service>
+
+const emptyConfigLayer = Service.defaultLayer.pipe(
+  Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown({}))),
+  Layer.orDie,
+)
+
+export const layer = (overrides: Partial<Info> = {}) =>
+  Layer.effect(
+    Service,
+    Effect.gen(function* () {
+      const flags = yield* Service
+      return Service.of({ ...flags, ...overrides })
+    }),
+  ).pipe(Layer.provide(emptyConfigLayer))
+
+export const defaultLayer = Service.defaultLayer.pipe(Layer.orDie)
+
+export * as RuntimeFlags from "./runtime-flags"

+ 10 - 5
packages/opencode/src/plugin/index.ts

@@ -9,7 +9,6 @@ import { Config } from "@/config/config"
 import { Bus } from "../bus"
 import * as Log from "@opencode-ai/core/util/log"
 import { createOpencodeClient } from "@opencode-ai/sdk"
-import { Flag } from "@opencode-ai/core/flag/flag"
 import { ServerAuth } from "@/server/auth"
 import { CodexAuthPlugin } from "./codex"
 import { Session } from "@/session/session"
@@ -28,6 +27,7 @@ import { PluginLoader } from "./loader"
 import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } from "./shared"
 import { registerAdapter } from "@/control-plane/adapters"
 import type { WorkspaceAdapter } from "@/control-plane/types"
+import { RuntimeFlags } from "@/effect/runtime-flags"
 
 const log = Log.create({ service: "plugin" })
 
@@ -112,6 +112,7 @@ export const layer = Layer.effect(
   Effect.gen(function* () {
     const bus = yield* Bus.Service
     const config = yield* Config.Service
+    const flags = yield* RuntimeFlags.Service
 
     const state = yield* InstanceState.make<State>(
       Effect.fn("Plugin.state")(function* (ctx) {
@@ -148,7 +149,7 @@ export const layer = Layer.effect(
           $: typeof Bun === "undefined" ? undefined : Bun.$,
         }
 
-        for (const plugin of INTERNAL_PLUGINS) {
+        for (const plugin of flags.disableDefaultPlugins ? [] : INTERNAL_PLUGINS) {
           log.info("loading internal plugin", { name: plugin.name })
           const init = yield* Effect.tryPromise({
             try: () => plugin(input),
@@ -159,8 +160,8 @@ export const layer = Layer.effect(
           if (init._tag === "Some") hooks.push(init.value)
         }
 
-        const plugins = Flag.OPENCODE_PURE ? [] : (cfg.plugin_origins ?? [])
-        if (Flag.OPENCODE_PURE && cfg.plugin_origins?.length) {
+        const plugins = flags.pure ? [] : (cfg.plugin_origins ?? [])
+        if (flags.pure && cfg.plugin_origins?.length) {
           log.info("skipping external plugins in pure mode", { count: cfg.plugin_origins.length })
         }
         if (plugins.length) yield* config.waitForDependencies()
@@ -285,6 +286,10 @@ export const layer = Layer.effect(
   }),
 )
 
-export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Config.defaultLayer))
+export const defaultLayer = layer.pipe(
+  Layer.provide(Bus.layer),
+  Layer.provide(Config.defaultLayer),
+  Layer.provide(RuntimeFlags.defaultLayer),
+)
 
 export * as Plugin from "."

+ 6 - 1
packages/opencode/test/agent/plugin-agent-regression.test.ts

@@ -7,6 +7,7 @@ import { Agent } from "../../src/agent/agent"
 import { Bus } from "../../src/bus"
 import { Config } from "../../src/config/config"
 import { Env } from "../../src/env"
+import { RuntimeFlags } from "../../src/effect/runtime-flags"
 import { Plugin } from "../../src/plugin"
 import { AccountTest } from "../fake/account"
 import { AuthTest } from "../fake/auth"
@@ -29,7 +30,11 @@ const configLayer = Config.layer.pipe(
   Layer.provide(AccountTest.empty),
   Layer.provide(NpmTest.noop),
 )
-const pluginLayer = Plugin.layer.pipe(Layer.provide(Bus.layer), Layer.provide(configLayer))
+const pluginLayer = Plugin.layer.pipe(
+  Layer.provide(Bus.layer),
+  Layer.provide(configLayer),
+  Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })),
+)
 const agentLayer = Agent.layer.pipe(
   Layer.provide(configLayer),
   Layer.provide(AuthTest.empty),

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

@@ -0,0 +1,55 @@
+import { describe, expect } from "bun:test"
+import { ConfigProvider, Effect, Layer } from "effect"
+import { RuntimeFlags } from "../../src/effect/runtime-flags"
+import { it } from "../lib/effect"
+
+const fromConfig = (input: Record<string, unknown>) =>
+  RuntimeFlags.defaultLayer.pipe(Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(input))))
+
+const readFlags = RuntimeFlags.Service.useSync((flags) => flags)
+
+describe("RuntimeFlags", () => {
+  it.effect("defaultLayer parses plugin flags from the active ConfigProvider", () =>
+    Effect.gen(function* () {
+      const flags = yield* readFlags.pipe(
+        Effect.provide(
+          fromConfig({
+            OPENCODE_PURE: "true",
+            OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
+          }),
+        ),
+      )
+
+      expect(flags.pure).toBe(true)
+      expect(flags.disableDefaultPlugins).toBe(true)
+    }),
+  )
+
+  it.effect("layer accepts partial test overrides and fills defaults from Config definitions", () =>
+    Effect.gen(function* () {
+      const flags = yield* readFlags.pipe(Effect.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })))
+
+      expect(flags.pure).toBe(false)
+      expect(flags.disableDefaultPlugins).toBe(true)
+    }),
+  )
+
+  it.effect("layer ignores the active ConfigProvider for omitted test overrides", () =>
+    Effect.gen(function* () {
+      const flags = yield* readFlags.pipe(
+        Effect.provide(RuntimeFlags.layer()),
+        Effect.provide(
+          ConfigProvider.layer(
+            ConfigProvider.fromUnknown({
+              OPENCODE_PURE: "true",
+              OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
+            }),
+          ),
+        ),
+      )
+
+      expect(flags.pure).toBe(false)
+      expect(flags.disableDefaultPlugins).toBe(false)
+    }),
+  )
+})

+ 2 - 0
packages/opencode/test/plugin/auth-override.test.ts

@@ -7,6 +7,7 @@ import { provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture"
 import { ProviderAuth } from "@/provider/auth"
 import { ProviderID } from "../../src/provider/schema"
 import { Plugin } from "@/plugin"
+import { RuntimeFlags } from "@/effect/runtime-flags"
 import { Auth } from "@/auth"
 import { Bus } from "@/bus"
 import { TestConfig } from "../fixture/config"
@@ -21,6 +22,7 @@ function layer(directory: string, plugins: string[]) {
     Layer.provide(
       Plugin.layer.pipe(
         Layer.provide(Bus.layer),
+        Layer.provide(RuntimeFlags.layer()),
         Layer.provide(
           TestConfig.layer({
             get: () =>

+ 12 - 32
packages/opencode/test/plugin/loader-shared.test.ts

@@ -1,4 +1,4 @@
-import { afterAll, afterEach, describe, expect, spyOn } from "bun:test"
+import { afterEach, describe, expect, spyOn } from "bun:test"
 import { Effect, Layer } from "effect"
 import fs from "fs/promises"
 import path from "path"
@@ -8,23 +8,13 @@ import { disposeAllInstances, provideInstance, tmpdirScoped } from "../fixture/f
 import { testEffect } from "../lib/effect"
 import { Filesystem } from "@/util/filesystem"
 
-const disableDefault = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS
-process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "1"
-
 const { Plugin } = await import("../../src/plugin/index")
 const { PluginLoader } = await import("../../src/plugin/loader")
 const { readPackageThemes } = await import("../../src/plugin/shared")
 const { Bus } = await import("../../src/bus")
 const { Npm } = await import("@opencode-ai/core/npm")
 const { TestConfig } = await import("../fixture/config")
-
-afterAll(() => {
-  if (disableDefault === undefined) {
-    delete process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS
-    return
-  }
-  process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = disableDefault
-})
+const { RuntimeFlags } = await import("../../src/effect/runtime-flags")
 
 afterEach(async () => {
   await disposeAllInstances()
@@ -43,7 +33,7 @@ function withTmp<T, A, E, R>(
   })
 }
 
-function load(dir: string) {
+function load(dir: string, flags?: Parameters<typeof RuntimeFlags.layer>[0]) {
   const source = path.join(dir, "opencode.json")
   return Effect.gen(function* () {
     const config = yield* Effect.promise(
@@ -57,6 +47,7 @@ function load(dir: string) {
       Effect.provide(
         Plugin.layer.pipe(
           Layer.provide(Bus.layer),
+          Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true, ...flags })),
           Layer.provide(
             TestConfig.layer({
               get: () =>
@@ -934,25 +925,14 @@ export default {
       },
       (tmp) =>
         Effect.gen(function* () {
-          const pure = process.env.OPENCODE_PURE
-          process.env.OPENCODE_PURE = "1"
-
-          try {
-            yield* load(tmp.path)
-            const called = yield* Effect.promise(() =>
-              fs
-                .readFile(tmp.extra.mark, "utf8")
-                .then(() => true)
-                .catch(() => false),
-            )
-            expect(called).toBe(false)
-          } finally {
-            if (pure === undefined) {
-              delete process.env.OPENCODE_PURE
-            } else {
-              process.env.OPENCODE_PURE = pure
-            }
-          }
+          yield* load(tmp.path, { pure: true })
+          const called = yield* Effect.promise(() =>
+            fs
+              .readFile(tmp.extra.mark, "utf8")
+              .then(() => true)
+              .catch(() => false),
+          )
+          expect(called).toBe(false)
         }),
     ),
   )

+ 6 - 1
packages/opencode/test/plugin/trigger.test.ts

@@ -10,6 +10,7 @@ import { Auth } from "../../src/auth"
 import { Bus } from "../../src/bus"
 import { Config } from "../../src/config/config"
 import { Env } from "../../src/env"
+import { RuntimeFlags } from "../../src/effect/runtime-flags"
 import { Plugin } from "../../src/plugin/index"
 import { ModelID, ProviderID } from "../../src/provider/schema"
 import { provideTmpdirInstance } from "../fixture/fixture"
@@ -33,7 +34,11 @@ const configLayer = Config.layer.pipe(
 )
 const it = testEffect(
   Layer.mergeAll(
-    Plugin.layer.pipe(Layer.provide(Bus.layer), Layer.provide(configLayer)),
+    Plugin.layer.pipe(
+      Layer.provide(Bus.layer),
+      Layer.provide(configLayer),
+      Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })),
+    ),
     CrossSpawnSpawner.defaultLayer,
   ),
 )

+ 6 - 1
packages/opencode/test/plugin/workspace-adapter.test.ts

@@ -11,6 +11,7 @@ import { Auth } from "../../src/auth"
 import { Bus } from "../../src/bus"
 import { Config } from "../../src/config/config"
 import { Env } from "../../src/env"
+import { RuntimeFlags } from "../../src/effect/runtime-flags"
 import { Workspace } from "../../src/control-plane/workspace"
 import { Plugin } from "../../src/plugin/index"
 import { InstanceBootstrap } from "../../src/project/bootstrap-service"
@@ -35,7 +36,11 @@ const configLayer = Config.layer.pipe(
   Layer.provide(emptyAccount),
   Layer.provide(NpmTest.noop),
 )
-const pluginLayer = Plugin.layer.pipe(Layer.provide(Bus.layer), Layer.provide(configLayer))
+const pluginLayer = Plugin.layer.pipe(
+  Layer.provide(Bus.layer),
+  Layer.provide(configLayer),
+  Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })),
+)
 const noopBootstrapLayer = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
 const workspaceLayer = Workspace.defaultLayer.pipe(
   Layer.provide(InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrapLayer))),

+ 0 - 1
packages/opencode/test/preload.ts

@@ -45,7 +45,6 @@ process.env["OPENCODE_TEST_HOME"] = testHome
 // Set test managed config directory to isolate tests from system managed settings
 const testManagedConfigDir = path.join(dir, "managed")
 process.env["OPENCODE_TEST_MANAGED_CONFIG_DIR"] = testManagedConfigDir
-process.env["OPENCODE_DISABLE_DEFAULT_PLUGINS"] = "true"
 
 // Write the cache version file to prevent global/index.ts from clearing the cache
 const cacheDir = path.join(dir, "cache", "opencode")