@reticlehq/server 2.7.0 → 2.8.0
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 +14 -5
- package/SKILL.md +64 -29
- package/dist/capsule/causal-summary.d.ts +20 -0
- package/dist/capsule/causal-summary.js +31 -6
- package/dist/capsule/divergence.js +7 -1
- package/dist/cli/chromium-hint.d.ts +33 -5
- package/dist/cli/chromium-hint.js +82 -12
- package/dist/cli/cli-doctor.js +28 -25
- package/dist/cli/cli-kill.d.ts +58 -0
- package/dist/cli/cli-kill.js +162 -0
- package/dist/cli/cli-launch.d.ts +17 -0
- package/dist/cli/cli-launch.js +28 -0
- package/dist/cli/cli-parse.d.ts +16 -1
- package/dist/cli/cli-parse.js +32 -9
- package/dist/cli/cli-port.d.ts +8 -0
- package/dist/cli/cli-port.js +16 -0
- package/dist/cli/cli-verify.js +2 -2
- package/dist/cli/doctor-sessions-line.d.ts +12 -0
- package/dist/cli/doctor-sessions-line.js +66 -0
- package/dist/cli/port-holder.d.ts +10 -0
- package/dist/cli/port-holder.js +26 -2
- package/dist/cli.js +99 -14
- package/dist/crawl/crawl-tools.js +12 -2
- package/dist/crawl/crawl.d.ts +13 -0
- package/dist/crawl/crawl.js +50 -2
- package/dist/events/contradictions.d.ts +14 -0
- package/dist/events/contradictions.js +26 -17
- package/dist/events/predicate-eval.d.ts +20 -1
- package/dist/events/predicate-eval.js +125 -3
- package/dist/events/predicate.js +170 -18
- package/dist/flows/flow-classify.js +1 -1
- package/dist/flows/flow-tools.js +12 -4
- package/dist/flows/heal-run.js +1 -1
- package/dist/honesty/blind-spots.js +6 -2
- package/dist/honesty/unsettled.d.ts +40 -0
- package/dist/honesty/unsettled.js +78 -0
- package/dist/honesty/verified.d.ts +7 -0
- package/dist/honesty/verified.js +3 -3
- package/dist/index.js +3 -0
- package/dist/init/agent-rules.d.ts +16 -0
- package/dist/init/agent-rules.js +96 -22
- package/dist/init/astro-patch.d.ts +2 -1
- package/dist/init/astro-patch.js +11 -5
- package/dist/init/capabilities.d.ts +22 -0
- package/dist/init/capabilities.js +76 -0
- package/dist/init/confirm.d.ts +96 -0
- package/dist/init/confirm.js +138 -0
- package/dist/init/cra.js +3 -2
- package/dist/init/non-js-project.js +29 -5
- package/dist/init/plan-framework.js +53 -15
- package/dist/init/plan.d.ts +40 -4
- package/dist/init/plan.js +163 -40
- package/dist/init/run.d.ts +24 -2
- package/dist/init/run.js +148 -42
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +46 -6
- package/dist/init/snippets.d.ts +44 -4
- package/dist/init/snippets.js +161 -37
- package/dist/init/vite-config.js +29 -3
- package/dist/input/network-mock-tools.d.ts +19 -0
- package/dist/input/network-mock-tools.js +29 -4
- package/dist/license/license.js +1 -1
- package/dist/mcp/mcp-proxy.d.ts +69 -3
- package/dist/mcp/mcp-proxy.js +205 -20
- package/dist/mcp/mcp.js +23 -7
- package/dist/mcp/proxy-lifecycle.d.ts +1 -1
- package/dist/mcp/proxy-lifecycle.js +1 -1
- package/dist/pool/browser-pool.d.ts +22 -2
- package/dist/pool/browser-pool.js +44 -4
- package/dist/project/learned-routes.d.ts +24 -0
- package/dist/project/learned-routes.js +63 -0
- package/dist/project/project-store.d.ts +6 -0
- package/dist/project/project-store.js +25 -1
- package/dist/session/no-session-diagnosis.d.ts +19 -0
- package/dist/session/no-session-diagnosis.js +71 -18
- package/dist/session/no-session-watch.js +12 -0
- package/dist/session/ref-provenance.d.ts +54 -0
- package/dist/session/ref-provenance.js +61 -0
- package/dist/session/session-lease.js +1 -1
- package/dist/telemetry/automation-hint.d.ts +17 -0
- package/dist/telemetry/automation-hint.js +71 -0
- package/dist/telemetry/bug-found.d.ts +0 -20
- package/dist/telemetry/bug-found.js +38 -21
- package/dist/telemetry/cli-telemetry.js +7 -1
- package/dist/telemetry/feedback.js +1 -1
- package/dist/telemetry/init-telemetry.js +9 -1
- package/dist/telemetry/install-source.d.ts +36 -0
- package/dist/telemetry/install-source.js +40 -0
- package/dist/telemetry/run-telemetry.js +4 -1
- package/dist/telemetry/session-metrics.js +5 -0
- package/dist/telemetry/telemetry.d.ts +5 -1
- package/dist/telemetry/telemetry.js +15 -2
- package/dist/telemetry/tool-refused.d.ts +19 -0
- package/dist/telemetry/tool-refused.js +55 -0
- package/dist/telemetry/verification-of.js +46 -7
- package/dist/temp-dir.d.ts +27 -0
- package/dist/temp-dir.js +46 -0
- package/dist/tools/act-tools.js +45 -1
- package/dist/tools/assert-grade.d.ts +1 -1
- package/dist/tools/assert-grade.js +27 -5
- package/dist/tools/assert-verdict.js +18 -1
- package/dist/tools/contract-tools.js +4 -0
- package/dist/tools/dynamic-tools.js +1 -1
- package/dist/tools/error-recovery.d.ts +10 -0
- package/dist/tools/error-recovery.js +64 -4
- package/dist/tools/feedback-invite.js +1 -1
- package/dist/tools/invoke-tool.js +139 -23
- package/dist/tools/lease-tools.d.ts +29 -1
- package/dist/tools/lease-tools.js +58 -5
- package/dist/tools/navigate-result.js +1 -1
- package/dist/tools/observe-tools.js +1 -1
- package/dist/tools/observed-nothing.js +1 -1
- package/dist/tools/react-grace.d.ts +48 -0
- package/dist/tools/react-grace.js +87 -0
- package/dist/tools/read-tools.js +20 -4
- package/dist/tools/settle-in-flight.d.ts +79 -0
- package/dist/tools/settle-in-flight.js +106 -0
- package/dist/tools/snapshot-delta.d.ts +7 -0
- package/dist/tools/snapshot-delta.js +130 -6
- package/dist/tools/tool-surface.js +19 -0
- package/dist/tools/tools.js +16 -4
- package/docs/README.md +56 -11
- package/docs/actions.mdx +87 -0
- package/docs/agent-cheatsheet.md +93 -29
- package/docs/architecture.md +46 -35
- package/docs/benchmarks.md +76 -72
- package/docs/best-practices.mdx +211 -0
- package/docs/capabilities.mdx +387 -0
- package/docs/cli/affected.mdx +51 -0
- package/docs/cli/capsules.mdx +69 -0
- package/docs/cli/cloud.mdx +127 -0
- package/docs/cli/doctor.mdx +76 -0
- package/docs/cli/drive.mdx +55 -0
- package/docs/cli/feedback.mdx +67 -0
- package/docs/cli/gate.mdx +64 -0
- package/docs/cli/help.mdx +92 -0
- package/docs/cli/hunt.mdx +57 -0
- package/docs/cli/identify.mdx +62 -0
- package/docs/cli/init.mdx +94 -0
- package/docs/cli/kill.mdx +91 -0
- package/docs/cli/license.mdx +48 -0
- package/docs/cli/mcp.mdx +61 -0
- package/docs/cli/open.mdx +68 -0
- package/docs/cli/restart.mdx +60 -0
- package/docs/cli/rollback.mdx +35 -0
- package/docs/cli/serve.mdx +62 -0
- package/docs/cli/status.mdx +64 -0
- package/docs/cli/stop.mdx +56 -0
- package/docs/cli/telemetry.mdx +57 -0
- package/docs/cli/update.mdx +52 -0
- package/docs/cli/verify.mdx +57 -0
- package/docs/cli/version.mdx +39 -0
- package/docs/cli/watch.mdx +55 -0
- package/docs/cli.mdx +219 -0
- package/docs/debugging.md +13 -9
- package/docs/deploy-checks.md +15 -11
- package/docs/desktop-apps.md +49 -40
- package/docs/desktop.mdx +188 -0
- package/docs/docs.json +363 -0
- package/docs/enterprise.md +20 -16
- package/docs/faq.mdx +88 -0
- package/docs/fixtures.md +11 -7
- package/docs/flows.md +26 -22
- package/docs/for-agents.md +71 -0
- package/docs/frameworks.mdx +199 -0
- package/docs/gate-plan.md +45 -41
- package/docs/gates.md +22 -18
- package/docs/getting-started.md +76 -65
- package/docs/human-control.md +22 -18
- package/docs/index.mdx +101 -0
- package/docs/install-agentic.mdx +215 -0
- package/docs/install-manual.mdx +194 -0
- package/docs/instrumentation.mdx +232 -0
- package/docs/integration-patterns.md +38 -34
- package/docs/local-registry.md +16 -12
- package/docs/multi-agent-testing.md +16 -12
- package/docs/packages/babel-plugin.mdx +70 -0
- package/docs/packages/browser.mdx +111 -0
- package/docs/packages/core.mdx +106 -0
- package/docs/packages/electron.mdx +87 -0
- package/docs/packages/eslint-plugin.mdx +84 -0
- package/docs/packages/next.mdx +91 -0
- package/docs/packages/react.mdx +91 -0
- package/docs/packages/server.mdx +98 -0
- package/docs/packages/tauri.mdx +63 -0
- package/docs/packages/test.mdx +108 -0
- package/docs/packages/vite-plugin.mdx +96 -0
- package/docs/packages.mdx +217 -0
- package/docs/platform-integration.md +33 -26
- package/docs/predicates.mdx +179 -0
- package/docs/quickstart.mdx +211 -0
- package/docs/recipes.mdx +242 -0
- package/docs/skill-file.mdx +105 -0
- package/docs/state-management.mdx +197 -0
- package/docs/style.css +14 -0
- package/docs/system-map.md +47 -32
- package/docs/telemetry-contract.md +93 -50
- package/docs/telemetry-events.mdx +430 -0
- package/docs/telemetry.md +57 -52
- package/docs/testing.md +10 -6
- package/docs/token-efficiency.md +30 -26
- package/docs/tools-act-and-wait.mdx +123 -0
- package/docs/tools-act-sequence.mdx +120 -0
- package/docs/tools-act.mdx +82 -0
- package/docs/tools-assert.mdx +70 -0
- package/docs/tools-console.mdx +59 -0
- package/docs/tools-inspect.mdx +85 -0
- package/docs/tools-navigate.mdx +63 -0
- package/docs/tools-network.mdx +86 -0
- package/docs/tools-observe.mdx +89 -0
- package/docs/tools-overview.mdx +157 -0
- package/docs/tools-query.mdx +82 -0
- package/docs/tools-session-and-feedback.mdx +91 -0
- package/docs/tools-sessions.mdx +65 -0
- package/docs/tools-snapshot.mdx +75 -0
- package/docs/tools-state.mdx +81 -0
- package/docs/tools-tools-and-run.mdx +125 -0
- package/docs/tools-wait-for.mdx +66 -0
- package/docs/troubleshooting.mdx +193 -0
- package/docs/usage.md +175 -153
- package/docs/vs-chrome-devtools-mcp.mdx +82 -0
- package/docs/vs-playwright-mcp.mdx +85 -0
- package/docs/vs-screenshots.mdx +96 -0
- package/docs/what-is-reticle.mdx +63 -0
- package/docs/why-reticle.mdx +107 -0
- package/package.json +2 -2
- package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
- package/docs/matrix/2.5.0/codex-machine.json +0 -20
- package/docs/matrix/2.5.0/cursor-machine.json +0 -22
- package/docs/matrix/2.5.0/gemini-machine.json +0 -22
- package/docs/matrix/2.5.0/opencode-machine.json +0 -22
- package/docs/matrix/2.5.0/vscode-machine.json +0 -22
- package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
- package/docs/matrix/MATRIX.md +0 -26
- package/docs/matrix/README.md +0 -59
- package/docs/mint.json +0 -61
package/README.md
CHANGED
|
@@ -3,16 +3,25 @@
|
|
|
3
3
|
The [Reticle](https://github.com/reticlehq/reticle) bridge + MCP server. It hosts a localhost WebSocket endpoint your app's `@reticlehq/browser` SDK connects to, and exposes MCP tools your coding agent uses to look at, act on, observe, and assert against the live app.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npx @reticlehq/server
|
|
6
|
+
npx @reticlehq/server init # wire the project: SDK + build plugin + MCP registration
|
|
7
|
+
npx @reticlehq/server mcp # MCP over stdio; auto-starts the bridge on ws://localhost:4400
|
|
7
8
|
```
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
The package installs a `reticle` bin, so inside a project `reticle mcp` and `npx @reticlehq/server mcp` are the same command. `reticle` is not a package name on npm.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
Point your agent at it. On Claude Code, register it at user scope:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
claude mcp add reticle -s user -- npx @reticlehq/server mcp
|
|
13
16
|
```
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
Default tools (18):
|
|
19
|
+
|
|
20
|
+
`reticle_sessions`, `reticle_snapshot`, `reticle_query`, `reticle_inspect`, `reticle_navigate`, `reticle_act`, `reticle_act_sequence`, `reticle_act_and_wait`, `reticle_observe`, `reticle_wait_for`, `reticle_assert`, `reticle_network`, `reticle_console`, `reticle_state`, `reticle_feedback`, `reticle_session`, `reticle_tools`, `reticle_run`.
|
|
21
|
+
|
|
22
|
+
Only `reticle_act_and_wait` and `reticle_assert` return a verdict; everything else moves or reads the app.
|
|
23
|
+
|
|
24
|
+
Another 30 tools ship but are not advertised, to keep the per-turn tool payload small: screenshots and visual diff, saved flows, recording, baselines, clock control, viewport, crawl, coverage, network mocking, leases and more. Reach any of them by name with `reticle_run`, list them with `reticle_tools`, or advertise the whole surface with `RETICLE_ADVERTISE_ALL_TOOLS=1` (this roughly doubles the per-turn tool payload).
|
|
16
25
|
|
|
17
26
|
See the [main README](https://github.com/reticlehq/reticle).
|
|
18
27
|
|
package/SKILL.md
CHANGED
|
@@ -7,9 +7,22 @@
|
|
|
7
7
|
> **The moment something is wrong, missing, awkward, or slower than it should be, file it — one call, then carry on with the task.** A step in this document that did not match reality, an install that needed a manual fix, a tool that returned nonsense, something you wished existed, something that works but takes five calls where it should take one. Nothing is too minor; the minor things are exactly what never gets reported and therefore never gets fixed.
|
|
8
8
|
>
|
|
9
9
|
> - Tools available: `reticle_feedback { kind, text, need?, impact?, currentApproach?, model?, trace? }` where `kind` is `bug` | `gap` | `ambiguity` | `feature_request` | `improvement`.
|
|
10
|
-
> - Not set up yet, or the daemon will not start (which is most of setup): `
|
|
10
|
+
> - Not set up yet, or the daemon will not start (which is most of setup): `npx @reticlehq/server feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement> "what happened"` — it needs nothing running.
|
|
11
11
|
> - Report defects in **Reticle**. A bug you find in the app under test is Reticle working; that one goes to the user. Never put app source, secrets, or user data in a report.
|
|
12
12
|
|
|
13
|
+
> ## Do not re-read this file to look something up
|
|
14
|
+
>
|
|
15
|
+
> This document is large, and a fetch of it may well have been truncated or summarised before it reached you. Everything in it also exists at `https://docs.reticle.sh`, one page at a time, and **appending `.md` to any page URL returns its source with no site chrome**:
|
|
16
|
+
>
|
|
17
|
+
> ```bash
|
|
18
|
+
> curl https://docs.reticle.sh/llms.txt # every page title and URL; read this first
|
|
19
|
+
> curl https://docs.reticle.sh/cli/doctor.md # one command: flags, real output, exit codes
|
|
20
|
+
> curl https://docs.reticle.sh/tools-act-and-wait.md # one tool: arguments, and what its verdict means
|
|
21
|
+
> curl https://docs.reticle.sh/troubleshooting.md # the failures people actually hit
|
|
22
|
+
> ```
|
|
23
|
+
>
|
|
24
|
+
> Pull the one page that answers the question rather than the whole document. Every page arrives with the rules that matter prepended, so a single fetch orients you without a second call.
|
|
25
|
+
|
|
13
26
|
Start by detecting which mode to run:
|
|
14
27
|
|
|
15
28
|
```bash
|
|
@@ -57,7 +70,7 @@ The one thing you may not do is stop early and call it done.
|
|
|
57
70
|
## Step 1 — Run `init`. Ask the user nothing.
|
|
58
71
|
|
|
59
72
|
```bash
|
|
60
|
-
npx @reticlehq/server init
|
|
73
|
+
RETICLE_INSTALL_SOURCE=skill_file npx @reticlehq/server init
|
|
61
74
|
```
|
|
62
75
|
|
|
63
76
|
That is the setup. It takes a few seconds and it does the whole job: detects the framework, package manager and UI library; registers the MCP server globally with whichever agents are installed; writes the agent verification rule **and the `/reticle` slash command**; installs the SDK pinned to the CLI's version; writes `.reticle.json`; wires the build config — the Vite plugin, or all three Next.js files (`next.config`, the root layout, and the `ReticleDev` component); and generates a **capabilities scaffold** pre-filled with the `data-testid` values it found and the state library it detected.
|
|
@@ -79,14 +92,15 @@ Reticle **attaches** to whatever is already running; it never starts or manages
|
|
|
79
92
|
|
|
80
93
|
Then read the report. Each line is marked:
|
|
81
94
|
|
|
82
|
-
| Mark | Meaning
|
|
83
|
-
|
|
|
84
|
-
| `✓`
|
|
85
|
-
| `·`
|
|
86
|
-
| `–`
|
|
87
|
-
|
|
|
95
|
+
| Mark | Meaning | What you do |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| `✓` | applied | nothing |
|
|
98
|
+
| `·` | already wired | nothing |
|
|
99
|
+
| `–` | skipped by a flag | nothing |
|
|
100
|
+
| `ℹ` | done, but incomplete in a way that matters | **read it** — the step ran and something about the result still stops a session appearing |
|
|
101
|
+
| `⚠` | needs a human/agent edit | **only these** — the line carries the exact snippet |
|
|
88
102
|
|
|
89
|
-
**If every line is `✓`, `·` or `–`, go to Step 1c.** The manual sections below exist for the `⚠` lines
|
|
103
|
+
**If every line is `✓`, `·` or `–`, go to Step 1c.** The manual sections below exist for the `⚠` and `ℹ` lines. `ℹ` is the one people skim past, because the step did not fail.
|
|
90
104
|
|
|
91
105
|
**`init` exits non-zero when a `⚠` lands on a step that makes the app CONNECT** (the Vite plugin, the `ReticleDev` component, the connect snippet). Nothing else applies that step, so the app will never dial the daemon and every tool will answer "no browser session connected" until you paste it in. A non-zero exit is therefore a to-do list for you, not a failed install — apply the snippet on that line, then validate. Other `⚠` lines (MCP registration, the agent rule) exit 0.
|
|
92
106
|
|
|
@@ -120,7 +134,7 @@ Register once globally so Reticle is available in every project:
|
|
|
120
134
|
claude mcp add reticle -s user -- npx @reticlehq/server mcp
|
|
121
135
|
```
|
|
122
136
|
|
|
123
|
-
Confirm with `claude mcp list` — `reticle` should appear. **After adding, restart Claude Code**
|
|
137
|
+
Confirm with `claude mcp list` — `reticle` should appear. **After adding, restart Claude Code** so it picks up the server. `/mcp` will not do it: that panel manages servers already loaded and never re-reads the config.
|
|
124
138
|
|
|
125
139
|
**If the `claude` CLI is unavailable**, fall back to merging `"reticle"` into `mcpServers` in `~/.claude.json` — Claude Code's user-scope config, and the same file `claude mcp add` writes. It is a large stateful file, so merge one key; never rewrite it:
|
|
126
140
|
|
|
@@ -239,25 +253,28 @@ Only add this if the user explicitly asks for the daemon to stop between turns:
|
|
|
239
253
|
|
|
240
254
|
---
|
|
241
255
|
|
|
242
|
-
## Step 1c —
|
|
256
|
+
## Step 1c — Do you already have the `reticle_*` tools? Usually yes.
|
|
257
|
+
|
|
258
|
+
**Check first. It costs one call, and most of the time it ends this step.**
|
|
243
259
|
|
|
244
|
-
|
|
260
|
+
- **`reticle_sessions` is callable → you have the tools. Skip straight to Step 4.**
|
|
261
|
+
- Not callable → read on.
|
|
245
262
|
|
|
246
|
-
**
|
|
263
|
+
`reticle init` registers the MCP server **globally, once per machine** (Claude Code user scope, `~/.cursor/mcp.json` for Cursor). So this step bites on the **first Reticle install on this machine and no other**. Every project after that starts with the tools already there — do not put a reload in front of a user who does not need one.
|
|
247
264
|
|
|
248
|
-
|
|
249
|
-
- If it is not, the tools have not loaded, and no amount of retrying will load them.
|
|
265
|
+
If they are not callable: your client read its server list when it started and has not read it again, so `reticle_*` is not callable in this session however successful the install was. **No amount of retrying loads it, and no slash command re-reads the config.** An agent that keeps calling `reticle_*` here gets "unknown tool" and misdiagnoses it as a broken install.
|
|
250
266
|
|
|
251
|
-
**
|
|
267
|
+
**Tell the user, in one line, to restart the client.** It takes them five seconds:
|
|
252
268
|
|
|
253
|
-
| Client
|
|
254
|
-
|
|
|
255
|
-
| Claude Code
|
|
256
|
-
| Cursor
|
|
257
|
-
| VS Code (Copilot) | reload the
|
|
258
|
-
|
|
|
269
|
+
| Client | What they do |
|
|
270
|
+
| --- | --- |
|
|
271
|
+
| Claude Code | **restart Claude Code.** `/mcp` does _not_ re-read the config — it only manages servers already loaded, so it cannot pick up a newly registered one |
|
|
272
|
+
| Cursor | reopen the window (Cmd/Ctrl-Shift-P → "Reload Window"). The MCP refresh button was removed in 1.0; the toggle in Settings → MCP sometimes works, the reload always does |
|
|
273
|
+
| VS Code (Copilot) | **no window reload needed** — open `.vscode/mcp.json` and hit the `Start` code lens, or Cmd/Ctrl-Shift-P → `MCP: List Servers` → Start. (Setting `chat.mcp.autostart` makes VS Code do this itself on config change.) |
|
|
274
|
+
| Windsurf / Zed | reopen the window |
|
|
275
|
+
| anything else | restart the client |
|
|
259
276
|
|
|
260
|
-
Say exactly this and nothing more: **"Reticle is installed.
|
|
277
|
+
Say exactly this and nothing more: **"Reticle is installed — this is a one-time step for this machine. Restart your client so it picks up the new MCP server (VS Code: just hit Start in `.vscode/mcp.json`), and tell me when the tools are back — then I'll verify a flow in your app."**
|
|
261
278
|
|
|
262
279
|
Then **wait for them, and continue where you left off.** Do not declare setup finished here: nothing has been verified yet, and this is the single most likely place for a user to walk away believing they are done. When the tools return, go to Step 4.
|
|
263
280
|
|
|
@@ -462,7 +479,18 @@ export default withReticle(nextConfig);
|
|
|
462
479
|
|
|
463
480
|
It configures **both** Turbopack and webpack, so it is correct on Next 16 (Turbopack by default) and on Next 15 and earlier. If `next dev` on Next 16 dies with _"This build is using Turbopack, with a webpack config and no turbopack config"_, you are on an old `@reticlehq/next` — upgrade it rather than dropping `withReticle`.
|
|
464
481
|
|
|
465
|
-
**Other frameworks** — call `reticle.connect()` and `install()` inside
|
|
482
|
+
**Other frameworks** — call `reticle.connect()` and `install()` inside the framework's own dev guard (`import.meta.env.DEV`, `import.meta.dev`, `process.env.NODE_ENV !== 'production'`). Do NOT guard on the hostname; see Step 4's checklist for why that one silently loses the whole setup.
|
|
483
|
+
|
|
484
|
+
**A page with NO build step** (plain HTML, or a Python/Ruby/PHP app serving templates) — you do not need npm, a bundler, or a `package.json`. Import the SDK from a URL, in a template you only serve in development:
|
|
485
|
+
|
|
486
|
+
```html
|
|
487
|
+
<script type="module">
|
|
488
|
+
import { reticle } from 'https://cdn.jsdelivr.net/npm/@reticlehq/browser@2.8.0/+esm';
|
|
489
|
+
reticle.connect({ token: '<contents of ~/.reticle/pairing-token>' });
|
|
490
|
+
</script>
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
The `token` is required: without it the bridge closes the socket with "authentication failed" and no session ever appears. Pin the version to the daemon's (`npx @reticlehq/server version`), because a floating import upgrades the page SDK underneath a daemon that did not move. Serving on something other than localhost? Add `allowNonLocalhost: true`.
|
|
466
494
|
|
|
467
495
|
---
|
|
468
496
|
|
|
@@ -534,6 +562,11 @@ Most no-connect cases are one of these. Fastest signal first:
|
|
|
534
562
|
3. Walk it with `reticle_act_and_wait`, narrating each step before you take it.
|
|
535
563
|
4. `reticle_assert` after each step — that the effect happened, not just that the click dispatched.
|
|
536
564
|
5. `reticle_console` + `reticle_network` at the end, for what the DOM does not show.
|
|
565
|
+
6. **`reticle_state` — and read what comes back.** This is the one check that proves the generated `reticle-dev` file is actually doing something. `init` writes it and registers every store it could prove from your source; the ones it could not (Jotai atoms, an XState actor, a TanStack `queryClient` — all need an argument only reading the code supplies) are left commented, and a file that registers nothing looks exactly like a working one from the outside.
|
|
566
|
+
- **State came back with your keys → done.** The agent can now check what the app BELIEVES, not only what it rendered.
|
|
567
|
+
- **State came back empty, or `hasCapabilities` is false → say so, in one line, and offer to finish it.** You have the codebase open; completing that file is a minute's work and it is the single highest-value thing left. What you may not do is report a clean install over an empty state read — `init` reported `✓` for writing the file, which is not the same as the file working.
|
|
568
|
+
|
|
569
|
+
This is a check, not a prerequisite: **an empty state read never blocks the flow you just drove.** Everything else — DOM, network, console, source pointers — works without it. Reporting it accurately is the requirement; treating it as a gate that must be cleared before you show the user anything is how a two-minute setup becomes an afternoon.
|
|
537
570
|
|
|
538
571
|
Then report what you drove and what it produced, with `file:line` for anything broken.
|
|
539
572
|
|
|
@@ -612,7 +645,7 @@ Two more worth knowing, because nothing else answers these:
|
|
|
612
645
|
- **`reticle_scroll_to`** — the only way to reach a row that a virtualised list has not mounted. A `query` that misses it is not evidence the row is absent.
|
|
613
646
|
- **`reticle_storage`** — what the app persisted (auth tokens, session ids). The difference between "logged in" and "looks logged in".
|
|
614
647
|
|
|
615
|
-
> **There is one tool surface, and nothing to pick.** The verify loop is advertised directly (navigate / snapshot / query / act / act_sequence / act_and_wait / observe / network / console / wait_for / assert / state / inspect / sessions / session / feedback). Everything else the skill names —
|
|
648
|
+
> **There is one tool surface, and nothing to pick.** The verify loop is advertised directly (navigate / snapshot / query / act / act_sequence / act_and_wait / observe / network / console / wait_for / assert / state / inspect / capabilities / sessions / session / feedback). Everything else the skill names — the flow and record tools — is one hop away through two meta-tools: `reticle_run({ tool, args })` invokes any tool by name, `reticle_tools` lists them (pass `names:[…]` for full params). `reticle_run` takes `sessionId` at the top level and forwards it, so you do not have to nest it in `args`. Advertised counts, checked by a gate (`surface-sizes.test.ts`): `default` 19, `all` 48.
|
|
616
649
|
>
|
|
617
650
|
> `all` is a verification switch, not a mode to drive agents in: `RETICLE_ADVERTISE_ALL_TOOLS=1` advertises every tool **with its output schema**, at several times the per-turn cost. It is read by the DAEMON at startup, never by your client — exporting it while a daemon is already running changes nothing, so run `npx @reticlehq/server stop` first.
|
|
618
651
|
|
|
@@ -763,7 +796,7 @@ For flows worth re-checking forever — the actual test suite — record them, t
|
|
|
763
796
|
Reticle's edge over a DOM/screenshot tool is **efficiency and robustness**, not that it sees things nothing else can. A capable browser-automation agent _can_ reach app state by running JS in the page (e.g. walking the React fiber via `browser_evaluate`) — but that's fragile (breaks on minified prod builds, non-React stores, framework-internals changes) and costs ~10× the tool-calls/tokens/time. Reticle reads the same truth with **one typed call**, stably:
|
|
764
797
|
|
|
765
798
|
- **UI-vs-state desync** (the UI shows one value, the store holds another — e.g. a count that didn't refresh, or a value corrupted in the store but rendered in no view): `reticle_state({ sessionId, store, path })` returns it directly. A pure snapshot can't see it; an agent spelunking framework internals can, but at far higher cost — measured head-to-head, Reticle-MCP caught a never-rendered store tamper in **4 tool calls / 45s** where a Playwright-MCP agent needed **45 calls / ~9min** reverse-engineering the React fiber.
|
|
766
|
-
- **Present-but-unusable / off-theme controls**: `reticle_inspect` returns `occluded` (covered by an overlay), `styles.cursor`/`opacity`, `box` (0×0), and `theme.offTheme` (color
|
|
799
|
+
- **Present-but-unusable / off-theme controls**: `reticle_inspect` returns `occluded` (covered by an overlay), `styles.cursor`/`opacity`, `box` (0×0), and `theme.offTheme` (color matches no design token; `theme.colorTokens` lists every token that does, and `theme.themeScope` says which theme it was measured under) — the "is it actually usable / on-brand?" read, in one call.
|
|
767
800
|
|
|
768
801
|
### Consume the human's bug reports (`reticle_session {action:"review"}`)
|
|
769
802
|
|
|
@@ -843,7 +876,7 @@ Use `npx @reticlehq/server status` to see which daemons are running and which se
|
|
|
843
876
|
|
|
844
877
|
### No Chromium / Playwright needed for standard use
|
|
845
878
|
|
|
846
|
-
Reticle does NOT download Chromium for normal agent workflows. The browser SDK runs inside the user's own browser — the agent sees the DOM, network, console, and state through the WebSocket bridge. Playwright is only installed if you explicitly call `
|
|
879
|
+
Reticle does NOT download Chromium for normal agent workflows. The browser SDK runs inside the user's own browser — the agent sees the DOM, network, console, and state through the WebSocket bridge. Playwright is only installed if you explicitly call `npx @reticlehq/server serve --drive <url>` or `npx @reticlehq/server verify`, which launch an autonomous browser for unattended automation.
|
|
847
880
|
|
|
848
881
|
To attach to a browser the user already has open (zero download, zero extra process):
|
|
849
882
|
|
|
@@ -867,7 +900,7 @@ npx --yes @reticlehq/server@latest version # force-resolves latest and prints
|
|
|
867
900
|
npx @reticlehq/server stop # stop any daemon running the old version
|
|
868
901
|
```
|
|
869
902
|
|
|
870
|
-
Then
|
|
903
|
+
Then restart Claude Code so the new version is picked up on next connection. `/mcp` does not respawn the stdio proxy — only a restart does.
|
|
871
904
|
|
|
872
905
|
**Second common cause: the Stop hook is killing the daemon between turns.** If `~/.claude/settings.json` has a Stop hook running `reticle stop --quiet`, remove it. The daemon must stay alive across turns — killing it forces a cold-boot spawn on every reconnect, and if that spawn takes longer than 10 seconds (cold npx cache, slow disk, first install), the proxy times out and exits with code 1. See Step 1b above.
|
|
873
906
|
|
|
@@ -884,7 +917,9 @@ Then reload Claude Code (`/mcp`) so the new version is picked up on next connect
|
|
|
884
917
|
|
|
885
918
|
2. **Check for the Stop hook:** `cat ~/.claude/settings.json | grep reticle` If present, delete that hook entry, then repeat step 1.
|
|
886
919
|
|
|
887
|
-
3. **If -32000 persists**, the daemon may be crashing on startup. Check the log: `cat ~/.reticle/daemon-4400.log | tail -30` Look for `reticle_daemon_start_failed` or `reticle_mcp_daemon_unavailable`. If the port is taken
|
|
920
|
+
3. **If -32000 persists**, the daemon may be crashing on startup. Check the log: `cat ~/.reticle/daemon-4400.log | tail -30` Look for `reticle_daemon_start_failed` or `reticle_mcp_daemon_unavailable`. If the port is taken: run `npx @reticlehq/server doctor`, which names the holder, then `npx @reticlehq/server kill` to free it and retry.
|
|
921
|
+
|
|
922
|
+
**Do not reach for `lsof -i :4400` and kill what it lists.** That matches every process on the port, and one of them is the agent's own MCP proxy holding a client socket, so the command that was supposed to fix your connection is the one that takes it away. It is the single most common cause of "the MCP went down". `reticle kill` signals only the listener. If you have no CLI to hand, the safe shell form is `lsof -nP -iTCP:4400 -sTCP:LISTEN -t | xargs kill -9`.
|
|
888
923
|
|
|
889
924
|
4. **Confirm the MCP config is user-level** (not project-level) and has no pinned version:
|
|
890
925
|
|
|
@@ -56,9 +56,29 @@ export interface CausalSummary {
|
|
|
56
56
|
stateSettleMs?: number;
|
|
57
57
|
/** Before→after for each changed storage key. Values capped. */
|
|
58
58
|
storageDiffs: StorageDiff[];
|
|
59
|
+
/**
|
|
60
|
+
* How many entries each list dropped to stay within MAX_SUMMARY_ENTRIES. Present only when
|
|
61
|
+
* something was dropped, so a trim is never silent — the same rule `ignoredDevTooling` follows.
|
|
62
|
+
*/
|
|
63
|
+
elided?: Partial<Record<CappedList, number>>;
|
|
59
64
|
route?: string;
|
|
60
65
|
signals: string[];
|
|
61
66
|
layoutShift?: number;
|
|
62
67
|
longTasks: number;
|
|
63
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* THE INVARIANT: every field of this summary has a bounded size that does not depend on how much the
|
|
71
|
+
* app did. A verdict travels in the same payload as its evidence, so an unbounded evidence field is
|
|
72
|
+
* an unbounded chance that the verdict is the part a client truncates — and an agent that cannot read
|
|
73
|
+
* the verdict has run the verification and got nothing, which is indistinguishable from not running
|
|
74
|
+
* it. Measured: one click on a page with a registered TanStack Query store returned ~363KB, because
|
|
75
|
+
* `capValue` bounded each diff and nothing bounded the COUNT of them.
|
|
76
|
+
*
|
|
77
|
+
* 20 keeps the signal that is actually read — the first paths to move, and the settle window over all
|
|
78
|
+
* of them — while the full names of everything that changed stay one `reticle_state` call away.
|
|
79
|
+
*/
|
|
80
|
+
export declare const MAX_SUMMARY_ENTRIES = 20;
|
|
81
|
+
/** The lists that are capped, named so `elided` can say which one lost entries. */
|
|
82
|
+
type CappedList = 'statePathsChanged' | 'storageKeysChanged' | 'stateDiffs' | 'storageDiffs' | 'signals';
|
|
64
83
|
export declare function causalSummary(events: readonly ReticleEvent[]): CausalSummary;
|
|
84
|
+
export {};
|
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
import { EventType, PerfMetric, isDevToolingUrl } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* THE INVARIANT: every field of this summary has a bounded size that does not depend on how much the
|
|
4
|
+
* app did. A verdict travels in the same payload as its evidence, so an unbounded evidence field is
|
|
5
|
+
* an unbounded chance that the verdict is the part a client truncates — and an agent that cannot read
|
|
6
|
+
* the verdict has run the verification and got nothing, which is indistinguishable from not running
|
|
7
|
+
* it. Measured: one click on a page with a registered TanStack Query store returned ~363KB, because
|
|
8
|
+
* `capValue` bounded each diff and nothing bounded the COUNT of them.
|
|
9
|
+
*
|
|
10
|
+
* 20 keeps the signal that is actually read — the first paths to move, and the settle window over all
|
|
11
|
+
* of them — while the full names of everything that changed stay one `reticle_state` call away.
|
|
12
|
+
*/
|
|
13
|
+
export const MAX_SUMMARY_ENTRIES = 20;
|
|
14
|
+
/** Trim one list to the cap and report the loss. Order is arrival order, so the head is the earliest. */
|
|
15
|
+
function cap(list, name, elided) {
|
|
16
|
+
if (list.length <= MAX_SUMMARY_ENTRIES)
|
|
17
|
+
return list;
|
|
18
|
+
elided[name] = list.length - MAX_SUMMARY_ENTRIES;
|
|
19
|
+
return list.slice(0, MAX_SUMMARY_ENTRIES);
|
|
20
|
+
}
|
|
2
21
|
function pushUnique(list, value) {
|
|
3
22
|
if ('string' === typeof value && value.length > 0 && !list.includes(value))
|
|
4
23
|
list.push(value);
|
|
@@ -118,6 +137,11 @@ export function causalSummary(events) {
|
|
|
118
137
|
break;
|
|
119
138
|
}
|
|
120
139
|
}
|
|
140
|
+
const elided = {};
|
|
141
|
+
// `stateSettleMs` is computed over EVERY diff, before the cap: the interval is a fact about the
|
|
142
|
+
// store, and shrinking it because the evidence was trimmed would make the transient window read as
|
|
143
|
+
// narrower than it was — the one number here that must survive the trim intact.
|
|
144
|
+
const settle = stateSettle(stateDiffs);
|
|
121
145
|
return {
|
|
122
146
|
net: {
|
|
123
147
|
total: netTotal,
|
|
@@ -126,13 +150,14 @@ export function causalSummary(events) {
|
|
|
126
150
|
...(headline === undefined ? {} : { headline }),
|
|
127
151
|
},
|
|
128
152
|
consoleErrors,
|
|
129
|
-
statePathsChanged,
|
|
130
|
-
storageKeysChanged,
|
|
131
|
-
stateDiffs,
|
|
132
|
-
...
|
|
133
|
-
storageDiffs,
|
|
153
|
+
statePathsChanged: cap(statePathsChanged, 'statePathsChanged', elided),
|
|
154
|
+
storageKeysChanged: cap(storageKeysChanged, 'storageKeysChanged', elided),
|
|
155
|
+
stateDiffs: cap(stateDiffs, 'stateDiffs', elided),
|
|
156
|
+
...settle,
|
|
157
|
+
storageDiffs: cap(storageDiffs, 'storageDiffs', elided),
|
|
134
158
|
...(route === undefined ? {} : { route }),
|
|
135
|
-
signals,
|
|
159
|
+
signals: cap(signals, 'signals', elided),
|
|
160
|
+
...(0 === Object.keys(elided).length ? {} : { elided }),
|
|
136
161
|
...(layoutShift === undefined ? {} : { layoutShift }),
|
|
137
162
|
longTasks,
|
|
138
163
|
};
|
|
@@ -7,7 +7,13 @@ function satisfies(link, events) {
|
|
|
7
7
|
case ConsequenceKind.SIGNAL:
|
|
8
8
|
return events.some((e) => e.type === EventType.SIGNAL && e.data['name'] === link.name);
|
|
9
9
|
case ConsequenceKind.STATE:
|
|
10
|
-
|
|
10
|
+
// Matched against the store name OR the changed path, because the link carries whichever half
|
|
11
|
+
// the predicate gave: `store` when it named one, and `path` otherwise (predicateToExpectedLinks).
|
|
12
|
+
// The event carries both. Comparing only the store meant every assertion written by path
|
|
13
|
+
// reported "never changed" however the app behaved — a false claim printed beside a `stateDiffs`
|
|
14
|
+
// entry showing that exact path changing.
|
|
15
|
+
return events.some((e) => e.type === EventType.STATE_CHANGE &&
|
|
16
|
+
(e.data['name'] === link.name || e.data['path'] === link.name));
|
|
11
17
|
case ConsequenceKind.NET:
|
|
12
18
|
return net(events).some((e) => 'string' === typeof e.data['url'] &&
|
|
13
19
|
e.data['url'].includes(link.urlContains) &&
|
|
@@ -7,9 +7,18 @@
|
|
|
7
7
|
* says missing afterwards. Reported from the field by an agent whose machine already had five
|
|
8
8
|
* chromium builds, none of them the wanted one, with no way to learn which one was wanted.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* The same reporter came back, because pinning the command fixes only half of it. Their five builds
|
|
11
|
+
* sat in the standard Windows browsers root and the line still read a flat `missing`, which leaves
|
|
12
|
+
* two readings — "the check is broken" and "none of these count" — and no way to choose. Present at
|
|
13
|
+
* the wrong revision and absent entirely are different problems: one is a single pinned download,
|
|
14
|
+
* the other means the lookup is aimed at a root nothing was ever installed into. Collapsing them is
|
|
15
|
+
* what made the loop unbreakable.
|
|
16
|
+
*
|
|
17
|
+
* Three things fix it, and they are what any unsatisfiable check needs: pin the command to the
|
|
18
|
+
* version doing the asking, say what was probed, and separate the verdicts that have different
|
|
19
|
+
* fixes. The wanted revision and the browsers root are both read back off the path Playwright itself
|
|
20
|
+
* resolves, so `PLAYWRIGHT_BROWSERS_PATH` and every platform default are honoured by construction
|
|
21
|
+
* rather than by a table of roots this file would have to keep in step with Playwright's.
|
|
13
22
|
*/
|
|
14
23
|
/** What the check found, gathered by the caller so this stays pure and testable. */
|
|
15
24
|
export interface ChromiumProbe {
|
|
@@ -19,6 +28,12 @@ export interface ChromiumProbe {
|
|
|
19
28
|
exists: boolean;
|
|
20
29
|
/** Version of the playwright the DAEMON bundles — not whatever `npx` would fetch. */
|
|
21
30
|
playwrightVersion?: string | undefined;
|
|
31
|
+
/** The revision that path names, e.g. `chromium-1223`. Undefined if the path has no revision in it. */
|
|
32
|
+
wantedRevision?: string | undefined;
|
|
33
|
+
/** The browsers root the lookup used, wherever `PLAYWRIGHT_BROWSERS_PATH` and the platform put it. */
|
|
34
|
+
browsersRoot?: string | undefined;
|
|
35
|
+
/** Chromium revisions actually sitting in that root, wanted or not. */
|
|
36
|
+
installedRevisions?: readonly string[] | undefined;
|
|
22
37
|
}
|
|
23
38
|
/**
|
|
24
39
|
* The install command, pinned when we know what to pin to.
|
|
@@ -27,11 +42,24 @@ export interface ChromiumProbe {
|
|
|
27
42
|
* at all is a worse one.
|
|
28
43
|
*/
|
|
29
44
|
export declare function chromiumInstallCommand(playwrightVersion?: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Split a resolved executable path into the browsers root and the revision under it, or null.
|
|
47
|
+
*
|
|
48
|
+
* Reading them back off Playwright's own answer is the point. The alternative is reconstructing the
|
|
49
|
+
* root from `PLAYWRIGHT_BROWSERS_PATH` plus a per-platform default, which is a copy of Playwright's
|
|
50
|
+
* resolution rules that drifts the first time they change one — and getting it wrong here means
|
|
51
|
+
* telling a user their browsers are missing while looking in the wrong directory, which is the exact
|
|
52
|
+
* failure this file exists to stop.
|
|
53
|
+
*/
|
|
54
|
+
export declare function parseChromiumRevision(executablePath: string): {
|
|
55
|
+
revision: string;
|
|
56
|
+
root: string;
|
|
57
|
+
} | null;
|
|
30
58
|
/**
|
|
31
59
|
* Ask the playwright THIS process would use, not the one `npx` would fetch.
|
|
32
60
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
61
|
+
* `executablePath()` resolves the revision the daemon will actually launch AND the root it will look
|
|
62
|
+
* in, which is why the probe never reads `PLAYWRIGHT_BROWSERS_PATH` itself.
|
|
35
63
|
*/
|
|
36
64
|
export declare function probeChromium(): Promise<ChromiumProbe>;
|
|
37
65
|
/** The bundled playwright's version, or undefined if it cannot be read (the hint then goes unpinned). */
|
|
@@ -7,12 +7,31 @@
|
|
|
7
7
|
* says missing afterwards. Reported from the field by an agent whose machine already had five
|
|
8
8
|
* chromium builds, none of them the wanted one, with no way to learn which one was wanted.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* The same reporter came back, because pinning the command fixes only half of it. Their five builds
|
|
11
|
+
* sat in the standard Windows browsers root and the line still read a flat `missing`, which leaves
|
|
12
|
+
* two readings — "the check is broken" and "none of these count" — and no way to choose. Present at
|
|
13
|
+
* the wrong revision and absent entirely are different problems: one is a single pinned download,
|
|
14
|
+
* the other means the lookup is aimed at a root nothing was ever installed into. Collapsing them is
|
|
15
|
+
* what made the loop unbreakable.
|
|
16
|
+
*
|
|
17
|
+
* Three things fix it, and they are what any unsatisfiable check needs: pin the command to the
|
|
18
|
+
* version doing the asking, say what was probed, and separate the verdicts that have different
|
|
19
|
+
* fixes. The wanted revision and the browsers root are both read back off the path Playwright itself
|
|
20
|
+
* resolves, so `PLAYWRIGHT_BROWSERS_PATH` and every platform default are honoured by construction
|
|
21
|
+
* rather than by a table of roots this file would have to keep in step with Playwright's.
|
|
13
22
|
*/
|
|
14
|
-
import { existsSync } from 'node:fs';
|
|
23
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
15
24
|
import { createRequire } from 'node:module';
|
|
25
|
+
/**
|
|
26
|
+
* A browsers-root entry that is a chromium build.
|
|
27
|
+
*
|
|
28
|
+
* Deliberately not matching `chromium_headless_shell-*`: it lives beside the full build and is a
|
|
29
|
+
* different artifact, so counting it would let a root holding only headless shells report the full
|
|
30
|
+
* browser as present-but-mismatched.
|
|
31
|
+
*/
|
|
32
|
+
const CHROMIUM_DIR = /^chromium-\d+$/;
|
|
33
|
+
/** The same directory name as a path segment, on either separator, so Windows paths split too. */
|
|
34
|
+
const CHROMIUM_SEGMENT = /[\\/](chromium-\d+)[\\/]/;
|
|
16
35
|
/**
|
|
17
36
|
* The install command, pinned when we know what to pin to.
|
|
18
37
|
*
|
|
@@ -23,20 +42,58 @@ export function chromiumInstallCommand(playwrightVersion) {
|
|
|
23
42
|
const pin = playwrightVersion === undefined ? 'playwright' : `playwright@${playwrightVersion}`;
|
|
24
43
|
return `npx ${pin} install chromium`;
|
|
25
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Split a resolved executable path into the browsers root and the revision under it, or null.
|
|
47
|
+
*
|
|
48
|
+
* Reading them back off Playwright's own answer is the point. The alternative is reconstructing the
|
|
49
|
+
* root from `PLAYWRIGHT_BROWSERS_PATH` plus a per-platform default, which is a copy of Playwright's
|
|
50
|
+
* resolution rules that drifts the first time they change one — and getting it wrong here means
|
|
51
|
+
* telling a user their browsers are missing while looking in the wrong directory, which is the exact
|
|
52
|
+
* failure this file exists to stop.
|
|
53
|
+
*/
|
|
54
|
+
export function parseChromiumRevision(executablePath) {
|
|
55
|
+
const match = CHROMIUM_SEGMENT.exec(executablePath);
|
|
56
|
+
if (null === match)
|
|
57
|
+
return null;
|
|
58
|
+
const revision = match[1];
|
|
59
|
+
if (revision === undefined)
|
|
60
|
+
return null;
|
|
61
|
+
return { revision, root: executablePath.slice(0, match.index) };
|
|
62
|
+
}
|
|
63
|
+
/** Chromium revisions present in a browsers root. Empty on an unreadable or absent root. */
|
|
64
|
+
function installedRevisions(root) {
|
|
65
|
+
try {
|
|
66
|
+
return readdirSync(root)
|
|
67
|
+
.filter((entry) => CHROMIUM_DIR.test(entry))
|
|
68
|
+
.sort();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// No root at all is the commonest case here and is not an error: it means nothing was installed.
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
26
75
|
/**
|
|
27
76
|
* Ask the playwright THIS process would use, not the one `npx` would fetch.
|
|
28
77
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
78
|
+
* `executablePath()` resolves the revision the daemon will actually launch AND the root it will look
|
|
79
|
+
* in, which is why the probe never reads `PLAYWRIGHT_BROWSERS_PATH` itself.
|
|
31
80
|
*/
|
|
32
81
|
export async function probeChromium() {
|
|
33
82
|
try {
|
|
34
83
|
const { chromium } = await import('playwright');
|
|
35
84
|
const executablePath = chromium.executablePath();
|
|
85
|
+
const parsed = parseChromiumRevision(executablePath);
|
|
36
86
|
return {
|
|
37
87
|
executablePath,
|
|
38
88
|
exists: existsSync(executablePath),
|
|
39
89
|
playwrightVersion: bundledPlaywrightVersion(),
|
|
90
|
+
...(null === parsed
|
|
91
|
+
? {}
|
|
92
|
+
: {
|
|
93
|
+
wantedRevision: parsed.revision,
|
|
94
|
+
browsersRoot: parsed.root,
|
|
95
|
+
installedRevisions: installedRevisions(parsed.root),
|
|
96
|
+
}),
|
|
40
97
|
};
|
|
41
98
|
}
|
|
42
99
|
catch {
|
|
@@ -62,14 +119,27 @@ export function bundledPlaywrightVersion() {
|
|
|
62
119
|
}
|
|
63
120
|
/** The doctor line for the Chromium check — verdict, what was probed, and how to satisfy it. */
|
|
64
121
|
export function chromiumHint(probe) {
|
|
65
|
-
if (probe.exists)
|
|
66
|
-
|
|
122
|
+
if (probe.exists) {
|
|
123
|
+
// Naming the revision on the happy line too: it is the number the mismatch line talks about, and
|
|
124
|
+
// a reader comparing two machines has nothing to compare without it.
|
|
125
|
+
return probe.wantedRevision === undefined
|
|
126
|
+
? '✓ installed'
|
|
127
|
+
: `✓ installed (${probe.wantedRevision})`;
|
|
128
|
+
}
|
|
67
129
|
const command = chromiumInstallCommand(probe.playwrightVersion);
|
|
130
|
+
if (probe.executablePath === undefined) {
|
|
131
|
+
return `✗ the playwright package is not installed; run: ${command}`;
|
|
132
|
+
}
|
|
133
|
+
const present = probe.installedRevisions ?? [];
|
|
134
|
+
if (present.length > 0 && probe.wantedRevision !== undefined) {
|
|
135
|
+
// The reported machine. Every fact here is one the reader could not otherwise get: what is
|
|
136
|
+
// wanted, what is there, and where "there" is. `npx playwright install chromium` unpinned adds
|
|
137
|
+
// a build to this list and changes nothing, which is what happened.
|
|
138
|
+
return (`✗ wrong revision — the bundled playwright wants ${probe.wantedRevision}; ` +
|
|
139
|
+
`${probe.browsersRoot ?? ''} holds ${present.join(', ')}. run: ${command}`);
|
|
140
|
+
}
|
|
68
141
|
// Naming the path is what turns "missing" from a verdict into evidence: a reader with browsers on
|
|
69
142
|
// disk can see immediately whether this is a missing install or a lookup pointed somewhere else
|
|
70
143
|
// (PLAYWRIGHT_BROWSERS_PATH, a different browsers root, a revision bump).
|
|
71
|
-
|
|
72
|
-
? 'the playwright package is not installed'
|
|
73
|
-
: `looked for ${probe.executablePath}`;
|
|
74
|
-
return `✗ missing — ${probed}; run: ${command}`;
|
|
144
|
+
return `✗ missing — looked for ${probe.executablePath}; run: ${command}`;
|
|
75
145
|
}
|