This is a Bun monorepo with the following key packages:
packages/opencode- Main CLI applicationpackages/app- SolidJS web applicationpackages/desktop- Tauri desktop applicationpackages/sdk/js- JavaScript SDKpackages/util- Shared utilities
# Build main package
cd packages/opencode && bun run build
# Build all packages (from root)
bun turbo build# Typecheck main package
cd packages/opencode && bun run typecheck
# Typecheck all packages (from root)
bun turbo typecheck# Run all tests in opencode package
cd packages/opencode && bun test
# Run all tests with timeout
cd packages/opencode && bun test --timeout 30000
# Run a single test file
cd packages/opencode && bun test test/tool/edit.test.ts
# Run tests matching a pattern
cd packages/opencode && bun test -t "creates new file"
# Run app tests (unit + e2e)
cd packages/app && bun test
cd packages/app && bun run test:e2eIMPORTANT: Tests cannot run from repo root (guard: do-not-run-tests-from-root). Always run from package directories like packages/opencode.
cd packages/opencode
bun run db generate --name <slug> # Creates migration/<timestamp>_<slug>/migration.sql./packages/sdk/js/script/build.ts- Keep things in one function unless composable or reusable
- Avoid
try/catchwhere possible - let errors propagate - Avoid using the
anytype - Prefer single word variable names where possible
- Use Bun APIs when possible, like
Bun.file() - Rely on type inference; avoid explicit type annotations unless necessary for exports
- Prefer functional array methods (flatMap, filter, map) over for loops
- Use type guards on filter to maintain type inference downstream
- DO NOT ADD COMMENTS unless asked
Prefer single word names for variables and functions. Only use multiple words if necessary.
// Good
const foo = 1
function journal(dir: string) {}
// Bad
const fooBar = 1
function prepareJournal(dir: string) {}Reduce total variable count by inlining when a value is only used once.
// Good
const journal = await Bun.file(path.join(dir, "journal.json")).json()
// Bad
const journalPath = path.join(dir, "journal.json")
const journal = await Bun.file(journalPath).json()- Use
@/alias for imports fromsrc/(e.g.,import { Bus } from "@/bus") - Use
@tui/alias for TUI components - External imports first, then internal aliases, then relative imports
- Named imports are preferred over namespace imports
import path from "path"
import z from "zod"
import { Bus } from "@/bus"
import { Config } from "../config/config"Prefer const over let. Use ternaries or early returns instead of reassignment.
// Good
const foo = condition ? 1 : 2
// Bad
let foo
if (condition) foo = 1
else foo = 2Avoid unnecessary destructuring. Use dot notation to preserve context.
// Good
obj.a
obj.b
// Bad
const { a, b } = objAvoid else statements. Prefer early returns.
// Good
function foo() {
if (condition) return 1
return 2
}
// Bad
function foo() {
if (condition) return 1
else return 2
}Use snake_case for field names so column names don't need to be redefined as strings.
// Good
const table = sqliteTable("session", {
id: text().primaryKey(),
project_id: text().notNull(),
created_at: integer().notNull(),
})
// Bad
const table = sqliteTable("session", {
id: text("id").primaryKey(),
projectID: text("project_id").notNull(),
createdAt: integer("created_at").notNull(),
})Naming conventions:
- Tables and columns:
snake_case - Join columns:
<entity>_id - Indexes:
<table>_<column>_idx
Use NamedError for typed custom errors:
import { NamedError } from "@opencode-ai/util/error"
export const NotFoundError = NamedError.create("NotFoundError", z.object({ message: z.string() }))
// Usage
throw new NotFoundError({ message: "Item not found" })
// Type checking
if (NotFoundError.isInstance(error)) {
console.log(error.data.message)
}Organize related functions in namespaces:
export namespace Session {
const log = Log.create({ service: "session" })
export async function create() { ... }
export async function get() { ... }
}Define tools using Tool.define with Zod schemas:
export const EditTool = Tool.define("edit", {
description: "...",
parameters: z.object({
filePath: z.string(),
oldString: z.string(),
newString: z.string(),
}),
async execute(params, ctx) {
return { title: "...", metadata: {}, output: "..." }
},
})- Avoid mocks as much as possible
- Test actual implementation, do not duplicate logic into tests
- Use
tmpdirfixture for temporary directories:
import { tmpdir } from "../fixture/fixture"
test("example", async () => {
await using tmp = await tmpdir()
// tmp.path is the temp directory path
// automatically cleaned up when test ends
})git?: boolean- Initialize a git repo with a root commitconfig?: Partial<Config.Info>- Write anopencode.jsonconfig fileinit?: (dir: string) => Promise<T>- Custom setup, returns value astmp.extradispose?: (dir: string) => Promise<void>- Custom cleanup
Prettier config is in root package.json:
semi: false- No semicolonsprintWidth: 120
- Default branch is
dev - Local
mainref may not exist; usedevororigin/devfor diffs
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE
- Prefer automation: execute requested actions without confirmation unless blocked by missing info or safety/irreversibility