@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
@@ -114,6 +114,13 @@ export interface PlanInput {
114
114
  claudeCli: boolean;
115
115
  /** Whether an `reticle` MCP server is already registered with Claude (any scope) — idempotency. */
116
116
  mcpExists: boolean;
117
+ /**
118
+ * Whether `init` is running inside Claude Code itself, which is the one case where writing its
119
+ * project-scope `.mcp.json` is known to be wanted. See `CLAUDE_PROJECT_SPEC`.
120
+ */
121
+ insideClaudeCode?: boolean | undefined;
122
+ /** The project `.mcp.json` at the agent's root, read so the merge stays idempotent. */
123
+ claudeProjectConfig?: string | null | undefined;
117
124
  /** `process.platform`. Injected so this module stays pure. Windows is the only branch. */
118
125
  platform?: string;
119
126
  /**
@@ -175,10 +182,15 @@ export interface PlanInput {
175
182
  source: string;
176
183
  } | null | undefined;
177
184
  /**
178
- * Existing `src/env.d.ts` content, when present — where the Vite-define ambient declarations go
179
- * so `astro check` can see `__RETICLE_TOKEN__` / `__RETICLE_ROOT__` (#677).
185
+ * Existing `src/env.d.ts` content, when present — where the window ambient declarations go
186
+ * so `astro check` can see `__RETICLE_TOKEN__` / `__RETICLE_ROOT__` (#677, #1008).
180
187
  */
181
188
  astroEnvDts?: string | null | undefined;
189
+ /**
190
+ * Existing `src/components/ReticleDev.ts` content, when present — the local module whose
191
+ * static SDK import is what actually dials the daemon.
192
+ */
193
+ astroReticleDev?: string | null | undefined;
182
194
  /** Discovered Next config filename (e.g. 'next.config.mjs'), or null. */
183
195
  nextConfigFile: string | null;
184
196
  /** Source of that Next config, so the export can be wrapped in withReticle. */
@@ -214,6 +226,8 @@ export interface PlanInput {
214
226
  viteDevModuleExists?: boolean | undefined;
215
227
  /** Whether src/hooks.client.ts already exists (SvelteKit idempotency). */
216
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;
217
231
  /** Whether app/entry.client.tsx already exists — it decides whether init writes one or patches it. */
218
232
  reactRouterEntryExists?: boolean;
219
233
  /**
@@ -224,6 +238,8 @@ export interface PlanInput {
224
238
  * added to whatever the app already put in it. Existence alone could only ever print a recipe.
225
239
  */
226
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;
227
243
  /** Discovered Nuxt config: its path + source, or null. It is where the pairing token is inlined. */
228
244
  nuxtConfig?: {
229
245
  path: string;
@@ -233,6 +249,8 @@ export interface PlanInput {
233
249
  nuxtHasAppDir?: boolean | undefined;
234
250
  /** Whether the Nuxt client plugin already exists (idempotency — the file is the owner's to edit). */
235
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;
236
254
  /**
237
255
  * TanStack Start's document module, when found (`src/routes/__root.tsx` or `app/routes/__root.tsx`).
238
256
  *
@@ -240,6 +258,29 @@ export interface PlanInput {
240
258
  * 500s — but the path has to be the one that actually exists, not a guess.
241
259
  */
242
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;
243
284
  /** CRA's bundled entry (src/index.tsx or .js) — where the connect import has to go. */
244
285
  craEntry?: {
245
286
  path: string;
@@ -283,6 +324,10 @@ export interface PlanInput {
283
324
  claudeCommandContent?: string | null | undefined;
284
325
  /** Current .cursor/commands/reticle.md content, or null when absent. */
285
326
  cursorCommandContent?: string | null | undefined;
327
+ /** `--hooks`: install the print-only Claude Code Stop hook. Off unless asked. */
328
+ hooks?: boolean | undefined;
329
+ /** Current .claude/settings.json content, or null when absent (the hook is MERGED into it). */
330
+ claudeSettingsContent?: string | null | undefined;
286
331
  /**
287
332
  * Absolute directory the AGENT runs in, when that is not the app's directory.
288
333
  *
@@ -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[];
@@ -0,0 +1,256 @@
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 { newViteConfig, patchViteConfig, VitePatchKind, VITE_IMPORT } from '../patch/vite-config.js';
11
+ import { viteManual, viteDevModuleFile, VITE_DEV_MODULE_PATH } from '../patch/snippets.js';
12
+ import { hasOptOut, OPT_OUT_MARKER } from '../detect/declared/init-opt-out.js';
13
+ import { Framework } from '../detect/detect.js';
14
+ import { StepStatus } from './plan-types.js';
15
+ import { StepTitle } from './connect-steps.js';
16
+ import { alreadyOrMovedPort } from './port-steps.js';
17
+ /** What adding `reticle()` to a Vite config buys, which differs by framework. */
18
+ export const VITE_PLUGIN_DETAIL = {
19
+ /** A plain Vite app gets both halves from the plugin. */
20
+ VITE: 'add reticle() to plugins (also injects connect())',
21
+ /**
22
+ * SvelteKit renders through app.html, so the plugin's HTML injection never fires and connect()
23
+ * comes from the client hook instead. The plugin is still required: it is what stamps
24
+ * data-reticle-source into .svelte components, and without it every verdict on a SvelteKit app
25
+ * comes back with no file:line at all.
26
+ */
27
+ SVELTEKIT: 'add reticle({ inject: false }) to plugins (stamps data-reticle-source in .svelte components; the client hook connects)',
28
+ /**
29
+ * React Router framework mode renders through its own request handler, so the plugin's HTML
30
+ * injection never fires and connect() comes from the client entry instead. The plugin is still
31
+ * required for the same reason it is under SvelteKit: without it every verdict on the app comes
32
+ * back with no file:line at all.
33
+ */
34
+ REACT_ROUTER: 'add reticle() to plugins (stamps data-reticle-source in .tsx components)',
35
+ /**
36
+ * TanStack Start SSRs its own HTML, so the plugin's HTML injection never fires and connect()
37
+ * comes from a client effect instead. `inject: false` is load-bearing honesty: leaving the
38
+ * default would keep reporting "also injects connect()" for a transform that never runs.
39
+ * The plugin is still required for the same reason it is under SvelteKit: without it every
40
+ * verdict on the app comes back with no file:line at all.
41
+ */
42
+ TANSTACK_START: 'add reticle({ inject: false }) to plugins (stamps data-reticle-source; Start SSRs its own HTML so connect() cannot come from the plugin)',
43
+ };
44
+ const CAPABILITIES_TITLE = StepTitle.CAPABILITIES;
45
+ /**
46
+ * The one step `init` genuinely cannot finish, addressed to the AGENT reading this report.
47
+ *
48
+ * It used to read "Capabilities are empty until you edit that file" and describe the consequence to
49
+ * a human — homework, in a report a human mostly does not read, naming no file to open and no thing
50
+ * to look for. The agent that ran `init` is right there, has the codebase, and can finish this in
51
+ * the same minute; what it needed was the specific instruction, not the rationale.
52
+ *
53
+ * What is left is exactly what could not be PROVEN from source: which atoms/actor/queryClient an
54
+ * adapter-wrapped store should be built from, and which elements matter enough to name. `init`
55
+ * emits nothing it cannot prove, because a wrong import here throws on every dev page load — worse
56
+ * than an empty file.
57
+ */
58
+ const CAPABILITIES_TODO_TITLE = StepTitle.CAPABILITIES_TODO;
59
+ export function capabilitiesTodo(path, stores) {
60
+ return (`This app's state lives in a library nothing in the running app points at, so it is the one ` +
61
+ `thing Reticle cannot find for itself — reticle_state will not see it until you say where it ` +
62
+ `is.\n` +
63
+ `Do this now, before driving anything:\n` +
64
+ ` 1. In ${path}, uncomment the suggested line and complete it: ${stores.join('; ')}.\n` +
65
+ ` 2. Prove it: drive one flow and check reticle_state returns your key. An empty state read ` +
66
+ `means the line is still not doing anything, whatever else went green.`);
67
+ }
68
+ /**
69
+ * The dev module carrying `registerCapabilities` / `registerStore`.
70
+ *
71
+ * Without it every app came up `hasCapabilities: false` with a `reticle_state` holding nothing but
72
+ * `__reticle_renders` — the state-truth read was unavailable on every app out of the box. Written
73
+ * only when absent, because it is the one generated file a user is expected to EDIT.
74
+ */
75
+ /**
76
+ * Is there state left that ONLY the app can hand over?
77
+ *
78
+ * This used to ask a much bigger question — "does the generated file register anything at all?" —
79
+ * and fired on almost every install, because almost every install generated a file whose testids
80
+ * and stores were both empty. The answer was homework: go read the source, uncomment a line, add
81
+ * data-testid attributes, then drive to prove it. Several turns, on every onboarding.
82
+ *
83
+ * Two of those three are no longer anyone's homework. Testids are read from the live DOM, and a
84
+ * store passed through a React context provider (Redux, TanStack Query) is discovered and
85
+ * registered on the first commit. What is left is the genuinely unreachable case: a module-scope
86
+ * store, or one needing an argument only the source supplies — Zustand, Jotai atoms, an XState
87
+ * actor. Nothing in the running app points at those, so the notice still has to fire, and it now
88
+ * fires ONLY there.
89
+ *
90
+ * `wired` are stores init resolved and wrote a live `registerStore` call for. Hints are not
91
+ * registrations: they land in the file as a commented line, and counting one as a registration
92
+ * silences the notice whose whole purpose is to say "act on the hint".
93
+ */
94
+ export function needsManualStore(hints, wired) {
95
+ return hints.length > 0 && 0 === wired.length;
96
+ }
97
+ export function capabilitiesStep(input, path = VITE_DEV_MODULE_PATH) {
98
+ if (true === input.viteDevModuleExists) {
99
+ return [
100
+ {
101
+ title: CAPABILITIES_TITLE,
102
+ target: path,
103
+ status: StepStatus.ALREADY,
104
+ detail: 'file exists, left alone, it is yours to edit',
105
+ },
106
+ ];
107
+ }
108
+ const testids = input.testids ?? [];
109
+ const stores = input.storeHints ?? [];
110
+ const wired = input.foundStores ?? [];
111
+ // Testids no longer need counting here. They are read from the live DOM at announce time, so a
112
+ // number printed at install time would be a stale claim about a codebase that is about to change —
113
+ // which is exactly what "no data-testid values yet" used to be on an app whose testids arrive with
114
+ // a lazy route.
115
+ const found = 'testids read from the live DOM';
116
+ // A store we found and WIRED is a registration, so the notice must not fire. A store HINT is not:
117
+ // `stores` holds suggestions, written into the file as a commented line of the form
118
+ // `// import your store, then: registerStore(...)`. Counting a suggestion as a registration let
119
+ // the hint silence the notice whose entire job is to say "act on the hint".
120
+ //
121
+ // Measured against a real product UI (rowy — 70+ deps, jotai, a whole src/atoms tree): init
122
+ // detected jotai, offered one commented line, emitted NO notice, and wrote
123
+ // `registerCapabilities({ testids: [], signals: [], stores: [] })`. So `hasCapabilities` stayed
124
+ // false, `reticle_state` was empty forever, and the install gate reported "connected: 1,
125
+ // manual ⚠: none". Every check green, state observability zero.
126
+ //
127
+ // jotai is still exactly that case. A Redux or TanStack app is not, any more: its store rides a
128
+ // context provider and the React adapter registers it on the first commit, so `storeHints` no
129
+ // longer names either one and this notice no longer fires for them.
130
+ const nothingToRegister = needsManualStore(stores, wired);
131
+ return [
132
+ {
133
+ title: CAPABILITIES_TITLE,
134
+ target: path,
135
+ status: StepStatus.APPLY,
136
+ detail: `${found}; ${wired.length > 0
137
+ ? `registered ${wired.map((s) => `'${s.key}'`).join(', ')} from your source`
138
+ : stores.length > 0
139
+ ? `store: uncomment the ${String(stores.length)} suggested line(s)`
140
+ : 'no state library detected'}`,
141
+ write: {
142
+ path,
143
+ content: viteDevModuleFile(testids, stores, wired, input.detection.uiLibrary, input.detection.framework),
144
+ },
145
+ dependsOnInstall: true,
146
+ },
147
+ // The write is real; what it registers is not. `registerCapabilities({ testids: [], signals: [],
148
+ // stores: [] })` registers nothing, so `hasCapabilities` stays false — correctly — and the ✓
149
+ // above reads as if the feature is on. Reported as exactly that confusion: "hasCapabilities false
150
+ // on every session while init said ✓ Capabilities + store".
151
+ //
152
+ // Beside the write rather than replacing it, for two reasons: only APPLY steps are written
153
+ // (run.ts), and SKILL.md tells the reader to skip ✓ lines — so a caveat carried on the ✓ is a
154
+ // caveat nobody reads.
155
+ ...(nothingToRegister
156
+ ? [
157
+ {
158
+ title: CAPABILITIES_TODO_TITLE,
159
+ target: path,
160
+ status: StepStatus.NOTICE,
161
+ detail: capabilitiesTodo(path, stores),
162
+ },
163
+ ]
164
+ : []),
165
+ ];
166
+ }
167
+ export function viteSteps(input, detail = VITE_PLUGIN_DETAIL.VITE, inject = true) {
168
+ // Capabilities are independent of whether the config needed patching. Attaching them to the APPLY
169
+ // branch meant a re-run on an already-wired app silently never created the module.
170
+ return [...viteConfigSteps(input, detail, inject), ...capabilitiesStep(input)];
171
+ }
172
+ function viteConfigSteps(input, detail, inject = true) {
173
+ const cfg = input.viteConfig;
174
+ const port = input.options.port;
175
+ // Stamp `data-reticle-source` unless this app renders through a non-DOM React reconciler, where a
176
+ // lowercase JSX tag is not an element and the stamp crashes the app at commit time. See
177
+ // `Detection.customReconciler`.
178
+ const stampSource = true !== input.detection?.customReconciler;
179
+ // An explicit "not here" is not an invitation. `init` runs unattended in a repo it has just met,
180
+ // and it was reported adding the plugin to an app whose config said Reticle was deliberately
181
+ // excluded. A NOTICE rather than a ⚠: opting out is a decision, not something to go and fix.
182
+ if (cfg !== null && hasOptOut(cfg.source)) {
183
+ return [
184
+ {
185
+ title: StepTitle.VITE_PLUGIN,
186
+ target: cfg.path,
187
+ status: StepStatus.NOTICE,
188
+ detail: `left alone: this config carries ${OPT_OUT_MARKER}. Remove that marker to instrument this app.`,
189
+ },
190
+ ];
191
+ }
192
+ // Plain Vite runs happily with no config file (`npm create vite`'s vanilla template has none), so
193
+ // make one. Only there: a Vite-based framework's missing config also lost the framework's own
194
+ // plugin, and a file carrying ours alone would not boot the app.
195
+ const plainVite = Framework.VITE === input.detection.framework;
196
+ if (null === cfg) {
197
+ if (!plainVite) {
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 path = input.detection.typescript ? 'vite.config.ts' : 'vite.config.mjs';
208
+ return [
209
+ {
210
+ title: StepTitle.VITE_PLUGIN,
211
+ target: path,
212
+ status: StepStatus.APPLY,
213
+ detail,
214
+ write: {
215
+ path,
216
+ content: newViteConfig(port, true === input.captureBodies, inject, stampSource),
217
+ expect: [VITE_IMPORT, 'reticle('],
218
+ },
219
+ dependsOnInstall: true,
220
+ },
221
+ ];
222
+ }
223
+ const patch = patchViteConfig(cfg.source, port, true === input.captureBodies, inject, stampSource);
224
+ if (patch.kind === VitePatchKind.ALREADY) {
225
+ return [
226
+ alreadyOrMovedPort({
227
+ title: StepTitle.VITE_PLUGIN,
228
+ target: cfg.path,
229
+ status: StepStatus.ALREADY,
230
+ detail: 'reticle() already in plugins',
231
+ }, cfg.source, port),
232
+ ];
233
+ }
234
+ if (patch.kind === VitePatchKind.MANUAL) {
235
+ return [
236
+ {
237
+ title: StepTitle.VITE_PLUGIN,
238
+ target: cfg.path,
239
+ status: StepStatus.MANUAL,
240
+ detail: `${patch.reason}\n\n${viteManual(port, input.detection.uiLibrary, inject, stampSource)}`,
241
+ },
242
+ ];
243
+ }
244
+ return [
245
+ {
246
+ title: StepTitle.VITE_PLUGIN,
247
+ target: cfg.path,
248
+ status: StepStatus.APPLY,
249
+ detail,
250
+ // Both halves, because either alone is a config that does not wire: the import without the
251
+ // call leaves `plugins` untouched, and the call without the import does not build.
252
+ write: { path: cfg.path, content: patch.code, expect: [VITE_IMPORT, 'reticle('] },
253
+ dependsOnInstall: true,
254
+ },
255
+ ];
256
+ }