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

@verirun/cli

v0.2.5

Published

Record a browser workflow, replay it, and capture the Supabase / Vercel / Railway logs your backend produced.

Downloads

1,331

Readme

verirun

Record a browser workflow, replay it, and capture the Supabase, Vercel + Railway logs your backend produced during the replay. Plus verirun map: a static interaction graph of your React/TypeScript app — every page, what you can click on it, and which backend calls those clicks make — without running anything.

Install

npm install -g @verirun/cli

That installs the CLI and, in a postinstall step, downloads the Chromium build Playwright drives. verirun is then on your PATH. To also record in Firefox or WebKit: npx playwright install firefox webkit. To skip the browser download (you already have Playwright browsers, or you are in CI): set PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 before installing.

verirun operates on the directory you run it from: it reads and writes .verirun/ at the root of that repo (the nearest ancestor with a .git or, if none, package.json; otherwise the current directory). cd into the project you want to test before recording or running.

Help

verirun --help and verirun <command> --help work everywhere. A verirun(1) man page ships with the package: a global npm install -g @verirun/cli puts it on your MANPATH (man verirun) on macOS, Linux, and WSL / Git Bash. Native Windows (cmd, PowerShell) has no man — use --help, or man -l the file under a POSIX shell. From a npm link checkout: man -l man/verirun.1.


Account

record and run need a connected Verirun account. First run on a machine:

verirun login

This prints a short code and a link. Open the link, sign in (a 7 day free trial starts automatically if you are new), and enter the code. The CLI then holds a device token for that machine.

verirun whoami     # account, plan, subscription status
verirun logout     # disconnect this machine

| | | | --- | --- | | ~/.verirun/config.json | Holds the device token. Per user, not per project. Restricted to your account (0600, or an icacls ACL on Windows). | | ~/.verirun/state.json | The last server-signed lease (Ed25519) plus a local tamper check. Editing it, or a lease not signed by Verirun's key, is ignored and forces a fresh online check. | | $VERIRUN_LICENSE_KEY | A raw key for CI, used instead of verirun login. | | $VERIRUN_LICENSE_SERVER | Point the CLI at a different server. | | $HTTPS_PROXY / $HTTP_PROXY / $NO_PROXY | Honoured for every request the CLI makes. Needed on networks that force traffic through a proxy — Node does not read these on its own, so without them the CLI cannot connect where your browser can. | | $NODE_EXTRA_CA_CERTS | Path to your organisation's CA bundle. Needed when a proxy inspects TLS, which otherwise fails certificate verification. |

Behind a proxy? If verirun login cannot connect on a network where your browser works, that is almost always a proxy: set HTTPS_PROXY (and NODE_EXTRA_CA_CERTS if the proxy inspects TLS). The CLI names the underlying error — ENOTFOUND, ECONNREFUSED, a certificate failure — so you do not have to guess which it is.

Revalidation. Each successful check hands back a short-lived lease signed by the license server; the CLI verifies it against a public key built into the binary, so a fake or patched server cannot mint one. A lease is trusted for ~3 days (by its own signed timestamp), so most runs need no network. After that the CLI re-checks. An explicit "expired", "canceled", "revoked" or "seat limit" answer stops record/run immediately. A network failure never counts as invalid: the CLI keeps working off the last lease, silently for 15 days, then with a "connect to the internet" notice every run, and stops at 30 days offline until it can revalidate. The server can also refuse very old CLI versions.

Hardware binding. Each check sends a fingerprint of this machine — a hash of the identifier your OS assigns its own installation (/etc/machine-id on Linux, IOPlatformUUID on macOS, MachineGuid on Windows), plus platform, architecture and OS user. The raw values never leave the machine. Because it is anchored on the install rather than on your network hardware, docking a laptop, changing Wi-Fi, toggling a VPN or renaming the machine all leave it unchanged.

On a system that exposes no such identifier the CLI falls back to hashing the physical MAC addresses and hostname, which can shift when the network changes and would then take a second seat.

Your plan includes a set number of machine seats; connecting a new machine past that is blocked until you release one from https://get-verirun.duely.in/account. Reinstalling the OS issues a new identifier, so that does count as a new machine — release the stale one.

verirun --help, verirun --version, verirun log and verirun agent do not require an account. verirun map does.


verirun record <name>

verirun record checkout --url https://app.example.com

Opens a headed browser. Perform the workflow by hand — click, type, navigate. Close the browser to finish; the step sequence is saved to .verirun/workflows/<name>.json.

| Flag | Meaning | | --- | --- | | -u, --url <url> | URL to open at. Without it the browser starts blank and you navigate yourself. | | -b, --browser <name> | chromium (default), firefox, or webkit. | | --channel <name> | Record in an installed browser instead of bundled Chromium: chrome, msedge, chrome-beta, msedge-dev, … Saved into the workflow file so run reuses it. | | --default-browser | Record in your OS default browser. Chrome / Edge → that install (--channel); Firefox → Playwright's Firefox; Safari, Brave, Arc, Vivaldi, Opera → nearest engine (bundled Chromium or WebKit) with a warning. Falls back to bundled Chromium if detection fails. Overrides -b / --channel. The resolved browser/channel is saved to the workflow, so run reuses it. | | --user-data-dir <dir> | Record in a persistent profile directory, so the session is already logged in and stays logged in for later runs. The real browser using that profile must be closed first. | | --test-id-attribute <attr> | Attribute preferred for test-id selectors. Defaults to data-testid; also settable as testIdAttribute in config. | | -f, --force | Overwrite an existing workflow of the same name. Without it, recording onto an existing name errors. |

Name rules. <name> becomes a filename: letters, digits, dot, dash, underscore; must start with a letter or digit; 64 chars max. Two Windows rules are enforced on every platform, so a workflow you record stays usable for a teammate who checks the repo out on Windows: no trailing dot (Windows drops it, colliding with the bare name), and no reserved device stem — con, prn, aux, nul, com1–com9, lpt1–lpt9, at any extension. Names are not sanitised — an invalid name errors rather than being rewritten.

Selectors. Recording goes through Playwright's codegen engine, so each step gets its stable selector — getByTestId, then getByRole with an accessible name, falling back to CSS only when nothing better exists. A step stores three views of its target:

{
  "index": 3,
  "action": "fill",
  "params": { "text": "[email protected]" },
  "selector": "internal:role=textbox[name=\"Email\"i]",
  "locator": { "kind": "role", "body": "textbox", "options": { "name": "Email" } },
  "description": "getByRole('textbox', { name: 'Email' })"
}

selector is the raw string replay feeds to page.locator(), locator is its structured form, description is for you to read. Replay uses selector, so what you recorded is what runs.

Blur quirk. A text field is only committed when it loses focus. If you type into a field and close the browser without clicking or tabbing away, that last value is not recorded. Tab out before closing.

Dialogs. Answer alert / confirm / prompt boxes in the recording browser as you normally would. Codegen notes that a step opened one but not which button you pressed, so after you close the browser record asks, per such step, [O]K / [c]ancel (Enter = OK; without a terminal every answer is OK). The answer is saved on the step and run replays it:

{ "index": 4, "action": "click", "selector": "…", "dialog": { "action": "dismiss" } }

action is accept (OK) or dismiss (Cancel). For a prompt(), add "promptText": "…" — without it, accepting submits the prompt's default value. Every dialog the step opens gets the same answer.

Nothing recorded. If you close the browser without doing anything, nothing is saved and the command says so.


Recording or replaying as a signed-in user

Point both commands at a Chrome profile that is already logged in:

verirun record checkout --channel chrome --user-data-dir "$HOME/.verirun-profile"
verirun run    checkout --channel chrome --user-data-dir "$HOME/.verirun-profile"

On Windows pass an absolute path instead — cmd and PowerShell don't expand ~ inside an argument like this, and verirun never expands it either, so a literal ~ just creates a directory called ~ and the run starts logged out:

verirun run checkout --channel chrome --user-data-dir "$env:USERPROFILE\.verirun-profile"

Create that profile once, by hand, and sign in there — identity providers block sign-in in an automation-launched browser:

# macOS / Linux
google-chrome --user-data-dir="$HOME/.verirun-profile" --password-store=basic
# Windows (PowerShell)
& "$env:ProgramFiles\Google\Chrome\Application\chrome.exe" `
  --user-data-dir="$env:USERPROFILE\.verirun-profile"

--password-store=basic (plus --use-mock-keychain on macOS) is not optional: Playwright always launches Chrome with those flags, and cookies written under your OS keyring can't be decrypted without them — the run starts logged out with no error. Both switches are Unix-only, so on Windows they're ignored — there just create the profile as the same Windows user that runs verirun. The Windows path is untested: if a run starts logged out despite a signed-in profile, that's the first thing to suspect. On every platform, don't reuse your everyday Chrome profile; use a dedicated one. Close the profile before recording or running, since Chrome locks it.

To check it took, reopen the profile on its own — if that window is signed in, verirun will be too.


verirun run <name>

verirun run checkout

Replays the recorded steps — headless, in the same browser kind the workflow was recorded in — then captures logs for the wall-clock window the replay occupied and writes .verirun/runs/<name>-<timestamp>/logs.json.

| Flag | Meaning | | --- | --- | | --slow-mo <ms> | Pause before each action. Defaults to 500 ms when --headed so you can follow the run by eye, and to 0 otherwise. --slow-mo 0 watches at full speed; a bigger number follows a fast form more easily. | | --headed | Show the browser window instead of running headless. | | --timeout <ms> | Per-action timeout, milliseconds. Non-negative. Defaults to Playwright's 30000. | | --channel <name> | Replay in an installed browser: chrome, msedge, … Overrides the channel saved in the workflow. Chromium family only. | | --user-data-dir <dir> | Replay in a persistent profile directory, so it runs already logged in. The profile must not be open in another browser. | | --cdp <url> | Attach to a browser already running with remote debugging and drive it. See below. | | --user <id> | Keep only log entries whose message or raw payload contains this string, case-insensitive — e.g. a user's email. Overrides config.userFilter. Recorded in logs.json as "filter": { "user": … }; entries and the per-source counts are post-filter. If everything is filtered out, run warns. | | --dialog <answer> | How to answer a browser dialog opened by a step with no saved dialog answer: accept (OK, the default) or dismiss (Cancel). A step's own dialog always wins. |

Where the user filter runs. For Supabase it goes into the log query's WHERE (matched against the rendered message and the JSON of metadata), so only that user's rows leave the project — this matters on a busy database. Plain identifier needles (emails, UUIDs, usernames) qualify; a needle with quotes, spaces or other symbols isn't spliced into SQL and is filtered client-side instead (a dim note says so), as is a source whose backend rejects the WHERE. For Railway it's passed as a quoted phrase to the GraphQL log filter, same idea. For Vercel there is no server-side option — a Log Drain just pushes, with no content predicate — so it's always filtered client-side after delivery. Volume there is bounded by the ~seconds-long run window, not your user count; if it's still too much, point the drain at one project or a staging environment, or set a sampling rate on the drain in Vercel. Every source is also run through the client-side match as the canonical pass.

Target selection is first match of: --cdp, then --user-data-dir, then --channel (or the workflow's saved channel), then a fresh Playwright browser of the workflow's kind.

Running against a browser you already have open

google-chrome --remote-debugging-port=9222        # keep this window open
verirun run checkout --cdp http://localhost:9222

run attaches over CDP, reuses the existing context (cookies, auth, open tab), replays, and on finish disconnects without closing your browser. Only Chromium-family browsers expose CDP. If the workflow's first step is a navigate it drives the current tab there; otherwise it acts on whatever tab is frontmost.

Replay behaviour

  • Steps replay off the raw selector, so the recorded target is reproduced exactly.
  • Supported actions: navigate, click, dblclick, fill, press (modifier bitmask + key are recombined), check, uncheck, select, setInputFiles, openPage, closePage.
  • Assertions added in codegen (assert*) are skipped with a warning — they do not change which actions run.
  • An action the replayer does not recognise stops the run.
  • A step that fails does not abort log capture: the run is marked failed, whatever logs already landed are still written, and the exit code is non-zero.
  • Popups and newly opened tabs become the active target as they open. A workflow that drives several tabs at once is not supported yet.
  • Browser dialogs (alert, confirm, prompt, beforeunload) are answered automatically: with the step's saved dialog if it has one, otherwise with --dialog (default accept). Each one is printed under its step and listed in logs.json dialogs. With --cdp, only tabs the run itself drives or opens are touched.

Sequencing

Start every configured log source → replay → collect from each source for the [start, end] window → dispose every source. Dispose always runs, including after a replay failure or a source that failed to start. If one source fails to start, the run aborts before replay; if one fails during collection, it is warned about and contributes no entries.

logs.json

{
  "schemaVersion": 1,
  "workflow": "checkout",
  "status": "ok",
  "window": { "startedAt": "2026-08-31T13:01:50.998Z", "endedAt": "2026-08-31T13:01:51.499Z" },
  "steps": { "total": 12, "replayed": 12 },
  "filter": { "user": "[email protected]" },
  "dialogs": [
    { "step": 7, "type": "confirm", "message": "Delete this item?", "action": "accept", "at": "2026-08-31T13:01:51.102Z" }
  ],
  "sources": [{ "id": "vercel", "name": "Vercel", "entries": 8 }],
  "entries": [
    {
      "source": "vercel",
      "stream": "lambda",
      "timestamp": "2026-08-31T13:01:50.000Z",
      "level": "info",
      "message": "POST /api/checkout 200",
      "raw": {}
    }
  ]
}
  • status is ok or failed. On failed, steps also carries failedAt (the step index) and the file carries a top-level error string.
  • filter is present only when --user / config.userFilter was in effect; entries and every sources[].entries count are then post-filter.
  • dialogs lists every browser dialog the replay answered, in order: the step running when it opened, its type and message, the prompt's defaultValue (prompts only), the action taken, and the promptText submitted to an accepted prompt. Empty when none opened.
  • entries merges every source, sorted by timestamp. stream is the sub-stream within a source (lambda, edge, postgres_logs, …). raw is the provider's untouched payload (for Supabase that now includes the row's metadata, so a --user filter can match a nested email field).
  • With no log sources configured, run still replays and warns; entries is empty.

verirun log [source]

verirun log                 # overview: which sources are configured
verirun log supabase        # connect Supabase logs (interactive)
verirun log vercel          # connect Vercel logs (interactive)
verirun log railway         # connect Railway logs (interactive)

All three are connect flows, not pages of instructions.

verirun log supabase — if nothing is configured it opens https://supabase.com/dashboard/account/tokens, asks you to paste the token you create there, works out the project ref (from the supabase CLI if it's installed, otherwise one prompt), writes both to .verirun/config.json, and runs a test query to confirm it works. Already connected → one-line status.

verirun log railway — Railway has no HTTP drain, but its GraphQL API serves point-in-time deployment logs, so this works like Supabase. Two things: (1) create a token on the page it opens (https://railway.com/account/tokens); (2) paste the URL of the service you want logs from — verirun pulls the project / service / environment IDs out of it. The newest deployment of that service is resolved on every run, so it survives redeploys. (Pin a specific one with railway.deploymentId in config.) Logs are already scoped to that service

  • environment, so there's no separate project filter. Already connected → one-line status.

verirun log vercel — same shape as the other two: it opens https://vercel.com/account/settings/tokens, you paste a token, and it picks the project. If the repo has a .vercel/project.json (i.e. you have run vercel link), the project and team are read from it and you are asked nothing else; otherwise it lists your teams and projects and you choose. It then resolves the newest ready production deployment and reads its runtime logs to confirm the token works. No tunnel, no drain, no second terminal.

On each run it re-resolves the newest ready deployment, so a redeploy doesn't break the config. Pin one with vercel.deploymentId, or read preview deployments instead with vercel.environment: "preview".

Vercel's runtime-log API is the easy path, but if it isn't available to you, the original Log Drain flow is still there under verirun log vercel --drain: a tunnel, a local listener, and a drain pointed at it (4-step checklist → tunnel → --listen → add the drain with Sources: Edge + Serverless, Format: JSON, Endpoint: <tunnel>/drain → paste the signing secret). Config keys listenerPort, listenerPath, drainSecret, drainGraceMs, project belong to this mode only. verirun uses drain mode whenever vercel.token is absent, so the two never collide.

Non-interactive shells get the env/config values to set instead of a prompt.

| Flag | Meaning | | --- | --- | | --reconnect | Run the connect flow again and replace what was saved. Any source. | | --test | Query the last 5 minutes and print what came back, or the error. Needs a connected source. Non-zero exit if it errored. | | --drain | vercel only. Set up the old push-based Log Drain instead of the API. | | --listen | vercel drain mode only. Start the Log Drain listener now and keep it running until Ctrl+C, printing each delivery as timestamp level stream message. Binds listenerPort + listenerPath from config (defaults 4319 / /drain). |

An unknown <source> errors. A flag meant for another source is ignored. Each verirun log <source> connect flow writes its own block of .verirun/config.json, restricted to your user account — mode 0600 on macOS and Linux, and an icacls ACL granting only you on Windows, since mode bits mean nothing there. If that cannot be applied the flow warns rather than leaving you to assume the token is protected. The overview is read-only. A secret supplied via $VERIRUN_SUPABASE_ACCESS_TOKEN / $VERIRUN_VERCEL_TOKEN / $VERIRUN_VERCEL_DRAIN_SECRET / $VERIRUN_RAILWAY_TOKEN is never written to the file.


verirun map

Reads this repository's React/TypeScript source and builds an interaction graph: one node per page, the interactive elements on it, the pages each one navigates to, and the backend calls each one makes. Nothing is executed — no browser, no server, no recording.

verirun map                          # per-page summary
verirun map --json                   # the whole graph
verirun map --report                 # graph + coverage.md into .verirun/map
verirun map --bfs /account --depth 2 # what is reachable from one page
verirun map --reach                  # how much of each page was captured

Routes come from a react-router <Routes> declaration or a Next.js App Router app/**/page.tsx tree ([id] → :id, route groups dropped, layout.tsx applied to its subtree). Components a page renders from another file are followed, so a page whose real UI lives in a client component is not reported empty. Import aliases are read from tsconfig.json; add more with --alias @/=src.

$ verirun map
6 pages · 6 backend effects · 31 edges (40 files)

  / — Page, 0 element(s), 10 nav, 0 effect
  /account — AccountPage, 5 element(s), 1 nav, 4 effect
  /login — LoginPage, 2 element(s), 2 nav, 3 effect

--bfs takes a route path (/account), a page id (page:/account) or a page component name, and reports each node once at its shortest distance, with the element that gets you there:

$ verirun map --bfs /account --depth 2
page:/account
  └─ nav "verirun" → page:/
  └─ effect "revoke" [if window.confirm("Revoke this terminal's access? …")] → effect:POST ?
     opens confirm "Revoke this terminal's access? …" — the answer decides what the click does
    └─ nav "Log in" → page:/login
# frontier at depth 2: page:/login, page:/signup

Browser dialogs. An element whose handler calls the native alert(), confirm() or prompt() (bare, or on window / globalThis; a locally imported confirm is a custom modal, not a dialog) carries dialogs: [{ type, message, messageStatic, gates, … }] in --json. gates is true when the handler reads the answer, so OK and Cancel lead to different outcomes. An early if (!window.confirm(…)) return; also shows up as the guardSource of every backend call after it, which is how you see that Cancel skips the POST. The per-page summary counts the elements that open a dialog, --bfs prints each one under its edge, and coverage.md lists them all. A workflow step that clicks one answers it with its dialog field (see verirun run); workflow check warns when that field is missing (see below).

Limits, honestly. It is static analysis: a target computed at runtime shows as unresolved, an endpoint passed through a variable as ?, and Supabase .auth.* / .storage.* calls are not yet classified as effects. --reach tells you what fraction of each page it actually attached, so you can see when it is missing something rather than guessing.


verirun workflow check <name>

Validates a workflow file without running it — JSON shape, schema version, known actions, a selector where one is needed, the required params per action, and warnings for the things that replay tolerates but you probably didn't mean (a bare role=button that matches five elements, an assert* step that replay skips, a first step that isn't navigate, and a navigate after the first step — walking the app by URL skips the UI, so a dead link or a button that stopped submitting still passes).

verirun workflow check login
verirun workflow check login --json     # machine-readable findings
verirun workflow check ./some/file.json

If verirun map --report has saved .verirun/map/interaction-tree.json, check also warns about a step that clicks an element the graph says opens a browser dialog but has no dialog answer. Replay would press OK, which is usually what you want, but when the handler reads the answer, Cancel is a different path and should be chosen on purpose. Matching is by exact selector, so it catches workflows written from map --json. A selector that elements without a dialog also match (a bare role=button) is skipped rather than guessed at. Re-run map --report when the app changes; the saved graph goes stale.

Static only: it never opens a browser, so it cannot tell you whether a selector matches the live page. Only verirun run can. Non-zero exit if anything is an error.

This exists because a workflow doesn't have to come from record. verirun map --json gives every element a selector, a selectorConfidence (high/medium/low) and a suggested action, which is enough for you — or an agent — to write .verirun/workflows/<name>.json directly:

{
  "schemaVersion": 1,
  "name": "login",
  "createdAt": "2026-09-20T00:00:00.000Z",
  "browser": "chromium",
  "steps": [
    { "index": 0, "action": "navigate", "params": { "url": "https://app.example.com/login" }, "signals": [] },
    { "index": 1, "action": "fill", "selector": "role=textbox[name=\"Email\"i]", "params": { "text": "[email protected]" }, "signals": [] },
    { "index": 2, "action": "click", "selector": "role=button[name=\"Log in\"i]", "params": {}, "signals": [] }
  ]
}

Actions and required params: navigate (url, absolute), click / dblclick, fill (text), press (key), check / uncheck, select (values), setInputFiles (files). Everything but navigate needs a selector, which is handed to page.locator() verbatim. A step that opens a browser dialog can carry "dialog": { "action": "accept" | "dismiss", "promptText"?: "…" }; without it run accepts.

A recorded workflow is ground truth; a written one is a guess until run confirms it. Don't overwrite a recording with a generated file — use a new name.


verirun agent

Prints a single self-contained reference for an AI coding agent: every command and flag (read from the CLI itself, so it can't drift), the .verirun/ layout, the logs.json schema, the recommended record-then-run loop, and a snapshot of this checkout — resolved repo root, configured log sources, recorded workflows, recent runs.

verirun agent > .verirun/AGENT.md      # save it as context for your agent
verirun agent --json                   # machine-readable form
verirun agent --skill --install         # .claude/skills/verirun/SKILL.md
verirun agent --skill --install cursor  # .cursor/rules/verirun.mdc
verirun agent --skill --install all     # both

--skill emits the same knowledge as an agent skill — frontmatter with a description that tells the agent when to load it, then a shorter body aimed at deciding which command to reach for. --install writes it where the agent looks:

| Target | File | Loaded | |---|---|---| | claude (default) | .claude/skills/verirun/SKILL.md | automatically, when a task looks like app testing | | cursor | .cursor/rules/verirun.mdc | when the task matches its description (an agent-requested rule, not always-on) | | all | both | |

Re-run it after upgrading verirun; the file is generated, not hand-maintained.

For any other agent, paste the output into CLAUDE.md, an AGENTS.md, or pipe it straight into a prompt. No account needed. The agent then knows to run verirun run <name>, read exit code + .verirun/runs/<name>-<newest>/logs.json, and act on status / error / entries without you explaining the tool.


.verirun/ layout

Everything lives at the repo root:

.verirun/
  config.json                        # secrets and endpoints
  workflows/<name>.json              # recorded step sequence
  runs/<name>-<timestamp>/logs.json  # captured logs, one directory per run

record appends .verirun/ to the repo's .gitignore the first time it runs. If the repo has no .gitignore, it warns instead of creating one — run logs contain request bodies, tokens and user data, and recorded steps contain whatever you typed, including passwords.


Config — .verirun/config.json

Read once at startup. Optional; include only the blocks you use. Credentials in it are sent only to the respective provider's own API.

{
  "testIdAttribute": "data-testid",
  "userFilter": "[email protected]",
  "supabase": {
    "projectRef": "abcdefghijklmnopqrst",
    "accessToken": "sbp_...",
    "sources": ["postgres_logs", "edge_logs", "auth_logs"]
  },
  "vercel": {
    "listenerPort": 4319,
    "listenerPath": "/drain",
    "drainSecret": "whsec_...",
    "drainGraceMs": 3000,
    "project": "my-app"
  },
  "railway": {
    "token": "...",
    "projectId": "...",
    "environmentId": "...",
    "serviceId": "..."
  }
}

| Key | Default | Meaning | | --- | --- | --- | | testIdAttribute | data-testid | Attribute for test-id selectors. --test-id-attribute overrides. | | supabase.projectRef | — | Supabase project ref. Required for Supabase capture. | | supabase.accessToken | — | Personal access token. Required for Supabase capture. | | supabase.sources | postgres_logs, edge_logs, auth_logs | Log sources to query, one request each. | | vercel.token | — | Vercel API token (vercel.com/account/settings/tokens). Its presence is what selects API mode over drain mode. | | vercel.projectId | — | prj_… id of the project to read logs from. Required in API mode. | | vercel.teamId | — | team_… id, when the project belongs to a team. | | vercel.deploymentId | — | Pin log capture to one deployment instead of the newest ready one. | | vercel.environment | production | Which target to resolve the newest deployment from: production or preview. | | vercel.readMs | 8000 | How long to read the runtime-log stream before giving up. | | vercel.tailGraceMs | 3000 | Extra window past the run's end to keep, for lines Vercel flushes late. | | vercel.listenerPort | 4319 | Drain mode. Port the local Log Drain listener binds during a run. | | vercel.listenerPath | /drain | Drain mode. Path the listener accepts POSTs on. | | vercel.drainSecret | — | Drain mode. If set, deliveries with a missing or wrong x-vercel-signature (HMAC-SHA1 of the body) are rejected. | | vercel.drainGraceMs | 3000 | Drain mode. How long collection waits after replay for trailing drain batches. | | vercel.project | — | Drain mode. Keep only deliveries whose projectName or projectId equals this (a drain carries the whole team's logs). Unset = keep all. If every delivery is filtered out, run warns. | | railway.token | — | Railway API token (Account/Workspace, from railway.com/account/tokens). Required for Railway capture. | | railway.deploymentId | — | Pin log capture to one deployment. Overrides the project/environment/service trio. | | railway.projectId / railway.environmentId / railway.serviceId | — | Together, the target for which the newest deployment is resolved each run (so a redeploy doesn't need a config change). | | userFilter | — | Keep only log entries mentioning this string, case-insensitive — e.g. a user's email. Pushed server-side into the Supabase query WHERE and the Railway log filter when it's a plain identifier; client-side for Vercel. verirun run --user <value> overrides it per run. |

Environment variables (override the file)

| Variable | Replaces | | --- | --- | | VERIRUN_SUPABASE_ACCESS_TOKEN | supabase.accessToken | | VERIRUN_VERCEL_TOKEN | vercel.token | | VERIRUN_VERCEL_DRAIN_SECRET | vercel.drainSecret (drain mode) | | VERIRUN_RAILWAY_TOKEN | railway.token |


Log capture

A source appears only when its config block is present. Run `verirun log

Supabase (pull). After replay, one query per configured source goes to the Management API's analytics/endpoints/logs.all endpoint, scoped to the run window. Needs projectRef and an access token — missing either aborts the run before replay. A source that returns an error is warned about and skipped; others still run. Timestamps are normalised to ISO 8601.

Railway (pull). After replay, one deploymentLogs GraphQL query to backboard.railway.com/graphql/v2, scoped to the run window. Needs token and either deploymentId or projectId + environmentId + serviceId (the newest deployment of that trio is resolved first). Errors are warned about; the run continues. Logs are already scoped to one service + environment.

Vercel (pull). After replay, verirun resolves the project's newest ready deployment (or vercel.deploymentId) and reads /v1/projects/<projectId>/deployments/<deploymentId>/runtime-logs, keeping the lines whose timestamp falls in the run window plus tailGraceMs. Needs token and projectId. The endpoint streams and stays open to tail live lines, so the read stops once the stream has been quiet for a moment, or after readMs. Errors are warned about; the run continues. Retention is Vercel's, not ours — on Hobby, runtime logs are short-lived, which is fine because collection happens seconds after the run.

During a run, verirun runs a local HTTP listener on listenerPort + listenerPath; whatever the Log Drain POSTs while it is up is kept. It accepts both JSON-array and NDJSON bodies, echoes the x-vercel-verify challenge (so Vercel can confirm the endpoint), and — if drainSecret is set — rejects unsigned or wrongly signed deliveries. After replay, collection waits drainGraceMs for late batches, then stops the listener.

One-time setup (verirun log vercel --drain walks you through it):

  1. Expose the listener port to the internet — ngrok http 4319, a Cloudflare Tunnel, etc. Note the public URL.
  2. Vercel dashboard → your team's Settings → Log Drains.
  3. New drain: source Runtime Logs, delivery format JSON, endpoint the public URL plus your listenerPath (e.g. https://your-tunnel.ngrok.app/drain). Vercel verifies the endpoint on creation, so the listener must be reachable then — run verirun log vercel --listen in another terminal during setup.
  4. Copy the signing secret into VERIRUN_VERCEL_DRAIN_SECRET or vercel.drainSecret.
  5. Put listenerPort and listenerPath in .verirun/config.json.

A free ngrok URL changes each session; a reserved hostname is worth it if you run this often.


Changelog

0.2.5

  • verirun run answers browser dialogs instead of cancelling them. With no handler, Playwright dismissed every alert / confirm / prompt, so each confirm() in a workflow came back as Cancel and the action it guarded quietly never happened. Replay now answers each one: with the step's saved dialog (accept / dismiss, plus promptText for a prompt), otherwise with the new --dialog accept|dismiss (default accept). Each dialog is printed under its step and listed in logs.json under dialogs. With --cdp, only the tabs the run drives are touched.
  • verirun record asks which button you pressed. Playwright's recorder notes that a step opened a dialog but not the answer, so after you close the browser record asks [O]K / [c]ancel for each such step and saves it on the step.
  • verirun map shows the dialogs an element opens. --json carries a dialogs list per element (type, message, and gates when the handler reads the answer). An early if (!window.confirm(…)) return; now shows up as the guardSource of the backend calls after it, so you can see that Cancel skips them. The summary counts them, --bfs prints them under their edge, and coverage.md lists them.
  • verirun workflow check validates the dialog field. After map --report, it also warns about a step that clicks a dialog-opening element without saying OK or Cancel.

0.2.4

  • verirun map reads the import aliases your project actually declares. Only "@/*": ["./src/*"] was understood before; a paths entry resolved against baseUrl ("@/*": ["*"]) or inherited through extends — the two shapes create-next-app and most monorepos emit — silently produced no aliases at all. Every @/… import then resolved to nothing, so the child components a page renders were never analysed and each page was reported as empty: pages listed, zero elements, zero effects, zero edges, no warning.
  • An unresolvable import alias now says so, and says what to do. map prints the exact --alias flag to re-run with, instead of quietly reporting an app it could not read as an app with nothing in it. A scoped npm package is not mistaken for a broken alias.
  • verirun agent --skill --install now installs for Cursor too. --install cursor writes .cursor/rules/verirun.mdc as an agent-requested rule, --install all writes that and the Claude Code skill; a bare --install still means Claude Code.
  • verirun record --url checks the URL is up before opening a browser. A dev server that is not running produced a raw Node stack trace from Playwright's child process, followed by a message blaming the jsonl codegen target — pointing at the wrong thing entirely. It now stops with Nothing is listening at http://localhost:3000. The message for a codegen exit that genuinely recorded nothing no longer leads with the jsonl theory.

0.2.3

  • verirun whoami shows this machine's fingerprint, in the same 24-character form the account page lists, so a user with several machines of the same hostname can tell which row is which. The seat-limit message names it too, instead of only saying to go release something.

  • An unlimited seat count (admin keys) no longer prints as a raw integer.

  • A seat now follows the machine, not the network. The fingerprint is anchored on the identifier the OS assigns its own installation (/etc/machine-id, IOPlatformUUID, MachineGuid) instead of on MAC addresses and hostname. Docking a laptop, changing Wi-Fi, toggling a VPN or renaming the machine used to produce a new fingerprint, taking a second seat and — on a one-seat plan — locking the user out of the machine they were already using. Where an OS exposes no such identifier the old MAC-based fingerprint remains as a fallback.

    This shifts every fingerprint once. Machines connected on an earlier version register as new; release the stale entry from the account page.

0.2.2

Networking fixes. On a network that forces traffic through a proxy, the CLI could not connect at all — and said only "Check your connection", which was both unhelpful and often wrong.

  • Proxy support. HTTPS_PROXY / HTTP_PROXY / NO_PROXY are now honoured for every request the CLI makes. Node's fetch ignores these on its own, so on a corporate or campus network the browser worked while verirun login failed with no way to configure around it.
  • The real error is reported. A failed request now names its cause — ENOTFOUND (DNS filter or captive portal), ECONNREFUSED / timeouts (blocked, or a proxy that is down), or a certificate failure, which points at NODE_EXTRA_CA_CERTS for a proxy that inspects TLS. Previously the cause was caught and discarded.
  • A server error no longer blames your connection. A 429 now says the server is rate limiting; any other non-2xx says it is server-side.
  • verirun whoami shows why the server is unreachable, and whether a proxy is in use.
  • Log collection (Supabase, Vercel, Railway) goes through the same path, so it works behind a proxy too.

Requires Node 18.17+ (was 18), following the undici dependency added for proxy support.

0.2.1

Windows support pass. Nothing in the CLI was Unix-only by design, but several paths had never been exercised on Windows.

  • Workflow names reject the two spellings Windows cannot store: a trailing dot (Windows drops it, colliding with the bare name) and a reserved device stem — con, prn, aux, nul, com1–com9, lpt1–lpt9, at any extension. Enforced on every platform, so a workflow recorded on macOS or Linux stays usable for a teammate on Windows.
  • Config permissions on Windows — .verirun/config.json, ~/.verirun/ and the files in it are now restricted with an icacls ACL granting only the current user. Mode bits are not an access control on Windows, so these held provider tokens and the device token on whatever ACL they inherited. A connect flow that cannot lock the file down now warns instead of implying protection it did not get.
  • verirun log supabase can find the supabase CLI on Windows, where it is a .cmd shim that needs a shell to launch. It previously always fell through to the manual project-ref prompt.
  • Hardware fingerprint filters Windows virtual adapters (Hyper-V, WSL, VMware, VirtualBox, VPN and loopback), which were being counted as physical and could shift the machine id — consuming an extra seat against the license.
  • Docs: --user-data-dir needs an absolute path on Windows, since neither cmd, PowerShell nor verirun expands ~.

Not verified on Windows. These fixes are from reading the code and the platform rules, not from a Windows test run. The signed-in Chrome profile path in particular is untested there.

0.2.0

  • verirun map — static interaction graph of a React/TypeScript app: every page, the interactive elements on it, where they navigate, and the backend calls they make. No browser, no recording. --json, --report, --reach, --bfs <node> --depth N. Routes come from react-router <Routes> or a Next.js app/**/page.tsx tree.
  • verirun workflow check — validate a workflow file without running it. Together with the selectors map emits, a workflow can now be written by hand (or by an agent) instead of recorded.
  • Vercel logs without a tunnel — verirun log vercel is now a token and a project, pulled from Vercel's runtime-log API. The old Log Drain flow remains under --drain and is still used automatically for existing configs.
  • verirun agent --skill --install — writes .claude/skills/verirun/SKILL.md so Claude Code picks the tool up on its own.
  • Watchable replays — --headed now paces actions (500ms), tunable with --slow-mo <ms>.
  • verirun record/run document how to use a signed-in Chrome profile.

0.1.0

  • record, run, log (Supabase / Vercel drain / Railway), agent, and the account commands login / logout / whoami.