Jelajahi Sumber

refactor: migrate namespace tooling to self-reexport pattern

Kit Langton 1 Minggu lalu
induk
melakukan
95f90ebfc8
50 mengubah file dengan 506 tambahan dan 702 penghapusan
  1. 1 1
      packages/opencode/script/schema.ts
  2. 254 112
      packages/opencode/script/unwrap-namespace.ts
  3. 102 440
      packages/opencode/specs/effect/namespace-treeshake.md
  4. 1 1
      packages/opencode/src/acp/agent.ts
  5. 1 1
      packages/opencode/src/agent/agent.ts
  6. 1 1
      packages/opencode/src/cli/cmd/debug/config.ts
  7. 1 1
      packages/opencode/src/cli/cmd/mcp.ts
  8. 1 1
      packages/opencode/src/cli/cmd/providers.ts
  9. 1 1
      packages/opencode/src/cli/cmd/tui/worker.ts
  10. 1 1
      packages/opencode/src/cli/network.ts
  11. 1 1
      packages/opencode/src/cli/upgrade.ts
  12. 1 1
      packages/opencode/src/command/command.ts
  13. 1 0
      packages/opencode/src/config/config.ts
  14. 1 1
      packages/opencode/src/effect/app-runtime.ts
  15. 103 104
      packages/opencode/src/file/time.ts
  16. 1 1
      packages/opencode/src/file/watcher.ts
  17. 1 1
      packages/opencode/src/format/format.ts
  18. 1 1
      packages/opencode/src/lsp/lsp.ts
  19. 1 1
      packages/opencode/src/mcp/mcp.ts
  20. 1 1
      packages/opencode/src/permission/permission.ts
  21. 1 1
      packages/opencode/src/plugin/plugin.ts
  22. 1 1
      packages/opencode/src/provider/provider.ts
  23. 1 1
      packages/opencode/src/server/instance/config.ts
  24. 1 1
      packages/opencode/src/server/instance/experimental.ts
  25. 1 1
      packages/opencode/src/server/instance/global.ts
  26. 1 1
      packages/opencode/src/server/instance/mcp.ts
  27. 1 1
      packages/opencode/src/server/instance/provider.ts
  28. 1 1
      packages/opencode/src/session/compaction.ts
  29. 1 1
      packages/opencode/src/session/instruction.ts
  30. 1 1
      packages/opencode/src/session/llm.ts
  31. 1 1
      packages/opencode/src/session/overflow.ts
  32. 1 1
      packages/opencode/src/session/processor.ts
  33. 1 1
      packages/opencode/src/share/session.ts
  34. 1 1
      packages/opencode/src/share/share-next.ts
  35. 1 1
      packages/opencode/src/skill/skill.ts
  36. 1 1
      packages/opencode/src/snapshot/snapshot.ts
  37. 1 1
      packages/opencode/src/tool/registry.ts
  38. 1 1
      packages/opencode/src/tool/task.ts
  39. 1 1
      packages/opencode/test/config/agent-color.test.ts
  40. 1 1
      packages/opencode/test/config/config.test.ts
  41. 1 1
      packages/opencode/test/config/tui.test.ts
  42. 1 1
      packages/opencode/test/file/watcher.test.ts
  43. 1 1
      packages/opencode/test/fixture/fixture.ts
  44. 1 1
      packages/opencode/test/permission-task.test.ts
  45. 1 1
      packages/opencode/test/session/compaction.test.ts
  46. 1 1
      packages/opencode/test/session/processor-effect.test.ts
  47. 1 1
      packages/opencode/test/session/prompt-effect.test.ts
  48. 1 1
      packages/opencode/test/session/snapshot-tool-race.test.ts
  49. 1 1
      packages/opencode/test/share/share-next.test.ts
  50. 1 1
      packages/opencode/test/tool/task.test.ts

+ 1 - 1
packages/opencode/script/schema.ts

@@ -1,7 +1,7 @@
 #!/usr/bin/env bun
 
 import { z } from "zod"
-import { Config } from "../src/config"
+import { Config } from "../src/config/config"
 import { TuiConfig } from "../src/cli/cmd/tui/config/tui"
 
 function generate(schema: z.ZodType) {

+ 254 - 112
packages/opencode/script/unwrap-namespace.ts

@@ -1,21 +1,26 @@
 #!/usr/bin/env bun
 /**
- * Unwrap a TypeScript `export namespace` into flat exports + barrel.
+ * Unwrap a TypeScript `export namespace` into flat exports with self-reexport.
  *
  * Usage:
- *   bun script/unwrap-namespace.ts src/bus/index.ts
- *   bun script/unwrap-namespace.ts src/bus/index.ts --dry-run
- *   bun script/unwrap-namespace.ts src/pty/index.ts --name service   # avoid collision with pty.ts
+ *   bun script/unwrap-namespace.ts src/session/session.ts           # convert namespace
+ *   bun script/unwrap-namespace.ts src/session/session.ts --dry-run
+ *   bun script/unwrap-namespace.ts src/pty/index.ts --name service  # avoid filename collision
+ *   bun script/unwrap-namespace.ts src/config/config.ts --retrofit  # already flat, add self-reexport
  *
- * What it does:
- *   1. Reads the file and finds the `export namespace Foo { ... }` block
- *      (uses ast-grep for accurate AST-based boundary detection)
- *   2. Removes the namespace wrapper and dedents the body
- *   3. Fixes self-references (e.g. Config.PermissionAction → PermissionAction)
- *   4. If the file is index.ts, renames it to <lowercase-name>.ts
- *   5. Creates/updates index.ts with `export * as Foo from "./<file>"`
- *   6. Rewrites import paths across src/, test/, and script/
- *   7. Fixes sibling imports within the same directory
+ * Default mode:
+ *   1. Finds `export namespace Foo { ... }` (ast-grep)
+ *   2. Removes wrapper, dedents body, fixes self-references
+ *   3. Appends `export * as Foo from "./file"` to the file (self-reexport)
+ *   4. Rewrites consumer imports to point at the file directly
+ *
+ * Retrofit mode (--retrofit):
+ *   File already has flat exports (from previous barrel migration).
+ *   1. Reads the barrel index.ts to find the namespace name
+ *   2. Adds `export * as Foo from "./file"` to the source file
+ *   3. Rewrites consumers from barrel import to direct file import
+ *
+ * Does NOT create barrel index.ts files.
  *
  * Requires: ast-grep (`brew install ast-grep` or `cargo install ast-grep`)
  */
@@ -25,11 +30,12 @@ import fs from "fs"
 
 const args = process.argv.slice(2)
 const dryRun = args.includes("--dry-run")
+const retrofit = args.includes("--retrofit")
 const nameFlag = args.find((a, i) => args[i - 1] === "--name")
 const filePath = args.find((a) => !a.startsWith("--") && args[args.indexOf(a) - 1] !== "--name")
 
 if (!filePath) {
-  console.error("Usage: bun script/unwrap-namespace.ts <file> [--dry-run] [--name <impl-name>]")
+  console.error("Usage: bun script/unwrap-namespace.ts <file> [--dry-run] [--name <impl>] [--retrofit]")
   process.exit(1)
 }
 
@@ -39,11 +45,76 @@ if (!fs.existsSync(absPath)) {
   process.exit(1)
 }
 
+const srcRoot = path.resolve("src")
+const dir = path.dirname(absPath)
+const basename = path.basename(absPath, ".ts")
+
+// ---------------------------------------------------------------------------
+// Barrel map: parse an index.ts to get namespace→file mapping
+// ---------------------------------------------------------------------------
+
+function parseBarrelMap(indexPath: string): Record<string, string> {
+  const map: Record<string, string> = {}
+  if (!fs.existsSync(indexPath)) return map
+  const content = fs.readFileSync(indexPath, "utf-8")
+  const re = /export\s+\*\s+as\s+(\w+)\s+from\s+["']\.\/([^"']+)["']/g
+  for (const m of content.matchAll(re)) {
+    map[m[1]] = m[2].replace(/\.ts$/, "")
+  }
+  return map
+}
+
+// ---------------------------------------------------------------------------
+// Retrofit mode: file is already flat, just add self-reexport + fix imports
+// ---------------------------------------------------------------------------
+
+if (retrofit) {
+  const indexFile = path.join(dir, "index.ts")
+  const barrelMap = parseBarrelMap(indexFile)
+
+  // Find this file's namespace name from the barrel
+  const relName = basename
+  let nsName: string | undefined
+  for (const [ns, file] of Object.entries(barrelMap)) {
+    if (file === relName) {
+      nsName = ns
+      break
+    }
+  }
+
+  if (!nsName) {
+    console.error(`Could not find namespace for ${basename}.ts in ${indexFile}`)
+    console.error("Barrel map:", barrelMap)
+    process.exit(1)
+  }
+
+  console.log(`Retrofit: ${basename}.ts → add self-reexport as ${nsName}`)
+
+  // Check if self-reexport already exists
+  const content = fs.readFileSync(absPath, "utf-8")
+  const selfReexport = `export * as ${nsName} from "./${basename}"`
+  if (content.includes(selfReexport)) {
+    console.log("Self-reexport already present, skipping file modification")
+  } else if (!dryRun) {
+    const trimmed = content.endsWith("\n") ? content : content + "\n"
+    fs.writeFileSync(absPath, trimmed + selfReexport + "\n")
+    console.log(`Added: ${selfReexport}`)
+  } else {
+    console.log(`Would add: ${selfReexport}`)
+  }
+
+  // Now rewrite consumers (same logic as default mode, below)
+  rewriteConsumers(nsName, absPath, basename, dir)
+  process.exit(0)
+}
+
+// ---------------------------------------------------------------------------
+// Default mode: unwrap namespace
+// ---------------------------------------------------------------------------
+
 const src = fs.readFileSync(absPath, "utf-8")
 const lines = src.split("\n")
 
-// Use ast-grep to find the namespace boundaries accurately.
-// This avoids false matches from braces in strings, templates, comments, etc.
 const astResult = Bun.spawnSync(
   ["ast-grep", "run", "--pattern", "export namespace $NAME { $$$BODY }", "--lang", "typescript", "--json", absPath],
   { stdout: "pipe", stderr: "pipe" },
@@ -61,34 +132,29 @@ const matches = JSON.parse(astResult.stdout.toString()) as Array<{
 }>
 
 if (matches.length === 0) {
-  console.error("No `export namespace Foo { ... }` found in file")
+  console.error("No `export namespace Foo { ... }` found. Use --retrofit for already-converted files.")
   process.exit(1)
 }
 
 if (matches.length > 1) {
   console.error(`Found ${matches.length} namespaces — this script handles one at a time`)
-  console.error("Namespaces found:")
   for (const m of matches) console.error(`  ${m.metaVariables.single.NAME.text} (line ${m.range.start.line + 1})`)
   process.exit(1)
 }
 
 const match = matches[0]
 const nsName = match.metaVariables.single.NAME.text
-const nsLine = match.range.start.line // 0-indexed
-const closeLine = match.range.end.line // 0-indexed, the line with closing `}`
+const nsLine = match.range.start.line
+const closeLine = match.range.end.line
 
 console.log(`Found: export namespace ${nsName} { ... }`)
 console.log(`  Lines ${nsLine + 1}–${closeLine + 1} (${closeLine - nsLine + 1} lines)`)
 
-// Build the new file content:
-// 1. Everything before the namespace declaration (imports, etc.)
-// 2. The namespace body, dedented by one level (2 spaces)
-// 3. Everything after the closing brace (rare, but possible)
+// Unwrap: remove namespace wrapper, dedent body
 const before = lines.slice(0, nsLine)
 const body = lines.slice(nsLine + 1, closeLine)
 const after = lines.slice(closeLine + 1)
 
-// Dedent: remove exactly 2 leading spaces from each line
 const dedented = body.map((line) => {
   if (line === "") return ""
   if (line.startsWith("  ")) return line.slice(2)
@@ -97,9 +163,7 @@ const dedented = body.map((line) => {
 
 let newContent = [...before, ...dedented, ...after].join("\n")
 
-// --- Fix self-references ---
-// After unwrapping, references like `Config.PermissionAction` inside the same file
-// need to become just `PermissionAction`. Only fix code positions, not strings.
+// Fix self-references (Foo.Bar → Bar when Bar is exported from this file)
 const exportedNames = new Set<string>()
 const exportRegex = /export\s+(?:const|function|class|interface|type|enum|abstract\s+class)\s+(\w+)/g
 for (const line of dedented) {
@@ -122,7 +186,6 @@ for (const line of dedented) {
 let selfRefCount = 0
 if (exportedNames.size > 0) {
   const fixedLines = newContent.split("\n").map((line) => {
-    // Split line into string-literal and code segments to avoid replacing inside strings
     const segments: Array<{ text: string; isString: boolean }> = []
     let i = 0
     let current = ""
@@ -186,120 +249,199 @@ if (exportedNames.size > 0) {
   newContent = fixedLines.join("\n")
 }
 
-// Figure out file naming
-const dir = path.dirname(absPath)
-const basename = path.basename(absPath, ".ts")
+// Handle index.ts rename
 const isIndex = basename === "index"
 const implName = nameFlag ?? (isIndex ? nsName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase() : basename)
-const implFile = path.join(dir, `${implName}.ts`)
-const indexFile = path.join(dir, "index.ts")
-const barrelLine = `export * as ${nsName} from "./${implName}"\n`
+const implFile = isIndex ? path.join(dir, `${implName}.ts`) : absPath
+
+// Add self-reexport at the bottom
+const selfReexport = `export * as ${nsName} from "./${implName}"`
+if (!newContent.endsWith("\n")) newContent += "\n"
+newContent += selfReexport + "\n"
 
 console.log("")
 if (isIndex) {
-  console.log(`Plan: rename ${basename}.ts → ${implName}.ts, create new index.ts barrel`)
+  console.log(`Plan: rename index.ts → ${implName}.ts, add self-reexport`)
 } else {
-  console.log(`Plan: rewrite ${basename}.ts in place, create index.ts barrel`)
+  console.log(`Plan: unwrap in place, add self-reexport`)
 }
 if (selfRefCount > 0) console.log(`Fixed ${selfRefCount} self-reference(s) (${nsName}.X → X)`)
-console.log("")
 
 if (dryRun) {
+  console.log("")
   console.log("--- DRY RUN ---")
   console.log("")
-  console.log(`=== ${implName}.ts (first 30 lines) ===`)
+  console.log(`=== ${implName}.ts (first 20 lines) ===`)
   newContent
     .split("\n")
-    .slice(0, 30)
+    .slice(0, 20)
     .forEach((l, i) => console.log(`  ${i + 1}: ${l}`))
   console.log("  ...")
   console.log("")
-  console.log(`=== index.ts ===`)
-  console.log(`  ${barrelLine.trim()}`)
+  console.log(`=== last 5 lines ===`)
+  const allLines = newContent.split("\n")
+  allLines.slice(-5).forEach((l, i) => console.log(`  ${allLines.length - 4 + i}: ${l}`))
   console.log("")
-  if (!isIndex) {
-    const relDir = path.relative(path.resolve("src"), dir)
-    console.log(`=== Import rewrites (would apply) ===`)
-    console.log(`  ${relDir}/${basename}" → ${relDir}" across src/, test/, script/`)
-  } else {
-    console.log("No import rewrites needed (was index.ts)")
-  }
+  rewriteConsumers(nsName, implFile, implName, dir)
 } else {
   if (isIndex) {
     fs.writeFileSync(implFile, newContent)
-    fs.writeFileSync(indexFile, barrelLine)
-    console.log(`Wrote ${implName}.ts (${newContent.split("\n").length} lines)`)
-    console.log(`Wrote index.ts (barrel)`)
+    fs.unlinkSync(absPath)
+    console.log(`Renamed to ${implName}.ts (${newContent.split("\n").length} lines)`)
   } else {
     fs.writeFileSync(absPath, newContent)
-    if (fs.existsSync(indexFile)) {
-      const existing = fs.readFileSync(indexFile, "utf-8")
-      if (!existing.includes(`export * as ${nsName}`)) {
-        fs.appendFileSync(indexFile, barrelLine)
-        console.log(`Appended to existing index.ts`)
-      } else {
-        console.log(`index.ts already has ${nsName} export`)
-      }
-    } else {
-      fs.writeFileSync(indexFile, barrelLine)
-      console.log(`Wrote index.ts (barrel)`)
-    }
     console.log(`Rewrote ${basename}.ts (${newContent.split("\n").length} lines)`)
   }
+  rewriteConsumers(nsName, implFile, implName, dir)
+}
 
-  // --- Rewrite import paths across src/, test/, script/ ---
-  const relDir = path.relative(path.resolve("src"), dir)
-  if (!isIndex) {
-    const oldTail = `${relDir}/${basename}`
-    const searchDirs = ["src", "test", "script"].filter((d) => fs.existsSync(d))
-    const rgResult = Bun.spawnSync(["rg", "-l", `from.*${oldTail}"`, ...searchDirs], {
-      stdout: "pipe",
-      stderr: "pipe",
-    })
-    const filesToRewrite = rgResult.stdout
-      .toString()
-      .trim()
-      .split("\n")
-      .filter((f) => f.length > 0)
-
-    if (filesToRewrite.length > 0) {
-      console.log(`\nRewriting imports in ${filesToRewrite.length} file(s)...`)
-      for (const file of filesToRewrite) {
-        const content = fs.readFileSync(file, "utf-8")
-        fs.writeFileSync(file, content.replaceAll(`${oldTail}"`, `${relDir}"`))
-      }
-      console.log(`  Done: ${oldTail}" → ${relDir}"`)
-    } else {
-      console.log("\nNo import rewrites needed")
-    }
-  } else {
-    console.log("\nNo import rewrites needed (was index.ts)")
-  }
+// ---------------------------------------------------------------------------
+// Consumer import rewriting (shared by default + retrofit mode)
+// ---------------------------------------------------------------------------
+
+function rewriteConsumers(nsName: string, implFile: string, implName: string, dir: string) {
+  const relImplFromSrc = path.relative(srcRoot, implFile).replace(/\.ts$/, "")
+  const barrelMap = parseBarrelMap(path.join(dir, "index.ts"))
 
-  // --- Fix sibling imports within the same directory ---
-  const siblingFiles = fs.readdirSync(dir).filter((f) => {
-    if (!f.endsWith(".ts")) return false
-    if (f === "index.ts" || f === `${implName}.ts`) return false
-    return true
+  // Find all files that reference the namespace name
+  const searchDirs = ["src", "test", "script"].filter((d) => fs.existsSync(d))
+  const rgResult = Bun.spawnSync(["rg", "-l", nsName, ...searchDirs, "--type", "ts"], {
+    stdout: "pipe",
+    stderr: "pipe",
   })
+  const candidates = rgResult.stdout
+    .toString()
+    .trim()
+    .split("\n")
+    .filter((f) => f.length > 0)
+
+  let totalChanges = 0
+  const changedFiles: string[] = []
+
+  for (const file of candidates) {
+    const absFile = path.resolve(file)
+    if (absFile === path.resolve(implFile) || absFile === path.resolve(absPath)) continue
+
+    let content = fs.readFileSync(file, "utf-8")
+    let changes = 0
+
+    // Match: import { Foo } or import { Foo, Bar } or import type { Foo }
+    const importRe = /^(import\s+(?:type\s+)?)\{\s*([^}]+)\}\s*from\s*["']([^"']+)["']/gm
+
+    content = content.replace(importRe, (original, prefix: string, names: string, importPath: string) => {
+      const nameList = names
+        .split(",")
+        .map((n) => n.trim())
+        .filter(Boolean)
+
+      // Check if this namespace is among the imported names
+      const nsEntry = nameList.find((n) => n.split(/\s+as\s+/)[0].trim() === nsName)
+      if (!nsEntry) return original
+
+      // Check if this import resolves to our directory (barrel) or our file
+      const resolved = resolveImportPath(importPath, file)
+      if (!resolved) return original
+
+      const resolvedAbs = path.resolve(resolved)
+      const isBarrelImport =
+        resolvedAbs === dir || resolvedAbs === path.join(dir, "index.ts") || resolvedAbs === path.join(dir, "index")
+      const isDirectImport = resolvedAbs === implFile.replace(/\.ts$/, "") || resolvedAbs === implFile
+
+      if (!isBarrelImport && !isDirectImport) return original
+
+      // If it's already a direct import with just this name, nothing to change
+      if (isDirectImport && nameList.length === 1) return original
+
+      // Build the correct import path for the impl file
+      const newImportPath = computeImportPath(file, implFile)
+
+      if (nameList.length === 1) {
+        // Simple: just repoint to the file
+        changes++
+        return `${prefix}{ ${nsEntry} } from "${newImportPath}"`
+      }
+
+      // Multi-import: split into separate lines
+      const newLines: string[] = []
+      for (const n of nameList) {
+        const imported = n.split(/\s+as\s+/)[0].trim()
+
+        if (imported === nsName) {
+          newLines.push(`${prefix}{ ${n} } from "${newImportPath}"`)
+          changes++
+        } else if (barrelMap[imported]) {
+          // Another namespace from the same barrel
+          const otherFile = path.join(dir, barrelMap[imported] + ".ts")
+          const otherPath = computeImportPath(file, otherFile)
+          newLines.push(`${prefix}{ ${n} } from "${otherPath}"`)
+          changes++
+        } else {
+          // Unknown — keep original path
+          newLines.push(`${prefix}{ ${n} } from "${importPath}"`)
+        }
+      }
+      return newLines.join("\n")
+    })
+
+    // Fix dynamic imports: const { Foo } = await import("...")
+    const dynRe = new RegExp(
+      `(const|let|var)\\s+\\{\\s*${nsName}\\s*\\}\\s*=\\s*await\\s+import\\(\\s*["']([^"']+)["']\\s*\\)`,
+      "g",
+    )
+    content = content.replace(dynRe, (original, decl, importPath) => {
+      const resolved = resolveImportPath(importPath, file)
+      if (!resolved) return original
+      const resolvedAbs = path.resolve(resolved)
+      const isTarget =
+        resolvedAbs === dir ||
+        resolvedAbs === path.join(dir, "index.ts") ||
+        resolvedAbs === path.join(dir, "index") ||
+        resolvedAbs === implFile.replace(/\.ts$/, "") ||
+        resolvedAbs === implFile
+      if (!isTarget) return original
+      const newPath = computeImportPath(file, implFile)
+      changes++
+      return `${decl} ${nsName} = await import("${newPath}")`
+    })
 
-  let siblingFixCount = 0
-  for (const sibFile of siblingFiles) {
-    const sibPath = path.join(dir, sibFile)
-    const content = fs.readFileSync(sibPath, "utf-8")
-    const pattern = new RegExp(`from\\s+["']\\./${basename}["']`, "g")
-    if (pattern.test(content)) {
-      fs.writeFileSync(sibPath, content.replace(pattern, `from "."`))
-      siblingFixCount++
+    if (changes > 0) {
+      if (!dryRun) fs.writeFileSync(file, content)
+      changedFiles.push(file)
+      totalChanges += changes
     }
   }
-  if (siblingFixCount > 0) {
-    console.log(`Fixed ${siblingFixCount} sibling import(s) in ${path.basename(dir)}/ (./${basename} → .)`)
+
+  console.log("")
+  if (totalChanges > 0) {
+    console.log(`${dryRun ? "Would rewrite" : "Rewrote"} ${totalChanges} import(s) in ${changedFiles.length} file(s):`)
+    for (const f of changedFiles) console.log(`  ${f}`)
+  } else {
+    console.log("No import rewrites needed")
   }
+
+  console.log("")
+  console.log("=== Verify ===")
+  console.log("")
+  console.log("bunx --bun tsgo --noEmit                                # typecheck")
+  console.log("bun run --conditions=browser ./src/index.ts generate    # circular import check")
 }
 
-console.log("")
-console.log("=== Verify ===")
-console.log("")
-console.log("bunx --bun tsgo --noEmit   # typecheck")
-console.log("bun run test               # run tests")
+// ---------------------------------------------------------------------------
+// Path utilities
+// ---------------------------------------------------------------------------
+
+function resolveImportPath(importPath: string, fromFile: string): string | null {
+  if (importPath.startsWith("@/")) return path.join(srcRoot, importPath.slice(2))
+  if (importPath.startsWith(".")) return path.resolve(path.dirname(fromFile), importPath)
+  return null
+}
+
+function computeImportPath(fromFile: string, toFile: string): string {
+  const fromAbs = path.resolve(fromFile)
+  if (fromAbs.startsWith(srcRoot + "/")) {
+    return `@/${path.relative(srcRoot, toFile).replace(/\.ts$/, "")}`
+  }
+  let rel = path.relative(path.dirname(fromAbs), toFile).replace(/\.ts$/, "")
+  if (!rel.startsWith(".")) rel = "./" + rel
+  return rel
+}

+ 102 - 440
packages/opencode/specs/effect/namespace-treeshake.md

@@ -1,499 +1,161 @@
-# Namespace → flat export migration
+# Namespace → self-reexport migration
 
-Migrate `export namespace` to the `export * as` / flat-export pattern used by
-effect-smol. Primary goal: tree-shakeability. Secondary: consistency with Effect
-conventions, LLM-friendliness for future migrations.
+Migrate `export namespace` to flat module exports with a self-referential
+`export * as` at the bottom of each file. No barrel files.
 
-## What changes and what doesn't
+## The pattern
 
-The **consumer API stays the same**. You still write `Provider.ModelNotFoundError`,
-`Config.JsonError`, `Bus.publish`, etc. The namespace ergonomics are preserved.
-
-What changes is **how** the namespace is constructed — the TypeScript
-`export namespace` keyword is replaced by `export * as` in a barrel file. This
-is a mechanical change: unwrap the namespace body into flat exports, add a
-one-line barrel. Consumers that import `{ Provider }` don't notice.
-
-Import paths actually get **nicer**. Today most consumers import from the
-explicit file (`"../provider/provider"`). After the migration, each module has a
-barrel `index.ts`, so imports become `"../provider"` or `"@/provider"`:
+Each module file has flat exports plus one line at the bottom that re-exports
+itself as a namespace:
 
 ```ts
-// BEFORE — points at the file directly
-import { Provider } from "../provider/provider"
-
-// AFTER — resolves to provider/index.ts, same Provider namespace
-import { Provider } from "../provider"
-```
-
-## Why this matters right now
-
-The CLI binary startup time (TOI) is too slow. Profiling shows we're loading
-massive dependency graphs that are never actually used at runtime — because
-bundlers cannot tree-shake TypeScript `export namespace` bodies.
-
-### The problem in one sentence
-
-`cli/error.ts` needs 6 lightweight `.isInstance()` checks on error classes, but
-importing `{ Provider }` from `provider.ts` forces the bundler to include **all
-20+ `@ai-sdk/*` packages**, `@aws-sdk/credential-providers`,
-`google-auth-library`, and every other top-level import in that 1709-line file.
-
-### Why `export namespace` defeats tree-shaking
-
-TypeScript compiles `export namespace Foo { ... }` to an IIFE:
+// config/config.ts
+import { Log } from "../util/log"
 
-```js
-// TypeScript output
-export var Provider;
-(function (Provider) {
-  Provider.ModelNotFoundError = NamedError.create(...)
-  // ... 1600 more lines of assignments ...
-})(Provider || (Provider = {}))
-```
-
-This is **opaque to static analysis**. The bundler sees one big function call
-whose return value populates an object. It cannot determine which properties are
-used downstream, so it keeps everything. Every `import` statement at the top of
-`provider.ts` executes unconditionally — that's 20+ AI SDK packages loaded into
-memory just so the CLI can check `Provider.ModelNotFoundError.isInstance(x)`.
-
-### What `export * as` does differently
-
-`export * as Provider from "./provider"` compiles to a static re-export. The
-bundler knows the exact shape of `Provider` at compile time — it's the named
-export list of `./provider.ts`. When it sees `Provider.ModelNotFoundError` used
-but `Provider.layer` unused, it can trace that `ModelNotFoundError` doesn't
-reference `createAnthropic` or any AI SDK import, and drop them. The namespace
-object still exists at runtime — same API — but the bundler can see inside it.
-
-### Concrete impact
+export interface Info { model: string }
+export function load(): Info { ... }
+export const JsonError = NamedError.create(...)
 
-The worst import chain in the codebase:
-
-```
-src/index.ts (entry point)
-  └── FormatError from src/cli/error.ts
-        ├── { Provider } from provider/provider.ts     (1709 lines)
-        │     ├── 20+ @ai-sdk/* packages
-        │     ├── @aws-sdk/credential-providers
-        │     ├── google-auth-library
-        │     ├── gitlab-ai-provider, venice-ai-sdk-provider
-        │     └── fuzzysort, remeda, etc.
-        ├── { Config } from config/config.ts           (1663 lines)
-        │     ├── jsonc-parser
-        │     ├── LSPServer (all server definitions)
-        │     └── Plugin, Auth, Env, Account, etc.
-        └── { MCP } from mcp/index.ts                  (930 lines)
-              ├── @modelcontextprotocol/sdk (3 transports)
-              └── open (browser launcher)
+// Self-reexport: creates a named `Config` export that consumers can import
+export * as Config from "./config"
 ```
 
-All of this gets pulled in to check `.isInstance()` on 6 error classes — code
-that needs maybe 200 bytes total. This inflates the binary, increases startup
-memory, and slows down initial module evaluation.
-
-### Why this also hurts memory
-
-Every module-level import is eagerly evaluated. Even with Bun's fast module
-loader, evaluating 20+ AI SDK factory functions, the AWS credential chain, and
-Google's auth library allocates objects, closures, and prototype chains that
-persist for the lifetime of the process. Most CLI commands never use a provider
-at all.
-
-## What effect-smol does
-
-effect-smol achieves tree-shakeable namespaced APIs via three structural choices.
-
-### 1. Each module is a separate file with flat named exports
-
-```ts
-// Effect.ts — no namespace wrapper, just flat exports
-export const gen: { ... } = internal.gen
-export const fail: <E>(error: E) => Effect<never, E> = internal.fail
-export const succeed: <A>(value: A) => Effect<A> = internal.succeed
-// ... 230+ individual named exports
-```
-
-### 2. Barrel file uses `export * as` (not `export namespace`)
+Consumers import the namespace by name — editors auto-import this like any
+named export:
 
 ```ts
-// index.ts
-export * as Effect from "./Effect.ts"
-export * as Schema from "./Schema.ts"
-export * as Stream from "./Stream.ts"
-// ~134 modules
+import { Config } from "../config/config"
+Config.load()
+Config.JsonError.isInstance(x)
 ```
 
-This creates a namespace-like API (`Effect.gen`, `Schema.parse`) but the
-bundler knows the **exact shape** at compile time — it's the static export list
-of that file. It can trace property accesses (`Effect.gen` → keep `gen`,
-drop `timeout` if unused). With `export namespace`, the IIFE is opaque and
-nothing can be dropped.
+## Why this pattern
 
-### 3. `sideEffects: []` and deep imports
+We tested every option with Bun. Three things matter: tree-shaking, circular
+imports, and editor autocomplete.
 
-```jsonc
-// package.json
-{ "sideEffects": [] }
 ```
+A. Barrel (export * as Foo + Bar from index.ts)
+   Runtime:  foo LOADED even though only Bar used  ❌
+   Bundled:  foo LOADED if it has side effects     ❌
+   Autocomplete: works (named export from barrel)
 
-Plus `"./*": "./src/*.ts"` in the exports map, enabling
-`import * as Effect from "effect/Effect"` to bypass the barrel entirely.
-
-### 4. Errors as flat exports, not class declarations
+B. import * as Bar from "./bar" (direct, no barrel)
+   Runtime:  only bar loaded                       ✅
+   Bundled:  only bar loaded                       ✅
+   Autocomplete: broken (editors can't auto-import) ❌
 
-```ts
-// Cause.ts
-export const NoSuchElementErrorTypeId = core.NoSuchElementErrorTypeId
-export interface NoSuchElementError extends YieldableError { ... }
-export const NoSuchElementError: new(msg?: string) => NoSuchElementError = core.NoSuchElementError
-export const isNoSuchElementError: (u: unknown) => u is NoSuchElementError = core.isNoSuchElementError
+C. Self-reexport: export * as Bar from "./bar" inside bar.ts
+   Runtime:  only bar loaded                       ✅
+   Bundled:  only bar loaded                       ✅
+   Autocomplete: works (named export from file)    ✅
 ```
 
-Each error is 4 independent exports: TypeId, interface, constructor (as const),
-type guard. All individually shakeable.
-
-## The plan
-
-The core migration is **Phase 1** — convert `export namespace` to
-`export * as`. Once that's done, the bundler can tree-shake individual exports
-within each module. You do NOT need to break things into subfiles for
-tree-shaking to work — the bundler traces which exports you actually access on
-the namespace object and drops the rest, including their transitive imports.
-
-Splitting errors/schemas into separate files (Phase 0) is optional — it's a
-lower-risk warmup step that can be done before or after the main conversion, and
-it provides extra resilience against bundler edge cases. But the big win comes
-from Phase 1.
-
-### Phase 0 (optional): Pre-split errors into subfiles
-
-This is a low-risk warmup that provides immediate benefit even before the full
-`export * as` conversion. It's optional because Phase 1 alone is sufficient for
-tree-shaking. But it's a good starting point if you want incremental progress:
-
-**For each namespace that defines errors** (15 files, ~30 error classes total):
-
-1. Create a sibling `errors.ts` file (e.g. `provider/errors.ts`) with the error
-   definitions as top-level named exports:
-
-   ```ts
-   // provider/errors.ts
-   import z from "zod"
-   import { NamedError } from "@opencode-ai/shared/util/error"
-   import { ProviderID, ModelID } from "./schema"
-
-   export const ModelNotFoundError = NamedError.create(
-     "ProviderModelNotFoundError",
-     z.object({
-       providerID: ProviderID.zod,
-       modelID: ModelID.zod,
-       suggestions: z.array(z.string()).optional(),
-     }),
-   )
-
-   export const InitError = NamedError.create("ProviderInitError", z.object({ providerID: ProviderID.zod }))
-   ```
-
-2. In the namespace file, re-export from the errors file to maintain backward
-   compatibility:
-
-   ```ts
-   // provider/provider.ts — inside the namespace
-   export { ModelNotFoundError, InitError } from "./errors"
-   ```
-
-3. Update `cli/error.ts` (and any other light consumers) to import directly:
-
-   ```ts
-   // BEFORE
-   import { Provider } from "../provider/provider"
-   Provider.ModelNotFoundError.isInstance(input)
-
-   // AFTER
-   import { ModelNotFoundError as ProviderModelNotFoundError } from "../provider/errors"
-   ProviderModelNotFoundError.isInstance(input)
-   ```
-
-**Files to split (Phase 0):**
-
-| Current file            | New errors file                 | Errors to extract                                                                                                       |
-| ----------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
-| `provider/provider.ts`  | `provider/errors.ts`            | ModelNotFoundError, InitError                                                                                           |
-| `provider/auth.ts`      | `provider/auth-errors.ts`       | OauthMissing, OauthCodeMissing, OauthCallbackFailed, ValidationFailed                                                   |
-| `config/config.ts`      | (already has `config/paths.ts`) | ConfigDirectoryTypoError → move to paths.ts                                                                             |
-| `config/markdown.ts`    | `config/markdown-errors.ts`     | FrontmatterError                                                                                                        |
-| `mcp/index.ts`          | `mcp/errors.ts`                 | Failed                                                                                                                  |
-| `session/message-v2.ts` | `session/message-errors.ts`     | OutputLengthError, AbortedError, StructuredOutputError, AuthError, APIError, ContextOverflowError                       |
-| `session/message.ts`    | (shares with message-v2)        | OutputLengthError, AuthError                                                                                            |
-| `cli/ui.ts`             | `cli/ui-errors.ts`              | CancelledError                                                                                                          |
-| `skill/index.ts`        | `skill/errors.ts`               | InvalidError, NameMismatchError                                                                                         |
-| `worktree/index.ts`     | `worktree/errors.ts`            | NotGitError, NameGenerationFailedError, CreateFailedError, StartCommandFailedError, RemoveFailedError, ResetFailedError |
-| `storage/storage.ts`    | `storage/errors.ts`             | NotFoundError                                                                                                           |
-| `npm/index.ts`          | `npm/errors.ts`                 | InstallFailedError                                                                                                      |
-| `ide/index.ts`          | `ide/errors.ts`                 | AlreadyInstalledError, InstallFailedError                                                                               |
-| `lsp/client.ts`         | `lsp/errors.ts`                 | InitializeError                                                                                                         |
-
-### Phase 1: The real migration — `export namespace` → `export * as`
-
-This is the phase that actually fixes tree-shaking. For each module:
-
-1. **Unwrap** the `export namespace Foo { ... }` — remove the namespace wrapper,
-   keep all the members as top-level `export const` / `export function` / etc.
-2. **Rename** the file if it's currently `index.ts` (e.g. `bus/index.ts` →
-   `bus/bus.ts`), so the barrel can take `index.ts`.
-3. **Create the barrel** `index.ts` with one line: `export * as Foo from "./foo"`
-
-The file structure change for a module that's currently a single file:
+The self-reexport gives us tree-shaking + autocomplete + no barrels.
 
-```
-# BEFORE
-provider/
-  provider.ts        ← 1709-line file with `export namespace Provider { ... }`
-
-# AFTER
-provider/
-  index.ts           ← NEW: `export * as Provider from "./provider"`
-  provider.ts        ← SAME file, same name, just unwrap the namespace
-```
+### Bundle overhead
 
-And the code change is purely removing the wrapper:
-
-```ts
-// BEFORE: provider/provider.ts
-export namespace Provider {
-  export class Service extends Context.Service<...>()("@opencode/Provider") {}
-  export const layer = Layer.effect(Service, ...)
-  export const ModelNotFoundError = NamedError.create(...)
-  export function parseModel(model: string) { ... }
-}
-
-// AFTER: provider/provider.ts — identical exports, no namespace keyword
-export class Service extends Context.Service<...>()("@opencode/Provider") {}
-export const layer = Layer.effect(Service, ...)
-export const ModelNotFoundError = NamedError.create(...)
-export function parseModel(model: string) { ... }
-```
-
-```ts
-// NEW: provider/index.ts
-export * as Provider from "./provider"
-```
-
-Consumer code barely changes — import path gets shorter:
-
-```ts
-// BEFORE
-import { Provider } from "../provider/provider"
+The self-reexport adds ~240 bytes per module (an `Object.defineProperty`
+wrapper). At 100 modules that's ~24KB — irrelevant for a CLI binary.
 
-// AFTER — resolves to provider/index.ts, same Provider object
-import { Provider } from "../provider"
-```
+### The `Foo.Foo.Foo` thing
 
-All access like `Provider.ModelNotFoundError`, `Provider.Service`,
-`Provider.layer` works exactly as before. The difference is invisible to
-consumers but lets the bundler see inside the namespace.
+`Config.Config.Config.load()` compiles and runs. It's a harmless side effect
+of self-referential modules. Nobody would write it.
 
-**Once this is done, you don't need to break anything into subfiles for
-tree-shaking.** The bundler traces that `Provider.ModelNotFoundError` only
-depends on `NamedError` + `zod` + the schema file, and drops
-`Provider.layer` + all 20 AI SDK imports when they're unused. This works because
-`export * as` gives the bundler a static export list it can do inner-graph
-analysis on — it knows which exports reference which imports.
+## Why barrel files don't work
 
-**Order of conversion** (by risk / size, do small modules first):
+Barrel files (`index.ts` with `export * as`) have two problems:
 
-1. Tiny utilities: `Archive`, `Color`, `Token`, `Rpc`, `LocalContext` (~7-66 lines each)
-2. Small services: `Auth`, `Env`, `BusEvent`, `SessionStatus`, `SessionRunState`, `Editor`, `Selection` (~25-91 lines)
-3. Medium services: `Bus`, `Format`, `FileTime`, `FileWatcher`, `Command`, `Question`, `Permission`, `Vcs`, `Project`
-4. Large services: `Config`, `Provider`, `MCP`, `Session`, `SessionProcessor`, `SessionPrompt`, `ACP`
+1. **Bun loads all re-exported modules** when you import through a barrel,
+   even if you only use one. This happens at both runtime and bundle time
+   for modules with side effects (which ours have — top-level imports).
 
-### Phase 2: Build configuration
+2. **Circular import risk.** Sibling files can't import through their own
+   barrel, and cross-directory barrel cycles cause runtime `ReferenceError`.
 
-After the module structure supports tree-shaking:
+## The migration
 
-1. Add `"sideEffects": []` to `packages/opencode/package.json` (or
-   `"sideEffects": false`) — this is safe because our services use explicit
-   layer composition, not import-time side effects.
-2. Verify Bun's bundler respects the new structure. If Bun's tree-shaking is
-   insufficient, evaluate whether the compiled binary path needs an esbuild
-   pre-pass.
-3. Consider adding `/*#__PURE__*/` annotations to `NamedError.create(...)` calls
-   — these are factory functions that return classes, and bundlers may not know
-   they're side-effect-free without the annotation.
+There are two tasks:
 
-## Automation
+### Task 1: Convert remaining `export namespace` files (~50)
 
-The transformation is scripted. From `packages/opencode`:
+For each file:
 
-```bash
-bun script/unwrap-namespace.ts <file> [--dry-run]
-```
+1. Remove the `export namespace Foo {` wrapper and closing `}`
+2. Dedent the body
+3. Add `export * as Foo from "./file"` at the bottom
+4. Rewrite consumer imports: `import { Foo } from "..."` stays the same
+   if the path already points at the file. If it points at a barrel,
+   change it to point at the file directly.
 
-The script uses ast-grep for accurate AST-based namespace boundary detection
-(no false matches from braces in strings/templates/comments), then:
+### Task 2: Fix already-converted files (~32 barrel dirs)
 
-1. Removes the `export namespace Foo {` line and its closing `}`
-2. Dedents the body by one indent level (2 spaces)
-3. If the file is `index.ts`, renames it to `<name>.ts` and creates a new
-   `index.ts` barrel
-4. If the file is NOT `index.ts`, rewrites it in place and creates `index.ts`
-5. Prints the exact commands to find and rewrite import paths
+These were converted in the earlier barrel-based migration. Each directory
+has an `index.ts` barrel and flat-exported source files. To migrate:
 
-### Walkthrough: converting a module
+1. Add `export * as Foo from "./file"` to the bottom of each source file
+2. Change consumers from `import { Foo } from "../dir"` (barrel) to
+   `import { Foo } from "../dir/file"` (direct)
+3. The barrel `index.ts` can be deleted or left in place (harmless once
+   nothing imports through it)
 
-Using `Provider` as an example:
+### Automation
 
 ```bash
-# 1. Preview what will change
-bun script/unwrap-namespace.ts src/provider/provider.ts --dry-run
-
-# 2. Apply the transformation
-bun script/unwrap-namespace.ts src/provider/provider.ts
-
-# 3. Rewrite import paths (script prints the exact command)
-rg -l 'from.*provider/provider' src/ | xargs sed -i '' 's|provider/provider"|provider"|g'
+# Convert an unconverted namespace file:
+bun script/unwrap-namespace.ts src/session/session.ts --dry-run
+bun script/unwrap-namespace.ts src/session/session.ts
 
-# 4. Verify
-bun typecheck
-bun run test
+# Retrofit an already-converted file (add self-reexport + fix consumers):
+bun script/unwrap-namespace.ts src/config/config.ts --retrofit --dry-run
+bun script/unwrap-namespace.ts src/config/config.ts --retrofit
 ```
 
-**What changes on disk:**
+The script handles both cases:
 
-```
-# BEFORE
-provider/
-  provider.ts        ← 1709 lines, `export namespace Provider { ... }`
-
-# AFTER
-provider/
-  index.ts           ← NEW: `export * as Provider from "./provider"`
-  provider.ts        ← same file, namespace unwrapped to flat exports
-```
+- **Default mode**: unwraps namespace + adds self-reexport + rewrites imports
+- **Retrofit mode** (`--retrofit`): file already has flat exports, just adds
+  the self-reexport line and rewrites consumers from barrel to direct
 
-**What changes in consumer code:**
+### Verification
 
-```ts
-// BEFORE
-import { Provider } from "../provider/provider"
+After any conversion:
 
-// AFTER — shorter path, same Provider object
-import { Provider } from "../provider"
+```bash
+bunx --bun tsgo --noEmit                                # typecheck
+bun run --conditions=browser ./src/index.ts generate    # circular import check
 ```
 
-All property access (`Provider.Service`, `Provider.ModelNotFoundError`, etc.)
-stays identical.
-
-### Two cases the script handles
-
-**Case A: file is NOT `index.ts`** (e.g. `provider/provider.ts`)
-
-- Rewrites the file in place (unwrap + dedent)
-- Creates `provider/index.ts` as the barrel
-- Import paths change: `"../provider/provider"` → `"../provider"`
-
-**Case B: file IS `index.ts`** (e.g. `bus/index.ts`)
-
-- Renames `index.ts` → `bus.ts` (kebab-case of namespace name)
-- Creates new `index.ts` as the barrel
-- **No import rewrites needed** — `"@/bus"` already resolves to `bus/index.ts`
-
-## Do I need to split errors/schemas into subfiles?
-
-**No.** Once you do the `export * as` conversion, the bundler can tree-shake
-individual exports within the file. If `cli/error.ts` only accesses
-`Provider.ModelNotFoundError`, the bundler traces that `ModelNotFoundError`
-doesn't reference `createAnthropic` and drops the AI SDK imports.
-
-Splitting into subfiles (errors.ts, schema.ts) is still a fine idea for **code
-organization** — smaller files are easier to read and review. But it's not
-required for tree-shaking. The `export * as` conversion alone is sufficient.
-
-The one case where subfile splitting provides extra tree-shake value is if an
-imported package has module-level side effects that the bundler can't prove are
-unused. In practice this is rare — most npm packages are side-effect-free — and
-adding `"sideEffects": []` to package.json handles the common cases.
-
-## Scope
-
-| Metric                                          | Count           |
-| ----------------------------------------------- | --------------- |
-| Files with `export namespace`                   | 106             |
-| Total namespace declarations                    | 118 (12 nested) |
-| Files with `NamedError.create` inside namespace | 15              |
-| Total error classes to extract                  | ~30             |
-| Files using `export * as` today                 | 0               |
-
-Phase 1 (the `export * as` conversion) is the main change. It's mechanical and
-LLM-friendly but touches every import site, so it should be done module by
-module with type-checking between each step. Each module is an independent PR.
-
 ## Rules for new code
 
-Going forward:
-
-- **No new `export namespace`**. Use a file with flat named exports and
-  `export * as` in the barrel.
-- Keep the service, layer, errors, schemas, and runtime wiring together in one
-  file if you want — that's fine now. The `export * as` barrel makes everything
-  individually shakeable regardless of file structure.
-- If a file grows large enough that it's hard to navigate, split by concern
-  (errors.ts, schema.ts, etc.) for readability. Not for tree-shaking — the
-  bundler handles that.
-
-## Circular import rules
-
-Barrel files (`index.ts` with `export * as`) introduce circular import risks.
-These cause `ReferenceError: Cannot access 'X' before initialization` at
-runtime — not caught by the type checker.
+- **No `export namespace`.** Use flat named exports.
+- **No barrel `index.ts` for internal code.**
+- **Every module file gets a self-reexport** at the bottom:
+  `export * as Foo from "./foo"`
+- **Consumers import the namespace by name:**
+  `import { Foo } from "../path/to/foo"`
 
-### Rule 1: Sibling files never import through their own barrel
+## Remaining work
 
-Files in the same directory must import directly from the source file, never
-through `"."` or `"@/<own-dir>"`:
+### Unconverted (~50 namespaces):
 
-```ts
-// BAD — circular: index.ts re-exports both files, so A → index → B → index → A
-import { Sibling } from "."
-
-// GOOD — direct, no cycle
-import * as Sibling from "./sibling"
-```
+**Session directory (14)** — deep cross-directory cycles currently via barrel:
 
-### Rule 2: Cross-directory imports must not form cycles through barrels
+- SessionRunState, SystemPrompt, Message, SessionRetry, SessionProcessor,
+  SessionRevert, Instruction, SessionSummary, Todo, LLM, SessionStatus,
+  SessionCompaction, SessionPrompt, MessageV2
 
-If `src/lsp/lsp.ts` imports `Config` from `"../config"`, and
-`src/config/config.ts` imports `LSPServer` from `"../lsp"`, that's a cycle:
+**Special cases:**
 
-```
-lsp/lsp.ts → config/index.ts → config/config.ts → lsp/index.ts → lsp/lsp.ts 💥
-```
+- `flag/flag.ts` — uses `Object.defineProperty(Flag, ...)`, needs restructuring
+- `account/repo.ts` — ast-grep fails, needs manual conversion
+- `v2/` (multi-namespace files) — SessionEvent (5 nested), etc.
 
-Fix by importing the specific file, breaking the cycle:
+**Other standalone modules** (~30 across server/, cli/, plugin/, etc.)
 
-```ts
-// In config/config.ts — import directly, not through the lsp barrel
-import * as LSPServer from "../lsp/server"
-```
-
-### Why the type checker doesn't catch this
-
-TypeScript resolves types lazily — it doesn't evaluate module-scope
-expressions. The `ReferenceError` only happens at runtime when a module-scope
-`const` or function call accesses a value from a circular dependency that
-hasn't finished initializing. The SDK build step (`bun run --conditions=browser
-./src/index.ts generate`) is the reliable way to catch these because it
-evaluates all modules eagerly.
-
-### How to verify
-
-After any namespace conversion, run:
-
-```bash
-cd packages/opencode
-bun run --conditions=browser ./src/index.ts generate
-```
+### Already converted (32 barrel dirs) — need retrofit:
 
-If this completes without `ReferenceError`, the module graph is safe.
+config, provider, bus, mcp, effect, util, file, tool, storage, lsp,
+project, plugin, permission, skill, auth, env, worktree, ide, snapshot,
+installation, pty, share, cli/cmd/tui/util, plugin/github-copilot, etc.

+ 1 - 1
packages/opencode/src/acp/agent.ts

@@ -43,7 +43,7 @@ import { Agent as AgentModule } from "../agent/agent"
 import { AppRuntime } from "@/effect/app-runtime"
 import { Installation } from "@/installation"
 import { MessageV2 } from "@/session/message-v2"
-import { Config } from "@/config"
+import { Config } from "@/config/config"
 import { Todo } from "@/session/todo"
 import { z } from "zod"
 import { LoadAPIKeyError } from "ai"

+ 1 - 1
packages/opencode/src/agent/agent.ts

@@ -1,4 +1,4 @@
-import { Config } from "../config"
+import { Config } from "@/config/config"
 import z from "zod"
 import { Provider } from "../provider"
 import { ModelID, ProviderID } from "../provider/schema"

+ 1 - 1
packages/opencode/src/cli/cmd/debug/config.ts

@@ -1,5 +1,5 @@
 import { EOL } from "os"
-import { Config } from "../../../config"
+import { Config } from "@/config/config"
 import { AppRuntime } from "@/effect/app-runtime"
 import { bootstrap } from "../../bootstrap"
 import { cmd } from "../cmd"

+ 1 - 1
packages/opencode/src/cli/cmd/mcp.ts

@@ -7,7 +7,7 @@ import { UI } from "../ui"
 import { MCP } from "../../mcp"
 import { McpAuth } from "../../mcp/auth"
 import { McpOAuthProvider } from "../../mcp/oauth-provider"
-import { Config } from "../../config"
+import { Config } from "@/config/config"
 import { Instance } from "../../project/instance"
 import { Installation } from "../../installation"
 import { InstallationVersion } from "../../installation/version"

+ 1 - 1
packages/opencode/src/cli/cmd/providers.ts

@@ -7,7 +7,7 @@ import { ModelsDev } from "../../provider"
 import { map, pipe, sortBy, values } from "remeda"
 import path from "path"
 import os from "os"
-import { Config } from "../../config"
+import { Config } from "@/config/config"
 import { Global } from "../../global"
 import { Plugin } from "../../plugin"
 import { Instance } from "../../project/instance"

+ 1 - 1
packages/opencode/src/cli/cmd/tui/worker.ts

@@ -5,7 +5,7 @@ import { Instance } from "@/project/instance"
 import { InstanceBootstrap } from "@/project/bootstrap"
 import { Rpc } from "@/util"
 import { upgrade } from "@/cli/upgrade"
-import { Config } from "@/config"
+import { Config } from "@/config/config"
 import { GlobalBus } from "@/bus/global"
 import { Flag } from "@/flag/flag"
 import { writeHeapSnapshot } from "node:v8"

+ 1 - 1
packages/opencode/src/cli/network.ts

@@ -1,5 +1,5 @@
 import type { Argv, InferredOptionTypes } from "yargs"
-import { Config } from "../config"
+import { Config } from "@/config/config"
 import { AppRuntime } from "@/effect/app-runtime"
 
 const options = {

+ 1 - 1
packages/opencode/src/cli/upgrade.ts

@@ -1,5 +1,5 @@
 import { Bus } from "@/bus"
-import { Config } from "@/config"
+import { Config } from "@/config/config"
 import { AppRuntime } from "@/effect/app-runtime"
 import { Flag } from "@/flag/flag"
 import { Installation } from "@/installation"

+ 1 - 1
packages/opencode/src/command/command.ts

@@ -5,7 +5,7 @@ import type { InstanceContext } from "@/project/instance"
 import { SessionID, MessageID } from "@/session/schema"
 import { Effect, Layer, Context } from "effect"
 import z from "zod"
-import { Config } from "../config"
+import { Config } from "@/config/config"
 import { MCP } from "../mcp"
 import { Skill } from "../skill"
 import PROMPT_INITIALIZE from "./template/initialize.txt"

+ 1 - 0
packages/opencode/src/config/config.ts

@@ -1583,3 +1583,4 @@ export const defaultLayer = layer.pipe(
   Layer.provide(Account.defaultLayer),
   Layer.provide(Npm.defaultLayer),
 )
+export * as Config from "./config"

+ 1 - 1
packages/opencode/src/effect/app-runtime.ts

@@ -6,7 +6,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 import { Bus } from "@/bus"
 import { Auth } from "@/auth"
 import { Account } from "@/account"
-import { Config } from "@/config"
+import { Config } from "@/config/config"
 import { Git } from "@/git"
 import { Ripgrep } from "@/file/ripgrep"
 import { FileTime } from "@/file/time"

+ 103 - 104
packages/opencode/src/file/time.ts

@@ -5,109 +5,108 @@ import { Flag } from "@/flag/flag"
 import type { SessionID } from "@/session/schema"
 import { Log } from "../util"
 
-export namespace FileTime {
-  const log = Log.create({ service: "file.time" })
-
-  export type Stamp = {
-    readonly read: Date
-    readonly mtime: number | undefined
-    readonly size: number | undefined
-  }
-
-  const session = (reads: Map<SessionID, Map<string, Stamp>>, sessionID: SessionID) => {
-    const value = reads.get(sessionID)
-    if (value) return value
-
-    const next = new Map<string, Stamp>()
-    reads.set(sessionID, next)
-    return next
-  }
-
-  interface State {
-    reads: Map<SessionID, Map<string, Stamp>>
-    locks: Map<string, Semaphore.Semaphore>
-  }
-
-  export interface Interface {
-    readonly read: (sessionID: SessionID, file: string) => Effect.Effect<void>
-    readonly get: (sessionID: SessionID, file: string) => Effect.Effect<Date | undefined>
-    readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect<void>
-    readonly withLock: <T>(filepath: string, fn: () => Effect.Effect<T>) => Effect.Effect<T>
-  }
-
-  export class Service extends Context.Service<Service, Interface>()("@opencode/FileTime") {}
-
-  export const layer = Layer.effect(
-    Service,
-    Effect.gen(function* () {
-      const fsys = yield* AppFileSystem.Service
-      const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK
-
-      const stamp = Effect.fnUntraced(function* (file: string) {
-        const info = yield* fsys.stat(file).pipe(Effect.catch(() => Effect.void))
-        return {
-          read: yield* DateTime.nowAsDate,
-          mtime: info ? Option.getOrUndefined(info.mtime)?.getTime() : undefined,
-          size: info ? Number(info.size) : undefined,
-        }
-      })
-      const state = yield* InstanceState.make<State>(
-        Effect.fn("FileTime.state")(() =>
-          Effect.succeed({
-            reads: new Map<SessionID, Map<string, Stamp>>(),
-            locks: new Map<string, Semaphore.Semaphore>(),
-          }),
-        ),
-      )
+const log = Log.create({ service: "file.time" })
+
+export type Stamp = {
+  readonly read: Date
+  readonly mtime: number | undefined
+  readonly size: number | undefined
+}
+
+const session = (reads: Map<SessionID, Map<string, Stamp>>, sessionID: SessionID) => {
+  const value = reads.get(sessionID)
+  if (value) return value
 
-      const getLock = Effect.fn("FileTime.lock")(function* (filepath: string) {
-        filepath = AppFileSystem.normalizePath(filepath)
-        const locks = (yield* InstanceState.get(state)).locks
-        const lock = locks.get(filepath)
-        if (lock) return lock
-
-        const next = Semaphore.makeUnsafe(1)
-        locks.set(filepath, next)
-        return next
-      })
-
-      const read = Effect.fn("FileTime.read")(function* (sessionID: SessionID, file: string) {
-        file = AppFileSystem.normalizePath(file)
-        const reads = (yield* InstanceState.get(state)).reads
-        log.info("read", { sessionID, file })
-        session(reads, sessionID).set(file, yield* stamp(file))
-      })
-
-      const get = Effect.fn("FileTime.get")(function* (sessionID: SessionID, file: string) {
-        file = AppFileSystem.normalizePath(file)
-        const reads = (yield* InstanceState.get(state)).reads
-        return reads.get(sessionID)?.get(file)?.read
-      })
-
-      const assert = Effect.fn("FileTime.assert")(function* (sessionID: SessionID, filepath: string) {
-        if (disableCheck) return
-        filepath = AppFileSystem.normalizePath(filepath)
-
-        const reads = (yield* InstanceState.get(state)).reads
-        const time = reads.get(sessionID)?.get(filepath)
-        if (!time) throw new Error(`You must read file ${filepath} before overwriting it. Use the Read tool first`)
-
-        const next = yield* stamp(filepath)
-        const changed = next.mtime !== time.mtime || next.size !== time.size
-        if (!changed) return
-
-        throw new Error(
-          `File ${filepath} has been modified since it was last read.\nLast modification: ${new Date(next.mtime ?? next.read.getTime()).toISOString()}\nLast read: ${time.read.toISOString()}\n\nPlease read the file again before modifying it.`,
-        )
-      })
-
-      const withLock = Effect.fn("FileTime.withLock")(function* <T>(filepath: string, fn: () => Effect.Effect<T>) {
-        return yield* fn().pipe((yield* getLock(filepath)).withPermits(1))
-      })
-
-      return Service.of({ read, get, assert, withLock })
-    }),
-  ).pipe(Layer.orDie)
-
-  export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
+  const next = new Map<string, Stamp>()
+  reads.set(sessionID, next)
+  return next
 }
+
+interface State {
+  reads: Map<SessionID, Map<string, Stamp>>
+  locks: Map<string, Semaphore.Semaphore>
+}
+
+export interface Interface {
+  readonly read: (sessionID: SessionID, file: string) => Effect.Effect<void>
+  readonly get: (sessionID: SessionID, file: string) => Effect.Effect<Date | undefined>
+  readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect<void>
+  readonly withLock: <T>(filepath: string, fn: () => Effect.Effect<T>) => Effect.Effect<T>
+}
+
+export class Service extends Context.Service<Service, Interface>()("@opencode/FileTime") {}
+
+export const layer = Layer.effect(
+  Service,
+  Effect.gen(function* () {
+    const fsys = yield* AppFileSystem.Service
+    const disableCheck = yield* Flag.OPENCODE_DISABLE_FILETIME_CHECK
+
+    const stamp = Effect.fnUntraced(function* (file: string) {
+      const info = yield* fsys.stat(file).pipe(Effect.catch(() => Effect.void))
+      return {
+        read: yield* DateTime.nowAsDate,
+        mtime: info ? Option.getOrUndefined(info.mtime)?.getTime() : undefined,
+        size: info ? Number(info.size) : undefined,
+      }
+    })
+    const state = yield* InstanceState.make<State>(
+      Effect.fn("FileTime.state")(() =>
+        Effect.succeed({
+          reads: new Map<SessionID, Map<string, Stamp>>(),
+          locks: new Map<string, Semaphore.Semaphore>(),
+        }),
+      ),
+    )
+
+    const getLock = Effect.fn("FileTime.lock")(function* (filepath: string) {
+      filepath = AppFileSystem.normalizePath(filepath)
+      const locks = (yield* InstanceState.get(state)).locks
+      const lock = locks.get(filepath)
+      if (lock) return lock
+
+      const next = Semaphore.makeUnsafe(1)
+      locks.set(filepath, next)
+      return next
+    })
+
+    const read = Effect.fn("FileTime.read")(function* (sessionID: SessionID, file: string) {
+      file = AppFileSystem.normalizePath(file)
+      const reads = (yield* InstanceState.get(state)).reads
+      log.info("read", { sessionID, file })
+      session(reads, sessionID).set(file, yield* stamp(file))
+    })
+
+    const get = Effect.fn("FileTime.get")(function* (sessionID: SessionID, file: string) {
+      file = AppFileSystem.normalizePath(file)
+      const reads = (yield* InstanceState.get(state)).reads
+      return reads.get(sessionID)?.get(file)?.read
+    })
+
+    const assert = Effect.fn("FileTime.assert")(function* (sessionID: SessionID, filepath: string) {
+      if (disableCheck) return
+      filepath = AppFileSystem.normalizePath(filepath)
+
+      const reads = (yield* InstanceState.get(state)).reads
+      const time = reads.get(sessionID)?.get(filepath)
+      if (!time) throw new Error(`You must read file ${filepath} before overwriting it. Use the Read tool first`)
+
+      const next = yield* stamp(filepath)
+      const changed = next.mtime !== time.mtime || next.size !== time.size
+      if (!changed) return
+
+      throw new Error(
+        `File ${filepath} has been modified since it was last read.\nLast modification: ${new Date(next.mtime ?? next.read.getTime()).toISOString()}\nLast read: ${time.read.toISOString()}\n\nPlease read the file again before modifying it.`,
+      )
+    })
+
+    const withLock = Effect.fn("FileTime.withLock")(function* <T>(filepath: string, fn: () => Effect.Effect<T>) {
+      return yield* fn().pipe((yield* getLock(filepath)).withPermits(1))
+    })
+
+    return Service.of({ read, get, assert, withLock })
+  }),
+).pipe(Layer.orDie)
+
+export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
+export * as FileTime from "./time"

+ 1 - 1
packages/opencode/src/file/watcher.ts

@@ -12,7 +12,7 @@ import { Flag } from "@/flag/flag"
 import { Git } from "@/git"
 import { Instance } from "@/project/instance"
 import { lazy } from "@/util/lazy"
-import { Config } from "../config"
+import { Config } from "@/config/config"
 import { FileIgnore } from "./ignore"
 import { Protected } from "./protected"
 import { Log } from "../util"

+ 1 - 1
packages/opencode/src/format/format.ts

@@ -5,7 +5,7 @@ import { InstanceState } from "@/effect"
 import path from "path"
 import { mergeDeep } from "remeda"
 import z from "zod"
-import { Config } from "../config"
+import { Config } from "@/config/config"
 import { Log } from "../util"
 import * as Formatter from "./formatter"
 

+ 1 - 1
packages/opencode/src/lsp/lsp.ts

@@ -6,7 +6,7 @@ import path from "path"
 import { pathToFileURL, fileURLToPath } from "url"
 import * as LSPServer from "./server"
 import z from "zod"
-import { Config } from "../config"
+import { Config } from "@/config/config"
 import { Instance } from "../project/instance"
 import { Flag } from "@/flag/flag"
 import { Process } from "../util"

+ 1 - 1
packages/opencode/src/mcp/mcp.ts

@@ -9,7 +9,7 @@ import {
   type Tool as MCPToolDef,
   ToolListChangedNotificationSchema,
 } from "@modelcontextprotocol/sdk/types.js"
-import { Config } from "../config"
+import { Config } from "@/config/config"
 import { Log } from "../util"
 import { NamedError } from "@opencode-ai/shared/util/error"
 import z from "zod/v4"

+ 1 - 1
packages/opencode/src/permission/permission.ts

@@ -1,6 +1,6 @@
 import { Bus } from "@/bus"
 import { BusEvent } from "@/bus/bus-event"
-import { Config } from "@/config"
+import { Config } from "@/config/config"
 import { InstanceState } from "@/effect"
 import { ProjectID } from "@/project/schema"
 import { MessageID, SessionID } from "@/session/schema"

+ 1 - 1
packages/opencode/src/plugin/plugin.ts

@@ -5,7 +5,7 @@ import type {
   PluginModule,
   WorkspaceAdaptor as PluginWorkspaceAdaptor,
 } from "@opencode-ai/plugin"
-import { Config } from "../config"
+import { Config } from "@/config/config"
 import { Bus } from "../bus"
 import { Log } from "../util"
 import { createOpencodeClient } from "@opencode-ai/sdk"

+ 1 - 1
packages/opencode/src/provider/provider.ts

@@ -1,7 +1,7 @@
 import z from "zod"
 import os from "os"
 import fuzzysort from "fuzzysort"
-import { Config } from "../config"
+import { Config } from "@/config/config"
 import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda"
 import { NoSuchModelError, type Provider as SDK } from "ai"
 import { Log } from "../util"

+ 1 - 1
packages/opencode/src/server/instance/config.ts

@@ -1,7 +1,7 @@
 import { Hono } from "hono"
 import { describeRoute, validator, resolver } from "hono-openapi"
 import z from "zod"
-import { Config } from "../../config"
+import { Config } from "@/config/config"
 import { Provider } from "../../provider"
 import { mapValues } from "remeda"
 import { errors } from "../error"

+ 1 - 1
packages/opencode/src/server/instance/experimental.ts

@@ -8,7 +8,7 @@ import { Instance } from "../../project/instance"
 import { Project } from "../../project"
 import { MCP } from "../../mcp"
 import { Session } from "../../session"
-import { Config } from "../../config"
+import { Config } from "@/config/config"
 import { ConsoleState } from "../../config/console-state"
 import { Account, AccountID, OrgID } from "../../account"
 import { AppRuntime } from "../../effect/app-runtime"

+ 1 - 1
packages/opencode/src/server/instance/global.ts

@@ -13,7 +13,7 @@ import { Installation } from "@/installation"
 import { InstallationVersion } from "@/installation/version"
 import { Log } from "../../util"
 import { lazy } from "../../util/lazy"
-import { Config } from "../../config"
+import { Config } from "@/config/config"
 import { errors } from "../error"
 
 const log = Log.create({ service: "server" })

+ 1 - 1
packages/opencode/src/server/instance/mcp.ts

@@ -2,7 +2,7 @@ import { Hono } from "hono"
 import { describeRoute, validator, resolver } from "hono-openapi"
 import z from "zod"
 import { MCP } from "../../mcp"
-import { Config } from "../../config"
+import { Config } from "@/config/config"
 import { AppRuntime } from "../../effect/app-runtime"
 import { errors } from "../error"
 import { lazy } from "../../util/lazy"

+ 1 - 1
packages/opencode/src/server/instance/provider.ts

@@ -1,7 +1,7 @@
 import { Hono } from "hono"
 import { describeRoute, validator, resolver } from "hono-openapi"
 import z from "zod"
-import { Config } from "../../config"
+import { Config } from "@/config/config"
 import { Provider } from "../../provider"
 import { ModelsDev } from "../../provider"
 import { ProviderAuth } from "../../provider"

+ 1 - 1
packages/opencode/src/session/compaction.ts

@@ -10,7 +10,7 @@ import { Log } from "../util"
 import { SessionProcessor } from "./processor"
 import { Agent } from "@/agent/agent"
 import { Plugin } from "@/plugin"
-import { Config } from "@/config"
+import { Config } from "@/config/config"
 import { NotFoundError } from "@/storage"
 import { ModelID, ProviderID } from "@/provider/schema"
 import { Effect, Layer, Context } from "effect"

+ 1 - 1
packages/opencode/src/session/instruction.ts

@@ -2,7 +2,7 @@ import os from "os"
 import path from "path"
 import { Effect, Layer, Context } from "effect"
 import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
-import { Config } from "@/config"
+import { Config } from "@/config/config"
 import { InstanceState } from "@/effect"
 import { Flag } from "@/flag/flag"
 import { AppFileSystem } from "@opencode-ai/shared/filesystem"

+ 1 - 1
packages/opencode/src/session/llm.ts

@@ -6,7 +6,7 @@ import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, json
 import { mergeDeep, pipe } from "remeda"
 import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider"
 import { ProviderTransform } from "@/provider"
-import { Config } from "@/config"
+import { Config } from "@/config/config"
 import { Instance } from "@/project/instance"
 import type { Agent } from "@/agent/agent"
 import type { MessageV2 } from "./message-v2"

+ 1 - 1
packages/opencode/src/session/overflow.ts

@@ -1,4 +1,4 @@
-import type { Config } from "@/config"
+import type { Config } from "@/config/config"
 import type { Provider } from "@/provider"
 import { ProviderTransform } from "@/provider"
 import type { MessageV2 } from "./message-v2"

+ 1 - 1
packages/opencode/src/session/processor.ts

@@ -2,7 +2,7 @@ import { Cause, Deferred, Effect, Layer, Context, Scope } from "effect"
 import * as Stream from "effect/Stream"
 import { Agent } from "@/agent/agent"
 import { Bus } from "@/bus"
-import { Config } from "@/config"
+import { Config } from "@/config/config"
 import { Permission } from "@/permission"
 import { Plugin } from "@/plugin"
 import { Snapshot } from "@/snapshot"

+ 1 - 1
packages/opencode/src/share/session.ts

@@ -2,7 +2,7 @@ import { Session } from "@/session"
 import { SessionID } from "@/session/schema"
 import { SyncEvent } from "@/sync"
 import { Effect, Layer, Scope, Context } from "effect"
-import { Config } from "../config"
+import { Config } from "@/config/config"
 import { Flag } from "../flag/flag"
 import * as ShareNext from "./share-next"
 

+ 1 - 1
packages/opencode/src/share/share-next.ts

@@ -10,7 +10,7 @@ import { Session } from "@/session"
 import { MessageV2 } from "@/session/message-v2"
 import type { SessionID } from "@/session/schema"
 import { Database, eq } from "@/storage"
-import { Config } from "@/config"
+import { Config } from "@/config/config"
 import { Log } from "@/util"
 import { SessionShareTable } from "./share.sql"
 

+ 1 - 1
packages/opencode/src/skill/skill.ts

@@ -11,7 +11,7 @@ import { Flag } from "@/flag/flag"
 import { Global } from "@/global"
 import { Permission } from "@/permission"
 import { AppFileSystem } from "@opencode-ai/shared/filesystem"
-import { Config } from "../config"
+import { Config } from "@/config/config"
 import { ConfigMarkdown } from "../config"
 import { Glob } from "@opencode-ai/shared/util/glob"
 import { Log } from "../util"

+ 1 - 1
packages/opencode/src/snapshot/snapshot.ts

@@ -7,7 +7,7 @@ import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
 import { InstanceState } from "@/effect"
 import { AppFileSystem } from "@opencode-ai/shared/filesystem"
 import { Hash } from "@opencode-ai/shared/util/hash"
-import { Config } from "../config"
+import { Config } from "@/config/config"
 import { Global } from "../global"
 import { Log } from "../util"
 

+ 1 - 1
packages/opencode/src/tool/registry.ts

@@ -13,7 +13,7 @@ import { WriteTool } from "./write"
 import { InvalidTool } from "./invalid"
 import { SkillTool } from "./skill"
 import * as Tool from "./tool"
-import { Config } from "../config"
+import { Config } from "@/config/config"
 import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin"
 import z from "zod"
 import { Plugin } from "../plugin"

+ 1 - 1
packages/opencode/src/tool/task.ts

@@ -6,7 +6,7 @@ import { SessionID, MessageID } from "../session/schema"
 import { MessageV2 } from "../session/message-v2"
 import { Agent } from "../agent/agent"
 import type { SessionPrompt } from "../session/prompt"
-import { Config } from "../config"
+import { Config } from "@/config/config"
 import { Effect } from "effect"
 
 export interface TaskPromptOps {

+ 1 - 1
packages/opencode/test/config/agent-color.test.ts

@@ -3,7 +3,7 @@ import { Effect } from "effect"
 import path from "path"
 import { provideInstance, tmpdir } from "../fixture/fixture"
 import { Instance } from "../../src/project/instance"
-import { Config } from "../../src/config"
+import { Config } from "../../src/config/config"
 import { Agent as AgentSvc } from "../../src/agent/agent"
 import { Color } from "../../src/util"
 import { AppRuntime } from "../../src/effect/app-runtime"

+ 1 - 1
packages/opencode/test/config/config.test.ts

@@ -1,7 +1,7 @@
 import { test, expect, describe, mock, afterEach, beforeEach, spyOn } from "bun:test"
 import { Deferred, Effect, Fiber, Layer, Option } from "effect"
 import { NodeFileSystem, NodePath } from "@effect/platform-node"
-import { Config } from "../../src/config"
+import { Config } from "../../src/config/config"
 import { EffectFlock } from "@opencode-ai/shared/util/effect-flock"
 
 import { Instance } from "../../src/project/instance"

+ 1 - 1
packages/opencode/test/config/tui.test.ts

@@ -4,7 +4,7 @@ import fs from "fs/promises"
 import { tmpdir } from "../fixture/fixture"
 import { Instance } from "../../src/project/instance"
 import { TuiConfig } from "../../src/cli/cmd/tui/config/tui"
-import { Config } from "../../src/config"
+import { Config } from "../../src/config/config"
 import { Global } from "../../src/global"
 import { Filesystem } from "../../src/util"
 import { AppRuntime } from "../../src/effect/app-runtime"

+ 1 - 1
packages/opencode/test/file/watcher.test.ts

@@ -5,7 +5,7 @@ import path from "path"
 import { ConfigProvider, Deferred, Effect, Layer, ManagedRuntime, Option } from "effect"
 import { tmpdir } from "../fixture/fixture"
 import { Bus } from "../../src/bus"
-import { Config } from "../../src/config"
+import { Config } from "../../src/config/config"
 import { FileWatcher } from "../../src/file/watcher"
 import { Git } from "../../src/git"
 import { Instance } from "../../src/project/instance"

+ 1 - 1
packages/opencode/test/fixture/fixture.ts

@@ -6,7 +6,7 @@ import { Effect, Context } from "effect"
 import type * as PlatformError from "effect/PlatformError"
 import type * as Scope from "effect/Scope"
 import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
-import type { Config } from "../../src/config"
+import type { Config } from "../../src/config/config"
 import { InstanceRef } from "../../src/effect/instance-ref"
 import { Instance } from "../../src/project/instance"
 import { TestLLMServer } from "../lib/llm-server"

+ 1 - 1
packages/opencode/test/permission-task.test.ts

@@ -1,6 +1,6 @@
 import { afterEach, describe, test, expect } from "bun:test"
 import { Permission } from "../src/permission"
-import { Config } from "../src/config"
+import { Config } from "../src/config/config"
 import { Instance } from "../src/project/instance"
 import { tmpdir } from "./fixture/fixture"
 import { AppRuntime } from "../src/effect/app-runtime"

+ 1 - 1
packages/opencode/test/session/compaction.test.ts

@@ -4,7 +4,7 @@ import { Cause, Effect, Exit, Layer, ManagedRuntime } from "effect"
 import * as Stream from "effect/Stream"
 import z from "zod"
 import { Bus } from "../../src/bus"
-import { Config } from "../../src/config"
+import { Config } from "../../src/config/config"
 import { Agent } from "../../src/agent/agent"
 import { LLM } from "../../src/session/llm"
 import { SessionCompaction } from "../../src/session/compaction"

+ 1 - 1
packages/opencode/test/session/processor-effect.test.ts

@@ -5,7 +5,7 @@ import path from "path"
 import type { Agent } from "../../src/agent/agent"
 import { Agent as AgentSvc } from "../../src/agent/agent"
 import { Bus } from "../../src/bus"
-import { Config } from "../../src/config"
+import { Config } from "../../src/config/config"
 import { Permission } from "../../src/permission"
 import { Plugin } from "../../src/plugin"
 import { Provider } from "../../src/provider"

+ 1 - 1
packages/opencode/test/session/prompt-effect.test.ts

@@ -6,7 +6,7 @@ import path from "path"
 import { Agent as AgentSvc } from "../../src/agent/agent"
 import { Bus } from "../../src/bus"
 import { Command } from "../../src/command"
-import { Config } from "../../src/config"
+import { Config } from "../../src/config/config"
 import { FileTime } from "../../src/file/time"
 import { LSP } from "../../src/lsp"
 import { MCP } from "../../src/mcp"

+ 1 - 1
packages/opencode/test/session/snapshot-tool-race.test.ts

@@ -32,7 +32,7 @@ import { NodeFileSystem } from "@effect/platform-node"
 import { Agent as AgentSvc } from "../../src/agent/agent"
 import { Bus } from "../../src/bus"
 import { Command } from "../../src/command"
-import { Config } from "../../src/config"
+import { Config } from "../../src/config/config"
 import { FileTime } from "../../src/file/time"
 import { LSP } from "../../src/lsp"
 import { MCP } from "../../src/mcp"

+ 1 - 1
packages/opencode/test/share/share-next.test.ts

@@ -8,7 +8,7 @@ import { Account } from "../../src/account"
 import { AccountRepo } from "../../src/account/repo"
 import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
 import { Bus } from "../../src/bus"
-import { Config } from "../../src/config"
+import { Config } from "../../src/config/config"
 import { Provider } from "../../src/provider"
 import { Session } from "../../src/session"
 import type { SessionID } from "../../src/session/schema"

+ 1 - 1
packages/opencode/test/tool/task.test.ts

@@ -1,7 +1,7 @@
 import { afterEach, describe, expect } from "bun:test"
 import { Effect, Layer } from "effect"
 import { Agent } from "../../src/agent/agent"
-import { Config } from "../../src/config"
+import { Config } from "../../src/config/config"
 import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
 import { Instance } from "../../src/project/instance"
 import { Session } from "../../src/session"