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

@aiscenblue/squab

v0.3.16

Published

An API workbench for the terminal — organise requests, send them, read the response, run the collection.

Readme

      __
    ,'o `.     squab
  =<      |    an api workbench you reach over ssh
    `.___,'
     ''  ''

A squab is a young pigeon. Pigeons carry messages; this one carries your requests. Organise them, send them, read the response, run the whole collection with assertions — in a terminal, over SSH.

See it running: https://aiscenblue.github.io/squab

Install

bunx @aiscenblue/squab
npm i -g @aiscenblue/squab

Or go install github.com/aiscenblue/squab@latest.

Run

squab                    # the workbench, on your own collections
squab serve              # over SSH: every public key gets a workspace
squab run example        # headless, exits non-zero if an expectation fails

Why

Collection runners, environment sharing and enough request history to be useful are all paid features elsewhere. They are also not hard problems — a runner is a for-loop with assertions, and an environment is a map. This is that, in a terminal, over SSH.

The workbench

 squab                                                   local · default
────────────────────────────────────────────────────────────────────────
 COLLECTIONS ────────│ REQUEST · example / get ──────────────────────────
 example             │  1 ### example · get
 ● GET    get        │  2 # @expect status 200
   POST   post json  │  3 GET {{base}}/get?from=squab
   GET    fails      │  4 Accept: application/json
                     │────────────────────────────────────────────────────
                     │ RESPONSE · 200 OK · 128ms · 688 B ─────────────────
                     │ jq ▸ .headers.Accept
                     │ ["application/json"]
────────────────────────────────────────────────────────────────────────
 ✓ all 1 variables resolve      ^p find · ^s send · ^r run · ^e vars · ?

| key | what it does | |---|---| | ctrl+p or / | fuzzy-find any request in any collection | | s or ctrl+s | save the buffer and send it | | ctrl+r | run the collection with its expectations | | i in the runner | jump straight into the request that failed, ready to edit | | r in the runner | rerun only what failed | | ctrl+e | variables, with the scope each value comes from | | e | edit the jq filter over the response | | H | response headers | | shift+J / shift+K | move between panes — the red bar shows which one has focus | | i | start editing the request; esc stops | | tab | move between panes | | ? | keys |

The request pane has two modes, like vim. It starts in normal mode, where letters are commands; press i to type, esc to stop. The pane title shows INSERT while you are editing.

Most terminals treat ctrl+s as flow control and freeze the screen with it, which is why s sends too. Run stty -ixon if you would rather have ctrl+s back.

Requests are .http files

A collection is a text file. It diffs, it reviews, and you can paste into it from a curl command or a docs page.

### get an order
# @expect status 200
# @expect jq .status == "paid"
GET {{base}}/v1/orders/42?expand=items
Authorization: Bearer {{token}}
# @off X-Debug: 1

### create one
# @expect status 201
POST {{base}}/v1/orders
Content-Type: application/json

{"sku": "A-19", "qty": 2}

### separates requests and names them. # @off keeps a header without sending it, and # @follow chases redirects for that one request.

Testing

A test is a # @expect line above the request it belongs to. There is no separate test file and no scripting language — the assertions live with the request they check, and travel with it in git.

The four checks

### the four kinds
# @expect status 200
# @expect jq .json.sku == "A-19"
# @expect header content-type json
# @expect contains "qty"
POST {{base}}/post
Content-Type: application/json

{"sku": "A-19", "qty": 2}

| check | asserts | |---|---| | status <code> | the response code, exactly | | jq <filter> == <value> | a value inside a JSON body | | header <name> <text> | that response header contains that text, case-insensitively | | contains <text> | the raw body contains that text |

A request may carry as many as you like, and every one is evaluated — a run reports all the failures, not just the first.

Writing jq assertions

The filter is real jq, so nesting, indexing and pipes all work:

# @expect jq .json.user.id == 7
# @expect jq .json.user.roles[0] == "admin"
# @expect jq (.json.user.roles | length) == 2
# @expect jq .items[-1].sku == "B-04"
# @expect jq [.items[].qty] == [2,1]

The right-hand side is JSON, which decides how you write it:

| value | written as | |---|---| | string | "paid" — with the quotes | | number | 1999 — without | | boolean | true | | null | null | | array | [2,1] | | object | {"a":1} |

.status == paid without quotes will not match a string, which is the usual first mistake.

Reading a failure

✗ one that fails so you can see the message   200 OK · 205ms
    .json.sku = "A-19", expected "B-99"

Failures report the actual value, so you rarely have to go and look at the body yourself. Each kind explains itself in its own terms:

expected status 200, got 409 Conflict
.json.user.id = 7, expected 8
header content-type = text/html; charset=utf-8, expected to contain "json"
body does not contain "already_cancelled"

A request that could not be sent at all says so instead of pretending to be an assertion failure:

✗ unreachable            could not resolve api.example.invalid
✗ missing variable       {{token}} is not set — nothing was sent

Chaining one request into the next

A named request publishes its response into the runtime scope, so later requests in the same run can use it:

### login
# @expect status 200
POST {{base}}/login

{"user": "ada", "password": "{{password}}"}

### orders, using the session login returned
# @expect status 200
GET {{base}}/orders
X-Prev-Status: {{login.status}}

{{login.body}} and {{login.status}} come from the request named login. Cookies persist across a run too, so a login that sets a session cookie is followed by requests that carry it.

Values you do not have to invent

### create an order with a fresh trace id
# @expect status 201
POST {{base}}/orders
X-Request-Id: {{$uuid}}

{"idempotency_key": "{{$uuid}}", "at": {{$timestamp}}}

Each use generates a new value, so the header and the body get different UUIDs. The full set:

{{$uuid}}         {{$guid}}           {{$timestamp}}        {{$epoch}}
{{$isoTimestamp}} {{$randomInt}}      {{$randomBoolean}}    {{$randomAlphaNumeric}}
{{$randomWord}}   {{$randomEmail}}    {{$randomFullName}}   {{$randomFirstName}}
{{$randomLastName}} {{$randomIP}}     {{$randomPort}}       {{$randomHexColor}}

A name that is not on this list stays unresolved and is reported, so a typo like {{$randomNmae}} fails loudly instead of being sent as literal text.

Running them

| where | how | |---|---| | the whole collection | ctrl+r in the workbench | | one request, with its checks | s — the status bar shows ✓ 2 of 2 asserts | | a failure you want to fix | i on the red row jumps into that request | | only what failed | r in the runner | | headless | squab run <collection> |

squab run exits non-zero when anything fails, which is what makes it usable in a pipeline:

squab run smoke -env staging
squab run smoke -reporter junit -reporter-export results.xml
squab run smoke -i "login" -i "orders"      # a subset
squab run smoke -env-var base=http://localhost:8080
squab run smoke -bail                        # stop at the first failure

Keeping tests honest in review

Deleting an assertion makes a suite pass by checking less, and that is invisible in a green build. squab diff names it:

  ~ get an order
      - expect       jq .status == "paid"
  ! weaker than HEAD: get an order

What this deliberately does not do

There is no scripting: you cannot compute a value, loop, or branch inside an assertion. Postman gives you a JavaScript sandbox for that.

It is a trade rather than an omission. # @expect jq .status == "paid" is readable by someone who does not write code, it diffs cleanly in a pull request, and squab diff can tell a reviewer when you removed it. A block of JavaScript buys power and costs all three.

Fix a failure without leaving the runner

A run leaves you looking at a list of results. Move to a red row and press i: squab closes the runner, selects that request, drops you into the editor with the reason it failed still on screen, and s sends it again. r reruns only the failures.

That loop — run, land on the break, fix, resend — is the reason to have a runner and an editor in the same program.

Review what changed in a pull request

Because a collection is text in your repo, squab can tell a reviewer what actually changed about your API tests — not what changed in a JSON blob:

squab diff              # working tree against HEAD
squab diff main         # against a branch
squab diff main..HEAD   # between two revisions
orders.http
  - cancel an order
      - request      POST {{base}}/v1/orders/42/cancel
  ~ get an order
      ~ url          {{base}}/v1/orders/42  →  {{base}}/v1/orders/{{id}}
      + header X-Trace {{$uuid}}
      - expect       jq .status == "paid"
  + list orders
  ! weaker than HEAD: cancel an order, get an order

3 change(s) since HEAD · 2 weakened

The last line is the point. Dropping an assertion or deleting a request makes a suite pass by checking less, and that is invisible in a green build. squab names it.

Variables have scopes, and the scope is visible

Precedence runs runtime > secret > env > global, and the variable table shows which layer won and how many others are shadowed — most variable bugs are a value arriving from a layer you forgot about.

Secrets live in secrets.json at mode 0600, outside the collections directory, and render masked. If that file is ever found readable by anyone else — copied from a backup, checked out with loose permissions — squab tightens it on load rather than reading tokens off a world-readable file. A value of the form $(command) is resolved by shelling out at send time and never written to disk:

token = $(op read op://api/token)

Over SSH

There is no hosted instance — this repository is source, not a server. You run the server, then SSH into the machine running it. On one machine that is:

# terminal 1 — run the server
squab serve -addr localhost:23234

# terminal 2 — connect to it
ssh -p 23234 localhost

ssh takes a host and a port, never a URL. To reach it from elsewhere, bind a public interface (-addr :23234), open that port, and connect with ssh -p 23234 you@your-host.

You need an SSH key for the client to offer — the server accepts any key but authenticates with keys only, so a machine with no key in ~/.ssh will be turned away. ssh-keygen -t ed25519 if you have none.

The public key is the account. A key that has not been seen before gets a workspace derived from its fingerprint, seeded with a working example collection — no password, no signup, no session store. Workspace directories are named by a hash of the fingerprint rather than the fingerprint itself, so a directory listing on the host does not enumerate anyone's keys.

In CI

run exits non-zero when an expectation fails:

squab run smoke -env staging

It writes JUnit or JSON for whatever picks up your test results:

squab run smoke -reporter junit -reporter-export results.xml
squab run smoke -reporter json  -reporter-export results.json

Other flags that matter in a pipeline: -i "one request" to run a subset, -env-var key=value to override a variable without editing a file, -bail to stop at the first failure, -timeout-request, -delay-request and -insecure.

Colour and motion

Colour comes from CharmTone, but it is only spent where it encodes something: the method, the status class, pass or fail, the scope of a variable. Chrome and keybinds stay neutral. The palette is declared per colour profile, so a 16-colour terminal degrades on purpose rather than by accident, and light and dark are chosen from the terminal's actual background colour.

Motion is Harmonica springs on anything that changes position, plus an ambient layer — a drifting gradient masthead, breathing focus rules, a shimmer while a response streams in. Ambient motion redraws constantly, which is worth thinking about over a network:

squab --no-ambient   # keep the springs, drop the drift
squab --still        # no animation at all

Layout

~/.squab/
  collections/*.http    the requests — commit these
  environments/*.json   per-environment variables
  globals.json          variables shared across environments
  secrets.json          masked values, mode 0600 — do not commit these

Built with

Bubble Tea · Lip Gloss · Bubbles · Wish · Harmonica · gojq

Charm's v2 modules live under charm.land, not github.com/charmbracelet:

charm.land/bubbletea/v2   charm.land/lipgloss/v2
charm.land/bubbles/v2     charm.land/wish/v2

Build

go build ./...
go test ./...