Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375export * as Config from "./config.js"
import { makeLocationNode } from "@opencode/util/effect/app-node"import path from "path"import { isDeepStrictEqual } from "node:util"import { applyEdits, modify, type ParseError, parse } from "jsonc-parser"import { Context, Effect, FiberMap, Layer, Option, PubSub, Ref, Schema, Semaphore, Stream } from "effect"import { Directory, Document, Info, type Patch, type Entry, Event } from "@opencode/schema/config"import { Credential } from "./credential.js"import { Bus } from "./bus.js"import { Watcher } from "./filesystem/watcher.js"import { FSUtil } from "@opencode/util/fs-util"import { Global } from "@opencode/util/global"import { Location } from "./location.js"import { AbsolutePath } from "./schema.js"import { ConfigVariable } from "./config/variable.js"import { ConfigNormalize } from "./config/normalize.js"import { ConfigDiscovery } from "./config/discovery.js"import { ConfigWatch } from "./config/watch.js"import { WellKnown } from "./wellknown.js"
export function latest<K extends keyof Info>(entries: readonly Entry[], key: K): Info[K] | undefined { return entries.findLast((entry): entry is Document => entry.type === "document" && entry.info[key] !== undefined) ?.info[key]}
export interface Interface { /** Returns location config documents and discovery sources from lowest to highest priority. */ readonly entries: () => Effect.Effect<Entry[]> /** Compatibility roots consumed by internal compatibility plugins. */ readonly compatibility?: () => Effect.Effect<{ readonly claude: readonly AbsolutePath[] readonly agents: readonly AbsolutePath[] }> /** * Streams raw filesystem updates under config roots. Config owns root * topology and watch reconciliation; domain owners filter this feed for the * source files they parse and rebuild their own state. */ readonly changes: () => Stream.Stream<Watcher.Update> /** Updates supported global config fields while preserving unrelated JSONC content. */ readonly update?: (patch: Patch) => Effect.Effect<void, FSUtil.Error>}
export const Options = Schema.Struct({ project: Schema.optional(Schema.Boolean), // false skips the global config dir, ~/.claude, and ~/.agents; wellknown, // file, and content entries still load. global: Schema.optional(Schema.Boolean), file: Schema.optional(Schema.String), content: Schema.optional(Schema.String),})export type Options = typeof Options.Type
export class Service extends Context.Service<Service, Interface>()("@opencode/Config") {}
export interface TestInterface extends Interface { /** Replaces the entries returned by subsequent entries() calls. */ readonly setEntries: (entries: Entry[]) => Effect.Effect<void> /** Emits one filesystem update to every changes() subscriber. */ readonly emitChange: (update: Watcher.Update) => Effect.Effect<void>}
export class Test extends Context.Service<Test, TestInterface>()("@opencode/Config/Test") {}
/** In-memory config for tests: static entries with replaceable state and a test-driven change feed. */export const testLayer = ( initial: Entry[] = [], compatibility: { readonly claude: readonly AbsolutePath[]; readonly agents: readonly AbsolutePath[] } = { claude: [], agents: [], },) => Layer.effectContext( Effect.gen(function* () { const entries = yield* Ref.make(initial) const updates = yield* PubSub.unbounded<Watcher.Update>() const service = Test.of({ entries: () => Ref.get(entries), compatibility: () => Effect.succeed(compatibility), changes: () => Stream.fromPubSub(updates), setEntries: (next) => Ref.set(entries, next), emitChange: (update) => PubSub.publish(updates, update).pipe(Effect.asVoid), }) return Context.empty().pipe(Context.add(Service, service), Context.add(Test, service)) }), )
export const layer = (options?: Options) => Layer.effect( Service, Effect.gen(function* () { const fs = yield* FSUtil.Service const location = yield* Location.Service const watcher = yield* Watcher.Service const bus = yield* Bus.Service const credentials = yield* Credential.Service const globalService = yield* Global.Service const wellknown = yield* WellKnown.Service const reloadLock = Semaphore.makeUnsafe(1) const updateLock = Semaphore.makeUnsafe(1) const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const const decodeInfo = Schema.decodeUnknownOption(Info, decodeOptions) const parseInfo = Effect.fn("Config.parseInfo")(function* (text: string, source: string) { const errors: ParseError[] = [] const input: unknown = parse(text, errors, { allowTrailingComma: true }) if (errors.length) { yield* Effect.logWarning("configuration normalization diagnostic", { source, path: "$", kind: "invalid", action: "rejected malformed JSON or JSONC document", }) return } const result = ConfigNormalize.normalize(input) yield* Effect.forEach(result.diagnostics, (diagnostic) => Effect.logWarning("configuration normalization diagnostic", { source, path: diagnostic.path[0] === "$" ? "$" : `$.${diagnostic.path.join(".")}`, kind: diagnostic.kind, action: diagnostic.message, }), ) if (result.type === "rejected") return const info = Option.getOrUndefined(decodeInfo(result.encoded)) if (info) return info yield* Effect.logWarning("configuration normalization diagnostic", { source, path: "$", kind: "invalid", action: "rejected canonical configuration after final validation", }) })
const loadFile = Effect.fnUntraced(function* (filepath: string) { const text = yield* fs.readFileStringSafe(filepath) if (text === undefined) return const substituted = yield* ConfigVariable.substitute({ type: "path", path: filepath, text }) const info = yield* parseInfo(substituted, filepath) if (!info) return return new Document({ type: "document", path: AbsolutePath.make(filepath), info }) })
const loadWellknownEntry = Effect.fnUntraced(function* (entry: WellKnown.Entry) { const auth = entry.manifest.auth if (!auth) return [] const credential = (yield* credentials.list(entry.integrationID)).at(-1) if (!credential || credential.value.type !== "key") return [] const variables = { [auth.env]: credential.value.key } const configs = yield* wellknown .resolve(entry, variables) .pipe( Effect.catch(() => Effect.logWarning("failed to load wellknown config", { source: entry.origin }).pipe( Effect.as([] as const), ), ), ) return yield* Effect.forEach(configs, (config) => ConfigVariable.substitute({ type: "virtual", source: entry.origin, dir: entry.origin, text: JSON.stringify(config), env: variables, }).pipe( Effect.flatMap((text) => parseInfo(text, entry.origin)), Effect.map((info) => (info ? new Document({ type: "document", info }) : undefined)), ), ).pipe(Effect.map((documents) => documents.filter((document) => document !== undefined))) })
const loadWellknown = Effect.fn("Config.loadWellknown")(function* () { const entries = yield* wellknown .entries() .pipe( Effect.catch((error) => Effect.logWarning("failed to discover wellknown config", { error }).pipe(Effect.as([] as const)), ), ) return yield* Effect.forEach(entries, loadWellknownEntry).pipe(Effect.map((documents) => documents.flat())) })
const loadDirectory = Effect.fnUntraced(function* (directory: AbsolutePath) { return [ ...(yield* Effect.forEach(ConfigDiscovery.names, (file) => loadFile(path.join(directory, file))).pipe( Effect.map((configs) => configs.filter((config): config is Document => config !== undefined)), )), new Directory({ type: "directory", path: directory }), ] })
const load = Effect.fn("Config.load")(function* (sources: ConfigDiscovery.Sources) { const direct = yield* Effect.forEach(sources.direct, (filepath) => loadFile(filepath)).pipe( Effect.orDie, Effect.map((entries) => entries.filter((entry): entry is Document => entry !== undefined)), )
const explicit = sources.explicit ? yield* loadFile(sources.explicit).pipe( Effect.map((config) => (config ? [config] : [])), Effect.orDie, ) : [] const content = options?.content !== undefined ? yield* ConfigVariable.substitute({ type: "virtual", source: "OPENCODE_CONFIG_CONTENT", dir: location.directory, text: options.content, }).pipe( Effect.flatMap((text) => parseInfo(text, "OPENCODE_CONFIG_CONTENT")), Effect.map((info) => (info ? [new Document({ type: "document", info })] : [])), Effect.orDie, ) : []
// Global entries sit below explicit and direct files; project // directories rank above them. const globalSupplementary = sources.global ? yield* loadDirectory(sources.global).pipe(Effect.orDie) : [] const projectSupplementary = yield* Effect.forEach( sources.project.filter((root) => root.present), (root) => loadDirectory(root.path), ).pipe( Effect.orDie, Effect.map((entries) => entries.flat()), ) return [ ...(yield* loadWellknown().pipe(Effect.orDie)), ...globalSupplementary, ...explicit, ...direct, ...projectSupplementary, ...content, ] })
const initial = yield* ConfigDiscovery.discover(options) let sources = initial let configs = yield* load(initial) const updates = yield* PubSub.unbounded<Watcher.Update>() const reloads = yield* PubSub.sliding<void>(1) // Readiness rescans recover writes made before a watch attached. const requestReload = PubSub.publish(reloads, undefined).pipe(Effect.asVoid) const watched = yield* FiberMap.make<string>() const reconcile = Effect.fn("Config.reconcileWatches")(function* (sources: ConfigDiscovery.Sources) { const plan = ConfigWatch.plan(sources) for (const key of Array.from(watched, ([key]) => key)) { if (!plan.has(key)) yield* FiberMap.remove(watched, key) } for (const [key, target] of plan) { yield* watcher .subscribe(target, requestReload) .pipe( Effect.flatMap( Stream.runForEach((update) => PubSub.publish(updates, update).pipe(Effect.andThen(requestReload))), ), FiberMap.run(watched, key, { onlyIfMissing: true, startImmediately: true }), ) } })
const reload = Effect.fn("Config.reload")( function* () { const discovered = yield* ConfigDiscovery.discover(options) const next = yield* load(discovered) yield* reconcile(discovered) const compatibilityChanged = !isDeepStrictEqual(sources.claude, discovered.claude) || !isDeepStrictEqual(sources.agents, discovered.agents) if (isDeepStrictEqual(configs, next) && !compatibilityChanged) return sources = discovered configs = next yield* bus.publish(Event.Updated, {}) }, (effect) => reloadLock.withPermit(effect), )
// Subscribe eagerly so synchronous watch readiness isn't dropped. const pendingReloads = yield* PubSub.subscribe(reloads) yield* Stream.fromSubscription(pendingReloads).pipe( Stream.debounce("100 millis"), Stream.runForEach(() => reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload config", { cause }))), ), Effect.forkScoped({ startImmediately: true }), ) yield* bus.subscribe(Credential.Event.Switched).pipe( Stream.filterEffect((event) => wellknown.entries().pipe( Effect.map((entries) => entries.some((entry) => entry.integrationID === event.data.integrationID)), Effect.orElseSucceed(() => false), ), ), Stream.runForEach(() => reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown config", { cause }))), ), Effect.forkScoped({ startImmediately: true }), ) yield* bus.subscribe(WellKnown.Event.Updated).pipe( Stream.runForEach(() => reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown sources", { cause }))), ), Effect.forkScoped({ startImmediately: true }), ) yield* Effect.sleep("10 minutes").pipe( Effect.andThen( Effect.suspend(() => { if (!wellknown.snapshot().length) return Effect.void return Effect.gen(function* () { const changed = yield* wellknown .refresh() .pipe( Effect.catch((error) => Effect.logWarning("failed to refresh wellknown manifests", { error }).pipe(Effect.as(false)), ), ) if (!changed) yield* reload() }).pipe(Effect.catchCause((cause) => Effect.logWarning("failed to refresh wellknown config", { cause }))) }), ), Effect.forever, Effect.forkScoped({ startImmediately: true }), ) yield* reloadLock.withPermit(reconcile(initial))
const update = Effect.fn("Config.update")( function* (patch: Patch) { const directory = initial.global ?? AbsolutePath.make(globalService.config) const candidates = ConfigDiscovery.names.map((name) => path.join(directory, name)) const filepath = (yield* Effect.filter(candidates, fs.isFile)).at(-1) ?? path.join(directory, "opencode.jsonc") const text = (yield* fs.readFileStringSafe(filepath)) ?? "{}\n" const updated = yield* Effect.try({ try: () => applyEdits( text, modify(text, ["shell"], patch.shell ?? undefined, { formattingOptions: { tabSize: 2, insertSpaces: true }, }), ), catch: (cause) => new FSUtil.FileSystemError({ method: "config.update", cause }), }) yield* fs.writeWithDirs(filepath, updated.endsWith("\n") ? updated : `${updated}\n`) yield* requestReload }, (effect) => updateLock.withPermit(effect), )
return Service.of({ entries: Effect.fnUntraced(function* () { return configs }), compatibility: () => Effect.all({ claude: Effect.filter(sources.claude, fs.isDir), agents: Effect.filter(sources.agents, fs.isDir), }), changes: () => Stream.fromPubSub(updates), update, }) }), )
export function configured(options?: Options) { return makeLocationNode({ service: Service, layer: layer(options), deps: [Watcher.node, Bus.node, FSUtil.node, Global.node, Location.node, Credential.node, WellKnown.node], })}
export const node = configured()