@reticlehq/init 3.3.0 → 3.5.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 (84) 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 +45 -2
  4. package/dist/detect/detect.js +76 -5
  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.js +28 -2
  12. package/dist/diagnose/containerised-dev-server.d.ts +12 -4
  13. package/dist/diagnose/containerised-dev-server.js +36 -4
  14. package/dist/diagnose/csp-check.d.ts +12 -6
  15. package/dist/diagnose/csp-check.js +61 -13
  16. package/dist/diagnose/csp-doctor.d.ts +13 -1
  17. package/dist/diagnose/csp-doctor.js +2 -2
  18. package/dist/diagnose/csp-step.d.ts +27 -0
  19. package/dist/diagnose/csp-step.js +86 -0
  20. package/dist/diagnose/desktop-doctor.js +8 -2
  21. package/dist/diagnose/init-failure.d.ts +7 -0
  22. package/dist/diagnose/init-failure.js +7 -0
  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 +16 -1
  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/index.d.ts +6 -2
  30. package/dist/index.js +6 -2
  31. package/dist/no-package-json.d.ts +10 -0
  32. package/dist/no-package-json.js +131 -0
  33. package/dist/node-io.d.ts +12 -6
  34. package/dist/node-io.js +33 -11
  35. package/dist/patch/angular.d.ts +68 -0
  36. package/dist/patch/angular.js +216 -0
  37. package/dist/patch/astro-patch.d.ts +1 -1
  38. package/dist/patch/astro-patch.js +2 -2
  39. package/dist/patch/cra.d.ts +0 -2
  40. package/dist/patch/cra.js +0 -2
  41. package/dist/patch/electron-patch.d.ts +10 -1
  42. package/dist/patch/electron-patch.js +79 -27
  43. package/dist/patch/electron-vite-patch.js +1 -1
  44. package/dist/patch/nuxt-patch.d.ts +1 -1
  45. package/dist/patch/nuxt-patch.js +2 -2
  46. package/dist/patch/nuxt-snippets.js +6 -6
  47. package/dist/patch/remix.d.ts +24 -0
  48. package/dist/patch/remix.js +59 -0
  49. package/dist/patch/snippets.d.ts +57 -9
  50. package/dist/patch/snippets.js +119 -28
  51. package/dist/patch/static-page.d.ts +39 -0
  52. package/dist/patch/static-page.js +102 -0
  53. package/dist/patch/tanstack-start.d.ts +36 -8
  54. package/dist/patch/tanstack-start.js +118 -29
  55. package/dist/patch/vite-config.d.ts +5 -0
  56. package/dist/patch/vite-config.js +8 -0
  57. package/dist/patch/vite-owning-config.d.ts +34 -0
  58. package/dist/patch/vite-owning-config.js +63 -5
  59. package/dist/plan/connect-steps.d.ts +7 -1
  60. package/dist/plan/connect-steps.js +18 -1
  61. package/dist/plan/framework-adapter.d.ts +3 -1
  62. package/dist/plan/framework-adapter.js +99 -6
  63. package/dist/plan/notices.js +4 -3
  64. package/dist/plan/plan-angular.d.ts +6 -0
  65. package/dist/plan/plan-angular.js +89 -0
  66. package/dist/plan/plan-electron-vite.d.ts +6 -3
  67. package/dist/plan/plan-electron-vite.js +39 -8
  68. package/dist/plan/plan-framework.d.ts +8 -46
  69. package/dist/plan/plan-framework.js +81 -312
  70. package/dist/plan/plan-types.d.ts +29 -0
  71. package/dist/plan/plan-vite.d.ts +67 -0
  72. package/dist/plan/plan-vite.js +256 -0
  73. package/dist/plan/plan.js +38 -34
  74. package/dist/plan/port-steps.d.ts +45 -0
  75. package/dist/plan/port-steps.js +119 -0
  76. package/dist/plan/preflight.d.ts +17 -3
  77. package/dist/plan/preflight.js +53 -23
  78. package/dist/register/mcp-clients.js +5 -3
  79. package/dist/register/mcp.d.ts +18 -5
  80. package/dist/register/mcp.js +48 -8
  81. package/dist/run-types.d.ts +8 -0
  82. package/dist/run.js +118 -80
  83. package/package.json +3 -4
  84. 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
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 [
@@ -666,7 +469,7 @@ export function astroSteps(input) {
666
469
  // layout ✓ — which reads as one step done and one caveat when it is actually a guaranteed
667
470
  // non-connection. If either half cannot be applied, BOTH go manual with the single recipe.
668
471
  const manualWithLayout = astroManual(input.options.port, input.options.projectId, layout.path);
669
- const configPatch = patchAstroConfig(config.source);
472
+ const configPatch = patchAstroConfig(config.source, input.viteMajor ?? null);
670
473
  const layoutPatch = patchAstroLayout(layout.source, layout.path);
671
474
  if (configPatch.kind === PatchKind.MANUAL || layoutPatch.kind === PatchKind.MANUAL) {
672
475
  return [
@@ -709,53 +512,6 @@ export function astroSteps(input) {
709
512
  patchStep(StepTitle.ASTRO_ENV_DTS, ASTRO_ENV_DTS_PATH, envPatch, 'declare window.__RETICLE_TOKEN__ / __RETICLE_ROOT__ for astro check', manualWithLayout),
710
513
  ];
711
514
  }
712
- /**
713
- * A Content-Security-Policy whose `connect-src` excludes the bridge, said out loud at install time.
714
- *
715
- * Both reported cases were Next apps where `init` printed success for every step and the app then
716
- * never connected: the browser blocked the WebSocket and reported it in its own console, which
717
- * nothing on this side reads. A NOTICE rather than a step, because editing someone's security policy
718
- * is theirs to do — but it carries the exact text to paste, which is the difference between a
719
- * warning and a fix.
720
- */
721
- export function cspStep(input) {
722
- // Reads the SAME list `reticle doctor` reads. It used to read a hand-written pair —
723
- // `[nextConfigSource, nextLayout?.source]` — while csp-doctor.ts already carried the full set
724
- // including `index.html`, which is where every Vite and Electron app declares its policy. So the
725
- // command you run BEFORE anything works checked less than the one you run after it has failed.
726
- //
727
- // Measured on MarkText, a production Electron editor: its renderer sets `default-src 'self'` with
728
- // no `connect-src`, the browser blocked the bridge WebSocket, and `init` printed a clean plan. The
729
- // daemon cannot see a dial that never left the page, so nothing anywhere said why — and the check
730
- // that would have said it was sitting one import away.
731
- // The pre-read Next sources are folded in ON TOP of the shared list, not replaced by it: init
732
- // resolves `nextConfigFile` across more spellings than CSP_FILES names (`.mts`, for one), and a
733
- // layout is found by search rather than by a fixed path. Two sources of the same truth is the
734
- // problem being fixed here — one of them being a SUPERSET is not.
735
- const extra = {
736
- ...(input.nextConfigFile !== null && input.nextConfigFile !== undefined
737
- ? { [input.nextConfigFile]: input.nextConfigSource ?? undefined }
738
- : {}),
739
- ...(input.nextLayout ? { [input.nextLayout.path]: input.nextLayout.source } : {}),
740
- };
741
- const read = (file) => extra[file] ?? input.cspSources?.[file];
742
- const findings = diagnoseWebCsp(read, input.options.port ?? RETICLE_DEFAULT_PORT, [
743
- ...Object.keys(extra),
744
- ]);
745
- const first = findings[0];
746
- if (first === undefined)
747
- return [];
748
- return [
749
- {
750
- title: CSP_STEP_TITLE,
751
- target: first.file,
752
- status: StepStatus.NOTICE,
753
- // `problem` already ends with the text to paste; `fix` is the same sentence for callers that
754
- // want it on its own (doctor renders them separately). Printing both said it twice.
755
- detail: first.problem,
756
- },
757
- ];
758
- }
759
515
  /**
760
516
  * The plain-HTML path: no bundler to hook, so the connect snippet is printed for a hand edit.
761
517
  *
@@ -764,12 +520,25 @@ export function cspStep(input) {
764
520
  * where the if/else chain this replaced used to hand it these instructions silently.
765
521
  */
766
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
+ }
767
536
  return [
768
537
  {
769
538
  title: StepTitle.CONNECT_SNIPPET,
770
- target: 'index.html',
539
+ target: HTML_INDEX_PATH,
771
540
  status: StepStatus.MANUAL,
772
- detail: htmlManual(input.options.port, input.options.projectId, input.pairingToken),
541
+ detail: htmlManual(input.options.port, input.options.projectId, input.pairingToken, input.detection.uiLibrary),
773
542
  },
774
543
  ];
775
544
  }
@@ -226,6 +226,8 @@ export interface PlanInput {
226
226
  viteDevModuleExists?: boolean | undefined;
227
227
  /** Whether src/hooks.client.ts already exists (SvelteKit idempotency). */
228
228
  svelteKitHooksExists?: boolean;
229
+ /** Its content, so a re-run with a new `--port` can move the URL baked into it. */
230
+ svelteKitHooksSource?: string | null | undefined;
229
231
  /** Whether app/entry.client.tsx already exists — it decides whether init writes one or patches it. */
230
232
  reactRouterEntryExists?: boolean;
231
233
  /**
@@ -236,6 +238,8 @@ export interface PlanInput {
236
238
  * added to whatever the app already put in it. Existence alone could only ever print a recipe.
237
239
  */
238
240
  reactRouterEntrySource?: string | null | undefined;
241
+ /** The installed Vite's major, for the configs that own their Vite (see `installedViteMajor`). */
242
+ viteMajor?: number | null | undefined;
239
243
  /** Discovered Nuxt config: its path + source, or null. It is where the pairing token is inlined. */
240
244
  nuxtConfig?: {
241
245
  path: string;
@@ -245,6 +249,8 @@ export interface PlanInput {
245
249
  nuxtHasAppDir?: boolean | undefined;
246
250
  /** Whether the Nuxt client plugin already exists (idempotency — the file is the owner's to edit). */
247
251
  nuxtPluginExists?: boolean | undefined;
252
+ /** Its content, so a re-run with a new `--port` can move the URL baked into it. */
253
+ nuxtPluginSource?: string | null | undefined;
248
254
  /**
249
255
  * TanStack Start's document module, when found (`src/routes/__root.tsx` or `app/routes/__root.tsx`).
250
256
  *
@@ -252,6 +258,29 @@ export interface PlanInput {
252
258
  * 500s — but the path has to be the one that actually exists, not a guess.
253
259
  */
254
260
  tanstackStartRoot?: string | undefined;
261
+ /** Its content, so `init` can render the connect component in it rather than print a recipe. */
262
+ tanstackStartRootSource?: string | null | undefined;
263
+ /** Whether the connect component `init` writes beside `routes/` already exists (it is the owner's). */
264
+ tanstackStartConnectExists?: boolean | undefined;
265
+ /** Its content, so a re-run with a new `--port` can move the URL baked into it. */
266
+ tanstackStartConnectSource?: string | null | undefined;
267
+ /** `angular.json`, read so the serve target can be pointed at the token route. */
268
+ angularWorkspace?: {
269
+ path: string;
270
+ source: string;
271
+ } | null | undefined;
272
+ /** The browser entry `angular.json` names (`src/main.ts` by default) — where the connect goes. */
273
+ angularEntry?: {
274
+ path: string;
275
+ source: string;
276
+ } | null | undefined;
277
+ /** The generated `reticle.proxy.mjs`, when a previous run left one (idempotency). */
278
+ angularProxySource?: string | null | undefined;
279
+ /**
280
+ * The plain-HTML app's `index.html`, when it has one — so a re-run over a page that already
281
+ * carries the connect snippet reports it as done instead of "will NOT connect".
282
+ */
283
+ htmlIndexSource?: string | null | undefined;
255
284
  /** CRA's bundled entry (src/index.tsx or .js) — where the connect import has to go. */
256
285
  craEntry?: {
257
286
  path: string;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The Vite half of the plan: the `reticle()` plugin in a Vite config, and the dev module that
3
+ * carries `registerCapabilities` / `registerStore`.
4
+ *
5
+ * Split out of `plan-framework.ts`, which had grown to the edge of the line cap. It is its own
6
+ * module because every Vite-based stack (plain Vite, SvelteKit, React Router, Remix, TanStack
7
+ * Start, the Electron renderers) composes these steps, while the rest of `plan-framework.ts` is one
8
+ * builder per framework that has its own files to write.
9
+ */
10
+ import { type PlanInput, type Step } from './plan-types.js';
11
+ /** What adding `reticle()` to a Vite config buys, which differs by framework. */
12
+ export declare const VITE_PLUGIN_DETAIL: {
13
+ /** A plain Vite app gets both halves from the plugin. */
14
+ readonly VITE: "add reticle() to plugins (also injects connect())";
15
+ /**
16
+ * SvelteKit renders through app.html, so the plugin's HTML injection never fires and connect()
17
+ * comes from the client hook instead. The plugin is still required: it is what stamps
18
+ * data-reticle-source into .svelte components, and without it every verdict on a SvelteKit app
19
+ * comes back with no file:line at all.
20
+ */
21
+ readonly SVELTEKIT: "add reticle({ inject: false }) to plugins (stamps data-reticle-source in .svelte components; the client hook connects)";
22
+ /**
23
+ * React Router framework mode renders through its own request handler, so the plugin's HTML
24
+ * injection never fires and connect() comes from the client entry instead. The plugin is still
25
+ * required for the same reason it is under SvelteKit: without it every verdict on the app comes
26
+ * back with no file:line at all.
27
+ */
28
+ readonly REACT_ROUTER: "add reticle() to plugins (stamps data-reticle-source in .tsx components)";
29
+ /**
30
+ * TanStack Start SSRs its own HTML, so the plugin's HTML injection never fires and connect()
31
+ * comes from a client effect instead. `inject: false` is load-bearing honesty: leaving the
32
+ * default would keep reporting "also injects connect()" for a transform that never runs.
33
+ * The plugin is still required for the same reason it is under SvelteKit: without it every
34
+ * verdict on the app comes back with no file:line at all.
35
+ */
36
+ readonly TANSTACK_START: "add reticle({ inject: false }) to plugins (stamps data-reticle-source; Start SSRs its own HTML so connect() cannot come from the plugin)";
37
+ };
38
+ export declare function capabilitiesTodo(path: string, stores: readonly string[]): string;
39
+ /**
40
+ * The dev module carrying `registerCapabilities` / `registerStore`.
41
+ *
42
+ * Without it every app came up `hasCapabilities: false` with a `reticle_state` holding nothing but
43
+ * `__reticle_renders` — the state-truth read was unavailable on every app out of the box. Written
44
+ * only when absent, because it is the one generated file a user is expected to EDIT.
45
+ */
46
+ /**
47
+ * Is there state left that ONLY the app can hand over?
48
+ *
49
+ * This used to ask a much bigger question — "does the generated file register anything at all?" —
50
+ * and fired on almost every install, because almost every install generated a file whose testids
51
+ * and stores were both empty. The answer was homework: go read the source, uncomment a line, add
52
+ * data-testid attributes, then drive to prove it. Several turns, on every onboarding.
53
+ *
54
+ * Two of those three are no longer anyone's homework. Testids are read from the live DOM, and a
55
+ * store passed through a React context provider (Redux, TanStack Query) is discovered and
56
+ * registered on the first commit. What is left is the genuinely unreachable case: a module-scope
57
+ * store, or one needing an argument only the source supplies — Zustand, Jotai atoms, an XState
58
+ * actor. Nothing in the running app points at those, so the notice still has to fire, and it now
59
+ * fires ONLY there.
60
+ *
61
+ * `wired` are stores init resolved and wrote a live `registerStore` call for. Hints are not
62
+ * registrations: they land in the file as a commented line, and counting one as a registration
63
+ * silences the notice whose whole purpose is to say "act on the hint".
64
+ */
65
+ export declare function needsManualStore(hints: readonly string[], wired: readonly unknown[]): boolean;
66
+ export declare function capabilitiesStep(input: PlanInput, path?: string): Step[];
67
+ export declare function viteSteps(input: PlanInput, detail?: string, inject?: boolean): Step[];