Przeglądaj źródła

tui: convert TuiConfig namespace to ES module exports

Dax Raad 18 godzin temu
rodzic
commit
d6af5a686c
1 zmienionych plików z 164 dodań i 166 usunięć
  1. 164 166
      packages/opencode/src/cli/cmd/tui/config/tui.ts

+ 164 - 166
packages/opencode/src/cli/cmd/tui/config/tui.ts

@@ -1,3 +1,5 @@
+export * as TuiConfig from "./tui"
+
 import z from "zod"
 import { mergeDeep, unique } from "remeda"
 import { Context, Effect, Fiber, Layer } from "effect"
@@ -17,203 +19,199 @@ import { InstallationLocal, InstallationVersion } from "@/installation/version"
 import { makeRuntime } from "@/cli/effect/runtime"
 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 = {
-    result: Info
-  }
+type Acc = {
+  result: Info
+}
+
+type State = {
+  config: Info
+  deps: Array<Fiber.Fiber<void, AppFileSystem.Error>>
+}
+
+export type Info = z.output<typeof Info> & {
+  // Internal resolved plugin list used by runtime loading.
+  plugin_origins?: ConfigPlugin.Origin[]
+}
+
+export interface Interface {
+  readonly get: () => Effect.Effect<Info>
+  readonly waitForDependencies: () => Effect.Effect<void>
+}
+
+export class Service extends Context.Service<Service, Interface>()("@opencode/TuiConfig") {}
+
+function pluginScope(file: string, ctx: { directory: string }): ConfigPlugin.Scope {
+  if (Filesystem.contains(ctx.directory, file)) return "local"
+  // if (ctx.worktree !== "/" && Filesystem.contains(ctx.worktree, file)) return "local"
+  return "global"
+}
 
-  type State = {
-    config: Info
-    deps: Array<Fiber.Fiber<void, AppFileSystem.Error>>
+function normalize(raw: Record<string, unknown>) {
+  const data = { ...raw }
+  if (!("tui" in data)) return data
+  if (!isRecord(data.tui)) {
+    delete data.tui
+    return data
   }
 
-  export type Info = z.output<typeof Info> & {
-    // Internal resolved plugin list used by runtime loading.
-    plugin_origins?: ConfigPlugin.Origin[]
+  const tui = data.tui
+  delete data.tui
+  return {
+    ...tui,
+    ...data,
   }
+}
 
-  export interface Interface {
-    readonly get: () => Effect.Effect<Info>
-    readonly waitForDependencies: () => Effect.Effect<void>
+async function resolvePlugins(config: Info, configFilepath: string) {
+  if (!config.plugin) return config
+  for (let i = 0; i < config.plugin.length; i++) {
+    config.plugin[i] = await ConfigPlugin.resolvePluginSpec(config.plugin[i], configFilepath)
   }
+  return config
+}
 
-  export class Service extends Context.Service<Service, Interface>()("@opencode/TuiConfig") {}
+async function mergeFile(acc: Acc, file: string, ctx: { directory: string }) {
+  const data = await loadFile(file)
+  acc.result = mergeDeep(acc.result, data)
+  if (!data.plugin?.length) return
+
+  const scope = pluginScope(file, ctx)
+  const plugins = ConfigPlugin.deduplicatePluginOrigins([
+    ...(acc.result.plugin_origins ?? []),
+    ...data.plugin.map((spec) => ({ spec, scope, source: file })),
+  ])
+  acc.result.plugin = plugins.map((item) => item.spec)
+  acc.result.plugin_origins = plugins
+}
 
-  function pluginScope(file: string, ctx: { directory: string }): ConfigPlugin.Scope {
-    if (Filesystem.contains(ctx.directory, file)) return "local"
-    // if (ctx.worktree !== "/" && Filesystem.contains(ctx.worktree, file)) return "local"
-    return "global"
-  }
+async function loadState(ctx: { directory: string }) {
+  // Every config dir we may read from: global config dir, any `.opencode`
+  // folders between cwd and home, and OPENCODE_CONFIG_DIR.
+  const directories = await ConfigPaths.directories(ctx.directory)
+  // One-time migration: extract tui keys (theme/keybinds/tui) from existing
+  // opencode.json files into sibling tui.json files.
+  await migrateTuiConfig({ directories, cwd: ctx.directory })
 
-  function normalize(raw: Record<string, unknown>) {
-    const data = { ...raw }
-    if (!("tui" in data)) return data
-    if (!isRecord(data.tui)) {
-      delete data.tui
-      return data
-    }
+  const projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG ? [] : await ConfigPaths.projectFiles("tui", ctx.directory)
 
-    const tui = data.tui
-    delete data.tui
-    return {
-      ...tui,
-      ...data,
-    }
+  const acc: Acc = {
+    result: {},
   }
 
-  async function resolvePlugins(config: Info, configFilepath: string) {
-    if (!config.plugin) return config
-    for (let i = 0; i < config.plugin.length; i++) {
-      config.plugin[i] = await ConfigPlugin.resolvePluginSpec(config.plugin[i], configFilepath)
-    }
-    return config
+  // 1. Global tui config (lowest precedence).
+  for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) {
+    await mergeFile(acc, file, ctx)
   }
 
-  async function mergeFile(acc: Acc, file: string, ctx: { directory: string }) {
-    const data = await loadFile(file)
-    acc.result = mergeDeep(acc.result, data)
-    if (!data.plugin?.length) return
-
-    const scope = pluginScope(file, ctx)
-    const plugins = ConfigPlugin.deduplicatePluginOrigins([
-      ...(acc.result.plugin_origins ?? []),
-      ...data.plugin.map((spec) => ({ spec, scope, source: file })),
-    ])
-    acc.result.plugin = plugins.map((item) => item.spec)
-    acc.result.plugin_origins = plugins
+  // 2. Explicit OPENCODE_TUI_CONFIG override, if set.
+  if (Flag.OPENCODE_TUI_CONFIG) {
+    await mergeFile(acc, Flag.OPENCODE_TUI_CONFIG, ctx)
+    log.debug("loaded custom tui config", { path: Flag.OPENCODE_TUI_CONFIG })
   }
 
-  async function loadState(ctx: { directory: string }) {
-    // Every config dir we may read from: global config dir, any `.opencode`
-    // folders between cwd and home, and OPENCODE_CONFIG_DIR.
-    const directories = await ConfigPaths.directories(ctx.directory)
-    // One-time migration: extract tui keys (theme/keybinds/tui) from existing
-    // opencode.json files into sibling tui.json files.
-    await migrateTuiConfig({ directories, cwd: ctx.directory })
-
-    const projectFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG
-      ? []
-      : await ConfigPaths.projectFiles("tui", ctx.directory)
+  // 3. Project tui files, applied root-first so the closest file wins.
+  for (const file of projectFiles) {
+    await mergeFile(acc, file, ctx)
+  }
 
-    const acc: Acc = {
-      result: {},
-    }
+  // 4. `.opencode` directories (and OPENCODE_CONFIG_DIR) discovered while
+  // walking up the tree. Also returned below so callers can install plugin
+  // dependencies from each location.
+  const dirs = unique(directories).filter((dir) => dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR)
 
-    // 1. Global tui config (lowest precedence).
-    for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) {
+  for (const dir of dirs) {
+    if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue
+    for (const file of ConfigPaths.fileInDirectory(dir, "tui")) {
       await mergeFile(acc, file, ctx)
     }
+  }
 
-    // 2. Explicit OPENCODE_TUI_CONFIG override, if set.
-    if (Flag.OPENCODE_TUI_CONFIG) {
-      await mergeFile(acc, Flag.OPENCODE_TUI_CONFIG, ctx)
-      log.debug("loaded custom tui config", { path: Flag.OPENCODE_TUI_CONFIG })
-    }
+  const keybinds = { ...(acc.result.keybinds ?? {}) }
+  if (process.platform === "win32") {
+    // Native Windows terminals do not support POSIX suspend, so prefer prompt undo.
+    keybinds.terminal_suspend = "none"
+    keybinds.input_undo ??= unique([
+      "ctrl+z",
+      ...ConfigKeybinds.Keybinds.shape.input_undo.parse(undefined).split(","),
+    ]).join(",")
+  }
+  acc.result.keybinds = ConfigKeybinds.Keybinds.parse(keybinds)
 
-    // 3. Project tui files, applied root-first so the closest file wins.
-    for (const file of projectFiles) {
-      await mergeFile(acc, file, ctx)
-    }
+  return {
+    config: acc.result,
+    dirs: acc.result.plugin?.length ? dirs : [],
+  }
+}
 
-    // 4. `.opencode` directories (and OPENCODE_CONFIG_DIR) discovered while
-    // walking up the tree. Also returned below so callers can install plugin
-    // dependencies from each location.
-    const dirs = unique(directories).filter((dir) => dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR)
+export const layer = Layer.effect(
+  Service,
+  Effect.gen(function* () {
+    const directory = yield* CurrentWorkingDirectory
+    const npm = yield* Npm.Service
+    const data = yield* Effect.promise(() => loadState({ directory }))
+    const deps = yield* Effect.forEach(
+      data.dirs,
+      (dir) =>
+        npm
+          .install(dir, {
+            add: ["@opencode-ai/plugin" + (InstallationLocal ? "" : "@" + InstallationVersion)],
+          })
+          .pipe(Effect.forkScoped),
+      {
+        concurrency: "unbounded",
+      },
+    )
 
-    for (const dir of dirs) {
-      if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue
-      for (const file of ConfigPaths.fileInDirectory(dir, "tui")) {
-        await mergeFile(acc, file, ctx)
-      }
-    }
+    const get = Effect.fn("TuiConfig.get")(() => Effect.succeed(data.config))
 
-    const keybinds = { ...(acc.result.keybinds ?? {}) }
-    if (process.platform === "win32") {
-      // Native Windows terminals do not support POSIX suspend, so prefer prompt undo.
-      keybinds.terminal_suspend = "none"
-      keybinds.input_undo ??= unique([
-        "ctrl+z",
-        ...ConfigKeybinds.Keybinds.shape.input_undo.parse(undefined).split(","),
-      ]).join(",")
-    }
-    acc.result.keybinds = ConfigKeybinds.Keybinds.parse(keybinds)
+    const waitForDependencies = Effect.fn("TuiConfig.waitForDependencies")(() =>
+      Effect.forEach(deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.ignore(), Effect.asVoid),
+    )
+    return Service.of({ get, waitForDependencies })
+  }).pipe(Effect.withSpan("TuiConfig.layer")),
+)
 
-    return {
-      config: acc.result,
-      dirs: acc.result.plugin?.length ? dirs : [],
-    }
-  }
+export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer))
 
-  export const layer = Layer.effect(
-    Service,
-    Effect.gen(function* () {
-      const directory = yield* CurrentWorkingDirectory
-      const npm = yield* Npm.Service
-      const data = yield* Effect.promise(() => loadState({ directory }))
-      const deps = yield* Effect.forEach(
-        data.dirs,
-        (dir) =>
-          npm
-            .install(dir, {
-              add: ["@opencode-ai/plugin" + (InstallationLocal ? "" : "@" + InstallationVersion)],
-            })
-            .pipe(Effect.forkScoped),
-        {
-          concurrency: "unbounded",
-        },
-      )
-
-      const get = Effect.fn("TuiConfig.get")(() => Effect.succeed(data.config))
-
-      const waitForDependencies = Effect.fn("TuiConfig.waitForDependencies")(() =>
-        Effect.forEach(deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.ignore(), Effect.asVoid),
-      )
-      return Service.of({ get, waitForDependencies })
-    }).pipe(Effect.withSpan("TuiConfig.layer")),
-  )
-
-  export const defaultLayer = layer.pipe(Layer.provide(Npm.defaultLayer))
-
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export async function waitForDependencies() {
-    await runPromise((svc) => svc.waitForDependencies())
-  }
+const { runPromise } = makeRuntime(Service, defaultLayer)
 
-  export async function get() {
-    return runPromise((svc) => svc.get())
-  }
+export async function waitForDependencies() {
+  await runPromise((svc) => svc.waitForDependencies())
+}
 
-  async function loadFile(filepath: string): Promise<Info> {
-    const text = await ConfigPaths.readFile(filepath)
-    if (!text) return {}
-    return load(text, filepath).catch((error) => {
-      log.warn("failed to load tui config", { path: filepath, error })
-      return {}
-    })
-  }
+export async function get() {
+  return runPromise((svc) => svc.get())
+}
 
-  async function load(text: string, configFilepath: string): Promise<Info> {
-    return ConfigParse.load(Info, text, {
-      type: "path",
-      path: configFilepath,
-      missing: "empty",
-      normalize: (data) => {
-        if (!isRecord(data)) return {}
-
-        // Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json
-        // (mirroring the old opencode.json shape) still get their settings applied.
-        return normalize(data)
-      },
+async function loadFile(filepath: string): Promise<Info> {
+  const text = await ConfigPaths.readFile(filepath)
+  if (!text) return {}
+  return load(text, filepath).catch((error) => {
+    log.warn("failed to load tui config", { path: filepath, error })
+    return {}
+  })
+}
+
+async function load(text: string, configFilepath: string): Promise<Info> {
+  return ConfigParse.load(Info, text, {
+    type: "path",
+    path: configFilepath,
+    missing: "empty",
+    normalize: (data) => {
+      if (!isRecord(data)) return {}
+
+      // Flatten a nested "tui" key so users who wrote `{ "tui": { ... } }` inside tui.json
+      // (mirroring the old opencode.json shape) still get their settings applied.
+      return normalize(data)
+    },
+  })
+    .then((data) => resolvePlugins(data, configFilepath))
+    .catch((error) => {
+      log.warn("invalid tui config", { path: configFilepath, error })
+      return {}
     })
-      .then((data) => resolvePlugins(data, configFilepath))
-      .catch((error) => {
-        log.warn("invalid tui config", { path: configFilepath, error })
-        return {}
-      })
-  }
 }