@reticlehq/init 2.14.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.
Files changed (114) hide show
  1. package/LICENSE +105 -0
  2. package/NOTICE +5 -0
  3. package/README.md +29 -0
  4. package/dist/agent-rules.d.ts +116 -0
  5. package/dist/agent-rules.js +279 -0
  6. package/dist/app-choice.d.ts +29 -0
  7. package/dist/app-choice.js +39 -0
  8. package/dist/astro-host.d.ts +21 -0
  9. package/dist/astro-host.js +27 -0
  10. package/dist/astro-patch.d.ts +33 -0
  11. package/dist/astro-patch.js +103 -0
  12. package/dist/brace-scan.d.ts +24 -0
  13. package/dist/brace-scan.js +46 -0
  14. package/dist/capabilities.d.ts +40 -0
  15. package/dist/capabilities.js +167 -0
  16. package/dist/closing-hint.d.ts +35 -0
  17. package/dist/closing-hint.js +122 -0
  18. package/dist/codex-toml.d.ts +10 -0
  19. package/dist/codex-toml.js +144 -0
  20. package/dist/connect-steps.d.ts +55 -0
  21. package/dist/connect-steps.js +92 -0
  22. package/dist/cra.d.ts +82 -0
  23. package/dist/cra.js +183 -0
  24. package/dist/csp-check.d.ts +49 -0
  25. package/dist/csp-check.js +168 -0
  26. package/dist/csp-doctor.d.ts +32 -0
  27. package/dist/csp-doctor.js +62 -0
  28. package/dist/desktop-doctor.d.ts +46 -0
  29. package/dist/desktop-doctor.js +201 -0
  30. package/dist/detect.d.ts +225 -0
  31. package/dist/detect.js +334 -0
  32. package/dist/dev-script.d.ts +51 -0
  33. package/dist/dev-script.js +66 -0
  34. package/dist/dev-server-ports.d.ts +20 -0
  35. package/dist/dev-server-ports.js +45 -0
  36. package/dist/electron-patch.d.ts +11 -0
  37. package/dist/electron-patch.js +70 -0
  38. package/dist/electron-vite-patch.d.ts +12 -0
  39. package/dist/electron-vite-patch.js +100 -0
  40. package/dist/existing-config.d.ts +31 -0
  41. package/dist/existing-config.js +63 -0
  42. package/dist/format-generated.d.ts +18 -0
  43. package/dist/format-generated.js +48 -0
  44. package/dist/framework-adapter.d.ts +60 -0
  45. package/dist/framework-adapter.js +138 -0
  46. package/dist/host.d.ts +41 -0
  47. package/dist/host.js +21 -0
  48. package/dist/index.d.ts +25 -0
  49. package/dist/index.js +25 -0
  50. package/dist/init-failure.d.ts +19 -0
  51. package/dist/init-failure.js +18 -0
  52. package/dist/init-opt-out.d.ts +17 -0
  53. package/dist/init-opt-out.js +30 -0
  54. package/dist/install-hint.d.ts +9 -0
  55. package/dist/install-hint.js +41 -0
  56. package/dist/install-retries.d.ts +22 -0
  57. package/dist/install-retries.js +65 -0
  58. package/dist/install-source-config.d.ts +18 -0
  59. package/dist/install-source-config.js +34 -0
  60. package/dist/lockfiles.d.ts +14 -0
  61. package/dist/lockfiles.js +49 -0
  62. package/dist/mcp-clients.d.ts +94 -0
  63. package/dist/mcp-clients.js +327 -0
  64. package/dist/mcp-registered.d.ts +12 -0
  65. package/dist/mcp-registered.js +14 -0
  66. package/dist/mcp.d.ts +89 -0
  67. package/dist/mcp.js +129 -0
  68. package/dist/next-patch.d.ts +49 -0
  69. package/dist/next-patch.js +237 -0
  70. package/dist/node-io.d.ts +22 -0
  71. package/dist/node-io.js +150 -0
  72. package/dist/non-js-project.d.ts +44 -0
  73. package/dist/non-js-project.js +137 -0
  74. package/dist/nuxt-patch.d.ts +18 -0
  75. package/dist/nuxt-patch.js +33 -0
  76. package/dist/patch-kind.d.ts +20 -0
  77. package/dist/patch-kind.js +10 -0
  78. package/dist/plan-electron-vite.d.ts +16 -0
  79. package/dist/plan-electron-vite.js +72 -0
  80. package/dist/plan-framework.d.ts +122 -0
  81. package/dist/plan-framework.js +773 -0
  82. package/dist/plan.d.ts +312 -0
  83. package/dist/plan.js +650 -0
  84. package/dist/platform.d.ts +16 -0
  85. package/dist/platform.js +16 -0
  86. package/dist/preflight.d.ts +30 -0
  87. package/dist/preflight.js +52 -0
  88. package/dist/project-id.d.ts +12 -0
  89. package/dist/project-id.js +25 -0
  90. package/dist/refresh-rules.d.ts +40 -0
  91. package/dist/refresh-rules.js +51 -0
  92. package/dist/remember-project.d.ts +24 -0
  93. package/dist/remember-project.js +40 -0
  94. package/dist/run.d.ts +122 -0
  95. package/dist/run.js +762 -0
  96. package/dist/slash-command.d.ts +27 -0
  97. package/dist/slash-command.js +132 -0
  98. package/dist/snippets.d.ts +267 -0
  99. package/dist/snippets.js +898 -0
  100. package/dist/tanstack-start.d.ts +18 -0
  101. package/dist/tanstack-start.js +57 -0
  102. package/dist/version.d.ts +18 -0
  103. package/dist/version.js +23 -0
  104. package/dist/vite-config.d.ts +18 -0
  105. package/dist/vite-config.js +111 -0
  106. package/dist/vite-owning-config.d.ts +25 -0
  107. package/dist/vite-owning-config.js +219 -0
  108. package/dist/windows-quote.d.ts +38 -0
  109. package/dist/windows-quote.js +70 -0
  110. package/dist/workspace-apps.d.ts +40 -0
  111. package/dist/workspace-apps.js +188 -0
  112. package/dist/workspace-redirect.d.ts +22 -0
  113. package/dist/workspace-redirect.js +97 -0
  114. package/package.json +53 -0
@@ -0,0 +1,898 @@
1
+ /**
2
+ * Generated file contents and copy-paste snippets for `reticle init`. Kept as named constants/builders
3
+ * so the runner never inlines free strings.
4
+ */
5
+ import { RETICLE_DEFAULT_PORT, ReticleDir, bridgeWsUrl, RETICLE_CLIENT_HOST, RETICLE_WS_PATH, } from '@reticlehq/core';
6
+ import { UiLibrary } from './detect.js';
7
+ import { RETICLE_VERSION } from './version.js';
8
+ /**
9
+ * The SDK as one import a plain page can actually resolve.
10
+ *
11
+ * `init` used to tell static-HTML users that a page with no build step could not load the SDK, and
12
+ * to go and stand up a bundler. That is true of a BARE specifier and false of a URL, and the
13
+ * difference was the whole road for every server-rendered app we hear from: FastAPI, Flask, Django,
14
+ * Streamlit, Rails. Proven end to end before this shipped, on a page served by `python3 -m
15
+ * http.server`: a session connected, a snapshot returned, and two act_and_wait calls came back
16
+ * `verified: "yes"`.
17
+ *
18
+ * jsDelivr rather than esm.sh, measured: a third of the requests and a third of the bytes for the
19
+ * same result. `/+esm` is what makes it work, and the bare package URL does NOT: every file in
20
+ * `dist` still carries bare workspace specifiers such as `@reticlehq/core`, so an unbundled entry
21
+ * point dies on the first import. That is also why adding `unpkg`/`jsdelivr` fields to package.json
22
+ * would not help.
23
+ *
24
+ * PINNED to this server's version on purpose. A floating import upgrades the page SDK underneath a
25
+ * daemon that did not move, which is `version_skew` arriving by a route nothing checks.
26
+ */
27
+ const CDN_SDK_URL = `https://cdn.jsdelivr.net/npm/@reticlehq/browser@${RETICLE_VERSION}/+esm`;
28
+ /**
29
+ * The connect argument literal: a non-default port adds a `url`, and a projectId is always passed
30
+ * (so the app is identifiable across port changes). Empty string only when neither applies.
31
+ */
32
+ export function connectArg(port, projectId) {
33
+ const parts = [];
34
+ if (port !== undefined && port !== RETICLE_DEFAULT_PORT) {
35
+ parts.push(`url: '${bridgeWsUrl(port)}'`);
36
+ }
37
+ if (projectId !== undefined && projectId.length > 0)
38
+ parts.push(`projectId: '${projectId}'`);
39
+ return parts.length > 0 ? `{ ${parts.join(', ')} }` : '';
40
+ }
41
+ /**
42
+ * The same literal, with the pairing token folded in.
43
+ *
44
+ * The token belongs INSIDE the call the user pastes. Every other stack has a build step to inline
45
+ * it (the Vite plugin's `define`, Next's NEXT_PUBLIC_*, Astro's config, CRA's .env); the hand-wired
46
+ * paths have none, so `init` inlines the literal it already read. Without it the bridge closes the
47
+ * socket with AUTH_FAILED and no session ever appears: see Bridge's hello handler.
48
+ *
49
+ * An empty token is omitted rather than emitted as `token: ''`. A daemon that could not write to
50
+ * $HOME runs without auth and trusts loopback, and an empty string would fail the comparison against
51
+ * one that does hold a token.
52
+ */
53
+ export function connectArgWithToken(port, projectId, pairingToken) {
54
+ const base = connectArg(port, projectId);
55
+ if (pairingToken === undefined || 0 === pairingToken.length)
56
+ return base;
57
+ const inner = base.length > 0 ? base.slice(1, -1).trim() : '';
58
+ return `{ ${[inner, `token: '${pairingToken}'`].filter((p) => p.length > 0).join(', ')} }`;
59
+ }
60
+ /**
61
+ * Which SDK package the GENERATED code should import, and whether `install()` applies.
62
+ *
63
+ * This has to agree with `frameworkPackages`, and it did not. That function was changed so a Vue or
64
+ * Svelte app installs `@reticlehq/browser` instead of the React adapter — correctly — while every
65
+ * generated connect snippet still said `import('@reticlehq/react')`. A SvelteKit app would have
66
+ * installed the sensor and then run a hook importing a package that is not there.
67
+ *
68
+ * `install()` is the React adapter's, not the sensor's: `@reticlehq/browser` exports `reticle` and no
69
+ * `install`, so swapping the specifier alone would trade a missing module for a missing export.
70
+ */
71
+ export function sdkImport(uiLibrary) {
72
+ const react = uiLibrary !== UiLibrary.VUE && uiLibrary !== UiLibrary.SVELTE;
73
+ return react
74
+ ? { specifier: '@reticlehq/react', usesInstall: true }
75
+ : { specifier: '@reticlehq/browser', usesInstall: false };
76
+ }
77
+ /**
78
+ * The framework plugin to show ALONGSIDE reticle() in the example, so the ordering is clear.
79
+ *
80
+ * It used to be `react()` unconditionally, which is what a Vue app was shown — a plugin it does not
81
+ * have, four lines after `init` had correctly detected Vue and said so. The example exists to show
82
+ * that `reticle()` goes last, not to tell anyone which UI framework they are using.
83
+ */
84
+ function frameworkPluginExample(uiLibrary) {
85
+ switch (uiLibrary) {
86
+ case UiLibrary.VUE:
87
+ return 'vue()';
88
+ case UiLibrary.SVELTE:
89
+ return 'svelte()';
90
+ case UiLibrary.REACT:
91
+ case UiLibrary.PREACT:
92
+ return 'react()';
93
+ default:
94
+ // Nothing detected: name no plugin rather than invent one. The reader keeps whatever they have.
95
+ return '/* your existing plugins */';
96
+ }
97
+ }
98
+ /** The Vite-config snippet printed when we can't safely auto-patch the config. */
99
+ export function viteManual(port, uiLibrary = UiLibrary.UNKNOWN, inject = true, sourceMapping = true) {
100
+ const options = [
101
+ ...(port === undefined ? [] : [`port: ${String(port)}`]),
102
+ ...(false === inject ? ['inject: false'] : []),
103
+ ...(sourceMapping ? [] : ['sourceMapping: false']),
104
+ ];
105
+ const call = 0 === options.length ? 'reticle()' : `reticle({ ${options.join(', ')} })`;
106
+ const note = sourceMapping
107
+ ? ''
108
+ : `\n\n\`sourceMapping: false\` because this app renders through a non-DOM React renderer, where a
109
+ lowercase JSX tag is not an element. Stamping one crashes the app at commit time. You lose source
110
+ pointers, not the app.`;
111
+ return `Add the Reticle plugin to your Vite config:
112
+
113
+ import { reticle } from '@reticlehq/vite-plugin';
114
+
115
+ export default defineConfig({
116
+ plugins: [${frameworkPluginExample(uiLibrary)}, ${call}],
117
+ });
118
+
119
+ Keep \`reticle()\` LAST so it sees the output of your other plugins. It only applies during \`vite\`
120
+ (dev) — it is dropped from \`vite build\`.${note}`;
121
+ }
122
+ /**
123
+ * The electron-vite recipe: the plugin belongs in `renderer`, never in `main` or `preload`.
124
+ *
125
+ * `desktop: true` is load-bearing. Without it the plugin is serve-only, so a packaged renderer
126
+ * (a production build with no dev server) ships with no connect() at all.
127
+ */
128
+ export function electronViteManual(port, uiLibrary = UiLibrary.UNKNOWN) {
129
+ const extras = port === undefined
130
+ ? 'desktop: true, captureNetworkBodies: true'
131
+ : `desktop: true, port: ${String(port)}, captureNetworkBodies: true`;
132
+ return `Add the Reticle plugin to the \`renderer\` block of electron.vite.config, not \`main\`:
133
+
134
+ import { reticle } from '@reticlehq/vite-plugin';
135
+
136
+ export default defineConfig({
137
+ main: { /* unchanged */ },
138
+ preload: { /* unchanged */ },
139
+ renderer: {
140
+ plugins: [${frameworkPluginExample(uiLibrary)}, reticle({ ${extras} })],
141
+ },
142
+ });
143
+
144
+ \`desktop: true\` is required: a packaged renderer is a production build with no dev server, so the
145
+ default serve-only plugin is dropped from \`vite build\` and the shipped app never connects.
146
+
147
+ Also add \`import '@reticlehq/electron/preload'\` as the first line of your preload, and
148
+ \`installReticleCapture(win)\` in main after you construct the BrowserWindow.`;
149
+ }
150
+ /** Next.js config wrap — always printed (we never auto-rewrite next.config). */
151
+ export function nextConfigManual(configFile, sourceMapping = true) {
152
+ const options = sourceMapping ? '' : ', { sourceMapping: false }';
153
+ const note = sourceMapping
154
+ ? ''
155
+ : `\n\n\`sourceMapping: false\` because this app renders through a non-DOM React renderer, where a
156
+ lowercase JSX tag is not an element. Stamping one crashes the app at commit time. You lose source
157
+ pointers, not the app.`;
158
+ return `Wrap your ${configFile} export with withReticle (keeps SWC, dev-only):
159
+
160
+ import { withReticle } from '@reticlehq/next';
161
+
162
+ export default withReticle(nextConfig${options});${note}`;
163
+ }
164
+ /**
165
+ * The dev-only client component that connects Reticle after hydration.
166
+ *
167
+ * The token is not optional. The bridge requires a pairing token even on localhost, and unlike Vite
168
+ * (where the plugin injects it) a Next app has to carry it through `withReticle`, which publishes it
169
+ * as `NEXT_PUBLIC_RETICLE_TOKEN`. This file used to connect with only a projectId, so every Next
170
+ * setup ended at `bridge refused the connection: authentication failed` and no session ever appeared.
171
+ */
172
+ export function nextReticleDevFile(port, projectId, testids = [], stores = [], found = []) {
173
+ const base = connectArg(port, projectId);
174
+ const fields = '' === base ? '' : `${base.slice(1, -1).trim()}, `;
175
+ const ids = testids.map((t) => `'${t}'`).join(', ');
176
+ // Same rule as the Vite module: a store we found is registered outright, and the commented hint
177
+ // survives only for the libraries we can name but not wire.
178
+ const storeImports = found.map((s) => `import { ${s.ident} } from '${s.importPath}';`).join('\n');
179
+ // Eight spaces: nested inside useEffect → .then callback (see multiline shape below for #684).
180
+ const storeBlock = found.length > 0
181
+ ? found.map((s) => ` registerStore('${s.key}', ${s.ident});`).join('\n')
182
+ : 0 === stores.length
183
+ ? ' // No state library detected. If you add one, register it here — see node_modules/@reticlehq/server/docs/usage.md.'
184
+ : stores.map((h) => ` // import your store, then: ${h}`).join('\n');
185
+ const registerNames = found.length > 0 ? ', registerStore' : '';
186
+ // Multiline connect + single blank after imports (#684): the one-line connect and the double blank
187
+ // both fail Prettier on a clean Next install.
188
+ return `'use client';
189
+ import { useEffect } from 'react';
190
+ ${storeImports.length > 0 ? `${storeImports}\n` : ''}
191
+ /** Dev-only: connect Reticle + install the React adapter, after hydration. */
192
+ export function ReticleDev() {
193
+ useEffect(() => {
194
+ if (process.env.NODE_ENV !== 'development') return;
195
+ void import('@reticlehq/react').then(
196
+ ({ reticle, install, registerCapabilities${registerNames} }) => {
197
+ install();
198
+ // Both provided by withReticle() in next.config. The bridge rejects a connect with no token;
199
+ // the root makes source paths repo-relative instead of absolute.
200
+ const token = process.env.NEXT_PUBLIC_RETICLE_TOKEN;
201
+ const root = process.env.NEXT_PUBLIC_RETICLE_ROOT;
202
+ // withReticle() finds the daemon serving this project on every dev-server start. It wins over
203
+ // any port written into this file at install time, so moving the daemon needs no edit here.
204
+ const url = process.env.NEXT_PUBLIC_RETICLE_URL;
205
+ reticle.connect({
206
+ ${fields}...(url ? { url } : {}),
207
+ ...(token ? { token } : {}),
208
+ ...(root ? { root } : {}),
209
+ });
210
+
211
+ // ── Start with ONE flow. ────────────────────────────────────────────────────────────────
212
+ // Registering a store is the highest-value line here: it lets the agent check what the app
213
+ // BELIEVES, not just what it rendered. Pass the STORE, not \`() => store.getState()\` — the
214
+ // store form wires \`subscribe\` too, so every mutation emits a diff; the getter form is
215
+ // read-only.
216
+ ${storeBlock}
217
+ registerCapabilities({
218
+ testids: [${ids}],${0 === testids.length ? ' // none found; add data-testid to your key elements' : ''}
219
+ signals: [], // names you pass to reticle.signal()
220
+ stores: [${found.map((s) => `'${s.key}'`).join(', ')}], // the keys you registered above
221
+ });
222
+ },
223
+ );
224
+ }, []);
225
+ return null;
226
+ }
227
+ `;
228
+ }
229
+ /**
230
+ * Astro connect instructions.
231
+ *
232
+ * Astro is Vite-based but SSRs its own HTML, so the plugin's index.html injection never fires, and
233
+ * `vite` is not a direct dependency — so this used to fall through to the generic HTML advice, which
234
+ * tells you to add a connect to an "entry module" Astro does not have, or to bundle the SDK with
235
+ * esbuild. Both are wrong for Astro, and following either gets you nothing.
236
+ *
237
+ * Astro bundles a page `<script>`, so the bare import resolves there. The token has to be inlined by
238
+ * the config because there is no plugin in the page's path to inject it, and `build.target` has to be
239
+ * raised or Astro down-levels the modern SDK bundle and dies on a destructuring transform.
240
+ */
241
+ /**
242
+ * Name the file the connect <script> should go in — the layout when one exists, otherwise the page,
243
+ * because a project with no layout has nowhere else to put it and should not be told otherwise.
244
+ */
245
+ function layoutHost(layoutPath) {
246
+ return layoutPath === undefined
247
+ ? '2. This project has no layout, so put it in the page you want instrumented (e.g.\n src/pages/index.astro) — every page you want a session from needs it — inside <body>:'
248
+ : `2. In ${layoutPath} (or any other page you want instrumented), inside <body>:`;
249
+ }
250
+ export function astroManual(port, projectId,
251
+ /**
252
+ * A layout file that actually exists, when one does.
253
+ *
254
+ * Reported from the field: on `examples/framework-react` — which has no layout at all, only
255
+ * `src/pages/index.astro` — init printed thirty lines telling the user to paste into "your
256
+ * layout". Instructions that name a file the project does not have read as a mistake by the
257
+ * reader, and cost them the time it takes to go and confirm it is missing.
258
+ */
259
+ layoutPath) {
260
+ const extra = port !== undefined && port !== RETICLE_DEFAULT_PORT
261
+ ? `\n url: '${bridgeWsUrl(port)}',`
262
+ : '';
263
+ const id = projectId !== undefined && projectId.length > 0 ? `\n projectId: '${projectId}',` : '';
264
+ // Astro owns its Vite instance, so it has a build-time channel of its own: the same `define` that
265
+ // already inlines the token can inline the daemon URL, resolved every time the config is read —
266
+ // that is, on every `astro dev`. The port above is written once at install time and goes stale the
267
+ // moment the daemon moves; this does not.
268
+ //
269
+ // Needs the projectId to know WHICH daemon is ours. Without one there is nothing to match on, and
270
+ // adopting a daemon serving another project would report its state as this app's, so the whole
271
+ // block is omitted rather than made to guess.
272
+ const canDiscover = projectId !== undefined && projectId.length > 0;
273
+ // Interpolated from core rather than typed into the template: this is the same wire string
274
+ // `bridgeWsUrl` builds, and a generator that spells it out by hand is exactly how the four call
275
+ // sites that constant exists to unify drifted apart in the first place.
276
+ const RETICLE_CLIENT_HOST_LITERAL = `ws://${RETICLE_CLIENT_HOST}:`;
277
+ const urlHelper = canDiscover
278
+ ? `
279
+ // The live daemon serving THIS project, re-read on every \`astro dev\`. Same rule as the Vite and
280
+ // Next plugins: skip dead daemons, match on projectId, lowest port wins, '' when none matches.
281
+ function reticleUrl() {
282
+ const dir = process.env['RETICLE_PAIRING_TOKEN_DIR'] || join(homedir(), '.reticle');
283
+ let best;
284
+ try {
285
+ for (const file of readdirSync(dir)) {
286
+ if (!file.startsWith('daemon-') || !file.endsWith('.json')) continue;
287
+ let entry;
288
+ try { entry = JSON.parse(readFileSync(join(dir, file), 'utf8')); } catch { continue; }
289
+ if (entry.projectId !== '${projectId}') continue;
290
+ try { process.kill(entry.pid, 0); } catch { continue; }
291
+ if (best === undefined || entry.port < best) best = entry.port;
292
+ }
293
+ } catch { return ''; }
294
+ return best === undefined ? '' : '${RETICLE_CLIENT_HOST_LITERAL}' + best + '${RETICLE_WS_PATH}';
295
+ }
296
+ `
297
+ : '';
298
+ const urlDefine = canDiscover ? `\n __RETICLE_URL__: JSON.stringify(reticleUrl()),` : '';
299
+ const urlRead = canDiscover
300
+ ? `\n const url = typeof __RETICLE_URL__ !== 'undefined' ? __RETICLE_URL__ : '';`
301
+ : '';
302
+ // After the baked port, so the discovered one wins: later key in an object literal.
303
+ const urlSpread = canDiscover ? `\n ...(url.length > 0 ? { url } : {}),` : '';
304
+ return `Astro renders its own HTML, so the connect goes in a page <script> and the pairing token is inlined by the config.
305
+
306
+ 1. In astro.config.mjs — inline the daemon's token and raise the build target:
307
+
308
+ import { readFileSync, readdirSync } from 'node:fs';
309
+ import { homedir } from 'node:os';
310
+ import { join } from 'node:path';
311
+
312
+ function reticleToken() {
313
+ const dir = process.env['RETICLE_PAIRING_TOKEN_DIR'] || join(homedir(), '.reticle');
314
+ try { return readFileSync(join(dir, 'pairing-token'), 'utf8').trim(); } catch { return ''; }
315
+ }
316
+ ${urlHelper}
317
+ export default defineConfig({
318
+ vite: {
319
+ // Astro's default target down-levels the modern SDK bundle and fails on a destructuring transform.
320
+ build: { target: 'es2022' },
321
+ optimizeDeps: { esbuildOptions: { target: 'es2022' } },
322
+ define: {
323
+ __RETICLE_TOKEN__: JSON.stringify(reticleToken()),${urlDefine}
324
+ // Without this, source pointers come back as absolute paths from YOUR machine — useless in a
325
+ // report. Every other framework gets it from its build plugin; Astro owns its Vite instance.
326
+ __RETICLE_ROOT__: JSON.stringify(process.cwd()),
327
+ },
328
+ },
329
+ });
330
+
331
+ ${layoutHost(layoutPath)}
332
+
333
+ <script>
334
+ if (import.meta.env.DEV) {
335
+ const token = typeof __RETICLE_TOKEN__ !== 'undefined' ? __RETICLE_TOKEN__ : '';
336
+ const root = typeof __RETICLE_ROOT__ !== 'undefined' ? __RETICLE_ROOT__ : '';${urlRead}
337
+ const { reticle, install } = await import('@reticlehq/react');
338
+ install();
339
+ reticle.connect({${id}${extra}${urlSpread}
340
+ ...(token.length > 0 ? { token } : {}),
341
+ ...(root.length > 0 ? { root } : {}),
342
+ });
343
+ }
344
+ </script>
345
+
346
+ 3. In src/env.d.ts — declare the Vite define names so \`astro check\` can see them (create-astro's
347
+ default build runs check first, and without this it fails with Cannot find name '__RETICLE_TOKEN__'):
348
+
349
+ declare const __RETICLE_TOKEN__: string | undefined;
350
+ declare const __RETICLE_ROOT__: string | undefined;
351
+
352
+ Start the daemon BEFORE \`astro dev\`, so the token file exists when the config is read. Until it does the token is empty and the page reloads once the daemon is up.`;
353
+ }
354
+ /**
355
+ * The `registerCapabilities` call every generated connect carries.
356
+ *
357
+ * Only the Vite and Next generators emitted one, so an Astro, SvelteKit, Nuxt or CRA app connected
358
+ * and then reported `hasCapabilities: false` — `init` printing, about its own work, "is instrumented
359
+ * and connected — but NOT verified". The testids are the ones already scanned out of the app's
360
+ * source; an empty list still declares the block, because the comment on it is how somebody learns
361
+ * the file is theirs to extend.
362
+ *
363
+ * `indent` because the call lands at four different depths: top level in a Vite dev module, inside a
364
+ * `.then` in the Nuxt plugin, inside a `<script>` in an Astro layout.
365
+ */
366
+ export function registerCapabilitiesCall(testids, indent) {
367
+ const ids = testids.map((t) => `'${t}'`).join(', ');
368
+ const none = 0 === testids.length ? ' // none found; add data-testid to your key elements' : '';
369
+ return `${indent}registerCapabilities({
370
+ ${indent} testids: [${ids}],${none}
371
+ ${indent} signals: [], // names you pass to reticle.signal()
372
+ ${indent} stores: [], // register a store above, then name its key here
373
+ ${indent}});`;
374
+ }
375
+ /**
376
+ * The app-side dev module the Vite plugin imports by convention.
377
+ *
378
+ * `registerCapabilities` tells the agent what it can drive without guessing; `registerStore` is the
379
+ * one that matters most and the one we cannot write for you — detecting that an app depends on
380
+ * zustand is easy, knowing which module exports the store instance is not, and a wrong import here
381
+ * breaks the module everything else hangs off. So the store lines are generated COMMENTED, naming
382
+ * the libraries actually found in package.json, with the exact call to uncomment.
383
+ */
384
+ export function viteDevModuleFile(testids, stores, found = [], uiLibrary = UiLibrary.REACT) {
385
+ // The specifier has to be the package `frameworkPackages` installed. This file is the Vite path —
386
+ // the commonest install there is — and it was hardcoded to `@reticlehq/react` while a Vue or
387
+ // Svelte app was being given `@reticlehq/browser`, so the generated file imported something that
388
+ // was not there. Caught by running `init` against a pristine Vue app, not by any gate.
389
+ //
390
+ // The sensor exports `registerCapabilities` and `registerStore` (and the store adapters) just as
391
+ // the React kit does; the only thing it lacks is `install()`, which this file never called.
392
+ const sdk = sdkImport(uiLibrary);
393
+ const ids = testids.map((t) => `'${t}'`).join(', ');
394
+ // A store we FOUND is imported and registered outright — the whole point of the file. The hints
395
+ // stay only for the libraries we can name but not wire (they need an argument we cannot infer).
396
+ const storeImports = found.map((s) => `import { ${s.ident} } from '${s.importPath}';`).join('\n');
397
+ const storeBlock = found.length > 0
398
+ ? found.map((s) => ` registerStore('${s.key}', ${s.ident});`).join('\n')
399
+ : 0 === stores.length
400
+ ? ' // No state library detected. If you add one, register it here — see node_modules/@reticlehq/server/docs/usage.md.'
401
+ : stores.map((h) => ` // import your store, then: ${h}`).join('\n');
402
+ const registerImport = found.length > 0 ? 'registerCapabilities, registerStore' : 'registerCapabilities';
403
+ return `// Dev-only. Imported automatically by @reticlehq/vite-plugin, so you do not need to import it.
404
+ // Self-guards on import.meta.env.DEV, so it is a no-op in a production build.
405
+ import { ${registerImport} } from '${sdk.specifier}';
406
+ ${storeImports.length > 0 ? `${storeImports}\n` : ''}
407
+ if (import.meta.env.DEV) {
408
+ // ── Start with ONE flow. ─────────────────────────────────────────────────────────────────────
409
+ // You do not need to describe the whole app to get value, and trying to is the slow path. Register
410
+ // the store your most important flow reads, and list the testids that flow touches. Add more later,
411
+ // when a flow you actually replay needs them.
412
+ //
413
+ // Registering a store is the highest-value line in this file: it lets the agent check what the app
414
+ // BELIEVES, not just what it rendered — the class of bug a screenshot cannot see. Pass the STORE,
415
+ // not \`() => store.getState()\`: the store form wires \`subscribe\` too, so every mutation emits a
416
+ // state diff; the getter form is read-only and silently produces empty diffs.
417
+ ${storeBlock}
418
+
419
+ registerCapabilities({
420
+ testids: [${ids}],${0 === testids.length ? ' // none found; add data-testid to your key elements' : ''}
421
+ signals: [], // names you pass to reticle.signal()
422
+ stores: [${found.map((s) => `'${s.key}'`).join(', ')}], // the keys you registered above
423
+ });
424
+ }
425
+ `;
426
+ }
427
+ /** Where that module goes. Matches @reticlehq/vite-plugin's convention list. */
428
+ export const VITE_DEV_MODULE_PATH = 'src/reticle-dev.ts';
429
+ /**
430
+ * electron-vite's renderer Vite root is `src/renderer`, so the plugin looks for
431
+ * `src/renderer/src/reticle-dev.ts` — not the package-root path a plain Vite app uses.
432
+ */
433
+ export const ELECTRON_VITE_DEV_MODULE_PATH = 'src/renderer/src/reticle-dev.ts';
434
+ /** Default root-layout path, used when no layout was found on disk (reporting only). */
435
+ export const NEXT_LAYOUT_PATH = 'app/layout.tsx';
436
+ /** Mount instruction for the root layout. */
437
+ export const NEXT_LAYOUT_MANUAL = `Mount <ReticleDev /> in your root layout (app/layout.tsx), dev-only:
438
+
439
+ import { ReticleDev } from './reticle-dev';
440
+ // inside <body>:
441
+ {process.env.NODE_ENV === 'development' ? <ReticleDev /> : null}`;
442
+ /**
443
+ * Manual connect guidance for projects without a Vite/Next plugin. Most such projects still use a
444
+ * BUNDLER (CRA, webpack, Parcel, Vue/Svelte CLIs) — for those, the connect goes in the entry MODULE,
445
+ * where a bare `@reticlehq/react` import resolves. A bare import in a plain index.html does NOT resolve in
446
+ * the browser, so we never tell a bundled app to do that (the old advice silently failed for CRA).
447
+ */
448
+ /**
449
+ * The whole install, for a page with no build step, as one block a person can paste.
450
+ *
451
+ * Extracted so the `no package.json` exit can print the SAME snippet `htmlManual` offers. That exit
452
+ * is the one every server-rendered app reaches (FastAPI, Flask, Django, Rails, Streamlit), it is
453
+ * where `init` stops, and until now it stopped with an explanation instead of an answer. A message
454
+ * that says "add the snippet below" and then prints no snippet is the same defect wearing the
455
+ * opposite sign, so the two share a builder rather than a copy.
456
+ */
457
+ export function staticPageSnippet(connectArgLiteral) {
458
+ return ` <script type="module">
459
+ import { reticle } from '${CDN_SDK_URL}';
460
+ reticle.connect(${connectArgLiteral});
461
+ </script>`;
462
+ }
463
+ /**
464
+ * Streamlit has no served HTML template, and `st.markdown` inserts script markup without executing
465
+ * it. Streamlit 1.63 added an explicit JavaScript boundary to `st.html`; use a classic script there
466
+ * so it can dynamically import the ESM SDK in the app document. A head marker makes reruns
467
+ * idempotent and is removed after an import failure so the next rerun can retry.
468
+ */
469
+ export function streamlitPageSnippet(connectArgLiteral) {
470
+ return `import streamlit as st
471
+
472
+ st.html(
473
+ """
474
+ <script>
475
+ (() => {
476
+ if (document.getElementById('reticle-streamlit-connect')) return;
477
+ const marker = document.createElement('meta');
478
+ marker.id = 'reticle-streamlit-connect';
479
+ document.head.appendChild(marker);
480
+ void import('${CDN_SDK_URL}')
481
+ .then(({ reticle }) => reticle.connect(${connectArgLiteral}))
482
+ .catch((error) => {
483
+ marker.remove();
484
+ throw error;
485
+ });
486
+ })();
487
+ </script>
488
+ """,
489
+ unsafe_allow_javascript=True,
490
+ )`;
491
+ }
492
+ /**
493
+ * DEBUG-only middleware that injects Reticle into every Django-rendered page.
494
+ *
495
+ * Django serves templates, so the generic script tag has no single home: an app has many templates
496
+ * and a base template it may not have. Middleware has exactly one, runs for every page, and is the
497
+ * idiomatic Django answer — a field reporter arrived at this same shape by hand after `init` printed
498
+ * a script tag and exited 1, and had to write the middleware and `.reticle.json` themselves.
499
+ *
500
+ * Guarded on `settings.DEBUG` so it can never reach production, and on the response being HTML so it
501
+ * does not corrupt a JSON API response or a file download. Injected before `</body>` rather than in
502
+ * `<head>`, so the SDK sees a parsed document.
503
+ */
504
+ export function djangoMiddlewareSnippet(connectArgLiteral) {
505
+ return `# reticle_dev.py — add to MIDDLEWARE while DEBUG is on:
506
+ # MIDDLEWARE = [..., "reticle_dev.ReticleDevMiddleware"]
507
+ from django.conf import settings
508
+
509
+ _SNIPPET = """<script type="module">
510
+ import { reticle } from '${CDN_SDK_URL}';
511
+ reticle.connect(${connectArgLiteral});
512
+ </script>"""
513
+
514
+
515
+ class ReticleDevMiddleware:
516
+ """Inject the Reticle SDK into HTML responses during development only."""
517
+
518
+ def __init__(self, get_response):
519
+ self.get_response = get_response
520
+
521
+ def __call__(self, request):
522
+ response = self.get_response(request)
523
+ if not settings.DEBUG:
524
+ return response
525
+ if "text/html" not in response.get("Content-Type", ""):
526
+ return response
527
+ # Streaming responses have no .content to rewrite; leave them alone.
528
+ if getattr(response, "streaming", False):
529
+ return response
530
+ body = response.content.decode(response.charset)
531
+ if "</body>" not in body:
532
+ return response
533
+ response.content = body.replace("</body>", _SNIPPET + "</body>", 1).encode(
534
+ response.charset
535
+ )
536
+ if response.has_header("Content-Length"):
537
+ response["Content-Length"] = str(len(response.content))
538
+ return response
539
+ `;
540
+ }
541
+ export function htmlManual(port, projectId, pairingToken) {
542
+ const withToken = connectArgWithToken(port, projectId, pairingToken);
543
+ const tokenNote = pairingToken === undefined || 0 === pairingToken.length
544
+ ? ''
545
+ : `\n\n The \`token\` is this machine's pairing token, read from ~/.reticle/pairing-token. Keep it: the
546
+ bridge REJECTS a connect without it ("authentication failed") and no session appears. It is
547
+ per-machine and local-only, so do not commit it — a teammate's daemon mints their own.`;
548
+ return `No Vite/Next plugin detected — wire the dev-only connect by hand. Pick the form for your setup:
549
+
550
+ • Bundled app (Create React App, webpack, Parcel, Vue/Svelte CLI, etc.) — add to your ENTRY module
551
+ (e.g. src/index.js or src/main.js), where '@reticlehq/react' resolves through your bundler:
552
+
553
+ if (process.env.NODE_ENV !== 'production') {
554
+ void import('@reticlehq/react').then(({ reticle, install }) => {
555
+ install();
556
+ reticle.connect(${withToken});
557
+ });
558
+ }${tokenNote}
559
+
560
+ • Plain HTML with NO build step (FastAPI, Flask, Django, Rails, Streamlit, a hand-written page) —
561
+ paste this into the page, in a template you only serve in development. There is nothing to
562
+ install: no npm, no bundler, no package.json.
563
+
564
+ ${staticPageSnippet(withToken)}
565
+
566
+ Serving the app on something other than localhost (a hosts-file alias, a LAN IP, a container, a
567
+ tunnel)? You need TWO things, not one: \`allowNonLocalhost: true\` AND a pairing token, passed as
568
+ \`token\` on the same connect. The flag alone is NOT sufficient — off localhost the SDK refuses
569
+ without a token as well, and that refusal is page-side, so the daemon sees only silence and every
570
+ \`reticle doctor\` check still passes. The token is the one in \`~/.reticle/pairing-token\` (the
571
+ build plugins read the same file). Without both, the SDK says so in the browser console only, so
572
+ from here it looks exactly like nothing happened.`;
573
+ }
574
+ export const NEXT_RETICLE_DEV_PATH = 'app/reticle-dev.tsx';
575
+ export const SVELTEKIT_HOOKS_PATH = 'src/hooks.client.ts';
576
+ /**
577
+ * Said to the user's face rather than discovered later. React, Next, Remix and Astro each have an
578
+ * app and a CI gate; SvelteKit has neither, so "it generated some wiring" is not evidence it works.
579
+ */
580
+ /**
581
+ * Said out loud when the app does not render through React. The SDK is framework-agnostic — DOM,
582
+ * network, console and routing all still work — but `@reticlehq/react` is a React adapter, so
583
+ * component names and source mapping do not, and no CI gate covers this stack. Reporting all-green
584
+ * here is the one thing this project exists not to do.
585
+ */
586
+ export function unverifiedUiLibraryNote(library) {
587
+ // Preact is not in the same position as Vue or Svelte and must not be told it is. The React
588
+ // adapter reaches Preact through `preact/compat`, which is what `docs/frameworks.mdx` has always
589
+ // said, so telling a Preact reader they get no component identity contradicts our own docs and
590
+ // talks them out of a package that is the right one for them. It is still ungated, which is the
591
+ // honest caveat, and #129 is the issue for closing that.
592
+ const identity = 'preact' === library
593
+ ? 'React component identity — component names and stacks — comes from `@reticlehq/react`, which reaches Preact through `preact/compat`. That path is not covered by a CI gate here, so treat it as expected-to-work rather than proven.'
594
+ : 'What `@reticlehq/react` adds and you will NOT get is React component identity: component names and component stacks.';
595
+ return `Detected a ${library} app. Reticle's DOM, network, console and state tools work here. ${'vue' === library ? 'Source `file:line` does NOT come through: the build plugin stamps JSX and, separately, Svelte components, and a Vue single-file component is neither — measured, a Svelte counter reports `src/lib/Counter.svelte:5` and the same drive on Vue reports no source at all.' : 'Source `file:line` does too — the build plugin stamps it for this library (measured on preact and svelte).'} ${identity} ${'vue' === library ? 'The install gate scaffolds a Vue app from scratch on every change, so this SETUP is proven; no gate drives a Vue app to a verdict, so the drive is not.' : `No CI gate covers ${library}.`} Driven on every change: Vite + React, Next.js, Remix, Astro. If something doesn't work, please open an issue.`;
596
+ }
597
+ export const UNVERIFIED_FRAMEWORK_NOTE = 'Reticle has no SvelteKit app and no CI gate for one, so this wiring is untested — it may work, but nothing proves it and nothing will tell us if it breaks. Supported and gated today: Vite + React, Next.js, Remix and Astro. If the hook does not register a session, please open an issue.';
598
+ export const UNVERIFIED_TANSTACK_START_NOTE = 'Reticle has no TanStack Start app and no CI gate for one, so this wiring is untested — it may work, but nothing proves it and nothing will tell us if it breaks. Supported and gated today: Vite + React, Next.js, Remix and Astro. If the client effect does not register a session, please open an issue.';
599
+ /**
600
+ * Printed when init detects react-three-fiber. Reticle's model is DOM + store + network; a WebGL
601
+ * canvas is none of those. Without this line, init succeeds, sessions connect, and surrounding UI
602
+ * verdicts pass while the canvas — often the product — stays a blank rectangle (#880).
603
+ */
604
+ export const WEBGL_CANVAS_LIMIT_NOTE = 'A WebGL / react-three-fiber subtree is not observable as a scene: reticle_query and ' +
605
+ 'reticle_snapshot see a single <canvas>, and Reticle cannot pick faces or drive the camera ' +
606
+ 'inside it. DOM, registered store state, and network around the canvas still work — verify ' +
607
+ 'consequences there (import counts, mesh stats, solver results), not geometry under the pointer.';
608
+ /**
609
+ * Dev-only client hook that connects Reticle in a SvelteKit app. SvelteKit renders through app.html and
610
+ * never triggers Vite's index.html injection (verified), so the standard plugin can't auto-connect —
611
+ * a client hook is the reliable path. SvelteKit runs src/hooks.client.ts on the client at startup.
612
+ */
613
+ export function svelteKitHooksFile(port, projectId, uiLibrary = UiLibrary.SVELTE, testids = []) {
614
+ // SvelteKit is Svelte, so this defaults to the sensor rather than the React adapter — and the
615
+ // import here MUST match what `frameworkPackages` installed, or the hook loads a package that is
616
+ // not in node_modules. See sdkImport.
617
+ const sdk = sdkImport(uiLibrary);
618
+ const base = connectArg(port, projectId);
619
+ const fields = '' === base ? '' : `${base.slice(1, -1).trim()}, `;
620
+ return `// Dev-only: connect Reticle on the client. SvelteKit renders via app.html, so the Vite-plugin
621
+ // index.html injection doesn't fire — connect from this client hook instead.
622
+ if (import.meta.env.DEV) {
623
+ void import('${sdk.specifier}').then(({ reticle${sdk.usesInstall ? ', install' : ''}, registerCapabilities }) => {
624
+ ${sdk.usesInstall ? 'install();' : '// No React adapter here: the sensor has no install() to call.'}
625
+ // The bridge requires the pairing token even on localhost. Nothing in a browser can read the
626
+ // file it lives in, so @reticlehq/vite-plugin inlines it here at build time. Without it the
627
+ // console reads "bridge refused the connection: authentication failed" and no session appears.
628
+ const token = typeof __RETICLE_TOKEN__ !== 'undefined' ? __RETICLE_TOKEN__ : '';
629
+ const root = typeof __RETICLE_ROOT__ !== 'undefined' ? __RETICLE_ROOT__ : '';
630
+ const sdkVersion = typeof __RETICLE_SDK_VERSION__ !== 'undefined' ? __RETICLE_SDK_VERSION__ : '';
631
+ reticle.connect({
632
+ ${fields}...(token.length > 0 ? { token } : {}),
633
+ ...(root.length > 0 ? { root } : {}),
634
+ ...(sdkVersion.length > 0 ? { sdkVersion } : {}),
635
+ });
636
+ ${registerCapabilitiesCall(testids, ' ')}
637
+ });
638
+ }
639
+
640
+ declare const __RETICLE_TOKEN__: string | undefined;
641
+ declare const __RETICLE_ROOT__: string | undefined;
642
+ declare const __RETICLE_SDK_VERSION__: string | undefined;
643
+ `;
644
+ }
645
+ /** The react-scripts major below which webpack cannot parse what @reticlehq/browser ships. */
646
+ export const WEBPACK4_REACT_SCRIPTS_MAJOR = 5;
647
+ /**
648
+ * What to do when the bundler cannot parse our SDK at all.
649
+ *
650
+ * `@reticlehq/browser` ships untranspiled optional chaining and logical assignment.
651
+ * react-scripts 4 runs webpack 4, whose parser predates both, AND excludes `node_modules` from
652
+ * Babel -- so the build dies inside our `dist/` with a syntax error pointing at a file the user did
653
+ * not write, before any session can exist (#680).
654
+ *
655
+ * Nothing else in `init` can catch this. Every check we run passes: the package installs, the entry
656
+ * import is written, the token is inlined. The app simply does not compile, and the error names
657
+ * `@reticlehq/browser/dist/index.js` rather than anything about Reticle.
658
+ *
659
+ * Two ways out, in the order a reader should consider them, because the second is the one that does
660
+ * not require editing a bundler config to run a dev-only tool.
661
+ */
662
+ export function webpack4TranspileNote(reactScriptsMajor) {
663
+ return `react-scripts ${String(reactScriptsMajor)} runs webpack 4, whose parser predates optional
664
+ chaining and logical assignment. @reticlehq/browser ships both untranspiled, and react-scripts
665
+ excludes node_modules from Babel — so \`npm start\` fails with a syntax error inside
666
+ @reticlehq/browser/dist/index.js and no session is ever possible. Nothing else in this report can
667
+ see that: the install, the import and the token are all correct.
668
+
669
+ Either upgrade to react-scripts 5 (webpack 5 parses both natively, and needs no change on your
670
+ side), or transpile this one package. With react-app-rewired or craco, add it to Babel's include:
671
+
672
+ // config-overrides.js (react-app-rewired)
673
+ const path = require('path');
674
+ module.exports = (config) => {
675
+ const rule = config.module.rules.find((r) => Array.isArray(r.oneOf))
676
+ .oneOf.find((r) => String(r.test).includes('js') && r.include);
677
+ rule.include = [rule.include, path.resolve('node_modules/@reticlehq/browser')];
678
+ return config;
679
+ };
680
+
681
+ Scope it to @reticlehq/browser and nothing else: widening Babel across node_modules costs every
682
+ rebuild in the project, for a dev-only dependency.`;
683
+ }
684
+ /** React Router's client-entry override point, in framework mode. */
685
+ export const REACT_ROUTER_ENTRY_PATH = 'app/entry.client.tsx';
686
+ /** The dev-only module @reticlehq/vite-plugin serves: connect() with the token already in it. */
687
+ const RETICLE_CONNECT_MODULE = '/@reticle-connect';
688
+ /** The one line that puts Reticle in a React Router client entry. */
689
+ export const REACT_ROUTER_CONNECT_LINE = `if (import.meta.env.DEV) void import('/@reticle-connect');`;
690
+ /**
691
+ * The client entry `init` writes when React Router framework mode has none.
692
+ *
693
+ * `app/entry.client.tsx` is an OVERRIDE: React Router supplies a default client entry, and the file
694
+ * only exists once an app opts out of it. So the generated one has to HYDRATE as well as connect —
695
+ * a file containing only our import would replace that default with one that never hydrates, an app
696
+ * that connects to Reticle and renders nothing. This is React Router v7's own default entry with one
697
+ * line added.
698
+ *
699
+ * The import is the module `@reticlehq/vite-plugin` serves. It already carries the port, the project
700
+ * id and this machine's pairing token, and it imports the app's `src/reticle-dev` dev module — which
701
+ * is where capabilities are registered. Nothing here needs filling in.
702
+ */
703
+ export function reactRouterEntryFile() {
704
+ return `import { HydratedRouter } from 'react-router/dom';
705
+ import { StrictMode, startTransition } from 'react';
706
+ import { hydrateRoot } from 'react-dom/client';
707
+
708
+ // Dev-only: React Router framework mode renders HTML through its own request handler, so the Vite
709
+ // plugin's index.html injection never fires and the connect script never reaches the page. The
710
+ // plugin is still doing the other half of the job — it stamps data-reticle-source, which is what
711
+ // puts file:line on every verdict.
712
+ ${REACT_ROUTER_CONNECT_LINE}
713
+
714
+ startTransition(() => {
715
+ hydrateRoot(
716
+ document,
717
+ <StrictMode>
718
+ <HydratedRouter />
719
+ </StrictMode>,
720
+ );
721
+ });
722
+ `;
723
+ }
724
+ /**
725
+ * Add the connect line to an entry the app already owns — or null when it is already there.
726
+ *
727
+ * After the LAST import, for the reason the CRA patch does the same: the entry's own imports must
728
+ * still be evaluated first, and this is a side-effect import.
729
+ */
730
+ export function reactRouterEntryPatch(source) {
731
+ if (source.includes(RETICLE_CONNECT_MODULE))
732
+ return null;
733
+ const lines = source.split('\n');
734
+ let lastImport = -1;
735
+ for (const [index, line] of lines.entries()) {
736
+ if (/^\s*import\s/.test(line))
737
+ lastImport = index;
738
+ }
739
+ lines.splice(lastImport + 1, 0, REACT_ROUTER_CONNECT_LINE);
740
+ return lines.join('\n');
741
+ }
742
+ /** Where a Nuxt dev-only client plugin belongs. `.client` keeps it out of SSR; Nuxt auto-registers it. */
743
+ export const NUXT_PLUGIN_PATH = 'app/plugins/reticle.client.ts';
744
+ /**
745
+ * The plugin directory Nuxt actually scans, which is not the same on 3 and 4.
746
+ *
747
+ * Nuxt 4's default `srcDir` is `app/`, so plugins live in `app/plugins/`. Nuxt 3's is the project
748
+ * root, so they live in `plugins/`. Writing to the wrong one is the silent failure this whole change
749
+ * exists to remove: the file is on disk, `init` reports it green, Nuxt never scans that directory,
750
+ * and no plugin is ever registered. Keyed on whether the app HAS an `app/` directory, which is the
751
+ * same signal Nuxt itself uses to pick its srcDir.
752
+ */
753
+ export function nuxtPluginPath(hasAppDir) {
754
+ return hasAppDir ? NUXT_PLUGIN_PATH : 'plugins/reticle.client.ts';
755
+ }
756
+ /**
757
+ * The dev-only Nuxt client plugin `init` writes.
758
+ *
759
+ * Every trap in it is one somebody actually hit. `import.meta.dev` rather than a hostname check: the
760
+ * reported failure was a snippet guarded on `window.location.hostname === 'localhost'`, which is
761
+ * false on any hosts-file alias or LAN address and throws during SSR, so the connect never ran with
762
+ * no error and no log line. `.client.ts` rather than an SSR guard, because that suffix is what keeps
763
+ * this out of the server bundle. `@reticlehq/browser` rather than the React kit, because installing
764
+ * a package named `@reticlehq/react` into a Vue codebase is the single thing most likely to make
765
+ * somebody abandon the setup.
766
+ *
767
+ * The token is inlined by `nuxt.config`'s `vite.define` (see nuxt-patch.ts). The bridge requires it
768
+ * even on localhost, and nothing in a browser can read the file it lives in.
769
+ */
770
+ export function nuxtPluginFile(port, projectId, testids = []) {
771
+ const base = connectArg(port, projectId);
772
+ const fields = '' === base ? '' : `${base.slice(1, -1).trim()}, `;
773
+ return `// Dev-only: connect Reticle from a Nuxt client plugin. Nuxt owns its own Vite instance and
774
+ // renders its own HTML, so the Vite plugin's index.html injection never fires — a .client plugin is
775
+ // the path that can register a session at all.
776
+ export default defineNuxtPlugin(() => {
777
+ // import.meta.dev is resolved at BUILD time, so it does not care what hostname you develop on.
778
+ // Do NOT guard on window.location.hostname === 'localhost': that is false on any hosts-file alias
779
+ // or LAN address, and window does not exist during SSR.
780
+ if (!import.meta.dev) return;
781
+ void import('@reticlehq/browser').then(({ reticle, registerCapabilities }) => {
782
+ // Both inlined by nuxt.config's vite.define, which \`reticle init\` wrote. The bridge refuses a
783
+ // connect with no token even on localhost; the root makes source paths repo-relative.
784
+ const token = typeof __RETICLE_TOKEN__ !== 'undefined' ? __RETICLE_TOKEN__ : '';
785
+ const root = typeof __RETICLE_ROOT__ !== 'undefined' ? __RETICLE_ROOT__ : '';
786
+ reticle.connect({
787
+ ${fields}...(token.length > 0 ? { token } : {}),
788
+ ...(root.length > 0 ? { root } : {}),
789
+ });
790
+
791
+ // What the agent can drive without guessing. Add a store here too — registerStore('cart',
792
+ // piniaStore(useCartStore())) — and the agent can check what the app BELIEVES, not just what it
793
+ // rendered. See node_modules/@reticlehq/server/docs/usage.md.
794
+ ${registerCapabilitiesCall(testids, ' ')}
795
+ });
796
+ });
797
+
798
+ declare const __RETICLE_TOKEN__: string | undefined;
799
+ declare const __RETICLE_ROOT__: string | undefined;
800
+ `;
801
+ }
802
+ /**
803
+ * Said as its own NOTICE beside the write, not inside it: a running dev server does not pick up a
804
+ * new plugin, and off localhost the connect needs two more arguments.
805
+ *
806
+ * A ✓ line is one SKILL.md tells the reader to skip, so a caveat carried on the ✓ is a caveat nobody
807
+ * reads — and both of these end with an app that boots, looks correct and never pairs.
808
+ */
809
+ export const NUXT_PLUGIN_NOTICE = 'Restart the dev server: one that is already running does not pick up a new plugin — it will not ' +
810
+ 'appear in .nuxt/plugins/client.mjs and the app comes up with no SDK at all. And if your dev host ' +
811
+ 'is anything other than localhost (a hosts-file alias, a LAN IP, a tunnel), that connect call ' +
812
+ 'needs TWO additions, not one: allowNonLocalhost: true AND the token from ~/.reticle/pairing-token. ' +
813
+ 'The flag alone is NOT sufficient off localhost.';
814
+ /**
815
+ * The Nuxt recipe, written out in full because every trap in it is one somebody actually hit.
816
+ *
817
+ * Reported from the field, in the order they were hit: `init` classified a Nuxt 4 app as `html`, so
818
+ * it installed a package named `@reticlehq/react` (with `react` in its peer dependencies) into a Vue
819
+ * codebase — the reporter only continued after auditing our dist to confirm there are no React
820
+ * imports at runtime, which most people will not do. It then handed over a snippet guarded on
821
+ * `window.location.hostname === 'localhost'`, which fails twice over in Nuxt: `window` does not
822
+ * exist during SSR, and the dev host here was a hosts-file alias (required for the backend's
823
+ * white-label origin detection), so the guard was false and the connect never ran — no error, no log
824
+ * line, nothing to debug. And nothing said a running dev server does not pick up a new plugin.
825
+ *
826
+ * So: `import.meta.dev` (build-time, host-independent) instead of a hostname check, `.client.ts`
827
+ * instead of an SSR guard, the framework-neutral sensor instead of the React kit, the non-localhost
828
+ * flag named up front, and the restart said out loud.
829
+ */
830
+ export function nuxtManual(port, projectId) {
831
+ const base = connectArg(port, projectId);
832
+ const fields = '' === base ? '' : base.slice(1, -1).trim();
833
+ const connect = '' === fields ? 'reticle.connect()' : `reticle.connect({ ${fields} })`;
834
+ return `Nuxt owns its own Vite instance and renders its own HTML, so there is no vite.config to patch
835
+ and no index.html to inject into. Wire it with a dev-only CLIENT plugin, which is Nuxt's own idiom:
836
+
837
+ 1. Create ${NUXT_PLUGIN_PATH}:
838
+
839
+ export default defineNuxtPlugin(() => {
840
+ // import.meta.dev is the correct guard: it is resolved at build time, so it does not care
841
+ // what hostname you develop on. Do NOT guard on window.location.hostname === 'localhost' —
842
+ // that is false on any hosts-file alias or LAN address, and window does not exist in SSR.
843
+ if (!import.meta.dev) return
844
+ void import('@reticlehq/browser').then(({ reticle }) => {
845
+ ${connect}
846
+ })
847
+ })
848
+
849
+ The .client.ts suffix is load-bearing: it is what keeps this out of the server bundle.
850
+
851
+ 2. Restart the dev server. A dev server that is already running does not pick up a new plugin —
852
+ it will not appear in .nuxt/plugins/client.mjs, and the app will come up with no SDK at all.
853
+
854
+ 3. If your dev host is anything other than localhost (a hosts-file alias, a LAN IP, a tunnel), that
855
+ connect call needs TWO additions, not one: allowNonLocalhost: true AND a pairing token passed as
856
+ token. The flag alone is NOT sufficient off localhost. The token is the one in
857
+ ~/.reticle/pairing-token. Without both, the SDK loads and then refuses, and the only sign is one
858
+ line in the browser console.
859
+
860
+ 4. Add this to nuxt.config, so the dev server does not watch Reticle's own journal:
861
+
862
+ vite: { server: { watch: { ignored: [/(^|[\\\\/])\\.${ReticleDir.ROOT.slice(1)}([\\\\/]|$)/] } } }
863
+
864
+ Reticle journals every session into ${ReticleDir.ROOT}/ in your project root, rewriting one file
865
+ in it continuously while a session is live. Nuxt's dev server watches that root, so without this
866
+ it sees each write as a project file changing and full-reloads the page — which reconnects the
867
+ SDK, which produces the next write. The loop runs several times a second and looks like anything
868
+ except what it is: refs go stale, actions die mid-flight, and the session appears to flap.
869
+ It is a RegExp rather than a glob on purpose — chokidar dropped glob support in v4, so a
870
+ double-star pattern here is accepted and matches nothing.
871
+
872
+ The package is @reticlehq/browser — the framework-neutral sensor. DOM, network, console, routing and
873
+ source file:line all work in Vue. What you do not get is React component identity, which is the only
874
+ thing the React adapter adds. There is no Nuxt app in this project's CI, so this path is UNVERIFIED:
875
+ if something does not work, please open an issue.`;
876
+ }
877
+ /**
878
+ * Root-level project config for Reticle. Written by `reticle init`; read by `reticle mcp` for the port
879
+ * and by tooling for the stable projectId (the app's identity across port changes).
880
+ */
881
+ export function reticleConfigContent(framework, port, projectId, installSource) {
882
+ const fields = { framework };
883
+ if (projectId !== undefined && projectId.length > 0)
884
+ fields['projectId'] = projectId;
885
+ if (port !== undefined && port !== RETICLE_DEFAULT_PORT)
886
+ fields['port'] = port;
887
+ // How this install arrived, recorded HERE because it is a property of the install and the only
888
+ // moment anything knows it is the moment it happens. It reaches us as an environment variable set
889
+ // by whichever channel ran the install, and an environment variable is gone by the next command,
890
+ // so every event after this one reported `unknown` and the question "which channel actually
891
+ // converts" could not be asked at all. Written once, read for the life of the project.
892
+ //
893
+ // A closed vocabulary, narrowed before it gets here, so this can never carry a path or a URL.
894
+ if (installSource !== undefined && installSource.length > 0) {
895
+ fields['installSource'] = installSource;
896
+ }
897
+ return `${JSON.stringify(fields, null, 2)}\n`;
898
+ }