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

@zendero/runctl

v0.1.20

Published

Picks a free port, runs your dev server in the background, and keeps PID + port state in .run/ so projects don't collide.

Downloads

208

Readme

Runctl

Picks a free port, runs your dev server in the background, and keeps PID + port state in .run/ and ~/.run so projects don't collide.

Needs Node.js 18+, bash, and lsof (for free-port detection and gc; common on macOS, often apt install lsof on Linux).

Platforms: macOS / Linux / WSL. Not aimed at native Windows shells.


Install

Published name on npm is @zendero/runctl; the CLI on your PATH is runctl.

| Goal | What to run | |------|-------------| | Use runctl inside one repo (recommended) | pnpm add -D @zendero/runctl — also npm install -D / yarn add -D | | runctl everywhere (global) | pnpm add -g @zendero/runctl or the curl installer below | | Track main from GitHub as a dev dependency | pnpm add -D "github:DoctorKhan/runctl#main" (still resolves as @zendero/runctl; reinstall to update) |

Global install: package manager vs script

Package manager is the straightforward choice if you already use pnpm or npm:

pnpm add -g @zendero/runctl

From Git only:

pnpm add -g "github:DoctorKhan/runctl#main"

scripts/install-global.sh is for “one command” setup, CI, or when you want npm first, then Git without writing two install lines yourself. It requires bash, pnpm or npm on PATH, and network access.

One-liner (same URL the script header documents):

curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" | bash

Pass script arguments after bash (stdin pipe has no argv). To pick a mode explicitly:

curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" | bash -s -- --registry

install-global.sh reference

If you do not pass --registry, --git, --auto, or --interactive: on an interactive TTY with CI not 1, the script prompts for install source (and related choices). Otherwise it behaves like --auto: global install from the npm registry first; if that fails, retry from Git (same URL/ref as --git).

Modes — each mode picks where the global install comes from. Under the hood the script runs pnpm add -g … or npm install -g … once per successful path (auto can run twice: registry attempt, then Git if the first fails).

| Mode | What it does | When to use it | |------|----------------|----------------| | --registry | Only installs RUNCTL_PACKAGE (default @zendero/runctl) from the npm registry. No Git fallback. | You want the published package only—e.g. CI that must not clone Git, or you know npm is enough. | | --git | Only installs from Git: RUNCTL_GIT_BASE + # + ref (default ref main, overridable with --ref). No registry attempt first. | You want main/a branch/tag from the repo, or the registry is unreachable. | | --auto | Tries --registry first; on failure, runs the same Git install as --git. | Headless installs, pipes, CI: resilient default when you’re fine with either source. | | --interactive | Prompts for registry / git / auto, optional Git ref when git/auto applies, and pnpm vs npm if both exist—only when a TTY is available. | You want to choose at install time instead of memorizing flags. |

If --interactive is requested but there is no usable TTY (or CI=1), the script falls back to --auto and prints a short notice.

Flags

| Flag | Meaning | |------|---------| | --pm pnpm | --pm npm | Use that package manager (must exist on PATH) | | --ref <ref> | Git ref for --git or for the Git step of --auto (default: main) |

Environment variables (optional)

| Variable | Purpose | |----------|---------| | RUNCTL_PACKAGE | npm package name (default: @zendero/runctl) | | RUNCTL_GIT_BASE | Git URL without fragment (default: git+https://github.com/DoctorKhan/runctl.git) | | RUNCTL_GIT_REF | Default ref when not overridden by --ref (default: main) |

Examples

Registry only (good for locked-down CI that should not hit Git):

curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" | bash -s -- --registry

Explicit auto (same as non-interactive default, but spelled out):

curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" | bash -s -- --auto

Git only, specific ref:

curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" | bash -s -- --git --ref main

Use npm explicitly (e.g. no pnpm on the machine):

curl -fsSL "https://raw.githubusercontent.com/DoctorKhan/runctl/main/scripts/install-global.sh" | bash -s -- --pm npm --registry

If runctl --help still looks old after install/update, remove the legacy package that can shadow this CLI and reinstall:

pnpm remove -g runctl
pnpm add -g @zendero/runctl@latest
hash -r

runctl update and pnpm version messages

If pnpm nags about versions or pnpm self-update does nothing useful, runctl update --help explains why and lists concrete fixes (same text is summarized after a successful pnpm-based update unless CI or RUNCTL_UPDATE_SKIP_PNPM_HINT=1).

--help on the install script prints the same usage summary.


Quick start

Add scripts to your package.json:

{
  "scripts": {
    "dev": "runctl start --script dev:server",
    "dev:server": "next dev",
    "dev:stop": "runctl stop"
  }
}
  • pnpm dev — start (port in .run/ports.env, logs in .run/logs/).
  • pnpm dev:stop — stop and release ports.

Why two scripts? runctl start runs pnpm run <name> under the hood. If dev called itself, it would loop. The real server lives on dev:server; --script tells runctl which one to run. Without --script, it defaults to running dev.

predev: If you define predev next to dev (e.g. a doctor step) and your script name is dev:* or dev_* without its own pre<script>, runctl runs predev once before starting. Set RUNCTL_SKIP_PREDEV=1 to skip.

Dashboard / API-only (split names): Avoid runctl start --script dev when dev is the runctl wrapper — that loops. Use a dedicated script for the real server:

{
  "scripts": {
    "dev": "runctl start --script dev:workbench",
    "dev:workbench": "node --env-file=.env src/dashboard/server.js",
    "dev:stop": "runctl stop"
  }
}

Listen on process.env.PORT (runctl sets it). Optional: runctl start --script dev:workbench --open to open the browser after start, or runctl start … && runctl open ..


Commands

| Command | What it does | |---------|-------------| | runctl start | runctl dev | Start dev server (same command; picks free port, backgrounds). Flags: --script, --open (open browser after a successful start) | | runctl stop [dir] | Stop daemons & release ports | | runctl status [dir] | Show .run state for this package | | runctl ps | List running programs with PID, port, service, project | | runctl logs [dir] [service] | Tail .run/logs/<service>.log (default service: RUNCTL_SERVICE, else package.json name basename, else web) | | runctl ports | List user-wide port registry (~/.run) | | runctl ports gc | Clean up stale port claims | | runctl env expand <manifest> [--out file] | Generate .env.local from manifest | | runctl doctor [dir] | Check Node 18+, lsof, package manager, package.json; reminds that child scripts get PORT / HOST (custom servers should listen on process.env.PORT) | | runctl update | Refresh global CLI: default auto (npm @latest, then Git). runctl update npm / git / auto or flags --registry / --git / --auto; runctl update --help; env RUNCTL_PACKAGE, RUNCTL_GIT_BASE, RUNCTL_GIT_REF (aligned with install-global.sh) | | runctl version | runctl --version | runctl -v | Print package version and install path (supported interchangeably) |

Monorepo: runctl start ./apps/web --script dev:server

Vite: if --port isn't forwarded, set server.port from process.env.PORT in vite.config.


Fits / doesn't fit

| Kind of repo | Runctl | |--------------|--------| | Next.js, Vite, SvelteKit, Nuxt, Astro, Remix | Good fit — port flags wired for common stacks. | | pnpm, npm, yarn, bun lockfiles | Supported for run <script>. | | predev + split dev / dev:server | Supported — see above. | | Monorepo app in a subfolder | Use runctl start ./apps/web. | | No package.json (Python, Go, etc.) | Not a fit — this tool is for Node package scripts. | | Custom Node entry (gateways, CLIs) | Weak fitPORT/HOST are injected; bind with server.listen(process.env.PORT) (see runctl doctor). |


Docs & examples

examples/consumer-package.json · docs/vercel-and-env.md · examples/env.manifest.example

CLI vs run-lib.sh: Most apps only need the runctl binary and package.json scripts. For shell-heavy repos, examples/run.sh.example shows sourcing lib/run-lib.sh (same library the CLI uses). Resolve the installed path with runctl lib-path.

Scaffold run.sh: runctl run-sh --write writes that example to ./run.sh (honors RUNCTL_PROJECT_ROOT; use -C dir for another directory). Use --force to replace an existing file. Equivalent to redirecting runctl run-sh to run.sh, but also sets the executable bit.

CI: Prefer pnpm add -D @zendero/runctl (or a global install) so runctl is on PATH with a stable version. pnpm dlx @zendero/runctl is fine for one-off recovery; avoid relying on it for every CI job (cold cache / latency).

Roadmap (ideas): runctl exec (one-off commands with the same port / .run contract as start); optional HTTP health gate before “ready”.

Develop this repo: pnpm install./run.sh (thin runner; default help) delegates to bin/runctl. Maintainer npm flows live in scripts/maintain.sh. pnpm test runs tests/run-all.sh (Jest-style output: suites, ✓/✗, PASS/FAIL per file, shared helpers in tests/lib/test-runner.sh)

Publish (maintainers) — workflow similar to elata’s release preflight, scaled for one package:

| Step | Command | |------|--------| | Preflight | ./run.sh release-check or pnpm run release-check | | Publish | ./run.sh release latest or pnpm run release (publishes, then commits + pushes release changes if any) | | Promote dist-tag | After publishing under next, ./run.sh promote sets latest for the version in package.json |

Put NPM_TOKEN in .env. release / npm-whoami use a temporary NPM_CONFIG_USERCONFIG so a stale ~/.npmrc token does not override .env (npm 10+ / pnpm). Token lines can use NPM_TOKEN= or npm_token=; quoted values are supported without source-ing secrets as shell code first.