bun
Scannednpx machina-cli add skill nklisch/skilltap/bun --openclawBun 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 case | API |
|---|---|
| Running git commands, shell scripts | Bun.$ |
| Piping between commands | Bun.$ |
| Simple command + capture output | Bun.$ |
| Streaming stdin/stdout | Bun.spawn |
| IPC with child process | Bun.spawn |
| Sync execution (CLI tools) | Bun.spawnSync |
| Invoking agent CLIs for semantic scan | Bun.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.js | Bun |
|---|---|
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).size | Bun.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
- Step 1: import { $ } from 'bun'
- Step 2: run a command with $ and capture output, e.g., const out = await $`git status`.text()
- 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.