@reticlehq/init 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/LICENSE +105 -0
  2. package/NOTICE +5 -0
  3. package/README.md +29 -0
  4. package/dist/agent-rules.d.ts +116 -0
  5. package/dist/agent-rules.js +279 -0
  6. package/dist/app-choice.d.ts +29 -0
  7. package/dist/app-choice.js +39 -0
  8. package/dist/astro-host.d.ts +21 -0
  9. package/dist/astro-host.js +27 -0
  10. package/dist/astro-patch.d.ts +33 -0
  11. package/dist/astro-patch.js +103 -0
  12. package/dist/brace-scan.d.ts +24 -0
  13. package/dist/brace-scan.js +46 -0
  14. package/dist/capabilities.d.ts +40 -0
  15. package/dist/capabilities.js +167 -0
  16. package/dist/closing-hint.d.ts +35 -0
  17. package/dist/closing-hint.js +122 -0
  18. package/dist/codex-toml.d.ts +10 -0
  19. package/dist/codex-toml.js +144 -0
  20. package/dist/connect-steps.d.ts +55 -0
  21. package/dist/connect-steps.js +92 -0
  22. package/dist/cra.d.ts +82 -0
  23. package/dist/cra.js +183 -0
  24. package/dist/csp-check.d.ts +49 -0
  25. package/dist/csp-check.js +168 -0
  26. package/dist/csp-doctor.d.ts +32 -0
  27. package/dist/csp-doctor.js +62 -0
  28. package/dist/desktop-doctor.d.ts +46 -0
  29. package/dist/desktop-doctor.js +201 -0
  30. package/dist/detect.d.ts +225 -0
  31. package/dist/detect.js +334 -0
  32. package/dist/dev-script.d.ts +51 -0
  33. package/dist/dev-script.js +66 -0
  34. package/dist/dev-server-ports.d.ts +20 -0
  35. package/dist/dev-server-ports.js +45 -0
  36. package/dist/electron-patch.d.ts +11 -0
  37. package/dist/electron-patch.js +70 -0
  38. package/dist/electron-vite-patch.d.ts +12 -0
  39. package/dist/electron-vite-patch.js +100 -0
  40. package/dist/existing-config.d.ts +31 -0
  41. package/dist/existing-config.js +63 -0
  42. package/dist/format-generated.d.ts +18 -0
  43. package/dist/format-generated.js +48 -0
  44. package/dist/framework-adapter.d.ts +60 -0
  45. package/dist/framework-adapter.js +138 -0
  46. package/dist/host.d.ts +41 -0
  47. package/dist/host.js +21 -0
  48. package/dist/index.d.ts +25 -0
  49. package/dist/index.js +25 -0
  50. package/dist/init-failure.d.ts +19 -0
  51. package/dist/init-failure.js +18 -0
  52. package/dist/init-opt-out.d.ts +17 -0
  53. package/dist/init-opt-out.js +30 -0
  54. package/dist/install-hint.d.ts +9 -0
  55. package/dist/install-hint.js +41 -0
  56. package/dist/install-retries.d.ts +22 -0
  57. package/dist/install-retries.js +65 -0
  58. package/dist/install-source-config.d.ts +18 -0
  59. package/dist/install-source-config.js +34 -0
  60. package/dist/lockfiles.d.ts +14 -0
  61. package/dist/lockfiles.js +49 -0
  62. package/dist/mcp-clients.d.ts +94 -0
  63. package/dist/mcp-clients.js +327 -0
  64. package/dist/mcp-registered.d.ts +12 -0
  65. package/dist/mcp-registered.js +14 -0
  66. package/dist/mcp.d.ts +89 -0
  67. package/dist/mcp.js +129 -0
  68. package/dist/next-patch.d.ts +49 -0
  69. package/dist/next-patch.js +237 -0
  70. package/dist/node-io.d.ts +22 -0
  71. package/dist/node-io.js +150 -0
  72. package/dist/non-js-project.d.ts +44 -0
  73. package/dist/non-js-project.js +137 -0
  74. package/dist/nuxt-patch.d.ts +18 -0
  75. package/dist/nuxt-patch.js +33 -0
  76. package/dist/patch-kind.d.ts +20 -0
  77. package/dist/patch-kind.js +10 -0
  78. package/dist/plan-electron-vite.d.ts +16 -0
  79. package/dist/plan-electron-vite.js +72 -0
  80. package/dist/plan-framework.d.ts +122 -0
  81. package/dist/plan-framework.js +773 -0
  82. package/dist/plan.d.ts +312 -0
  83. package/dist/plan.js +650 -0
  84. package/dist/platform.d.ts +16 -0
  85. package/dist/platform.js +16 -0
  86. package/dist/preflight.d.ts +30 -0
  87. package/dist/preflight.js +52 -0
  88. package/dist/project-id.d.ts +12 -0
  89. package/dist/project-id.js +25 -0
  90. package/dist/refresh-rules.d.ts +40 -0
  91. package/dist/refresh-rules.js +51 -0
  92. package/dist/remember-project.d.ts +24 -0
  93. package/dist/remember-project.js +40 -0
  94. package/dist/run.d.ts +122 -0
  95. package/dist/run.js +762 -0
  96. package/dist/slash-command.d.ts +27 -0
  97. package/dist/slash-command.js +132 -0
  98. package/dist/snippets.d.ts +267 -0
  99. package/dist/snippets.js +898 -0
  100. package/dist/tanstack-start.d.ts +18 -0
  101. package/dist/tanstack-start.js +57 -0
  102. package/dist/version.d.ts +18 -0
  103. package/dist/version.js +23 -0
  104. package/dist/vite-config.d.ts +18 -0
  105. package/dist/vite-config.js +111 -0
  106. package/dist/vite-owning-config.d.ts +25 -0
  107. package/dist/vite-owning-config.js +219 -0
  108. package/dist/windows-quote.d.ts +38 -0
  109. package/dist/windows-quote.js +70 -0
  110. package/dist/workspace-apps.d.ts +40 -0
  111. package/dist/workspace-apps.js +188 -0
  112. package/dist/workspace-redirect.d.ts +22 -0
  113. package/dist/workspace-redirect.js +97 -0
  114. package/package.json +53 -0
@@ -0,0 +1,773 @@
1
+ /**
2
+ * How each framework gets wired: the Vite plugin, the three Next files, the SvelteKit client hook.
3
+ * Split out of `plan.ts` — that file is the plan's SHAPE (statuses, ordering, the agent/MCP steps),
4
+ * this one is the per-framework detail, and they grow for different reasons.
5
+ */
6
+ import { bridgeWsUrl } from '@reticlehq/core';
7
+ import { patchViteConfig, VitePatchKind, VITE_IMPORT } from './vite-config.js';
8
+ import { patchNextConfig, patchRootLayout, patchPagesApp } from './next-patch.js';
9
+ import { ASTRO_ENV_DTS_PATH, patchAstroConfig, patchAstroEnvDts, patchAstroLayout, } from './astro-patch.js';
10
+ import { CRA_DEV_MODULE_IMPORT, CRA_ENV_PATH, CRA_TOKEN_PER_MACHINE_NOTICE, TOKEN_VAR, craDevModuleFile, craDevModulePath, craEnvPatch, craImportPatch, } from './cra.js';
11
+ import { PatchKind } from './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 './snippets.js';
13
+ import { hasOptOut, OPT_OUT_MARKER } from './init-opt-out.js';
14
+ import { StepStatus } from './plan.js';
15
+ import { RETICLE_DEFAULT_PORT } from '@reticlehq/core';
16
+ import { CSP_STEP_TITLE } from './csp-check.js';
17
+ import { StepTitle } from './connect-steps.js';
18
+ import { tanstackStartManual, TANSTACK_START_ROOT_PATH } from './tanstack-start.js';
19
+ import { FRAMEWORK_ADAPTERS } from './framework-adapter.js';
20
+ import { diagnoseWebCsp } from './csp-doctor.js';
21
+ import { patchNuxtConfig } from './nuxt-patch.js';
22
+ /** What adding `reticle()` to a Vite config buys, which differs by framework. */
23
+ export const VITE_PLUGIN_DETAIL = {
24
+ /** A plain Vite app gets both halves from the plugin. */
25
+ VITE: 'add reticle() to plugins (also injects connect())',
26
+ /**
27
+ * SvelteKit renders through app.html, so the plugin's HTML injection never fires and connect()
28
+ * comes from the client hook instead. The plugin is still required: it is what stamps
29
+ * data-reticle-source into .svelte components, and without it every verdict on a SvelteKit app
30
+ * comes back with no file:line at all.
31
+ */
32
+ SVELTEKIT: 'add reticle() to plugins (stamps data-reticle-source in .svelte components)',
33
+ /**
34
+ * React Router framework mode renders through its own request handler, so the plugin's HTML
35
+ * injection never fires and connect() comes from the client entry instead. The plugin is still
36
+ * required for the same reason it is under SvelteKit: without it every verdict on the app comes
37
+ * back with no file:line at all.
38
+ */
39
+ REACT_ROUTER: 'add reticle() to plugins (stamps data-reticle-source in .tsx components)',
40
+ /**
41
+ * TanStack Start SSRs its own HTML, so the plugin's HTML injection never fires and connect()
42
+ * comes from a client effect instead. `inject: false` is load-bearing honesty: leaving the
43
+ * default would keep reporting "also injects connect()" for a transform that never runs.
44
+ * The plugin is still required for the same reason it is under SvelteKit: without it every
45
+ * verdict on the app comes back with no file:line at all.
46
+ */
47
+ TANSTACK_START: 'add reticle({ inject: false }) to plugins (stamps data-reticle-source; Start SSRs its own HTML so connect() cannot come from the plugin)',
48
+ };
49
+ const CAPABILITIES_TITLE = StepTitle.CAPABILITIES;
50
+ /**
51
+ * The one step `init` genuinely cannot finish, addressed to the AGENT reading this report.
52
+ *
53
+ * It used to read "Capabilities are empty until you edit that file" and describe the consequence to
54
+ * a human — homework, in a report a human mostly does not read, naming no file to open and no thing
55
+ * to look for. The agent that ran `init` is right there, has the codebase, and can finish this in
56
+ * the same minute; what it needed was the specific instruction, not the rationale.
57
+ *
58
+ * What is left is exactly what could not be PROVEN from source: which atoms/actor/queryClient an
59
+ * adapter-wrapped store should be built from, and which elements matter enough to name. `init`
60
+ * emits nothing it cannot prove, because a wrong import here throws on every dev page load — worse
61
+ * than an empty file.
62
+ */
63
+ const CAPABILITIES_TODO_TITLE = StepTitle.CAPABILITIES_TODO;
64
+ function capabilitiesTodo(path, stores) {
65
+ return (`This app's state lives in a library nothing in the running app points at, so it is the one ` +
66
+ `thing Reticle cannot find for itself — reticle_state will not see it until you say where it ` +
67
+ `is.\n` +
68
+ `Do this now, before driving anything:\n` +
69
+ ` 1. In ${path}, uncomment the suggested line and complete it: ${stores.join('; ')}.\n` +
70
+ ` 2. Prove it: drive one flow and check reticle_state returns your key. An empty state read ` +
71
+ `means the line is still not doing anything, whatever else went green.`);
72
+ }
73
+ /**
74
+ * The dev module carrying `registerCapabilities` / `registerStore`.
75
+ *
76
+ * Without it every app came up `hasCapabilities: false` with a `reticle_state` holding nothing but
77
+ * `__reticle_renders` — the state-truth read was unavailable on every app out of the box. Written
78
+ * only when absent, because it is the one generated file a user is expected to EDIT.
79
+ */
80
+ /**
81
+ * Is there state left that ONLY the app can hand over?
82
+ *
83
+ * This used to ask a much bigger question — "does the generated file register anything at all?" —
84
+ * and fired on almost every install, because almost every install generated a file whose testids
85
+ * and stores were both empty. The answer was homework: go read the source, uncomment a line, add
86
+ * data-testid attributes, then drive to prove it. Several turns, on every onboarding.
87
+ *
88
+ * Two of those three are no longer anyone's homework. Testids are read from the live DOM, and a
89
+ * store passed through a React context provider (Redux, TanStack Query) is discovered and
90
+ * registered on the first commit. What is left is the genuinely unreachable case: a module-scope
91
+ * store, or one needing an argument only the source supplies — Zustand, Jotai atoms, an XState
92
+ * actor. Nothing in the running app points at those, so the notice still has to fire, and it now
93
+ * fires ONLY there.
94
+ *
95
+ * `wired` are stores init resolved and wrote a live `registerStore` call for. Hints are not
96
+ * registrations: they land in the file as a commented line, and counting one as a registration
97
+ * silences the notice whose whole purpose is to say "act on the hint".
98
+ */
99
+ function needsManualStore(hints, wired) {
100
+ return hints.length > 0 && 0 === wired.length;
101
+ }
102
+ export function capabilitiesStep(input, path = VITE_DEV_MODULE_PATH) {
103
+ if (true === input.viteDevModuleExists) {
104
+ return [
105
+ {
106
+ title: CAPABILITIES_TITLE,
107
+ target: path,
108
+ status: StepStatus.ALREADY,
109
+ detail: 'file exists, left alone, it is yours to edit',
110
+ },
111
+ ];
112
+ }
113
+ const testids = input.testids ?? [];
114
+ const stores = input.storeHints ?? [];
115
+ const wired = input.foundStores ?? [];
116
+ // Testids no longer need counting here. They are read from the live DOM at announce time, so a
117
+ // number printed at install time would be a stale claim about a codebase that is about to change —
118
+ // which is exactly what "no data-testid values yet" used to be on an app whose testids arrive with
119
+ // a lazy route.
120
+ const found = 'testids read from the live DOM';
121
+ // A store we found and WIRED is a registration, so the notice must not fire. A store HINT is not:
122
+ // `stores` holds suggestions, written into the file as a commented line of the form
123
+ // `// import your store, then: registerStore(...)`. Counting a suggestion as a registration let
124
+ // the hint silence the notice whose entire job is to say "act on the hint".
125
+ //
126
+ // Measured against a real product UI (rowy — 70+ deps, jotai, a whole src/atoms tree): init
127
+ // detected jotai, offered one commented line, emitted NO notice, and wrote
128
+ // `registerCapabilities({ testids: [], signals: [], stores: [] })`. So `hasCapabilities` stayed
129
+ // false, `reticle_state` was empty forever, and the install gate reported "connected: 1,
130
+ // manual ⚠: none". Every check green, state observability zero.
131
+ //
132
+ // jotai is still exactly that case. A Redux or TanStack app is not, any more: its store rides a
133
+ // context provider and the React adapter registers it on the first commit, so `storeHints` no
134
+ // longer names either one and this notice no longer fires for them.
135
+ const nothingToRegister = needsManualStore(stores, wired);
136
+ return [
137
+ {
138
+ title: CAPABILITIES_TITLE,
139
+ target: path,
140
+ status: StepStatus.APPLY,
141
+ detail: `${found}; ${wired.length > 0
142
+ ? `registered ${wired.map((s) => `'${s.key}'`).join(', ')} from your source`
143
+ : stores.length > 0
144
+ ? `store: uncomment the ${String(stores.length)} suggested line(s)`
145
+ : 'no state library detected'}`,
146
+ write: {
147
+ path,
148
+ content: viteDevModuleFile(testids, stores, wired, input.detection.uiLibrary),
149
+ },
150
+ dependsOnInstall: true,
151
+ },
152
+ // The write is real; what it registers is not. `registerCapabilities({ testids: [], signals: [],
153
+ // stores: [] })` registers nothing, so `hasCapabilities` stays false — correctly — and the ✓
154
+ // above reads as if the feature is on. Reported as exactly that confusion: "hasCapabilities false
155
+ // on every session while init said ✓ Capabilities + store".
156
+ //
157
+ // Beside the write rather than replacing it, for two reasons: only APPLY steps are written
158
+ // (run.ts), and SKILL.md tells the reader to skip ✓ lines — so a caveat carried on the ✓ is a
159
+ // caveat nobody reads.
160
+ ...(nothingToRegister
161
+ ? [
162
+ {
163
+ title: CAPABILITIES_TODO_TITLE,
164
+ target: path,
165
+ status: StepStatus.NOTICE,
166
+ detail: capabilitiesTodo(path, stores),
167
+ },
168
+ ]
169
+ : []),
170
+ ];
171
+ }
172
+ export function viteSteps(input, detail = VITE_PLUGIN_DETAIL.VITE, inject = true) {
173
+ // Capabilities are independent of whether the config needed patching. Attaching them to the APPLY
174
+ // branch meant a re-run on an already-wired app silently never created the module.
175
+ return [...viteConfigSteps(input, detail, inject), ...capabilitiesStep(input)];
176
+ }
177
+ function viteConfigSteps(input, detail, inject = true) {
178
+ const cfg = input.viteConfig;
179
+ const port = input.options.port;
180
+ // Stamp `data-reticle-source` unless this app renders through a non-DOM React reconciler, where a
181
+ // lowercase JSX tag is not an element and the stamp crashes the app at commit time. See
182
+ // `Detection.customReconciler`.
183
+ const stampSource = true !== input.detection?.customReconciler;
184
+ // An explicit "not here" is not an invitation. `init` runs unattended in a repo it has just met,
185
+ // and it was reported adding the plugin to an app whose config said Reticle was deliberately
186
+ // excluded. A NOTICE rather than a ⚠: opting out is a decision, not something to go and fix.
187
+ if (cfg !== null && hasOptOut(cfg.source)) {
188
+ return [
189
+ {
190
+ title: StepTitle.VITE_PLUGIN,
191
+ target: cfg.path,
192
+ status: StepStatus.NOTICE,
193
+ detail: `left alone: this config carries ${OPT_OUT_MARKER}. Remove that marker to instrument this app.`,
194
+ },
195
+ ];
196
+ }
197
+ if (null === cfg) {
198
+ return [
199
+ {
200
+ title: StepTitle.VITE_PLUGIN,
201
+ target: 'vite.config',
202
+ status: StepStatus.MANUAL,
203
+ detail: viteManual(port, input.detection.uiLibrary, inject, stampSource),
204
+ },
205
+ ];
206
+ }
207
+ const patch = patchViteConfig(cfg.source, port, true === input.captureBodies, inject, stampSource);
208
+ if (patch.kind === VitePatchKind.ALREADY) {
209
+ return [
210
+ {
211
+ title: StepTitle.VITE_PLUGIN,
212
+ target: cfg.path,
213
+ status: StepStatus.ALREADY,
214
+ detail: 'reticle() already in plugins',
215
+ },
216
+ ];
217
+ }
218
+ if (patch.kind === VitePatchKind.MANUAL) {
219
+ return [
220
+ {
221
+ title: StepTitle.VITE_PLUGIN,
222
+ target: cfg.path,
223
+ status: StepStatus.MANUAL,
224
+ detail: `${patch.reason}\n\n${viteManual(port, input.detection.uiLibrary, inject, stampSource)}`,
225
+ },
226
+ ];
227
+ }
228
+ return [
229
+ {
230
+ title: StepTitle.VITE_PLUGIN,
231
+ target: cfg.path,
232
+ status: StepStatus.APPLY,
233
+ detail,
234
+ // Both halves, because either alone is a config that does not wire: the import without the
235
+ // call leaves `plugins` untouched, and the call without the import does not build.
236
+ write: { path: cfg.path, content: patch.code, expect: [VITE_IMPORT, 'reticle('] },
237
+ dependsOnInstall: true,
238
+ },
239
+ ];
240
+ }
241
+ /**
242
+ * Turn a conservative source patch into a step: applied when it patched, already when the wiring is
243
+ * there, and the hand-edit instructions when the file shape wasn't one we recognise.
244
+ */
245
+ export function patchStep(title, path, patch, applyDetail, manualDetail) {
246
+ if (patch.kind === PatchKind.ALREADY) {
247
+ return { title, target: path, status: StepStatus.ALREADY, detail: 'already wired' };
248
+ }
249
+ if (patch.kind === PatchKind.MANUAL) {
250
+ return {
251
+ title,
252
+ target: path,
253
+ status: StepStatus.MANUAL,
254
+ detail: `${patch.reason}\n\n${manualDetail}`,
255
+ };
256
+ }
257
+ return {
258
+ title,
259
+ target: path,
260
+ status: StepStatus.APPLY,
261
+ detail: applyDetail,
262
+ write: { path, content: patch.code },
263
+ dependsOnInstall: true,
264
+ };
265
+ }
266
+ /**
267
+ * Next used to be the ONLY stack with hand edits left over — and both of them fail silently when
268
+ * skipped, so a Next user's app booted, connected to nothing, and said nothing about why. Both are
269
+ * now patched by the same conservative rules the Vite config gets.
270
+ */
271
+ /** The env var withReticle sets from the discovered daemon, and the component must read. */
272
+ const NEXT_DAEMON_URL_ENV = 'NEXT_PUBLIC_RETICLE_URL';
273
+ /**
274
+ * The two-line edit that unfreezes an existing install's port.
275
+ *
276
+ * Stated as the edit rather than as a problem, because the reader is an agent that can apply it in
277
+ * one write and is otherwise going to ask what to do.
278
+ */
279
+ const NEXT_DEV_FILE_STALE_DETAIL = 'predates daemon discovery, so it dials the port init saw when it ran. In this file add ' +
280
+ '`const url = process.env.NEXT_PUBLIC_RETICLE_URL;` and spread `...(url ? { url } : {})` into ' +
281
+ 'reticle.connect(), after any url already there. Nothing else needs to change.';
282
+ export function nextSteps(input) {
283
+ const configFile = input.nextConfigFile ?? 'next.config.mjs';
284
+ const devPath = input.nextReticleDevPath ?? NEXT_RETICLE_DEV_PATH;
285
+ // An install that predates daemon discovery has a component that never reads the discovered URL,
286
+ // so it keeps dialling whatever port `init` saw on the day it ran. Re-running `init` used to call
287
+ // that "file exists" and move on, which is why the defect survives an upgrade. Named as work.
288
+ //
289
+ // Never overwritten: this file is the one an app owner edits — registered stores, capabilities,
290
+ // their own signals. Rewriting it to fix two lines would take the rest with it. Undefined source
291
+ // means it was not read, and an unread file stays ALREADY rather than becoming invented work.
292
+ const devStale = true === input.nextReticleDevExists &&
293
+ 'string' === typeof input.nextReticleDevSource &&
294
+ !input.nextReticleDevSource.includes(NEXT_DAEMON_URL_ENV);
295
+ const devFile = input.nextReticleDevExists
296
+ ? devStale
297
+ ? {
298
+ title: StepTitle.RETICLE_DEV_COMPONENT,
299
+ target: devPath,
300
+ status: StepStatus.MANUAL,
301
+ detail: NEXT_DEV_FILE_STALE_DETAIL,
302
+ }
303
+ : {
304
+ title: StepTitle.RETICLE_DEV_COMPONENT,
305
+ target: devPath,
306
+ status: StepStatus.ALREADY,
307
+ detail: 'file exists',
308
+ }
309
+ : {
310
+ title: StepTitle.RETICLE_DEV_COMPONENT,
311
+ target: devPath,
312
+ status: StepStatus.APPLY,
313
+ detail: 'create dev-only connect component',
314
+ write: {
315
+ path: devPath,
316
+ content: nextReticleDevFile(input.options.port, input.options.projectId, input.testids ?? [], input.storeHints ?? [],
317
+ // Next's dev module is not a sibling of `src/` — its imports are resolved from wherever
318
+ // the component actually lands (`app/`, or `src/app/` in a --src-dir app).
319
+ input.nextFoundStores ?? []),
320
+ },
321
+ dependsOnInstall: true,
322
+ };
323
+ const configPatch = null === input.nextConfigSource || input.nextConfigSource === undefined
324
+ ? { kind: PatchKind.MANUAL, reason: `no ${configFile} found` }
325
+ : patchNextConfig(input.nextConfigSource, true !== input.detection?.customReconciler);
326
+ const layout = input.nextLayout ?? null;
327
+ // Pages Router mounts through pages/_app, App Router through the root layout — different edits,
328
+ // and picking by path is what stops a Pages app being handed the layout patch that cannot apply.
329
+ const isPagesRouter = layout !== null && /(^|\/)pages\/_app\.[jt]sx?$/.test(layout.path);
330
+ const layoutPatch = null === layout
331
+ ? { kind: PatchKind.MANUAL, reason: 'no root layout (app/layout.tsx) or pages/_app found' }
332
+ : isPagesRouter
333
+ ? patchPagesApp(layout.source, input.nextReticleDevImport)
334
+ : patchRootLayout(layout.source);
335
+ // The same question the Vite path asks. Skipped when the module already exists: its contents are
336
+ // the user's, and re-nagging about a file we did not write is noise.
337
+ const nextTodo = true !== input.nextReticleDevExists &&
338
+ needsManualStore(input.storeHints ?? [], input.nextFoundStores ?? [])
339
+ ? [
340
+ {
341
+ title: CAPABILITIES_TODO_TITLE,
342
+ target: devPath,
343
+ status: StepStatus.NOTICE,
344
+ detail: capabilitiesTodo(devPath, input.storeHints ?? []),
345
+ },
346
+ ]
347
+ : [];
348
+ return [
349
+ devFile,
350
+ ...nextTodo,
351
+ patchStep(StepTitle.NEXT_CONFIG, configFile, configPatch, 'wrap the export in withReticle (source mapping, dev-only)', nextConfigManual(configFile, true !== input.detection?.customReconciler)),
352
+ patchStep(StepTitle.MOUNT_RETICLE_DEV, layout?.path ?? NEXT_LAYOUT_PATH, layoutPatch, 'mount <ReticleDev /> in the root layout (dev-only)', NEXT_LAYOUT_MANUAL),
353
+ ];
354
+ }
355
+ /**
356
+ * SvelteKit is WIRED but not SUPPORTED, and the plan says so out loud.
357
+ *
358
+ * There is no SvelteKit app in `apps/` and no CI gate for one, so nothing proves this hook still
359
+ * registers a session — every other framework init offers (React, Next, Remix, Astro) has both. The
360
+ * wiring is real and may well work; what is missing is anything that would tell us when it stops.
361
+ * Silently emitting it reads as a support claim, which is the thing this project exists to not do.
362
+ */
363
+ /**
364
+ * The one failure `init` cannot otherwise see: the bundler will not parse our SDK.
365
+ *
366
+ * Every check in this report passes on a react-scripts 4 app -- the package installs, the entry
367
+ * import is written, the token is inlined -- and then `npm start` dies with a syntax error inside
368
+ * `@reticlehq/browser/dist/index.js`, a file the user did not write, naming nothing about Reticle
369
+ * (#680). A green report over a build that cannot compile is the same class of lie as a green report
370
+ * over an app that cannot connect.
371
+ *
372
+ * FIRST in the CRA step list, deliberately: it decides whether any of the steps below it can run at
373
+ * all.
374
+ */
375
+ function webpack4Step(input) {
376
+ const major = input.detection.reactScriptsMajor;
377
+ if (major === undefined || major >= WEBPACK4_REACT_SCRIPTS_MAJOR)
378
+ return [];
379
+ return [
380
+ {
381
+ title: `react-scripts ${String(major)} cannot parse the SDK`,
382
+ target: 'package.json',
383
+ status: StepStatus.NOTICE,
384
+ detail: webpack4TranspileNote(major),
385
+ },
386
+ ];
387
+ }
388
+ /**
389
+ * Create React App: the connect goes in `src/index.tsx`, the token in `.env.development.local`.
390
+ *
391
+ * The previous plan pointed at `index.html`, which cannot work — CRA's is a static template the
392
+ * bundler never processes for modules. Reported from a real cra-redux-saga app.
393
+ */
394
+ export function craSteps(input) {
395
+ const entry = input.craEntry ?? null;
396
+ // Match the project language the same way Next does (#675): a JS CRA app cannot resolve `.ts`.
397
+ const modulePath = craDevModulePath(input.detection.typescript);
398
+ const steps = [
399
+ ...webpack4Step(input),
400
+ {
401
+ title: StepTitle.CONNECT_MODULE,
402
+ target: modulePath,
403
+ status: StepStatus.APPLY,
404
+ detail: 'create the dev-only connect (CRA cannot inject through public/index.html)',
405
+ write: {
406
+ path: modulePath,
407
+ content: craDevModuleFile(input.options.port, input.options.projectId, {
408
+ typescript: input.detection.typescript,
409
+ testids: input.testids ?? [],
410
+ }),
411
+ },
412
+ dependsOnInstall: true,
413
+ },
414
+ ];
415
+ const token = input.pairingToken ?? '';
416
+ // The daemon that is live NOW, not the one baked into the module at first install. CRA has no
417
+ // build hook, so this is refreshed by re-running `init` rather than by starting the dev server.
418
+ const env = craEnvPatch(input.craEnv ?? null, token, input.options.port === undefined ? undefined : bridgeWsUrl(input.options.port));
419
+ if (env !== null) {
420
+ steps.push({
421
+ title: StepTitle.PAIRING_TOKEN,
422
+ target: CRA_ENV_PATH,
423
+ status: StepStatus.APPLY,
424
+ // REACT_APP_* is the only thing CRA inlines into browser code; without the token the bridge
425
+ // refuses the connection and no session appears. Say the file is gitignored HERE, at the one
426
+ // moment someone is looking: the token is per-machine and cannot travel, so every teammate
427
+ // has to run init once or their clone is dead with no explanation.
428
+ detail: `set ${TOKEN_VAR} (the only channel CRA inlines) — ${CRA_ENV_PATH} is gitignored, so each teammate must run \`reticle init\` on their own machine`,
429
+ write: { path: CRA_ENV_PATH, content: env },
430
+ });
431
+ // Beside the write, not inside it. A ✓ line is one SKILL.md tells the reader to skip, and this
432
+ // is the fact that decides whether the install works for anyone but the person running it.
433
+ steps.push({
434
+ title: StepTitle.PAIRING_TOKEN_PER_MACHINE,
435
+ target: CRA_ENV_PATH,
436
+ status: StepStatus.NOTICE,
437
+ detail: CRA_TOKEN_PER_MACHINE_NOTICE,
438
+ });
439
+ }
440
+ else if ('' === token) {
441
+ // No daemon has ever run here, so there is no token to inline. Omitting the step entirely made
442
+ // init report all-green for an app that could never pair.
443
+ steps.push({
444
+ title: StepTitle.PAIRING_TOKEN,
445
+ target: CRA_ENV_PATH,
446
+ status: StepStatus.MANUAL,
447
+ // `reticle serve`, not `reticle start` — the latter is not a verb this CLI dispatches, and
448
+ // this message is read by someone whose app boots and never pairs. Handing them a command
449
+ // that errors is a second dead end on the first.
450
+ detail: `no pairing token yet — the daemon writes one on first run. Start it with \`reticle serve\` (or let your agent run \`reticle mcp\`), then \`reticle init\` again to write ${TOKEN_VAR}`,
451
+ });
452
+ }
453
+ if (null === entry) {
454
+ steps.push({
455
+ title: StepTitle.CONNECT_SNIPPET_CRA,
456
+ target: 'src/index.tsx',
457
+ status: StepStatus.MANUAL,
458
+ detail: `Add \`${CRA_DEV_MODULE_IMPORT}\` to your app entry (src/index.tsx or src/index.js), after the existing imports.`,
459
+ });
460
+ return steps;
461
+ }
462
+ const patched = craImportPatch(entry.source);
463
+ steps.push(null === patched
464
+ ? {
465
+ title: StepTitle.CONNECT_SNIPPET_CRA,
466
+ target: entry.path,
467
+ status: StepStatus.ALREADY,
468
+ detail: 'already imported',
469
+ }
470
+ : {
471
+ title: StepTitle.CONNECT_SNIPPET_CRA,
472
+ target: entry.path,
473
+ status: StepStatus.APPLY,
474
+ detail: 'import the dev-only connect module',
475
+ write: { path: entry.path, content: patched },
476
+ dependsOnInstall: true,
477
+ });
478
+ return steps;
479
+ }
480
+ /**
481
+ * Nuxt: the client plugin written, and the config patched so it can pair.
482
+ *
483
+ * This used to be one MANUAL step carrying the whole recipe, so `init` ended on a ⚠ and exited 1 —
484
+ * a correct recipe nobody applies is the same outcome as no recipe. What it could not do until now
485
+ * was the pairing token: the bridge requires one even on localhost, nothing in a browser can read
486
+ * the file it lives in, and Nuxt loads no plugin of ours that could inject it. `nuxt.config`'s
487
+ * `vite.define` is the one place that can, which is also where the journal watch-ignore goes.
488
+ *
489
+ * ATOMIC, for the reason `astroSteps` is: a plugin written beside a config we could not patch is an
490
+ * app that dials the bridge and is refused, reported as one green step and one caveat when it is a
491
+ * guaranteed non-connection. If the config cannot be patched, BOTH halves go back to the recipe.
492
+ */
493
+ export function nuxtSteps(input) {
494
+ const config = input.nuxtConfig ?? null;
495
+ const pluginPath = nuxtPluginPath(true === input.nuxtHasAppDir);
496
+ const manual = [
497
+ {
498
+ title: StepTitle.CONNECT_SNIPPET_NUXT,
499
+ target: null === config ? 'nuxt.config + a client plugin' : `${config.path} + ${pluginPath}`,
500
+ status: StepStatus.MANUAL,
501
+ detail: nuxtManual(input.options.port, input.options.projectId),
502
+ },
503
+ ];
504
+ if (null === config)
505
+ return manual;
506
+ const configPatch = patchNuxtConfig(config.source);
507
+ if (configPatch.kind === PatchKind.MANUAL)
508
+ return manual;
509
+ const notice = {
510
+ title: StepTitle.NUXT_RESTART,
511
+ target: pluginPath,
512
+ status: StepStatus.NOTICE,
513
+ detail: NUXT_PLUGIN_NOTICE,
514
+ };
515
+ // Never overwritten: this file is the one an app owner edits — their registered stores, their own
516
+ // capabilities. Rewriting it would take those with it.
517
+ const plugin = true === input.nuxtPluginExists
518
+ ? {
519
+ title: StepTitle.CONNECT_SNIPPET_NUXT,
520
+ target: pluginPath,
521
+ status: StepStatus.ALREADY,
522
+ detail: 'file exists',
523
+ }
524
+ : {
525
+ title: StepTitle.CONNECT_SNIPPET_NUXT,
526
+ target: pluginPath,
527
+ status: StepStatus.APPLY,
528
+ detail: 'create the dev-only client plugin (Nuxt renders its own HTML)',
529
+ write: {
530
+ path: pluginPath,
531
+ content: nuxtPluginFile(input.options.port, input.options.projectId, input.testids ?? []),
532
+ },
533
+ dependsOnInstall: true,
534
+ };
535
+ return [
536
+ plugin,
537
+ 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)),
538
+ notice,
539
+ ];
540
+ }
541
+ /**
542
+ * React Router framework mode: the client entry, WRITTEN.
543
+ *
544
+ * #678 happened here — every step reported green and the daemon showed zero sessions for 20+
545
+ * minutes, because framework mode renders HTML through its own request handler and the Vite plugin's
546
+ * `transformIndexHtml` injection never fires. The connect has to come from `app/entry.client.tsx`,
547
+ * and that used to be printed as a recipe: `init` exited 0 over an app that could not connect.
548
+ *
549
+ * The file is an OVERRIDE of a default React Router supplies, so when it is absent the generated one
550
+ * carries React Router's own default entry as well as the import — a file containing only our import
551
+ * would replace that default with one that never hydrates. When it is present, one line is added to
552
+ * it and nothing else is touched.
553
+ */
554
+ export function reactRouterSteps(input) {
555
+ const detail = 'connect from the client entry — framework mode renders HTML through its own request handler, ' +
556
+ "so the Vite plugin's index.html injection never fires";
557
+ const existing = input.reactRouterEntrySource ?? null;
558
+ if (true !== input.reactRouterEntryExists || null === existing) {
559
+ return [
560
+ {
561
+ title: StepTitle.CONNECT_SNIPPET_REACT_ROUTER,
562
+ target: REACT_ROUTER_ENTRY_PATH,
563
+ status: StepStatus.APPLY,
564
+ detail: `create the dev-only ${detail}`,
565
+ write: { path: REACT_ROUTER_ENTRY_PATH, content: reactRouterEntryFile() },
566
+ dependsOnInstall: true,
567
+ },
568
+ ];
569
+ }
570
+ const patched = reactRouterEntryPatch(existing);
571
+ return [
572
+ null === patched
573
+ ? {
574
+ title: StepTitle.CONNECT_SNIPPET_REACT_ROUTER,
575
+ target: REACT_ROUTER_ENTRY_PATH,
576
+ status: StepStatus.ALREADY,
577
+ detail: 'already imported',
578
+ }
579
+ : {
580
+ title: StepTitle.CONNECT_SNIPPET_REACT_ROUTER,
581
+ target: REACT_ROUTER_ENTRY_PATH,
582
+ status: StepStatus.APPLY,
583
+ detail: `add one line to ${detail}`,
584
+ write: { path: REACT_ROUTER_ENTRY_PATH, content: patched },
585
+ dependsOnInstall: true,
586
+ },
587
+ ];
588
+ }
589
+ /**
590
+ * TanStack Start: the client-document connect, printed rather than written.
591
+ *
592
+ * `src/routes/__root.tsx` is the document Start SSRs. A static import of the SDK on that module
593
+ * 500s, so writing one is worse than a documented manual step. See `tanstackStartManual`.
594
+ */
595
+ export function tanstackStartSteps(input) {
596
+ const root = input.tanstackStartRoot ?? TANSTACK_START_ROOT_PATH;
597
+ return [
598
+ {
599
+ title: StepTitle.TANSTACK_START_UNVERIFIED,
600
+ target: root,
601
+ status: StepStatus.NOTICE,
602
+ detail: UNVERIFIED_TANSTACK_START_NOTE,
603
+ },
604
+ {
605
+ title: StepTitle.CONNECT_SNIPPET_TANSTACK_START,
606
+ target: root,
607
+ status: StepStatus.MANUAL,
608
+ detail: tanstackStartManual(input.options.port, input.options.projectId, root),
609
+ },
610
+ ];
611
+ }
612
+ export function svelteKitSteps(input) {
613
+ const unverified = {
614
+ title: StepTitle.SVELTEKIT_UNVERIFIED,
615
+ target: SVELTEKIT_HOOKS_PATH,
616
+ status: StepStatus.NOTICE,
617
+ detail: UNVERIFIED_FRAMEWORK_NOTE,
618
+ };
619
+ // SvelteKit can't use the Vite-plugin injection (it renders via app.html) — wire a client hook
620
+ // that SvelteKit runs on startup, which is the path that can register a session at all.
621
+ if (true === input.svelteKitHooksExists) {
622
+ return [
623
+ unverified,
624
+ {
625
+ title: StepTitle.CLIENT_HOOK,
626
+ target: SVELTEKIT_HOOKS_PATH,
627
+ status: StepStatus.ALREADY,
628
+ detail: 'file exists',
629
+ },
630
+ ];
631
+ }
632
+ return [
633
+ unverified,
634
+ {
635
+ title: StepTitle.CLIENT_HOOK,
636
+ target: SVELTEKIT_HOOKS_PATH,
637
+ status: StepStatus.APPLY,
638
+ detail: 'create dev-only client connect (SvelteKit renders via app.html)',
639
+ write: {
640
+ path: SVELTEKIT_HOOKS_PATH,
641
+ content: svelteKitHooksFile(input.options.port, input.options.projectId, input.detection.uiLibrary, input.testids ?? []),
642
+ },
643
+ dependsOnInstall: true,
644
+ },
645
+ ];
646
+ }
647
+ /**
648
+ * Astro: the config define + build target, and the connect script in ONE layout.
649
+ *
650
+ * Astro was the last gated stack left printing a recipe it did not apply. It still falls back to the
651
+ * printed one whenever the choice is not obvious — no config, no single layout, or a shape the
652
+ * patchers do not fully recognise — because which page or layout to instrument is a real decision
653
+ * and a half-edited build config is worse than a documented manual step.
654
+ */
655
+ export function astroSteps(input) {
656
+ const config = input.astroConfig ?? null;
657
+ const layout = input.astroLayout ?? null;
658
+ const manual = astroManual(input.options.port, input.options.projectId, layout?.path);
659
+ if (null === config || null === layout) {
660
+ return [
661
+ {
662
+ title: StepTitle.CONNECT_SNIPPET_ASTRO,
663
+ // Name what is actually there. `astro.config + layout` pointed at a layout this project may
664
+ // not have — reported on a fixture with only src/pages/index.astro.
665
+ target: null === layout ? 'astro.config + a page (no layout found)' : 'astro.config + layout',
666
+ status: StepStatus.MANUAL,
667
+ detail: manual,
668
+ },
669
+ ];
670
+ }
671
+ // ATOMIC. The connect snippet is useless without the config: the token is inlined by the config,
672
+ // so a layout patched on its own gives an app that dials the bridge and is refused. Measured on a
673
+ // real fixture — config ⚠, layout ✓ — which reads as one step done and one caveat when it is
674
+ // actually a guaranteed non-connection. If either half cannot be applied, BOTH go manual with the
675
+ // single recipe that does the whole job.
676
+ const manualWithLayout = astroManual(input.options.port, input.options.projectId, layout.path);
677
+ const configPatch = patchAstroConfig(config.source);
678
+ const layoutPatch = patchAstroLayout(layout.source, input.options.port, input.options.projectId, input.detection.uiLibrary, input.testids ?? []);
679
+ if (configPatch.kind === PatchKind.MANUAL || layoutPatch.kind === PatchKind.MANUAL) {
680
+ return [
681
+ {
682
+ title: StepTitle.CONNECT_SNIPPET_ASTRO,
683
+ target: `${config.path} + ${layout.path}`,
684
+ status: StepStatus.MANUAL,
685
+ detail: manualWithLayout,
686
+ },
687
+ ];
688
+ }
689
+ const envPatch = patchAstroEnvDts(input.astroEnvDts ?? null);
690
+ return [
691
+ patchStep(StepTitle.ASTRO_CONFIG, config.path, configPatch, 'inline the pairing token and raise build.target to es2022', manualWithLayout),
692
+ patchStep(StepTitle.CONNECT_SNIPPET_ASTRO, layout.path, layoutPatch, 'add the dev-only connect <script> before </body>', manualWithLayout),
693
+ // Declares the Vite define names so `astro check` (create-astro's default build) can see them
694
+ // (#677). Independent of the two halves above: even an ALREADY config/layout still needs this
695
+ // when the env file was never written.
696
+ patchStep(StepTitle.ASTRO_ENV_DTS, ASTRO_ENV_DTS_PATH, envPatch, 'declare __RETICLE_TOKEN__ / __RETICLE_ROOT__ for astro check', manualWithLayout),
697
+ ];
698
+ }
699
+ /**
700
+ * A Content-Security-Policy whose `connect-src` excludes the bridge, said out loud at install time.
701
+ *
702
+ * Both reported cases were Next apps where `init` printed success for every step and the app then
703
+ * never connected: the browser blocked the WebSocket and reported it in its own console, which
704
+ * nothing on this side reads. A NOTICE rather than a step, because editing someone's security policy
705
+ * is theirs to do — but it carries the exact text to paste, which is the difference between a
706
+ * warning and a fix.
707
+ */
708
+ export function cspStep(input) {
709
+ // Reads the SAME list `reticle doctor` reads. It used to read a hand-written pair —
710
+ // `[nextConfigSource, nextLayout?.source]` — while csp-doctor.ts already carried the full set
711
+ // including `index.html`, which is where every Vite and Electron app declares its policy. So the
712
+ // command you run BEFORE anything works checked less than the one you run after it has failed.
713
+ //
714
+ // Measured on MarkText, a production Electron editor: its renderer sets `default-src 'self'` with
715
+ // no `connect-src`, the browser blocked the bridge WebSocket, and `init` printed a clean plan. The
716
+ // daemon cannot see a dial that never left the page, so nothing anywhere said why — and the check
717
+ // that would have said it was sitting one import away.
718
+ // The pre-read Next sources are folded in ON TOP of the shared list, not replaced by it: init
719
+ // resolves `nextConfigFile` across more spellings than CSP_FILES names (`.mts`, for one), and a
720
+ // layout is found by search rather than by a fixed path. Two sources of the same truth is the
721
+ // problem being fixed here — one of them being a SUPERSET is not.
722
+ const extra = {
723
+ ...(input.nextConfigFile !== null && input.nextConfigFile !== undefined
724
+ ? { [input.nextConfigFile]: input.nextConfigSource ?? undefined }
725
+ : {}),
726
+ ...(input.nextLayout ? { [input.nextLayout.path]: input.nextLayout.source } : {}),
727
+ };
728
+ const read = (file) => extra[file] ?? input.cspSources?.[file];
729
+ const findings = diagnoseWebCsp(read, input.options.port ?? RETICLE_DEFAULT_PORT, [
730
+ ...Object.keys(extra),
731
+ ]);
732
+ const first = findings[0];
733
+ if (first === undefined)
734
+ return [];
735
+ return [
736
+ {
737
+ title: CSP_STEP_TITLE,
738
+ target: first.file,
739
+ status: StepStatus.NOTICE,
740
+ // `problem` already ends with the text to paste; `fix` is the same sentence for callers that
741
+ // want it on its own (doctor renders them separately). Printing both said it twice.
742
+ detail: first.problem,
743
+ },
744
+ ];
745
+ }
746
+ /**
747
+ * The plain-HTML path: no bundler to hook, so the connect snippet is printed for a hand edit.
748
+ *
749
+ * A function of its own, and NOT a fallthrough. Every framework is sent here on purpose or not at
750
+ * all — a member of `Framework` with no adapter entry is a compile error in `FRAMEWORK_ADAPTERS`,
751
+ * where the if/else chain this replaced used to hand it these instructions silently.
752
+ */
753
+ export function htmlSteps(input) {
754
+ return [
755
+ {
756
+ title: StepTitle.CONNECT_SNIPPET,
757
+ target: 'index.html',
758
+ status: StepStatus.MANUAL,
759
+ detail: htmlManual(input.options.port, input.options.projectId, input.pairingToken),
760
+ },
761
+ ];
762
+ }
763
+ /**
764
+ * The per-framework half of the plan, looked up rather than switched.
765
+ *
766
+ * `FRAMEWORK_ADAPTERS` is a `Record<Framework, FrameworkAdapter>`, so a member added to `Framework`
767
+ * is a compile error in that one table instead of a framework the plan quietly does not serve. The
768
+ * exhaustive switch this replaced gave the same guarantee for the steps alone; the record gives it
769
+ * for the packages, the connect-step titles and the unverified-note flag in the same edit.
770
+ */
771
+ export function frameworkSteps(input) {
772
+ return FRAMEWORK_ADAPTERS[input.detection.framework].steps(input);
773
+ }