tui: convert TuiConfig namespace to ES module exports

This commit is contained in:
Dax Raad
2026-04-16 20:46:40 -04:00
parent 39342b0e75
commit d6af5a686c
+35 -37
View File
@@ -1,3 +1,5 @@
export * as TuiConfig from "./tui"
import z from "zod" import z from "zod"
import { mergeDeep, unique } from "remeda" import { mergeDeep, unique } from "remeda"
import { Context, Effect, Fiber, Layer } from "effect" import { Context, Effect, Fiber, Layer } from "effect"
@@ -17,39 +19,38 @@ import { InstallationLocal, InstallationVersion } from "@/installation/version"
import { makeRuntime } from "@/cli/effect/runtime" import { makeRuntime } from "@/cli/effect/runtime"
import { Filesystem, Log } from "@/util" import { Filesystem, Log } from "@/util"
export namespace TuiConfig { const log = Log.create({ service: "tui.config" })
const log = Log.create({ service: "tui.config" })
export const Info = TuiInfo export const Info = TuiInfo
type Acc = { type Acc = {
result: Info result: Info
} }
type State = { type State = {
config: Info config: Info
deps: Array<Fiber.Fiber<void, AppFileSystem.Error>> deps: Array<Fiber.Fiber<void, AppFileSystem.Error>>
} }
export type Info = z.output<typeof Info> & { export type Info = z.output<typeof Info> & {
// Internal resolved plugin list used by runtime loading. // Internal resolved plugin list used by runtime loading.
plugin_origins?: ConfigPlugin.Origin[] plugin_origins?: ConfigPlugin.Origin[]
} }
export interface Interface { export interface Interface {
readonly get: () => Effect.Effect<Info> readonly get: () => Effect.Effect<Info>
readonly waitForDependencies: () => Effect.Effect<void> readonly waitForDependencies: () => Effect.Effect<void>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/TuiConfig") {} export class Service extends Context.Service<Service, Interface>()("@opencode/TuiConfig") {}
function pluginScope(file: string, ctx: { directory: string }): ConfigPlugin.Scope { function pluginScope(file: string, ctx: { directory: string }): ConfigPlugin.Scope {
if (Filesystem.contains(ctx.directory, file)) return "local" if (Filesystem.contains(ctx.directory, file)) return "local"
// if (ctx.worktree !== "/" && Filesystem.contains(ctx.worktree, file)) return "local" // if (ctx.worktree !== "/" && Filesystem.contains(ctx.worktree, file)) return "local"
return "global" return "global"
} }
function normalize(raw: Record<string, unknown>) { function normalize(raw: Record<string, unknown>) {
const data = { ...raw } const data = { ...raw }
if (!("tui" in data)) return data if (!("tui" in data)) return data
if (!isRecord(data.tui)) { if (!isRecord(data.tui)) {
@@ -63,17 +64,17 @@ export namespace TuiConfig {
...tui, ...tui,
...data, ...data,
} }
} }
async function resolvePlugins(config: Info, configFilepath: string) { async function resolvePlugins(config: Info, configFilepath: string) {
if (!config.plugin) return config if (!config.plugin) return config
for (let i = 0; i < config.plugin.length; i++) { for (let i = 0; i < config.plugin.length; i++) {
config.plugin[i] = await ConfigPlugin.resolvePluginSpec(config.plugin[i], configFilepath) config.plugin[i] = await ConfigPlugin.resolvePluginSpec(config.plugin[i], configFilepath)
} }
return config return config
} }
async function mergeFile(acc: Acc, file: string, ctx: { directory: string }) { async function mergeFile(acc: Acc, file: string, ctx: { directory: string }) {
const data = await loadFile(file) const data = await loadFile(file)
acc.result = mergeDeep(acc.result, data) acc.result = mergeDeep(acc.result, data)
if (!data.plugin?.length) return if (!data.plugin?.length) return
@@ -85,9 +86,9 @@ export namespace TuiConfig {
]) ])
acc.result.plugin = plugins.map((item) => item.spec) acc.result.plugin = plugins.map((item) => item.spec)
acc.result.plugin_origins = plugins acc.result.plugin_origins = plugins
} }
async function loadState(ctx: { directory: string }) { async function loadState(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 = await ConfigPaths.directories(ctx.directory)
@@ -95,9 +96,7 @@ export namespace TuiConfig {
// opencode.json files into sibling tui.json files. // opencode.json files into sibling tui.json files.
await migrateTuiConfig({ directories, cwd: ctx.directory }) await migrateTuiConfig({ directories, cwd: ctx.directory })
const projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG const projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("tui", ctx.directory)
? []
: await ConfigPaths.projectFiles("tui", ctx.directory)
const acc: Acc = { const acc: Acc = {
result: {}, result: {},
@@ -146,9 +145,9 @@ export namespace TuiConfig {
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
@@ -174,30 +173,30 @@ export namespace TuiConfig {
) )
return Service.of({ get, waitForDependencies }) return Service.of({ get, waitForDependencies })
}).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))
const { runPromise } = makeRuntime(Service, defaultLayer) const { runPromise } = makeRuntime(Service, defaultLayer)
export async function waitForDependencies() { export async function waitForDependencies() {
await runPromise((svc) => svc.waitForDependencies()) await runPromise((svc) => svc.waitForDependencies())
} }
export async function get() { export async function get() {
return runPromise((svc) => svc.get()) return runPromise((svc) => svc.get())
} }
async function loadFile(filepath: string): Promise<Info> { async function loadFile(filepath: string): Promise<Info> {
const text = await ConfigPaths.readFile(filepath) const text = await ConfigPaths.readFile(filepath)
if (!text) return {} if (!text) return {}
return load(text, filepath).catch((error) => { return load(text, filepath).catch((error) => {
log.warn("failed to load tui config", { path: filepath, error }) log.warn("failed to load tui config", { path: filepath, error })
return {} return {}
}) })
} }
async function load(text: string, configFilepath: string): Promise<Info> { async function load(text: string, configFilepath: string): Promise<Info> {
return ConfigParse.load(Info, text, { return ConfigParse.load(Info, text, {
type: "path", type: "path",
path: configFilepath, path: configFilepath,
@@ -215,5 +214,4 @@ export namespace TuiConfig {
log.warn("invalid tui config", { path: configFilepath, error }) log.warn("invalid tui config", { path: configFilepath, error })
return {} return {}
}) })
}
} }