opencode/packages/opencode/MIGRATION.md
Dax Raad 02d527aaf9 docs: update MIGRATION.md with additional migrated files
Add 16 newly migrated files to the checklist:
2026-02-18 11:18:18 -05:00

12 KiB

Migration Guide: Replacing Bun.file with Node.js APIs

Goal: Replace all Bun.file() and Bun.write() calls in the opencode codebase with the Filesystem utility module from src/util/filesystem.ts. This utility provides Node.js fs/promises equivalents optimized for performance.

What to do: Pick one unchecked file from the checklist below, migrate it using the Filesystem module, ensure tests pass, open a PR, and check off the file once merged. Repeat until all files are done.

Migration Process

Each file should be migrated one at a time with a separate PR opened for each file. This approach ensures:

  • Easier code review and debugging
  • Isolated changes that can be rolled back independently
  • Clear tracking of which files have been migrated
  • Reduced risk of introducing multiple bugs at once

Workflow:

  1. Pick one unchecked file from the checklist below
  2. Create a new branch for that file
  3. Migrate all Bun.file() and related APIs to Node.js equivalents using the Filesystem module
  4. Run tests for the specific file and ensure they pass before pushing
  5. Open a PR specifically for that single file
  6. Once merged, check off the file in the checklist below

Using the Filesystem Module

Use the Filesystem module from src/util/filesystem.ts for all file operations:

import { Filesystem } from "./util/filesystem"

// Before: Bun.file().exists()
const exists = await Bun.file(path).exists()
// After: Filesystem.exists()
const exists = await Filesystem.exists(path)

// Before: Bun.file().text()
const content = await Bun.file(path).text()
// After: Filesystem.readText()
const content = await Filesystem.readText(path)

// Before: Bun.file().json()
const data = await Bun.file(path).json()
// After: Filesystem.readJson()
const data = await Filesystem.readJson<Config>(path)

// Before: Bun.file().bytes()
const bytes = await Bun.file(path).bytes()
// After: Filesystem.readBytes()
const buffer = await Filesystem.readBytes(path)

// Before: Bun.file().stat()
const stats = await Bun.file(path).stat()
// After: Filesystem.isDir() and size
const isDir = await Filesystem.isDir(path)
const size = await Filesystem.size(path)

// Before: Bun.file().size
const size = Bun.file(path).size
// After: Filesystem.size()
const size = await Filesystem.size(path)

// Before: Bun.write()
await Bun.write(path, content)
// After: Filesystem.write()
await Filesystem.write(path, content)

// Before: Bun.write() with JSON
await Bun.write(path, JSON.stringify(data, null, 2))
// After: Filesystem.writeJson()
await Filesystem.writeJson(path, data)

// Before: Bun.file().type
const mime = Bun.file(path).type
// After: Filesystem.mimeType()
const mime = Filesystem.mimeType(path)

Files Requiring Updates

Core Utilities

  • src/util/filesystem.ts - exists() and isDir() functions use Bun.file().stat()
  • src/util/log.ts - Uses Bun.file() for log file access

Tool Implementations

  • src/tool/read.ts - Uses Bun.file() for file type, stat, and bytes
  • src/tool/write.ts - Uses Bun.file().exists() and Bun.file().text()
  • src/tool/edit.ts - Uses Bun.file().exists()
  • src/tool/grep.ts - Uses Bun.file()
  • src/tool/glob.ts - Uses Bun.file() for stats
  • src/tool/lsp.ts - Uses Bun.file().exists()
  • src/tool/truncation.ts - Uses Bun.write()

Storage & Data

  • src/storage/storage.ts - Multiple uses of Bun.file().json() and Bun.write()
  • src/storage/json-migration.ts - Uses Bun.file().json()
  • src/storage/db.ts - Uses Bun.file().size
  • src/mcp/auth.ts - Uses Bun.file().json() and Bun.write()

Project Management

  • src/project/project.ts - Uses Bun.file().text() and Bun.file().stat()

Session & Prompts

  • src/session/prompt.ts - Uses Bun.file().stat(), Bun.file().exists(), and Bun.file().text()
  • src/session/instruction.ts - Uses Bun.file().exists() and Bun.file().text()

Provider & Models

  • src/provider/models.ts - Uses Bun.file() and Bun.write()
  • src/provider/provider.ts - Uses Bun.file().text()

Skill Discovery

  • src/skill/discovery.ts - Uses Bun.file().exists() and Bun.write()

LSP

  • src/lsp/client.ts - Uses Bun.file()
  • src/lsp/server.ts - Uses Bun.file().exists() and Bun.file().write()

Shell & CLI

  • src/shell/shell.ts - Uses Bun.file().size
  • src/cli/cmd/tui/thread.ts - Uses Bun.file().exists()

Additional Files Migrated

  • src/acp/agent.ts - Uses Bun.file().exists() and Bun.file().text()
  • src/auth/index.ts - Uses Bun.file().json() and Bun.write()
  • src/bun/index.ts - Uses Bun.file().json() and Bun.write()
  • src/cli/cmd/agent.ts - Uses Bun.file().exists() and Bun.write()
  • src/cli/cmd/github.ts - Uses Bun.write()
  • src/cli/cmd/import.ts - Uses Bun.file().json()
  • src/cli/cmd/mcp.ts - Uses Bun.file() and Bun.write()
  • src/cli/cmd/run.ts - Uses Bun.file()
  • src/cli/cmd/session.ts - Uses Bun.file().size
  • src/cli/cmd/uninstall.ts - Uses Bun.file().text() and Bun.write()
  • src/cli/cmd/tui/util/clipboard.ts - Uses Bun.file().arrayBuffer()
  • src/cli/cmd/tui/util/editor.ts - Uses Bun.write() and Bun.file().text()
  • src/config/markdown.ts - Uses Bun.file().text()
  • src/file/index.ts - Uses Bun.file() for text, exists, arrayBuffer, and mime type
  • src/file/time.ts - Uses Bun.file().stat()
  • src/format/formatter.ts - Uses Bun.file().json() and Bun.file().text()
  • src/global/index.ts - Uses Bun.file().text() and Bun.file().write()

Centralized File API Module

The src/util/filesystem.ts module provides optimized Node.js equivalents for all Bun.file operations:

import { Filesystem } from "./util/filesystem"

// Migration examples:

// Before: Bun.file().exists()
const exists = await Bun.file(path).exists()
// After: Filesystem.exists() (uses fast existsSync internally)
const exists = await Filesystem.exists(path)

// Before: Bun.file().text()
const content = await Bun.file(path).text()
// After: Filesystem.readText()
const content = await Filesystem.readText(path)

// Before: Bun.file().json()
const data = await Bun.file(path).json()
// After: Filesystem.readJson()
const data = await Filesystem.readJson<Config>(path)

// Before: Bun.file().bytes()
const bytes = await Bun.file(path).bytes()
// After: Filesystem.readBytes()
const buffer = await Filesystem.readBytes(path)

// Before: Bun.file().stat()
const stats = await Bun.file(path).stat()
// After: Filesystem.isDir() and size (uses fast statSync)
const isDir = await Filesystem.isDir(path)
const size = await Filesystem.size(path)

// Before: Bun.file().size
const size = Bun.file(path).size
// After: Filesystem.size() (8x faster than async stat)
const size = await Filesystem.size(path)

// Before: Bun.write()
await Bun.write(path, content)
// After: Filesystem.write() (auto-creates directories)
await Filesystem.write(path, content)

// Before: Bun.write() with JSON
await Bun.write(path, JSON.stringify(data, null, 2))
// After: Filesystem.writeJson()
await Filesystem.writeJson(path, data)

// Before: Bun.file().type
const mime = Bun.file(path).type
// After: Filesystem.mimeType()
const mime = Filesystem.mimeType(path)

API Reference

Bun API Filesystem Equivalent Implementation
Bun.file(p).exists() Filesystem.exists(p) existsSync() (15x faster)
Bun.file(p).stat() Filesystem.isDir(p), Filesystem.size(p) statSync() (8x faster)
Bun.file(p).text() Filesystem.readText(p) readFile()
Bun.file(p).json() Filesystem.readJson<T>(p) readFile() + JSON.parse()
Bun.file(p).bytes() Filesystem.readBytes(p) readFile() (returns Buffer)
Bun.file(p).size Filesystem.size(p) statSync().size (8x faster)
Bun.file(p).type Filesystem.mimeType(p) mime-types library
Bun.write(p, data) Filesystem.write(p, data, mode?) writeFile() + lazy mkdir
- Filesystem.writeJson(p, data, mode?) write() with JSON.stringify

Performance Notes

The Filesystem module uses optimized implementations:

  • Metadata operations (exists, isDir, size): Use sync APIs (existsSync, statSync) for 8-15x speedup
  • Content operations (readText, write): Use async APIs to avoid blocking thread during I/O
  • Directory creation: Lazy - only creates directories when write fails with ENOENT
  • MIME type detection: Uses mime-types library for broader format support than Bun

Testing

Always run tests before pushing changes:

cd packages/opencode
bun test

Quick Reference for LLM Agents

What you're doing: Migrating one file at a time from Bun-specific file APIs to the Filesystem module.

Commands to run:

  1. cd packages/opencode - Always work from the package directory
  2. After migration: bun test - Ensure tests pass before pushing

What to migrate:

  • Bun.file(path).exists()Filesystem.exists(path)
  • Bun.file(path).text()Filesystem.readText(path)
  • Bun.file(path).json()Filesystem.readJson(path)
  • Bun.file(path).bytes()Filesystem.readBytes(path)
  • Bun.file(path).stat()Filesystem.isDir(path) or Filesystem.size(path)
  • Bun.file(path).sizeFilesystem.size(path)
  • Bun.file(path).typeFilesystem.mimeType(path)
  • Bun.write(path, content)Filesystem.write(path, content)
  • Bun.write(path, JSON.stringify(data))Filesystem.writeJson(path, data)

Do NOT migrate (leave as-is):

  • Bun.hash.xxHash32 - Keep using Bun's hash
  • Bun.stdin - Keep using Bun's stdin

After merging your PR: Return to this file and check off the file you just migrated from the checklist.

Migration Checklist

Phase 1: Centralize File Operations (DONE )

  • Create src/util/filesystem.ts with all file operations
  • Implement optimized sync variants for metadata (exists, isDir, size)
  • Implement async variants for content operations (read, write)
  • Add lazy directory creation for write operations
  • Add comprehensive test coverage (31 tests)

Phase 2: Migrate Source Files

For each file using Bun.file APIs, replace with Filesystem equivalents:

Pattern:

// Remove Bun imports
import { Bun } from "bun" // ❌ Remove this

// Add Filesystem import
import { Filesystem } from "./util/filesystem" // ✅ Add this

// Replace all Bun.file() calls
const exists = await Bun.file(path).exists() // ❌
const exists = await Filesystem.exists(path) // ✅

const content = await Bun.file(path).text() // ❌
const content = await Filesystem.readText(path) // ✅

await Bun.write(path, content) // ❌
await Filesystem.write(path, content) // ✅

Files to migrate:

  • src/tool/read.ts - stat(), type, bytes(), text()
  • src/tool/write.ts - exists(), text(), Bun.write()
  • src/tool/edit.ts - exists(), stat(), text(), write()
  • src/file/time.ts - stat()
  • src/file/index.ts - text(), bytes(), exists(), type
  • src/mcp/auth.ts - json(), Bun.write()
  • src/storage/storage.ts - json(), Bun.write()
  • src/lsp/server.ts - exists(), write(), text()
  • ... (see full list above)

Phase 3: Handle Special Cases

  • Install @types/mime-types (already done)
  • Run all tests to verify functionality
  • Update package.json to remove Bun-only dependencies if any

Migration Priority

  1. High: Files with >5 Bun.file usages or critical paths
  2. Medium: Files with 2-5 usages
  3. Low: Files with 1-2 usages or covered by other tests

Notes

  • Bun APIs return null on some errors where Node.js throws; ensure .catch(() => ...) is used appropriately
  • File permissions: Bun defaults may differ from Node.js defaults; explicitly pass mode: 0o600 where needed
  • Performance: Node.js fs/promises is generally comparable to Bun.file for most read operations (within 2x)
  • Metadata operations (exists, size) will be ~10x slower after migration
  • Streaming: Both support similar streaming APIs via createReadStream and createWriteStream