refactor(core): replace legacy logger with Effect logging (#31310)

This commit is contained in:
Dax
2026-06-08 15:41:56 -04:00
committed by GitHub
parent 0a7cb20e66
commit c06ad7c881
152 changed files with 698 additions and 2243 deletions
+2 -11
View File
@@ -2,15 +2,12 @@ export * as ConfigAgent from "./agent"
import path from "path"
import { Exit, Schema } from "effect"
import * as Log from "@opencode-ai/core/util/log"
import { Glob } from "@opencode-ai/core/util/glob"
import { ConfigAgentV1 } from "@opencode-ai/core/v1/config/agent"
import { configEntryNameFromPath } from "./entry-name"
import * as ConfigMarkdown from "./markdown"
import { ConfigParse } from "./parse"
const log = Log.create({ service: "config" })
export async function load(dir: string) {
const result: Record<string, ConfigAgentV1.Info> = {}
for (const item of await Glob.scan("{agent,agents}/**/*.md", {
@@ -19,10 +16,7 @@ export async function load(dir: string) {
dot: true,
symlink: true,
})) {
const md = await ConfigMarkdown.parse(item).catch((err) => {
log.error("failed to load agent", { agent: item, err })
return undefined
})
const md = await ConfigMarkdown.parse(item).catch(() => undefined)
if (!md) continue
const name = configEntryNameFromPath(path.relative(dir, item), ["agent/", "agents/"])
@@ -45,10 +39,7 @@ export async function loadMode(dir: string) {
dot: true,
symlink: true,
})) {
const md = await ConfigMarkdown.parse(item).catch((err) => {
log.error("failed to load mode", { mode: item, err })
return undefined
})
const md = await ConfigMarkdown.parse(item).catch(() => undefined)
if (!md) continue
const config = {
+1 -7
View File
@@ -1,7 +1,6 @@
export * as ConfigCommand from "./command"
import path from "path"
import * as Log from "@opencode-ai/core/util/log"
import { Cause, Exit, Schema } from "effect"
import { Glob } from "@opencode-ai/core/util/glob"
import { ConfigCommandV1 } from "@opencode-ai/core/v1/config/command"
@@ -9,8 +8,6 @@ import { configEntryNameFromPath } from "./entry-name"
import { InvalidError } from "@opencode-ai/core/v1/config/error"
import * as ConfigMarkdown from "./markdown"
const log = Log.create({ service: "config" })
const decodeInfo = Schema.decodeUnknownExit(ConfigCommandV1.Info)
export async function load(dir: string) {
@@ -21,10 +18,7 @@ export async function load(dir: string) {
dot: true,
symlink: true,
})) {
const md = await ConfigMarkdown.parse(item).catch((err) => {
log.error("failed to load command", { command: item, err })
return undefined
})
const md = await ConfigMarkdown.parse(item).catch(() => undefined)
if (!md) continue
const name = configEntryNameFromPath(path.relative(dir, item), ["command/", "commands/"])
+18 -25
View File
@@ -1,4 +1,3 @@
import * as Log from "@opencode-ai/core/util/log"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import path from "path"
import { pathToFileURL } from "url"
@@ -34,8 +33,6 @@ import { ConfigVariable } from "./variable"
import { Npm } from "@opencode-ai/core/npm"
import { withTransientReadRetry } from "@/util/effect-http-client"
const log = Log.create({ service: "config" })
// Custom merge function that concatenates array fields instead of replacing them
// Keep remeda's deep conditional merge type out of hot config-loading paths; TS profiling showed it dominates here.
function mergeConfig(target: Info, source: Info): Info {
@@ -50,7 +47,7 @@ function mergeConfigConcatArrays(target: Info, source: Info): Info {
return merged
}
function normalizeLoadedConfig(data: unknown, source: string) {
function normalizeLoadedConfig(data: unknown) {
if (!isRecord(data)) return data
const copy = { ...data }
const hadLegacy = "theme" in copy || "keybinds" in copy || "tui" in copy
@@ -58,7 +55,6 @@ function normalizeLoadedConfig(data: unknown, source: string) {
delete copy.theme
delete copy.keybinds
delete copy.tui
log.warn("tui keys in opencode config are deprecated; move them to tui.json", { path: source })
return copy
}
@@ -216,7 +212,7 @@ export const layer = Layer.effect(
),
)
const parsed = ConfigParse.jsonc(expanded, source)
const data = ConfigParse.schema(ConfigV1.Info, normalizeLoadedConfig(parsed, source), source)
const data = ConfigParse.schema(ConfigV1.Info, normalizeLoadedConfig(parsed), source)
if (!("path" in options)) return data
yield* Effect.promise(() => resolveLoadedPlugins(data, options.path))
@@ -229,7 +225,7 @@ export const layer = Layer.effect(
})
const loadFile = Effect.fnUntraced(function* (filepath: string, env?: Record<string, string>) {
log.info("loading", { path: filepath })
yield* Effect.logInfo("loading", { path: filepath })
const text = yield* readConfigFile(filepath)
if (!text) return {} as Info
return yield* loadConfig(text, { path: filepath }, env)
@@ -273,7 +269,7 @@ export const layer = Layer.effect(
const [cachedGlobal, invalidateGlobal] = yield* Effect.cachedInvalidateWithTTL(
loadGlobal().pipe(
Effect.tapError((error) =>
Effect.sync(() => log.error("failed to load global config, using defaults", { error: String(error) })),
Effect.logError("failed to load global config, using defaults", { error: String(error) }),
),
Effect.orElseSucceed((): Info => ({})),
),
@@ -349,7 +345,7 @@ export const layer = Layer.effect(
const url = key.replace(/\/+$/, "")
authEnv[value.key] = value.token
const wellknownURL = `${url}/.well-known/opencode`
log.debug("fetching remote config", { url: wellknownURL })
yield* Effect.logDebug("fetching remote config", { url: wellknownURL })
const wellknown = yield* fetchRemoteJson(wellknownURL, undefined, ConfigV1.WellKnown)
const remote = yield* Effect.promise(() =>
substituteWellKnownRemoteConfig({
@@ -361,7 +357,7 @@ export const layer = Layer.effect(
)
const fetchedConfig = remote
? yield* Effect.gen(function* () {
log.debug("fetching remote config", { url: remote.url })
yield* Effect.logDebug("fetching remote config", { url: remote.url })
const data = yield* fetchRemoteJson(remote.url, remote.headers, Schema.Json)
if (isRecord(data) && isRecord(data.config)) return data.config
if (isRecord(data)) return data
@@ -382,7 +378,7 @@ export const layer = Layer.effect(
authEnv,
)
yield* merge(source, next, "global")
log.debug("loaded remote config from well-known", { url })
yield* Effect.logDebug("loaded remote config from well-known", { url })
}
}
@@ -391,7 +387,7 @@ export const layer = Layer.effect(
if (Flag.OPENCODE_CONFIG) {
yield* merge(Flag.OPENCODE_CONFIG, yield* loadFile(Flag.OPENCODE_CONFIG, authEnv))
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
yield* Effect.logDebug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
}
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
@@ -407,7 +403,7 @@ export const layer = Layer.effect(
const directories = yield* ConfigPaths.directories(ctx.directory, ctx.worktree)
if (Flag.OPENCODE_CONFIG_DIR) {
log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
yield* Effect.logDebug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
}
const deps: Fiber.Fiber<void>[] = []
@@ -416,7 +412,7 @@ export const layer = Layer.effect(
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
for (const file of ["opencode.json", "opencode.jsonc"]) {
const source = path.join(dir, file)
log.debug(`loading config from ${source}`)
yield* Effect.logDebug(`loading config from ${source}`)
yield* merge(source, yield* loadFile(source, authEnv))
result.agent ??= {}
result.mode ??= {}
@@ -439,9 +435,7 @@ export const layer = Layer.effect(
Effect.exit,
Effect.tap((exit) =>
Exit.isFailure(exit)
? Effect.sync(() => {
log.warn("background dependency install failed", { dir, error: String(exit.cause) })
})
? Effect.logWarning("background dependency install failed", { dir, error: String(exit.cause) })
: Effect.void,
),
Effect.asVoid,
@@ -465,7 +459,7 @@ export const layer = Layer.effect(
source,
})
yield* merge(source, next, "local")
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
yield* Effect.logDebug("loaded custom config from OPENCODE_CONFIG_CONTENT")
}
const activeAccount = Option.getOrUndefined(
@@ -498,12 +492,11 @@ export const layer = Layer.effect(
}
}).pipe(
Effect.withSpan("Config.loadActiveOrgConfig"),
Effect.catch((err) => {
log.debug("failed to fetch remote account config", {
Effect.catch((err) =>
Effect.logDebug("failed to fetch remote account config", {
error: err instanceof Error ? err.message : String(err),
})
return Effect.void
}),
}),
),
)
}
@@ -540,7 +533,7 @@ export const layer = Layer.effect(
try {
result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION))
} catch (err) {
log.warn("OPENCODE_PERMISSION contains invalid JSON, skipping", { err })
yield* Effect.logWarning("OPENCODE_PERMISSION contains invalid JSON, skipping", { err })
}
}
@@ -561,7 +554,7 @@ export const layer = Layer.effect(
try {
result.username = os.userInfo().username || "user"
} catch (err) {
log.warn("failed to read system username, using fallback", { err })
yield* Effect.logWarning("failed to read system username, using fallback", { err })
result.username = "user"
}
}
+2 -10
View File
@@ -3,11 +3,8 @@ export * as ConfigManaged from "./managed"
import { existsSync } from "fs"
import os from "os"
import path from "path"
import * as Log from "@opencode-ai/core/util/log"
import { Process } from "@/util/process"
const log = Log.create({ service: "config" })
const MANAGED_PLIST_DOMAIN = "ai.opencode.managed"
// Keys injected by macOS/MDM into the managed plist that are not OpenCode config
@@ -49,8 +46,7 @@ export async function readManagedPreferences() {
const user = (() => {
try {
return os.userInfo().username || "user"
} catch (err) {
log.warn("failed to read system username, using fallback", { err })
} catch {
return "user"
}
})()
@@ -61,12 +57,8 @@ export async function readManagedPreferences() {
for (const plist of paths) {
if (!existsSync(plist)) continue
log.info("reading macOS managed preferences", { path: plist })
const result = await Process.run(["plutil", "-convert", "json", "-o", "-", plist], { nothrow: true })
if (result.code !== 0) {
log.warn("failed to convert managed preferences plist", { path: plist })
continue
}
if (result.code !== 0) continue
return {
source: `mobileconfig:${plist}`,
text: parseManagedPlist(result.stdout.toString()),
+6 -28
View File
@@ -6,11 +6,8 @@ import { TuiConfig } from "@opencode-ai/tui/config"
import { Flag } from "@opencode-ai/core/flag/flag"
import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util/filesystem"
import * as Log from "@opencode-ai/core/util/log"
import * as ConfigPaths from "@/config/paths"
const log = Log.create({ service: "tui.migrate" })
const TUI_SCHEMA_URL = "https://opencode.ai/tui.json"
const decodeTheme = Schema.decodeUnknownOption(Schema.String)
@@ -32,10 +29,7 @@ interface MigrateInput {
export async function migrateTuiConfig(input: MigrateInput) {
const opencode = await opencodeFiles(input)
for (const file of opencode) {
const source = await Filesystem.readText(file).catch((error) => {
log.warn("failed to read config for tui migration", { path: file, error })
return undefined
})
const source = await Filesystem.readText(file).catch(() => undefined)
if (!source) continue
const errors: JsoncParseError[] = []
const data = parseJsonc(source, errors, { allowTrailingComma: true })
@@ -65,18 +59,11 @@ export async function migrateTuiConfig(input: MigrateInput) {
const wrote = await Filesystem.write(target, JSON.stringify(payload, null, 2))
.then(() => true)
.catch((error) => {
log.warn("failed to write tui migration target", { from: file, to: target, error })
return false
})
.catch(() => false)
if (!wrote) continue
const stripped = await backupAndStripLegacy(file, source)
if (!stripped) {
log.warn("tui config migrated but source file was not stripped", { from: file, to: target })
continue
}
log.info("migrated tui config", { from: file, to: target })
if (!stripped) continue
}
}
@@ -106,10 +93,7 @@ async function backupAndStripLegacy(file: string, source: string) {
? true
: await Filesystem.write(backup, source)
.then(() => true)
.catch((error) => {
log.warn("failed to backup source config during tui migration", { path: file, backup, error })
return false
})
.catch(() => false)
if (!backed) return false
const text = ["theme", "keybinds", "tui"].reduce((acc, key) => {
@@ -124,14 +108,8 @@ async function backupAndStripLegacy(file: string, source: string) {
}, source)
return Filesystem.write(file, text)
.then(() => {
log.info("stripped tui keys from server config", { path: file, backup })
return true
})
.catch((error) => {
log.warn("failed to strip legacy tui keys from server config", { path: file, backup, error })
return false
})
.then(() => true)
.catch(() => false)
}
async function opencodeFiles(input: { directories: string[]; cwd: string }) {
+13 -31
View File
@@ -17,14 +17,11 @@ import { TuiKeybind } from "@opencode-ai/tui/config/keybind"
import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version"
import { makeRuntime } from "@opencode-ai/core/effect/runtime"
import { Filesystem } from "@/util/filesystem"
import * as Log from "@opencode-ai/core/util/log"
import { ConfigVariable } from "@/config/variable"
import { Npm } from "@opencode-ai/core/npm"
import { FormatError, FormatUnknownError } from "@/cli/error"
import { TuiConfig } from "@opencode-ai/tui/config"
const log = Log.create({ service: "tui.config" })
export const Info = TuiConfig.Info
export type Info = TuiConfig.Info
@@ -69,17 +66,12 @@ function normalize(raw: Record<string, unknown>) {
}
}
function dropUnknownKeybinds(input: Record<string, unknown>, configFilepath: string) {
function dropUnknownKeybinds(input: Record<string, unknown>) {
if (!isRecord(input.keybinds)) return input
const invalid = TuiKeybind.unknownKeys(input.keybinds)
if (!invalid.length) return input
log.warn("ignored unknown tui keybinds", {
path: configFilepath,
keybinds: invalid,
hint: "Remove these entries or rename them to keys from the tui.json schema.",
})
return {
...input,
keybinds: Object.fromEntries(Object.entries(input.keybinds).filter(([key]) => !invalid.includes(key))),
@@ -111,7 +103,7 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
if (!isRecord(data)) return {} as Info
// Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json
// (mirroring the old opencode.json shape) still get their settings applied.
const normalized = dropUnknownKeybinds(normalize(data), configFilepath)
const normalized = dropUnknownKeybinds(normalize(data))
const parsed = ConfigParse.schema(Info, normalized, configFilepath)
const validated = parsed.attention?.sounds
? {
@@ -127,15 +119,10 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
// catchCause (not tapErrorCause + orElseSucceed) because JSONC parsing and validation
// can sync-throw — those become defects, which orElseSucceed wouldn't catch.
Effect.catchCause((cause) =>
Effect.sync(() => {
const error = Cause.squash(cause)
const reason = FormatError(error) ?? FormatUnknownError(error)
log.warn("skipping invalid tui config", {
path: configFilepath,
reason,
})
return {} as Info
}),
Effect.logWarning("skipping invalid tui config", {
path: configFilepath,
reason: FormatError(Cause.squash(cause)) ?? FormatUnknownError(Cause.squash(cause)),
}).pipe(Effect.as({} as Info)),
),
)
@@ -146,19 +133,14 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
// broken-config path degrades gracefully rather than crashing TUI startup.
const text = yield* afs.readFileStringSafe(filepath).pipe(
Effect.catchCause((cause) =>
Effect.sync(() => {
const error = Cause.squash(cause)
const reason = FormatError(error) ?? FormatUnknownError(error)
log.warn("failed to read tui config", {
path: filepath,
reason,
})
return undefined
}),
Effect.logWarning("failed to read tui config", {
path: filepath,
reason: FormatError(Cause.squash(cause)) ?? FormatUnknownError(Cause.squash(cause)),
}).pipe(Effect.as(undefined)),
),
)
if (!text) return {} as Info
log.info("loading tui config", { path: filepath })
yield* Effect.logInfo("loading tui config", { path: filepath })
return yield* load(text, filepath)
})
@@ -167,7 +149,7 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
const data = yield* loadFile(file)
if (Object.keys(data).length) {
appliedOrder += 1
log.info("applying tui config", { path: file, order: appliedOrder })
yield* Effect.logInfo("applying tui config", { path: file, order: appliedOrder })
}
acc.result = mergeDeep(acc.result, data)
if (!data.plugin?.length) return
@@ -205,7 +187,7 @@ const loadState = Effect.fn("TuiConfig.loadState")(function* (ctx: { directory:
if (Flag.OPENCODE_TUI_CONFIG) {
const configFile = Flag.OPENCODE_TUI_CONFIG
yield* mergeFile(acc, configFile)
log.debug("loaded custom tui config", { path: configFile })
yield* Effect.logDebug("loaded custom tui config", { path: configFile })
}
// 3. Project tui files, applied root-first so the closest file wins.