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

This commit is contained in:
Dax
2026-04-17 06:44:01 +00:00
committed by GitHub
parent 81f0885879
commit d9950598d0
5 changed files with 275 additions and 263 deletions
@@ -135,7 +135,9 @@ export function tui(input: {
await TuiPluginRuntime.dispose() await TuiPluginRuntime.dispose()
} }
console.log("starting renderer")
const renderer = await createCliRenderer(rendererConfig(input.config)) const renderer = await createCliRenderer(rendererConfig(input.config))
console.log("renderer started")
await render(() => { await render(() => {
return ( return (
@@ -132,8 +132,10 @@ async function backupAndStripLegacy(file: string, source: string) {
} }
async function opencodeFiles(input: { directories: string[]; cwd: string }) { async function opencodeFiles(input: { directories: string[]; cwd: string }) {
const project = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("opencode", input.cwd) const files = [
const files = [...project, ...ConfigPaths.fileInDirectory(Global.Path.config, "opencode")] ...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)) { for (const dir of unique(input.directories)) {
files.push(...ConfigPaths.fileInDirectory(dir, "opencode")) files.push(...ConfigPaths.fileInDirectory(dir, "opencode"))
} }
+13 -14
View File
@@ -89,15 +89,13 @@ async function mergeFile(acc: Acc, file: string, ctx: { directory: string }) {
acc.result.plugin_origins = plugins 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` // 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 = await ConfigPaths.directories(ctx.directory) const directories = yield* ConfigPaths.directories(ctx.directory)
// One-time migration: extract tui keys (theme/keybinds/tui) from existing yield* Effect.promise(() => migrateTuiConfig({ directories, cwd: ctx.directory }))
// opencode.json files into sibling tui.json files.
await 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 = { const acc: Acc = {
result: {}, result: {},
@@ -105,18 +103,19 @@ async function loadState(ctx: { directory: string }) {
// 1. Global tui config (lowest precedence). // 1. Global tui config (lowest precedence).
for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) { 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. // 2. Explicit OPENCODE_TUI_CONFIG override, if set.
if (Flag.OPENCODE_TUI_CONFIG) { if (Flag.OPENCODE_TUI_CONFIG) {
await mergeFile(acc, Flag.OPENCODE_TUI_CONFIG, ctx) const configFile = Flag.OPENCODE_TUI_CONFIG
log.debug("loaded custom tui config", { path: 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. // 3. Project tui files, applied root-first so the closest file wins.
for (const file of projectFiles) { 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 // 4. `.opencode` directories (and OPENCODE_CONFIG_DIR) discovered while
@@ -127,7 +126,7 @@ async function loadState(ctx: { directory: string }) {
for (const dir of dirs) { for (const dir of dirs) {
if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue
for (const file of ConfigPaths.fileInDirectory(dir, "tui")) { 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, config: acc.result,
dirs: acc.result.plugin?.length ? dirs : [], dirs: acc.result.plugin?.length ? dirs : [],
} }
} })
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const directory = yield* CurrentWorkingDirectory const directory = yield* CurrentWorkingDirectory
const npm = yield* Npm.Service const npm = yield* Npm.Service
const data = yield* Effect.promise(() => loadState({ directory })) const data = yield* loadState({ directory })
const deps = yield* Effect.forEach( const deps = yield* Effect.forEach(
data.dirs, data.dirs,
(dir) => (dir) =>
@@ -176,7 +175,7 @@ export const layer = Layer.effect(
}).pipe(Effect.withSpan("TuiConfig.layer")), }).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) const { runPromise } = makeRuntime(Service, defaultLayer)
+8 -7
View File
@@ -413,7 +413,8 @@ export const layer = Layer.effect(
} }
}) })
const loadInstanceState = Effect.fn("Config.loadInstanceState")(function* (ctx: InstanceContext) { const loadInstanceState = Effect.fn("Config.loadInstanceState")(
function* (ctx: InstanceContext) {
const auth = yield* authSvc.all().pipe(Effect.orDie) const auth = yield* authSvc.all().pipe(Effect.orDie)
let result: Info = {} let result: Info = {}
@@ -484,9 +485,7 @@ export const layer = Layer.effect(
} }
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) { if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
for (const file of yield* Effect.promise(() => for (const file of yield* ConfigPaths.files("opencode", ctx.directory, ctx.worktree).pipe(Effect.orDie)) {
ConfigPaths.projectFiles("opencode", ctx.directory, ctx.worktree),
)) {
yield* merge(file, yield* loadFile(file), "local") yield* merge(file, yield* loadFile(file), "local")
} }
} }
@@ -495,7 +494,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* Effect.promise(() => ConfigPaths.directories(ctx.directory, ctx.worktree)) const directories = yield* ConfigPaths.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 })
@@ -662,11 +661,13 @@ export const layer = Layer.effect(
switchableOrgCount: 0, switchableOrgCount: 0,
}, },
} }
}) },
Effect.provideService(AppFileSystem.Service, fs),
)
const state = yield* InstanceState.make<State>( const state = yield* InstanceState.make<State>(
Effect.fn("Config.state")(function* (ctx) { Effect.fn("Config.state")(function* (ctx) {
return yield* loadInstanceState(ctx) return yield* loadInstanceState(ctx).pipe(Effect.orDie)
}), }),
) )
+21 -13
View File
@@ -6,33 +6,41 @@ import { Flag } from "@/flag/flag"
import { Global } from "@/global" import { Global } from "@/global"
import { unique } from "remeda" import { unique } from "remeda"
import { JsonError } from "./error" 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) { export const files = Effect.fn("ConfigPaths.projectFiles")(function* (
return Filesystem.findUp([`${name}.json`, `${name}.jsonc`], directory, worktree, { rootFirst: true }) 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([ return unique([
Global.Path.config, Global.Path.config,
...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG ...(!Flag.OPENCODE_DISABLE_PROJECT_CONFIG
? await Array.fromAsync( ? yield* afs.up({
Filesystem.up({
targets: [".opencode"], targets: [".opencode"],
start: directory, start: directory,
stop: worktree, stop: worktree,
}), })
)
: []), : []),
...(await Array.fromAsync( ...(yield* afs.up({
Filesystem.up({
targets: [".opencode"], targets: [".opencode"],
start: Global.Path.home, start: Global.Path.home,
stop: Global.Path.home, stop: Global.Path.home,
}), })),
)),
...(Flag.OPENCODE_CONFIG_DIR ? [Flag.OPENCODE_CONFIG_DIR] : []), ...(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`)]