From fd45c7bb0f6914557266cb3304723c60bed3e47e Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 12 May 2026 16:24:31 -0400 Subject: [PATCH] effect(config): extract ConfigPaths.Service from config/paths --- .../src/cli/cmd/tui/config/tui-migrate.ts | 2 +- .../opencode/src/cli/cmd/tui/config/tui.ts | 13 ++- packages/opencode/src/config/config.ts | 6 +- packages/opencode/src/config/paths.ts | 99 ++++++++++++------- packages/opencode/src/plugin/install.ts | 2 +- .../agent/plugin-agent-regression.test.ts | 2 + packages/opencode/test/config/config.test.ts | 7 ++ 7 files changed, 89 insertions(+), 42 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts b/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts index b4dc02d3b8..fe55f35f4b 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts @@ -7,7 +7,7 @@ import { Flag } from "@opencode-ai/core/flag/flag" import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util/filesystem" import * as Log from "@opencode-ai/core/util/log" -import * as ConfigPaths from "@/config/paths" +import { ConfigPaths } from "@/config/paths" const log = Log.create({ service: "tui.migrate" }) diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts index 0d4be41dfc..48b073654e 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts @@ -5,7 +5,7 @@ import { createBindingLookup } from "@opentui/keymap/extras" import { mergeDeep, unique } from "remeda" import { Cause, Context, Effect, Fiber, Layer, Schema } from "effect" import { ConfigParse } from "@/config/parse" -import * as ConfigPaths from "@/config/paths" +import { ConfigPaths } from "@/config/paths" import { migrateTuiConfig } from "./tui-migrate" import { KeymapLeaderTimeoutDefault, resolveAttentionSoundPaths, TuiInfo } from "./tui-schema" import { Flag } from "@opencode-ai/core/flag/flag" @@ -99,6 +99,7 @@ function dropUnknownKeybinds(input: Record, configFilepath: str const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory: string }) { const afs = yield* AppFileSystem.Service let appliedOrder = 0 + const paths = yield* ConfigPaths.Service const resolvePlugins = (config: Info, configFilepath: string): Effect.Effect => Effect.gen(function* () { @@ -191,10 +192,10 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory: // Every config dir we may read from: global config dir, any `.opencode` // folders between cwd and home, and OPENCODE_CONFIG_DIR. - const directories = yield* ConfigPaths.directories(ctx.directory) + const directories = yield* paths.directories(ctx.directory) yield* Effect.promise(() => migrateTuiConfig({ directories, cwd: ctx.directory })) - const projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : yield* ConfigPaths.files("tui", ctx.directory) + const projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : yield* paths.projectFiles("tui", ctx.directory) const acc: Acc = { result: {}, @@ -295,7 +296,11 @@ export const layer = Layer.effect( }).pipe(Effect.withSpan("TuiConfig.layer")), ) -export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer), Layer.provide(AppFileSystem.defaultLayer)) +export const defaultLayer = layer.pipe( + Layer.provide(Npm.defaultLayer), + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(ConfigPaths.defaultLayer), +) const { runPromise } = makeRuntime(Service, defaultLayer) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 545e48e64d..2e36ab4431 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -370,6 +370,7 @@ export const layer = Layer.effect( const accountSvc = yield* Account.Service const env = yield* Env.Service const npmSvc = yield* Npm.Service + const paths = yield* ConfigPaths.Service const readConfigFile = (filepath: string) => fs.readFileStringSafe(filepath).pipe(Effect.orDie) @@ -562,7 +563,7 @@ export const layer = Layer.effect( } if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) { - for (const file of yield* ConfigPaths.files("opencode", ctx.directory, ctx.worktree).pipe(Effect.orDie)) { + for (const file of yield* paths.projectFiles("opencode", ctx.directory, ctx.worktree).pipe(Effect.orDie)) { yield* merge(file, yield* loadFile(file), "local") } } @@ -571,7 +572,7 @@ export const layer = Layer.effect( result.mode = result.mode || {} result.plugin = result.plugin || [] - const directories = yield* ConfigPaths.directories(ctx.directory, ctx.worktree) + const directories = yield* paths.directories(ctx.directory, ctx.worktree) if (Flag.OPENCODE_CONFIG_DIR) { log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR }) @@ -829,6 +830,7 @@ export const defaultLayer = layer.pipe( Layer.provide(Auth.defaultLayer), Layer.provide(Account.defaultLayer), Layer.provide(Npm.defaultLayer), + Layer.provide(ConfigPaths.defaultLayer), ) export * as Config from "./config" diff --git a/packages/opencode/src/config/paths.ts b/packages/opencode/src/config/paths.ts index 82fca570f4..0530f10fc5 100644 --- a/packages/opencode/src/config/paths.ts +++ b/packages/opencode/src/config/paths.ts @@ -4,42 +4,73 @@ import path from "path" import { Flag } from "@opencode-ai/core/flag/flag" import { Global } from "@opencode-ai/core/global" import { unique } from "remeda" -import * as Effect from "effect/Effect" +import { Context, Effect, Layer } from "effect" import { AppFileSystem } from "@opencode-ai/core/filesystem" - -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 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 - ? yield* afs.up({ - targets: [".opencode"], - start: directory, - stop: worktree, - }) - : []), - ...(yield* afs.up({ - targets: [".opencode"], - start: Global.Path.home, - stop: Global.Path.home, - })), - ...(Flag.OPENCODE_CONFIG_DIR ? [Flag.OPENCODE_CONFIG_DIR] : []), - ]) -}) +import { ConfigParse } from "./parse" export function fileInDirectory(dir: string, name: string) { return [path.join(dir, `${name}.json`), path.join(dir, `${name}.jsonc`)] } + +export interface Interface { + readonly projectFiles: ( + name: string, + directory: string, + worktree?: string, + ) => Effect.Effect + readonly directories: (directory: string, worktree?: string) => Effect.Effect + readonly readFile: (filepath: string) => Effect.Effect + readonly parseText: (text: string, filepath: string) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/ConfigPaths") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const afs = yield* AppFileSystem.Service + + const projectFiles = Effect.fn("ConfigPaths.projectFiles")(function* ( + name: string, + directory: string, + worktree?: string, + ) { + return (yield* afs.up({ + targets: [`${name}.jsonc`, `${name}.json`], + start: directory, + stop: worktree, + })).toReversed() + }) + + const directories = Effect.fn("ConfigPaths.directories")(function* (directory: string, worktree?: string) { + return unique([ + Global.Path.config, + ...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG + ? yield* afs.up({ + targets: [".opencode"], + start: directory, + stop: worktree, + }) + : []), + ...(yield* afs.up({ + targets: [".opencode"], + start: Global.Path.home, + stop: Global.Path.home, + })), + ...(Flag.OPENCODE_CONFIG_DIR ? [Flag.OPENCODE_CONFIG_DIR] : []), + ]) + }) + + const readFile = Effect.fn("ConfigPaths.readFile")(function* (filepath: string) { + return yield* afs.readFileStringSafe(filepath) + }) + + const parseText = Effect.fn("ConfigPaths.parseText")(function* (text: string, filepath: string) { + return ConfigParse.jsonc(text, filepath) + }) + + return Service.of({ projectFiles, directories, readFile, parseText }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer)) diff --git a/packages/opencode/src/plugin/install.ts b/packages/opencode/src/plugin/install.ts index f07b78bc36..2bef7b030c 100644 --- a/packages/opencode/src/plugin/install.ts +++ b/packages/opencode/src/plugin/install.ts @@ -7,7 +7,7 @@ import { printParseErrorCode, } from "jsonc-parser" -import * as ConfigPaths from "@/config/paths" +import { ConfigPaths } from "@/config/paths" import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util/filesystem" import { Flock } from "@opencode-ai/core/util/flock" diff --git a/packages/opencode/test/agent/plugin-agent-regression.test.ts b/packages/opencode/test/agent/plugin-agent-regression.test.ts index c437281cc6..ffa000e26c 100644 --- a/packages/opencode/test/agent/plugin-agent-regression.test.ts +++ b/packages/opencode/test/agent/plugin-agent-regression.test.ts @@ -6,6 +6,7 @@ import { pathToFileURL } from "url" import { Agent } from "../../src/agent/agent" import { Bus } from "../../src/bus" import { Config } from "../../src/config/config" +import { ConfigPaths } from "../../src/config/paths" import { Env } from "../../src/env" import { RuntimeFlags } from "../../src/effect/runtime-flags" import { Plugin } from "../../src/plugin" @@ -29,6 +30,7 @@ const configLayer = Config.layer.pipe( Layer.provide(AuthTest.empty), Layer.provide(AccountTest.empty), Layer.provide(NpmTest.noop), + Layer.provide(ConfigPaths.defaultLayer), ) const pluginLayer = Plugin.layer.pipe( Layer.provide(Bus.layer), diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 90e78efcdb..648937be23 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -3,6 +3,7 @@ import { Effect, Layer, Option } from "effect" import { NodeFileSystem, NodePath } from "@effect/platform-node" import { Config } from "@/config/config" import { ConfigManaged } from "@/config/managed" +import { ConfigPaths } from "@/config/paths" import { ConfigParse } from "../../src/config/parse" import { EffectFlock } from "@opencode-ai/core/util/effect-flock" @@ -57,6 +58,7 @@ const layer = Config.layer.pipe( Layer.provide(emptyAccount), Layer.provideMerge(infra), Layer.provide(noopNpm), + Layer.provide(ConfigPaths.defaultLayer), ) const it = testEffect(layer) @@ -548,6 +550,7 @@ test("resolves env templates in account config with account token", async () => Layer.provide(fakeAccount), Layer.provideMerge(infra), Layer.provide(noopNpm), + Layer.provide(ConfigPaths.defaultLayer), ) try { @@ -1059,6 +1062,7 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => { Layer.provide(emptyAccount), Layer.provideMerge(infra), Layer.provide(noopNpm), + Layer.provide(ConfigPaths.defaultLayer), ) try { @@ -1944,6 +1948,7 @@ test("project config overrides remote well-known config", async () => { Layer.provide(emptyAccount), Layer.provideMerge(infra), Layer.provide(noopNpm), + Layer.provide(ConfigPaths.defaultLayer), ) try { @@ -2002,6 +2007,7 @@ test("wellknown URL with trailing slash is normalized", async () => { Layer.provide(emptyAccount), Layer.provideMerge(infra), Layer.provide(noopNpm), + Layer.provide(ConfigPaths.defaultLayer), ) try { @@ -2074,6 +2080,7 @@ test("wellknown remote_config supports templated env vars in headers", async () Layer.provide(emptyAccount), Layer.provideMerge(infra), Layer.provide(noopNpm), + Layer.provide(ConfigPaths.defaultLayer), ) try {