mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-25 23:15:18 +00:00
refactor: migrate namespace tooling to self-reexport pattern
This commit is contained in:
parent
7341718f92
commit
95f90ebfc8
50 changed files with 517 additions and 713 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)`)
|
||||
}
|
||||
|
||||
// --- 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)")
|
||||
}
|
||||
|
||||
// --- 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
|
||||
})
|
||||
|
||||
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 (siblingFixCount > 0) {
|
||||
console.log(`Fixed ${siblingFixCount} sibling import(s) in ${path.basename(dir)}/ (./${basename} → .)`)
|
||||
}
|
||||
rewriteConsumers(nsName, implFile, implName, dir)
|
||||
}
|
||||
|
||||
console.log("")
|
||||
console.log("=== Verify ===")
|
||||
console.log("")
|
||||
console.log("bunx --bun tsgo --noEmit # typecheck")
|
||||
console.log("bun run test # run tests")
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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"))
|
||||
|
||||
// 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}")`
|
||||
})
|
||||
|
||||
if (changes > 0) {
|
||||
if (!dryRun) fs.writeFileSync(file, content)
|
||||
changedFiles.push(file)
|
||||
totalChanges += changes
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
// config/config.ts
|
||||
import { Log } from "../util/log"
|
||||
|
||||
// AFTER — resolves to provider/index.ts, same Provider namespace
|
||||
import { Provider } from "../provider"
|
||||
export interface Info { model: string }
|
||||
export function load(): Info { ... }
|
||||
export const JsonError = NamedError.create(...)
|
||||
|
||||
// Self-reexport: creates a named `Config` export that consumers can import
|
||||
export * as Config from "./config"
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
```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
|
||||
|
||||
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)
|
||||
```
|
||||
|
||||
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
|
||||
Consumers import the namespace by name — editors auto-import this like any
|
||||
named export:
|
||||
|
||||
```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
|
||||
import { Config } from "../config/config"
|
||||
Config.load()
|
||||
Config.JsonError.isInstance(x)
|
||||
```
|
||||
|
||||
### 2. Barrel file uses `export * as` (not `export namespace`)
|
||||
## Why this pattern
|
||||
|
||||
```ts
|
||||
// index.ts
|
||||
export * as Effect from "./Effect.ts"
|
||||
export * as Schema from "./Schema.ts"
|
||||
export * as Stream from "./Stream.ts"
|
||||
// ~134 modules
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### 3. `sideEffects: []` and deep imports
|
||||
|
||||
```jsonc
|
||||
// package.json
|
||||
{ "sideEffects": [] }
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
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:
|
||||
We tested every option with Bun. Three things matter: tree-shaking, circular
|
||||
imports, and editor autocomplete.
|
||||
|
||||
```
|
||||
# BEFORE
|
||||
provider/
|
||||
provider.ts ← 1709-line file with `export namespace Provider { ... }`
|
||||
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)
|
||||
|
||||
# AFTER
|
||||
provider/
|
||||
index.ts ← NEW: `export * as Provider from "./provider"`
|
||||
provider.ts ← SAME file, same name, just unwrap the namespace
|
||||
B. import * as Bar from "./bar" (direct, no barrel)
|
||||
Runtime: only bar loaded ✅
|
||||
Bundled: only bar loaded ✅
|
||||
Autocomplete: broken (editors can't auto-import) ❌
|
||||
|
||||
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) ✅
|
||||
```
|
||||
|
||||
And the code change is purely removing the wrapper:
|
||||
The self-reexport gives us tree-shaking + autocomplete + no barrels.
|
||||
|
||||
```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) { ... }
|
||||
}
|
||||
### Bundle overhead
|
||||
|
||||
// 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) { ... }
|
||||
```
|
||||
The self-reexport adds ~240 bytes per module (an `Object.defineProperty`
|
||||
wrapper). At 100 modules that's ~24KB — irrelevant for a CLI binary.
|
||||
|
||||
```ts
|
||||
// NEW: provider/index.ts
|
||||
export * as Provider from "./provider"
|
||||
```
|
||||
### The `Foo.Foo.Foo` thing
|
||||
|
||||
Consumer code barely changes — import path gets shorter:
|
||||
`Config.Config.Config.load()` compiles and runs. It's a harmless side effect
|
||||
of self-referential modules. Nobody would write it.
|
||||
|
||||
```ts
|
||||
// BEFORE
|
||||
import { Provider } from "../provider/provider"
|
||||
## Why barrel files don't work
|
||||
|
||||
// AFTER — resolves to provider/index.ts, same Provider object
|
||||
import { Provider } from "../provider"
|
||||
```
|
||||
Barrel files (`index.ts` with `export * as`) have two problems:
|
||||
|
||||
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.
|
||||
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).
|
||||
|
||||
**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.
|
||||
2. **Circular import risk.** Sibling files can't import through their own
|
||||
barrel, and cross-directory barrel cycles cause runtime `ReferenceError`.
|
||||
|
||||
**Order of conversion** (by risk / size, do small modules first):
|
||||
## The migration
|
||||
|
||||
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`
|
||||
There are two tasks:
|
||||
|
||||
### Phase 2: Build configuration
|
||||
### Task 1: Convert remaining `export namespace` files (~50)
|
||||
|
||||
After the module structure supports tree-shaking:
|
||||
For each file:
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Automation
|
||||
### Task 2: Fix already-converted files (~32 barrel dirs)
|
||||
|
||||
The transformation is scripted. From `packages/opencode`:
|
||||
These were converted in the earlier barrel-based migration. Each directory
|
||||
has an `index.ts` barrel and flat-exported source files. To migrate:
|
||||
|
||||
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)
|
||||
|
||||
### Automation
|
||||
|
||||
```bash
|
||||
bun script/unwrap-namespace.ts <file> [--dry-run]
|
||||
# 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
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
The script uses ast-grep for accurate AST-based namespace boundary detection
|
||||
(no false matches from braces in strings/templates/comments), then:
|
||||
The script handles both cases:
|
||||
|
||||
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
|
||||
- **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
|
||||
|
||||
### Walkthrough: converting a module
|
||||
### Verification
|
||||
|
||||
Using `Provider` as an example:
|
||||
After any conversion:
|
||||
|
||||
```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'
|
||||
|
||||
# 4. Verify
|
||||
bun typecheck
|
||||
bun run test
|
||||
bunx --bun tsgo --noEmit # typecheck
|
||||
bun run --conditions=browser ./src/index.ts generate # circular import check
|
||||
```
|
||||
|
||||
**What changes on disk:**
|
||||
|
||||
```
|
||||
# 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
|
||||
```
|
||||
|
||||
**What changes in consumer code:**
|
||||
|
||||
```ts
|
||||
// BEFORE
|
||||
import { Provider } from "../provider/provider"
|
||||
|
||||
// AFTER — shorter path, same Provider object
|
||||
import { Provider } from "../provider"
|
||||
```
|
||||
|
||||
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 `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"`
|
||||
|
||||
- **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.
|
||||
## Remaining work
|
||||
|
||||
## Circular import rules
|
||||
### Unconverted (~50 namespaces):
|
||||
|
||||
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.
|
||||
**Session directory (14)** — deep cross-directory cycles currently via barrel:
|
||||
|
||||
### Rule 1: Sibling files never import through their own barrel
|
||||
- SessionRunState, SystemPrompt, Message, SessionRetry, SessionProcessor,
|
||||
SessionRevert, Instruction, SessionSummary, Todo, LLM, SessionStatus,
|
||||
SessionCompaction, SessionPrompt, MessageV2
|
||||
|
||||
Files in the same directory must import directly from the source file, never
|
||||
through `"."` or `"@/<own-dir>"`:
|
||||
**Special cases:**
|
||||
|
||||
```ts
|
||||
// BAD — circular: index.ts re-exports both files, so A → index → B → index → A
|
||||
import { Sibling } from "."
|
||||
- `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.
|
||||
|
||||
// GOOD — direct, no cycle
|
||||
import * as Sibling from "./sibling"
|
||||
```
|
||||
**Other standalone modules** (~30 across server/, cli/, plugin/, etc.)
|
||||
|
||||
### Rule 2: Cross-directory imports must not form cycles through barrels
|
||||
### Already converted (32 barrel dirs) — need retrofit:
|
||||
|
||||
If `src/lsp/lsp.ts` imports `Config` from `"../config"`, and
|
||||
`src/config/config.ts` imports `LSPServer` from `"../lsp"`, that's a cycle:
|
||||
|
||||
```
|
||||
lsp/lsp.ts → config/index.ts → config/config.ts → lsp/index.ts → lsp/lsp.ts 💥
|
||||
```
|
||||
|
||||
Fix by importing the specific file, breaking the cycle:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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,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,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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,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,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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1583,3 +1583,4 @@ export const defaultLayer = layer.pipe(
|
|||
Layer.provide(Account.defaultLayer),
|
||||
Layer.provide(Npm.defaultLayer),
|
||||
)
|
||||
export * as Config from "./config"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" })
|
||||
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 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 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 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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,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"
|
||||
|
|
|
|||
|
|
@ -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,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,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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" })
|
||||
|
|
|
|||
|
|
@ -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,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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue