Compare commits

..
Author SHA1 Message Date
Kit Langton 4e023060cd refactor: switch Permission to direct self-reexport imports 2026-04-16 11:40:21 -04:00
32 changed files with 28 additions and 237 deletions
-1
View File
@@ -450,7 +450,6 @@
"@typescript/native-preview": "catalog:",
"drizzle-kit": "catalog:",
"drizzle-orm": "catalog:",
"rollup": "4.60.1",
"typescript": "catalog:",
"vscode-languageserver-types": "3.17.5",
"why-is-node-running": "3.2.2",
-1
View File
@@ -1,5 +1,4 @@
research
.rollup-tmp
dist
dist-*
gen
-1
View File
@@ -69,7 +69,6 @@
"@typescript/native-preview": "catalog:",
"drizzle-kit": "catalog:",
"drizzle-orm": "catalog:",
"rollup": "4.60.1",
"typescript": "catalog:",
"vscode-languageserver-types": "3.17.5",
"why-is-node-running": "3.2.2",
+1 -21
View File
@@ -5,7 +5,6 @@ import fs from "fs"
import path from "path"
import { fileURLToPath } from "url"
import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin"
import { treeshakePrepass } from "./treeshake-prepass"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
@@ -51,23 +50,9 @@ console.log(`Loaded ${migrations.length} migrations`)
const singleFlag = process.argv.includes("--single")
const baselineFlag = process.argv.includes("--baseline")
const skipInstall = process.argv.includes("--skip-install")
const skipTreeshake = process.argv.includes("--skip-treeshake")
const plugin = createSolidTransformPlugin()
const skipEmbedWebUi = process.argv.includes("--skip-embed-web-ui")
// Run Rollup tree-shaking pre-pass on the main entrypoint.
// Bun/esbuild can't tree-shake `export * as X` barrels (evanw/esbuild#1420).
// Rollup can — it does AST-level analysis to drop unused exports and their
// transitive imports. Workers are excluded since they're separate bundles.
const rollupTmpDir = path.join(dir, ".rollup-tmp")
let treeshakenEntry: string | undefined
if (!skipTreeshake) {
const entryMap = await treeshakePrepass(["./src/index.ts"], rollupTmpDir)
treeshakenEntry = entryMap.get("index")
} else {
console.log("[treeshake] Skipped (--skip-treeshake)")
}
const createEmbeddedWebUIBundle = async () => {
console.log(`Building Web UI to embed in the binary`)
const appDir = path.join(import.meta.dirname, "../../app")
@@ -228,7 +213,7 @@ for (const item of targets) {
},
files: embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {},
entrypoints: [
treeshakenEntry ?? "./src/index.ts",
"./src/index.ts",
parserWorker,
workerPath,
rgPath,
@@ -285,9 +270,4 @@ if (Script.release) {
await $`gh release upload v${Script.version} ./dist/*.zip ./dist/*.tar.gz --clobber --repo ${process.env.GH_REPO}`
}
// Clean up Rollup temp directory
if (fs.existsSync(rollupTmpDir)) {
fs.rmSync(rollupTmpDir, { recursive: true })
}
export { binaries }
@@ -1,187 +0,0 @@
#!/usr/bin/env bun
/**
* Rollup tree-shaking pre-pass for the opencode build.
*
* Bun's bundler cannot tree-shake `export * as X from "./mod"` barrels
* (nor can esbuild — see evanw/esbuild#1420). Rollup can.
*
* This script runs Rollup on the source entrypoints to eliminate unused
* exports and their transitive imports, then writes the tree-shaken ESM
* to .rollup-tmp/ for Bun to compile into the final binary.
*
* Usage:
* bun script/treeshake-prepass.ts [entrypoints...]
*
* If no entrypoints are given, defaults to ./src/index.ts.
* Output goes to .rollup-tmp/ preserving the entry filename.
*/
import { rollup, type Plugin as RollupPlugin } from "rollup"
import path from "path"
import fs from "fs"
const dir = path.resolve(import.meta.dirname, "..")
const srcDir = path.join(dir, "src")
// Path alias mappings from tsconfig.json
const aliases: Record<string, string> = {
"@/": path.join(srcDir, "/"),
"@tui/": path.join(srcDir, "cli/cmd/tui/"),
}
// Conditional imports from package.json "#imports"
const hashImports: Record<string, string> = {
"#db": path.join(srcDir, "storage/db.bun.ts"),
"#pty": path.join(srcDir, "pty/pty.bun.ts"),
"#hono": path.join(srcDir, "server/adapter.bun.ts"),
}
function resolveWithAliases(source: string, importerDir: string): string | null {
// Handle hash imports
if (hashImports[source]) return hashImports[source]
// Handle path aliases
for (const [alias, target] of Object.entries(aliases)) {
if (source.startsWith(alias)) {
return target + source.slice(alias.length)
}
}
// Handle relative imports
if (source.startsWith(".")) {
return path.resolve(importerDir, source)
}
return null
}
// Binary/asset extensions that Bun imports natively but Rollup can't parse
const assetExtensions = new Set([".wav", ".wasm", ".node", ".png", ".jpg", ".gif", ".svg", ".css"])
function tryResolveFile(base: string): string | null {
// Try exact file, then .ts, then .tsx, then /index.ts, then /index.tsx
for (const suffix of ["", ".ts", ".tsx", "/index.ts", "/index.tsx"]) {
const p = base + suffix
if (fs.existsSync(p) && fs.statSync(p).isFile()) return p
}
// Bun.Transpiler rewrites .ts → .js in import paths, so try .ts for .js
if (base.endsWith(".js")) {
const tsBase = base.slice(0, -3)
for (const suffix of [".ts", ".tsx"]) {
const p = tsBase + suffix
if (fs.existsSync(p) && fs.statSync(p).isFile()) return p
}
}
return null
}
/**
* Rollup plugin that resolves TypeScript paths and transpiles TS/TSX.
* Uses Bun.Transpiler for speed — no separate TS compilation step.
*/
const bunTranspilePlugin: RollupPlugin = {
name: "bun-transpile",
resolveId(source, importer) {
if (!importer) return null
const importerDir = path.dirname(importer)
const resolved = resolveWithAliases(source, importerDir)
if (!resolved) return null // external (node_modules, node builtins)
const file = tryResolveFile(resolved)
if (file) return file
// If it's a local import we can't resolve (generated file, missing, etc.),
// mark it external so Bun handles it later
return { id: source, external: true }
},
load(id) {
if (id.endsWith(".ts") || id.endsWith(".tsx")) {
return fs.readFileSync(id, "utf-8")
}
// Handle non-JS assets that Bun imports natively
if (id.endsWith(".txt")) {
const content = fs.readFileSync(id, "utf-8")
return `export default ${JSON.stringify(content)};`
}
if (id.endsWith(".json")) {
const content = fs.readFileSync(id, "utf-8")
return `export default ${content};`
}
if (id.endsWith(".sql")) {
const content = fs.readFileSync(id, "utf-8")
return `export default ${JSON.stringify(content)};`
}
// Binary assets — return a placeholder (Bun handles the real import)
const ext = path.extname(id)
if (assetExtensions.has(ext)) {
return `export default "asset:${path.basename(id)}";`
}
return null
},
transform(code, id) {
if (!id.endsWith(".ts") && !id.endsWith(".tsx")) return null
const loader = id.endsWith(".tsx") ? "tsx" : "ts"
const t = new Bun.Transpiler({ loader, tsconfig: JSON.stringify({ compilerOptions: { jsx: "preserve" } }) })
return { code: t.transformSync(code), map: null }
},
}
export async function treeshakePrepass(entrypoints: string[], outDir: string) {
const absEntries = entrypoints.map((e) => path.resolve(dir, e))
const startTime = performance.now()
console.log(`[treeshake] Running Rollup pre-pass on ${absEntries.length} entrypoint(s)...`)
const bundle = await rollup({
input: absEntries,
plugins: [bunTranspilePlugin],
treeshake: {
moduleSideEffects: false, // equivalent to sideEffects: false
},
// Mark everything that isn't local source as external.
// Bun handles node_modules resolution + bundling in the compile step.
external: (id) => {
if (id.startsWith(".") || id.startsWith("/") || id.startsWith("@/") || id.startsWith("@tui/") || id.startsWith("#"))
return false
return true
},
logLevel: "warn",
})
fs.mkdirSync(outDir, { recursive: true })
const { output } = await bundle.write({
dir: outDir,
format: "esm",
preserveModules: false,
entryFileNames: "[name].js",
})
await bundle.close()
const elapsed = (performance.now() - startTime).toFixed(0)
const totalSize = output.reduce((sum, chunk) => sum + ("code" in chunk ? chunk.code.length : 0), 0)
console.log(`[treeshake] Done in ${elapsed}ms — ${output.length} chunks, ${(totalSize / 1024).toFixed(0)}KB total`)
// Return a mapping of original entry basenames to output paths
const entryMap = new Map<string, string>()
for (const chunk of output) {
if (chunk.type === "chunk" && chunk.isEntry) {
entryMap.set(chunk.name, path.join(outDir, chunk.fileName))
}
}
return entryMap
}
// CLI mode: run directly
if (import.meta.main) {
const args = process.argv.slice(2)
const entries = args.length > 0 ? args : ["./src/index.ts"]
const outDir = path.join(dir, ".rollup-tmp")
const result = await treeshakePrepass(entries, outDir)
for (const [name, out] of result) {
console.log(` ${name}${path.relative(dir, out)}`)
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ import PROMPT_COMPACTION from "./prompt/compaction.txt"
import PROMPT_EXPLORE from "./prompt/explore.txt"
import PROMPT_SUMMARY from "./prompt/summary.txt"
import PROMPT_TITLE from "./prompt/title.txt"
import { Permission } from "@/permission"
import { Permission } from "@/permission/permission"
import { mergeDeep, pipe, sortBy, values } from "remeda"
import { Global } from "@/global"
import path from "path"
+1 -1
View File
@@ -11,7 +11,7 @@ import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@openc
import { Server } from "../../server/server"
import { Provider } from "../../provider"
import { Agent } from "../../agent/agent"
import { Permission } from "../../permission"
import { Permission } from "../../permission/permission"
import { Tool } from "../../tool"
import { GlobTool } from "../../tool/glob"
import { GrepTool } from "../../tool/grep"
+1 -1
View File
@@ -21,7 +21,7 @@ import { Agent } from "@/agent/agent"
import { Skill } from "@/skill"
import { Discovery } from "@/skill/discovery"
import { Question } from "@/question"
import { Permission } from "@/permission"
import { Permission } from "@/permission/permission"
import { Todo } from "@/session/todo"
import { Session } from "@/session"
import { SessionStatus } from "@/session/status"
@@ -1 +0,0 @@
export * as Permission from "./permission"
@@ -321,3 +321,5 @@ export function disabled(tools: string[], ruleset: Ruleset): Set<string> {
}
export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
export * as Permission from "./permission"
@@ -1,4 +1,4 @@
import { Permission } from "@/permission"
import { Permission } from "@/permission/permission"
import { PermissionID } from "@/permission/schema"
import { Effect, Layer, Schema } from "effect"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
@@ -2,7 +2,7 @@ import { Hono } from "hono"
import { describeRoute, validator, resolver } from "hono-openapi"
import z from "zod"
import { AppRuntime } from "@/effect/app-runtime"
import { Permission } from "@/permission"
import { Permission } from "@/permission/permission"
import { PermissionID } from "@/permission/schema"
import { errors } from "../error"
import { lazy } from "../../util/lazy"
@@ -19,7 +19,7 @@ import { Agent } from "../../agent/agent"
import { Snapshot } from "@/snapshot"
import { Command } from "../../command"
import { Log } from "../../util"
import { Permission } from "@/permission"
import { Permission } from "@/permission/permission"
import { PermissionID } from "@/permission/schema"
import { ModelID, ProviderID } from "@/provider/schema"
import { errors } from "../error"
+1 -1
View File
@@ -13,7 +13,7 @@ import type { MessageV2 } from "./message-v2"
import { Plugin } from "@/plugin"
import { SystemPrompt } from "./system"
import { Flag } from "@/flag/flag"
import { Permission } from "@/permission"
import { Permission } from "@/permission/permission"
import { PermissionID } from "@/permission/schema"
import { Bus } from "@/bus"
import { Wildcard } from "@/util"
+1 -1
View File
@@ -3,7 +3,7 @@ import * as Stream from "effect/Stream"
import { Agent } from "@/agent/agent"
import { Bus } from "@/bus"
import { Config } from "@/config"
import { Permission } from "@/permission"
import { Permission } from "@/permission/permission"
import { Plugin } from "@/plugin"
import { Snapshot } from "@/snapshot"
import * as Session from "./session"
+1 -1
View File
@@ -35,7 +35,7 @@ import { SessionSummary } from "./summary"
import { NamedError } from "@opencode-ai/shared/util/error"
import { SessionProcessor } from "./processor"
import { Tool } from "@/tool"
import { Permission } from "@/permission"
import { Permission } from "@/permission/permission"
import { SessionStatus } from "./status"
import { LLM } from "./llm"
import { Shell } from "@/shell/shell"
+1 -1
View File
@@ -3,7 +3,7 @@ import { ProjectTable } from "../project/project.sql"
import type { MessageV2 } from "./message-v2"
import type { SessionEntry } from "../v2/session-entry"
import type { Snapshot } from "../snapshot"
import type { Permission } from "../permission"
import type { Permission } from "../permission/permission"
import type { ProjectID } from "../project/schema"
import type { SessionID, MessageID, PartID } from "./schema"
import type { WorkspaceID } from "../control-plane/schema"
+1 -1
View File
@@ -26,7 +26,7 @@ import { WorkspaceID } from "../control-plane/schema"
import { SessionID, MessageID, PartID } from "./schema"
import type { Provider } from "@/provider"
import { Permission } from "@/permission"
import { Permission } from "@/permission/permission"
import { Global } from "@/global"
import { Effect, Layer, Option, Context } from "effect"
+1 -1
View File
@@ -13,7 +13,7 @@ import PROMPT_CODEX from "./prompt/codex.txt"
import PROMPT_TRINITY from "./prompt/trinity.txt"
import type { Provider } from "@/provider"
import type { Agent } from "@/agent/agent"
import { Permission } from "@/permission"
import { Permission } from "@/permission/permission"
import { Skill } from "@/skill"
export namespace SystemPrompt {
+1 -1
View File
@@ -9,7 +9,7 @@ import { Bus } from "@/bus"
import { InstanceState } from "@/effect"
import { Flag } from "@/flag/flag"
import { Global } from "@/global"
import { Permission } from "@/permission"
import { Permission } from "@/permission/permission"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Config } from "../config"
import { ConfigMarkdown } from "../config"
+1 -1
View File
@@ -45,7 +45,7 @@ import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { Bus } from "../bus"
import { Agent } from "../agent/agent"
import { Skill } from "../skill"
import { Permission } from "@/permission"
import { Permission } from "@/permission/permission"
const log = Log.create({ service: "tool.registry" })
+1 -1
View File
@@ -1,7 +1,7 @@
import z from "zod"
import { Effect } from "effect"
import type { MessageV2 } from "../session/message-v2"
import type { Permission } from "../permission"
import type { Permission } from "../permission/permission"
import type { SessionID, MessageID } from "../session/schema"
import * as Truncate from "./truncate"
import { Agent } from "@/agent/agent"
+1 -1
View File
@@ -4,7 +4,7 @@ import path from "path"
import { provideInstance, tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
import { Agent } from "../../src/agent/agent"
import { Permission } from "../../src/permission"
import { Permission } from "../../src/permission/permission"
// Helper to evaluate permission for a tool with wildcard pattern
function evalPerm(agent: Agent.Info | undefined, permission: string): Permission.Action | undefined {
@@ -3,7 +3,7 @@ import os from "os"
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
import { Bus } from "../../src/bus"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Permission } from "../../src/permission"
import { Permission } from "../../src/permission/permission"
import { PermissionID } from "../../src/permission/schema"
import { Instance } from "../../src/project/instance"
import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture"
@@ -11,7 +11,7 @@ import { SessionCompaction } from "../../src/session/compaction"
import { Token } from "../../src/util"
import { Instance } from "../../src/project/instance"
import { Log } from "../../src/util"
import { Permission } from "../../src/permission"
import { Permission } from "../../src/permission/permission"
import { Plugin } from "../../src/plugin"
import { provideTmpdirInstance, tmpdir } from "../fixture/fixture"
import { Session as SessionNs } from "../../src/session"
@@ -6,7 +6,7 @@ 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 { Permission } from "../../src/permission"
import { Permission } from "../../src/permission/permission"
import { Plugin } from "../../src/plugin"
import { Provider } from "../../src/provider"
import { ModelID, ProviderID } from "../../src/provider/schema"
@@ -10,7 +10,7 @@ import { Config } from "../../src/config"
import { FileTime } from "../../src/file/time"
import { LSP } from "../../src/lsp"
import { MCP } from "../../src/mcp"
import { Permission } from "../../src/permission"
import { Permission } from "../../src/permission/permission"
import { Plugin } from "../../src/plugin"
import { Provider as ProviderSvc } from "../../src/provider"
import { Env } from "../../src/env"
@@ -36,7 +36,7 @@ import { Config } from "../../src/config"
import { FileTime } from "../../src/file/time"
import { LSP } from "../../src/lsp"
import { MCP } from "../../src/mcp"
import { Permission } from "../../src/permission"
import { Permission } from "../../src/permission/permission"
import { Plugin } from "../../src/plugin"
import { Provider as ProviderSvc } from "../../src/provider"
import { Env } from "../../src/env"
+1 -1
View File
@@ -7,7 +7,7 @@ import { BashTool } from "../../src/tool/bash"
import { Instance } from "../../src/project/instance"
import { Filesystem } from "../../src/util"
import { tmpdir } from "../fixture/fixture"
import type { Permission } from "../../src/permission"
import type { Permission } from "../../src/permission/permission"
import { Agent } from "../../src/agent/agent"
import { Truncate } from "../../src/tool"
import { SessionID, MessageID } from "../../src/session/schema"
@@ -6,7 +6,7 @@ import { Instance } from "../../src/project/instance"
import { assertExternalDirectory } from "../../src/tool/external-directory"
import { Filesystem } from "../../src/util"
import { tmpdir } from "../fixture/fixture"
import type { Permission } from "../../src/permission"
import type { Permission } from "../../src/permission/permission"
import { SessionID, MessageID } from "../../src/session/schema"
const baseCtx: Omit<Tool.Context, "ask"> = {
+1 -1
View File
@@ -6,7 +6,7 @@ import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { AppFileSystem } from "@opencode-ai/shared/filesystem"
import { FileTime } from "../../src/file/time"
import { LSP } from "../../src/lsp"
import { Permission } from "../../src/permission"
import { Permission } from "../../src/permission/permission"
import { Instance } from "../../src/project/instance"
import { SessionID, MessageID } from "../../src/session/schema"
import { Instruction } from "../../src/session/instruction"
+1 -1
View File
@@ -3,7 +3,7 @@ import { Effect, Layer } from "effect"
import { afterEach, describe, expect } from "bun:test"
import path from "path"
import { pathToFileURL } from "url"
import type { Permission } from "../../src/permission"
import type { Permission } from "../../src/permission/permission"
import type { Tool } from "../../src/tool"
import { Instance } from "../../src/project/instance"
import { SkillTool } from "../../src/tool/skill"