| name | bun |
| description | Reference for Bun runtime APIs used in this project. Use this skill whenever writing code that runs shell commands (Bun.$ or Bun.spawn), reading/writing files (Bun.file/Bun.write), running tests (bun test), or configuring the monorepo (workspaces, bunfig.toml). Prefer Bun APIs over Node.js equivalents — use Bun.$ instead of child_process, Bun.file() instead of fs.readFile, bun:test instead of jest. |
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
await $`git clone --depth 1 ${url} ${dest}`
const sha = await $`git rev-parse HEAD`.cwd(dir).text()
const sha = (await $`git rev-parse HEAD`.cwd(dir).text()).trim()
const result = await $`echo '{"ok": true}'`.json()
for await (const line of $`git log --oneline -5`.cwd(dir).lines()) {
console.log(line)
}
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) {
}
Setting cwd and env
await $`git status`.cwd("/path/to/repo")
await $`echo $MY_VAR`.env({ ...process.env, MY_VAR: "value" })
$.cwd("/default/dir")
$.env({ ...process.env, GIT_TERMINAL_PROMPT: "0" })
Piping and redirection
const count = await $`git log --oneline | wc -l`.cwd(dir).text()
await $`git diff > ${Bun.file("changes.patch")}`
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}`
To pass raw (unescaped) strings:
await $`echo ${{ raw: "$(date)" }}`
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.
const proc = Bun.spawn(["git", "clone", url, dest], {
cwd: "/tmp",
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
stdout: "pipe",
stderr: "pipe",
})
const exitCode = await proc.exited
const stdout = await proc.stdout.text()
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).
const content = await Bun.file("config.toml").text()
const data = await Bun.file("installed.json").json()
const buf = await Bun.file("binary.dat").arrayBuffer()
const exists = await Bun.file("config.toml").exists()
const size = Bun.file("config.toml").size
await Bun.write("config.toml", tomlString)
await Bun.write("dest.txt", Bun.file("src.txt"))
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
bun test scanner
bun test -t "validates"
bun test --watch
bun test --timeout 10000
bun test --bail
Workspaces
Bun uses the standard package.json workspaces field. Same as npm/yarn.
{
"name": "skilltap-monorepo",
"private": true,
"workspaces": ["packages/*"]
}
Reference workspace packages with workspace:*:
{
"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]
exact = true
[test]
preload = ["./packages/test-utils/src/setup.ts"]
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")
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 })
await fs.rm(tmpDir, { recursive: true })
Path utilities
import { join, resolve, dirname, basename } from "node:path"
Environment
Bun.env.HOME
Bun.env.XDG_CONFIG_HOME