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

curtain-cli

v0.5.3

Published

Let your agents show you what they built: bring the stack up, drive the real app, record what happened, tear it down.

Readme

Curtain

Let your agents show you what they built.

Your agent says the feature works. Curtain is how it proves it: bring your stack up, drive the real app in a real browser, record what happened, tear it down.

Playwright writes the script. Curtain stages it.

ci license node npm dependencies


v0.1.0 built the somewhere, v0.2.0 films it, v0.4.0 gave a workspace data of its own, and v0.5.0 takes it all back on request and never before. Demos and tests as one declaration are next. The badge above is the released version; the roadmap marks every unreleased one as planned, on purpose.

Try it in under a minute

No install, no dependencies, no database. Clone the repo and the fixture is waiting: two real apps in a single file you can read in a minute.

$ cd fixture
$ curtain up
started  admin      http://localhost:52902  pid 10884
started  guest      http://localhost:52903  pid 10885

$ curtain up
reused   admin      http://localhost:52902  pid 10884
reused   guest      http://localhost:52903  pid 10885

Nothing restarted the second time, which is the point: a cold framework compile costs more than everything else this tool does put together.

Now make it prove itself. The walk signs in, adds an item, and waits for the card to go green, all against whichever port the resolver just found:

$ curtain walk add-an-item
walk     add-an-item
target   admin at http://localhost:52902
video    /Users/you/code/curtain/.curtain/walks/add-an-item/add-an-item.mp4
gif      /Users/you/code/curtain/.curtain/walks/add-an-item/add-an-item.gif

passed

That gif is not a mockup. It is the file that command wrote, and Curtain recorded it by driving the app in this repository.

Now break it on purpose, and watch what does not appear:

$ curtain walk _broken
walk     _broken
target   admin at http://localhost:52902
webm     .curtain/walks/_broken/video/[email protected]

WALK_FAILED
  locator.boundingBox: Timeout 10000ms exceeded.
  Call log:
    - waiting for getByRole('button', { name: 'A button that does not exist' })

  the recording up to the failure is at .curtain/walks/_broken/video/[email protected]

FAILED

Exit 1, and no mp4. A shareable video exists only for a run that passed, so a clean recording is a passing test rather than a promise. The raw webm is kept, because the frames leading up to a failure are usually the fastest way to understand it.

Now start a rival copy from its own git root and watch Curtain refuse to adopt it:

$ node rogue.mjs &
rogue root: /tmp/curtain-rogue-EQPK1W

$ curtain resolve
other checkouts
  52904      curtain-rogue-EQPK1W  pid 10901

$ curtain down
stopped  admin      was http://localhost:52902
stopped  guest      was http://localhost:52903

Two stopped, and the rogue is still serving. It was never yours to stop.

What you get

$ curtain resolve

workspace  /Users/you/code/app/.worktrees/feature-x
           worktree on feature/x, id 7c382ad4

running
  admin      http://localhost:4310  pid 41022  via runfile
  guest      http://localhost:4311  pid 41023  via runfile

other checkouts
  3001       app                 pid 4109
  3003       app-hotfix          pid 34039
  3010       worktrees/redesign  pid 97912

Two of those are yours and three are not, across four checkouts of one repo. An agent that cannot tell them apart will demo you someone else's branch and be convincing about it.

Curtain identifies a server by who started it, never by guessing from paths, and a server it cannot account for is reported rather than adopted. The design notes explain why paths cannot answer this and what happens under monorepo task runners.

Use it on your own project

| Skill | What it does | |---|---| | /setup | detects how your project starts, asks only what it cannot detect | | /up | starts what is missing, reuses what is healthy, links missing env files | | /env | gets a checkout its env files without ever reading a value | | /seed | gives this workspace data of its own, from your provisioning script | | /walk | drives the app in a real browser and records it | | /cleanup | shows what could be deleted, and deletes only when told | | /down | stops exactly what this workspace started |

Under the skills is a CLI you can run yourself:

curtain doctor            # can this phase run, and what debt has piled up
curtain resolve --json    # the raw truth, for when you want to see it
curtain up   [app...]
curtain down [app...]
curtain seed [name]       # no name lists them
curtain walk [name]       # no name lists them
curtain cleanup [--yes]   # bare is a dry run that deletes nothing
curtain env  [link|adopt] # bare = status; values never appear in any output
curtain setup detect | apply | browser

Every command takes --json. Failures are values with stable codes and a fix line, never a stack trace, so an agent branches on the code and never on wording.

A walk lives in curtain/walks/<name>.mjs and is a module Curtain calls, not a script you run. So it imports no Playwright, resolves no paths into the plugin, and above all names no port:

export const meta = { target: 'admin', viewport: 'phone' }

export default async function ({ page, url, click, type, sleep }) {
  await page.goto(url('/login'))
  await type(page.getByLabel('Email'), '[email protected]')
  await click(page.getByRole('button', { name: 'Sign in' }))
  await page.locator('.saved').waitFor()   // the assertion is the demo
  await sleep(1200)                        // let the payoff land on camera
}

export async function cleanup({ request, url }) {   // runs even if the walk throws
  await request.delete(url('/items'))
}

target: 'admin' is the entire address. Whatever port that app landed on this morning, in this worktree, is the resolver's problem and never the walk's.

Install

Curtain is a zero-dependency Node CLI, so it is not tied to one agent harness. Every command takes --json and fails with a stable code and a fix line, which is the whole interface: anything that can run a command can drive it. Claude Code gets a plugin manifest today.

/plugin marketplace add jdsalomon/curtain
/plugin install curtain@curtain

Skills and the Playwright MCP server come with it, and bin/ lands on the Bash tool's PATH, so a bare curtain just works.

npm i -g curtain-cli    # the binary is `curtain`
curtain --version

Or from a clone, which is also how you get the fixture tour above:

git clone https://github.com/jdsalomon/curtain
cd curtain && npm link          # or just put ./bin on your PATH

.mcp.json registers Playwright's own MCP server, which ships with the plugin so your harness can drive a browser interactively. Curtain never calls it, since a walk drives Playwright in process; register it only if you want it. The CLI is the whole engine: nothing under lib/ has harness-specific code. The prose half lives in skills/ as markdown with YAML frontmatter, so point your harness at those files or read them yourself.

First-class packaging for other harnesses is on the roadmap.

Configuration

One committed file, curtain.json, holding only facts that cannot go stale:

{
  "name": "myproject",
  "apps": {
    "admin": {
      "start": "make admin-dev",
      "ready": "Ready in",
      "env": ["apps/admin/.env.local"],
      "fingerprint": { "path": "/login", "expect": "password" }
    },
    "guest": { "start": "make guest-dev", "ready": "Ready in" }
  },
  "envs": { "prod": "myproject.com" }
}

A fingerprint is what the app must actually serve, so it does double duty: it identifies a listener nothing else claims, and it is the health check on your own server. Answering is not working, since a dev server with a broken build answers 500 to everything, and a walk filmed against that looks like a broken feature.

env names the gitignored files an app needs, which is exactly what a fresh clone or worktree is missing. The values live once per project on your machine, in a store keyed by name; every checkout reaches them through a symlink that curtain up creates itself. If your project already keeps its values somewhere, name that directory with "envStore" and Curtain uses it instead of inventing a second one. The schema stays in your committed .env.example, so a branch that adds a variable is caught by name, and the values never appear in any output.

envs declares your deployed hostnames so a recording can refuse them: a walk mutates data, an unknown host classifies as prod, and only --force gets past the refusal.

Ports and pids are never stored. They are discovered every time, because a stored port is a lie waiting to happen. curtain.local.json is gitignored and merged over the top for the teammate who starts the app differently. .curtain/ is machine-local state and belongs in .gitignore, which setup handles: the dot is the mnemonic, dotted is disposable.

Where this is going

Your agent should never have to say "it works, trust me."

Bring the stack up. Give it data of its own. Drive the real app. Record what happened. Tear it down. One command each, in a repo Curtain has never seen.

One declaration rendered as either a demo or a test suite, a cache that makes each new recording cheaper than the last, and layout truth across every viewport are the releases after this one. See ROADMAP.md.

The rest

  • Platforms: macOS and Linux, both covered by CI, because reading a process's working directory is /proc on one and lsof on the other. Windows reports UNSUPPORTED_PLATFORM rather than pretending.
  • Design: docs/DESIGN.md, including the decisions that were wrong first.
  • Brand: docs/BRAND.md and assets/.
  • Contributing: CONTRIBUTING.md.

License

MIT