diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index b6a8f98af9..58ddbbe940 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -137,8 +137,7 @@ export const rpc = { }) }, async reload() { - await Config.global.reset() - await Instance.disposeAll() + await Config.invalidate(true) }, async setWorkspace(input: { workspaceID?: string }) { startEventStream({ directory: process.cwd(), workspaceID: input.workspaceID }) diff --git a/packages/opencode/src/cli/network.ts b/packages/opencode/src/cli/network.ts index dd09e1689f..84268e2675 100644 --- a/packages/opencode/src/cli/network.ts +++ b/packages/opencode/src/cli/network.ts @@ -37,7 +37,7 @@ export function withNetworkOptions(yargs: Argv) { } export async function resolveNetworkOptions(args: NetworkOptions) { - const config = await Config.global() + const config = await Config.getGlobal() const portExplicitlySet = process.argv.includes("--port") const hostnameExplicitlySet = process.argv.includes("--hostname") const mdnsExplicitlySet = process.argv.includes("--mdns") diff --git a/packages/opencode/src/cli/upgrade.ts b/packages/opencode/src/cli/upgrade.ts index e40750a2ec..7b7199d4ea 100644 --- a/packages/opencode/src/cli/upgrade.ts +++ b/packages/opencode/src/cli/upgrade.ts @@ -4,7 +4,7 @@ import { Flag } from "@/flag/flag" import { Installation } from "@/installation" export async function upgrade() { - const config = await Config.global() + const config = await Config.getGlobal() const method = await Installation.method() const latest = await Installation.latest(method).catch(() => {}) if (!latest) return diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index f5a1295db6..eef215ee52 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -8,6 +8,7 @@ import { ModelsDev } from "../provider/models" import { mergeDeep, pipe, unique } from "remeda" import { Global } from "../global" import fs from "fs/promises" +import { lazy } from "../util/lazy" import { NamedError } from "@opencode-ai/util/error" import { Flag } from "../flag/flag" import { Auth } from "../auth" @@ -77,7 +78,8 @@ export namespace Config { return merged } - async function loadState(ctx: Shape, glb: Info) { + async function loadState(ctx: Shape) { + const glb = global() const auth = await Auth.all() // Config loading order (low -> high precedence): https://opencode.ai/docs/config#precedence-order @@ -114,7 +116,7 @@ export namespace Config { } // Global user config overrides remote config. - result = mergeConfigConcatArrays(result, glb) + result = mergeConfigConcatArrays(result, await glb) // Custom config path overrides global config. if (Flag.OPENCODE_CONFIG) { @@ -1239,17 +1241,14 @@ export namespace Config { export interface Interface { readonly get: () => Effect.Effect - readonly getGlobal: () => Effect.Effect - readonly resetGlobal: () => Effect.Effect readonly update: (config: Info) => Effect.Effect - readonly updateGlobal: (config: Info) => Effect.Effect readonly directories: () => Effect.Effect readonly waitForDependencies: () => Effect.Effect } export class Service extends ServiceMap.Service()("@opencode/Config") {} - async function loadGlobal() { + const global = lazy(async () => { let result: Info = pipe( {}, mergeDeep(await loadFile(path.join(Global.Path.config, "config.json"))), @@ -1276,35 +1275,14 @@ export namespace Config { } return result - } + }) export const layer = Layer.effect( Service, Effect.gen(function* () { - const glb: { task?: Promise } = {} - - const getGlobal = Effect.fn("Config.getGlobal")(() => - Effect.promise(async () => { - if (glb.task) return glb.task - const task = loadGlobal().catch((err) => { - if (glb.task === task) glb.task = undefined - throw err - }) - glb.task = task - return task - }), - ) - - const resetGlobal = Effect.fn("Config.resetGlobal")(() => - Effect.sync(() => { - glb.task = undefined - }), - ) - const state = yield* InstanceState.make( Effect.fn("Config.state")(function* (ctx) { - const glb = yield* getGlobal() - return yield* Effect.promise(() => loadState(ctx, glb)) + return yield* Effect.promise(() => loadState(ctx)) }), ) @@ -1329,48 +1307,9 @@ export namespace Config { yield* Effect.promise(() => Instance.dispose()) }) - const updateGlobal = Effect.fn("Config.updateGlobal")(function* (config: Info) { - const file = globalConfigFile() - const before = (yield* Effect.promise(() => readFile(file))) ?? "{}" - - const next = yield* Effect.promise(async () => { - if (!file.endsWith(".jsonc")) { - const existing = parseConfig(before, file) - const merged = mergeDeep(existing, config) - await Filesystem.writeJson(file, merged) - return merged - } - - const updated = patchJsonc(before, config) - const merged = parseConfig(updated, file) - await Filesystem.write(file, updated) - return merged - }) - - yield* resetGlobal() - yield* Effect.sync(() => { - void Instance.disposeAll() - .catch(() => undefined) - .finally(() => { - GlobalBus.emit("event", { - directory: "global", - payload: { - type: Event.Disposed.type, - properties: {}, - }, - }) - }) - }) - - return next - }) - return Service.of({ get, - getGlobal, - resetGlobal, update, - updateGlobal, directories, waitForDependencies, }) @@ -1460,11 +1399,11 @@ export namespace Config { } export async function getGlobal() { - return runPromise((svc) => svc.getGlobal()) + return global() } - export async function resetGlobal() { - return runPromise((svc) => svc.resetGlobal()) + export function resetGlobal() { + global.reset() } export async function update(config: Info) { @@ -1536,8 +1475,45 @@ export namespace Config { }) } + function disposed() { + GlobalBus.emit("event", { + directory: "global", + payload: { + type: Event.Disposed.type, + properties: {}, + }, + }) + } + + export async function invalidate(wait = false) { + resetGlobal() + const task = Instance.disposeAll() + .catch(() => undefined) + .finally(disposed) + if (wait) return task + void task + } + export async function updateGlobal(config: Info) { - return runPromise((svc) => svc.updateGlobal(config)) + const file = globalConfigFile() + const before = (await readFile(file)) ?? "{}" + + const next = await (async () => { + if (!file.endsWith(".jsonc")) { + const existing = parseConfig(before, file) + const merged = mergeDeep(existing, config) + await Filesystem.writeJson(file, merged) + return merged + } + + const updated = patchJsonc(before, config) + const merged = parseConfig(updated, file) + await Filesystem.write(file, updated) + return merged + })() + + void invalidate() + return next } export async function directories() { @@ -1547,8 +1523,4 @@ export namespace Config { export async function waitForDependencies() { return runPromise((svc) => svc.waitForDependencies()) } - - export const global = Object.assign(async () => getGlobal(), { - reset: resetGlobal, - }) } diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 76786c54a3..a1fd6ccb71 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -34,7 +34,7 @@ async function check(map: (dir: string) => string) { await using tmp = await tmpdir({ git: true, config: { snapshot: true } }) const prev = Global.Path.config ;(Global.Path as { config: string }).config = globalTmp.path - Config.global.reset() + Config.resetGlobal() try { await writeConfig(globalTmp.path, { $schema: "https://opencode.ai/config.json", @@ -52,7 +52,7 @@ async function check(map: (dir: string) => string) { } finally { await Instance.disposeAll() ;(Global.Path as { config: string }).config = prev - Config.global.reset() + Config.resetGlobal() } }