@verirun/cli 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # verirun
2
2
 
3
3
  Record a browser workflow, replay it, and capture the Supabase, Vercel + Railway logs
4
- your backend produced during the replay.
4
+ your backend produced during the replay. Plus `verirun map`: a static
5
+ interaction graph of your React/TypeScript app — every page, what you can click
6
+ on it, and which backend calls those clicks make — without running anything.
5
7
 
6
8
  ## Install
7
9
 
@@ -49,7 +51,7 @@ verirun logout # disconnect this machine
49
51
 
50
52
  | | |
51
53
  | --- | --- |
52
- | `~/.verirun/config.json` | Holds the device token. Per user, not per project. |
54
+ | `~/.verirun/config.json` | Holds the device token. Per user, not per project. Restricted to your account (0600, or an `icacls` ACL on Windows). |
53
55
  | `~/.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. |
54
56
  | `$VERIRUN_LICENSE_KEY` | A raw key for CI, used instead of `verirun login`. |
55
57
  | `$VERIRUN_LICENSE_SERVER` | Point the CLI at a different server. |
@@ -72,6 +74,7 @@ machine past that is blocked until you release one from
72
74
  fingerprint and use a seat; release the stale one to fix it.
73
75
 
74
76
  `verirun --help`, `verirun --version`, `verirun log` and `verirun agent` do not require an account.
77
+ `verirun map` does.
75
78
 
76
79
  ---
77
80
 
@@ -96,8 +99,12 @@ Opens a headed browser. Perform the workflow by hand — click, type, navigate.
96
99
  | `-f, --force` | Overwrite an existing workflow of the same name. Without it, recording onto an existing name errors. |
97
100
 
98
101
  **Name rules.** `<name>` becomes a filename: letters, digits, dot, dash,
99
- underscore; must start with a letter or digit; 64 chars max. It is not
100
- sanitised — an invalid name errors rather than being rewritten.
102
+ underscore; must start with a letter or digit; 64 chars max. Two Windows rules
103
+ are enforced on every platform, so a workflow you record stays usable for a
104
+ teammate who checks the repo out on Windows: no trailing dot (Windows drops it,
105
+ colliding with the bare name), and no reserved device stem — `con`, `prn`, `aux`,
106
+ `nul`, `com1`–`com9`, `lpt1`–`lpt9`, at any extension. Names are not sanitised —
107
+ an invalid name errors rather than being rewritten.
101
108
 
102
109
  **Selectors.** Recording goes through Playwright's codegen engine, so each step
103
110
  gets its stable selector — `getByTestId`, then `getByRole` with an accessible
@@ -128,6 +135,52 @@ is saved and the command says so.
128
135
 
129
136
  ---
130
137
 
138
+ ### Recording or replaying as a signed-in user
139
+
140
+ Point both commands at a Chrome profile that is already logged in:
141
+
142
+ ```bash
143
+ verirun record checkout --channel chrome --user-data-dir "$HOME/.verirun-profile"
144
+ verirun run checkout --channel chrome --user-data-dir "$HOME/.verirun-profile"
145
+ ```
146
+
147
+ On Windows pass an absolute path instead — cmd and PowerShell don't expand `~`
148
+ inside an argument like this, and verirun never expands it either, so a literal
149
+ `~` just creates a directory called `~` and the run starts logged out:
150
+
151
+ ```powershell
152
+ verirun run checkout --channel chrome --user-data-dir "$env:USERPROFILE\.verirun-profile"
153
+ ```
154
+
155
+ Create that profile once, by hand, and sign in there — identity providers block
156
+ sign-in in an automation-launched browser:
157
+
158
+ ```bash
159
+ # macOS / Linux
160
+ google-chrome --user-data-dir="$HOME/.verirun-profile" --password-store=basic
161
+ ```
162
+
163
+ ```powershell
164
+ # Windows (PowerShell)
165
+ & "$env:ProgramFiles\Google\Chrome\Application\chrome.exe" `
166
+ --user-data-dir="$env:USERPROFILE\.verirun-profile"
167
+ ```
168
+
169
+ `--password-store=basic` (plus `--use-mock-keychain` on macOS) is not optional:
170
+ Playwright always launches Chrome with those flags, and cookies written under
171
+ your OS keyring can't be decrypted without them — the run starts logged out with
172
+ no error. Both switches are Unix-only, so on Windows they're ignored — there just
173
+ create the profile as the same Windows user that runs verirun. **The Windows path
174
+ is untested:** if a run starts logged out despite a signed-in profile, that's the
175
+ first thing to suspect. On every platform, don't reuse your everyday Chrome
176
+ profile; use a dedicated one. Close the profile before recording or running, since
177
+ Chrome locks it.
178
+
179
+ To check it took, reopen the profile on its own — if that window is signed in,
180
+ verirun will be too.
181
+
182
+ ---
183
+
131
184
  ## `verirun run <name>`
132
185
 
133
186
  ```bash
@@ -140,6 +193,7 @@ and writes `.verirun/runs/<name>-<timestamp>/logs.json`.
140
193
 
141
194
  | Flag | Meaning |
142
195
  | --- | --- |
196
+ | `--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. |
143
197
  | `--headed` | Show the browser window instead of running headless. |
144
198
  | `--timeout <ms>` | Per-action timeout, milliseconds. Non-negative. Defaults to Playwright's 30000. |
145
199
  | `--channel <name>` | Replay in an installed browser: `chrome`, `msedge`, … Overrides the channel saved in the workflow. Chromium family only. |
@@ -265,28 +319,151 @@ with `railway.deploymentId` in config.) Logs are already scoped to that service
265
319
  + environment, so there's no separate project filter. Already connected →
266
320
  one-line status.
267
321
 
268
- **`verirun log vercel`** — Vercel only *pushes* runtime logs (no pull API), so
269
- the listener needs a public tunnel and a Log Drain pointed at it, which the CLI
270
- can't create. It prints a 4-step checklist (tunnel → `--listen` → add the drain
271
- with **Sources: Edge + Serverless, Format: JSON, Endpoint: `<tunnel>/drain`** →
272
- copy the signing secret), opens the Log Drains page, then prompts for the secret
273
- (refusing to save silently without one) and an optional **project** name/ID to
274
- scope deliveries. Already connected → one-line status. Change the listener port
275
- via `vercel.listenerPort` in config, not a prompt.
322
+ **`verirun log vercel`** — same shape as the other two: it opens
323
+ `https://vercel.com/account/settings/tokens`, you paste a token, and it picks
324
+ the project. If the repo has a `.vercel/project.json` (i.e. you have run
325
+ `vercel link`), the project and team are read from it and **you are asked
326
+ nothing else**; otherwise it lists your teams and projects and you choose. It
327
+ then resolves the newest ready production deployment and reads its runtime logs
328
+ to confirm the token works. No tunnel, no drain, no second terminal.
329
+
330
+ On each run it re-resolves the newest ready deployment, so a redeploy doesn't
331
+ break the config. Pin one with `vercel.deploymentId`, or read preview
332
+ deployments instead with `vercel.environment: "preview"`.
333
+
334
+ <details>
335
+ <summary><b>Drain mode</b> (<code>--drain</code>) — the old push-based setup</summary>
336
+
337
+ Vercel's runtime-log API is the easy path, but if it isn't available to you, the
338
+ original Log Drain flow is still there under `verirun log vercel --drain`: a
339
+ tunnel, a local listener, and a drain pointed at it (4-step checklist → tunnel →
340
+ `--listen` → add the drain with **Sources: Edge + Serverless, Format: JSON,
341
+ Endpoint: `<tunnel>/drain`** → paste the signing secret). Config keys
342
+ `listenerPort`, `listenerPath`, `drainSecret`, `drainGraceMs`, `project` belong
343
+ to this mode only. verirun uses drain mode whenever `vercel.token` is absent,
344
+ so the two never collide.
345
+
346
+ </details>
276
347
 
277
348
  Non-interactive shells get the env/config values to set instead of a prompt.
278
349
 
279
350
  | Flag | Meaning |
280
351
  | --- | --- |
281
352
  | `--reconnect` | Run the connect flow again and replace what was saved. Any source. |
282
- | `--test` | **`supabase` / `railway`.** Query the last 5 minutes and print `<source> <row count>` or the error. Needs a connected source. Non-zero exit if it errored. |
283
- | `--listen` | **`vercel` only.** Start the Log Drain listener now and keep it running until Ctrl+C, printing each delivery as `timestamp level stream message`. Use it for the dashboard's endpoint-verification step and for live tailing. Binds `listenerPort` + `listenerPath` from config (defaults `4319` / `/drain`). |
353
+ | `--test` | Query the last 5 minutes and print what came back, or the error. Needs a connected source. Non-zero exit if it errored. |
354
+ | `--drain` | **`vercel` only.** Set up the old push-based Log Drain instead of the API. |
355
+ | `--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`). |
284
356
 
285
357
  An unknown `<source>` errors. A flag meant for another source is ignored.
286
358
  Each `verirun log <source>` connect flow *writes* its own block of
287
- `.verirun/config.json` (0600); the overview is read-only. A secret supplied via
288
- `$VERIRUN_SUPABASE_ACCESS_TOKEN` / `$VERIRUN_VERCEL_DRAIN_SECRET` /
289
- `$VERIRUN_RAILWAY_TOKEN` is never written to the file.
359
+ `.verirun/config.json`, restricted to your user account — mode 0600 on macOS and
360
+ Linux, and an `icacls` ACL granting only you on Windows, since mode bits mean
361
+ nothing there. If that cannot be applied the flow warns rather than leaving you
362
+ to assume the token is protected. The overview is read-only. A secret supplied via
363
+ `$VERIRUN_SUPABASE_ACCESS_TOKEN` / `$VERIRUN_VERCEL_TOKEN` /
364
+ `$VERIRUN_VERCEL_DRAIN_SECRET` / `$VERIRUN_RAILWAY_TOKEN` is never written to
365
+ the file.
366
+
367
+ ---
368
+
369
+ ## `verirun map`
370
+
371
+ Reads this repository's React/TypeScript source and builds an **interaction
372
+ graph**: one node per page, the interactive elements on it, the pages each one
373
+ navigates to, and the backend calls each one makes. Nothing is executed — no
374
+ browser, no server, no recording.
375
+
376
+ ```bash
377
+ verirun map # per-page summary
378
+ verirun map --json # the whole graph
379
+ verirun map --report # graph + coverage.md into .verirun/map
380
+ verirun map --bfs /account --depth 2 # what is reachable from one page
381
+ verirun map --reach # how much of each page was captured
382
+ ```
383
+
384
+ Routes come from a react-router `<Routes>` declaration **or** a Next.js App
385
+ Router `app/**/page.tsx` tree (`[id]` → `:id`, route groups dropped, `layout.tsx`
386
+ applied to its subtree). Components a page renders from another file are
387
+ followed, so a page whose real UI lives in a client component is not reported
388
+ empty. Import aliases are read from `tsconfig.json`; add more with
389
+ `--alias @/=src`.
390
+
391
+ ```
392
+ $ verirun map
393
+ 6 pages · 6 backend effects · 31 edges (40 files)
394
+
395
+ / — Page, 0 element(s), 10 nav, 0 effect
396
+ /account — AccountPage, 5 element(s), 1 nav, 4 effect
397
+ /login — LoginPage, 2 element(s), 2 nav, 3 effect
398
+ ```
399
+
400
+ `--bfs` takes a route path (`/account`), a page id (`page:/account`) or a page
401
+ component name, and reports each node once at its shortest distance, with the
402
+ element that gets you there:
403
+
404
+ ```
405
+ $ verirun map --bfs /account --depth 2
406
+ page:/account
407
+ └─ nav "verirun" → page:/
408
+ └─ effect "revoke" [if !(tokens.length === 0)] → effect:POST ?
409
+ └─ nav "Log in" → page:/login
410
+ # frontier at depth 2: page:/login, page:/signup
411
+ ```
412
+
413
+ **Limits, honestly.** It is static analysis: a target computed at runtime shows
414
+ as `unresolved`, an endpoint passed through a variable as `?`, and Supabase
415
+ `.auth.*` / `.storage.*` calls are not yet classified as effects. `--reach`
416
+ tells you what fraction of each page it actually attached, so you can see when
417
+ it is missing something rather than guessing.
418
+
419
+ ---
420
+
421
+ ## `verirun workflow check <name>`
422
+
423
+ Validates a workflow file without running it — JSON shape, schema version, known
424
+ actions, a `selector` where one is needed, the required `params` per action, and
425
+ warnings for the things that replay tolerates but you probably didn't mean
426
+ (a bare `role=button` that matches five elements, an `assert*` step that replay
427
+ skips, a first step that isn't `navigate`, and a `navigate` *after* the first
428
+ step — walking the app by URL skips the UI, so a dead link or a button that
429
+ stopped submitting still passes).
430
+
431
+ ```bash
432
+ verirun workflow check login
433
+ verirun workflow check login --json # machine-readable findings
434
+ verirun workflow check ./some/file.json
435
+ ```
436
+
437
+ Static only: it never opens a browser, so it cannot tell you whether a selector
438
+ matches the live page. Only `verirun run` can. Non-zero exit if anything is an
439
+ error.
440
+
441
+ This exists because a workflow doesn't have to come from `record`. `verirun map
442
+ --json` gives every element a `selector`, a `selectorConfidence`
443
+ (`high`/`medium`/`low`) and a suggested `action`, which is enough for you — or
444
+ an agent — to write `.verirun/workflows/<name>.json` directly:
445
+
446
+ ```jsonc
447
+ {
448
+ "schemaVersion": 1,
449
+ "name": "login",
450
+ "createdAt": "2026-09-20T00:00:00.000Z",
451
+ "browser": "chromium",
452
+ "steps": [
453
+ { "index": 0, "action": "navigate", "params": { "url": "https://app.example.com/login" }, "signals": [] },
454
+ { "index": 1, "action": "fill", "selector": "role=textbox[name=\"Email\"i]", "params": { "text": "ada@example.com" }, "signals": [] },
455
+ { "index": 2, "action": "click", "selector": "role=button[name=\"Log in\"i]", "params": {}, "signals": [] }
456
+ ]
457
+ }
458
+ ```
459
+
460
+ Actions and required `params`: `navigate` (`url`, absolute), `click` /
461
+ `dblclick`, `fill` (`text`), `press` (`key`), `check` / `uncheck`, `select`
462
+ (`values`), `setInputFiles` (`files`). Everything but `navigate` needs a
463
+ `selector`, which is handed to `page.locator()` verbatim.
464
+
465
+ A recorded workflow is ground truth; a written one is a guess until `run`
466
+ confirms it. Don't overwrite a recording with a generated file — use a new name.
290
467
 
291
468
  ---
292
469
 
@@ -301,8 +478,16 @@ workflows, recent runs.
301
478
  ```bash
302
479
  verirun agent > .verirun/AGENT.md # save it as context for your agent
303
480
  verirun agent --json # machine-readable form
481
+ verirun agent --skill --install # .claude/skills/verirun/SKILL.md
304
482
  ```
305
483
 
484
+ `--skill` emits the same knowledge as a **Claude Code skill** — YAML frontmatter
485
+ with a `description` that tells Claude when to load it, then a shorter body
486
+ aimed at deciding *which* command to reach for. With `--install` it writes
487
+ `.claude/skills/verirun/SKILL.md` in the repo, and Claude Code loads it on its
488
+ own whenever a task looks like app testing. Re-run it after upgrading verirun;
489
+ the file is generated, not hand-maintained.
490
+
306
491
  Paste the output into `CLAUDE.md`, `.cursorrules`, an `AGENTS.md`, or pipe it
307
492
  straight into a prompt. No account needed. The agent then knows to run
308
493
  `verirun run <name>`, read exit code + `.verirun/runs/<name>-<newest>/logs.json`,
@@ -364,11 +549,18 @@ it are sent only to the respective provider's own API.
364
549
  | `supabase.projectRef` | — | Supabase project ref. Required for Supabase capture. |
365
550
  | `supabase.accessToken` | — | Personal access token. Required for Supabase capture. |
366
551
  | `supabase.sources` | `postgres_logs`, `edge_logs`, `auth_logs` | Log sources to query, one request each. |
367
- | `vercel.listenerPort` | `4319` | Port the local Log Drain listener binds during a run. |
368
- | `vercel.listenerPath` | `/drain` | Path the listener accepts POSTs on. |
369
- | `vercel.drainSecret` | — | If set, deliveries with a missing or wrong `x-vercel-signature` (HMAC-SHA1 of the body) are rejected. |
370
- | `vercel.drainGraceMs` | `3000` | How long collection waits after replay for trailing drain batches. |
371
- | `vercel.project` | — | 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. |
552
+ | `vercel.token` | — | Vercel API token (vercel.com/account/settings/tokens). Its presence is what selects API mode over drain mode. |
553
+ | `vercel.projectId` | — | `prj_…` id of the project to read logs from. Required in API mode. |
554
+ | `vercel.teamId` | — | `team_…` id, when the project belongs to a team. |
555
+ | `vercel.deploymentId` | — | Pin log capture to one deployment instead of the newest ready one. |
556
+ | `vercel.environment` | `production` | Which target to resolve the newest deployment from: `production` or `preview`. |
557
+ | `vercel.readMs` | `8000` | How long to read the runtime-log stream before giving up. |
558
+ | `vercel.tailGraceMs` | `3000` | Extra window past the run's end to keep, for lines Vercel flushes late. |
559
+ | `vercel.listenerPort` | `4319` | *Drain mode.* Port the local Log Drain listener binds during a run. |
560
+ | `vercel.listenerPath` | `/drain` | *Drain mode.* Path the listener accepts POSTs on. |
561
+ | `vercel.drainSecret` | — | *Drain mode.* If set, deliveries with a missing or wrong `x-vercel-signature` (HMAC-SHA1 of the body) are rejected. |
562
+ | `vercel.drainGraceMs` | `3000` | *Drain mode.* How long collection waits after replay for trailing drain batches. |
563
+ | `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. |
372
564
  | `railway.token` | — | Railway API token (Account/Workspace, from railway.com/account/tokens). Required for Railway capture. |
373
565
  | `railway.deploymentId` | — | Pin log capture to one deployment. Overrides the project/environment/service trio. |
374
566
  | `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). |
@@ -379,7 +571,8 @@ it are sent only to the respective provider's own API.
379
571
  | Variable | Replaces |
380
572
  | --- | --- |
381
573
  | `VERIRUN_SUPABASE_ACCESS_TOKEN` | `supabase.accessToken` |
382
- | `VERIRUN_VERCEL_DRAIN_SECRET` | `vercel.drainSecret` |
574
+ | `VERIRUN_VERCEL_TOKEN` | `vercel.token` |
575
+ | `VERIRUN_VERCEL_DRAIN_SECRET` | `vercel.drainSecret` (drain mode) |
383
576
  | `VERIRUN_RAILWAY_TOKEN` | `railway.token` |
384
577
 
385
578
  ---
@@ -387,8 +580,8 @@ it are sent only to the respective provider's own API.
387
580
  ## Log capture
388
581
 
389
582
  A source appears only when its config block is present. Run `verirun log
390
- <source>` for guided setup, `verirun log supabase --test` to check the Supabase
391
- token, and `verirun log vercel --listen` to bring the listener up on its own.
583
+ <source>` for guided setup and `verirun log <source> --test` to check a
584
+ connected one against the last 5 minutes.
392
585
 
393
586
  **Supabase (pull).** After replay, one query per configured source goes to the
394
587
  Management API's `analytics/endpoints/logs.all` endpoint, scoped to the run
@@ -402,17 +595,27 @@ either `deploymentId` or `projectId` + `environmentId` + `serviceId` (the newest
402
595
  deployment of that trio is resolved first). Errors are warned about; the run
403
596
  continues. Logs are already scoped to one service + environment.
404
597
 
405
- **Vercel (push).** Vercel has no pull API for runtime logs. During a run,
406
- verirun runs a local HTTP listener on `listenerPort` + `listenerPath`; whatever
407
- the Log Drain POSTs while it is up is kept. It accepts both JSON-array and
408
- NDJSON bodies, echoes the `x-vercel-verify` challenge (so Vercel can confirm the
409
- endpoint), and — if `drainSecret` is set — rejects unsigned or wrongly signed
410
- deliveries. After replay, collection waits `drainGraceMs` for late batches, then
411
- stops the listener.
598
+ **Vercel (pull).** After replay, verirun resolves the project's newest ready
599
+ deployment (or `vercel.deploymentId`) and reads
600
+ `/v1/projects/<projectId>/deployments/<deploymentId>/runtime-logs`, keeping the
601
+ lines whose timestamp falls in the run window plus `tailGraceMs`. Needs `token`
602
+ and `projectId`. The endpoint streams and stays open to tail live lines, so the
603
+ read stops once the stream has been quiet for a moment, or after `readMs`.
604
+ Errors are warned about; the run continues. Retention is Vercel's, not ours —
605
+ on Hobby, runtime logs are short-lived, which is fine because collection happens
606
+ seconds after the run.
412
607
 
413
- ### One-time Vercel Log Drain setup
608
+ <details>
609
+ <summary><b>Vercel drain mode</b> (only when <code>vercel.token</code> is unset)</summary>
414
610
 
415
- Vercel must be able to reach your listener, so this is manual:
611
+ During a run, verirun runs a local HTTP listener on `listenerPort` +
612
+ `listenerPath`; whatever the Log Drain POSTs while it is up is kept. It accepts
613
+ both JSON-array and NDJSON bodies, echoes the `x-vercel-verify` challenge (so
614
+ Vercel can confirm the endpoint), and — if `drainSecret` is set — rejects
615
+ unsigned or wrongly signed deliveries. After replay, collection waits
616
+ `drainGraceMs` for late batches, then stops the listener.
617
+
618
+ One-time setup (`verirun log vercel --drain` walks you through it):
416
619
 
417
620
  1. Expose the listener port to the internet — `ngrok http 4319`, a Cloudflare
418
621
  Tunnel, etc. Note the public URL.
@@ -428,3 +631,62 @@ Vercel must be able to reach your listener, so this is manual:
428
631
 
429
632
  A free ngrok URL changes each session; a reserved hostname is worth it if you
430
633
  run this often.
634
+
635
+ </details>
636
+
637
+ ---
638
+
639
+ ## Changelog
640
+
641
+ ### 0.2.1
642
+
643
+ Windows support pass. Nothing in the CLI was Unix-only by design, but several
644
+ paths had never been exercised on Windows.
645
+
646
+ - **Workflow names** reject the two spellings Windows cannot store: a trailing
647
+ dot (Windows drops it, colliding with the bare name) and a reserved device
648
+ stem — `con`, `prn`, `aux`, `nul`, `com1`–`com9`, `lpt1`–`lpt9`, at any
649
+ extension. Enforced on every platform, so a workflow recorded on macOS or
650
+ Linux stays usable for a teammate on Windows.
651
+ - **Config permissions on Windows** — `.verirun/config.json`, `~/.verirun/` and
652
+ the files in it are now restricted with an `icacls` ACL granting only the
653
+ current user. Mode bits are not an access control on Windows, so these held
654
+ provider tokens and the device token on whatever ACL they inherited. A
655
+ connect flow that cannot lock the file down now warns instead of implying
656
+ protection it did not get.
657
+ - **`verirun log supabase`** can find the `supabase` CLI on Windows, where it
658
+ is a `.cmd` shim that needs a shell to launch. It previously always fell
659
+ through to the manual project-ref prompt.
660
+ - **Hardware fingerprint** filters Windows virtual adapters (Hyper-V, WSL,
661
+ VMware, VirtualBox, VPN and loopback), which were being counted as physical
662
+ and could shift the machine id — consuming an extra seat against the license.
663
+ - Docs: `--user-data-dir` needs an absolute path on Windows, since neither cmd,
664
+ PowerShell nor verirun expands `~`.
665
+
666
+ **Not verified on Windows.** These fixes are from reading the code and the
667
+ platform rules, not from a Windows test run. The signed-in Chrome profile path
668
+ in particular is untested there.
669
+
670
+ ### 0.2.0
671
+
672
+ - **`verirun map`** — static interaction graph of a React/TypeScript app:
673
+ every page, the interactive elements on it, where they navigate, and the
674
+ backend calls they make. No browser, no recording. `--json`, `--report`,
675
+ `--reach`, `--bfs <node> --depth N`. Routes come from react-router
676
+ `<Routes>` or a Next.js `app/**/page.tsx` tree.
677
+ - **`verirun workflow check`** — validate a workflow file without running it.
678
+ Together with the selectors `map` emits, a workflow can now be written by
679
+ hand (or by an agent) instead of recorded.
680
+ - **Vercel logs without a tunnel** — `verirun log vercel` is now a token and a
681
+ project, pulled from Vercel's runtime-log API. The old Log Drain flow
682
+ remains under `--drain` and is still used automatically for existing configs.
683
+ - **`verirun agent --skill --install`** — writes `.claude/skills/verirun/SKILL.md`
684
+ so Claude Code picks the tool up on its own.
685
+ - **Watchable replays** — `--headed` now paces actions (500ms), tunable with
686
+ `--slow-mo <ms>`.
687
+ - `verirun record`/`run` document how to use a signed-in Chrome profile.
688
+
689
+ ### 0.1.0
690
+
691
+ - `record`, `run`, `log` (Supabase / Vercel drain / Railway), `agent`, and the
692
+ account commands `login` / `logout` / `whoami`.