Skip to content

Latest commit

 

History

History
397 lines (316 loc) · 10.3 KB

File metadata and controls

397 lines (316 loc) · 10.3 KB

Agent Guide: opencode-dotenv

This document provides essential information for AI agents working on the opencode-dotenv project.

Project Overview

OpenCode plugin that loads .env files at startup. This is a Bun runtime plugin that parses environment files and makes variables available to the OpenCode environment.

Runtime: Bun (required - not compatible with Node.js) Type: OpenCode plugin Language: TypeScript (ESM)

Critical Architectural Limitation

This plugin cannot set environment variables for use in OpenCode's config file. OpenCode parses opencode.jsonc (including {env:VAR} resolution) before loading plugins. Therefore:

  • Variables set by this plugin are available to chat sessions and tool executions
  • Variables set by this plugin are NOT available to {env:VAR} references in opencode.jsonc
  • For config variables (API keys, etc.), users must set them in shell profile before starting OpenCode

See docs/ARCHITECTURE.md for detailed startup sequence diagrams.

Essential Commands

Build & Install

# Install dependencies
bun install
# or
make install

# Build project (compiles src/index.ts to dist/)
bun run build
# or
make build

# Clean build artifacts and dependencies
make clean

Testing

# Run tests
make test
# or
bun test

# Note: Uses bun:test framework
# Test files are located alongside source files (src/index.test.ts)

Benchmarking

# Run performance benchmarks
make bench
# or
bun run bench/init.bench.ts

Code Quality

# Lint code (placeholder - not configured)
make lint

# Format code (placeholder - not configured)
make fmt

Publishing

# Build and publish to npm
npm publish
# or
make publish

# Dry-run publish to preview
make publish-dry

Manual Testing (Bun-specific)

# Run with Bun directly
bun run src/index.ts

Code Organization

opencode-dotenv/
├── src/
│   ├── index.ts              # Entry point, re-exports
│   ├── plugin.ts             # Main plugin implementation
│   ├── test-utils.ts         # Internal test utilities
│   └── profiler/
│       ├── index.ts          # Profiler exports
│       └── profiler.ts       # Performance profiler
├── bench/
│   ├── utils.ts              # Benchmark utilities
│   └── init.bench.ts         # Initialization benchmarks
├── docs/
│   └── ARCHITECTURE.md       # Plugin architecture and startup sequence
├── dist/                     # Built output (generated, not in git)
├── package.json
├── Makefile
└── README.md

Key Points:

  • Main plugin entry at src/index.ts
  • Profiler module at src/profiler/ for performance tracking
  • Benchmarks in bench/ directory
  • Tests co-located with source code
  • Only dist/ directory is published to npm (see package.json files field)
  • Source files are excluded from npm package via .npmignore
  • Architecture docs in docs/ directory

Naming Conventions & Style

Constants

Upper case with underscores:

const PLUGIN_NAME = "opencode-dotenv"
const CONFIG_NAME = "dotenv.jsonc"
const LOG_FILE = `${homedir()}/.local/share/opencode/dotenv.log`
const LOAD_GUARD = "__opencodeDotenvLoaded"

Interfaces & Types

PascalCase:

interface DotEnvConfig {
  files: string[]
  load_cwd_env?: boolean
  logging?: {
    enabled?: boolean
  }
}

Functions

camelCase:

function parseDotenv(content: unknown): Record<string, string>
function expandPath(path: string): string
async function loadConfig(): Promise<DotEnvConfig>

Exports

  • Named exports for utilities: export { parseDotenv, globalProfiler }
  • Default export for main plugin: export default DotEnvPlugin
  • Named export for plugin type: export const DotEnvPlugin: Plugin

Import Style

Named imports from packages:

import type { Plugin } from "@opencode-ai/plugin"
import { homedir } from "node:os"
import { parse } from "jsonc-parser"
import { globalProfiler } from "./profiler"

Testing Approach

Framework

  • Uses bun:test (Bun's built-in test framework)
  • Test files use *.test.ts naming pattern
  • Tests located in same directory as source code

Test Patterns

Comprehensive coverage including:

  • Edge cases (empty lines, comments, mixed whitespace)
  • Quote handling (single, double, unquoted values)
  • Error handling (non-string content, type safety)
  • Complex scenarios (multiple variables, special characters)

Example test structure:

import { test, expect } from "bun:test"
import { parseDotenv } from "../src/index"

test("descriptive test name", () => {
  const result = parseDotenv("KEY=value")
  expect(result.KEY).toBe("value")
})

Running Tests

make test
# or directly
bun test

Key Code Patterns

Async/Await Pattern

All file operations use async/await with Bun API:

const file = Bun.file(configPath)
const content = await file.text()

Non-Blocking Logging

Fire-and-forget async logging (never blocks):

function log(message: string): void {
  if (!loggingEnabled || isTestEnv) return

  const timestamp = new Date().toISOString()
  const line = `[${timestamp}] ${message}\n`

  // Fire and forget - never block
  mkdir(LOG_DIR, { recursive: true })
    .then(() => appendFile(LOG_FILE, line))
    .catch(() => {
      // Ignore errors - never block
    })
}

Configuration Loading

Multi-path search with first-match strategy:

  1. Local config: ./dotenv.jsonc
  2. Global config: ~/.config/opencode/dotenv.jsonc

First found file is used; no merging.

Path Expansion

Tilde (~) expansion for home directory:

function expandPath(path: string): string {
  return path.replace(/^~/, homedir())
}

Load Guard Pattern

Prevents double plugin initialization:

if ((globalThis as any)[LOAD_GUARD]) {
  return {}
}
(globalThis as any)[LOAD_GUARD] = true

Profiler Integration

Performance tracking with the profiler module:

import { globalProfiler } from "./profiler"

// Start timing
globalProfiler.initStart()

// Record file load metrics
globalProfiler.recordFileLoad(filePath, duration, varCount, success)

// Complete initialization
globalProfiler.initComplete("ready")

// Export performance report
const report = globalProfiler.export()

Performance Optimizations

The plugin is optimized for fast startup (<1ms typical):

Sequential Config Loading

  • Config files are checked in order (local first, then global)
  • First successful config is used; no merging
  • Fast failure on missing/invalid configs

Sequential File Loading

  • .env files are loaded sequentially to maintain order
  • Later files override earlier ones (important for variable precedence)
  • Performance metrics recorded for each file

Non-Blocking Logging

  • Logging is disabled by default for maximum performance
  • When enabled, uses async fire-and-forget writes
  • Never blocks the main plugin execution
  • Skipped entirely in test environments

Load Guard

  • Early return if plugin already loaded (globalThis.__opencodeDotenvLoaded)
  • Prevents redundant initialization (subsequent calls: ~0.01ms)
  • Essential for performance when plugin is called multiple times

Profiler

  • Built-in profiler for performance tracking
  • Records config load times, file load times, and total initialization
  • Can be exported for analysis via getPerformanceReport()

To run benchmarks:

make bench

Important Gotchas

Runtime Requirement

  • Must use Bun runtime - this plugin uses Bun-specific APIs (Bun.file())
  • Not compatible with Node.js

Configuration Format

  • Uses JSONC format (JSON with Comments)
  • Supports trailing commas and inline comments
  • Parsed with jsonc-parser library
  • Config file name: dotenv.jsonc

Logging Behavior

  • Defaults to disabled for performance
  • Only enabled when explicitly set to true in config (logging.enabled = true)
  • Uses async fire-and-forget writes (never blocks)
  • Skipped in test environments (NODE_ENV=test or BUN_TEST set)
  • Writes to ~/.local/share/opencode/dotenv.log
  • Silent failures in logging - won't crash if log file is unwritable

Makefile Targets

The following targets are stubs/placeholders:

  • make lint - only echoes "Linting code..."
  • make fmt - only echoes "Formatting code..."

Functional targets:

  • make test - runs bun test
  • make bench - runs bun run bench/init.bench.ts

Type Assertions

Code uses type assertions in some places:

const config = parse(content, [], { allowTrailingComma: true }) as DotEnvConfig
(globalThis as any)[LOAD_GUARD] = true

Config File Search Order

Config files are searched in this order; first found wins:

  1. ./dotenv.jsonc (project-specific)
  2. ~/.config/opencode/dotenv.jsonc (global)

No merging between config files.

Environment Variable Load Order

Variables are loaded in this order; later values override earlier ones:

  1. Files from config.files array (in specified order)
  2. .env from current working directory (if load_cwd_env !== false)

Process.cwd() Usage

The plugin uses process.cwd() to determine the current working directory, which is where OpenCode was launched from.

Dependencies

Production

  • jsonc-parser - Parses JSONC configuration files with comments support

Peer Dependencies

  • @opencode-ai/plugin - OpenCode plugin type definitions

Dev Dependencies

None specified (uses Bun's built-in test runner)

Publishing

The package is published to npm with:

  • Main entry: ./dist/index.js
  • Includes only: dist/, README.md, LICENSE
  • Prepublish hook: automatically builds before publishing (prepublishOnly: "bun run build")

Development Workflow

  1. Make changes to src/index.ts
  2. Add/update tests in src/index.test.ts
  3. Run bun test to verify
  4. Run make bench to check performance
  5. Run make build to compile
  6. Test locally with Bun
  7. Publish with make publish

Debugging

View plugin activity logs:

tail -f ~/.local/share/opencode/dotenv.log

Logs include:

  • Plugin startup/shutdown
  • Config file loading
  • File loading attempts
  • Variable counts
  • Errors (with details)

Get performance report programmatically:

import { getPerformanceReport } from "opencode-dotenv"

const report = getPerformanceReport()
console.log(JSON.stringify(report, null, 2))