npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@outfitter/file-ops

v0.1.0

Published

Workspace detection, secure path handling, and file locking for Outfitter

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-ops

Quick 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); // false

Path 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 byte

UNSAFE 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 segment

Glob 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