Compare commits

...

1 Commits

Author SHA1 Message Date
Kit Langton fd45c7bb0f effect(config): extract ConfigPaths.Service from config/paths 2026-05-13 19:40:54 -04:00
7 changed files with 89 additions and 42 deletions
@@ -7,7 +7,7 @@ import { Flag } from "@opencode-ai/core/flag/flag"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util/filesystem"
import * as Log from "@opencode-ai/core/util/log" 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" }) const log = Log.create({ service: "tui.migrate" })
@@ -5,7 +5,7 @@ import { createBindingLookup } from "@opentui/keymap/extras"
import { mergeDeep, unique } from "remeda" import { mergeDeep, unique } from "remeda"
import { Cause, Context, Effect, Fiber, Layer, Schema } from "effect" import { Cause, Context, Effect, Fiber, Layer, Schema } from "effect"
import { ConfigParse } from "@/config/parse" import { ConfigParse } from "@/config/parse"
import * as ConfigPaths from "@/config/paths" import { ConfigPaths } from "@/config/paths"
import { migrateTuiConfig } from "./tui-migrate" import { migrateTuiConfig } from "./tui-migrate"
import { KeymapLeaderTimeoutDefault, resolveAttentionSoundPaths, TuiInfo } from "./tui-schema" import { KeymapLeaderTimeoutDefault, resolveAttentionSoundPaths, TuiInfo } from "./tui-schema"
import { Flag } from "@opencode-ai/core/flag/flag" 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 loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory: string }) {
const afs = yield* AppFileSystem.Service const afs = yield* AppFileSystem.Service
let appliedOrder = 0 let appliedOrder = 0
const paths = yield* ConfigPaths.Service
const resolvePlugins = (config: Info, configFilepath: string): Effect.Effect<Info> => const resolvePlugins = (config: Info, configFilepath: string): Effect.Effect<Info> =>
Effect.gen(function* () { 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` // Every config dir we may read from: global config dir, any `.opencode`
// folders between cwd and home, and OPENCODE_CONFIG_DIR. // 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 })) 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 = { const acc: Acc = {
result: {}, result: {},
@@ -295,7 +296,11 @@ export const layer = Layer.effect(
}).pipe(Effect.withSpan("TuiConfig.layer")), }).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) const { runPromise } = makeRuntime(Service, defaultLayer)
+4 -2
View File
@@ -370,6 +370,7 @@ export const layer = Layer.effect(
const accountSvc = yield* Account.Service const accountSvc = yield* Account.Service
const env = yield* Env.Service const env = yield* Env.Service
const npmSvc = yield* Npm.Service const npmSvc = yield* Npm.Service
const paths = yield* ConfigPaths.Service
const readConfigFile = (filepath: string) => fs.readFileStringSafe(filepath).pipe(Effect.orDie) 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) { 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") yield* merge(file, yield* loadFile(file), "local")
} }
} }
@@ -571,7 +572,7 @@ export const layer = Layer.effect(
result.mode = result.mode || {} result.mode = result.mode || {}
result.plugin = result.plugin || [] 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) { if (Flag.OPENCODE_CONFIG_DIR) {
log.debug("loading config from OPENCODE_CONFIG_DIR", { path: 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(Auth.defaultLayer),
Layer.provide(Account.defaultLayer), Layer.provide(Account.defaultLayer),
Layer.provide(Npm.defaultLayer), Layer.provide(Npm.defaultLayer),
Layer.provide(ConfigPaths.defaultLayer),
) )
export * as Config from "./config" export * as Config from "./config"
+65 -34
View File
@@ -4,42 +4,73 @@ import path from "path"
import { Flag } from "@opencode-ai/core/flag/flag" import { Flag } from "@opencode-ai/core/flag/flag"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { unique } from "remeda" import { unique } from "remeda"
import * as Effect from "effect/Effect" import { Context, Effect, Layer } from "effect"
import { AppFileSystem } from "@opencode-ai/core/filesystem" import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { ConfigParse } from "./parse"
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] : []),
])
})
export function fileInDirectory(dir: string, name: string) { export function fileInDirectory(dir: string, name: string) {
return [path.join(dir, `${name}.json`), path.join(dir, `${name}.jsonc`)] 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))
+1 -1
View File
@@ -7,7 +7,7 @@ import {
printParseErrorCode, printParseErrorCode,
} from "jsonc-parser" } from "jsonc-parser"
import * as ConfigPaths from "@/config/paths" import { ConfigPaths } from "@/config/paths"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util/filesystem"
import { Flock } from "@opencode-ai/core/util/flock" import { Flock } from "@opencode-ai/core/util/flock"
@@ -6,6 +6,7 @@ import { pathToFileURL } from "url"
import { Agent } from "../../src/agent/agent" import { Agent } from "../../src/agent/agent"
import { Bus } from "../../src/bus" import { Bus } from "../../src/bus"
import { Config } from "../../src/config/config" import { Config } from "../../src/config/config"
import { ConfigPaths } from "../../src/config/paths"
import { Env } from "../../src/env" import { Env } from "../../src/env"
import { RuntimeFlags } from "../../src/effect/runtime-flags" import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { Plugin } from "../../src/plugin" import { Plugin } from "../../src/plugin"
@@ -29,6 +30,7 @@ const configLayer = Config.layer.pipe(
Layer.provide(AuthTest.empty), Layer.provide(AuthTest.empty),
Layer.provide(AccountTest.empty), Layer.provide(AccountTest.empty),
Layer.provide(NpmTest.noop), Layer.provide(NpmTest.noop),
Layer.provide(ConfigPaths.defaultLayer),
) )
const pluginLayer = Plugin.layer.pipe( const pluginLayer = Plugin.layer.pipe(
Layer.provide(Bus.layer), Layer.provide(Bus.layer),
@@ -3,6 +3,7 @@ import { Effect, Layer, Option } from "effect"
import { NodeFileSystem, NodePath } from "@effect/platform-node" import { NodeFileSystem, NodePath } from "@effect/platform-node"
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { ConfigManaged } from "@/config/managed" import { ConfigManaged } from "@/config/managed"
import { ConfigPaths } from "@/config/paths"
import { ConfigParse } from "../../src/config/parse" import { ConfigParse } from "../../src/config/parse"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
@@ -57,6 +58,7 @@ const layer = Config.layer.pipe(
Layer.provide(emptyAccount), Layer.provide(emptyAccount),
Layer.provideMerge(infra), Layer.provideMerge(infra),
Layer.provide(noopNpm), Layer.provide(noopNpm),
Layer.provide(ConfigPaths.defaultLayer),
) )
const it = testEffect(layer) const it = testEffect(layer)
@@ -548,6 +550,7 @@ test("resolves env templates in account config with account token", async () =>
Layer.provide(fakeAccount), Layer.provide(fakeAccount),
Layer.provideMerge(infra), Layer.provideMerge(infra),
Layer.provide(noopNpm), Layer.provide(noopNpm),
Layer.provide(ConfigPaths.defaultLayer),
) )
try { try {
@@ -1059,6 +1062,7 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => {
Layer.provide(emptyAccount), Layer.provide(emptyAccount),
Layer.provideMerge(infra), Layer.provideMerge(infra),
Layer.provide(noopNpm), Layer.provide(noopNpm),
Layer.provide(ConfigPaths.defaultLayer),
) )
try { try {
@@ -1944,6 +1948,7 @@ test("project config overrides remote well-known config", async () => {
Layer.provide(emptyAccount), Layer.provide(emptyAccount),
Layer.provideMerge(infra), Layer.provideMerge(infra),
Layer.provide(noopNpm), Layer.provide(noopNpm),
Layer.provide(ConfigPaths.defaultLayer),
) )
try { try {
@@ -2002,6 +2007,7 @@ test("wellknown URL with trailing slash is normalized", async () => {
Layer.provide(emptyAccount), Layer.provide(emptyAccount),
Layer.provideMerge(infra), Layer.provideMerge(infra),
Layer.provide(noopNpm), Layer.provide(noopNpm),
Layer.provide(ConfigPaths.defaultLayer),
) )
try { try {
@@ -2074,6 +2080,7 @@ test("wellknown remote_config supports templated env vars in headers", async ()
Layer.provide(emptyAccount), Layer.provide(emptyAccount),
Layer.provideMerge(infra), Layer.provideMerge(infra),
Layer.provide(noopNpm), Layer.provide(noopNpm),
Layer.provide(ConfigPaths.defaultLayer),
) )
try { try {