Get the FREE Ultimate OpenClaw Setup Guide →

bun

Scanned
npx machina-cli add skill nklisch/skilltap/bun --openclaw
Files (1)
SKILL.md
9.1 KB

Bun Runtime Reference

This project uses Bun as its runtime. Prefer Bun-native APIs over Node.js equivalents.

Bun.$ — Shell API

The primary way to run external commands. Cross-platform, auto-escapes interpolated values, throws on non-zero exit by default.

import { $ } from "bun"

Running commands

// Simple command — prints to stdout by default
await $`git clone --depth 1 ${url} ${dest}`

// Capture output as string (.text() auto-quiets)
const sha = await $`git rev-parse HEAD`.cwd(dir).text()
// sha === "abc123def456\n"

// Capture output as trimmed string
const sha = (await $`git rev-parse HEAD`.cwd(dir).text()).trim()

// Capture as JSON
const result = await $`echo '{"ok": true}'`.json()

// Read output line-by-line
for await (const line of $`git log --oneline -5`.cwd(dir).lines()) {
  console.log(line)
}

// Capture stdout and stderr as Buffers
const { stdout, stderr } = await $`git status`.cwd(dir).quiet()

Error handling

Non-zero exit codes throw ShellError by default:

try {
  await $`git clone ${url} ${dest}`
} catch (err) {
  console.error(`Exit code: ${err.exitCode}`)
  console.error(err.stderr.toString())
}

Use .nothrow() to handle exit codes manually:

const { exitCode, stdout, stderr } = await $`git diff --quiet`.cwd(dir).nothrow().quiet()
if (exitCode !== 0) {
  // There are unstaged changes
}

Setting cwd and env

// Per-command
await $`git status`.cwd("/path/to/repo")
await $`echo $TOKEN`.env({ ...process.env, TOKEN: "secret" })

// Global defaults
$.cwd("/default/dir")
$.env({ ...process.env, GIT_TERMINAL_PROMPT: "0" })

Piping and redirection

// Pipe between commands
const count = await $`git log --oneline | wc -l`.cwd(dir).text()

// Redirect to file
await $`git diff > ${Bun.file("changes.patch")}`

// Redirect stderr to stdout
const output = await $`git clone ${url} 2>&1`.text()

Interpolation safety

All interpolated values are auto-escaped — no shell injection:

const userInput = "foo; rm -rf /"
await $`echo ${userInput}`  // SAFE: treated as single argument

To pass raw (unescaped) strings:

await $`echo ${{ raw: "$(date)" }}`  // Executes command substitution

Pattern: skilltap git.ts module

import { $ } from "bun"
import type { Result } from "./types"

export async function clone(
  url: string,
  dest: string,
  opts?: { depth?: number; branch?: string }
): Promise<Result<void, GitError>> {
  try {
    const args = ["git", "clone"]
    if (opts?.depth) args.push("--depth", String(opts.depth))
    if (opts?.branch) args.push("--branch", opts.branch)
    args.push(url, dest)

    await $`${args}`.quiet()
    return { ok: true, value: undefined }
  } catch (err) {
    return {
      ok: false,
      error: new GitError(err.stderr?.toString() ?? err.message),
    }
  }
}

export async function revParse(dir: string): Promise<Result<string, GitError>> {
  try {
    const sha = (await $`git rev-parse HEAD`.cwd(dir).text()).trim()
    return { ok: true, value: sha }
  } catch (err) {
    return { ok: false, error: new GitError(err.message) }
  }
}

Bun.spawn — Subprocess API

Lower-level than $. Use when you need fine-grained control over stdin/stdout streams, IPC, or synchronous execution.

// Async
const proc = Bun.spawn(["git", "clone", url, dest], {
  cwd: "/tmp",
  env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
  stdout: "pipe",   // capture stdout as ReadableStream
  stderr: "pipe",   // capture stderr as ReadableStream
})

const exitCode = await proc.exited
const stdout = await proc.stdout.text()

// Sync (blocking — good for CLI tools)
const result = Bun.spawnSync(["git", "rev-parse", "HEAD"], { cwd: dir })
if (result.success) {
  console.log(result.stdout.toString().trim())
}

When to use Bun.$ vs Bun.spawn

Use caseAPI
Running git commands, shell scriptsBun.$
Piping between commandsBun.$
Simple command + capture outputBun.$
Streaming stdin/stdoutBun.spawn
IPC with child processBun.spawn
Sync execution (CLI tools)Bun.spawnSync
Invoking agent CLIs for semantic scanBun.spawn (need stdout stream)

Bun.file / Bun.write — File I/O

Faster than Node's fs module. Returns a BunFile (lazy, doesn't read until consumed).

// Read file as string
const content = await Bun.file("config.toml").text()

// Read as JSON
const data = await Bun.file("installed.json").json()

// Read as ArrayBuffer
const buf = await Bun.file("binary.dat").arrayBuffer()

// Check if file exists
const exists = await Bun.file("config.toml").exists()

// Get file size
const size = Bun.file("config.toml").size  // bytes

// Write string to file
await Bun.write("config.toml", tomlString)

// Write from another BunFile (copy)
await Bun.write("dest.txt", Bun.file("src.txt"))

// Write JSON
await Bun.write("data.json", JSON.stringify(data, null, 2))

vs Node.js fs

Node.jsBun
fs.readFileSync(path, "utf-8")await Bun.file(path).text()
fs.writeFileSync(path, data)await Bun.write(path, data)
fs.existsSync(path)await Bun.file(path).exists()
fs.statSync(path).sizeBun.file(path).size

Node's fs still works in Bun — use it when you need fs.mkdirSync, fs.readdirSync, symlink operations, etc. that Bun.file/Bun.write don't cover.

bun test — Test Runner

Jest-compatible test runner. Import from "bun:test".

import { describe, test, expect, beforeAll, afterAll, mock, spyOn } from "bun:test"

See references/testing.md for the full test API reference.

Quick patterns

import { describe, test, expect, beforeAll, afterAll } from "bun:test"

describe("scanner", () => {
  let tmpDir: string

  beforeAll(async () => {
    tmpDir = await createFixtureRepo()
  })

  afterAll(async () => {
    await fs.rm(tmpDir, { recursive: true })
  })

  test("finds root SKILL.md", async () => {
    const skills = await scanForSkills(tmpDir)
    expect(skills).toHaveLength(1)
    expect(skills[0].name).toBe("test-skill")
  })

  test("validates frontmatter", async () => {
    const skills = await scanForSkills(tmpDir)
    expect(skills[0].valid).toBe(true)
    expect(skills[0].description).toMatch(/test/)
  })

  test.todo("handles deep scan with confirmation")
})

Running tests

bun test                          # Run all tests
bun test scanner                  # Filter by filename
bun test -t "validates"           # Filter by test name
bun test --watch                  # Watch mode
bun test --timeout 10000          # 10s per-test timeout
bun test --bail                   # Stop on first failure

Workspaces

Bun uses the standard package.json workspaces field. Same as npm/yarn.

// Root package.json
{
  "name": "skilltap-monorepo",
  "private": true,
  "workspaces": ["packages/*"]
}

Reference workspace packages with workspace:*:

// packages/cli/package.json
{
  "name": "skilltap",
  "dependencies": {
    "@skilltap/core": "workspace:*"
  },
  "devDependencies": {
    "@skilltap/test-utils": "workspace:*"
  }
}

Import workspace packages normally:

import { installSkill } from "@skilltap/core"

bunfig.toml

Bun-specific configuration. Lives at project root.

[install]
# Use exact versions by default
exact = true

[test]
# Preload files before tests
preload = ["./packages/test-utils/src/setup.ts"]
# Default timeout
timeout = 10000

bun build --compile

Compile to a standalone binary with no runtime dependency:

bun build --compile packages/cli/src/index.ts --outfile skilltap

The binary includes the Bun runtime — runs on machines without Bun installed.

Cross-compile targets

bun build --compile --target=bun-linux-x64 packages/cli/src/index.ts --outfile skilltap-linux
bun build --compile --target=bun-darwin-arm64 packages/cli/src/index.ts --outfile skilltap-macos

Other Bun APIs

Hashing

const hash = Bun.hash("some string")              // fast non-crypto hash
const sha = new Bun.CryptoHasher("sha256")
  .update("content")
  .digest("hex")

Temporary files

const tmpDir = `${import.meta.dir}/../.tmp/${crypto.randomUUID()}`
await fs.mkdir(tmpDir, { recursive: true })
// ... use tmpDir ...
await fs.rm(tmpDir, { recursive: true })

Path utilities

import { join, resolve, dirname, basename } from "node:path"
// Node path utilities work perfectly in Bun

Environment

Bun.env.HOME           // same as process.env.HOME
Bun.env.XDG_CONFIG_HOME // standard config path

Source

git clone https://github.com/nklisch/skilltap/blob/main/.agents/skills/bun/SKILL.mdView on GitHub

Overview

This skill provides a reference to Bun's native APIs used in the project, including shell commands (Bun.$, Bun.spawn), file I/O (Bun.file, Bun.write), test running (bun test), and monorepo config (workspaces, bunfig.toml). It emphasizes preferring Bun APIs over Node.js equivalents to improve consistency and performance.

How This Skill Works

The skill demonstrates using Bun.$ for shell interactions with automatic escaping, cwd/env controls, and various output capture modes. It also covers Bun.file for file reads, Bun.write for writes, and bun test for testing, plus basic usage of bunfig.toml and workspaces for monorepo setup. Examples show safe interpolation, error handling, and redirect/pipeline patterns.

When to Use It

  • You need to run shell commands and capture stdout/stderr within a Bun-based script (e.g., $`git status`.text() or .lines()).
  • You must read or write files using Bun-native APIs instead of Node.js fs equivalents.
  • You are configuring or querying monorepo settings with workspaces or bunfig.toml.
  • You want to run tests with bun test instead of Jest or other test runners.
  • You want fine-grained control over subprocesses using Bun.spawn or similar, beyond simple command execution.

Quick Start

  1. Step 1: import { $ } from 'bun'
  2. Step 2: run a command with $ and capture output, e.g., const out = await $`git status`.text()
  3. Step 3: read or write files with Bun.file('path') and Bun.write('path', content)

Best Practices

  • Prefer Bun.$ over Node's child_process for shell commands to leverage Bun's escaping and cross-platform behavior.
  • Use Bun.file() and Bun.write() instead of fs.readFile/fs.writeFile for I/O operations.
  • Use bun:test instead of jest to align with Bun's native testing tooling.
  • Leverage per-command cwd and env controls and consider .nothrow() for manual exit-code handling.
  • Sanitize or safely interpolate user input with automatic escaping, and use raw strings when needed with explicit constructs.

Example Use Cases

  • await $`git clone --depth 1 ${url} ${dest}` to clone a repo and print output by default.
  • const sha = await $`git rev-parse HEAD`.cwd(dir).text().then(s => s.trim()) to read the current commit.
  • await $`git diff > ${Bun.file('changes.patch')}` to redirect output to a file.
  • const userInput = 'foo; rm -rf /'; await $`echo ${userInput}` // SAFE: auto-escaped as a single arg.
  • const config = await Bun.file('bunfig.toml').text() // Read config-like file content via Bun.file.

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers