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

@pilaniaanand/ndeploy

v0.0.1

Published

Zero-downtime SSH deployment tool for Laravel and Node.js projects — inspired by Deployer

Readme

ndeploy

[!WARNING] Work in Progress: This project is currently in active development. Features and API are subject to change until the v1.0.0 release.

Zero-downtime SSH deployment for Laravel and Node.js — inspired by Deployer, rebuilt for Node.

npx ndeploy deploy production
npx ndeploy rollback staging
npx ndeploy run artisan:migrate production
npx ndeploy releases production

Features

| Feature | Details | | -------------------- | ------------------------------------------------------------------ | | Zero-downtime | Atomic current → symlink swap — no interrupted requests | | Strategies | Built-in recipes for Laravel and Node.js | | Multi-host | Deploy to many servers in series or parallel | | Config isolation | Separate stages blocks per environment — no shared mutable state | | AWS provider | Resolve EC2 hosts by tag, ASG, or instance ID at deploy time | | Hooks | before / after hooks per task | | Custom tasks | Register any async (ctx) => {} as a named task | | Rollback | Single command rolls back to the previous release | | npx-ready | No global install needed | | SSH-native | Connects over standard SSH — no agents, no remote daemons | | Dry-run | --dry-run simulates every step without touching the server |


Installation

# Use without installing (recommended for CI)
npx ndeploy deploy production

# Or install globally
npm install -g ndeploy

# Or as a dev dependency in your project
npm install --save-dev ndeploy

Quick start

# 1. Generate a config in the current directory
npx ndeploy init

# 2. Deploy
npx ndeploy deploy production

# 3. Rollback if something is wrong
npx ndeploy rollback production

CLI Reference

Usage: ndeploy <command> [stage] [options]

Commands:
  init                  Interactively create deploy.config.js
  deploy  [stage]       Deploy to a stage            (default: production)
  rollback [stage]      Roll back to previous release
  run <task> [stage]    Run a single named task remotely
  releases [stage]      List releases on remote server
  ssh [stage]           Open interactive SSH session

Options:
  -c, --config <path>   Path to config file       (default: deploy.config.js)
  -t, --task <name>     Run only this task (inside deploy)
  -H, --host <host>     Target a specific host from the stage
      --dry-run         Simulate without executing
  -v, --verbose         Show all remote command output
      --no-color        Disable color output
  -V, --version         Print version
  -h, --help            Print help

Configuration

ndeploy looks for deploy.config.js (ESM default export) in the current directory. Also supported: deploy.config.cjs, deploy.config.json, deploy.config.yml.

Minimal config

// deploy.config.js
export default {
  strategy:   'laravel',          // 'laravel' | 'nodejs'
  repository: '[email protected]:your-org/app.git',

  stages: {
    production: {
      hosts: [{
        host:        '10.0.0.1',
        user:        'deploy',
        deploy_path: '/var/www/app',
      }],
    },
  },
};

Full schema

export default {
  // ── Required ──────────────────────────────────────────────────────────────
  strategy:   'laravel',   // or 'nodejs'
  repository: '[email protected]:org/repo.git',

  // ── Source control ────────────────────────────────────────────────────────
  branch:     'main',      // default branch; can be overridden per stage
  git_depth:  1,           // shallow clone depth (0 = full)

  // ── Release management ────────────────────────────────────────────────────
  keep_releases: 5,        // how many old releases to keep

  // ── Shared state (persisted across releases) ─────────────────────────────
  shared_files: ['.env'],
  shared_dirs:  ['storage'],   // laravel default; ['uploads','logs'] for node

  // ── Writable by web server ────────────────────────────────────────────────
  writable_dirs: ['storage', 'bootstrap/cache'],
  writable_mode: 'chmod',      // 'chmod' | 'acl' | 'chown'
  http_user: 'www-data',       // used when writable_mode = 'acl' | 'chown'

  // ── Custom pipeline (optional — strategy provides a sensible default) ─────
  pipeline: [
    'deploy:lock',
    'setup:dirs',
    'git:clone',
    'deploy:shared',
    'deploy:writable',
    'composer:install',   // laravel
    'artisan:migrate',    // laravel
    'artisan:optimize',   // laravel
    'deploy:symlink',
    'deploy:cleanup',
    'deploy:unlock',
  ],

  // ── Deploy all hosts in parallel (default: false = serial) ───────────────
  parallel_deployment: false,

  // ── Custom tasks ──────────────────────────────────────────────────────────
  tasks: {
    'slack:notify': async (ctx) => {
      await ctx.run(`curl -s -X POST "$SLACK_URL" -d '{"text":"deployed"}' || true`,
        { ignoreError: true });
    },
  },

  // ── Hooks: inject tasks before / after any built-in task ─────────────────
  hooks: {
    before: { 'deploy:symlink': ['slack:notify'] },
    after:  { 'deploy:cleanup': ['slack:notify'] },
  },

  // ── Laravel-specific ──────────────────────────────────────────────────────
  php_bin:         'php8.2',
  composer_bin:    'composer',
  composer_flags:  '--no-dev --no-interaction --optimize-autoloader',
  build_assets:    false,      // set true to add npm:build to pipeline
  php_fpm_service: 'php8.2-fpm',

  // ── Node.js-specific ──────────────────────────────────────────────────────
  package_manager:       'npm',          // 'npm' | 'yarn' | 'pnpm'
  npm_flags:             'ci --omit=dev',
  build_command:         'run build',    // runs: npm run build
  process_manager:       'pm2',          // 'pm2' | 'systemd'
  pm2_app_name:          'app',
  pm2_config:            'ecosystem.config.cjs',   // optional
  systemd_unit:          'myapp',
  health_check_url:      'http://localhost:3000/health',
  health_check_retries:  5,
  health_check_interval: 3,             // seconds

  // ── Stages (project isolation) ────────────────────────────────────────────
  stages: {
    production: {
      branch: 'main',
      hosts: [
        {
          label:       'web-1',          // friendly name shown in output
          host:        '10.0.0.1',       // IP or hostname
          user:        'deploy',
          port:        22,
          deploy_path: '/var/www/app',
          privateKey:  '~/.ssh/id_ed25519',
          // identityFile, password, passphrase, jumpHost also supported
        },
      ],
    },
    staging: {
      branch: 'develop',
      hosts: [{ host: '10.0.1.1', user: 'deploy', deploy_path: '/var/www/app-staging' }],
    },
  },
};

Per-stage & per-host overrides

Every key in the top-level config can be overridden at the stage or host level. Resolution order (lowest → highest precedence):

global config  →  stage config  →  host config  →  CLI flags

Deployment strategies

Laravel

strategy: 'laravel'

Default pipeline:

deploy:lock          Prevent concurrent deployments
setup:dirs           mkdir releases/ shared/
git:clone            Clone repository into releases/<timestamp>
deploy:shared        Symlink shared/ dirs & files (.env, storage/)
deploy:writable      chmod/acl storage/ bootstrap/cache/
composer:install     composer install --no-dev --optimize-autoloader
artisan:storage:link php artisan storage:link
artisan:migrate      php artisan migrate --force
artisan:optimize     php artisan optimize (config+route+view cache)
deploy:symlink       Atomic: mv current → new release
artisan:queue:restart php artisan queue:restart
deploy:cleanup       Remove releases beyond keep_releases
deploy:unlock        Release lock

Available Laravel tasks:

| Task | Description | | ------------------------ | ----------------------------- | | composer:install | Install PHP deps | | composer:dump-autoload | Regenerate autoloader | | artisan:migrate | Run migrations --force | | artisan:seed | Run seeders --force | | artisan:optimize | Cache config + routes + views | | artisan:config:cache | Config cache only | | artisan:route:cache | Route cache only | | artisan:view:cache | Blade view cache | | artisan:cache:clear | Clear application cache | | artisan:queue:restart | Graceful queue restart | | artisan:storage:link | Public storage symlink | | artisan:down | Maintenance mode on | | artisan:up | Maintenance mode off | | npm:build | npm ci && npm run build | | php-fpm:reload | service php-fpm reload |


Node.js

strategy: 'nodejs'

Default pipeline (PM2):

deploy:lock
setup:dirs
git:clone
deploy:shared        Symlink .env, uploads/, logs/
npm:install          npm ci --omit=dev  (or yarn/pnpm equivalent)
node:build           npm run build  (only if build_command is set)
deploy:writable
deploy:symlink
pm2:reload           Zero-downtime reload via PM2
health:check         HTTP check (only if health_check_url is set)
deploy:cleanup
deploy:unlock

Available Node.js tasks:

| Task | Description | | ----------------- | --------------------------------------------- | | npm:install | npm ci --omit=dev | | yarn:install | yarn install --frozen-lockfile | | pnpm:install | pnpm install --frozen-lockfile | | node:build | Run configured build command | | pm2:start | Start or reload app via PM2 | | pm2:reload | Zero-downtime reload | | pm2:stop | Stop PM2 process | | pm2:delete | Delete PM2 process | | systemd:reload | systemctl reload (with fallback to restart) | | systemd:restart | systemctl restart | | health:check | HTTP check with retries | | nginx:reload | nginx -t && systemctl reload nginx |


AWS Provider

Dynamically resolve EC2 hosts at deploy time instead of hard-coding IPs.

npm install @aws-sdk/client-ec2 @aws-sdk/client-ssm
// deploy.config.js
import { AwsProvider } from 'ndeploy';

const aws = new AwsProvider({
  region:          'us-east-1',
  // profile:      'myprofile',          // AWS CLI profile
  // accessKeyId + secretAccessKey work too; defaults to env / instance role
});

// Resolve by Name tag
const webHosts = await aws.byTag('web-production', {
  user:        'ec2-user',
  deploy_path: '/srv/app',
  privateKey:  '~/.ssh/ec2.pem',
});

// Resolve by multiple tags
const apiHosts = await aws.byTags(
  { Environment: 'production', Role: 'api' },
  { user: 'ubuntu', deploy_path: '/srv/api' }
);

// Resolve all instances in an Auto Scaling Group
const asgHosts = await aws.byAutoScalingGroup('my-asg-name', {
  user: 'ec2-user', deploy_path: '/srv/app',
});

export default {
  strategy:   'nodejs',
  repository: '[email protected]:org/api.git',

  stages: {
    production: {
      hosts: webHosts,   // ← EC2 instances resolved above
    },
  },
};

Custom tasks & hooks

export default {
  strategy: 'nodejs',
  repository: '…',

  tasks: {
    // Any async function receiving a Context object
    'db:migrate': async (ctx) => {
      ctx.set('remote_cwd', ctx.paths.release);
      await ctx.run('node scripts/migrate.js');
    },

    'notify:slack': async (ctx) => {
      const msg = `✅ Deployed ${ctx.releaseName} to ${ctx.stage}`;
      await ctx.run(
        `curl -s -X POST "$SLACK_WEBHOOK" -d '{"text":"${msg}"}' || true`,
        { ignoreError: true }
      );
    },
  },

  hooks: {
    before: {
      'deploy:symlink': ['db:migrate'],      // run before the atomic swap
    },
    after: {
      'deploy:cleanup': ['notify:slack'],    // run after cleanup
    },
  },

  stages: { production: { hosts: [{ … }] } },
};

The ctx Context API

Every task receives a Context instance:

// ── Variable store ────────────────────────────────────────────────────────
ctx.get('key')           // reads var → host config → global config
ctx.set('key', value)    // sets a runtime variable
ctx.add('key', ...vals)  // appends to an array variable

// ── Remote execution ──────────────────────────────────────────────────────
await ctx.run('command')                    // run; throw on non-zero exit
await ctx.run('command', { ignoreError: true })   // run; ignore failure
await ctx.capture('command')               // run; return trimmed stdout

// ── File transfer ─────────────────────────────────────────────────────────
await ctx.upload('/local/path', '/remote/path')
await ctx.download('/remote/path', '/local/path')

// ── Well-known paths ──────────────────────────────────────────────────────
ctx.paths.deploy     // /var/www/app
ctx.paths.releases   // /var/www/app/releases
ctx.paths.current    // /var/www/app/current   ← the live symlink
ctx.paths.shared     // /var/www/app/shared
ctx.paths.release    // /var/www/app/releases/20240315120000  ← this deploy

// ── Metadata ──────────────────────────────────────────────────────────────
ctx.releaseName      // '20240315120000'
ctx.stage            // 'production'
ctx.host             // host config object
ctx.log              // Logger instance

Remote directory layout

/var/www/app/
├── current -> releases/20240315120000   ← always points to live release
├── releases/
│   ├── 20240315120000/   ← newest (just deployed)
│   ├── 20240314093000/   ← previous
│   └── 20240313150000/   ← older (removed by cleanup)
└── shared/
    ├── .env              ← persisted across releases
    ├── storage/          ← laravel: symlinked into each release
    └── uploads/          ← nodejs: symlinked into each release

Programmatic API

import { Deployer, ConfigLoader, AwsProvider } from 'ndeploy';

const config   = await new ConfigLoader().load('./deploy.config.js');
const deployer = new Deployer(config, { verbose: true });

// Deploy
await deployer.deploy('production');

// Rollback
await deployer.rollback('production');

// Run a single task
await deployer.runTask('artisan:migrate', 'production');

// List releases
await deployer.listReleases('production');

Tips

Zero-downtime with Nginx

Point your Nginx root at current/public (Laravel) or proxy to the PM2 process. The atomic symlink swap means Nginx keeps serving the old release while the new one is prepared; the cutover happens in microseconds.

# Laravel
root /var/www/app/current/public;

# Node.js (PM2 handles the reload; nginx just proxies)
location / { proxy_pass http://localhost:3000; }

SSH key setup

hosts: [{
  host:        '10.0.0.1',
  user:        'deploy',
  deploy_path: '/var/www/app',

  // Option 1: explicit key file
  privateKey: '~/.ssh/id_ed25519',

  // Option 2: key content from env var (great for CI)
  privateKey: process.env.SSH_PRIVATE_KEY,

  // Option 3: password (not recommended)
  password: process.env.SSH_PASSWORD,
}]

CI/CD (GitHub Actions)

- name: Deploy
  run: npx ndeploy deploy production
  env:
    SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
    SLACK_WEBHOOK:   ${{ secrets.SLACK_WEBHOOK }}

In your config:

hosts: [{
  host:       '10.0.0.1',
  user:       'deploy',
  deploy_path: '/var/www/app',
  privateKey: process.env.SSH_PRIVATE_KEY,
}]

License

MIT