@reticlehq/init 3.3.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 (81) hide show
  1. package/dist/detect/capabilities.d.ts +4 -1
  2. package/dist/detect/capabilities.js +45 -9
  3. package/dist/detect/detect.d.ts +45 -2
  4. package/dist/detect/detect.js +76 -5
  5. package/dist/detect/dev-script.d.ts +26 -2
  6. package/dist/detect/dev-script.js +80 -7
  7. package/dist/detect/non-js-project.d.ts +1 -1
  8. package/dist/detect/non-js-project.js +6 -1
  9. package/dist/detect/workspace-apps.d.ts +21 -0
  10. package/dist/detect/workspace-apps.js +111 -12
  11. package/dist/detect/workspace-redirect.js +28 -2
  12. package/dist/diagnose/containerised-dev-server.d.ts +12 -4
  13. package/dist/diagnose/containerised-dev-server.js +36 -4
  14. package/dist/diagnose/csp-check.d.ts +12 -6
  15. package/dist/diagnose/csp-check.js +61 -13
  16. package/dist/diagnose/csp-doctor.d.ts +13 -1
  17. package/dist/diagnose/csp-doctor.js +2 -2
  18. package/dist/diagnose/csp-step.d.ts +27 -0
  19. package/dist/diagnose/csp-step.js +86 -0
  20. package/dist/diagnose/desktop-doctor.js +8 -2
  21. package/dist/diagnose/install-hint.d.ts +9 -1
  22. package/dist/diagnose/install-hint.js +17 -7
  23. package/dist/diagnose/install-retries.d.ts +16 -1
  24. package/dist/diagnose/install-retries.js +50 -3
  25. package/dist/diagnose/terminal-wrap.d.ts +18 -0
  26. package/dist/diagnose/terminal-wrap.js +63 -0
  27. package/dist/index.d.ts +6 -2
  28. package/dist/index.js +6 -2
  29. package/dist/no-package-json.d.ts +10 -0
  30. package/dist/no-package-json.js +121 -0
  31. package/dist/node-io.d.ts +9 -5
  32. package/dist/node-io.js +25 -7
  33. package/dist/patch/angular.d.ts +68 -0
  34. package/dist/patch/angular.js +216 -0
  35. package/dist/patch/astro-patch.d.ts +1 -1
  36. package/dist/patch/astro-patch.js +2 -2
  37. package/dist/patch/cra.d.ts +0 -2
  38. package/dist/patch/cra.js +0 -2
  39. package/dist/patch/electron-patch.d.ts +10 -1
  40. package/dist/patch/electron-patch.js +79 -27
  41. package/dist/patch/electron-vite-patch.js +1 -1
  42. package/dist/patch/nuxt-patch.d.ts +1 -1
  43. package/dist/patch/nuxt-patch.js +2 -2
  44. package/dist/patch/nuxt-snippets.js +6 -6
  45. package/dist/patch/remix.d.ts +24 -0
  46. package/dist/patch/remix.js +59 -0
  47. package/dist/patch/snippets.d.ts +57 -9
  48. package/dist/patch/snippets.js +119 -28
  49. package/dist/patch/static-page.d.ts +39 -0
  50. package/dist/patch/static-page.js +102 -0
  51. package/dist/patch/tanstack-start.d.ts +36 -8
  52. package/dist/patch/tanstack-start.js +118 -29
  53. package/dist/patch/vite-config.d.ts +5 -0
  54. package/dist/patch/vite-config.js +8 -0
  55. package/dist/patch/vite-owning-config.d.ts +34 -0
  56. package/dist/patch/vite-owning-config.js +63 -5
  57. package/dist/plan/connect-steps.d.ts +7 -1
  58. package/dist/plan/connect-steps.js +18 -1
  59. package/dist/plan/framework-adapter.d.ts +3 -1
  60. package/dist/plan/framework-adapter.js +99 -6
  61. package/dist/plan/notices.js +4 -3
  62. package/dist/plan/plan-angular.d.ts +6 -0
  63. package/dist/plan/plan-angular.js +89 -0
  64. package/dist/plan/plan-electron-vite.d.ts +6 -3
  65. package/dist/plan/plan-electron-vite.js +39 -8
  66. package/dist/plan/plan-framework.d.ts +8 -46
  67. package/dist/plan/plan-framework.js +81 -312
  68. package/dist/plan/plan-types.d.ts +29 -0
  69. package/dist/plan/plan-vite.d.ts +67 -0
  70. package/dist/plan/plan-vite.js +256 -0
  71. package/dist/plan/plan.js +38 -34
  72. package/dist/plan/port-steps.d.ts +45 -0
  73. package/dist/plan/port-steps.js +119 -0
  74. package/dist/plan/preflight.d.ts +17 -3
  75. package/dist/plan/preflight.js +53 -23
  76. package/dist/register/mcp.d.ts +18 -5
  77. package/dist/register/mcp.js +48 -8
  78. package/dist/run-types.d.ts +8 -0
  79. package/dist/run.js +111 -78
  80. package/package.json +3 -4
  81. package/NOTICE +0 -5
@@ -1,52 +1,141 @@
1
1
  /**
2
- * The TanStack Start connect recipe.
2
+ * The TanStack Start connect: a client-only component, rendered in the document the root route SSRs.
3
3
  *
4
- * Its own module because Start is the one framework whose document module `init` deliberately
5
- * refuses to write, so the recipe and the reasoning behind that refusal are a unit.
4
+ * Its own module because Start is the one framework whose document module is ALSO the server
5
+ * render, so what may be written into it — and what must stay out — is a unit.
6
6
  */
7
+ import { posix } from 'node:path';
7
8
  import { UiLibrary } from '../detect/detect.js';
8
- import { connectArg, sdkImport, UNVERIFIED_TANSTACK_START_NOTE } from './snippets.js';
9
+ import { connectArg, devModuleSpecifier, sdkImport, UNVERIFIED_TANSTACK_START_NOTE, } from './snippets.js';
10
+ import { PatchKind } from './patch-kind.js';
11
+ import { VITE_ENV_DEV_DECLARATION } from './vite-env-types.js';
9
12
  /** TanStack Start's document module — the file that SSRs `<html>`. */
10
13
  export const TANSTACK_START_ROOT_PATH = 'src/routes/__root.tsx';
14
+ /** The component `init` writes, and renders in the root document. */
15
+ const CONNECT_COMPONENT = 'ReticleConnect';
16
+ const CONNECT_FILE_BASENAME = 'reticle-connect';
17
+ /** `<Scripts />` — Start's own client-script outlet, which every root document has to render. */
18
+ const SCRIPTS_OUTLET = /^([ \t]*)<Scripts\s*\/>/m;
11
19
  /**
12
- * The TanStack Start recipe, printed rather than written.
20
+ * Where the connect component goes: beside the `routes/` directory, never inside it — a file under
21
+ * `routes/` is a route to Start's file-based router.
22
+ */
23
+ export function tanstackStartConnectPath(rootPath = TANSTACK_START_ROOT_PATH) {
24
+ return posix.join(posix.dirname(posix.dirname(rootPath)), `${CONNECT_FILE_BASENAME}.tsx`);
25
+ }
26
+ /**
27
+ * The connect component.
28
+ *
29
+ * A dynamic SDK import inside `useEffect`, because a static import on anything the root document
30
+ * imports is evaluated during the server render and 500s the page. Effects never run on the server.
31
+ * The pairing token is the one the Vite plugin inlines as `__RETICLE_TOKEN__`, declared here so the
32
+ * file passes the scaffold's strict `tsc` — the recipe this replaced used the name undeclared and
33
+ * failed with `TS2304: Cannot find name '__RETICLE_TOKEN__'`.
13
34
  *
14
- * `__root.tsx` is the document. A static import of the SDK on that module SSRs and 500s, so `init`
15
- * does not write one. The connect has to be a client-only dynamic import inside `useEffect`, with
16
- * the pairing token the plugin inlines as `__RETICLE_TOKEN__`. Same judgement React Router already
17
- * makes about `app/entry.client.tsx`: a half-written document is worse than a documented manual step.
35
+ * It also loads the app's `src/reticle-dev.ts` after connecting. Start is written `inject: false`,
36
+ * and the plugin's injected connect is the only other thing that imports that file, so without this
37
+ * line every store and capability registered there silently never ran.
18
38
  */
19
- export function tanstackStartManual(port, projectId, rootPath = TANSTACK_START_ROOT_PATH) {
39
+ export function tanstackStartConnectFile(port, projectId, rootPath = TANSTACK_START_ROOT_PATH) {
20
40
  const sdk = sdkImport(UiLibrary.REACT);
21
41
  const base = connectArg(port, projectId);
22
42
  const fields = '' === base ? '' : `${base.slice(1, -1).trim()}, `;
23
- const installLine = sdk.usesInstall
24
- ? ' install();'
25
- : ' // No React adapter here: the sensor has no install() to call.';
26
43
  const imports = sdk.usesInstall ? 'reticle, install' : 'reticle';
44
+ const installLine = sdk.usesInstall
45
+ ? ' install();'
46
+ : ' // No React adapter here: the sensor has no install() to call.';
47
+ const devModule = devModuleSpecifier(tanstackStartConnectPath(rootPath));
48
+ return `import { useEffect } from 'react';
49
+
50
+ /**
51
+ * Dev-only: connect Reticle from the client. Start SSRs its own <html> and never sends Vite's
52
+ * index.html, so the plugin's injection cannot do it. Rendered once in the root document.
53
+ */
54
+ export function ${CONNECT_COMPONENT}() {
55
+ useEffect(() => {
56
+ if (!import.meta.env.DEV) return;
57
+ // Inlined by @reticlehq/vite-plugin. The bridge refuses a connect without it, even on localhost.
58
+ const token = typeof __RETICLE_TOKEN__ !== 'undefined' ? __RETICLE_TOKEN__ : '';
59
+ const root = typeof __RETICLE_ROOT__ !== 'undefined' ? __RETICLE_ROOT__ : '';
60
+ void import('${sdk.specifier}').then(({ ${imports} }) => {
61
+ ${installLine}
62
+ reticle.connect({
63
+ ${fields}...(token.length > 0 ? { token } : {}),
64
+ ...(root.length > 0 ? { root } : {}),
65
+ });
66
+ // Your stores and capabilities live here; nothing else loads this file under Start.
67
+ void import('${devModule}');
68
+ });
69
+ }, []);
70
+ return null;
71
+ }
72
+
73
+ declare const __RETICLE_TOKEN__: string | undefined;
74
+ declare const __RETICLE_ROOT__: string | undefined;
75
+
76
+ ${VITE_ENV_DEV_DECLARATION}`;
77
+ }
78
+ /** The import the root document gets, relative to where it lives. */
79
+ function connectImport(rootPath) {
80
+ const target = tanstackStartConnectPath(rootPath).replace(/\.tsx$/, '');
81
+ const rel = posix.relative(posix.dirname(rootPath), target);
82
+ return `import { ${CONNECT_COMPONENT} } from '${rel.startsWith('.') ? rel : `./${rel}`}';`;
83
+ }
84
+ /**
85
+ * Render the component in the root document, just before `<Scripts />`.
86
+ *
87
+ * `<Scripts />` is the anchor because every Start root has to render it — it is how the client
88
+ * bundle reaches the page — whatever the component around it is called (`RootDocument` as the
89
+ * route's `shellComponent` in the current template; `RootComponent` in older ones). A root without
90
+ * one is a shape we do not recognise, and a half-edited document is worse than a documented step.
91
+ */
92
+ export function patchTanstackStartRoot(source, rootPath) {
93
+ if (source.includes(CONNECT_COMPONENT))
94
+ return { kind: PatchKind.ALREADY };
95
+ if (!rootPath.endsWith('.tsx')) {
96
+ return { kind: PatchKind.MANUAL, reason: `${rootPath} is not TypeScript` };
97
+ }
98
+ const outlet = SCRIPTS_OUTLET.exec(source);
99
+ if (null === outlet) {
100
+ return { kind: PatchKind.MANUAL, reason: `no <Scripts /> found in ${rootPath}` };
101
+ }
102
+ const indent = outlet[1] ?? '';
103
+ const rendered = source.replace(SCRIPTS_OUTLET, `${indent}<${CONNECT_COMPONENT} />\n$&`);
104
+ const lines = rendered.split('\n');
105
+ let lastImport = -1;
106
+ for (const [index, line] of lines.entries()) {
107
+ if (/^import\s/.test(line))
108
+ lastImport = index;
109
+ }
110
+ lines.splice(lastImport + 1, 0, connectImport(rootPath));
111
+ return { kind: PatchKind.APPLY, code: lines.join('\n') };
112
+ }
113
+ /**
114
+ * The recipe, for a root `patchTanstackStartRoot` would not edit.
115
+ *
116
+ * The same two edits it makes, stated as edits: create the component, render it before
117
+ * `<Scripts />`. It used to say "add this useEffect to the App component", naming a component the
118
+ * current template does not have, and its snippet used `__RETICLE_TOKEN__` undeclared.
119
+ */
120
+ export function tanstackStartManual(port, projectId, rootPath = TANSTACK_START_ROOT_PATH) {
121
+ const component = tanstackStartConnectFile(port, projectId, rootPath)
122
+ .split('\n')
123
+ .map((line) => (0 === line.length ? line : ` ${line}`))
124
+ .join('\n');
27
125
  return `TanStack Start SSRs <html> from ${rootPath} (HeadContent / Scripts) and never sends Vite's
28
126
  index.html, so the plugin's connect injection never fires. Keep the plugin anyway, with
29
127
  reticle({ inject: false }): only the injection half is inapplicable, and the stamping half is
30
128
  what puts data-reticle-source on the JSX. Dropping the plugin because one of its two jobs did
31
129
  not apply is how Remix lost file:line.
32
130
 
33
- Connect from a CLIENT-ONLY effect in ${rootPath}. A static SDK import on that module SSRs and
34
- 500s. Do not guard on window.location.hostname === 'localhost' — import.meta.env.DEV is the
35
- correct guard, and it does not care what host you develop on.
36
-
37
- Add this useEffect to the App component (import useEffect from react if it is not already there):
131
+ 1. Create ${tanstackStartConnectPath(rootPath)}. It connects from a CLIENT-ONLY effect: a static SDK
132
+ import on anything the root document imports SSRs and 500s.
133
+ Do not guard on window.location.hostname === 'localhost' — import.meta.env.DEV is the correct
134
+ guard, and it does not care what host you develop on.
38
135
 
39
- useEffect(() => {
40
- if (import.meta.env.DEV) {
41
- const token = typeof __RETICLE_TOKEN__ !== 'undefined' ? __RETICLE_TOKEN__ : '';
42
- void import('${sdk.specifier}').then(({ ${imports} }) => {
43
- ${installLine}
44
- reticle.connect({
45
- ${fields}...(token.length > 0 ? { token } : {}),
46
- });
47
- });
48
- }
49
- }, []);
136
+ ${component}
137
+ 2. In ${rootPath}, add \`${connectImport(rootPath)}\` and render <${CONNECT_COMPONENT} /> in the
138
+ component that renders <html> (the route's shellComponent), just before <Scripts />.
50
139
 
51
140
  The plugin inlines __RETICLE_TOKEN__ when Vite resolves its config. Start the daemon BEFORE the
52
141
  dev server so that file exists; a server that started first froze an empty token and the page
@@ -14,5 +14,10 @@ export declare const VitePatchKind: {
14
14
  };
15
15
  export type VitePatchKind = PatchKind;
16
16
  type VitePatch = SourcePatch;
17
+ /**
18
+ * A whole config for a plain Vite app that has none: Vite's defaults plus the plugin. Written out
19
+ * rather than patched from an empty one, because it is a file the person will open and read.
20
+ */
21
+ export declare function newViteConfig(port?: number, captureBodies?: boolean, inject?: boolean, sourceMapping?: boolean): string;
17
22
  export declare function patchViteConfig(source: string, port?: number, captureBodies?: boolean, inject?: boolean, sourceMapping?: boolean): VitePatch;
18
23
  export {};
@@ -98,6 +98,14 @@ function insertPluginsKey(source, call) {
98
98
  return multiline ? `${prefix}{\n${indent}${key}` : `${prefix}{ ${key}`;
99
99
  });
100
100
  }
101
+ /**
102
+ * A whole config for a plain Vite app that has none: Vite's defaults plus the plugin. Written out
103
+ * rather than patched from an empty one, because it is a file the person will open and read.
104
+ */
105
+ export function newViteConfig(port, captureBodies = false, inject = true, sourceMapping = true) {
106
+ const call = reticlePluginCall({ port, captureBodies, inject, sourceMapping });
107
+ return `import { defineConfig } from 'vite';\n${VITE_IMPORT}\n\nexport default defineConfig({\n plugins: [${call}],\n});\n`;
108
+ }
101
109
  export function patchViteConfig(source, port, captureBodies = false, inject = true, sourceMapping = true) {
102
110
  if (source.includes(RETICLE_MARKER)) {
103
111
  return { kind: VitePatchKind.ALREADY };
@@ -34,5 +34,39 @@ export interface ViteOwningConfig {
34
34
  * path writes. Astro's path never writes that name into the config, so it supplies its own.
35
35
  */
36
36
  readonly alreadyMarker?: string;
37
+ /**
38
+ * The installed Vite's major (see `installedViteMajor`), or null when nothing on disk said.
39
+ *
40
+ * Decides whether the dep-optimizer target is written at all. Vite 7 moved the optimizer to
41
+ * rolldown and deprecated `optimizeDeps.esbuildOptions`: on Nuxt 4.5 / Vite 8 the key printed a
42
+ * deprecation warning on every dev start and build. The target exists for esbuild, which cannot
43
+ * lower some of the SDK's syntax to Vite's older default; rolldown has no such gap, so on 7+ there
44
+ * is nothing to write under either key.
45
+ */
46
+ readonly viteMajor?: number | null | undefined;
37
47
  }
38
48
  export declare function patchViteOwningConfig(source: string, config: ViteOwningConfig): SourcePatch;
49
+ /**
50
+ * The major version of the Vite this app will actually run, read from disk.
51
+ *
52
+ * Needed by the configs `init` patches for frameworks that own their Vite instance (Nuxt, Astro):
53
+ * Vite 7 moved the dep optimizer to rolldown and deprecated `optimizeDeps.esbuildOptions`, so the
54
+ * key `init` wrote printed a deprecation warning on every dev start and build there. The Vite
55
+ * plugin answers the same question for itself (`optimizerOptionsKey` in @reticlehq/vite-plugin),
56
+ * but these frameworks never load the plugin.
57
+ *
58
+ * Nuxt and Astro do not list `vite` in the app's package.json — it comes with the framework — so the
59
+ * declared range is the last resort, after the hoisted package and pnpm's store. Null when none
60
+ * says: the caller keeps the older key, as the plugin does, because a deprecation notice is a much
61
+ * smaller failure than an option the installed Vite has never heard of.
62
+ */
63
+ interface ViteReader {
64
+ readFile(relPath: string): string | null;
65
+ listDirs(relPath: string): readonly string[];
66
+ }
67
+ interface DeclaredDeps {
68
+ dependencies?: Record<string, string>;
69
+ devDependencies?: Record<string, string>;
70
+ }
71
+ export declare function installedViteMajor(io: ViteReader, pkg: DeclaredDeps): number | null;
72
+ export {};
@@ -15,8 +15,31 @@
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
44
  const DEFINE_TOKEN_MARKER = '__RETICLE_TOKEN__';
22
45
  function usesDefineToken(config) {
@@ -92,7 +115,7 @@ function viteKeys(config) {
92
115
  { key: 'build', inner: `\n target: 'es2022',` },
93
116
  {
94
117
  key: 'optimizeDeps',
95
- inner: `\n include: [${sdkInclude(config)}],\n esbuildOptions: { target: 'es2022' },`,
118
+ inner: `\n include: [${sdkInclude(config)}],${writesEsbuildTarget(config) ? ESBUILD_TARGET_INNER : ''}`,
96
119
  },
97
120
  // Merged in first, so a `server: { port }` the app already set keeps its port. An app that
98
121
  // already sets `server.watch` itself is the one shape this loses to — the inner `watch` key would
@@ -111,7 +134,7 @@ function viteKeys(config) {
111
134
  function wholeKeys(config) {
112
135
  const keys = {
113
136
  build: `\n build: { target: 'es2022' },`,
114
- optimizeDeps: `\n optimizeDeps: { include: [${sdkInclude(config)}], esbuildOptions: { target: 'es2022' } },`,
137
+ optimizeDeps: `\n optimizeDeps: { include: [${sdkInclude(config)}]${writesEsbuildTarget(config) ? ESBUILD_TARGET_WHOLE : ''} },`,
115
138
  server: `\n server: { watch: { ignored: [${WATCH_IGNORE_LITERAL}] } },`,
116
139
  };
117
140
  if (usesDefineToken(config)) {
@@ -182,7 +205,7 @@ function viteBlock(config) {
182
205
  : '';
183
206
  return ` vite: {
184
207
  ${marker} build: { target: 'es2022' },
185
- optimizeDeps: { include: [${sdkInclude(config)}], esbuildOptions: { target: 'es2022' } },
208
+ optimizeDeps: { include: [${sdkInclude(config)}]${writesEsbuildTarget(config) ? ESBUILD_TARGET_WHOLE : ''} },
186
209
  ${define} server: { watch: { ignored: [${WATCH_IGNORE_LITERAL}] } },
187
210
  },
188
211
  `;
@@ -199,8 +222,12 @@ function withOwningMarker(source, config, braceAt) {
199
222
  return insertAt(source, braceAt, `\n /* ${alreadyMarker(config)} */`);
200
223
  }
201
224
  export function patchViteOwningConfig(source, config) {
202
- if (source.includes(alreadyMarker(config)))
203
- 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
+ }
204
231
  // A `vite: { ... }` block is an object literal, so our keys can go straight after the brace and
205
232
  // whatever is already in there is untouched. Refusing outright was the only genuine install defect
206
233
  // left in the gate: the config bailed while the LAYOUT patch applied anyway, leaving an app with a
@@ -244,3 +271,34 @@ export function patchViteOwningConfig(source, config) {
244
271
  code: withHelper(withBlock, config),
245
272
  };
246
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,11 +27,17 @@ 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
43
  readonly ASTRO_RETICLE_DEV: "Astro ReticleDev module";
@@ -28,11 +28,17 @@ 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
44
  ASTRO_RETICLE_DEV: 'Astro ReticleDev module',
@@ -63,6 +69,14 @@ export const CONNECT_STEP_TITLES = new Set([
63
69
  StepTitle.NUXT_CONFIG,
64
70
  StepTitle.CONNECT_SNIPPET_REACT_ROUTER,
65
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,
66
80
  StepTitle.CLIENT_HOOK,
67
81
  StepTitle.CONNECT_MODULE,
68
82
  StepTitle.RETICLE_DEV_COMPONENT,
@@ -87,6 +101,9 @@ export const CONNECT_STEP_TITLES = new Set([
87
101
  // electron-vite's connect IS the renderer plugin. Without it the SDK never injects, and the
88
102
  // preload/capture steps are not enough to produce a session.
89
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,
90
107
  ]);
91
108
  /** True when this step is what makes the app dial the daemon. */
92
109
  export function isConnectStep(title) {
@@ -18,7 +18,7 @@
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";
@@ -53,6 +53,8 @@ interface FrameworkAdapter {
53
53
  */
54
54
  readonly carriesOwnUnverifiedNote: boolean;
55
55
  }
56
+ /** Whether the install gate scaffolds this stack — see INSTALL_GATE_STACKS. */
57
+ export declare function installGated(framework: Framework, uiLibrary: UiLibrary): boolean;
56
58
  export declare const FRAMEWORK_ADAPTERS: Record<Framework, FrameworkAdapter>;
57
59
  /**
58
60
  * The per-framework half of the plan, looked up rather than switched.
@@ -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
@@ -33,6 +63,29 @@ const RETICLE_VITE_PLUGIN = '@reticlehq/vite-plugin';
33
63
  const RETICLE_NEXT_PLUGIN = '@reticlehq/next';
34
64
  /** The Electron main/preload helper — what makes IPC and screenshots exist at all. */
35
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,7 +141,12 @@ 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
  },
@@ -109,6 +169,35 @@ export const FRAMEWORK_ADAPTERS = {
109
169
  connectStepTitles: [StepTitle.ELECTRON_VITE_PLUGIN],
110
170
  carriesOwnUnverifiedNote: false,
111
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
+ },
112
201
  [Framework.TANSTACK_START]: {
113
202
  // Start IS a Vite app, so the plugin still stamps `data-reticle-source` — only the connect
114
203
  // injection is inapplicable, and that is the plan's business (`inject: false`).
@@ -117,7 +206,11 @@ export const FRAMEWORK_ADAPTERS = {
117
206
  ...tanstackStartSteps(input),
118
207
  ...viteSteps(input, VITE_PLUGIN_DETAIL.TANSTACK_START, false),
119
208
  ],
120
- 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
+ ],
121
214
  // The Start recipe carries its own UNVERIFIED line; a second generic notice would argue with it.
122
215
  carriesOwnUnverifiedNote: true,
123
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[];