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

core: migrate config loading to Effect framework (#23032)

Dax 1 день назад
Родитель
Сommit
d9950598d0

+ 2 - 0
packages/opencode/src/cli/cmd/tui/app.tsx

@@ -135,7 +135,9 @@ export function tui(input: {
       await TuiPluginRuntime.dispose()
     }
 
+    console.log("starting renderer")
     const renderer = await createCliRenderer(rendererConfig(input.config))
+    console.log("renderer started")
 
     await render(() => {
       return (

+ 4 - 2
packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts

@@ -132,8 +132,10 @@ async function backupAndStripLegacy(file: string, source: string) {
 }
 
 async function opencodeFiles(input: { directories: string[]; cwd: string }) {
-  const project = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("opencode", input.cwd)
-  const files = [...project, ...ConfigPaths.fileInDirectory(Global.Path.config, "opencode")]
+  const files = [
+    ...ConfigPaths.fileInDirectory(Global.Path.config, "opencode"),
+    ...(await Filesystem.findUp(["opencode.json", "opencode.jsonc"], input.cwd, undefined, { rootFirst: true })),
+  ]
   for (const dir of unique(input.directories)) {
     files.push(...ConfigPaths.fileInDirectory(dir, "opencode"))
   }

+ 13 - 14
packages/opencode/src/cli/cmd/tui/config/tui.ts

@@ -89,15 +89,13 @@ async function mergeFile(acc: Acc, file: string, ctx: { directory: string }) {
   acc.result.plugin_origins = plugins
 }
 
-async function loadState(ctx: { directory: string }) {
+const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory: string }) {
   // Every config dir we may read from: global config dir, any `.opencode`
   // folders between cwd and home, and OPENCODE_CONFIG_DIR.
-  const directories = await ConfigPaths.directories(ctx.directory)
-  // One-time migration: extract tui keys (theme/keybinds/tui) from existing
-  // opencode.json files into sibling tui.json files.
-  await migrateTuiConfig({ directories, cwd: ctx.directory })
+  const directories = yield* ConfigPaths.directories(ctx.directory)
+  yield* Effect.promise(() => migrateTuiConfig({ directories, cwd: ctx.directory }))
 
-  const projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("tui", ctx.directory)
+  const projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : yield* ConfigPaths.files("tui", ctx.directory)
 
   const acc: Acc = {
     result: {},
@@ -105,18 +103,19 @@ async function loadState(ctx: { directory: string }) {
 
   // 1. Global tui config (lowest precedence).
   for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) {
-    await mergeFile(acc, file, ctx)
+    yield* Effect.promise(() => mergeFile(acc, file, ctx)).pipe(Effect.orDie)
   }
 
   // 2. Explicit OPENCODE_TUI_CONFIG override, if set.
   if (Flag.OPENCODE_TUI_CONFIG) {
-    await mergeFile(acc, Flag.OPENCODE_TUI_CONFIG, ctx)
-    log.debug("loaded custom tui config", { path: Flag.OPENCODE_TUI_CONFIG })
+    const configFile = Flag.OPENCODE_TUI_CONFIG
+    yield* Effect.promise(() => mergeFile(acc, configFile, ctx)).pipe(Effect.orDie)
+    log.debug("loaded custom tui config", { path: configFile })
   }
 
   // 3. Project tui files, applied root-first so the closest file wins.
   for (const file of projectFiles) {
-    await mergeFile(acc, file, ctx)
+    yield* Effect.promise(() => mergeFile(acc, file, ctx)).pipe(Effect.orDie)
   }
 
   // 4. `.opencode` directories (and OPENCODE_CONFIG_DIR) discovered while
@@ -127,7 +126,7 @@ async function loadState(ctx: { directory: string }) {
   for (const dir of dirs) {
     if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue
     for (const file of ConfigPaths.fileInDirectory(dir, "tui")) {
-      await mergeFile(acc, file, ctx)
+      yield* Effect.promise(() => mergeFile(acc, file, ctx)).pipe(Effect.orDie)
     }
   }
 
@@ -146,14 +145,14 @@ async function loadState(ctx: { directory: string }) {
     config: acc.result,
     dirs: acc.result.plugin?.length ? dirs : [],
   }
-}
+})
 
 export const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
     const directory = yield* CurrentWorkingDirectory
     const npm = yield* Npm.Service
-    const data = yield* Effect.promise(() => loadState({ directory }))
+    const data = yield* loadState({ directory })
     const deps = yield* Effect.forEach(
       data.dirs,
       (dir) =>
@@ -176,7 +175,7 @@ export const layer = Layer.effect(
   }).pipe(Effect.withSpan("TuiConfig.layer")),
 )
 
-export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer))
+export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer), Layer.provide(AppFileSystem.defaultLayer))
 
 const { runPromise } = makeRuntime(Service, defaultLayer)
 

+ 219 - 218
packages/opencode/src/config/config.ts

@@ -413,260 +413,261 @@ export const layer = Layer.effect(
       }
     })
 
-    const loadInstanceState = Effect.fn("Config.loadInstanceState")(function* (ctx: InstanceContext) {
-      const auth = yield* authSvc.all().pipe(Effect.orDie)
-
-      let result: Info = {}
-      const consoleManagedProviders = new Set<string>()
-      let activeOrgName: string | undefined
-
-      const pluginScopeForSource = Effect.fnUntraced(function* (source: string) {
-        if (source.startsWith("http://") || source.startsWith("https://")) return "global"
-        if (source === "OPENCODE_CONFIG_CONTENT") return "local"
-        if (yield* InstanceRef.use((ctx) => Effect.succeed(Instance.containsPath(source, ctx)))) return "local"
-        return "global"
-      })
+    const loadInstanceState = Effect.fn("Config.loadInstanceState")(
+      function* (ctx: InstanceContext) {
+        const auth = yield* authSvc.all().pipe(Effect.orDie)
+
+        let result: Info = {}
+        const consoleManagedProviders = new Set<string>()
+        let activeOrgName: string | undefined
+
+        const pluginScopeForSource = Effect.fnUntraced(function* (source: string) {
+          if (source.startsWith("http://") || source.startsWith("https://")) return "global"
+          if (source === "OPENCODE_CONFIG_CONTENT") return "local"
+          if (yield* InstanceRef.use((ctx) => Effect.succeed(Instance.containsPath(source, ctx)))) return "local"
+          return "global"
+        })
 
-      const mergePluginOrigins = Effect.fnUntraced(function* (
-        source: string,
-        // mergePluginOrigins receives raw Specs from one config source, before provenance for this merge step
-        // is attached.
-        list: ConfigPlugin.Spec[] | undefined,
-        // Scope can be inferred from the source path, but some callers already know whether the config should
-        // behave as global or local and can pass that explicitly.
-        kind?: ConfigPlugin.Scope,
-      ) {
-        if (!list?.length) return
-        const hit = kind ?? (yield* pluginScopeForSource(source))
-        // Merge newly seen plugin origins with previously collected ones, then dedupe by plugin identity while
-        // keeping the winning source/scope metadata for downstream installs, writes, and diagnostics.
-        const plugins = ConfigPlugin.deduplicatePluginOrigins([
-          ...(result.plugin_origins ?? []),
-          ...list.map((spec) => ({ spec, source, scope: hit })),
-        ])
-        result.plugin = plugins.map((item) => item.spec)
-        result.plugin_origins = plugins
-      })
+        const mergePluginOrigins = Effect.fnUntraced(function* (
+          source: string,
+          // mergePluginOrigins receives raw Specs from one config source, before provenance for this merge step
+          // is attached.
+          list: ConfigPlugin.Spec[] | undefined,
+          // Scope can be inferred from the source path, but some callers already know whether the config should
+          // behave as global or local and can pass that explicitly.
+          kind?: ConfigPlugin.Scope,
+        ) {
+          if (!list?.length) return
+          const hit = kind ?? (yield* pluginScopeForSource(source))
+          // Merge newly seen plugin origins with previously collected ones, then dedupe by plugin identity while
+          // keeping the winning source/scope metadata for downstream installs, writes, and diagnostics.
+          const plugins = ConfigPlugin.deduplicatePluginOrigins([
+            ...(result.plugin_origins ?? []),
+            ...list.map((spec) => ({ spec, source, scope: hit })),
+          ])
+          result.plugin = plugins.map((item) => item.spec)
+          result.plugin_origins = plugins
+        })
 
-      const merge = (source: string, next: Info, kind?: ConfigPlugin.Scope) => {
-        result = mergeConfigConcatArrays(result, next)
-        return mergePluginOrigins(source, next.plugin, kind)
-      }
+        const merge = (source: string, next: Info, kind?: ConfigPlugin.Scope) => {
+          result = mergeConfigConcatArrays(result, next)
+          return mergePluginOrigins(source, next.plugin, kind)
+        }
 
-      for (const [key, value] of Object.entries(auth)) {
-        if (value.type === "wellknown") {
-          const url = key.replace(/\/+$/, "")
-          process.env[value.key] = value.token
-          log.debug("fetching remote config", { url: `${url}/.well-known/opencode` })
-          const response = yield* Effect.promise(() => fetch(`${url}/.well-known/opencode`))
-          if (!response.ok) {
-            throw new Error(`failed to fetch remote config from ${url}: ${response.status}`)
+        for (const [key, value] of Object.entries(auth)) {
+          if (value.type === "wellknown") {
+            const url = key.replace(/\/+$/, "")
+            process.env[value.key] = value.token
+            log.debug("fetching remote config", { url: `${url}/.well-known/opencode` })
+            const response = yield* Effect.promise(() => fetch(`${url}/.well-known/opencode`))
+            if (!response.ok) {
+              throw new Error(`failed to fetch remote config from ${url}: ${response.status}`)
+            }
+            const wellknown = (yield* Effect.promise(() => response.json())) as { config?: Record<string, unknown> }
+            const remoteConfig = wellknown.config ?? {}
+            if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json"
+            const source = `${url}/.well-known/opencode`
+            const next = yield* loadConfig(JSON.stringify(remoteConfig), {
+              dir: path.dirname(source),
+              source,
+            })
+            yield* merge(source, next, "global")
+            log.debug("loaded remote config from well-known", { url })
           }
-          const wellknown = (yield* Effect.promise(() => response.json())) as { config?: Record<string, unknown> }
-          const remoteConfig = wellknown.config ?? {}
-          if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json"
-          const source = `${url}/.well-known/opencode`
-          const next = yield* loadConfig(JSON.stringify(remoteConfig), {
-            dir: path.dirname(source),
-            source,
-          })
-          yield* merge(source, next, "global")
-          log.debug("loaded remote config from well-known", { url })
         }
-      }
-
-      const global = yield* getGlobal()
-      yield* merge(Global.Path.config, global, "global")
 
-      if (Flag.OPENCODE_CONFIG) {
-        yield* merge(Flag.OPENCODE_CONFIG, yield* loadFile(Flag.OPENCODE_CONFIG))
-        log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
-      }
+        const global = yield* getGlobal()
+        yield* merge(Global.Path.config, global, "global")
 
-      if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
-        for (const file of yield* Effect.promise(() =>
-          ConfigPaths.projectFiles("opencode", ctx.directory, ctx.worktree),
-        )) {
-          yield* merge(file, yield* loadFile(file), "local")
+        if (Flag.OPENCODE_CONFIG) {
+          yield* merge(Flag.OPENCODE_CONFIG, yield* loadFile(Flag.OPENCODE_CONFIG))
+          log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
         }
-      }
 
-      result.agent = result.agent || {}
-      result.mode = result.mode || {}
-      result.plugin = result.plugin || []
+        if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
+          for (const file of yield* ConfigPaths.files("opencode", ctx.directory, ctx.worktree).pipe(Effect.orDie)) {
+            yield* merge(file, yield* loadFile(file), "local")
+          }
+        }
 
-      const directories = yield* Effect.promise(() => ConfigPaths.directories(ctx.directory, ctx.worktree))
+        result.agent = result.agent || {}
+        result.mode = result.mode || {}
+        result.plugin = result.plugin || []
 
-      if (Flag.OPENCODE_CONFIG_DIR) {
-        log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
-      }
+        const directories = yield* ConfigPaths.directories(ctx.directory, ctx.worktree)
 
-      const deps: Fiber.Fiber<void, never>[] = []
+        if (Flag.OPENCODE_CONFIG_DIR) {
+          log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
+        }
 
-      for (const dir of directories) {
-        if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
-          for (const file of ["opencode.json", "opencode.jsonc"]) {
-            const source = path.join(dir, file)
-            log.debug(`loading config from ${source}`)
-            yield* merge(source, yield* loadFile(source))
-            result.agent ??= {}
-            result.mode ??= {}
-            result.plugin ??= []
+        const deps: Fiber.Fiber<void, never>[] = []
+
+        for (const dir of directories) {
+          if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
+            for (const file of ["opencode.json", "opencode.jsonc"]) {
+              const source = path.join(dir, file)
+              log.debug(`loading config from ${source}`)
+              yield* merge(source, yield* loadFile(source))
+              result.agent ??= {}
+              result.mode ??= {}
+              result.plugin ??= []
+            }
           }
-        }
 
-        yield* ensureGitignore(dir).pipe(Effect.orDie)
+          yield* ensureGitignore(dir).pipe(Effect.orDie)
+
+          const dep = yield* npmSvc
+            .install(dir, {
+              add: ["@opencode-ai/plugin" + (InstallationLocal ? "" : "@" + InstallationVersion)],
+            })
+            .pipe(
+              Effect.exit,
+              Effect.tap((exit) =>
+                Exit.isFailure(exit)
+                  ? Effect.sync(() => {
+                      log.warn("background dependency install failed", { dir, error: String(exit.cause) })
+                    })
+                  : Effect.void,
+              ),
+              Effect.asVoid,
+              Effect.forkDetach,
+            )
+          deps.push(dep)
+
+          result.command = mergeDeep(result.command ?? {}, yield* Effect.promise(() => ConfigCommand.load(dir)))
+          result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.load(dir)))
+          result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.loadMode(dir)))
+          // Auto-discovered plugins under `.opencode/plugin(s)` are already local files, so ConfigPlugin.load
+          // returns normalized Specs and we only need to attach origin metadata here.
+          const list = yield* Effect.promise(() => ConfigPlugin.load(dir))
+          yield* mergePluginOrigins(dir, list)
+        }
 
-        const dep = yield* npmSvc
-          .install(dir, {
-            add: ["@opencode-ai/plugin" + (InstallationLocal ? "" : "@" + InstallationVersion)],
+        if (process.env.OPENCODE_CONFIG_CONTENT) {
+          const source = "OPENCODE_CONFIG_CONTENT"
+          const next = yield* loadConfig(process.env.OPENCODE_CONFIG_CONTENT, {
+            dir: ctx.directory,
+            source,
           })
-          .pipe(
-            Effect.exit,
-            Effect.tap((exit) =>
-              Exit.isFailure(exit)
-                ? Effect.sync(() => {
-                    log.warn("background dependency install failed", { dir, error: String(exit.cause) })
-                  })
-                : Effect.void,
-            ),
-            Effect.asVoid,
-            Effect.forkDetach,
-          )
-        deps.push(dep)
-
-        result.command = mergeDeep(result.command ?? {}, yield* Effect.promise(() => ConfigCommand.load(dir)))
-        result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.load(dir)))
-        result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.loadMode(dir)))
-        // Auto-discovered plugins under `.opencode/plugin(s)` are already local files, so ConfigPlugin.load
-        // returns normalized Specs and we only need to attach origin metadata here.
-        const list = yield* Effect.promise(() => ConfigPlugin.load(dir))
-        yield* mergePluginOrigins(dir, list)
-      }
+          yield* merge(source, next, "local")
+          log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
+        }
 
-      if (process.env.OPENCODE_CONFIG_CONTENT) {
-        const source = "OPENCODE_CONFIG_CONTENT"
-        const next = yield* loadConfig(process.env.OPENCODE_CONFIG_CONTENT, {
-          dir: ctx.directory,
-          source,
-        })
-        yield* merge(source, next, "local")
-        log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
-      }
+        const activeAccount = Option.getOrUndefined(
+          yield* accountSvc.active().pipe(Effect.catch(() => Effect.succeed(Option.none()))),
+        )
+        if (activeAccount?.active_org_id) {
+          const accountID = activeAccount.id
+          const orgID = activeAccount.active_org_id
+          const url = activeAccount.url
+          yield* Effect.gen(function* () {
+            const [configOpt, tokenOpt] = yield* Effect.all(
+              [accountSvc.config(accountID, orgID), accountSvc.token(accountID)],
+              { concurrency: 2 },
+            )
+            if (Option.isSome(tokenOpt)) {
+              process.env["OPENCODE_CONSOLE_TOKEN"] = tokenOpt.value
+              yield* env.set("OPENCODE_CONSOLE_TOKEN", tokenOpt.value)
+            }
 
-      const activeAccount = Option.getOrUndefined(
-        yield* accountSvc.active().pipe(Effect.catch(() => Effect.succeed(Option.none()))),
-      )
-      if (activeAccount?.active_org_id) {
-        const accountID = activeAccount.id
-        const orgID = activeAccount.active_org_id
-        const url = activeAccount.url
-        yield* Effect.gen(function* () {
-          const [configOpt, tokenOpt] = yield* Effect.all(
-            [accountSvc.config(accountID, orgID), accountSvc.token(accountID)],
-            { concurrency: 2 },
+            if (Option.isSome(configOpt)) {
+              const source = `${url}/api/config`
+              const next = yield* loadConfig(JSON.stringify(configOpt.value), {
+                dir: path.dirname(source),
+                source,
+              })
+              for (const providerID of Object.keys(next.provider ?? {})) {
+                consoleManagedProviders.add(providerID)
+              }
+              yield* merge(source, next, "global")
+            }
+          }).pipe(
+            Effect.withSpan("Config.loadActiveOrgConfig"),
+            Effect.catch((err) => {
+              log.debug("failed to fetch remote account config", {
+                error: err instanceof Error ? err.message : String(err),
+              })
+              return Effect.void
+            }),
           )
-          if (Option.isSome(tokenOpt)) {
-            process.env["OPENCODE_CONSOLE_TOKEN"] = tokenOpt.value
-            yield* env.set("OPENCODE_CONSOLE_TOKEN", tokenOpt.value)
-          }
+        }
 
-          if (Option.isSome(configOpt)) {
-            const source = `${url}/api/config`
-            const next = yield* loadConfig(JSON.stringify(configOpt.value), {
-              dir: path.dirname(source),
-              source,
-            })
-            for (const providerID of Object.keys(next.provider ?? {})) {
-              consoleManagedProviders.add(providerID)
-            }
-            yield* merge(source, next, "global")
+        const managedDir = ConfigManaged.managedConfigDir()
+        if (existsSync(managedDir)) {
+          for (const file of ["opencode.json", "opencode.jsonc"]) {
+            const source = path.join(managedDir, file)
+            yield* merge(source, yield* loadFile(source), "global")
           }
-        }).pipe(
-          Effect.withSpan("Config.loadActiveOrgConfig"),
-          Effect.catch((err) => {
-            log.debug("failed to fetch remote account config", {
-              error: err instanceof Error ? err.message : String(err),
-            })
-            return Effect.void
-          }),
-        )
-      }
-
-      const managedDir = ConfigManaged.managedConfigDir()
-      if (existsSync(managedDir)) {
-        for (const file of ["opencode.json", "opencode.jsonc"]) {
-          const source = path.join(managedDir, file)
-          yield* merge(source, yield* loadFile(source), "global")
         }
-      }
 
-      // macOS managed preferences (.mobileconfig deployed via MDM) override everything
-      const managed = yield* Effect.promise(() => ConfigManaged.readManagedPreferences())
-      if (managed) {
-        result = mergeConfigConcatArrays(
-          result,
-          yield* loadConfig(managed.text, {
-            dir: path.dirname(managed.source),
-            source: managed.source,
-          }),
-        )
-      }
+        // macOS managed preferences (.mobileconfig deployed via MDM) override everything
+        const managed = yield* Effect.promise(() => ConfigManaged.readManagedPreferences())
+        if (managed) {
+          result = mergeConfigConcatArrays(
+            result,
+            yield* loadConfig(managed.text, {
+              dir: path.dirname(managed.source),
+              source: managed.source,
+            }),
+          )
+        }
 
-      for (const [name, mode] of Object.entries(result.mode ?? {})) {
-        result.agent = mergeDeep(result.agent ?? {}, {
-          [name]: {
-            ...mode,
-            mode: "primary" as const,
-          },
-        })
-      }
+        for (const [name, mode] of Object.entries(result.mode ?? {})) {
+          result.agent = mergeDeep(result.agent ?? {}, {
+            [name]: {
+              ...mode,
+              mode: "primary" as const,
+            },
+          })
+        }
 
-      if (Flag.OPENCODE_PERMISSION) {
-        result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION))
-      }
+        if (Flag.OPENCODE_PERMISSION) {
+          result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION))
+        }
 
-      if (result.tools) {
-        const perms: Record<string, ConfigPermission.Action> = {}
-        for (const [tool, enabled] of Object.entries(result.tools)) {
-          const action: ConfigPermission.Action = enabled ? "allow" : "deny"
-          if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
-            perms.edit = action
-            continue
+        if (result.tools) {
+          const perms: Record<string, ConfigPermission.Action> = {}
+          for (const [tool, enabled] of Object.entries(result.tools)) {
+            const action: ConfigPermission.Action = enabled ? "allow" : "deny"
+            if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") {
+              perms.edit = action
+              continue
+            }
+            perms[tool] = action
           }
-          perms[tool] = action
+          result.permission = mergeDeep(perms, result.permission ?? {})
         }
-        result.permission = mergeDeep(perms, result.permission ?? {})
-      }
 
-      if (!result.username) result.username = os.userInfo().username
+        if (!result.username) result.username = os.userInfo().username
 
-      if (result.autoshare === true && !result.share) {
-        result.share = "auto"
-      }
+        if (result.autoshare === true && !result.share) {
+          result.share = "auto"
+        }
 
-      if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) {
-        result.compaction = { ...result.compaction, auto: false }
-      }
-      if (Flag.OPENCODE_DISABLE_PRUNE) {
-        result.compaction = { ...result.compaction, prune: false }
-      }
+        if (Flag.OPENCODE_DISABLE_AUTOCOMPACT) {
+          result.compaction = { ...result.compaction, auto: false }
+        }
+        if (Flag.OPENCODE_DISABLE_PRUNE) {
+          result.compaction = { ...result.compaction, prune: false }
+        }
 
-      return {
-        config: result,
-        directories,
-        deps,
-        consoleState: {
-          consoleManagedProviders: Array.from(consoleManagedProviders),
-          activeOrgName,
-          switchableOrgCount: 0,
-        },
-      }
-    })
+        return {
+          config: result,
+          directories,
+          deps,
+          consoleState: {
+            consoleManagedProviders: Array.from(consoleManagedProviders),
+            activeOrgName,
+            switchableOrgCount: 0,
+          },
+        }
+      },
+      Effect.provideService(AppFileSystem.Service, fs),
+    )
 
     const state = yield* InstanceState.make<State>(
       Effect.fn("Config.state")(function* (ctx) {
-        return yield* loadInstanceState(ctx)
+        return yield* loadInstanceState(ctx).pipe(Effect.orDie)
       }),
     )
 

+ 27 - 19
packages/opencode/src/config/paths.ts

@@ -6,33 +6,41 @@ import { Flag } from "@/flag/flag"
 import { Global } from "@/global"
 import { unique } from "remeda"
 import { JsonError } from "./error"
+import * as Effect from "effect/Effect"
+import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 
-export async function projectFiles(name: string, directory: string, worktree?: string) {
-  return Filesystem.findUp([`${name}.json`, `${name}.jsonc`], directory, worktree, { rootFirst: true })
-}
+export const files = Effect.fn("ConfigPaths.projectFiles")(function* (
+  name: string,
+  directory: string,
+  worktree?: string,
+) {
+  const afs = yield* AppFileSystem.Service
+  return (yield* afs.up({
+    targets: [`${name}.jsonc`, `${name}.json`],
+    start: directory,
+    stop: worktree,
+  })).toReversed()
+})
 
-export async function directories(directory: string, worktree?: string) {
+export const directories = Effect.fn("ConfigPaths.directories")(function* (directory: string, worktree?: string) {
+  const afs = yield* AppFileSystem.Service
   return unique([
     Global.Path.config,
     ...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG
-      ? await Array.fromAsync(
-          Filesystem.up({
-            targets: [".opencode"],
-            start: directory,
-            stop: worktree,
-          }),
-        )
+      ? yield* afs.up({
+          targets: [".opencode"],
+          start: directory,
+          stop: worktree,
+        })
       : []),
-    ...(await Array.fromAsync(
-      Filesystem.up({
-        targets: [".opencode"],
-        start: Global.Path.home,
-        stop: Global.Path.home,
-      }),
-    )),
+    ...(yield* afs.up({
+      targets: [".opencode"],
+      start: Global.Path.home,
+      stop: Global.Path.home,
+    })),
     ...(Flag.OPENCODE_CONFIG_DIR ? [Flag.OPENCODE_CONFIG_DIR] : []),
   ])
-}
+})
 
 export function fileInDirectory(dir: string, name: string) {
   return [path.join(dir, `${name}.json`), path.join(dir, `${name}.jsonc`)]