Docs Packages

@procedures/fs

packages/client-fs/README.md on GitHub · Package map · Procedures

File system operations as RPC procedures. Read, write, copy, move, glob files and directories via client.call().

Overview#

client-fs provides comprehensive file system access through the client procedure ecosystem with:

  • File operations: Read, write, stat, exists, remove
  • Directory operations: Create, list, copy, move
  • Pattern matching: Glob file search via the Node.js fs.glob API (node:fs/promises, requires Node >= 22)
  • JSON utilities: Read and parse JSON files
  • Zod validation: Runtime type safety on all inputs

The package auto-registers 11 procedures: fs.read, fs.write, fs.exists, fs.stat, fs.mkdir, fs.rm, fs.readdir, fs.copy, fs.move, fs.glob, fs.read.json

Architecture#

Diagram source (Mermaid). GitHub draws it on the page of the file.
graph TB
    subgraph "Client Ecosystem"
        Client["@procedures/client"]
        Registry["Procedure Registry"]
    end

    subgraph "client-fs Package"
        Register["register.ts<br/>(Auto-registers procedures)"]
        Types["types.ts<br/>(Zod Schemas + Types)"]

        subgraph "Procedure Categories"
            FileOps["File Operations<br/>read, write, exists, stat, rm"]
            DirOps["Directory Operations<br/>mkdir, readdir, copy, move"]
            PatternOps["Pattern Operations<br/>glob, read.json"]
        end
    end

    subgraph "Node.js Modules"
        NodeFs["node:fs/promises"]
        NodePath["node:path"]
        NodeGlob["node:fs/promises glob<br/>(Node >= 22)"]
    end

    subgraph "File System"
        FS["Local File System"]
    end

    Register --> Client
    Register --> Registry

    FileOps --> NodeFs
    DirOps --> NodeFs
    PatternOps --> NodeGlob

    NodeFs --> NodePath
    NodeFs --> FS
    NodeGlob --> FS

    Types -.validates.-> FileOps
    Types -.validates.-> DirOps
    Types -.validates.-> PatternOps

    style Register fill:#e1f5ff
    style Types fill:#f0f0f0

Package Dependency Flow#

Diagram source (Mermaid). GitHub draws it on the page of the file.
graph LR
    App["Your Application"]
    ClientFs["@procedures/fs"]
    Client["@procedures/client"]
    Zod["zod"]
    NodeModules["node:fs/promises<br/>(glob: Node >= 22)"]

    App --> ClientFs
    ClientFs --> Client
    ClientFs --> Zod
    ClientFs --> NodeModules

    App -.client.call.-> Client
    Client -.executes.-> ClientFs
    ClientFs -.validates.-> Zod
    ClientFs -.file ops.-> NodeModules

Installation#

npm install @procedures/fs

Peer Dependencies#

{
  "@procedures/client": "github:mark1russell7/client#main",
  "zod": "^3.24.0"
}

API Reference#

File Operations#

fs.read#

Read file contents as a string.

const result = await client.exec(
  ["fs", "read"],
  {
    path: "./config.json",
    encoding: "utf8"
  }
);

console.log(result.content);
console.log(result.stats.size);

Input:

interface ReadInput {
  path: string;              // File path (absolute or relative)
  encoding?: string;         // Default: "utf8"
}

Output:

interface ReadOutput {
  content: string;           // File contents
  path: string;              // Resolved absolute path
  stats: StatOutput;         // File statistics
}

fs.write#

Write content to a file.

await client.exec(
  ["fs", "write"],
  {
    path: "./output.txt",
    content: "Hello, World!",
    mode: "write"  // or "append"
  }
);

Input:

interface WriteInput {
  path: string;              // File path
  content: string;           // Content to write
  encoding?: string;         // Default: "utf8"
  mode?: "write" | "append"; // Default: "write"
}

Output:

interface WriteOutput {
  path: string;              // Written file path
  bytesWritten: number;      // Number of bytes written
}

fs.exists#

Check if a path exists and get its type.

const result = await client.exec(
  ["fs", "exists"],
  { path: "./my-file.txt" }
);

if (result.exists) {
  console.log(`Path exists and is a ${result.stats.type}`);
}

Input:

interface ExistsInput {
  path: string;
}

Output:

type ExistsOutput =
  | { exists: false }
  | { exists: true; stats: StatOutput };

fs.stat#

Get file or directory statistics.

const result = await client.exec(
  ["fs", "stat"],
  { path: "./my-directory" }
);

console.log(result.type);  // "file" | "directory" | "other"
console.log(result.size);  // Size in bytes

Input:

interface StatInput {
  path: string;
}

Output:

interface StatOutput {
  path: string;
  type: FileType;            // "file" | "directory" | "other"
  size: number;              // Size in bytes
  mtime: string;             // Modified time (ISO)
  ctime: string;             // Created time (ISO)
  atime: string;             // Accessed time (ISO)
  mode: number;              // Unix permissions
}

enum FileType {
  File = "file",
  Directory = "directory",
  Other = "other"
}

fs.rm#

Remove a file or directory.

// Remove file
await client.exec(
  ["fs", "rm"],
  { path: "./old-file.txt" }
);

// Remove directory recursively
await client.exec(
  ["fs", "rm"],
  {
    path: "./old-directory",
    recursive: true,
    force: true
  }
);

Input:

interface RmInput {
  path: string;
  recursive?: boolean;       // Default: false
  force?: boolean;           // Ignore if doesn't exist (default: false)
}

Output:

interface RmOutput {
  path: string;
  removed: boolean;          // false if didn't exist with force=true
}

Directory Operations#

fs.mkdir#

Create a directory.

await client.exec(
  ["fs", "mkdir"],
  {
    path: "./new/nested/directory",
    recursive: true  // Create parents if needed
  }
);

Input:

interface MkdirInput {
  path: string;
  recursive?: boolean;       // Default: true
}

Output:

interface MkdirOutput {
  path: string;
  created: boolean;          // false if already existed
}

fs.readdir#

List directory contents.

const result = await client.exec(
  ["fs", "readdir"],
  {
    path: "./src",
    recursive: true,
    includeStats: true
  }
);

result.entries.forEach(entry => {
  console.log(`${entry.type}: ${entry.path}`);
  if (entry.stats) {
    console.log(`  Size: ${entry.stats.size} bytes`);
  }
});

Input:

interface ReaddirInput {
  path: string;
  recursive?: boolean;       // Default: false
  includeStats?: boolean;    // Default: false
}

Output:

interface ReaddirOutput {
  path: string;
  entries: ReaddirEntry[];
}

interface ReaddirEntry {
  name: string;              // Entry name
  path: string;              // Full path
  type: FileType;            // "file" | "directory" | "other"
  stats?: StatOutput;        // If includeStats=true
}

fs.copy#

Copy a file or directory.

// Copy file
await client.exec(
  ["fs", "copy"],
  {
    src: "./source.txt",
    dest: "./destination.txt",
    overwrite: true
  }
);

// Copy directory recursively
await client.exec(
  ["fs", "copy"],
  {
    src: "./src-dir",
    dest: "./dest-dir",
    recursive: true
  }
);

Input:

interface CopyInput {
  src: string;
  dest: string;
  recursive?: boolean;       // Default: true
  overwrite?: boolean;       // Default: false
}

Output:

interface CopyOutput {
  src: string;
  dest: string;
}

fs.move#

Move or rename a file or directory.

await client.exec(
  ["fs", "move"],
  {
    src: "./old-name.txt",
    dest: "./new-name.txt",
    overwrite: false
  }
);

Input:

interface MoveInput {
  src: string;
  dest: string;
  overwrite?: boolean;       // Default: false
}

Output:

interface MoveOutput {
  src: string;
  dest: string;
}

Pattern Matching Operations#

fs.glob#

Find files matching a glob pattern using the Node.js fs.glob API (node:fs/promises, requires Node >= 22).

const result = await client.exec(
  ["fs", "glob"],
  {
    pattern: "src/**/*.{ts,tsx}",
    cwd: "/my/project",
    absolute: true,
    dot: false
  }
);

console.log(result.matches);
// ["/my/project/src/index.ts", "/my/project/src/components/App.tsx", ...]

Input:

interface GlobInput {
  pattern: string;           // Glob pattern (e.g., "**/*.ts")
  cwd?: string;              // Base directory (default: process.cwd())
  absolute?: boolean;        // Return absolute paths (default: false)
  dot?: boolean;             // Include dotfiles (default: false)
}

Output:

interface GlobOutput {
  pattern: string;
  matches: string[];         // Matching file paths
}

Glob Pattern Examples:

// All TypeScript files
"**/*.ts"

// All files in src directory
"src/**/*"

// TypeScript and JavaScript files
"**/*.{ts,js}"

// Note: node:fs/promises glob does NOT exclude node_modules by default;
// pass an `exclude` pattern or scope the pattern (e.g. "src/**/*.ts")
"src/**/*.ts"

// Include test files
"**/*.{test,spec}.ts"

fs.read.json#

Read and parse a JSON file.

const result = await client.exec(
  ["fs", "read.json"],
  { path: "./package.json" }
);

console.log(result.data);  // Parsed JSON object

Input:

interface ReadJsonInput {
  path: string;
}

Output:

interface ReadJsonOutput {
  path: string;
  data: unknown;             // Parsed JSON (cast to your type)
  stats: StatOutput;
}

Type-safe example:

interface PackageJson {
  name: string;
  version: string;
  dependencies: Record<string, string>;
}

const result = await client.exec(
  ["fs", "read.json"],
  { path: "./package.json" }
);

const pkg = result.data as PackageJson;
console.log(pkg.name, pkg.version);

Usage Examples#

Basic File Operations#

import { Client, LocalTransport, PROCEDURE_REGISTRY } from "@procedures/client";
import "@procedures/fs"; // registers its procedures

const client = new Client(new LocalTransport({ registry: PROCEDURE_REGISTRY }));

// Read a file
const { content } = await client.exec(
  ["fs", "read"],
  { path: "./README.md" }
);

// Write to a file
await client.exec(
  ["fs", "write"],
  {
    path: "./output.txt",
    content: "Generated content\n",
    mode: "write"
  }
);

// Append to a file
await client.exec(
  ["fs", "write"],
  {
    path: "./log.txt",
    content: `[${new Date().toISOString()}] Log entry\n`,
    mode: "append"
  }
);

// Check if file exists
const exists = await client.exec(
  ["fs", "exists"],
  { path: "./config.json" }
);

if (exists.exists) {
  // Get file stats
  const stats = await client.exec(
    ["fs", "stat"],
    { path: "./config.json" }
  );
  console.log(`File size: ${stats.size} bytes`);
}

Directory Operations#

// Create nested directories
await client.exec(
  ["fs", "mkdir"],
  {
    path: "./build/assets/images",
    recursive: true
  }
);

// List directory contents
const { entries } = await client.exec(
  ["fs", "readdir"],
  {
    path: "./src",
    includeStats: true
  }
);

entries.forEach(entry => {
  if (entry.type === "file") {
    console.log(`File: ${entry.name} (${entry.stats?.size} bytes)`);
  } else {
    console.log(`Directory: ${entry.name}`);
  }
});

// Recursive directory listing
const { entries: allFiles } = await client.exec(
  ["fs", "readdir"],
  {
    path: "./src",
    recursive: true
  }
);

console.log(`Total items: ${allFiles.length}`);

Copy and Move Operations#

// Backup a file
await client.exec(
  ["fs", "copy"],
  {
    src: "./important-data.json",
    dest: "./backups/important-data.backup.json"
  }
);

// Copy entire directory
await client.exec(
  ["fs", "copy"],
  {
    src: "./templates",
    dest: "./build/templates",
    recursive: true,
    overwrite: true
  }
);

// Rename a file
await client.exec(
  ["fs", "move"],
  {
    src: "./old-name.txt",
    dest: "./new-name.txt"
  }
);

// Move to different directory
await client.exec(
  ["fs", "move"],
  {
    src: "./temp/file.txt",
    dest: "./archive/file.txt",
    overwrite: false
  }
);

Pattern Matching with Glob#

// Find all TypeScript files
const { matches: tsFiles } = await client.exec(
  ["fs", "glob"],
  {
    pattern: "**/*.ts",
    cwd: "./src",
    absolute: true
  }
);

console.log(`Found ${tsFiles.length} TypeScript files`);

// Find test files
const { matches: testFiles } = await client.exec(
  ["fs", "glob"],
  {
    pattern: "**/*.{test,spec}.{ts,js}",
    cwd: "."
  }
);

// Find config files
const { matches: configs } = await client.exec(
  ["fs", "glob"],
  {
    pattern: "**/*.config.{js,ts,json}",
    dot: true  // Include dotfiles like .eslintrc.json
  }
);

JSON File Operations#

// Read package.json
const { data: pkg } = await client.exec(
  ["fs", "read.json"],
  { path: "./package.json" }
);

console.log(`Package: ${pkg.name}@${pkg.version}`);

// Read tsconfig.json
const { data: tsconfig } = await client.exec(
  ["fs", "read.json"],
  { path: "./tsconfig.json" }
);

console.log("Compiler options:", tsconfig.compilerOptions);

// Read and modify JSON
const { data: config } = await client.exec(
  ["fs", "read.json"],
  { path: "./config.json" }
);

config.lastUpdated = new Date().toISOString();

await client.exec(
  ["fs", "write"],
  {
    path: "./config.json",
    content: JSON.stringify(config, null, 2)
  }
);

Build Script Example#

// Clean build directory
await client.exec(
  ["fs", "rm"],
  {
    path: "./dist",
    recursive: true,
    force: true
  }
);

// Create build directory
await client.exec(
  ["fs", "mkdir"],
  { path: "./dist" }
);

// Find all source files
const { matches: sourceFiles } = await client.exec(
  ["fs", "glob"],
  {
    pattern: "src/**/*.ts",
    absolute: true
  }
);

// Process each file...
for (const file of sourceFiles) {
  const { content } = await client.exec(
    ["fs", "read"],
    { path: file }
  );

  // Transform content...
  const transformed = content.replace(/\.ts$/, ".js");

  await client.exec(
    ["fs", "write"],
    {
      path: file.replace("src/", "dist/").replace(".ts", ".js"),
      content: transformed
    }
  );
}

// Copy assets
await client.exec(
  ["fs", "copy"],
  {
    src: "./public",
    dest: "./dist/public",
    recursive: true
  }
);

Integration with Ecosystem#

Bundle Packages#

client-fs can be included in bundle packages:

// In bundle-dev/src/register.ts
import "@procedures/fs"; // registers its procedures
import "@procedures/shell"; // registers its procedures
import "@procedures/cli-run"; // registers its procedures

Dependencies#

Runtime Dependencies#

  • @procedures/client - Core client procedure system
  • zod (^3.24.0) - Runtime type validation

Implicit Dependencies#

  • node:fs/promises - Node.js file system promises API
  • node:path - Node.js path utilities
  • node:fs/promises glob - Glob pattern matching (used by the fs.glob procedure; requires Node >= 22)

Development Dependencies#

  • @mark1russell7/cue - Configuration and type utilities
  • typescript (^5.9.3) - TypeScript compiler
  • @types/node (^22.0.0) - Node.js type definitions

Package Structure#

client-fs/
├── src/
│   ├── types.ts                  # Zod schemas + TypeScript types
│   ├── register.ts               # Auto-registration
│   ├── index.ts                  # Main entry point
│   ├── procedures/
│   │   ├── fs.ts                 # Barrel export
│   │   └── fs/
│   │       ├── index.ts          # Procedure exports
│   │       ├── read.ts           # fs.read
│   │       ├── write.ts          # fs.write
│   │       ├── exists.ts         # fs.exists
│   │       ├── stat.ts           # fs.stat
│   │       ├── mkdir.ts          # fs.mkdir
│   │       ├── rm.ts             # fs.rm
│   │       ├── readdir.ts        # fs.readdir
│   │       ├── copy.ts           # fs.copy
│   │       ├── move.ts           # fs.move
│   │       ├── glob.ts           # fs.glob
│   │       └── json.ts           # fs.read.json
├── dist/                         # Compiled output
├── package.json
├── tsconfig.json
└── README.md

Development#

Build#

npm run build

Type Check#

npm run typecheck

Clean#

npm run clean

License#

MIT

Author#

Mark Russell marktheprogrammer17@gmail.com