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

run-user-nested-invokable-commands

v1.1.0

Published

Turn any script folder into a CLI

Readme

runic

Point a CLI at a folder. Drop in scripts. They become commands.

That's the whole idea.

eval "$(runic init zsh --name ops --dir ~/scripts)"

Now your folder is a CLI:

~/scripts/                       $ ops lint
├── lint.sh                 →    $ ops format
├── format.sh                    $ ops dev start
├── dev/                         $ ops gen client
│   ├── start.sh                 $ ops help
│   └── seed.py
└── gen/
    ├── client.sh
    └── component.js

Want a new command? Drop a file.

$ cat > ~/scripts/pr.sh <<'SCRIPT'
#!/bin/bash
# Open a PR using our team template
gh pr create --template .github/pull_request_template.md --web
SCRIPT

$ chmod +x ~/scripts/pr.sh

$ ops pr    # live. no reload, no registration, no restart.

No manifest. No config file. The shell function reads the filesystem on every invocation, so the CLI is always whatever's currently on disk.

Scripts can be in any language. Directories become subcommands. Filenames become command names. And if you opt in, native executables (Rust, Go, C, anything the kernel can run) drop into the same folder as first-class commands — compiled speed tier, right next to your bash one-liners.

How it works

runic generates a shell function with the name you choose. That function does all the dispatching — no Node.js startup on every invocation, no manifest to maintain, no config files. It probes the filesystem directly, finds the file that matches your command, and runs it.

How it runs the file depends on what the file is:

  • Known extension (.sh, .py, .rb, .js, .ts, …) — invoked via the mapped interpreter.
  • Extensionless with a shebang — the shebang picks the interpreter.
  • Extensionless and executable (opt-in via --allow-binaries) — exec'd directly. Native binaries run at kernel speed with no interpreter in the loop.

The generated function handles everything: command dispatch, help, doctor diagnostics, tab completions. You interact with your CLI, not with runic.

Install

npm install -g run-user-nested-invokable-commands

Sorry, all the good names were already taken.

Setup

Add one line to your shell config (.zshrc, .bashrc, or config.fish):

# zsh
eval "$(runic init zsh --name ops --dir ~/scripts)"

# bash
eval "$(runic init bash --name ops --dir ~/scripts)"

# fish
runic init fish --name ops --dir ~/scripts | source

That's it. You now have a CLI called ops.

Append --allow-binaries to also dispatch native executables from your script dir. Off by default — see Native binaries for why.

Supported runtimes

Most of what lands in your script dir is interpreted. runic knows the common extensions and invokes the right runner automatically:

| Extension | Runner | |-----------|-----------| | .sh | bash | | .bash | bash | | .zsh | zsh | | .py | python3 | | .rb | ruby | | .js | node | | .ts | npx tsx | | .php | php | | .pl | perl |

Extensionless files with a shebang line work too — the interpreter is read from the shebang. So #!/usr/bin/env python3 or #!/usr/bin/env -S python3 -u both resolve correctly with no extension in the filename.

Run runic runtimes to see which of these are installed on the current host, with versions.

Native binaries

Off by default. Pass --allow-binaries at init and runic will also dispatch extensionless executables — Mach-O, ELF, or any other format the kernel knows how to execve. Your Rust CLI, your Go tool, your hand-compiled C util: drop the binary in the script dir, it's a command.

eval "$(runic init zsh --name ops --dir ~/scripts --allow-binaries)"

Why opt-in? Shell scripts are readable source. You can open ~/scripts/lint.sh and see exactly what it does before running it. Binaries are opaque — you're trusting the bytes. Making it a conscious flag means you've thought about whose bytes you're running, not just which folder they live in.

Per-call override without re-init:

__runic_allow_binaries=1 ops fastgrep foo    # one-off, still off by default

In help and doctor, binaries show up as binary instead of a description — there's no shebang to read a comment from. doctor also skips the missing-shebang and runner-availability checks for binaries, since they're their own runner.

Descriptions

The first comment after the shebang becomes the command's description in help output:

#!/bin/bash
# Deploy the application to the target environment
#!/usr/bin/env python3
# Run database migrations
#!/usr/bin/env node
// Rotate API keys for all services

No config files needed. Your scripts document themselves.

Multiple directories — PATH-style overrides

Point at multiple directories. They're searched in order, like $PATH: the first one to match wins. This isn't a conflict to resolve, it's how you override.

eval "$(runic init zsh --name ops --dir ~/my-scripts --dir /shared/team-scripts)"

Anything in ~/my-scripts overrides the same-named command in /shared/team-scripts. Same as how ~/bin:/usr/local/bin:/usr/bin lets you shadow a system binary by dropping a script in ~/bin.

What this enables:

  • Personal overrides on a shared repo. Your team maintains /shared/team-scripts/test.sh that runs the standard test suite. You want it to also reset your local DB first. Drop your own test.sh in ~/my-scripts — it shadows the team version, just for you, no fork required.
  • Try a change without a PR. The team script has a rough edge you want to smooth out. Copy it to ~/my-scripts, edit, use it for a week, then PR the change once you're sure. The override evaporates when you delete it.
  • Per-context behavior. Same command name, different implementation in ~/my-scripts/work/ vs ~/my-scripts/personal/. Switch by reordering --dir.
  • Layered teams. Platform team's repo, then your squad's repo, then your personal scripts: --dir ~/personal --dir ~/squad-scripts --dir /platform-scripts. Each layer can shadow the next.

runic doctor reports every shadowed command so nothing happens behind your back.

Whitelabeling

The --name flag is the CLI's identity. Create as many as you want:

eval "$(runic init zsh --name dev --dir ~/scripts/dev)"
eval "$(runic init zsh --name docs --dir ~/scripts/docs)"
eval "$(runic init zsh --name lint --dir /opt/lint-tools)"

Three separate CLIs. Three names. Independent.

Tradeoffs

runic dispatches by reading the filesystem on every invocation. The consequences of that choice:

Upsides

  • Shell-native everything. runic execs your scripts, so they inherit how Unix already works: arg quoting, pipes (ops logs | grep ERROR), redirection, signals, TTY detection, exit codes, process groups, job control.
  • Login state, sudo prompts, file locks, tmux sessions. Scripts can ssh-add, prompt for sudo, hold a flock, attach to a tmux session — anything any other shell process can do.
  • Polyglot. bash, python, node, ruby, typescript, perl, php — any combination in the same CLI. With --allow-binaries, native executables (Rust, Go, C, …) drop into the same folder and run at kernel speed, no interpreter in the loop.
  • No registration step. Scripts on disk are the source of truth; there's no manifest. ops help and tab completions are generated from the filesystem.
  • Each script is independent. No framework coupling between commands. Delete one, the others still work. A broken script doesn't take down the CLI.
  • Descriptions come from comments. The first comment after the shebang is what ops help prints for that command.
  • Migration is filesystem-only. Adopt by pointing --dir at a folder of scripts. Stop using runic by deleting the eval line — the scripts remain as files with shebangs.

Downsides

  • A filesystem read on every invocation. Each ops <command> walks the script dirs. Millisecond-scale, proportional to script-dir size.
  • No global flags. --verbose doesn't transparently apply to every command — each script handles its own arg parsing.
  • No central validation. You can't declare "this command needs --env=staging|prod" once. Each script enforces its own contract.
  • Auto-help is minimal. One-line description per command; no generated usage / options / examples. Scripts that need rich help implement their own --help.
  • Refactor cost is filesystem cost. Rename db/ to database/ and the command name moves with it. There's no manifest layer insulating command names from file paths.
  • Distribution still requires runic. Sharing your CLI means sharing the script folder and asking the recipient to install runic and eval the init line. There's no single-binary pack.
  • Binaries are opaque. Source-code scripts can be inspected before they run; compiled executables can't. --allow-binaries puts that tradeoff in your hands rather than hiding it — enable it for dirs whose contents you trust, leave it off for dirs you don't.

Have an idea for addressing one of the downsides without changing the core model? Open an issue and let's talk.

Built-in commands

Every generated CLI has these built-in:

| Command | What it does | |---------|-------------| | ops help | List all commands with descriptions | | ops doctor | Check permissions, shebangs, runners for your scripts | | ops runtimes | Show which interpreters are installed on this host (with versions) | | ops completions zsh | Generate tab-completion script |

These delegate to the runic binary for the heavy lifting (directory walking, formatting). Dispatch — the thing that runs 100 times a day — stays in the shell function at zero cost.

Note: help, doctor, runtimes, completions, and create are reserved at the top level. A script literally named help.sh (or doctor.sh, etc.) at the root of your script folder will be unreachable — the built-in wins. Nest it (scripts/admin/help.sh) if you need that name.

runic runtimes

Lists every runtime runic supports, whether it's installed on this host, and what version. Run it before you start writing scripts to see what languages you can reach for; run it later when something unexpectedly stops working.

$ runic runtimes

  runic runtimes

  ✓ bash     5.2.37               (.sh, .bash)
  ✓ zsh     5.9                   (.zsh)
  ✓ python3 3.12.5                (.py)
  ✓ ruby    3.3.0                 (.rb)
  ✓ node    20.10.0               (.js)
  ✓ tsx     4.19.2                (.ts — via npx tsx)
  ✗ php     not installed         (.php)
  ✓ perl    5.38.0                (.pl)

  Extensionless files dispatch via their shebang line — any interpreter on PATH works.

Tab completions

Generate and load completions for your shell:

# zsh — add to .zshrc after the eval line
source <(ops completions zsh)

# bash
eval "$(ops completions bash)"

# fish
ops completions fish | source

Scaffolding new scripts

runic create scripts/lint.sh             # creates with #!/bin/bash
runic create scripts/dev/seed.py          # creates with #!/usr/bin/env python3
runic create scripts/gen/component.js     # creates with #!/usr/bin/env node

Creates the file with the right shebang, a description placeholder, and executable permissions.

Environment variables

Every script receives context through environment variables:

| Variable | Example | Description | |----------|---------|-------------| | RUNIC_COMMAND | dev start | The command path as typed | | RUNIC_CLI_NAME | ops | The CLI name | | RUNIC_SCRIPT_PATH | /home/you/scripts/dev/start.sh | Absolute path to the script | | RUNIC_DIR | /home/you/scripts | Which directory this script was found in | | RUNIC_DIRS | /home/you/scripts:/shared/scripts | All directories (colon-separated) |

Scripts can detect they're running inside a generated CLI by checking for RUNIC_COMMAND.

Command resolution

runic uses greedy longest-match. Given ops test unit:

  1. Try <dir>/test/unit.{sh,py,js,...} — if found, run it
  2. Try <dir>/test.{sh,py,js,...} with unit as an argument

This means a script and a directory can coexist with the same name:

scripts/
├── test.sh             # ops test            (default: run everything)
└── test/
    └── unit.sh         # ops test unit       (just the unit tests)

Both work. The longer match wins when it exists.

Extension probe order

When two scripts share a base name with different extensions (e.g., lint.sh and lint.py), the first match in probe order wins:

sh, bash, zsh, py, rb, js, ts, php, pl

ops doctor will flag this as a conflict.

Example: team CLI

A platform team maintains shared scripts in a git repo:

platform-tools/
├── dev/
│   ├── start.sh             # Start the local dev stack
│   ├── seed.py              # Seed dev DB with sample data
│   └── reset.sh             # Reset local state to a clean slate
├── gen/
│   ├── client.sh            # Generate typed API client from OpenAPI
│   ├── component.js         # Scaffold a new React component
│   └── service.rb           # Scaffold a new microservice from template
├── docs/
│   ├── serve.sh             # Run the docs site locally
│   └── lint.py              # Check docs for broken links
└── bin/
    └── fastgrep             # Rust binary for ripgrep-speed search
                             # (platform team vetted; opt-in with --allow-binaries)

Each team member adds to their .zshrc:

eval "$(runic init zsh --name plat --dir ~/repos/platform-tools --dir ~/my-scripts --allow-binaries)"

The Perl dev, the Python dev, and the bash purist all contribute scripts in their language. Nobody has to agree on a runtime. The Rust-shop subgroup ships a few compiled helpers in bin/ for anything that needs real throughput. The ~/my-scripts directory lets individuals add overrides or personal utilities without touching the shared repo — and --allow-binaries is a platform-level decision, not a per-script one.

Development

git clone https://github.com/rodrigopsasaki/runic.git
cd runic
pnpm install
pnpm test
pnpm run build   # produces dist/runic.cjs

License

MIT