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

portshift

v0.0.1

Published

Give a stack of dev servers the first block of ports that is free, the same offsets in every block

Readme

portshift

Gives a stack of dev servers the first block of ports that is free, with every server at the same offset in every block: the app on 3001, else 4001, else 5001. Several checkouts of one repository — git worktrees, parallel agent workspaces — then run their whole stacks side by side, and each app still finds the others, because every URL derived from the ports moves with the block. An agent in one checkout starts its servers, waits for them and tests them without ever being blocked by another checkout's.

Install

npm install --save-dev portshift

Add a .portshift.json at the root of the stack (see Configuration), and run the dev servers through it:

{
  "scripts": {
    "dev": "portshift run -- turbo dev"
  }
}

Each server reads its own port from the environment, keeping the first block's port as the default for a script run on its own: next dev -p ${APP_PORT:-3001}. A URL one app builds for another is built from the same variables.

It needs Node 22 or later, on macOS or Linux.

Commands

| Command | What it does | | --- | --- | | portshift run -- <command…> | Picks a block, exports its ports to the command as environment variables, runs it, and answers with its exit code. When this directory's stack already runs, it joins that block instead of taking another, so a Stripe listener or an e2e run started beside the dev servers reaches the same stack. Every run leases the block for itself, joining or not, so the block stays held while any process of the directory runs, whichever started first. It prints the block, and the blocks it passed over and why, on stderr. A signal it receives (SIGINT, SIGTERM, SIGHUP) goes on to the command, and the block is given back once the command has exited. | | portshift port <name> [--wait] [--timeout <seconds>] | The port <name> (APP_PORT) has in this directory's running stack, on stdout. Without --wait it fails at once when no stack runs; with it, it waits — two minutes by default — for the stack to take its block and for something to listen on the port. | | portshift status [--json] | The block this directory's stack runs on, and every running stack's lease: block, pid, directory and command. |

For an agent

npm run dev &                                 # Through portshift run
port=$(npx portshift port APP_PORT --wait)    # Up, and where
curl -s "http://localhost:$port/"
npx portshift status                          # Each process on the block, with its pid and command
  • Start servers through portshift run. A script that runs a server on its own takes the first block's port whoever holds it.
  • Wait without getting in the way. port --wait asks by connecting to the port, never by binding it, so it cannot take the port from the server starting on it.
  • Never assume the first block. Ask portshift port for a port, rather than writing 3001.
  • When this checkout's stack already runs, use it. A second portshift run joins its block, where the first one's servers already hold the ports.
  • Stop what you started. status lists each process on the block with its pid, its command's pid and its command: kill <pid> of the one running the dev servers stops them, or its command's pid when portshift run itself is gone. The block is free once every process of the directory has ended.

Picking a block

A block is taken when every one of its ports is free and no other running stack holds it. Every port must be free even when only some of the servers will start, so the URLs each app derives for the others always point at its own stack.

  • Free means nothing listens on it at 0.0.0.0, 127.0.0.1, :: or ::1. No single bind sees every listener: on macOS a server on 127.0.0.1 leaves 0.0.0.0 bindable. AirPlay Receiver holds 5000 and 7000 on macOS, so with WEB_PORT at offset 0 those blocks are passed over.
  • Refused by browsers means one of its ports is on the Fetch standard's list of bad ports, which Chromium, Firefox, Safari and Node's fetch refuse to open: a server there starts, and nothing reaches it. With WEB_PORT at offset 0 that is 6000, X11's, so the 60xx block is passed over.
  • Leased means another portshift run holds it. A dev server takes seconds to bind its port, so a probe alone would hand one block to two workspaces started together; the lease holds the block from the moment it is picked until the process that took it ends. Leases live in $TMPDIR/portshift-<uid>/, one file per process, in a directory only its user may write in: portshift refuses one that is a symlink, another user's or open to others, since anyone who could write there could plant a symlink where a lease is written and have it overwrite a file of yours. Users therefore do not see each other's leases, and a block another user's servers have bound is passed over as in use. A lease is dropped, the next time any portshift reads them, once its process and the command it started are both gone: a portshift run killed outright cannot stop its command, which keeps running and binds the block's ports later, so the lease records the command's pid and lasts while either lives. Picking runs under a lock: port 2999 on 127.0.0.1, held for the milliseconds of the pick. The system frees a port the moment its process ends, however it ends, so no lock is left behind for another process to judge abandoned and break; with a lock file, two processes breaking one at once could both pick the same block. A program other than portshift listening on 2999 makes run give up after ten seconds, saying so. A lease whose pid an unrelated process has since reused looks alive, and only costs a block until that process ends.

Configuration

A .portshift.json at the root of the stack, found from any directory below it. The directory it sits in is the stack's identity: one lease per directory.

{
  "blocks": { "first": 3000, "step": 1000, "last": 29000 },
  "ports": { "WEB_PORT": 0, "APP_PORT": 1, "API_PORT": 10, "E2E_PORT": 99 }
}

blocks gives the base ports tried, in order: first, first + step, up to last. ports maps each environment variable to its offset in a block, from 0 to below step, no two alike. Every setting is required, and one out of range is an error.

Keep last below the ports the system hands out to outgoing connections — from 32768 on Linux, from 49152 on macOS — where a port can be taken at any moment. The example stops at 29000 for that reason: 26 blocks once 60xx is passed over, 24 on macOS.

Developing

pnpm --filter portshift test:coverage   # 100% coverage
pnpm --filter portshift check-types
pnpm --filter portshift build           # dist/cli.js, what the package ships

The source runs as TypeScript on Node 24, which strips the types itself. Node does not do that for a file inside node_modules, so the package ships it built (prepack).

License

MIT