| name | bun-fs-helpers |
| description | Pure Bun-native filesystem utilities from @sidequest/core/fs. Use when you need command-injection-safe filesystem operations, prefer Bun over node:fs, or want token-efficient fs helpers. All functions use Bun.spawn, Bun.file(), or Bun.write() - no node:fs dependencies. |
Bun Filesystem Helpers
Pure Bun-native filesystem utilities from @sidequest/core/fs - zero node:fs dependencies, command-injection safe.
When to Use
- Writing new code - Always prefer these over node:fs
- Command injection concerns - All shell commands use array args (safe)
- Token efficiency - Smaller imports, faster operations
- Bun-first projects - Leverages Bun's native APIs
Available Functions
File Existence
import { pathExists, pathExistsSync } from "@sidequest/core/fs";
// Async
if (await pathExists("/path/to/file")) { }
// Sync
if (pathExistsSync("/path/to/file")) { }
Implementation: Uses Bun.file().exists() (async) or test -e command (sync)
Reading Files
import { readTextFile, readTextFileSync, readJsonFile, readJsonFileSync } from "@sidequest/core/fs";
// Async text
const content = await readTextFile("/path/to/file.txt");
// Sync text
const content = readTextFileSync("/path/to/file.txt");
// Async JSON
const data = await readJsonFile<MyType>("/path/to/data.json");
// Sync JSON
const data = readJsonFileSync<MyType>("/path/to/data.json");
Implementation: Uses Bun.file().text() (async) or cat command (sync)
Writing Files
import { writeTextFile, writeTextFileSync, writeJsonFile, writeJsonFileSync } from "@sidequest/core/fs";
// Async text
await writeTextFile("/path/to/file.txt", "content");
// Sync text
writeTextFileSync("/path/to/file.txt", "content");
// Async JSON
await writeJsonFile("/path/to/data.json", { foo: "bar" }, 2);
// Sync JSON
writeJsonFileSync("/path/to/data.json", { foo: "bar" }, 2);
Implementation: Uses Bun.write() (async) or printf via shell (sync)
Directory Operations
import { ensureDir, ensureDirSync, readDir, readDirAsync } from "@sidequest/core/fs";
// Create directory (recursive)
await ensureDir("/path/to/nested/dir");
ensureDirSync("/path/to/nested/dir");
// List directory contents
const files = readDir("/path/to/dir"); // Sync
const files = await readDirAsync("/path/to/dir"); // Async
Implementation: Uses mkdir -p command, ls -1 command
File Operations
import { copyFile, moveFile, rename, unlink, unlinkSync } from "@sidequest/core/fs";
// Copy file
await copyFile("/source.txt", "/dest.txt");
// Move file (copy + delete)
await moveFile("/source.txt", "/dest.txt");
// Rename/move atomically
await rename("/old-path.txt", "/new-path.txt");
// Delete file
await unlink("/path/to/file.txt");
unlinkSync("/path/to/file.txt");
Implementation: Uses Bun.write() for copy, mv command for rename, rm command for delete
File Stats
import { stat } from "@sidequest/core/fs";
const stats = await stat("/path/to/file.txt");
console.log(stats.size); // File size in bytes
console.log(stats.mtimeMs); // Last modified timestamp
Implementation: Uses Bun.file().size and Bun.file().lastModified
Use case: TOCTOU protection - check file before AND after operations
Hashing
import { sha256, sha256File, fastHash } from "@sidequest/core/fs";
// Hash string
const hash = sha256("content"); // Hex string (64 chars)
// Hash file
const hash = await sha256File("/path/to/file.pdf");
// Fast non-cryptographic hash (cache keys)
const hash = fastHash("content"); // bigint or number
Implementation: Uses Bun.CryptoHasher (SHA256) or Bun.hash (xxHash64)
Deep Equality
import { deepEquals } from "@sidequest/core/fs";
const equal = deepEquals(obj1, obj2); // Loose mode
const equal = deepEquals(obj1, obj2, true); // Strict mode
Implementation: Uses Bun.deepEquals()
Security Guarantees
✅ Command injection safe - All shell commands use array arguments:
// Safe - array args prevent injection
Bun.spawnSync(["mkdir", "-p", userInput]);
// Never - string interpolation allows injection
Bun.spawnSync(`mkdir -p ${userInput}`); // ❌ DON'T DO THIS
✅ TOCTOU protection - Use stat() before AND after file operations to detect tampering
Migration from node:fs
// Before (node:fs)
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
existsSync("/path");
mkdirSync("/path", { recursive: true });
readFileSync("/path", "utf8");
writeFileSync("/path", "content", "utf8");
// After (@sidequest/core/fs)
import { pathExistsSync, ensureDirSync, readTextFileSync, writeTextFileSync } from "@sidequest/core/fs";
pathExistsSync("/path");
ensureDirSync("/path");
readTextFileSync("/path");
writeTextFileSync("/path", "content");
Performance Characteristics
| Operation | Speed | Notes |
|---|---|---|
pathExists |
~1ms | Bun.file().exists() |
pathExistsSync |
~2ms | test -e command |
readTextFile |
~5-50ms | Depends on file size |
writeTextFile |
~5-20ms | Bun.write() is fast |
ensureDir |
~10ms | mkdir -p command |
readDir |
~5-15ms | ls -1 command |
sha256File |
~50-500ms | Depends on file size |
fastHash |
<1ms | xxHash64 is very fast |
Common Patterns
Atomic File Updates
import { pathExistsSync, writeTextFileSync, rename } from "@sidequest/core/fs";
// Write to temp file, then atomically rename
const tempPath = `${targetPath}.tmp`;
writeTextFileSync(tempPath, newContent);
await rename(tempPath, targetPath); // POSIX guarantees atomicity
Safe File Modification with TOCTOU Protection
import { stat, readTextFileSync, writeTextFileSync } from "@sidequest/core/fs";
// Get pre-modification stats
const preStat = await stat(filePath);
// Read and modify
const content = readTextFileSync(filePath);
const modified = transform(content);
// Verify file unchanged before writing
const postStat = await stat(filePath);
if (postStat.mtimeMs !== preStat.mtimeMs) {
throw new Error("File was modified during read (TOCTOU attack detected)");
}
writeTextFileSync(filePath, modified);
Idempotent Processing with Content Hashing
import { sha256File, pathExistsSync } from "@sidequest/core/fs";
const hash = await sha256File(sourceFile);
const processedMarker = `.processed/${hash}`;
if (pathExistsSync(processedMarker)) {
console.log("Already processed - skipping");
return;
}
// Process file...
writeTextFileSync(processedMarker, new Date().toISOString());
Implementation Details
All functions are exported from core/src/fs/index.ts and use:
Bun.file()- File existence, reading, size, timestampsBun.write()- Writing filesBun.spawn()/Bun.spawnSync()- Shell commands with array argsBun.CryptoHasher- SHA256 hashingBun.hash- Fast non-cryptographic hashing (xxHash64)Bun.deepEquals()- Deep equality checksBun.Glob- Recursive file scanning (used elsewhere, not exported from fs)
Zero node:fs dependencies in production code.
References
- Core package source:
core/src/fs/index.ts - Test coverage: 423 tests passing
- Used by: para-obsidian inbox processing, registry, PDF processor