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

@nurv-llc/runce

v1.0.4

Published

Run one-time tasks for Node.js services - like migrations, but for arbitrary TypeScript

Readme

@nurv-llc/runce

CI Tests npm version codecov License: MIT Node.js Version

Run one-time runce tasks for your Node service—like migrations, but for arbitrary TypeScript/JavaScript. Ships with MongoDB, PostgreSQL, and File trackers with a pluggable interface for others.

Features

  • ✅ Run TS/JS tasks exactly once
  • ✅ Pluggable Tracker (MongoDB, PostgreSQL + File tracker out of the box)
  • ✅ CLI + programmatic API
  • ✅ Distributed lock (MongoDB & PostgreSQL lease) to prevent concurrent runners
  • ✅ Dry-run, filters, JSON list, rich logs
  • ✅ Task scaffolding

Quick Start

npm i @nurv-llc/runce mongodb
# OR for PostgreSQL
npm i @nurv-llc/runce pg
npx runce make "initialize queues"

Generate a config:

// config/runce.config.ts
import { defineConfig } from '@nurv-llc/runce';

export default defineConfig({
  tasksDir: './tasks',
  tracker: {
    type: 'mongo', // 'postgresql', or 'file' for development
    options: { uri: process.env.MONGO_URI, db: 'infra' },
  },
  lock: { enabled: true, ttlMs: 60000, owner: process.env.HOSTNAME },
});

Or use JavaScript:

// config/runce.config.js
export default {
  tasksDir: './tasks',
  tracker: {
    type: 'mongo', // 'postgresql', or 'file' for development
    options: { uri: process.env.MONGO_URI, db: 'infra' },
  },
  lock: { enabled: true, ttlMs: 60000, owner: process.env.HOSTNAME },
};

Run pending tasks:

MONGO_URI="mongodb://localhost:27017" npx runce run

List applied:

npx runce list --json | jq .

Writing a Task

Tasks can be written in either TypeScript or JavaScript:

TypeScript (Recommended):

// tasks/001-setup-db.ts
import { RunceTask } from '@nurv-llc/runce';

export default {
  id: '001-setup-db',
  title: 'Set up database indexes',
  async run({ log }) {
    log('Setting up database indexes...');
    // Your one-time setup logic here
    log('Database indexes created successfully');
  },
  
  // Optional: check if task is already done
  async alreadyDone({ log }) {
    // Return true if task should be skipped
    return false;
  },
} satisfies RunceTask;

Run-Always Tasks:

// tasks/backup-logs.ts
import { RunceTask } from '@nurv-llc/runce';

export default {
  id: 'backup-logs',
  title: 'Backup application logs',
  runAlways: true, // 🔄 Runs every time, not tracked for completion
  async run({ log }) {
    log('Backing up logs...');
    // This runs on every execution
    log('Logs backed up successfully');
  },
  
  // Optional: still respected for runAlways tasks
  async alreadyDone({ log }) {
    // Can still conditionally skip if needed
    return false;
  },
} satisfies RunceTask;

JavaScript:

// tasks/20251003T120000.init-queues.js
const task = {
  id: '20251003T120000.init-queues',
  title: 'Initialize message queues',
  async run({ log }) {
    log('creating SQS queues ...');
    // ... do one-time setup
  },
};
export default task;

Naming: prefix with an ISO timestamp for natural ordering. id must be unique and stable.

Task Organization

Tasks can be organized in subdirectories for better structure:

tasks/
├── 001-setup-db.ts          # Run-once migration
├── 002-create-indexes.ts    # Run-once migration
├── daily/
│   ├── backup-logs.ts       # runAlways: true
│   └── cleanup-temp.ts      # runAlways: true
├── monitoring/
│   └── health-check.ts      # runAlways: true
└── migrations/
    └── 003-add-column.ts    # Run-once migration

Key Points:

  • 📁 Recursive Loading: Tasks are loaded from all subdirectories
  • 🔄 Run-Always First: Tasks with runAlways: true execute before run-once tasks
  • 📝 Flexible Organization: Organize tasks however makes sense for your project
  • 🎯 Execution Order: Within each category, tasks run in alphabetical order by task ID

Configuration

export interface Config {
  tasksDir: string;
  tracker: { type: string; options: Record<string, unknown> };
  lock?: { enabled: boolean; ttlMs: number; owner?: string };
  dryRun?: boolean;
}

You can keep config in JS or TS. The CLI resolves via --config or defaults to TypeScript config files.

Swapping Trackers

Change tracker.type and provide matching options.

export default {
  tracker: { type: 'file', options: { path: '.runce.json' } }
}

PostgreSQL Example:

export default {
  tracker: { 
    type: 'postgresql', 
    options: { 
      connectionString: 'postgresql://user:password@localhost:5432/mydb'
      // OR use individual connection parameters:
      // host: 'localhost', port: 5432, database: 'mydb', 
      // username: 'user', password: 'password'
    } 
  }
}

Add your own by implementing ITracker and registering it. No changes to tasks or runner needed.

CLI

# Create a new task file (generates TypeScript by default)
runce make "human readable name"

# Run tasks (loads from all subdirectories)
runce run [--config ./config/runce.config.ts] [--tasks-dir ./tasks] [--dry-run]
         [--only id1,id2] [--since 2025-09-01] [--until 2025-10-01]

# List applied tasks  
runce list [--config ./config/runce.config.ts] [--json]

# Get help
runce --help
runce run --help

Execution Order

When you run runce run, tasks execute in this order:

  1. 🔄 Always-run tasks (runAlways: true) - Execute every time
  2. 🎯 Run-once tasks - Execute only if not already applied

Within each category, tasks run in alphabetical order by task ID.

Programmatic API

import { runWithConfig, runWithConfigObject, listApplied, listAppliedWithConfig, defineConfig } from '@nurv-llc/runce';

// File-based configuration
await runWithConfig('./config/runce.config.ts');

// Or pass config object directly (useful for dynamic configuration)
const config = defineConfig({
  tasksDir: './tasks',
  tracker: {
    type: 'mongo',
    options: { uri: process.env.MONGO_URI, db: 'infra' },
  },
});
await runWithConfigObject(config);

// List applied tasks
const records = await listApplied('./config/runce.config.ts');
// Or with config object
const records2 = await listAppliedWithConfig(config);

Use Cases for Config Objects

Passing config objects directly is useful when:

  • Environment-based configuration: Build config dynamically based on environment variables
  • Testing: Create test configs without filesystem dependencies
  • Serverless: Configure runce without requiring config files in deployment packages
  • Multi-tenant: Different configurations per tenant or database
// Example: Environment-based configuration
const getTrackerConfig = () => {
  if (process.env.NODE_ENV === 'production') {
    return process.env.DATABASE_TYPE === 'postgres' 
      ? { type: 'postgresql', options: { connectionString: process.env.DATABASE_URL } }
      : { type: 'mongo', options: { uri: process.env.MONGO_URI, db: process.env.DB_NAME } };
  }
  return { type: 'file', options: { path: './.runce-dev.json' } };
};

const config = defineConfig({
  tasksDir: './tasks',
  tracker: getTrackerConfig(),
  lock: { 
    enabled: process.env.NODE_ENV === 'production',
    ttlMs: 60000,
    owner: process.env.HOSTNAME 
  }
});

await runWithConfigObject(config);

Tracker Interface

export interface AppliedRecord {
  id: string; name: string; checksum: string; appliedAt: Date; durationMs: number; status: 'success'|'failed'; error?: string;
}
export interface ITracker {
  init(options: Record<string, unknown>): Promise<void>;
  getAppliedIds(): Promise<Set<string>>;
  recordApplied(rec: AppliedRecord): Promise<void>;
  listApplied(): Promise<AppliedRecord[]>;
}

MongoDB Tracker Options

{
  uri: string;         // Mongo connection string
  db: string;          // database name
  appliedCollection?: string; // default: runce.applied
  lockCollection?: string;    // default: runce.lock
}

PostgreSQL Tracker Options

{
  // Option 1: Connection string
  connectionString: string;   // e.g., 'postgresql://user:pass@localhost:5432/dbname'
  
  // Option 2: Individual parameters
  host?: string;              // default: localhost
  port?: number;              // default: 5432
  database?: string;          // database name
  username?: string;          // database user
  password?: string;          // database password
  
  // Optional settings
  appliedTable?: string;      // default: runce_applied
  lockTable?: string;         // default: runce_lock
  schema?: string;            // default: public
  ssl?: boolean;              // default: false
}

File Tracker Options

{
  path?: string;       // default: .runce.json
}

Locking

The MongoDB and PostgreSQL trackers use a lease document/record (_id: 'global' or name: 'global') updated with leaseUntil. If another process holds a valid lease, run exits with code 1. The file tracker doesn't support locking.

Idempotency Patterns

  • Prefer external state checks inside tasks (e.g., does the bucket/queue exist?).
  • Use alreadyDone() for cheap pre-checks when possible.
  • Avoid destructive operations; make tasks additive or guarded.

Checksums & Drift

A SHA‑256 checksum of task files is stored. If the on-disk checksum changes after apply, you can detect drift by comparing stored vs current checksums.

Development

npm i
npm run build          # Compile TypeScript
npm run lint           # Run ESLint
npm run test           # Run tests with Jest
npm run test:coverage  # Run tests with coverage
npm run test:watch     # Run tests in watch mode
npm run dev            # Watch mode compilation

TypeScript Support

Runce fully supports TypeScript out of the box:

  • Task Files: Write tasks in .ts files with full type safety
  • Config Files: Use TypeScript config with defineConfig() helper
  • Runtime: Uses tsx for TypeScript execution at runtime
  • Type Safety: Full IntelliSense and compile-time checking

No compilation step required - TypeScript files run directly!

Testing

  • Unit tests for loader, runner, filters, checksum.
  • MongoDB tracker tests using mongodb-memory-server.
  • Tests run with Jest and support TypeScript out of the box.

Extending with a New Tracker

  1. Implement ITracker in src/trackers/my-tracker.ts.
  2. Export it via the tracker factory map.
  3. Use tracker.type = 'my' in config.

Examples

Database Index Creation

const task = {
  id: '20251003T120000.user-indexes',
  title: 'Create user indexes',
  async run({ log }) {
    log('Creating user email index...');
    // await db.collection('users').createIndex({ email: 1 }, { unique: true });
    log('User indexes created');
  },
  async alreadyDone({ log }) {
    // Check if index already exists
    // const indexes = await db.collection('users').indexes();
    // return indexes.some(idx => idx.key.email);
    return false;
  },
};
export default task;

S3 Bucket Setup

const task = {
  id: '20251003T120001.s3-buckets',
  title: 'Initialize S3 buckets',
  async run({ log }) {
    log('Creating upload bucket...');
    // await s3.createBucket({ Bucket: 'app-uploads' });
    log('S3 buckets ready');
  },
};
export default task;

FAQ

Why not Prisma? Prisma tracks SQL schema, not arbitrary JS. Runce tasks handle service setup and data chores.

Can I rollback? Not built-in. Author tasks to be safe to re-run or provide explicit compensating logic.

Can I run in Docker entrypoint? Yes: runce run in your container startup before the app listener.

License

MIT