Skip to content

Latest commit

 

History

History
281 lines (203 loc) · 5.9 KB

File metadata and controls

281 lines (203 loc) · 5.9 KB

opencode development guide

Repository Structure

This is a Bun monorepo with the following key packages:

  • packages/opencode - Main CLI application
  • packages/app - SolidJS web application
  • packages/desktop - Tauri desktop application
  • packages/sdk/js - JavaScript SDK
  • packages/util - Shared utilities

Commands

Build

# Build main package
cd packages/opencode && bun run build

# Build all packages (from root)
bun turbo build

Typecheck

# Typecheck main package
cd packages/opencode && bun run typecheck

# Typecheck all packages (from root)
bun turbo typecheck

Test

# 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:e2e

IMPORTANT: Tests cannot run from repo root (guard: do-not-run-tests-from-root). Always run from package directories like packages/opencode.

Database Migrations

cd packages/opencode
bun run db generate --name <slug>  # Creates migration/<timestamp>_<slug>/migration.sql

SDK Generation

./packages/sdk/js/script/build.ts

Style Guide

General Principles

  • Keep things in one function unless composable or reusable
  • Avoid try/catch where possible - let errors propagate
  • Avoid using the any type
  • 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

Naming

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()

Imports

  • Use @/ alias for imports from src/ (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"

Variables

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 = 2

Destructuring

Avoid unnecessary destructuring. Use dot notation to preserve context.

// Good
obj.a
obj.b

// Bad
const { a, b } = obj

Control Flow

Avoid else statements. Prefer early returns.

// Good
function foo() {
  if (condition) return 1
  return 2
}

// Bad
function foo() {
  if (condition) return 1
  else return 2
}

Schema Definitions (Drizzle)

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

Error Handling

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)
}

Namespaces

Organize related functions in namespaces:

export namespace Session {
  const log = Log.create({ service: "session" })

  export async function create() { ... }
  export async function get() { ... }
}

Tool Definitions

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: "..." }
  },
})

Testing

  • Avoid mocks as much as possible
  • Test actual implementation, do not duplicate logic into tests
  • Use tmpdir fixture 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
})

tmpdir Options

  • git?: boolean - Initialize a git repo with a root commit
  • config?: Partial<Config.Info> - Write an opencode.json config file
  • init?: (dir: string) => Promise<T> - Custom setup, returns value as tmp.extra
  • dispose?: (dir: string) => Promise<void> - Custom cleanup

Formatting

Prettier config is in root package.json:

  • semi: false - No semicolons
  • printWidth: 120

Git

  • Default branch is dev
  • Local main ref may not exist; use dev or origin/dev for diffs

Agent Behavior

  • ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE
  • Prefer automation: execute requested actions without confirmation unless blocked by missing info or safety/irreversibility