refactor(core): replace legacy logger with Effect logging (#31310)
This commit is contained in:
@@ -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 +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/"])
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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 }) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user