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

@procedures/snapshot

v0.1.0

Published

Snapshot and restore development environments with S3: git state, packages and files, with streamed archives and a safe restore.

Readme

@procedures/snapshot

npm version License: MIT

Environment snapshot/restore procedures for testing and recovery

Overview

client-snapshot provides comprehensive procedures for capturing and restoring complete development environment snapshots. It supports multiple preset levels (light, medium, heavy) to balance snapshot size vs. completeness, integrates with S3 for cloud storage, and orchestrates multiple client packages for Git, filesystem, and package management operations.

Key Features

  • Multi-Level Presets: Light (Git only ~500MB), Medium (+ node_modules ~2GB), Heavy (+ pnpm store ~10GB+)
  • Cloud Storage: S3 integration for remote snapshot storage and sharing
  • Complete State Capture: Git repos, branches, stashes, working trees, dependencies, and more
  • Intelligent Diffing: Compare current state against snapshots to track changes
  • Multipart Upload: Handles large snapshots with efficient S3 multipart uploads
  • Rich Metadata: Captures environment info, repository state, checksums for verification
  • Restore Safety: Configurable overwrite protection and target path options

Architecture

graph TB
    subgraph "client-snapshot"
        CREATE[snapshot.create]
        RESTORE[snapshot.restore]
        LIST[snapshot.list]
        DIFF[snapshot.diff]
        DELETE[snapshot.delete]
    end

    subgraph "Integrated Services"
        GIT[client-git<br/>Status, Stashes]
        FS[client-fs<br/>File Operations]
        S3[client-s3<br/>Upload/Download]
        PNPM[client-pnpm<br/>Dependencies]
    end

    subgraph "Storage"
        LOCAL[Local Temp<br/>Tarball Creation]
        CLOUD[S3 Bucket<br/>Archive + Metadata]
    end

    CREATE --> GIT
    CREATE --> FS
    CREATE --> LOCAL
    CREATE --> S3
    LOCAL --> CLOUD

    RESTORE --> S3
    RESTORE --> FS
    RESTORE --> PNPM
    CLOUD --> RESTORE

    LIST --> S3
    DIFF --> GIT
    DIFF --> S3
    DELETE --> S3

    style CREATE fill:#7ed321
    style RESTORE fill:#f5a623
    style LIST fill:#50e3c2
    style DIFF fill:#bd10e0
    style DELETE fill:#d0021b

Snapshot Flow

sequenceDiagram
    participant Client
    participant snapshot.create
    participant Git Repos
    participant Local FS
    participant S3

    Client->>snapshot.create: { name, preset, bucket }
    snapshot.create->>Git Repos: Get status, stashes, commits
    Git Repos-->>snapshot.create: Repository info
    snapshot.create->>Local FS: Create tarball based on preset
    Local FS-->>snapshot.create: Compressed archive
    snapshot.create->>snapshot.create: Calculate checksum
    snapshot.create->>S3: Upload archive + metadata
    S3-->>snapshot.create: S3 location
    snapshot.create-->>Client: { id, location, metadata, duration }

Installation

npm install @procedures/snapshot

Or with pnpm:

pnpm add @procedures/snapshot

Dependencies

This package depends on:

  • @procedures/client - Core client/procedure system
  • @procedures/s3 - S3 storage operations
  • @procedures/git - Git repository operations
  • @procedures/pnpm - Package manager operations
  • @procedures/fs - Filesystem operations
  • zod - Schema validation
  • tar - Archive creation and extraction

External Requirements

  • AWS Credentials: Configured for S3 access (via environment or AWS CLI)
  • Git: For repository state capture
  • pnpm (optional): For full dependency restoration
  • Node.js >= 22: Required for all operations

Usage

Creating Snapshots

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

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

// Create a lightweight snapshot (Git repos only)
const result = await client.exec(["snapshot", "create"], {
  name: "feature-auth-impl",
  preset: "light",
  bucket: "my-snapshots",
  description: "Before refactoring authentication",
});

console.log(result);
// {
//   id: "feature-auth-impl-1234567890-abc123",
//   location: "s3://my-snapshots/snapshots/feature-auth-impl/...",
//   metadata: {
//     id: "feature-auth-impl-1234567890-abc123",
//     name: "feature-auth-impl",
//     preset: "light",
//     createdAt: "2025-01-15T10:30:00.000Z",
//     environment: {
//       os: "linux",
//       nodeVersion: "v20.0.0",
//       pnpmVersion: "8.15.0",
//       username: "developer",
//       hostname: "dev-machine"
//     },
//     repositories: [
//       {
//         path: "/path/to/repo",
//         name: "@my/package",
//         branch: "feature/auth",
//         commit: "abc123...",
//         dirty: true,
//         stashCount: 2,
//         ahead: 3,
//         behind: 0
//       }
//     ],
//     checksum: "sha256:...",
//     archiveSize: 524288000,
//     description: "Before refactoring authentication"
//   },
//   uploadDuration: 15234
// }

Snapshot Presets

Choose the right preset for your needs:

// Light: ~500MB - Git repos only (fast, good for code snapshots)
await client.exec(["snapshot", "create"], {
  name: "quick-checkpoint",
  preset: "light",
  bucket: "my-snapshots",
});

// Medium: ~2GB - Git + node_modules (good for reproducible builds)
await client.exec(["snapshot", "create"], {
  name: "stable-state",
  preset: "medium",
  bucket: "my-snapshots",
});

// Heavy: ~10GB+ - Everything including pnpm store (full recovery)
await client.exec(["snapshot", "create"], {
  name: "complete-backup",
  preset: "heavy",
  bucket: "my-snapshots",
});

Listing Snapshots

// List all snapshots
const list = await client.exec(["snapshot", "list"], {
  bucket: "my-snapshots",
});

console.log(list);
// {
//   snapshots: [
//     {
//       id: "feature-auth-impl-1234567890-abc123",
//       name: "feature-auth-impl",
//       preset: "light",
//       createdAt: "2025-01-15T10:30:00.000Z",
//       size: 524288000,
//       os: "linux"
//     },
//     // ...
//   ],
//   count: 5
// }

// Filter by prefix
const filtered = await client.exec(["snapshot", "list"], {
  bucket: "my-snapshots",
  prefix: "feature-",
  maxResults: 10,
});

Restoring Snapshots

// Restore to original locations
const restore = await client.exec(["snapshot", "restore"], {
  id: "feature-auth-impl-1234567890-abc123",
  bucket: "my-snapshots",
  overwrite: false, // Safety: don't overwrite existing files
});

console.log(restore);
// {
//   success: true,
//   metadata: { ... },
//   restoredPaths: [
//     "/path/to/repo",
//     "/path/to/another-repo"
//   ],
//   downloadDuration: 8234,
//   extractDuration: 3456
// }

// Restore to different location
await client.exec(["snapshot", "restore"], {
  id: "feature-auth-impl-1234567890-abc123",
  bucket: "my-snapshots",
  targetPath: "/tmp/restored",
  overwrite: true,
});

Comparing Against Snapshots

// Diff current state vs snapshot
const diff = await client.exec(["snapshot", "diff"], {
  id: "feature-auth-impl-1234567890-abc123",
  bucket: "my-snapshots",
});

console.log(diff);
// {
//   snapshotMetadata: { ... },
//   repositories: [
//     {
//       path: "/path/to/repo",
//       branchDiff: {
//         current: "main",
//         snapshot: "feature/auth"
//       },
//       commitDiff: {
//         current: "xyz789...",
//         snapshot: "abc123..."
//       },
//       filesChanged: 15,
//       newStashes: 1
//     }
//   ],
//   summary: {
//     reposChanged: 1,
//     totalFilesChanged: 15,
//     isMatch: false
//   }
// }

// Check specific paths only
const partialDiff = await client.exec(["snapshot", "diff"], {
  id: "snapshot-id",
  bucket: "my-snapshots",
  paths: ["/path/to/specific/repo"],
});

Deleting Snapshots

// Delete a snapshot
const deleted = await client.exec(["snapshot", "delete"], {
  id: "old-snapshot-1234567890-xyz789",
  bucket: "my-snapshots",
});

console.log(deleted);
// {
//   deleted: true,
//   id: "old-snapshot-1234567890-xyz789"
// }

API Reference

Procedures

snapshot.create

Create an environment snapshot and upload to S3.

Input:

{
  name: string;              // Required: Snapshot name
  preset?: SnapshotPreset;   // Default: "medium" ("light" | "medium" | "heavy")
  bucket: string;            // Required: S3 bucket name
  paths?: string[];          // Specific paths to include (default: all ecosystem repos)
  description?: string;      // Optional description
}

Output:

{
  id: string;                // Unique snapshot ID
  location: string;          // S3 location (s3://bucket/key)
  metadata: SnapshotMetadata;
  uploadDuration: number;    // Milliseconds
}

snapshot.restore

Restore environment from S3 snapshot.

Input:

{
  id: string;                // Required: Snapshot ID
  bucket: string;            // Required: S3 bucket name
  targetPath: string;        // Required: the folder to restore into
  overwrite?: boolean;       // Default: false - safety protection
}

The archive is extracted into a staging folder first. Then each restored folder moves into targetPath. With overwrite: true, a restored folder replaces the existing folder of that name, so no stale file stays. Other folders of targetPath do not change.

Output:

{
  success: boolean;
  metadata: SnapshotMetadata;
  restoredPaths: string[];   // Paths that were restored
  downloadDuration: number;  // Milliseconds
  extractDuration: number;   // Milliseconds
}

snapshot.list

List available snapshots in S3 bucket.

Input:

{
  bucket: string;            // Required: S3 bucket name
  prefix?: string;           // Filter by name prefix
  maxResults?: number;       // Default: 100
}

The procedure reads all pages of the listing and returns the newest maxResults snapshots.

Output:

{
  snapshots: SnapshotListEntry[];
  count: number;
}

// SnapshotListEntry
{
  id: string;
  name: string;
  preset: SnapshotPreset;
  createdAt: string;         // ISO timestamp
  size: number;              // Bytes
  os: string;                // Platform it was created on
}

snapshot.diff

Compare current state against a snapshot.

Input:

{
  id: string;                // Required: Snapshot ID
  bucket: string;            // Required: S3 bucket name
  paths?: string[];          // Compare specific paths only
}

Output:

{
  snapshotMetadata: SnapshotMetadata;
  repositories: SnapshotDiffRepository[];
  summary: {
    reposChanged: number;
    totalFilesChanged: number;
    isMatch: boolean;        // true if no differences
  };
}

// SnapshotDiffRepository
{
  path: string;
  branchDiff?: { current: string, snapshot: string };
  commitDiff?: { current: string, snapshot: string };
  filesChanged: number;
  newStashes: number;
}

snapshot.delete

Delete a snapshot from S3.

Input:

{
  id: string;                // Required: Snapshot ID
  bucket: string;            // Required: S3 bucket name
}

Output:

{
  deleted: boolean;
  id: string;
}

Types

SnapshotMetadata

Complete metadata stored with each snapshot:

interface SnapshotMetadata {
  id: string;
  name: string;
  preset: SnapshotPreset;
  createdAt: string;         // ISO timestamp
  environment: {
    os: string;              // win32, darwin, linux
    nodeVersion: string;
    pnpmVersion: string;
    username: string;
    hostname: string;
  };
  repositories: RepositoryInfo[];
  checksum: string;          // SHA-256
  archiveSize: number;       // Bytes
  description?: string;
}

RepositoryInfo

Per-repository state information:

interface RepositoryInfo {
  path: string;              // Relative to snapshot root
  name: string;              // Package name
  branch: string;            // Current branch
  commit: string;            // HEAD commit hash
  dirty: boolean;            // Working tree has changes
  stashCount: number;        // Number of stashes
  remoteUrl?: string;        // Remote origin URL
  ahead: number;             // Commits ahead of remote
  behind: number;            // Commits behind remote
}

SnapshotPreset

type SnapshotPreset = "light" | "medium" | "heavy";

// Preset Details:
// - light:  Git repos only (.git, working tree, branches, stashes)        ~500MB
// - medium: + node_modules + pnpm lockfiles                              ~2GB
// - heavy:  + pnpm store + global installs + configs                     ~10GB+

Use Cases

1. Feature Branch Snapshots

// Before starting risky refactor
await client.exec(["snapshot", "create"], {
  name: `before-refactor-${Date.now()}`,
  preset: "medium",
  bucket: "team-snapshots",
  description: "Safe state before authentication refactor",
});

// After refactor, compare
const diff = await client.exec(["snapshot", "diff"], {
  id: "snapshot-id",
  bucket: "team-snapshots",
});

if (diff.summary.totalFilesChanged > 100) {
  console.warn("Large changes detected!");
}

2. CI/CD Reproducible Builds

// In CI pipeline - capture exact build state
await client.exec(["snapshot", "create"], {
  name: `build-${process.env.CI_COMMIT_SHA}`,
  preset: "medium",
  bucket: "ci-snapshots",
});

// Later - restore exact build environment
await client.exec(["snapshot", "restore"], {
  id: `build-${commitSha}`,
  bucket: "ci-snapshots",
  targetPath: "/tmp/build",
});

3. Team Environment Sharing

// Developer creates snapshot
await client.exec(["snapshot", "create"], {
  name: "onboarding-2025-01",
  preset: "heavy",
  bucket: "team-snapshots",
  description: "Complete setup for new team members",
});

// New team member restores
await client.exec(["snapshot", "restore"], {
  id: "onboarding-2025-01-...",
  bucket: "team-snapshots",
  targetPath: "/home/newdev/workspace",
});

4. Disaster Recovery

// Daily backup
await client.exec(["snapshot", "create"], {
  name: `daily-${new Date().toISOString().split('T')[0]}`,
  preset: "heavy",
  bucket: "backups",
});

// After disaster - full restore
await client.exec(["snapshot", "restore"], {
  id: "daily-2025-01-14-...",
  bucket: "backups",
  overwrite: true,
});

Advanced Usage

Custom Snapshot Paths

// Snapshot specific repositories only
await client.exec(["snapshot", "create"], {
  name: "critical-services",
  preset: "medium",
  bucket: "my-snapshots",
  paths: [
    "/workspace/api-gateway",
    "/workspace/auth-service",
    "/workspace/payment-service",
  ],
});

Snapshot Rotation

// List and delete old snapshots
const list = await client.exec(["snapshot", "list"], {
  bucket: "my-snapshots",
  prefix: "daily-",
  maxResults: 100,
});

// Keep last 7, delete older
const toDelete = list.snapshots
  .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
  .slice(7);

for (const snapshot of toDelete) {
  await client.exec(["snapshot", "delete"], {
    id: snapshot.id,
    bucket: "my-snapshots",
  });
}

Verification Before Restore

// Check what would change before restore
const diff = await client.exec(["snapshot", "diff"], {
  id: "snapshot-id",
  bucket: "my-snapshots",
});

if (!diff.summary.isMatch) {
  console.log(`Will modify ${diff.summary.reposChanged} repositories`);
  console.log(`${diff.summary.totalFilesChanged} files will change`);

  // Prompt user for confirmation
  const confirmed = await askUserConfirmation();

  if (confirmed) {
    await client.exec(["snapshot", "restore"], {
      id: "snapshot-id",
      bucket: "my-snapshots",
      overwrite: true,
    });
  }
}

Performance Considerations

  • Preset Selection: Use lightest preset that meets your needs
    • light for code-only snapshots (fast, small)
    • medium for reproducible builds
    • heavy for complete disaster recovery
  • Multipart Upload: Automatically used for files > 5MB
  • Compression: Gzip level varies by preset (9 for light/medium, 6 for heavy)
  • Parallel Operations: Repository info gathering runs in parallel
  • S3 Regions: Use bucket in same region as compute for faster transfers

Error Handling

try {
  await client.exec(["snapshot", "create"], {
    name: "my-snapshot",
    preset: "medium",
    bucket: "my-snapshots",
  });
} catch (error) {
  if (error.message.includes("NoSuchBucket")) {
    console.error("S3 bucket does not exist");
  } else if (error.message.includes("AccessDenied")) {
    console.error("Insufficient S3 permissions");
  } else if (error.message.includes("Not a git repository")) {
    console.error("Path is not a git repository");
  } else {
    console.error("Snapshot failed:", error);
  }
}

Integration with Ecosystem

With bundle-dev

import "@procedures/bundle-dev"; // registers its procedures

// snapshot.* procedures available
await client.exec(["snapshot", "create"], { ... });

With client-s3

client-snapshot uses client-s3 for all S3 operations:

  • Multipart uploads for large archives
  • Metadata storage as separate JSON objects
  • Bucket operations and listing

With client-git

Captures comprehensive Git state:

  • Current branch and commit
  • Working tree status (dirty/clean)
  • Stash list and counts
  • Remote tracking information
  • Ahead/behind commit counts

License

MIT

Related Packages

  • @procedures/client - Core client/procedure system
  • @procedures/s3 - S3 storage operations
  • @procedures/git - Git repository operations
  • @procedures/pnpm - Package manager operations
  • @procedures/fs - Filesystem operations
  • @procedures/bundle-dev - Development bundle