diff --git a/packages/opencode/script/schema.ts b/packages/opencode/script/schema.ts index c0f302f21a..b250da8e19 100755 --- a/packages/opencode/script/schema.ts +++ b/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) { diff --git a/packages/opencode/script/unwrap-namespace.ts b/packages/opencode/script/unwrap-namespace.ts index 45c16f6c73..fb115544ab 100644 --- a/packages/opencode/script/unwrap-namespace.ts +++ b/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 .ts - * 5. Creates/updates index.ts with `export * as Foo from "./"` - * 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 [--dry-run] [--name ]") + console.error("Usage: bun script/unwrap-namespace.ts [--dry-run] [--name ] [--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 { + const map: Record = {} + 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() 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 +} diff --git a/packages/opencode/specs/effect/namespace-treeshake.md b/packages/opencode/specs/effect/namespace-treeshake.md index 5d1fbd07e5..36b00c65cb 100644 --- a/packages/opencode/specs/effect/namespace-treeshake.md +++ b/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" +// 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: (error: E) => Effect = internal.fail -export const succeed: (value: A) => Effect = 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 [--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 `.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 `"@/"`: +**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. diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 53bc7ed5fb..d1ad532aca 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/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" diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 54ca484555..e4395794ff 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/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" diff --git a/packages/opencode/src/cli/cmd/debug/config.ts b/packages/opencode/src/cli/cmd/debug/config.ts index b1f1c25e9c..a80b6a5819 100644 --- a/packages/opencode/src/cli/cmd/debug/config.ts +++ b/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" diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index dc6d5e8896..48bf837105 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/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" diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index 4bc3f0ea6c..79c677f983 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/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" diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index 393a407eb0..262faa0432 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/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" diff --git a/packages/opencode/src/cli/network.ts b/packages/opencode/src/cli/network.ts index a489ea14c5..a6cecdfacd 100644 --- a/packages/opencode/src/cli/network.ts +++ b/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 = { diff --git a/packages/opencode/src/cli/upgrade.ts b/packages/opencode/src/cli/upgrade.ts index 7c6f08874b..e1d7887d7b 100644 --- a/packages/opencode/src/cli/upgrade.ts +++ b/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" diff --git a/packages/opencode/src/command/command.ts b/packages/opencode/src/command/command.ts index 4ea1325240..40d3630908 100644 --- a/packages/opencode/src/command/command.ts +++ b/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" diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 97e7a662d0..96eec45741 100644 --- a/packages/opencode/src/config/config.ts +++ b/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" diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index f06c41e319..dc0aa25591 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/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" diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts index 327eadbef5..29f81aca2f 100644 --- a/packages/opencode/src/file/time.ts +++ b/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" }) +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: SessionID) => { - const value = reads.get(sessionID) - if (value) return value - - const next = new Map() - reads.set(sessionID, next) - return next - } - - interface State { - reads: Map> - locks: Map - } - - export interface Interface { - readonly read: (sessionID: SessionID, file: string) => Effect.Effect - readonly get: (sessionID: SessionID, file: string) => Effect.Effect - readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect - readonly withLock: (filepath: string, fn: () => Effect.Effect) => Effect.Effect - } - - export class Service extends Context.Service()("@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( - Effect.fn("FileTime.state")(() => - Effect.succeed({ - reads: new Map>(), - locks: new Map(), - }), - ), - ) - - 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* (filepath: string, fn: () => Effect.Effect) { - 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: SessionID) => { + const value = reads.get(sessionID) + if (value) return value + + const next = new Map() + reads.set(sessionID, next) + return next +} + +interface State { + reads: Map> + locks: Map +} + +export interface Interface { + readonly read: (sessionID: SessionID, file: string) => Effect.Effect + readonly get: (sessionID: SessionID, file: string) => Effect.Effect + readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect + readonly withLock: (filepath: string, fn: () => Effect.Effect) => Effect.Effect +} + +export class Service extends Context.Service()("@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( + Effect.fn("FileTime.state")(() => + Effect.succeed({ + reads: new Map>(), + locks: new Map(), + }), + ), + ) + + 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* (filepath: string, fn: () => Effect.Effect) { + 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" diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index 3e3da444a5..8ee1e36974 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/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" diff --git a/packages/opencode/src/format/format.ts b/packages/opencode/src/format/format.ts index 40855636f9..a3f557685c 100644 --- a/packages/opencode/src/format/format.ts +++ b/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" diff --git a/packages/opencode/src/lsp/lsp.ts b/packages/opencode/src/lsp/lsp.ts index 2c0982eca5..2cfd6f6b41 100644 --- a/packages/opencode/src/lsp/lsp.ts +++ b/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" diff --git a/packages/opencode/src/mcp/mcp.ts b/packages/opencode/src/mcp/mcp.ts index 1f1022538f..15555e6bff 100644 --- a/packages/opencode/src/mcp/mcp.ts +++ b/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" diff --git a/packages/opencode/src/permission/permission.ts b/packages/opencode/src/permission/permission.ts index fe7fb85455..f19eea590d 100644 --- a/packages/opencode/src/permission/permission.ts +++ b/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" diff --git a/packages/opencode/src/plugin/plugin.ts b/packages/opencode/src/plugin/plugin.ts index d1fc60d993..fe1243d2a2 100644 --- a/packages/opencode/src/plugin/plugin.ts +++ b/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" diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 43ae9a5e9f..7d5aaaebd0 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/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" diff --git a/packages/opencode/src/server/instance/config.ts b/packages/opencode/src/server/instance/config.ts index e3291a8c36..6beb84b66c 100644 --- a/packages/opencode/src/server/instance/config.ts +++ b/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" diff --git a/packages/opencode/src/server/instance/experimental.ts b/packages/opencode/src/server/instance/experimental.ts index fe80173a8b..338cb9c59e 100644 --- a/packages/opencode/src/server/instance/experimental.ts +++ b/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" diff --git a/packages/opencode/src/server/instance/global.ts b/packages/opencode/src/server/instance/global.ts index 8208cf9669..be990c51c7 100644 --- a/packages/opencode/src/server/instance/global.ts +++ b/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" }) diff --git a/packages/opencode/src/server/instance/mcp.ts b/packages/opencode/src/server/instance/mcp.ts index 695008fc4e..9dd648f9dd 100644 --- a/packages/opencode/src/server/instance/mcp.ts +++ b/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" diff --git a/packages/opencode/src/server/instance/provider.ts b/packages/opencode/src/server/instance/provider.ts index c1580437da..2dacd1e302 100644 --- a/packages/opencode/src/server/instance/provider.ts +++ b/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" diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 3ef6977547..c2fd284c31 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/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" diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index cd2050adf5..3ed90d3b08 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/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" diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index d38c29765a..6a450ad1dc 100644 --- a/packages/opencode/src/session/llm.ts +++ b/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" diff --git a/packages/opencode/src/session/overflow.ts b/packages/opencode/src/session/overflow.ts index 6f48a760df..e9c0d1f268 100644 --- a/packages/opencode/src/session/overflow.ts +++ b/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" diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 415639fbe5..44dede23be 100644 --- a/packages/opencode/src/session/processor.ts +++ b/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" diff --git a/packages/opencode/src/share/session.ts b/packages/opencode/src/share/session.ts index 63b7670785..3bb6390c43 100644 --- a/packages/opencode/src/share/session.ts +++ b/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" diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index 1991e75ff6..cf49fec0ad 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/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" diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index f8ff7b8f5f..5485b40d65 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/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" diff --git a/packages/opencode/src/snapshot/snapshot.ts b/packages/opencode/src/snapshot/snapshot.ts index 7a5c0a4dca..2e1ae7af9b 100644 --- a/packages/opencode/src/snapshot/snapshot.ts +++ b/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" diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index a8ab4c27ea..282707a8c7 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/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" diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 3da0664f3d..90a20e90ba 100644 --- a/packages/opencode/src/tool/task.ts +++ b/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 { diff --git a/packages/opencode/test/config/agent-color.test.ts b/packages/opencode/test/config/agent-color.test.ts index bfa948619b..390d0ccff9 100644 --- a/packages/opencode/test/config/agent-color.test.ts +++ b/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" diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 1f36312447..8fe0f32b04 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/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" diff --git a/packages/opencode/test/config/tui.test.ts b/packages/opencode/test/config/tui.test.ts index c7b6d4a504..91ec03fd7f 100644 --- a/packages/opencode/test/config/tui.test.ts +++ b/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" diff --git a/packages/opencode/test/file/watcher.test.ts b/packages/opencode/test/file/watcher.test.ts index 0c23550083..0c8968d94b 100644 --- a/packages/opencode/test/file/watcher.test.ts +++ b/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" diff --git a/packages/opencode/test/fixture/fixture.ts b/packages/opencode/test/fixture/fixture.ts index fd7f5e3808..7970543547 100644 --- a/packages/opencode/test/fixture/fixture.ts +++ b/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" diff --git a/packages/opencode/test/permission-task.test.ts b/packages/opencode/test/permission-task.test.ts index 3c53314b6a..d415d23ebc 100644 --- a/packages/opencode/test/permission-task.test.ts +++ b/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" diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index ee3f645c52..2ba352c674 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/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" diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts index 74ce913077..f3de9b77a7 100644 --- a/packages/opencode/test/session/processor-effect.test.ts +++ b/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" diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts index 121d662e5f..bef4990767 100644 --- a/packages/opencode/test/session/prompt-effect.test.ts +++ b/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" diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index 1f66ccb995..d497b83cba 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/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" diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts index 2359f06a31..1128f479a8 100644 --- a/packages/opencode/test/share/share-next.test.ts +++ b/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" diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index b94dd52086..b85797df06 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/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"