@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
@@ -1,242 +1,22 @@
1
1
  /**
2
- * How each framework gets wired: the Vite plugin, the three Next files, the SvelteKit client hook.
3
- * `plan.ts` is the plan's SHAPE (statuses, ordering, the agent/MCP steps); this is the per-framework
4
- * detail, and they grow for different reasons.
2
+ * How each framework gets wired: the three Next files, the SvelteKit client hook, CRA, Nuxt, Astro
3
+ * and the rest. `plan.ts` is the plan's SHAPE (statuses, ordering, the agent/MCP steps); this is the
4
+ * per-framework detail, and they grow for different reasons. The Vite plugin and the capabilities
5
+ * module every Vite-based stack shares live in `plan-vite.ts`.
5
6
  */
6
7
  import { bridgeWsUrl } from '@reticlehq/core';
7
- import { patchViteConfig, VitePatchKind, VITE_IMPORT } from '../patch/vite-config.js';
8
8
  import { patchNextConfig, patchRootLayout, patchPagesApp } from '../patch/next-patch.js';
9
- import { ASTRO_ENV_DTS_PATH, patchAstroConfig, patchAstroEnvDts, patchAstroLayout, } from '../patch/astro-patch.js';
9
+ import { ASTRO_ENV_DTS_PATH, ASTRO_RETICLE_DEV_PATH, astroReticleDevFile, patchAstroConfig, patchAstroEnvDts, patchAstroLayout, } from '../patch/astro-patch.js';
10
10
  import { CRA_DEV_MODULE_IMPORT, CRA_ENV_PATH, CRA_TOKEN_PER_MACHINE_NOTICE, TOKEN_VAR, craDevModuleFile, craDevModulePath, craEnvPatch, craImportPatch, } from '../patch/cra.js';
11
11
  import { PatchKind } from '../patch/patch-kind.js';
12
- import { viteManual, NEXT_LAYOUT_MANUAL, NEXT_LAYOUT_PATH, viteDevModuleFile, VITE_DEV_MODULE_PATH, nextReticleDevFile, NEXT_RETICLE_DEV_PATH, nextConfigManual, svelteKitHooksFile, SVELTEKIT_HOOKS_PATH, UNVERIFIED_FRAMEWORK_NOTE, astroManual, nuxtManual, nuxtPluginFile, nuxtPluginPath, NUXT_PLUGIN_NOTICE, webpack4TranspileNote, WEBPACK4_REACT_SCRIPTS_MAJOR, reactRouterEntryFile, reactRouterEntryPatch, REACT_ROUTER_ENTRY_PATH, UNVERIFIED_TANSTACK_START_NOTE, htmlManual, } from '../patch/snippets.js';
13
- import { hasOptOut, OPT_OUT_MARKER } from '../detect/declared/init-opt-out.js';
12
+ import { NEXT_LAYOUT_MANUAL, NEXT_LAYOUT_PATH, nextReticleDevFile, NEXT_RETICLE_DEV_PATH, nextConfigManual, svelteKitHooksFile, SVELTEKIT_HOOKS_PATH, SVELTEKIT_SETUP_GATED_NOTE, astroManual, nuxtManual, nuxtPluginFile, nuxtPluginPath, NUXT_PLUGIN_NOTICE, webpack4TranspileNote, WEBPACK4_REACT_SCRIPTS_MAJOR, reactRouterEntryFile, reactRouterEntryPatch, REACT_ROUTER_ENTRY_PATH, UNVERIFIED_TANSTACK_START_NOTE, htmlManual, } from '../patch/snippets.js';
14
13
  import { StepStatus } from './plan-types.js';
15
- import { RETICLE_DEFAULT_PORT } from '@reticlehq/core';
16
- import { CSP_STEP_TITLE } from '../diagnose/csp-check.js';
17
14
  import { StepTitle } from './connect-steps.js';
18
- import { tanstackStartManual, TANSTACK_START_ROOT_PATH } from '../patch/tanstack-start.js';
19
- import { diagnoseWebCsp } from '../diagnose/csp-doctor.js';
15
+ import { patchTanstackStartRoot, tanstackStartConnectFile, tanstackStartConnectPath, tanstackStartManual, TANSTACK_START_ROOT_PATH, } from '../patch/tanstack-start.js';
20
16
  import { patchNuxtConfig } from '../patch/nuxt-patch.js';
21
- /** What adding `reticle()` to a Vite config buys, which differs by framework. */
22
- export const VITE_PLUGIN_DETAIL = {
23
- /** A plain Vite app gets both halves from the plugin. */
24
- VITE: 'add reticle() to plugins (also injects connect())',
25
- /**
26
- * SvelteKit renders through app.html, so the plugin's HTML injection never fires and connect()
27
- * comes from the client hook instead. The plugin is still required: it is what stamps
28
- * data-reticle-source into .svelte components, and without it every verdict on a SvelteKit app
29
- * comes back with no file:line at all.
30
- */
31
- SVELTEKIT: 'add reticle() to plugins (stamps data-reticle-source in .svelte components)',
32
- /**
33
- * React Router framework mode renders through its own request handler, so the plugin's HTML
34
- * injection never fires and connect() comes from the client entry instead. The plugin is still
35
- * required for the same reason it is under SvelteKit: without it every verdict on the app comes
36
- * back with no file:line at all.
37
- */
38
- REACT_ROUTER: 'add reticle() to plugins (stamps data-reticle-source in .tsx components)',
39
- /**
40
- * TanStack Start SSRs its own HTML, so the plugin's HTML injection never fires and connect()
41
- * comes from a client effect instead. `inject: false` is load-bearing honesty: leaving the
42
- * default would keep reporting "also injects connect()" for a transform that never runs.
43
- * The plugin is still required for the same reason it is under SvelteKit: without it every
44
- * verdict on the app comes back with no file:line at all.
45
- */
46
- TANSTACK_START: 'add reticle({ inject: false }) to plugins (stamps data-reticle-source; Start SSRs its own HTML so connect() cannot come from the plugin)',
47
- };
48
- const CAPABILITIES_TITLE = StepTitle.CAPABILITIES;
49
- /**
50
- * The one step `init` genuinely cannot finish, addressed to the AGENT reading this report.
51
- *
52
- * It used to read "Capabilities are empty until you edit that file" and describe the consequence to
53
- * a human — homework, in a report a human mostly does not read, naming no file to open and no thing
54
- * to look for. The agent that ran `init` is right there, has the codebase, and can finish this in
55
- * the same minute; what it needed was the specific instruction, not the rationale.
56
- *
57
- * What is left is exactly what could not be PROVEN from source: which atoms/actor/queryClient an
58
- * adapter-wrapped store should be built from, and which elements matter enough to name. `init`
59
- * emits nothing it cannot prove, because a wrong import here throws on every dev page load — worse
60
- * than an empty file.
61
- */
62
- const CAPABILITIES_TODO_TITLE = StepTitle.CAPABILITIES_TODO;
63
- function capabilitiesTodo(path, stores) {
64
- return (`This app's state lives in a library nothing in the running app points at, so it is the one ` +
65
- `thing Reticle cannot find for itself — reticle_state will not see it until you say where it ` +
66
- `is.\n` +
67
- `Do this now, before driving anything:\n` +
68
- ` 1. In ${path}, uncomment the suggested line and complete it: ${stores.join('; ')}.\n` +
69
- ` 2. Prove it: drive one flow and check reticle_state returns your key. An empty state read ` +
70
- `means the line is still not doing anything, whatever else went green.`);
71
- }
72
- /**
73
- * The dev module carrying `registerCapabilities` / `registerStore`.
74
- *
75
- * Without it every app came up `hasCapabilities: false` with a `reticle_state` holding nothing but
76
- * `__reticle_renders` — the state-truth read was unavailable on every app out of the box. Written
77
- * only when absent, because it is the one generated file a user is expected to EDIT.
78
- */
79
- /**
80
- * Is there state left that ONLY the app can hand over?
81
- *
82
- * This used to ask a much bigger question — "does the generated file register anything at all?" —
83
- * and fired on almost every install, because almost every install generated a file whose testids
84
- * and stores were both empty. The answer was homework: go read the source, uncomment a line, add
85
- * data-testid attributes, then drive to prove it. Several turns, on every onboarding.
86
- *
87
- * Two of those three are no longer anyone's homework. Testids are read from the live DOM, and a
88
- * store passed through a React context provider (Redux, TanStack Query) is discovered and
89
- * registered on the first commit. What is left is the genuinely unreachable case: a module-scope
90
- * store, or one needing an argument only the source supplies — Zustand, Jotai atoms, an XState
91
- * actor. Nothing in the running app points at those, so the notice still has to fire, and it now
92
- * fires ONLY there.
93
- *
94
- * `wired` are stores init resolved and wrote a live `registerStore` call for. Hints are not
95
- * registrations: they land in the file as a commented line, and counting one as a registration
96
- * silences the notice whose whole purpose is to say "act on the hint".
97
- */
98
- function needsManualStore(hints, wired) {
99
- return hints.length > 0 && 0 === wired.length;
100
- }
101
- export function capabilitiesStep(input, path = VITE_DEV_MODULE_PATH) {
102
- if (true === input.viteDevModuleExists) {
103
- return [
104
- {
105
- title: CAPABILITIES_TITLE,
106
- target: path,
107
- status: StepStatus.ALREADY,
108
- detail: 'file exists, left alone, it is yours to edit',
109
- },
110
- ];
111
- }
112
- const testids = input.testids ?? [];
113
- const stores = input.storeHints ?? [];
114
- const wired = input.foundStores ?? [];
115
- // Testids no longer need counting here. They are read from the live DOM at announce time, so a
116
- // number printed at install time would be a stale claim about a codebase that is about to change —
117
- // which is exactly what "no data-testid values yet" used to be on an app whose testids arrive with
118
- // a lazy route.
119
- const found = 'testids read from the live DOM';
120
- // A store we found and WIRED is a registration, so the notice must not fire. A store HINT is not:
121
- // `stores` holds suggestions, written into the file as a commented line of the form
122
- // `// import your store, then: registerStore(...)`. Counting a suggestion as a registration let
123
- // the hint silence the notice whose entire job is to say "act on the hint".
124
- //
125
- // Measured against a real product UI (rowy — 70+ deps, jotai, a whole src/atoms tree): init
126
- // detected jotai, offered one commented line, emitted NO notice, and wrote
127
- // `registerCapabilities({ testids: [], signals: [], stores: [] })`. So `hasCapabilities` stayed
128
- // false, `reticle_state` was empty forever, and the install gate reported "connected: 1,
129
- // manual ⚠: none". Every check green, state observability zero.
130
- //
131
- // jotai is still exactly that case. A Redux or TanStack app is not, any more: its store rides a
132
- // context provider and the React adapter registers it on the first commit, so `storeHints` no
133
- // longer names either one and this notice no longer fires for them.
134
- const nothingToRegister = needsManualStore(stores, wired);
135
- return [
136
- {
137
- title: CAPABILITIES_TITLE,
138
- target: path,
139
- status: StepStatus.APPLY,
140
- detail: `${found}; ${wired.length > 0
141
- ? `registered ${wired.map((s) => `'${s.key}'`).join(', ')} from your source`
142
- : stores.length > 0
143
- ? `store: uncomment the ${String(stores.length)} suggested line(s)`
144
- : 'no state library detected'}`,
145
- write: {
146
- path,
147
- content: viteDevModuleFile(testids, stores, wired, input.detection.uiLibrary),
148
- },
149
- dependsOnInstall: true,
150
- },
151
- // The write is real; what it registers is not. `registerCapabilities({ testids: [], signals: [],
152
- // stores: [] })` registers nothing, so `hasCapabilities` stays false — correctly — and the ✓
153
- // above reads as if the feature is on. Reported as exactly that confusion: "hasCapabilities false
154
- // on every session while init said ✓ Capabilities + store".
155
- //
156
- // Beside the write rather than replacing it, for two reasons: only APPLY steps are written
157
- // (run.ts), and SKILL.md tells the reader to skip ✓ lines — so a caveat carried on the ✓ is a
158
- // caveat nobody reads.
159
- ...(nothingToRegister
160
- ? [
161
- {
162
- title: CAPABILITIES_TODO_TITLE,
163
- target: path,
164
- status: StepStatus.NOTICE,
165
- detail: capabilitiesTodo(path, stores),
166
- },
167
- ]
168
- : []),
169
- ];
170
- }
171
- export function viteSteps(input, detail = VITE_PLUGIN_DETAIL.VITE, inject = true) {
172
- // Capabilities are independent of whether the config needed patching. Attaching them to the APPLY
173
- // branch meant a re-run on an already-wired app silently never created the module.
174
- return [...viteConfigSteps(input, detail, inject), ...capabilitiesStep(input)];
175
- }
176
- function viteConfigSteps(input, detail, inject = true) {
177
- const cfg = input.viteConfig;
178
- const port = input.options.port;
179
- // Stamp `data-reticle-source` unless this app renders through a non-DOM React reconciler, where a
180
- // lowercase JSX tag is not an element and the stamp crashes the app at commit time. See
181
- // `Detection.customReconciler`.
182
- const stampSource = true !== input.detection?.customReconciler;
183
- // An explicit "not here" is not an invitation. `init` runs unattended in a repo it has just met,
184
- // and it was reported adding the plugin to an app whose config said Reticle was deliberately
185
- // excluded. A NOTICE rather than a ⚠: opting out is a decision, not something to go and fix.
186
- if (cfg !== null && hasOptOut(cfg.source)) {
187
- return [
188
- {
189
- title: StepTitle.VITE_PLUGIN,
190
- target: cfg.path,
191
- status: StepStatus.NOTICE,
192
- detail: `left alone: this config carries ${OPT_OUT_MARKER}. Remove that marker to instrument this app.`,
193
- },
194
- ];
195
- }
196
- if (null === cfg) {
197
- return [
198
- {
199
- title: StepTitle.VITE_PLUGIN,
200
- target: 'vite.config',
201
- status: StepStatus.MANUAL,
202
- detail: viteManual(port, input.detection.uiLibrary, inject, stampSource),
203
- },
204
- ];
205
- }
206
- const patch = patchViteConfig(cfg.source, port, true === input.captureBodies, inject, stampSource);
207
- if (patch.kind === VitePatchKind.ALREADY) {
208
- return [
209
- {
210
- title: StepTitle.VITE_PLUGIN,
211
- target: cfg.path,
212
- status: StepStatus.ALREADY,
213
- detail: 'reticle() already in plugins',
214
- },
215
- ];
216
- }
217
- if (patch.kind === VitePatchKind.MANUAL) {
218
- return [
219
- {
220
- title: StepTitle.VITE_PLUGIN,
221
- target: cfg.path,
222
- status: StepStatus.MANUAL,
223
- detail: `${patch.reason}\n\n${viteManual(port, input.detection.uiLibrary, inject, stampSource)}`,
224
- },
225
- ];
226
- }
227
- return [
228
- {
229
- title: StepTitle.VITE_PLUGIN,
230
- target: cfg.path,
231
- status: StepStatus.APPLY,
232
- detail,
233
- // Both halves, because either alone is a config that does not wire: the import without the
234
- // call leaves `plugins` untouched, and the call without the import does not build.
235
- write: { path: cfg.path, content: patch.code, expect: [VITE_IMPORT, 'reticle('] },
236
- dependsOnInstall: true,
237
- },
238
- ];
239
- }
17
+ import { alreadyOrMovedPort } from './port-steps.js';
18
+ import { capabilitiesTodo, needsManualStore } from './plan-vite.js';
19
+ import { HTML_INDEX_PATH, hasStaticSnippet } from '../patch/static-page.js';
240
20
  /**
241
21
  * Turn a conservative source patch into a step: applied when it patched, already when the wiring is
242
22
  * there, and the hand-edit instructions when the file shape wasn't one we recognise.
@@ -299,12 +79,12 @@ export function nextSteps(input) {
299
79
  status: StepStatus.MANUAL,
300
80
  detail: NEXT_DEV_FILE_STALE_DETAIL,
301
81
  }
302
- : {
82
+ : alreadyOrMovedPort({
303
83
  title: StepTitle.RETICLE_DEV_COMPONENT,
304
84
  target: devPath,
305
85
  status: StepStatus.ALREADY,
306
86
  detail: 'file exists',
307
- }
87
+ }, input.nextReticleDevSource, input.options.port)
308
88
  : {
309
89
  title: StepTitle.RETICLE_DEV_COMPONENT,
310
90
  target: devPath,
@@ -337,7 +117,7 @@ export function nextSteps(input) {
337
117
  needsManualStore(input.storeHints ?? [], input.nextFoundStores ?? [])
338
118
  ? [
339
119
  {
340
- title: CAPABILITIES_TODO_TITLE,
120
+ title: StepTitle.CAPABILITIES_TODO,
341
121
  target: devPath,
342
122
  status: StepStatus.NOTICE,
343
123
  detail: capabilitiesTodo(devPath, input.storeHints ?? []),
@@ -494,7 +274,7 @@ export function nuxtSteps(input) {
494
274
  ];
495
275
  if (null === config)
496
276
  return manual;
497
- const configPatch = patchNuxtConfig(config.source);
277
+ const configPatch = patchNuxtConfig(config.source, input.viteMajor ?? null);
498
278
  if (configPatch.kind === PatchKind.MANUAL)
499
279
  return manual;
500
280
  const notice = {
@@ -506,12 +286,12 @@ export function nuxtSteps(input) {
506
286
  // Never overwritten: this file is the one an app owner edits — their registered stores, their own
507
287
  // capabilities. Rewriting it would take those with it.
508
288
  const plugin = true === input.nuxtPluginExists
509
- ? {
289
+ ? alreadyOrMovedPort({
510
290
  title: StepTitle.CONNECT_SNIPPET_NUXT,
511
291
  target: pluginPath,
512
292
  status: StepStatus.ALREADY,
513
293
  detail: 'file exists',
514
- }
294
+ }, input.nuxtPluginSource, input.options.port)
515
295
  : {
516
296
  title: StepTitle.CONNECT_SNIPPET_NUXT,
517
297
  target: pluginPath,
@@ -526,7 +306,11 @@ export function nuxtSteps(input) {
526
306
  return [
527
307
  plugin,
528
308
  patchStep(StepTitle.NUXT_CONFIG, config.path, configPatch, 'inline the pairing token and keep the journal out of the watcher', nuxtManual(input.options.port, input.options.projectId)),
529
- notice,
309
+ // Only when this run changed something Nuxt reads at startup. A re-run that found everything in
310
+ // place, attached to the running server and connected was still told to restart it.
311
+ ...(StepStatus.ALREADY === plugin.status && PatchKind.ALREADY === configPatch.kind
312
+ ? []
313
+ : [notice]),
530
314
  ];
531
315
  }
532
316
  /**
@@ -542,18 +326,21 @@ export function nuxtSteps(input) {
542
326
  * would replace that default with one that never hydrates. When it is present, one line is added to
543
327
  * it and nothing else is touched.
544
328
  */
545
- export function reactRouterSteps(input) {
329
+ export function reactRouterSteps(input,
330
+ // Remix v2 is the same framework under its old name and takes the same entry, with its own
331
+ // default hydration (`RemixBrowser`) and its own title — see plan-remix.ts.
332
+ title = StepTitle.CONNECT_SNIPPET_REACT_ROUTER, newEntry = reactRouterEntryFile) {
546
333
  const detail = 'connect from the client entry — framework mode renders HTML through its own request handler, ' +
547
334
  "so the Vite plugin's index.html injection never fires";
548
335
  const existing = input.reactRouterEntrySource ?? null;
549
336
  if (true !== input.reactRouterEntryExists || null === existing) {
550
337
  return [
551
338
  {
552
- title: StepTitle.CONNECT_SNIPPET_REACT_ROUTER,
339
+ title,
553
340
  target: REACT_ROUTER_ENTRY_PATH,
554
341
  status: StepStatus.APPLY,
555
342
  detail: `create the dev-only ${detail}`,
556
- write: { path: REACT_ROUTER_ENTRY_PATH, content: reactRouterEntryFile() },
343
+ write: { path: REACT_ROUTER_ENTRY_PATH, content: newEntry() },
557
344
  dependsOnInstall: true,
558
345
  },
559
346
  ];
@@ -562,13 +349,13 @@ export function reactRouterSteps(input) {
562
349
  return [
563
350
  null === patched
564
351
  ? {
565
- title: StepTitle.CONNECT_SNIPPET_REACT_ROUTER,
352
+ title,
566
353
  target: REACT_ROUTER_ENTRY_PATH,
567
354
  status: StepStatus.ALREADY,
568
355
  detail: 'already imported',
569
356
  }
570
357
  : {
571
- title: StepTitle.CONNECT_SNIPPET_REACT_ROUTER,
358
+ title,
572
359
  target: REACT_ROUTER_ENTRY_PATH,
573
360
  status: StepStatus.APPLY,
574
361
  detail: `add one line to ${detail}`,
@@ -578,46 +365,62 @@ export function reactRouterSteps(input) {
578
365
  ];
579
366
  }
580
367
  /**
581
- * TanStack Start: the client-document connect, printed rather than written.
368
+ * TanStack Start: a client connect component, written, and rendered in the root document.
582
369
  *
583
- * `src/routes/__root.tsx` is the document Start SSRs. A static import of the SDK on that module
584
- * 500s, so writing one is worse than a documented manual step. See `tanstackStartManual`.
370
+ * ATOMIC, like Astro: the component on disk with nothing rendering it is a guaranteed
371
+ * non-connection, so when the root cannot be patched BOTH halves go back to the recipe.
585
372
  */
586
373
  export function tanstackStartSteps(input) {
587
374
  const root = input.tanstackStartRoot ?? TANSTACK_START_ROOT_PATH;
588
- return [
589
- {
590
- title: StepTitle.TANSTACK_START_UNVERIFIED,
591
- target: root,
592
- status: StepStatus.NOTICE,
593
- detail: UNVERIFIED_TANSTACK_START_NOTE,
594
- },
595
- {
596
- title: StepTitle.CONNECT_SNIPPET_TANSTACK_START,
597
- target: root,
598
- status: StepStatus.MANUAL,
599
- detail: tanstackStartManual(input.options.port, input.options.projectId, root),
600
- },
601
- ];
375
+ const unverified = {
376
+ title: StepTitle.TANSTACK_START_UNVERIFIED,
377
+ target: root,
378
+ status: StepStatus.NOTICE,
379
+ detail: UNVERIFIED_TANSTACK_START_NOTE,
380
+ };
381
+ const recipe = tanstackStartManual(input.options.port, input.options.projectId, root);
382
+ const source = input.tanstackStartRootSource ?? null;
383
+ const patch = null === source
384
+ ? { kind: PatchKind.MANUAL, reason: `could not read ${root}` }
385
+ : patchTanstackStartRoot(source, root);
386
+ const rendered = patchStep(StepTitle.CONNECT_SNIPPET_TANSTACK_START, root, patch, 'render <ReticleConnect /> before <Scripts /> in the root document', recipe);
387
+ if (PatchKind.MANUAL === patch.kind)
388
+ return [unverified, rendered];
389
+ const title = StepTitle.TANSTACK_START_CONNECT_COMPONENT;
390
+ const path = tanstackStartConnectPath(root);
391
+ const content = tanstackStartConnectFile(input.options.port, input.options.projectId, root);
392
+ const component = true === input.tanstackStartConnectExists
393
+ ? // "file exists" used to end it without reading the port inside, so a re-run with a new
394
+ // `--port` left the page dialling the daemon that had moved.
395
+ alreadyOrMovedPort({ title, target: path, status: StepStatus.ALREADY, detail: 'file exists' }, input.tanstackStartConnectSource, input.options.port)
396
+ : {
397
+ title,
398
+ target: path,
399
+ status: StepStatus.APPLY,
400
+ detail: 'create the dev-only client connect (Start SSRs its own HTML)',
401
+ write: { path, content },
402
+ dependsOnInstall: true,
403
+ };
404
+ return [unverified, component, rendered];
602
405
  }
603
406
  export function svelteKitSteps(input) {
604
407
  const unverified = {
605
- title: StepTitle.SVELTEKIT_UNVERIFIED,
408
+ title: StepTitle.SVELTEKIT_SETUP_GATED,
606
409
  target: SVELTEKIT_HOOKS_PATH,
607
410
  status: StepStatus.NOTICE,
608
- detail: UNVERIFIED_FRAMEWORK_NOTE,
411
+ detail: SVELTEKIT_SETUP_GATED_NOTE,
609
412
  };
610
413
  // SvelteKit can't use the Vite-plugin injection (it renders via app.html) — wire a client hook
611
414
  // that SvelteKit runs on startup, which is the path that can register a session at all.
612
415
  if (true === input.svelteKitHooksExists) {
613
416
  return [
614
417
  unverified,
615
- {
418
+ alreadyOrMovedPort({
616
419
  title: StepTitle.CLIENT_HOOK,
617
420
  target: SVELTEKIT_HOOKS_PATH,
618
421
  status: StepStatus.ALREADY,
619
422
  detail: 'file exists',
620
- },
423
+ }, input.svelteKitHooksSource, input.options.port),
621
424
  ];
622
425
  }
623
426
  return [
@@ -636,7 +439,7 @@ export function svelteKitSteps(input) {
636
439
  ];
637
440
  }
638
441
  /**
639
- * Astro: the config define + build target, and the connect script in ONE layout.
442
+ * Astro: the config build target, and the connect script (with the pairing token) in ONE layout.
640
443
  *
641
444
  * Astro was the last gated stack left printing a recipe it did not apply. It still falls back to the
642
445
  * printed one whenever the choice is not obvious — no config, no single layout, or a shape the
@@ -659,14 +462,15 @@ export function astroSteps(input) {
659
462
  },
660
463
  ];
661
464
  }
662
- // ATOMIC. The connect snippet is useless without the config: the token is inlined by the config,
663
- // so a layout patched on its own gives an app that dials the bridge and is refused. Measured on a
664
- // real fixture — config ⚠, layout ✓ — which reads as one step done and one caveat when it is
665
- // actually a guaranteed non-connection. If either half cannot be applied, BOTH go manual with the
666
- // single recipe that does the whole job.
465
+ // ATOMIC. The connect snippet is useless without the config: without `build.target: 'es2022'`
466
+ // Astro down-levels the SDK and the dynamic import dies, and without `optimizeDeps.include` the
467
+ // first load 404s the hashed module. The token itself now lives in the layout frontmatter (#1008),
468
+ // but a layout patched on its own still cannot connect. Measured on a real fixture — config ⚠,
469
+ // layout ✓ — which reads as one step done and one caveat when it is actually a guaranteed
470
+ // non-connection. If either half cannot be applied, BOTH go manual with the single recipe.
667
471
  const manualWithLayout = astroManual(input.options.port, input.options.projectId, layout.path);
668
- const configPatch = patchAstroConfig(config.source);
669
- const layoutPatch = patchAstroLayout(layout.source, input.options.port, input.options.projectId, input.detection.uiLibrary, input.testids ?? []);
472
+ const configPatch = patchAstroConfig(config.source, input.viteMajor ?? null);
473
+ const layoutPatch = patchAstroLayout(layout.source, layout.path);
670
474
  if (configPatch.kind === PatchKind.MANUAL || layoutPatch.kind === PatchKind.MANUAL) {
671
475
  return [
672
476
  {
@@ -678,60 +482,34 @@ export function astroSteps(input) {
678
482
  ];
679
483
  }
680
484
  const envPatch = patchAstroEnvDts(input.astroEnvDts ?? null);
485
+ const existingDev = input.astroReticleDev ?? null;
486
+ const devAlready = 'string' === typeof existingDev && existingDev.includes('reticle.connect');
487
+ const devStep = devAlready
488
+ ? {
489
+ title: StepTitle.ASTRO_RETICLE_DEV,
490
+ target: ASTRO_RETICLE_DEV_PATH,
491
+ status: StepStatus.ALREADY,
492
+ detail: 'file exists, left alone',
493
+ }
494
+ : {
495
+ title: StepTitle.ASTRO_RETICLE_DEV,
496
+ target: ASTRO_RETICLE_DEV_PATH,
497
+ status: StepStatus.APPLY,
498
+ detail: 'write the local connect module (static SDK import, token from <meta>)',
499
+ write: {
500
+ path: ASTRO_RETICLE_DEV_PATH,
501
+ content: astroReticleDevFile(input.options.port, input.options.projectId, input.detection.uiLibrary, input.testids ?? []),
502
+ },
503
+ dependsOnInstall: true,
504
+ };
681
505
  return [
682
- patchStep(StepTitle.ASTRO_CONFIG, config.path, configPatch, 'inline the pairing token and raise build.target to es2022', manualWithLayout),
683
- patchStep(StepTitle.CONNECT_SNIPPET_ASTRO, layout.path, layoutPatch, 'add the dev-only connect <script> before </body>', manualWithLayout),
506
+ patchStep(StepTitle.ASTRO_CONFIG, config.path, configPatch, 'raise build.target to es2022 and keep .reticle/ out of the watcher', manualWithLayout),
507
+ patchStep(StepTitle.CONNECT_SNIPPET_ASTRO, layout.path, layoutPatch, 'add the pairing-token <meta> and a script that statically imports ReticleDev', manualWithLayout),
508
+ devStep,
684
509
  // Declares the Vite define names so `astro check` (create-astro's default build) can see them
685
510
  // (#677). Independent of the two halves above: even an ALREADY config/layout still needs this
686
511
  // when the env file was never written.
687
- patchStep(StepTitle.ASTRO_ENV_DTS, ASTRO_ENV_DTS_PATH, envPatch, 'declare __RETICLE_TOKEN__ / __RETICLE_ROOT__ for astro check', manualWithLayout),
688
- ];
689
- }
690
- /**
691
- * A Content-Security-Policy whose `connect-src` excludes the bridge, said out loud at install time.
692
- *
693
- * Both reported cases were Next apps where `init` printed success for every step and the app then
694
- * never connected: the browser blocked the WebSocket and reported it in its own console, which
695
- * nothing on this side reads. A NOTICE rather than a step, because editing someone's security policy
696
- * is theirs to do — but it carries the exact text to paste, which is the difference between a
697
- * warning and a fix.
698
- */
699
- export function cspStep(input) {
700
- // Reads the SAME list `reticle doctor` reads. It used to read a hand-written pair —
701
- // `[nextConfigSource, nextLayout?.source]` — while csp-doctor.ts already carried the full set
702
- // including `index.html`, which is where every Vite and Electron app declares its policy. So the
703
- // command you run BEFORE anything works checked less than the one you run after it has failed.
704
- //
705
- // Measured on MarkText, a production Electron editor: its renderer sets `default-src 'self'` with
706
- // no `connect-src`, the browser blocked the bridge WebSocket, and `init` printed a clean plan. The
707
- // daemon cannot see a dial that never left the page, so nothing anywhere said why — and the check
708
- // that would have said it was sitting one import away.
709
- // The pre-read Next sources are folded in ON TOP of the shared list, not replaced by it: init
710
- // resolves `nextConfigFile` across more spellings than CSP_FILES names (`.mts`, for one), and a
711
- // layout is found by search rather than by a fixed path. Two sources of the same truth is the
712
- // problem being fixed here — one of them being a SUPERSET is not.
713
- const extra = {
714
- ...(input.nextConfigFile !== null && input.nextConfigFile !== undefined
715
- ? { [input.nextConfigFile]: input.nextConfigSource ?? undefined }
716
- : {}),
717
- ...(input.nextLayout ? { [input.nextLayout.path]: input.nextLayout.source } : {}),
718
- };
719
- const read = (file) => extra[file] ?? input.cspSources?.[file];
720
- const findings = diagnoseWebCsp(read, input.options.port ?? RETICLE_DEFAULT_PORT, [
721
- ...Object.keys(extra),
722
- ]);
723
- const first = findings[0];
724
- if (first === undefined)
725
- return [];
726
- return [
727
- {
728
- title: CSP_STEP_TITLE,
729
- target: first.file,
730
- status: StepStatus.NOTICE,
731
- // `problem` already ends with the text to paste; `fix` is the same sentence for callers that
732
- // want it on its own (doctor renders them separately). Printing both said it twice.
733
- detail: first.problem,
734
- },
512
+ patchStep(StepTitle.ASTRO_ENV_DTS, ASTRO_ENV_DTS_PATH, envPatch, 'declare window.__RETICLE_TOKEN__ / __RETICLE_ROOT__ for astro check', manualWithLayout),
735
513
  ];
736
514
  }
737
515
  /**
@@ -742,12 +520,25 @@ export function cspStep(input) {
742
520
  * where the if/else chain this replaced used to hand it these instructions silently.
743
521
  */
744
522
  export function htmlSteps(input) {
523
+ // Read the page before calling the step undone. It used to be MANUAL unconditionally, so a re-run
524
+ // over a page that already connects said "This app will NOT connect until the ⚠ step".
525
+ const index = input.htmlIndexSource ?? null;
526
+ if (null !== index && hasStaticSnippet(index)) {
527
+ return [
528
+ alreadyOrMovedPort({
529
+ title: StepTitle.CONNECT_SNIPPET,
530
+ target: HTML_INDEX_PATH,
531
+ status: StepStatus.ALREADY,
532
+ detail: 'index.html already loads the Reticle SDK',
533
+ }, index, input.options.port),
534
+ ];
535
+ }
745
536
  return [
746
537
  {
747
538
  title: StepTitle.CONNECT_SNIPPET,
748
- target: 'index.html',
539
+ target: HTML_INDEX_PATH,
749
540
  status: StepStatus.MANUAL,
750
- detail: htmlManual(input.options.port, input.options.projectId, input.pairingToken),
541
+ detail: htmlManual(input.options.port, input.options.projectId, input.pairingToken, input.detection.uiLibrary),
751
542
  },
752
543
  ];
753
544
  }