core: migrate config loading to Effect framework (#23032)
This commit is contained in:
@@ -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"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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`)]
|
||||||
|
|||||||
Reference in New Issue
Block a user