effect(config): extract ConfigPaths.Service from config/paths

This commit is contained in:
Kit Langton
2026-05-12 16:24:31 -04:00
parent df3895d74f
commit fd45c7bb0f
7 changed files with 89 additions and 42 deletions

View File

@@ -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" })

View File

@@ -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<string, unknown>, 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<Info> =>
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)

View File

@@ -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"

View File

@@ -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<string[], AppFileSystem.Error>
readonly directories: (directory: string, worktree?: string) => Effect.Effect<string[], AppFileSystem.Error>
readonly readFile: (filepath: string) => Effect.Effect<string | undefined, AppFileSystem.Error>
readonly parseText: (text: string, filepath: string) => Effect.Effect<unknown>
}
export class Service extends Context.Service<Service, Interface>()("@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))

View File

@@ -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"

View File

@@ -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),

View File

@@ -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 {