@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
@@ -15,10 +15,39 @@
15
15
  * Half-editing somebody's build config is worse than a documented manual step.
16
16
  */
17
17
  import { ReticleDir } from '@reticlehq/core';
18
+ import { parseMajor } from '../detect/detect.js';
18
19
  import { blockAfter } from './brace-scan.js';
19
20
  import { PatchKind } from './patch-kind.js';
21
+ /** The first Vite whose dep optimizer is rolldown — the same line the Vite plugin draws. */
22
+ const ROLLDOWN_OPTIMIZER_MAJOR = 7;
23
+ function writesEsbuildTarget(config) {
24
+ const major = config.viteMajor;
25
+ return undefined === major || null === major || major < ROLLDOWN_OPTIMIZER_MAJOR;
26
+ }
27
+ /** The optimizer target, in each of the two shapes this file writes it. */
28
+ const ESBUILD_TARGET = "esbuildOptions: { target: 'es2022' }";
29
+ const ESBUILD_TARGET_INNER = `\n ${ESBUILD_TARGET},`;
30
+ const ESBUILD_TARGET_WHOLE = `, ${ESBUILD_TARGET}`;
31
+ /**
32
+ * A config an older `init` patched, with the esbuild target taken back out on a rolldown Vite.
33
+ *
34
+ * Only our own literal, in the exact shapes we wrote it — an `esbuildOptions` the app set itself is
35
+ * theirs, and is left to Vite's deprecation notice to talk about.
36
+ */
37
+ function withoutStaleEsbuildTarget(source, config) {
38
+ if (writesEsbuildTarget(config) || !source.includes(ESBUILD_TARGET))
39
+ return null;
40
+ const out = source.replace(ESBUILD_TARGET_INNER, '').replace(ESBUILD_TARGET_WHOLE, '');
41
+ return out === source ? null : out;
42
+ }
20
43
  /** Present in a patched config AND in a hand-followed recipe — so both count as already wired. */
21
- const CONFIG_MARKER = '__RETICLE_TOKEN__';
44
+ const DEFINE_TOKEN_MARKER = '__RETICLE_TOKEN__';
45
+ function usesDefineToken(config) {
46
+ return config.defineToken !== false;
47
+ }
48
+ function alreadyMarker(config) {
49
+ return config.alreadyMarker ?? DEFINE_TOKEN_MARKER;
50
+ }
22
51
  /**
23
52
  * The SDK, declared so Vite pre-bundles it BEFORE the first page load.
24
53
  *
@@ -82,30 +111,36 @@ import { join } from 'node:path';
82
111
  * cosmetic: Astro's default down-levels the modern SDK bundle and dies on a destructuring transform.
83
112
  */
84
113
  function viteKeys(config) {
85
- return [
114
+ const keys = [
86
115
  { key: 'build', inner: `\n target: 'es2022',` },
87
116
  {
88
117
  key: 'optimizeDeps',
89
- inner: `\n include: [${sdkInclude(config)}],\n esbuildOptions: { target: 'es2022' },`,
90
- },
91
- {
92
- key: 'define',
93
- inner: `\n __RETICLE_TOKEN__: JSON.stringify(reticleToken()),\n __RETICLE_ROOT__: JSON.stringify(process.cwd()),`,
118
+ inner: `\n include: [${sdkInclude(config)}],${writesEsbuildTarget(config) ? ESBUILD_TARGET_INNER : ''}`,
94
119
  },
95
120
  // Merged in first, so a `server: { port }` the app already set keeps its port. An app that
96
121
  // already sets `server.watch` itself is the one shape this loses to — the inner `watch` key would
97
122
  // be duplicated and the app's would win — which is the same nested-merge ceiling `build` has.
98
123
  { key: 'server', inner: `\n watch: { ignored: [${WATCH_IGNORE_LITERAL}] },` },
99
124
  ];
125
+ if (usesDefineToken(config)) {
126
+ keys.splice(2, 0, {
127
+ key: 'define',
128
+ inner: `\n __RETICLE_TOKEN__: JSON.stringify(reticleToken()),\n __RETICLE_ROOT__: JSON.stringify(process.cwd()),`,
129
+ });
130
+ }
131
+ return keys;
100
132
  }
101
133
  /** Whole-key form, for a `vite:` block that does not have the key at all. */
102
134
  function wholeKeys(config) {
103
- return {
135
+ const keys = {
104
136
  build: `\n build: { target: 'es2022' },`,
105
- optimizeDeps: `\n optimizeDeps: { include: [${sdkInclude(config)}], esbuildOptions: { target: 'es2022' } },`,
106
- define: `\n define: {\n __RETICLE_TOKEN__: JSON.stringify(reticleToken()),\n __RETICLE_ROOT__: JSON.stringify(process.cwd()),\n },`,
137
+ optimizeDeps: `\n optimizeDeps: { include: [${sdkInclude(config)}]${writesEsbuildTarget(config) ? ESBUILD_TARGET_WHOLE : ''} },`,
107
138
  server: `\n server: { watch: { ignored: [${WATCH_IGNORE_LITERAL}] } },`,
108
139
  };
140
+ if (usesDefineToken(config)) {
141
+ keys.define = `\n define: {\n __RETICLE_TOKEN__: JSON.stringify(reticleToken()),\n __RETICLE_ROOT__: JSON.stringify(process.cwd()),\n },`;
142
+ }
143
+ return keys;
109
144
  }
110
145
  /**
111
146
  * Add our keys to an existing `vite: { ... }` block, merging into any the app already set.
@@ -160,20 +195,39 @@ function withoutInclude(inner, config) {
160
195
  * that ran `init`.
161
196
  */
162
197
  function viteBlock(config) {
163
- return ` vite: {
164
- build: { target: 'es2022' },
165
- optimizeDeps: { include: [${sdkInclude(config)}], esbuildOptions: { target: 'es2022' } },
166
- define: {
198
+ const marker = usesDefineToken(config) ? '' : ` /* ${alreadyMarker(config)} */\n`;
199
+ const define = usesDefineToken(config)
200
+ ? ` define: {
167
201
  __RETICLE_TOKEN__: JSON.stringify(reticleToken()),
168
202
  __RETICLE_ROOT__: JSON.stringify(process.cwd()),
169
203
  },
170
- server: { watch: { ignored: [${WATCH_IGNORE_LITERAL}] } },
204
+ `
205
+ : '';
206
+ return ` vite: {
207
+ ${marker} build: { target: 'es2022' },
208
+ optimizeDeps: { include: [${sdkInclude(config)}]${writesEsbuildTarget(config) ? ESBUILD_TARGET_WHOLE : ''} },
209
+ ${define} server: { watch: { ignored: [${WATCH_IGNORE_LITERAL}] } },
171
210
  },
172
211
  `;
173
212
  }
213
+ function withHelper(source, config) {
214
+ if (!usesDefineToken(config)) {
215
+ return source.endsWith('\n') ? source : `${source}\n`;
216
+ }
217
+ return `${HELPER_IMPORTS}${source.trimStart()}\n${HELPER}`.trimEnd() + '\n';
218
+ }
219
+ function withOwningMarker(source, config, braceAt) {
220
+ if (usesDefineToken(config) || source.includes(alreadyMarker(config)))
221
+ return source;
222
+ return insertAt(source, braceAt, `\n /* ${alreadyMarker(config)} */`);
223
+ }
174
224
  export function patchViteOwningConfig(source, config) {
175
- if (source.includes(CONFIG_MARKER))
176
- return { kind: PatchKind.ALREADY };
225
+ if (source.includes(alreadyMarker(config))) {
226
+ const cleaned = withoutStaleEsbuildTarget(source, config);
227
+ return null === cleaned
228
+ ? { kind: PatchKind.ALREADY }
229
+ : { kind: PatchKind.APPLY, code: cleaned };
230
+ }
177
231
  // A `vite: { ... }` block is an object literal, so our keys can go straight after the brace and
178
232
  // whatever is already in there is untouched. Refusing outright was the only genuine install defect
179
233
  // left in the gate: the config bailed while the LAYOUT patch applied anyway, leaving an app with a
@@ -186,7 +240,7 @@ export function patchViteOwningConfig(source, config) {
186
240
  if (merged !== null) {
187
241
  return {
188
242
  kind: PatchKind.APPLY,
189
- code: `${HELPER_IMPORTS}${merged.trimStart()}\n${HELPER}`.trimEnd() + '\n',
243
+ code: withHelper(withOwningMarker(merged, config, at), config),
190
244
  };
191
245
  }
192
246
  return {
@@ -214,6 +268,37 @@ export function patchViteOwningConfig(source, config) {
214
268
  const withBlock = `${source.slice(0, at)}\n${viteBlock(config)}${source.slice(at)}`;
215
269
  return {
216
270
  kind: PatchKind.APPLY,
217
- code: `${HELPER_IMPORTS}${withBlock.trimStart()}\n${HELPER}`.trimEnd() + '\n',
271
+ code: withHelper(withBlock, config),
218
272
  };
219
273
  }
274
+ const HOISTED_VITE = 'node_modules/vite/package.json';
275
+ const PNPM_STORE = 'node_modules/.pnpm';
276
+ /** A pnpm store entry for vite itself: `vite@8.0.16` or `vite@7.1.2_@types+node@24…`. */
277
+ const PNPM_VITE_ENTRY = /^vite@(\d+)\./;
278
+ export function installedViteMajor(io, pkg) {
279
+ const hoisted = io.readFile(HOISTED_VITE);
280
+ if (null !== hoisted) {
281
+ try {
282
+ const parsed = JSON.parse(hoisted);
283
+ const version = 'object' === typeof parsed && null !== parsed
284
+ ? parsed.version
285
+ : undefined;
286
+ const major = 'string' === typeof version ? parseMajor(version) : undefined;
287
+ if (major !== undefined)
288
+ return major;
289
+ }
290
+ catch {
291
+ // Unreadable manifest: fall through to the store and the declared range.
292
+ }
293
+ }
294
+ // ponytail: several copies can sit in the store and the newest is taken; resolving the framework's
295
+ // own `vite` through its dependency chain is the upgrade if a stale newer copy ever misleads this.
296
+ const stored = io
297
+ .listDirs(PNPM_STORE)
298
+ .map((name) => PNPM_VITE_ENTRY.exec(name)?.[1])
299
+ .filter((major) => major !== undefined)
300
+ .map(Number);
301
+ if (stored.length > 0)
302
+ return Math.max(...stored);
303
+ return parseMajor(pkg.dependencies?.['vite'] ?? pkg.devDependencies?.['vite']) ?? null;
304
+ }
@@ -27,15 +27,22 @@ export declare const StepTitle: {
27
27
  readonly NUXT_RESTART: "Restart the Nuxt dev server";
28
28
  readonly CONNECT_SNIPPET_REACT_ROUTER: "Connect snippet (React Router)";
29
29
  readonly CONNECT_SNIPPET_TANSTACK_START: "Connect snippet (TanStack Start)";
30
+ readonly TANSTACK_START_CONNECT_COMPONENT: "ReticleConnect component (TanStack Start)";
31
+ readonly CONNECT_SNIPPET_REMIX: "Connect snippet (Remix)";
32
+ readonly CONNECT_SNIPPET_ANGULAR: "Connect snippet (Angular)";
33
+ readonly ANGULAR_TOKEN_PROXY: "Angular dev-server token route";
34
+ readonly ANGULAR_SERVE_CONFIG: "Angular serve target (proxyConfig)";
35
+ readonly ANGULAR_SETUP_GATED: "Angular setup verified, drive unverified";
30
36
  readonly ELECTRON_VITE_PLUGIN: "Vite plugin (electron-vite renderer)";
31
37
  readonly ELECTRON_PRELOAD: "Electron preload (IPC shim)";
32
38
  readonly ELECTRON_CAPTURE: "Electron capture (screenshots)";
33
39
  readonly TANSTACK_START_UNVERIFIED: "TanStack Start is UNVERIFIED";
34
- readonly SVELTEKIT_UNVERIFIED: "SvelteKit is UNVERIFIED";
40
+ readonly SVELTEKIT_SETUP_GATED: "SvelteKit setup verified, drive unverified";
35
41
  readonly CLIENT_HOOK: "Reticle client hook";
36
42
  readonly CONNECT_SNIPPET_ASTRO: "Connect snippet (Astro)";
37
- readonly ASTRO_CONFIG: "Astro config (token + build target)";
38
- readonly ASTRO_ENV_DTS: "Astro env types (Vite defines)";
43
+ readonly ASTRO_RETICLE_DEV: "Astro ReticleDev module";
44
+ readonly ASTRO_CONFIG: "Astro config (build target)";
45
+ readonly ASTRO_ENV_DTS: "Astro env types (window token)";
39
46
  readonly CSP: "Content-Security-Policy";
40
47
  readonly CONNECT_SNIPPET: "Connect snippet";
41
48
  };
@@ -28,15 +28,22 @@ export const StepTitle = {
28
28
  NUXT_RESTART: 'Restart the Nuxt dev server',
29
29
  CONNECT_SNIPPET_REACT_ROUTER: 'Connect snippet (React Router)',
30
30
  CONNECT_SNIPPET_TANSTACK_START: 'Connect snippet (TanStack Start)',
31
+ TANSTACK_START_CONNECT_COMPONENT: 'ReticleConnect component (TanStack Start)',
32
+ CONNECT_SNIPPET_REMIX: 'Connect snippet (Remix)',
33
+ CONNECT_SNIPPET_ANGULAR: 'Connect snippet (Angular)',
34
+ ANGULAR_TOKEN_PROXY: 'Angular dev-server token route',
35
+ ANGULAR_SERVE_CONFIG: 'Angular serve target (proxyConfig)',
36
+ ANGULAR_SETUP_GATED: 'Angular setup verified, drive unverified',
31
37
  ELECTRON_VITE_PLUGIN: 'Vite plugin (electron-vite renderer)',
32
38
  ELECTRON_PRELOAD: 'Electron preload (IPC shim)',
33
39
  ELECTRON_CAPTURE: 'Electron capture (screenshots)',
34
40
  TANSTACK_START_UNVERIFIED: 'TanStack Start is UNVERIFIED',
35
- SVELTEKIT_UNVERIFIED: 'SvelteKit is UNVERIFIED',
41
+ SVELTEKIT_SETUP_GATED: 'SvelteKit setup verified, drive unverified',
36
42
  CLIENT_HOOK: 'Reticle client hook',
37
43
  CONNECT_SNIPPET_ASTRO: 'Connect snippet (Astro)',
38
- ASTRO_CONFIG: 'Astro config (token + build target)',
39
- ASTRO_ENV_DTS: 'Astro env types (Vite defines)',
44
+ ASTRO_RETICLE_DEV: 'Astro ReticleDev module',
45
+ ASTRO_CONFIG: 'Astro config (build target)',
46
+ ASTRO_ENV_DTS: 'Astro env types (window token)',
40
47
  CSP: CSP_STEP_TITLE,
41
48
  CONNECT_SNIPPET: 'Connect snippet',
42
49
  };
@@ -54,6 +61,7 @@ export const CONNECT_STEP_TITLES = new Set([
54
61
  StepTitle.CONNECT_SNIPPET,
55
62
  StepTitle.CONNECT_SNIPPET_CRA,
56
63
  StepTitle.CONNECT_SNIPPET_ASTRO,
64
+ StepTitle.ASTRO_RETICLE_DEV,
57
65
  StepTitle.CONNECT_SNIPPET_NUXT,
58
66
  // The config is the other half of the Nuxt connect, not a nicety: it is the only thing in a Nuxt
59
67
  // app that can inline the pairing token, and the bridge refuses a connect without one even on
@@ -61,6 +69,14 @@ export const CONNECT_STEP_TITLES = new Set([
61
69
  StepTitle.NUXT_CONFIG,
62
70
  StepTitle.CONNECT_SNIPPET_REACT_ROUTER,
63
71
  StepTitle.CONNECT_SNIPPET_TANSTACK_START,
72
+ // The component and the line that renders it are two steps, for the reason Next's are.
73
+ StepTitle.TANSTACK_START_CONNECT_COMPONENT,
74
+ StepTitle.CONNECT_SNIPPET_REMIX,
75
+ StepTitle.CONNECT_SNIPPET_ANGULAR,
76
+ // Both halves of how an Angular page gets its pairing token. Without the route the connect is
77
+ // refused on the token, and without the serve-target entry `ng serve` never mounts the route.
78
+ StepTitle.ANGULAR_TOKEN_PROXY,
79
+ StepTitle.ANGULAR_SERVE_CONFIG,
64
80
  StepTitle.CLIENT_HOOK,
65
81
  StepTitle.CONNECT_MODULE,
66
82
  StepTitle.RETICLE_DEV_COMPONENT,
@@ -85,6 +101,9 @@ export const CONNECT_STEP_TITLES = new Set([
85
101
  // electron-vite's connect IS the renderer plugin. Without it the SDK never injects, and the
86
102
  // preload/capture steps are not enough to produce a session.
87
103
  StepTitle.ELECTRON_VITE_PLUGIN,
104
+ // A policy that refuses the bridge WebSocket is a guaranteed non-connection on EVERY framework, so
105
+ // a ⚠ on it (a policy init could not edit) must fail init rather than read as advice.
106
+ StepTitle.CSP,
88
107
  ]);
89
108
  /** True when this step is what makes the app dial the daemon. */
90
109
  export function isConnectStep(title) {
@@ -18,17 +18,13 @@
18
18
  *
19
19
  * `framework-adapter.test.ts` asserts the seam between the two halves.
20
20
  */
21
- import { Framework } from '../detect/detect.js';
21
+ import { Framework, UiLibrary } from '../detect/detect.js';
22
22
  import { StepTitle } from './connect-steps.js';
23
23
  import type { PlanInput, Step } from './plan-types.js';
24
24
  export declare const RETICLE_REACT_KIT = "@reticlehq/react";
25
25
  /** The framework-neutral sensor, for stacks the React adapter has nothing to attach to. */
26
26
  export declare const RETICLE_BROWSER_SDK = "@reticlehq/browser";
27
- export declare const RETICLE_VITE_PLUGIN = "@reticlehq/vite-plugin";
28
- export declare const RETICLE_NEXT_PLUGIN = "@reticlehq/next";
29
- /** The Electron main/preload helper — what makes IPC and screenshots exist at all. */
30
- export declare const RETICLE_ELECTRON = "@reticlehq/electron";
31
- export interface FrameworkAdapter {
27
+ interface FrameworkAdapter {
32
28
  /**
33
29
  * The dev dependencies `init` installs, given the kit the UI-library check chose
34
30
  * (`@reticlehq/react` or the neutral sensor). A framework that knows its own renderer ignores the
@@ -57,6 +53,8 @@ export interface FrameworkAdapter {
57
53
  */
58
54
  readonly carriesOwnUnverifiedNote: boolean;
59
55
  }
56
+ /** Whether the install gate scaffolds this stack — see INSTALL_GATE_STACKS. */
57
+ export declare function installGated(framework: Framework, uiLibrary: UiLibrary): boolean;
60
58
  export declare const FRAMEWORK_ADAPTERS: Record<Framework, FrameworkAdapter>;
61
59
  /**
62
60
  * The per-framework half of the plan, looked up rather than switched.
@@ -67,3 +65,4 @@ export declare const FRAMEWORK_ADAPTERS: Record<Framework, FrameworkAdapter>;
67
65
  * for the packages, the connect-step titles and the unverified-note flag in the same edit.
68
66
  */
69
67
  export declare function frameworkSteps(input: PlanInput): Step[];
68
+ export {};
@@ -18,10 +18,40 @@
18
18
  *
19
19
  * `framework-adapter.test.ts` asserts the seam between the two halves.
20
20
  */
21
- import { Framework } from '../detect/detect.js';
21
+ import { Framework, UiLibrary } from '../detect/detect.js';
22
22
  import { StepTitle } from './connect-steps.js';
23
- import { VITE_PLUGIN_DETAIL, astroSteps, craSteps, htmlSteps, nextSteps, nuxtSteps, reactRouterSteps, svelteKitSteps, tanstackStartSteps, viteSteps, } from './plan-framework.js';
24
- import { electronViteSteps } from './plan-electron-vite.js';
23
+ import { astroSteps, craSteps, htmlSteps, nextSteps, nuxtSteps, reactRouterSteps, svelteKitSteps, tanstackStartSteps, } from './plan-framework.js';
24
+ import { VITE_PLUGIN_DETAIL, viteSteps } from './plan-vite.js';
25
+ import { electronForgeSteps, electronViteSteps } from './plan-electron-vite.js';
26
+ import { angularSteps } from './plan-angular.js';
27
+ import { REMIX_CLASSIC_MANUAL, remixEntryFile } from '../patch/remix.js';
28
+ import { StepStatus } from './plan-types.js';
29
+ /** Where the classic Remix compiler's ⚠ points: the file that says which compiler this is. */
30
+ const REMIX_CLASSIC_TARGET = 'remix.config.js';
31
+ /**
32
+ * Remix v2: React Router framework mode's steps, with Remix's own default entry and title.
33
+ *
34
+ * No Vite config is the classic compiler — `vitePlugin` is the only way Remix runs on Vite, and it
35
+ * lives in that file — and wiring the entry there would import a module nothing serves.
36
+ */
37
+ function remixSteps(input) {
38
+ if (null === input.viteConfig) {
39
+ return [
40
+ {
41
+ title: StepTitle.CONNECT_SNIPPET_REMIX,
42
+ target: REMIX_CLASSIC_TARGET,
43
+ status: StepStatus.MANUAL,
44
+ detail: REMIX_CLASSIC_MANUAL,
45
+ },
46
+ ];
47
+ }
48
+ return [
49
+ ...reactRouterSteps(input, StepTitle.CONNECT_SNIPPET_REMIX, remixEntryFile),
50
+ // The plugin stamps file:line, and it is also what serves the `/@reticle-connect` module the
51
+ // entry imports.
52
+ ...viteSteps(input, VITE_PLUGIN_DETAIL.REACT_ROUTER),
53
+ ];
54
+ }
25
55
  // An app dev installs exactly the audience-scoped browser-side dependencies — never the retired
26
56
  // `@reticlehq/core` umbrella (which dragged the Node MCP server + ws into every app). The kit is the
27
57
  // framework adapter (it re-exports the browser sensor), paired with that framework's dev-only build
@@ -29,10 +59,33 @@ import { electronViteSteps } from './plan-electron-vite.js';
29
59
  export const RETICLE_REACT_KIT = '@reticlehq/react';
30
60
  /** The framework-neutral sensor, for stacks the React adapter has nothing to attach to. */
31
61
  export const RETICLE_BROWSER_SDK = '@reticlehq/browser';
32
- export const RETICLE_VITE_PLUGIN = '@reticlehq/vite-plugin';
33
- export const RETICLE_NEXT_PLUGIN = '@reticlehq/next';
62
+ const RETICLE_VITE_PLUGIN = '@reticlehq/vite-plugin';
63
+ const RETICLE_NEXT_PLUGIN = '@reticlehq/next';
34
64
  /** The Electron main/preload helper — what makes IPC and screenshots exist at all. */
35
- export const RETICLE_ELECTRON = '@reticlehq/electron';
65
+ const RETICLE_ELECTRON = '@reticlehq/electron';
66
+ /**
67
+ * The stacks the install gate scaffolds from scratch on every change (`SCAFFOLDS` in
68
+ * `apps/e2e/install-gate.mjs`), as framework → the UI libraries its scaffolds render.
69
+ *
70
+ * The one place `init` says which setups are PROVEN. Nuxt was labelled "vue is UNVERIFIED" while the
71
+ * gate scaffolded it on every change, because nothing here knew the gate existed. Add a scaffold
72
+ * there, add its stack here. Only the setup is proven: no gate drives these apps to a verdict.
73
+ */
74
+ const INSTALL_GATE_STACKS = {
75
+ [Framework.VITE]: [UiLibrary.REACT, UiLibrary.VUE, UiLibrary.UNKNOWN],
76
+ [Framework.NEXT]: [UiLibrary.REACT],
77
+ [Framework.NUXT]: [UiLibrary.VUE],
78
+ [Framework.REACT_ROUTER]: [UiLibrary.REACT],
79
+ [Framework.REMIX]: [UiLibrary.REACT],
80
+ [Framework.ASTRO]: [UiLibrary.UNKNOWN],
81
+ [Framework.SVELTEKIT]: [UiLibrary.SVELTE],
82
+ [Framework.CRA]: [UiLibrary.REACT],
83
+ [Framework.ANGULAR]: [UiLibrary.UNKNOWN],
84
+ };
85
+ /** Whether the install gate scaffolds this stack — see INSTALL_GATE_STACKS. */
86
+ export function installGated(framework, uiLibrary) {
87
+ return INSTALL_GATE_STACKS[framework]?.includes(uiLibrary) ?? false;
88
+ }
36
89
  export const FRAMEWORK_ADAPTERS = {
37
90
  [Framework.NEXT]: {
38
91
  // Next is React by construction, so the detection cannot disagree in a way worth honouring.
@@ -72,7 +125,9 @@ export const FRAMEWORK_ADAPTERS = {
72
125
  packages: (kit) => [kit, RETICLE_VITE_PLUGIN],
73
126
  // The Vite plugin too, for the reason SvelteKit gets it: React Router framework mode IS a Vite
74
127
  // app, and the plugin is what stamps data-reticle-source. Without it the app connects and every
75
- // verdict comes back with no file:line.
128
+ // verdict comes back with no file:line. Injection stays ON, unlike SvelteKit and Start: the
129
+ // client entry imports `/@reticle-connect`, which the plugin serves only while inject is on, and
130
+ // that module is what loads the dev module here.
76
131
  steps: (input) => [
77
132
  ...reactRouterSteps(input),
78
133
  ...viteSteps(input, VITE_PLUGIN_DETAIL.REACT_ROUTER),
@@ -86,16 +141,22 @@ export const FRAMEWORK_ADAPTERS = {
86
141
  // The Vite plugin as well as the client hook. `init` already INSTALLS @reticlehq/vite-plugin for
87
142
  // SvelteKit and then never wired it into the config, so it sat in package.json doing nothing —
88
143
  // which is why a SvelteKit app connected fine and every verdict had no file:line.
89
- steps: (input) => [...svelteKitSteps(input), ...viteSteps(input, VITE_PLUGIN_DETAIL.SVELTEKIT)],
144
+ // `inject: false` because the client hook connects; the plugin's injection never fires here, and
145
+ // left on it only warns ten seconds into every page load that the app will never connect.
146
+ steps: (input) => [
147
+ ...svelteKitSteps(input),
148
+ ...viteSteps(input, VITE_PLUGIN_DETAIL.SVELTEKIT, false),
149
+ ],
90
150
  connectStepTitles: [StepTitle.CLIENT_HOOK, StepTitle.VITE_PLUGIN],
91
151
  carriesOwnUnverifiedNote: true,
92
152
  },
93
153
  [Framework.ASTRO]: {
94
154
  // Astro owns its own Vite instance and renders its own HTML, so there is no config for the
95
- // plugin to attach to — the kit alone, connected from a page <script> (see astroManual).
155
+ // plugin to attach to — the kit alone, connected from a local module the page imports
156
+ // (see astroManual).
96
157
  packages: (kit) => [kit],
97
158
  steps: astroSteps,
98
- connectStepTitles: [StepTitle.CONNECT_SNIPPET_ASTRO],
159
+ connectStepTitles: [StepTitle.CONNECT_SNIPPET_ASTRO, StepTitle.ASTRO_RETICLE_DEV],
99
160
  carriesOwnUnverifiedNote: false,
100
161
  },
101
162
  [Framework.ELECTRON_VITE]: {
@@ -108,6 +169,35 @@ export const FRAMEWORK_ADAPTERS = {
108
169
  connectStepTitles: [StepTitle.ELECTRON_VITE_PLUGIN],
109
170
  carriesOwnUnverifiedNote: false,
110
171
  },
172
+ [Framework.ELECTRON_FORGE]: {
173
+ // electron-vite's three packages, with the kit the UI-library check chose: Forge's Vite template
174
+ // renders no React, and `@reticlehq/react` carries `react` in its peer dependencies.
175
+ packages: (kit) => [kit, RETICLE_VITE_PLUGIN, RETICLE_ELECTRON],
176
+ steps: electronForgeSteps,
177
+ // The renderer config's plugin IS the connect, as under electron-vite.
178
+ connectStepTitles: [StepTitle.VITE_PLUGIN],
179
+ carriesOwnUnverifiedNote: false,
180
+ },
181
+ [Framework.REMIX]: {
182
+ // React Router framework mode's packages, for the reason its steps are that mode's: the same
183
+ // framework under its previous name.
184
+ packages: (kit) => [kit, RETICLE_VITE_PLUGIN],
185
+ steps: remixSteps,
186
+ connectStepTitles: [StepTitle.CONNECT_SNIPPET_REMIX, StepTitle.VITE_PLUGIN],
187
+ carriesOwnUnverifiedNote: false,
188
+ },
189
+ [Framework.ANGULAR]: {
190
+ // The sensor alone. There is no build plugin for the Angular CLI to load, and the React kit is
191
+ // what used to be installed here, with `react` beside it.
192
+ packages: () => [RETICLE_BROWSER_SDK],
193
+ steps: angularSteps,
194
+ connectStepTitles: [
195
+ StepTitle.CONNECT_SNIPPET_ANGULAR,
196
+ StepTitle.ANGULAR_TOKEN_PROXY,
197
+ StepTitle.ANGULAR_SERVE_CONFIG,
198
+ ],
199
+ carriesOwnUnverifiedNote: true,
200
+ },
111
201
  [Framework.TANSTACK_START]: {
112
202
  // Start IS a Vite app, so the plugin still stamps `data-reticle-source` — only the connect
113
203
  // injection is inapplicable, and that is the plan's business (`inject: false`).
@@ -116,7 +206,11 @@ export const FRAMEWORK_ADAPTERS = {
116
206
  ...tanstackStartSteps(input),
117
207
  ...viteSteps(input, VITE_PLUGIN_DETAIL.TANSTACK_START, false),
118
208
  ],
119
- connectStepTitles: [StepTitle.CONNECT_SNIPPET_TANSTACK_START, StepTitle.VITE_PLUGIN],
209
+ connectStepTitles: [
210
+ StepTitle.CONNECT_SNIPPET_TANSTACK_START,
211
+ StepTitle.TANSTACK_START_CONNECT_COMPONENT,
212
+ StepTitle.VITE_PLUGIN,
213
+ ],
120
214
  // The Start recipe carries its own UNVERIFIED line; a second generic notice would argue with it.
121
215
  carriesOwnUnverifiedNote: true,
122
216
  },
@@ -11,7 +11,7 @@
11
11
  import { CONTAINERISED_TITLE, containerisedDevServerNote } from '../diagnose/containerised-dev-server.js';
12
12
  import { mcpWindowsNote } from '../register/mcp.js';
13
13
  import { unverifiedUiLibraryNote, WEBGL_CANVAS_LIMIT_NOTE } from '../patch/snippets.js';
14
- import { FRAMEWORK_ADAPTERS } from './framework-adapter.js';
14
+ import { FRAMEWORK_ADAPTERS, installGated } from './framework-adapter.js';
15
15
  import { NodePlatform } from '../detect/platform.js';
16
16
  import { UiLibrary } from '../detect/detect.js';
17
17
  // `Step` and `PlanInput` are TYPES, so importing them back from `plan.ts` is erased at build
@@ -64,12 +64,13 @@ export function uiLibraryStep(input) {
64
64
  return [];
65
65
  if (lib === UiLibrary.UNKNOWN)
66
66
  return [];
67
+ const gated = installGated(input.detection.framework, lib);
67
68
  return [
68
69
  {
69
- title: `${lib} is UNVERIFIED`,
70
+ title: gated ? `${lib} setup verified, drive unverified` : `${lib} is UNVERIFIED`,
70
71
  target: 'package.json',
71
72
  status: StepStatus.NOTICE,
72
- detail: unverifiedUiLibraryNote(lib),
73
+ detail: unverifiedUiLibraryNote(lib, gated),
73
74
  },
74
75
  ];
75
76
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Angular CLI: the dev-only connect in the browser entry, and the dev-server route that hands it
3
+ * the pairing token. See `patch/angular.ts` for why the token takes that route and no other.
4
+ */
5
+ import { type PlanInput, type Step } from './plan-types.js';
6
+ export declare function angularSteps(input: PlanInput): Step[];
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Angular CLI: the dev-only connect in the browser entry, and the dev-server route that hands it
3
+ * the pairing token. See `patch/angular.ts` for why the token takes that route and no other.
4
+ */
5
+ import { ANGULAR_DEFAULT_ENTRY, ANGULAR_PROXY_PATH, ANGULAR_TOKEN_PATH, angularProxyFile, patchAngularJson, patchAngularMain, } from '../patch/angular.js';
6
+ import { PatchKind } from '../patch/patch-kind.js';
7
+ import { ANGULAR_WORKSPACE_FILE } from '../detect/detect.js';
8
+ import { StepTitle } from './connect-steps.js';
9
+ import { patchStep } from './plan-framework.js';
10
+ import { alreadyOrMovedPort, retargetPort } from './port-steps.js';
11
+ import { StepStatus } from './plan-types.js';
12
+ const ANGULAR_SETUP_GATED_NOTE = 'The install gate scaffolds an Angular app (Angular 21, no SSR) on every release and requires a ' +
13
+ 'session, and this wiring was proven by hand on Angular 22 with and without SSR; nothing yet ' +
14
+ 'drives an Angular app to a verdict on each change. DOM, network, console and state ' +
15
+ 'tools work here; component identity does not (that is the React adapter). If no session ' +
16
+ 'appears, please open an issue.';
17
+ const ENTRY_DETAIL = 'connect from the browser entry, dev-only (`ngDevMode`), through a dynamic import the ' +
18
+ 'production build drops';
19
+ const PROXY_DETAIL = 'the `ng serve`-only module that hands the page this machine’s pairing token, read from ' +
20
+ '~/.reticle at request time — never written into the project';
21
+ const SERVE_DETAIL = `point the serve target's proxyConfig at ${ANGULAR_PROXY_PATH} (\`ng build\` never reads it)`;
22
+ /** The full recipe, for when the entry is not a file `init` can see. */
23
+ function entryManual(input, entryPath) {
24
+ const recipe = patchAngularMain('', input.options.port, input.options.projectId);
25
+ const code = recipe.kind === PatchKind.APPLY ? recipe.code : '';
26
+ return `Add this to your browser entry (${entryPath}):\n\n${code}`;
27
+ }
28
+ function proxyStep(input) {
29
+ const existing = input.angularProxySource ?? null;
30
+ if (null !== existing && existing.includes(ANGULAR_TOKEN_PATH)) {
31
+ return {
32
+ title: StepTitle.ANGULAR_TOKEN_PROXY,
33
+ target: ANGULAR_PROXY_PATH,
34
+ status: StepStatus.ALREADY,
35
+ detail: 'already there',
36
+ };
37
+ }
38
+ return {
39
+ title: StepTitle.ANGULAR_TOKEN_PROXY,
40
+ target: ANGULAR_PROXY_PATH,
41
+ status: StepStatus.APPLY,
42
+ detail: PROXY_DETAIL,
43
+ write: { path: ANGULAR_PROXY_PATH, content: angularProxyFile(), expect: [ANGULAR_TOKEN_PATH] },
44
+ };
45
+ }
46
+ function serveStep(input) {
47
+ const workspace = input.angularWorkspace ?? null;
48
+ const manual = `In ${ANGULAR_WORKSPACE_FILE}, set the serve target's "options.proxyConfig" to "${ANGULAR_PROXY_PATH}".`;
49
+ if (null === workspace) {
50
+ return {
51
+ title: StepTitle.ANGULAR_SERVE_CONFIG,
52
+ target: ANGULAR_WORKSPACE_FILE,
53
+ status: StepStatus.MANUAL,
54
+ detail: manual,
55
+ };
56
+ }
57
+ return patchStep(StepTitle.ANGULAR_SERVE_CONFIG, workspace.path, patchAngularJson(workspace.source), SERVE_DETAIL, manual);
58
+ }
59
+ function entryStep(input) {
60
+ const entry = input.angularEntry ?? null;
61
+ if (null === entry) {
62
+ return {
63
+ title: StepTitle.CONNECT_SNIPPET_ANGULAR,
64
+ target: ANGULAR_DEFAULT_ENTRY,
65
+ status: StepStatus.MANUAL,
66
+ detail: entryManual(input, ANGULAR_DEFAULT_ENTRY),
67
+ };
68
+ }
69
+ // "already wired" read the entry without reading the port baked into its connect, so a re-run
70
+ // with a new `--port` left the page dialling the old daemon.
71
+ const patch = patchAngularMain(entry.source, input.options.port, input.options.projectId);
72
+ // An entry the previous version wired is rewritten in place (its guard), so a re-run that ALSO
73
+ // moves the port has to move it in the rewritten code — the ALREADY path below never sees it.
74
+ const moved = PatchKind.APPLY === patch.kind ? retargetPort(patch.code, input.options.port) : null;
75
+ return alreadyOrMovedPort(patchStep(StepTitle.CONNECT_SNIPPET_ANGULAR, entry.path, null === moved ? patch : { kind: PatchKind.APPLY, code: moved.code }, ENTRY_DETAIL, entryManual(input, entry.path)), entry.source, input.options.port);
76
+ }
77
+ export function angularSteps(input) {
78
+ return [
79
+ {
80
+ title: StepTitle.ANGULAR_SETUP_GATED,
81
+ target: ANGULAR_WORKSPACE_FILE,
82
+ status: StepStatus.NOTICE,
83
+ detail: ANGULAR_SETUP_GATED_NOTE,
84
+ },
85
+ entryStep(input),
86
+ proxyStep(input),
87
+ serveStep(input),
88
+ ];
89
+ }
@@ -1,8 +1,10 @@
1
1
  /**
2
- * The plan's electron-vite half: the renderer plugin, capabilities, and the two Electron steps.
2
+ * The plan's Electron half: the renderer plugin, capabilities, and the two Electron steps — for
3
+ * electron-vite and for Electron Forge's Vite template, which differ only in where the renderer's
4
+ * Vite config lives.
3
5
  *
4
- * Its own module because this is the only framework whose wiring reaches outside the renderer, into
5
- * a preload and a main process the web frameworks have no equivalent of.
6
+ * Its own module because these are the only frameworks whose wiring reaches outside the renderer,
7
+ * into a preload and a main process the web frameworks have no equivalent of.
6
8
  */
7
9
  import { type PlanInput, type Step } from './plan-types.js';
8
10
  /**
@@ -13,3 +15,4 @@ import { type PlanInput, type Step } from './plan-types.js';
13
15
  * manual recipe the way Astro's config+layout pair does.
14
16
  */
15
17
  export declare function electronViteSteps(input: PlanInput): Step[];
18
+ export declare function electronForgeSteps(input: PlanInput): Step[];