@outfitter/file-ops
v0.1.0
Published
Workspace detection, secure path handling, and file locking for Outfitter
Maintainers
Readme
@outfitter/file-ops
Workspace detection, secure path handling, glob patterns, file locking, and atomic write utilities for Outfitter projects.
Installation
bun add @outfitter/file-opsQuick Start
import {
findWorkspaceRoot,
securePath,
glob,
withLock,
atomicWrite
} from "@outfitter/file-ops";
// Find workspace root by marker files (.git, package.json)
const rootResult = await findWorkspaceRoot(process.cwd());
if (rootResult.isOk()) {
const root = rootResult.value;
// Secure path resolution (prevents traversal attacks)
const pathResult = securePath("src/config.json", root);
if (pathResult.isOk()) {
console.log("Safe path:", pathResult.value);
}
}
// Find files with glob patterns
const files = await glob("**/*.ts", {
cwd: "/project",
ignore: ["node_modules/**", "**/*.test.ts"]
});
// Atomic write with file locking
await withLock("/path/to/file.json", async () => {
await atomicWrite("/path/to/file.json", JSON.stringify(data));
});API Reference
Workspace Detection
findWorkspaceRoot(startPath, options?)
Finds the workspace root by searching for marker files/directories.
const result = await findWorkspaceRoot("/project/src/lib");
if (result.isOk()) {
console.log("Workspace:", result.value); // "/project"
}Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| markers | string[] | [".git", "package.json"] | Marker files/directories to search for |
| stopAt | string | filesystem root | Stop searching at this directory |
// Custom markers for Rust or Python projects
const result = await findWorkspaceRoot(startPath, {
markers: ["Cargo.toml", "pyproject.toml"],
stopAt: "/home/user"
});getRelativePath(absolutePath)
Returns the path relative to the workspace root.
const result = await getRelativePath("/project/src/lib/utils.ts");
if (result.isOk()) {
console.log(result.value); // "src/lib/utils.ts"
}isInsideWorkspace(path, workspaceRoot)
Checks if a path is inside a workspace directory.
const inside = await isInsideWorkspace("/project/src/file.ts", "/project");
console.log(inside); // true
const outside = await isInsideWorkspace("/etc/passwd", "/project");
console.log(outside); // falsePath Security
IMPORTANT: These functions protect against path traversal attacks. Always use them when handling user-provided paths.
Security Model
| Attack Vector | Protection |
|--------------|------------|
| Path traversal (../) | Blocked by all security functions |
| Null bytes (\x00) | Rejected immediately |
| Absolute paths | Blocked when relative expected |
| Escape from base directory | Defense-in-depth verification |
securePath(path, basePath)
Validates and secures a user-provided path, preventing path traversal attacks.
// SAFE: Validates path stays within basePath
const result = securePath("data/file.json", "/app/workspace");
if (result.isOk()) {
// Safe to use: /app/workspace/data/file.json
console.log(result.value);
}
// These all return ValidationError:
securePath("../etc/passwd", base); // Traversal sequence
securePath("/etc/passwd", base); // Absolute path
securePath("file\x00.txt", base); // Null byteUNSAFE pattern - never do this:
// DON'T: User input directly in path.join
const bad = path.join("/base", userInput); // VULNERABLE!
// DO: Always validate with securePath first
const result = securePath(userInput, "/base");
if (result.isOk()) {
// Now safe to use
}isPathSafe(path, basePath)
Quick boolean check for path safety.
if (isPathSafe(userInput, basePath)) {
// Safe to proceed
}resolveSafePath(basePath, ...segments)
Safely joins multiple path segments.
const result = resolveSafePath("/app", "data", "users", "profile.json");
if (result.isOk()) {
console.log(result.value); // "/app/data/users/profile.json"
}
// Rejects dangerous segments
resolveSafePath("/app", "..", "etc"); // Error: traversal
resolveSafePath("/app", "/etc/passwd"); // Error: absolute segmentGlob Patterns
glob(pattern, options?)
Finds files matching a glob pattern. Uses Bun.Glob internally.
// Find all TypeScript files
const result = await glob("**/*.ts", { cwd: "/project" });
// Exclude test files and node_modules
const result = await glob("**/*.ts", {
cwd: "/project",
ignore: ["**/*.test.ts", "**/node_modules/**"]
});
// Include dot files
const result = await glob("**/.*", { cwd: "/project", dot: true });Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| cwd | string | process.cwd() | Base directory for matching |
| ignore | string[] | [] | Patterns to exclude |
| followSymlinks | boolean | false | Follow symbolic links |
| dot | boolean | false | Include dot files |
Pattern Syntax:
| Pattern | Matches |
|---------|---------|
| * | Any characters except / |
| ** | Any characters including / (recursive) |
| {a,b} | Alternation (matches a or b) |
| [abc] | Character class (matches a, b, or c) |
| !pattern | Negation (in ignore array) |
// Negation patterns in ignore array
const result = await glob("src/**/*.ts", {
cwd: "/project",
ignore: ["**/*.ts", "!**/index.ts"] // Ignore all except index.ts
});globSync(pattern, options?)
Synchronous version of glob.
const result = globSync("src/*.ts", { cwd: "/project" });File Locking
Advisory file locking for cross-process coordination. Uses .lock files to indicate locks.
Note: This is advisory locking. All processes must cooperate by using these APIs.
withLock(path, callback)
Recommended approach. Executes a callback while holding an exclusive lock, with automatic release.
const result = await withLock("/data/config.json", async () => {
const config = JSON.parse(await Bun.file("/data/config.json").text());
config.counter++;
await atomicWrite("/data/config.json", JSON.stringify(config));
return config.counter;
});
if (result.isOk()) {
console.log("New counter:", result.value);
} else if (result.error._tag === "ConflictError") {
console.log("File is locked by another process");
}acquireLock(path) / releaseLock(lock)
Manual lock management. Use withLock when possible.
const lockResult = await acquireLock("/data/file.db");
if (lockResult.isOk()) {
const lock = lockResult.value;
try {
// ... do work ...
} finally {
await releaseLock(lock);
}
}isLocked(path)
Checks if a file is currently locked.
if (await isLocked("/data/file.db")) {
console.log("File is in use");
}FileLock Interface
interface FileLock {
path: string; // Path to the locked file
lockPath: string; // Path to the .lock file
pid: number; // Process ID holding the lock
timestamp: number; // When lock was acquired
}Atomic Writes
Write files atomically using temp-file-then-rename strategy. This prevents partial writes and corruption.
atomicWrite(path, content, options?)
Writes content to a file atomically.
const result = await atomicWrite("/data/config.json", JSON.stringify(data));
if (result.isErr()) {
console.error("Write failed:", result.error.message);
}Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| createParentDirs | boolean | true | Create parent directories if needed |
| preservePermissions | boolean | false | Keep permissions from existing file |
| mode | number | 0o644 | File mode for new files |
// Preserve executable permissions
await atomicWrite("/scripts/run.sh", newContent, {
preservePermissions: true
});
// Create nested directories automatically
await atomicWrite("/data/deep/nested/file.json", content, {
createParentDirs: true
});atomicWriteJson(path, data, options?)
Serializes and writes JSON data atomically.
const result = await atomicWriteJson("/data/config.json", {
name: "app",
version: "1.0.0",
settings: { debug: false }
});Error Handling
All functions return Result types from @outfitter/contracts. Use .isOk() and .isErr() to handle outcomes.
import type { Result } from "@outfitter/contracts";
const result = await findWorkspaceRoot("/path");
if (result.isOk()) {
const workspace = result.value;
} else {
// result.error has _tag, message, and error-specific fields
console.error(result.error._tag, result.error.message);
}Error Types:
| Error | Functions | When |
|-------|-----------|------|
| NotFoundError | findWorkspaceRoot, getRelativePath | No workspace marker found |
| ValidationError | securePath, isPathSafe, resolveSafePath, atomicWriteJson | Invalid path or data |
| ConflictError | acquireLock, withLock | File already locked |
| InternalError | glob, releaseLock, withLock, atomicWrite | Filesystem or system error |
Dependencies
@outfitter/contracts- Result types and error classes@outfitter/types- Type utilities
License
MIT
