@reticlehq/init 3.2.0 → 3.4.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 (93) hide show
  1. package/dist/detect/capabilities.d.ts +4 -1
  2. package/dist/detect/capabilities.js +45 -9
  3. package/dist/detect/detect.d.ts +58 -4
  4. package/dist/detect/detect.js +117 -7
  5. package/dist/detect/dev-script.d.ts +26 -2
  6. package/dist/detect/dev-script.js +80 -7
  7. package/dist/detect/non-js-project.d.ts +1 -1
  8. package/dist/detect/non-js-project.js +6 -1
  9. package/dist/detect/workspace-apps.d.ts +21 -0
  10. package/dist/detect/workspace-apps.js +111 -12
  11. package/dist/detect/workspace-redirect.d.ts +2 -1
  12. package/dist/detect/workspace-redirect.js +28 -2
  13. package/dist/diagnose/containerised-dev-server.d.ts +12 -4
  14. package/dist/diagnose/containerised-dev-server.js +36 -4
  15. package/dist/diagnose/csp-check.d.ts +12 -6
  16. package/dist/diagnose/csp-check.js +61 -13
  17. package/dist/diagnose/csp-doctor.d.ts +33 -2
  18. package/dist/diagnose/csp-doctor.js +103 -6
  19. package/dist/diagnose/csp-step.d.ts +27 -0
  20. package/dist/diagnose/csp-step.js +86 -0
  21. package/dist/diagnose/desktop-doctor.d.ts +1 -1
  22. package/dist/diagnose/desktop-doctor.js +8 -2
  23. package/dist/diagnose/install-hint.d.ts +9 -1
  24. package/dist/diagnose/install-hint.js +17 -7
  25. package/dist/diagnose/install-retries.d.ts +18 -2
  26. package/dist/diagnose/install-retries.js +50 -3
  27. package/dist/diagnose/terminal-wrap.d.ts +18 -0
  28. package/dist/diagnose/terminal-wrap.js +63 -0
  29. package/dist/host.d.ts +6 -0
  30. package/dist/host.js +3 -0
  31. package/dist/index.d.ts +8 -3
  32. package/dist/index.js +8 -3
  33. package/dist/no-package-json.d.ts +10 -0
  34. package/dist/no-package-json.js +121 -0
  35. package/dist/node-io.d.ts +9 -5
  36. package/dist/node-io.js +25 -7
  37. package/dist/patch/angular.d.ts +68 -0
  38. package/dist/patch/angular.js +216 -0
  39. package/dist/patch/astro-patch.d.ts +48 -13
  40. package/dist/patch/astro-patch.js +151 -43
  41. package/dist/patch/cra.d.ts +0 -2
  42. package/dist/patch/cra.js +0 -2
  43. package/dist/patch/electron-patch.d.ts +10 -1
  44. package/dist/patch/electron-patch.js +79 -27
  45. package/dist/patch/electron-vite-patch.js +1 -1
  46. package/dist/patch/nuxt-patch.d.ts +1 -1
  47. package/dist/patch/nuxt-patch.js +2 -2
  48. package/dist/patch/nuxt-snippets.d.ts +44 -0
  49. package/dist/patch/nuxt-snippets.js +135 -0
  50. package/dist/patch/remix.d.ts +24 -0
  51. package/dist/patch/remix.js +59 -0
  52. package/dist/patch/snippets.d.ts +58 -57
  53. package/dist/patch/snippets.js +181 -202
  54. package/dist/patch/static-page.d.ts +39 -0
  55. package/dist/patch/static-page.js +102 -0
  56. package/dist/patch/tanstack-start.d.ts +36 -8
  57. package/dist/patch/tanstack-start.js +118 -29
  58. package/dist/patch/vite-config.d.ts +5 -0
  59. package/dist/patch/vite-config.js +8 -0
  60. package/dist/patch/vite-env-types.d.ts +9 -0
  61. package/dist/patch/vite-env-types.js +19 -0
  62. package/dist/patch/vite-owning-config.d.ts +47 -0
  63. package/dist/patch/vite-owning-config.js +104 -19
  64. package/dist/plan/connect-steps.d.ts +10 -3
  65. package/dist/plan/connect-steps.js +22 -3
  66. package/dist/plan/framework-adapter.d.ts +5 -6
  67. package/dist/plan/framework-adapter.js +105 -11
  68. package/dist/plan/notices.js +4 -3
  69. package/dist/plan/plan-angular.d.ts +6 -0
  70. package/dist/plan/plan-angular.js +89 -0
  71. package/dist/plan/plan-electron-vite.d.ts +6 -3
  72. package/dist/plan/plan-electron-vite.js +39 -8
  73. package/dist/plan/plan-framework.d.ts +9 -47
  74. package/dist/plan/plan-framework.js +114 -323
  75. package/dist/plan/plan-types.d.ts +47 -2
  76. package/dist/plan/plan-vite.d.ts +67 -0
  77. package/dist/plan/plan-vite.js +256 -0
  78. package/dist/plan/plan.js +155 -69
  79. package/dist/plan/port-steps.d.ts +45 -0
  80. package/dist/plan/port-steps.js +119 -0
  81. package/dist/plan/preflight.d.ts +17 -3
  82. package/dist/plan/preflight.js +53 -23
  83. package/dist/plan/stop-hook-step.d.ts +4 -0
  84. package/dist/plan/stop-hook-step.js +49 -0
  85. package/dist/project/agent-rules.js +2 -0
  86. package/dist/register/mcp-clients.d.ts +13 -0
  87. package/dist/register/mcp-clients.js +22 -0
  88. package/dist/register/mcp.d.ts +26 -5
  89. package/dist/register/mcp.js +64 -8
  90. package/dist/run-types.d.ts +10 -0
  91. package/dist/run.js +132 -78
  92. package/package.json +3 -4
  93. package/NOTICE +0 -5
@@ -2,9 +2,11 @@
2
2
  * Generated file contents and copy-paste snippets for `reticle init`. Kept as named constants/builders
3
3
  * so the runner never inlines free strings.
4
4
  */
5
- import { RETICLE_DEFAULT_PORT, ReticleDir, bridgeWsUrl, RETICLE_CLIENT_HOST, RETICLE_WS_PATH, } from '@reticlehq/core';
6
- import { UiLibrary } from '../detect/detect.js';
5
+ import { RETICLE_DEFAULT_PORT, bridgeWsUrl, RETICLE_CLIENT_HOST, RETICLE_WS_PATH, } from '@reticlehq/core';
6
+ import { Framework, UiLibrary } from '../detect/detect.js';
7
7
  import { RETICLE_VERSION } from '../version.js';
8
+ import { VITE_ENV_DEV_DECLARATION } from './vite-env-types.js';
9
+ import { posix } from 'node:path';
8
10
  /**
9
11
  * The SDK as one import a plain page can actually resolve.
10
12
  *
@@ -24,7 +26,13 @@ import { RETICLE_VERSION } from '../version.js';
24
26
  * PINNED to this server's version on purpose. A floating import upgrades the page SDK underneath a
25
27
  * daemon that did not move, which is `version_skew` arriving by a route nothing checks.
26
28
  */
27
- const CDN_SDK_URL = `https://cdn.jsdelivr.net/npm/@reticlehq/browser@${RETICLE_VERSION}/+esm`;
29
+ /**
30
+ * Where a generated file sends someone to learn `registerStore`. A URL, not a path into
31
+ * `node_modules/@reticlehq/server`: the server is never installed into the app, so the path the
32
+ * generated files used to name did not exist in any project they were written into.
33
+ */
34
+ export const STATE_DOCS_URL = 'https://docs.reticle.sh/state-management';
35
+ export const CDN_SDK_URL = `https://cdn.jsdelivr.net/npm/@reticlehq/browser@${RETICLE_VERSION}/+esm`;
28
36
  /**
29
37
  * The connect argument literal: a non-default port adds a `url`, and a projectId is always passed
30
38
  * (so the app is identifiable across port changes). Empty string only when neither applies.
@@ -57,6 +65,35 @@ export function connectArgWithToken(port, projectId, pairingToken) {
57
65
  const inner = base.length > 0 ? base.slice(1, -1).trim() : '';
58
66
  return `{ ${[inner, `token: '${pairingToken}'`].filter((p) => p.length > 0).join(', ')} }`;
59
67
  }
68
+ /**
69
+ * Does this codebase want the React kit, or the framework-neutral sensor? The ONE answer, read by
70
+ * both `frameworkPackages` (what is installed) and `sdkImport` (what generated code imports).
71
+ *
72
+ * The kit is what adds component identity — component names and stacks — and it is worth having
73
+ * wherever React or Preact is rendering (the adapter reaches Preact through `preact/compat`).
74
+ * Everywhere else it is a package named `@reticlehq/react`, carrying `react` in its peer
75
+ * dependencies, being installed into a codebase that has no React in it.
76
+ *
77
+ * UNKNOWN keeps the kit deliberately. Absence of evidence is not evidence of Vue, and guessing
78
+ * "sensor" on no information silently drops component identity from apps that should have it.
79
+ *
80
+ * Except where the absence IS the evidence: on these stacks nothing renders React but a `react` the
81
+ * app depends on itself, so no renderer found is a vanilla, Lit, Solid or Angular app.
82
+ */
83
+ const RENDERER_IS_DECLARED = new Set([
84
+ Framework.VITE,
85
+ // A package.json with no UI library and no bundler Reticle recognises. It was handed
86
+ // `@reticlehq/react` and `react` — a React install into a page with no React in it.
87
+ Framework.HTML,
88
+ // Forge's Vite template is vanilla TypeScript; a React renderer declares `react` like any app.
89
+ Framework.ELECTRON_FORGE,
90
+ Framework.ANGULAR,
91
+ ]);
92
+ export function usesReactKit(uiLibrary, framework) {
93
+ if (UiLibrary.UNKNOWN === uiLibrary)
94
+ return !RENDERER_IS_DECLARED.has(framework);
95
+ return uiLibrary !== UiLibrary.VUE && uiLibrary !== UiLibrary.SVELTE;
96
+ }
60
97
  /**
61
98
  * Which SDK package the GENERATED code should import, and whether `install()` applies.
62
99
  *
@@ -68,9 +105,8 @@ export function connectArgWithToken(port, projectId, pairingToken) {
68
105
  * `install()` is the React adapter's, not the sensor's: `@reticlehq/browser` exports `reticle` and no
69
106
  * `install`, so swapping the specifier alone would trade a missing module for a missing export.
70
107
  */
71
- export function sdkImport(uiLibrary) {
72
- const react = uiLibrary !== UiLibrary.VUE && uiLibrary !== UiLibrary.SVELTE;
73
- return react
108
+ export function sdkImport(uiLibrary, framework) {
109
+ return usesReactKit(uiLibrary, framework)
74
110
  ? { specifier: '@reticlehq/react', usesInstall: true }
75
111
  : { specifier: '@reticlehq/browser', usesInstall: false };
76
112
  }
@@ -95,6 +131,15 @@ function frameworkPluginExample(uiLibrary) {
95
131
  return '/* your existing plugins */';
96
132
  }
97
133
  }
134
+ /**
135
+ * The `defineConfig` import each printed config needs. The snippets call `defineConfig`, and one
136
+ * pasted as printed into an empty file used to die with `ReferenceError: defineConfig is not
137
+ * defined` because the import was left out.
138
+ */
139
+ const DEFINE_CONFIG_IMPORT = {
140
+ VITE: "import { defineConfig } from 'vite';",
141
+ ELECTRON_VITE: "import { defineConfig } from 'electron-vite';",
142
+ };
98
143
  /** The Vite-config snippet printed when we can't safely auto-patch the config. */
99
144
  export function viteManual(port, uiLibrary = UiLibrary.UNKNOWN, inject = true, sourceMapping = true) {
100
145
  const options = [
@@ -110,6 +155,7 @@ lowercase JSX tag is not an element. Stamping one crashes the app at commit time
110
155
  pointers, not the app.`;
111
156
  return `Add the Reticle plugin to your Vite config:
112
157
 
158
+ ${DEFINE_CONFIG_IMPORT.VITE}
113
159
  import { reticle } from '@reticlehq/vite-plugin';
114
160
 
115
161
  export default defineConfig({
@@ -124,8 +170,11 @@ from \`vite build\`.${note}`;
124
170
  /**
125
171
  * The electron-vite recipe: the plugin belongs in `renderer`, never in `main` or `preload`.
126
172
  *
127
- * `desktop: true` is load-bearing. Without it the plugin is serve-only, so a packaged renderer
128
- * (a production build with no dev server) ships with no connect() at all.
173
+ * `desktop: true` is load-bearing: it injects connect() into the entry module and passes
174
+ * `allowInProduction`, because a packaged renderer has no dev server and reports
175
+ * `NODE_ENV=production`, so without it the SDK's production backstop refuses to start. It does not
176
+ * instrument a release — a production-mode `electron-vite build` is stubbed like a web bundle — so
177
+ * a packaged renderer that should be driven is built with `--mode development`.
129
178
  */
130
179
  export function electronViteManual(port, uiLibrary = UiLibrary.UNKNOWN) {
131
180
  const extras = port === undefined
@@ -133,6 +182,7 @@ export function electronViteManual(port, uiLibrary = UiLibrary.UNKNOWN) {
133
182
  : `desktop: true, port: ${String(port)}, captureNetworkBodies: true`;
134
183
  return `Add the Reticle plugin to the \`renderer\` block of electron.vite.config, not \`main\`:
135
184
 
185
+ ${DEFINE_CONFIG_IMPORT.ELECTRON_VITE}
136
186
  import { reticle } from '@reticlehq/vite-plugin';
137
187
 
138
188
  export default defineConfig({
@@ -143,8 +193,9 @@ export function electronViteManual(port, uiLibrary = UiLibrary.UNKNOWN) {
143
193
  },
144
194
  });
145
195
 
146
- \`desktop: true\` is required: a packaged renderer is a production build with no dev server, so the
147
- default serve-only plugin is dropped from \`vite build\` and the shipped app never connects.
196
+ \`desktop: true\` is required: a packaged renderer has no dev server and reports production, and
197
+ without it the SDK refuses to start there. A production-mode build still ships no Reticle code; to drive a
198
+ packaged renderer with no dev server, build it with \`--mode development\`.
148
199
 
149
200
  Also add \`import '@reticlehq/electron/preload'\` as the first line of your preload, and
150
201
  \`installReticleCapture(win)\` in main after you construct the BrowserWindow.`;
@@ -182,7 +233,7 @@ export function nextReticleDevFile(port, projectId, testids = [], stores = [], f
182
233
  const storeBlock = found.length > 0
183
234
  ? found.map((s) => ` registerStore('${s.key}', ${s.ident});`).join('\n')
184
235
  : 0 === stores.length
185
- ? ' // No state library detected. If you add one, register it here — see node_modules/@reticlehq/server/docs/usage.md.'
236
+ ? ` // No state library detected. If you add one, register it here — see ${STATE_DOCS_URL}`
186
237
  : stores.map((h) => ` // import your store, then: ${h}`).join('\n');
187
238
  const registerNames = found.length > 0 ? ', registerStore' : '';
188
239
  // Multiline connect + single blank after imports (#684): the one-line connect and the double blank
@@ -204,10 +255,13 @@ export function ReticleDev() {
204
255
  // withReticle() finds the daemon serving this project on every dev-server start. It wins over
205
256
  // any port written into this file at install time, so moving the daemon needs no edit here.
206
257
  const url = process.env.NEXT_PUBLIC_RETICLE_URL;
258
+ // So a version-skewed pair names the SDK's version instead of "an unknown older wire".
259
+ const sdkVersion = process.env.NEXT_PUBLIC_RETICLE_SDK_VERSION;
207
260
  reticle.connect({
208
261
  ${fields}...(url ? { url } : {}),
209
262
  ...(token ? { token } : {}),
210
263
  ...(root ? { root } : {}),
264
+ ...(sdkVersion ? { sdkVersion } : {}),
211
265
  });
212
266
 
213
267
  // ── Start with ONE flow. ────────────────────────────────────────────────────────────────
@@ -263,10 +317,9 @@ layoutPath) {
263
317
  ? `\n url: '${bridgeWsUrl(port)}',`
264
318
  : '';
265
319
  const id = projectId !== undefined && projectId.length > 0 ? `\n projectId: '${projectId}',` : '';
266
- // Astro owns its Vite instance, so it has a build-time channel of its own: the same `define` that
267
- // already inlines the token can inline the daemon URL, resolved every time the config is read —
268
- // that is, on every `astro dev`. The port above is written once at install time and goes stale the
269
- // moment the daemon moves; this does not.
320
+ // Astro owns its Vite instance, so the layout can resolve the daemon URL in frontmatter on every
321
+ // `astro dev` and put it on a <meta> the client script reads. The port above is written once at
322
+ // install time and goes stale the moment the daemon moves; this does not.
270
323
  //
271
324
  // Needs the projectId to know WHICH daemon is ours. Without one there is nothing to match on, and
272
325
  // adopting a daemon serving another project would report its state as this app's, so the whole
@@ -276,10 +329,8 @@ layoutPath) {
276
329
  // `bridgeWsUrl` builds, and a generator that spells it out by hand is exactly how the four call
277
330
  // sites that constant exists to unify drifted apart in the first place.
278
331
  const RETICLE_CLIENT_HOST_LITERAL = `ws://${RETICLE_CLIENT_HOST}:`;
279
- const urlHelper = canDiscover
332
+ const urlFrontmatter = canDiscover
280
333
  ? `
281
- // The live daemon serving THIS project, re-read on every \`astro dev\`. Same rule as the Vite and
282
- // Next plugins: skip dead daemons, match on projectId, lowest port wins, '' when none matches.
283
334
  function reticleUrl() {
284
335
  const dir = process.env['RETICLE_PAIRING_TOKEN_DIR'] || join(homedir(), '.reticle');
285
336
  let best;
@@ -295,63 +346,77 @@ layoutPath) {
295
346
  } catch { return ''; }
296
347
  return best === undefined ? '' : '${RETICLE_CLIENT_HOST_LITERAL}' + best + '${RETICLE_WS_PATH}';
297
348
  }
349
+ const pairingUrl = import.meta.env.DEV ? reticleUrl() : '';
298
350
  `
299
351
  : '';
300
- const urlDefine = canDiscover ? `\n __RETICLE_URL__: JSON.stringify(reticleUrl()),` : '';
352
+ const urlMeta = canDiscover ? `\n <meta name="reticle-pairing-url" content={pairingUrl} />` : '';
301
353
  const urlRead = canDiscover
302
- ? `\n const url = typeof __RETICLE_URL__ !== 'undefined' ? __RETICLE_URL__ : '';`
354
+ ? `\n const url = document.querySelector('meta[name="reticle-pairing-url"]')?.getAttribute('content') ?? '';`
303
355
  : '';
304
- // After the baked port, so the discovered one wins: later key in an object literal.
305
- const urlSpread = canDiscover ? `\n ...(url.length > 0 ? { url } : {}),` : '';
306
- return `Astro renders its own HTML, so the connect goes in a page <script> and the pairing token is inlined by the config.
356
+ const urlSpread = canDiscover ? `\n ...(url.length > 0 ? { url } : {}),` : '';
357
+ return `Astro renders its own HTML, so the connect goes in a local module the page statically imports. Do not put the pairing token through \`vite.define\`: on Astro 7.2+ that substitution never reaches the client, the identifier stays literal, and the bridge refuses the dial. Read the token in frontmatter and put it on a <meta> the module can query. Reading it per request also means a token written after the dev server started is picked up on the next load instead of needing a restart.
307
358
 
308
- 1. In astro.config.mjs — inline the daemon's token and raise the build target:
359
+ 1. In astro.config.mjs, raise the build target. The token does NOT belong here:
309
360
 
310
- import { readFileSync, readdirSync } from 'node:fs';
311
- import { homedir } from 'node:os';
312
- import { join } from 'node:path';
313
-
314
- function reticleToken() {
315
- const dir = process.env['RETICLE_PAIRING_TOKEN_DIR'] || join(homedir(), '.reticle');
316
- try { return readFileSync(join(dir, 'pairing-token'), 'utf8').trim(); } catch { return ''; }
317
- }
318
- ${urlHelper}
319
361
  export default defineConfig({
320
362
  vite: {
321
363
  // Astro's default target down-levels the modern SDK bundle and fails on a destructuring transform.
322
364
  build: { target: 'es2022' },
323
- optimizeDeps: { esbuildOptions: { target: 'es2022' } },
324
- define: {
325
- __RETICLE_TOKEN__: JSON.stringify(reticleToken()),${urlDefine}
326
- // Without this, source pointers come back as absolute paths from YOUR machine — useless in a
327
- // report. Every other framework gets it from its build plugin; Astro owns its Vite instance.
328
- __RETICLE_ROOT__: JSON.stringify(process.cwd()),
329
- },
365
+ optimizeDeps: { include: ['@reticlehq/react'] },
330
366
  },
331
367
  });
332
368
 
333
369
  ${layoutHost(layoutPath)}
334
370
 
371
+ ---
372
+ import { readFileSync${canDiscover ? ', readdirSync' : ''} } from 'node:fs';
373
+ import { homedir } from 'node:os';
374
+ import { join } from 'node:path';
375
+ const pairingToken = (() => {
376
+ if (!import.meta.env.DEV) return '';
377
+ const dir = process.env['RETICLE_PAIRING_TOKEN_DIR'] || join(homedir(), '.reticle');
378
+ try { return readFileSync(join(dir, 'pairing-token'), 'utf8').trim(); } catch { return ''; }
379
+ })();
380
+ const pairingRoot = import.meta.env.DEV ? process.cwd() : '';${urlFrontmatter} ---
381
+ <meta name="reticle-pairing-token" content={pairingToken} />
382
+ <meta name="reticle-pairing-root" content={pairingRoot} />${urlMeta}
335
383
  <script>
384
+ import connectReticle from '../components/ReticleDev';
336
385
  if (import.meta.env.DEV) {
337
- const token = typeof __RETICLE_TOKEN__ !== 'undefined' ? __RETICLE_TOKEN__ : '';
338
- const root = typeof __RETICLE_ROOT__ !== 'undefined' ? __RETICLE_ROOT__ : '';${urlRead}
339
- const { reticle, install } = await import('@reticlehq/react');
340
- install();
341
- reticle.connect({${id}${extra}${urlSpread}
342
- ...(token.length > 0 ? { token } : {}),
343
- ...(root.length > 0 ? { root } : {}),
344
- });
386
+ connectReticle();
345
387
  }
346
388
  </script>
347
389
 
348
- 3. In src/env.d.ts — declare the Vite define names so \`astro check\` can see them (create-astro's
349
- default build runs check first, and without this it fails with Cannot find name '__RETICLE_TOKEN__'):
390
+ The <script> tag must not sit inside a conditional. Astro hoists them statically, so
391
+ \`{isDev && (<script />)}\` is never hoisted and the module 404s.
392
+ The dev gate belongs in the imported module.
393
+
394
+ 3. Create src/components/ReticleDev.ts — Vite owns this file, so the SDK is a static import:
395
+
396
+ import { reticle, install } from '@reticlehq/react';
397
+ export default function connectReticle() {
398
+ const token = document.querySelector('meta[name="reticle-pairing-token"]')?.getAttribute('content') ?? '';
399
+ const root = document.querySelector('meta[name="reticle-pairing-root"]')?.getAttribute('content') ?? '';${urlRead}
400
+ if (token.length === 0) {
401
+ console.warn('[reticle] no pairing token was available when this page rendered, so the app will connect and be refused — you will see NO SESSION even though the SDK loads and the socket opens. The token is written by the Reticle daemon: start it and reload this page.');
402
+ }
403
+ install();
404
+ reticle.connect({${id}${extra}${urlSpread}
405
+ ...(token.length > 0 ? { token } : {}),
406
+ ...(root.length > 0 ? { root } : {}),
407
+ });
408
+ }
409
+
410
+ 4. In src/env.d.ts — declare the window names so \`astro check\` can see them (create-astro's
411
+ default build runs check first):
350
412
 
351
- declare const __RETICLE_TOKEN__: string | undefined;
352
- declare const __RETICLE_ROOT__: string | undefined;
413
+ interface Window {
414
+ __RETICLE_TOKEN__?: string;
415
+ __RETICLE_ROOT__?: string;
416
+ }
353
417
 
354
- 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.`;
418
+ Start the daemon, then load the page. The token is read when the page renders, so a reload is enough
419
+ once the token file exists — you do not have to restart \`astro dev\`.`;
355
420
  }
356
421
  /**
357
422
  * The `registerCapabilities` call every generated connect carries.
@@ -383,7 +448,7 @@ ${indent}});`;
383
448
  * breaks the module everything else hangs off. So the store lines are generated COMMENTED, naming
384
449
  * the libraries actually found in package.json, with the exact call to uncomment.
385
450
  */
386
- export function viteDevModuleFile(testids, stores, found = [], uiLibrary = UiLibrary.REACT) {
451
+ export function viteDevModuleFile(testids, stores, found = [], uiLibrary = UiLibrary.REACT, framework) {
387
452
  // The specifier has to be the package `frameworkPackages` installed. This file is the Vite path —
388
453
  // the commonest install there is — and it was hardcoded to `@reticlehq/react` while a Vue or
389
454
  // Svelte app was being given `@reticlehq/browser`, so the generated file imported something that
@@ -391,7 +456,7 @@ export function viteDevModuleFile(testids, stores, found = [], uiLibrary = UiLib
391
456
  //
392
457
  // The sensor exports `registerCapabilities` and `registerStore` (and the store adapters) just as
393
458
  // the React kit does; the only thing it lacks is `install()`, which this file never called.
394
- const sdk = sdkImport(uiLibrary);
459
+ const sdk = sdkImport(uiLibrary, framework);
395
460
  const ids = testids.map((t) => `'${t}'`).join(', ');
396
461
  // A store we FOUND is imported and registered outright — the whole point of the file. The hints
397
462
  // stay only for the libraries we can name but not wire (they need an argument we cannot infer).
@@ -399,10 +464,11 @@ export function viteDevModuleFile(testids, stores, found = [], uiLibrary = UiLib
399
464
  const storeBlock = found.length > 0
400
465
  ? found.map((s) => ` registerStore('${s.key}', ${s.ident});`).join('\n')
401
466
  : 0 === stores.length
402
- ? ' // No state library detected. If you add one, register it here — see node_modules/@reticlehq/server/docs/usage.md.'
467
+ ? ` // No state library detected. If you add one, register it here — see ${STATE_DOCS_URL}`
403
468
  : stores.map((h) => ` // import your store, then: ${h}`).join('\n');
404
469
  const registerImport = found.length > 0 ? 'registerCapabilities, registerStore' : 'registerCapabilities';
405
- return `// Dev-only. Imported automatically by @reticlehq/vite-plugin, so you do not need to import it.
470
+ return `// Dev-only. Loaded for you after connect() — by @reticlehq/vite-plugin's injected connect, or by
471
+ // the connect file init wrote for a framework that connects itself — so you do not import it.
406
472
  // Self-guards on import.meta.env.DEV, so it is a no-op in a production build.
407
473
  import { ${registerImport} } from '${sdk.specifier}';
408
474
  ${storeImports.length > 0 ? `${storeImports}\n` : ''}
@@ -424,10 +490,19 @@ ${storeBlock}
424
490
  stores: [${found.map((s) => `'${s.key}'`).join(', ')}], // the keys you registered above
425
491
  });
426
492
  }
427
- `;
493
+
494
+ ${VITE_ENV_DEV_DECLARATION}`;
428
495
  }
429
496
  /** Where that module goes. Matches @reticlehq/vite-plugin's convention list. */
430
497
  export const VITE_DEV_MODULE_PATH = 'src/reticle-dev.ts';
498
+ /**
499
+ * The dev module as a relative import from `fromFile` — for the frameworks that connect themselves
500
+ * and so have to load it themselves (see svelteKitHooksFile, tanstackStartConnectFile).
501
+ */
502
+ export function devModuleSpecifier(fromFile) {
503
+ const rel = posix.relative(posix.dirname(fromFile), VITE_DEV_MODULE_PATH.replace(/\.ts$/, ''));
504
+ return rel.startsWith('.') ? rel : `./${rel}`;
505
+ }
431
506
  /**
432
507
  * electron-vite's renderer Vite root is `src/renderer`, so the plugin looks for
433
508
  * `src/renderer/src/reticle-dev.ts` — not the package-root path a plain Vite app uses.
@@ -504,7 +579,7 @@ st.html(
504
579
  * `<head>`, so the SDK sees a parsed document.
505
580
  */
506
581
  export function djangoMiddlewareSnippet(connectArgLiteral) {
507
- return `# reticle_dev.py — add to MIDDLEWARE while DEBUG is on:
582
+ return `# reticle_dev.py: add to MIDDLEWARE while DEBUG is on:
508
583
  # MIDDLEWARE = [..., "reticle_dev.ReticleDevMiddleware"]
509
584
  from django.conf import settings
510
585
 
@@ -540,8 +615,21 @@ class ReticleDevMiddleware:
540
615
  return response
541
616
  `;
542
617
  }
543
- export function htmlManual(port, projectId, pairingToken) {
618
+ export function htmlManual(port, projectId, pairingToken,
619
+ // Which SDK the bundled form imports must be the one init INSTALLED. A page with no UI library
620
+ // now gets the sensor, and a snippet still naming `@reticlehq/react` would import a package that
621
+ // is not there.
622
+ uiLibrary = UiLibrary.UNKNOWN) {
544
623
  const withToken = connectArgWithToken(port, projectId, pairingToken);
624
+ const sdk = sdkImport(uiLibrary, Framework.HTML);
625
+ const bundledImport = sdk.usesInstall
626
+ ? `void import('${sdk.specifier}').then(({ reticle, install }) => {
627
+ install();
628
+ reticle.connect(${withToken});
629
+ });`
630
+ : `void import('${sdk.specifier}').then(({ reticle }) => {
631
+ reticle.connect(${withToken});
632
+ });`;
545
633
  const tokenNote = pairingToken === undefined || 0 === pairingToken.length
546
634
  ? ''
547
635
  : `\n\n The \`token\` is this machine's pairing token, read from ~/.reticle/pairing-token. Keep it: the
@@ -550,13 +638,10 @@ export function htmlManual(port, projectId, pairingToken) {
550
638
  return `No Vite/Next plugin detected — wire the dev-only connect by hand. Pick the form for your setup:
551
639
 
552
640
  • Bundled app (Create React App, webpack, Parcel, Vue/Svelte CLI, etc.) — add to your ENTRY module
553
- (e.g. src/index.js or src/main.js), where '@reticlehq/react' resolves through your bundler:
641
+ (e.g. src/index.js or src/main.js), where '${sdk.specifier}' resolves through your bundler:
554
642
 
555
643
  if (process.env.NODE_ENV !== 'production') {
556
- void import('@reticlehq/react').then(({ reticle, install }) => {
557
- install();
558
- reticle.connect(${withToken});
559
- });
644
+ ${bundledImport}
560
645
  }${tokenNote}
561
646
 
562
647
  • Plain HTML with NO build step (FastAPI, Flask, Django, Rails, Streamlit, a hand-written page) —
@@ -581,7 +666,7 @@ export const SVELTEKIT_HOOKS_PATH = 'src/hooks.client.ts';
581
666
  * component names and source mapping do not, and no CI gate covers this stack. Saying so beats
582
667
  * reporting all-green.
583
668
  */
584
- export function unverifiedUiLibraryNote(library) {
669
+ export function unverifiedUiLibraryNote(library, installGated = false) {
585
670
  // Preact is not in the same position as Vue or Svelte and must not be told it is. The React
586
671
  // adapter reaches Preact through `preact/compat`, which is what `docs/frameworks.mdx` has always
587
672
  // said, so telling a Preact reader they get no component identity contradicts our own docs and
@@ -590,10 +675,10 @@ export function unverifiedUiLibraryNote(library) {
590
675
  const identity = 'preact' === library
591
676
  ? '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.'
592
677
  : 'What `@reticlehq/react` adds and you will NOT get is React component identity: component names and component stacks.';
593
- 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.`;
678
+ 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} ${installGated ? `The install gate scaffolds this ${library} stack from scratch on every change, so this SETUP is proven; no gate drives it to a verdict, so the drive is not.` : `No CI gate covers ${library}.`} Driven on every change: Vite + React, Next.js, Astro. If something doesn't work, please open an issue.`;
594
679
  }
595
- 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.';
596
- 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.';
680
+ export const SVELTEKIT_SETUP_GATED_NOTE = 'The install gate scaffolds a SvelteKit app on every release and requires this hook to connect a session, so the wiring is proven; driving flows on SvelteKit to a verdict is not. If the hook does not register a session, please open an issue.';
681
+ 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. Gated today: Vite (React, Vue), Next.js, Nuxt, React Router, Remix, Astro, SvelteKit, Create React App and Angular. If the client effect does not register a session, please open an issue.';
597
682
  /**
598
683
  * Printed when init detects react-three-fiber. Reticle's model is DOM + store + network; a WebGL
599
684
  * canvas is none of those. Without this line, init succeeds, sessions connect, and surrounding UI
@@ -632,6 +717,9 @@ if (import.meta.env.DEV) {
632
717
  ...(sdkVersion.length > 0 ? { sdkVersion } : {}),
633
718
  });
634
719
  ${registerCapabilitiesCall(testids, ' ')}
720
+ // Your stores and capabilities live in ${VITE_DEV_MODULE_PATH}. Only the plugin's injected
721
+ // connect imports that file, and SvelteKit never runs it, so this hook loads it instead.
722
+ void import('${devModuleSpecifier(SVELTEKIT_HOOKS_PATH)}');
635
723
  });
636
724
  }
637
725
 
@@ -681,10 +769,26 @@ export function webpack4TranspileNote(reactScriptsMajor) {
681
769
  }
682
770
  /** React Router's client-entry override point, in framework mode. */
683
771
  export const REACT_ROUTER_ENTRY_PATH = 'app/entry.client.tsx';
684
- /** The dev-only module @reticlehq/vite-plugin serves: connect() with the token already in it. */
685
- const RETICLE_CONNECT_MODULE = '/@reticle-connect';
686
- /** The one line that puts Reticle in a React Router client entry. */
687
- export const REACT_ROUTER_CONNECT_LINE = `if (import.meta.env.DEV) void import('/@reticle-connect');`;
772
+ /**
773
+ * The dev-only module @reticlehq/vite-plugin serves: connect() with the token already in it. Named
774
+ * without its leading slash, because that is the part both the current line and the old one carry.
775
+ */
776
+ const RETICLE_CONNECT_MODULE = '@reticle-connect';
777
+ /**
778
+ * The one line that puts Reticle in a React Router client entry — and in a Remix one.
779
+ *
780
+ * The specifier is BUILT, not written as a literal. `import('/@reticle-connect')` is a module only
781
+ * the Vite dev server can answer, so `tsc` cannot resolve it: a fresh framework-mode scaffold went
782
+ * red on `npm run typecheck` (`TS2307: Cannot find module '/@reticle-connect'`) the moment `init`
783
+ * wrote it. A computed specifier is typed `Promise<any>` and never resolved, so no declaration, no
784
+ * suppression comment and no tsconfig edit is needed. The browser still asks the dev server for the
785
+ * same URL, which the plugin serves; `BASE_URL` keeps it right under a non-root `base`. `@vite-ignore`
786
+ * is Vite's own marker for a dynamic import it should not try to analyse, not a lint suppression. In
787
+ * a production build `import.meta.env.DEV` is false and the whole line is dropped.
788
+ */
789
+ export const REACT_ROUTER_CONNECT_LINE = 'if (import.meta.env.DEV) void import(/* @vite-ignore */ `${import.meta.env.BASE_URL}@reticle-connect`);';
790
+ /** The line `init` used to write, which fails `tsc`; replaced in place on a re-run. */
791
+ const REACT_ROUTER_CONNECT_LINE_UNTYPED = "if (import.meta.env.DEV) void import('/@reticle-connect');";
688
792
  /**
689
793
  * The client entry `init` writes when React Router framework mode has none.
690
794
  *
@@ -726,6 +830,9 @@ startTransition(() => {
726
830
  * still be evaluated first, and this is a side-effect import.
727
831
  */
728
832
  export function reactRouterEntryPatch(source) {
833
+ if (source.includes(REACT_ROUTER_CONNECT_LINE_UNTYPED)) {
834
+ return source.replace(REACT_ROUTER_CONNECT_LINE_UNTYPED, REACT_ROUTER_CONNECT_LINE);
835
+ }
729
836
  if (source.includes(RETICLE_CONNECT_MODULE))
730
837
  return null;
731
838
  const lines = source.split('\n');
@@ -737,135 +844,7 @@ export function reactRouterEntryPatch(source) {
737
844
  lines.splice(lastImport + 1, 0, REACT_ROUTER_CONNECT_LINE);
738
845
  return lines.join('\n');
739
846
  }
740
- /** Where a Nuxt dev-only client plugin belongs. `.client` keeps it out of SSR; Nuxt auto-registers it. */
741
- export const NUXT_PLUGIN_PATH = 'app/plugins/reticle.client.ts';
742
- /**
743
- * The plugin directory Nuxt actually scans, which is not the same on 3 and 4.
744
- *
745
- * Nuxt 4's default `srcDir` is `app/`, so plugins live in `app/plugins/`. Nuxt 3's is the project
746
- * root, so they live in `plugins/`. Writing to the wrong one is the silent failure this whole change
747
- * exists to remove: the file is on disk, `init` reports it green, Nuxt never scans that directory,
748
- * and no plugin is ever registered. Keyed on whether the app HAS an `app/` directory, which is the
749
- * same signal Nuxt itself uses to pick its srcDir.
750
- */
751
- export function nuxtPluginPath(hasAppDir) {
752
- return hasAppDir ? NUXT_PLUGIN_PATH : 'plugins/reticle.client.ts';
753
- }
754
- /**
755
- * The dev-only Nuxt client plugin `init` writes.
756
- *
757
- * Every trap in it is one somebody actually hit. `import.meta.dev` rather than a hostname check: the
758
- * reported failure was a snippet guarded on `window.location.hostname === 'localhost'`, which is
759
- * false on any hosts-file alias or LAN address and throws during SSR, so the connect never ran with
760
- * no error and no log line. `.client.ts` rather than an SSR guard, because that suffix is what keeps
761
- * this out of the server bundle. `@reticlehq/browser` rather than the React kit, because installing
762
- * a package named `@reticlehq/react` into a Vue codebase is the single thing most likely to make
763
- * somebody abandon the setup.
764
- *
765
- * The token is inlined by `nuxt.config`'s `vite.define` (see nuxt-patch.ts). The bridge requires it
766
- * even on localhost, and nothing in a browser can read the file it lives in.
767
- */
768
- export function nuxtPluginFile(port, projectId, testids = []) {
769
- const base = connectArg(port, projectId);
770
- const fields = '' === base ? '' : `${base.slice(1, -1).trim()}, `;
771
- return `// Dev-only: connect Reticle from a Nuxt client plugin. Nuxt owns its own Vite instance and
772
- // renders its own HTML, so the Vite plugin's index.html injection never fires — a .client plugin is
773
- // the path that can register a session at all.
774
- export default defineNuxtPlugin(() => {
775
- // import.meta.dev is resolved at BUILD time, so it does not care what hostname you develop on.
776
- // Do NOT guard on window.location.hostname === 'localhost': that is false on any hosts-file alias
777
- // or LAN address, and window does not exist during SSR.
778
- if (!import.meta.dev) return;
779
- void import('@reticlehq/browser').then(({ reticle, registerCapabilities }) => {
780
- // Both inlined by nuxt.config's vite.define, which \`reticle init\` wrote. The bridge refuses a
781
- // connect with no token even on localhost; the root makes source paths repo-relative.
782
- const token = typeof __RETICLE_TOKEN__ !== 'undefined' ? __RETICLE_TOKEN__ : '';
783
- const root = typeof __RETICLE_ROOT__ !== 'undefined' ? __RETICLE_ROOT__ : '';
784
- reticle.connect({
785
- ${fields}...(token.length > 0 ? { token } : {}),
786
- ...(root.length > 0 ? { root } : {}),
787
- });
788
-
789
- // What the agent can drive without guessing. Add a store here too — registerStore('cart',
790
- // piniaStore(useCartStore())) — and the agent can check what the app BELIEVES, not just what it
791
- // rendered. See node_modules/@reticlehq/server/docs/usage.md.
792
- ${registerCapabilitiesCall(testids, ' ')}
793
- });
794
- });
795
-
796
- declare const __RETICLE_TOKEN__: string | undefined;
797
- declare const __RETICLE_ROOT__: string | undefined;
798
- `;
799
- }
800
- /**
801
- * Said as its own NOTICE beside the write, not inside it: a running dev server does not pick up a
802
- * new plugin, and off localhost the connect needs two more arguments.
803
- *
804
- * A ✓ line is one SKILL.md tells the reader to skip, so a caveat carried on the ✓ is a caveat nobody
805
- * reads — and both of these end with an app that boots, looks correct and never pairs.
806
- */
807
- export const NUXT_PLUGIN_NOTICE = 'Restart the dev server: one that is already running does not pick up a new plugin — it will not ' +
808
- 'appear in .nuxt/plugins/client.mjs and the app comes up with no SDK at all. And if your dev host ' +
809
- 'is anything other than localhost (a hosts-file alias, a LAN IP, a tunnel), that connect call ' +
810
- 'needs TWO additions, not one: allowNonLocalhost: true AND the token from ~/.reticle/pairing-token. ' +
811
- 'The flag alone is NOT sufficient off localhost.';
812
- /**
813
- * The Nuxt recipe, written out in full because each trap in it is one a Nuxt 4 app actually hit:
814
- * a Vue codebase classified as `html` and handed `@reticlehq/react`, a connect guarded on
815
- * `window.location.hostname === 'localhost'` (false on any hosts-file alias, and `window` does not
816
- * exist during SSR), and a running dev server that never picked up the new plugin.
817
- *
818
- * So: `import.meta.dev` instead of a hostname check, `.client.ts` instead of an SSR guard, the
819
- * framework-neutral sensor instead of the React kit, the non-localhost flag named up front, and the
820
- * restart said out loud.
821
- */
822
- export function nuxtManual(port, projectId) {
823
- const base = connectArg(port, projectId);
824
- const fields = '' === base ? '' : base.slice(1, -1).trim();
825
- const connect = '' === fields ? 'reticle.connect()' : `reticle.connect({ ${fields} })`;
826
- return `Nuxt owns its own Vite instance and renders its own HTML, so there is no vite.config to patch
827
- and no index.html to inject into. Wire it with a dev-only CLIENT plugin, which is Nuxt's own idiom:
828
-
829
- 1. Create ${NUXT_PLUGIN_PATH}:
830
-
831
- export default defineNuxtPlugin(() => {
832
- // import.meta.dev is the correct guard: it is resolved at build time, so it does not care
833
- // what hostname you develop on. Do NOT guard on window.location.hostname === 'localhost' —
834
- // that is false on any hosts-file alias or LAN address, and window does not exist in SSR.
835
- if (!import.meta.dev) return
836
- void import('@reticlehq/browser').then(({ reticle }) => {
837
- ${connect}
838
- })
839
- })
840
-
841
- The .client.ts suffix is load-bearing: it is what keeps this out of the server bundle.
842
-
843
- 2. Restart the dev server. A dev server that is already running does not pick up a new plugin —
844
- it will not appear in .nuxt/plugins/client.mjs, and the app will come up with no SDK at all.
845
-
846
- 3. If your dev host is anything other than localhost (a hosts-file alias, a LAN IP, a tunnel), that
847
- connect call needs TWO additions, not one: allowNonLocalhost: true AND a pairing token passed as
848
- token. The flag alone is NOT sufficient off localhost. The token is the one in
849
- ~/.reticle/pairing-token. Without both, the SDK loads and then refuses, and the only sign is one
850
- line in the browser console.
851
-
852
- 4. Add this to nuxt.config, so the dev server does not watch Reticle's own journal:
853
-
854
- vite: { server: { watch: { ignored: [/(^|[\\\\/])\\.${ReticleDir.ROOT.slice(1)}([\\\\/]|$)/] } } }
855
-
856
- Reticle journals every session into ${ReticleDir.ROOT}/ in your project root, rewriting one file
857
- in it continuously while a session is live. Nuxt's dev server watches that root, so without this
858
- it sees each write as a project file changing and full-reloads the page — which reconnects the
859
- SDK, which produces the next write. The loop runs several times a second and looks like anything
860
- except what it is: refs go stale, actions die mid-flight, and the session appears to flap.
861
- It is a RegExp rather than a glob on purpose — chokidar dropped glob support in v4, so a
862
- double-star pattern here is accepted and matches nothing.
863
-
864
- The package is @reticlehq/browser — the framework-neutral sensor. DOM, network, console, routing and
865
- source file:line all work in Vue. What you do not get is React component identity, which is the only
866
- thing the React adapter adds. There is no Nuxt app in this project's CI, so this path is UNVERIFIED:
867
- if something does not work, please open an issue.`;
868
- }
847
+ export { nuxtPluginPath, nuxtPluginFile, NUXT_PLUGIN_NOTICE, nuxtManual } from './nuxt-snippets.js';
869
848
  /**
870
849
  * Root-level project config for Reticle. Written by `reticle init`; read by `reticle mcp` for the port
871
850
  * and by tooling for the stable projectId (the app's identity across port changes).