@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
@@ -1,66 +1,170 @@
1
1
  /**
2
- * Pure, conservative patchers for an Astro app: the config gets the token/root `define` and the
3
- * raised build target, one layout gets the dev-only connect `<script>`.
2
+ * Pure, conservative patchers for an Astro app: the config gets the raised build target and the
3
+ * watcher ignore, one layout gets the pairing token via frontmatter `<meta>` tags and a processed
4
+ * `<script>` that statically imports a local connect module.
4
5
  *
5
6
  * Astro was the last gated framework where `init` printed a correct recipe and applied none of it —
6
7
  * the only ⚠ left on a supported stack, and the user's first session was two hand-copied snippets
7
8
  * away. Astro is Vite-based but renders its own HTML, so the Vite plugin's injection never fires and
8
- * there is no entry module to wire: the connect has to live in a page or layout `<script>`, and the
9
- * token has to be inlined by the config because nothing else is in the page's path to inject it.
9
+ * there is no entry module to wire: the connect has to live in a page or layout.
10
+ *
11
+ * The token used to be inlined by `vite.define`. On Astro 7.2+ that substitution no longer reaches
12
+ * the client pipeline (#1008), so the identifier stays literal, `connect()` omits `token`, and the
13
+ * bridge refuses with "no pairing token on the page". The channel that does work is a `<meta>`: read
14
+ * the file in frontmatter and let a local module query `meta[name="reticle-pairing-token"]`. Reading
15
+ * per request also means a token written after the dev server started needs no restart. The SDK import
16
+ * lives in `src/components/ReticleDev.ts` so the page only ever statically imports a project module.
10
17
  *
11
18
  * Both patchers bail to `manual` (the printed recipe) on any shape they do not fully recognise.
12
19
  * Half-editing a build config is worse than a documented manual step.
13
20
  */
21
+ import { posix } from 'node:path';
14
22
  import { RETICLE_DEFAULT_PORT, bridgeWsUrl } from '@reticlehq/core';
15
23
  import { PatchKind } from './patch-kind.js';
16
24
  import { UiLibrary } from '../detect/detect.js';
17
25
  import { registerCapabilitiesCall, sdkImport } from './snippets.js';
18
26
  import { patchViteOwningConfig } from './vite-owning-config.js';
19
- /** Present in a patched layout and in the printed recipe's script. */
27
+ /**
28
+ * Local module `init` writes for Astro. A processed page `<script>` statically imports this file
29
+ * so Vite owns the SDK the same way it owns any other project module.
30
+ *
31
+ * `.ts`, not `.tsx`: the install-gate scaffold is `create-astro --template minimal`, which has no
32
+ * React integration. The example app uses the same static import in a processed page `<script>`.
33
+ */
34
+ export const ASTRO_RETICLE_DEV_PATH = 'src/components/ReticleDev.ts';
35
+ /**
36
+ * Dev-only Astro connect module. Static SDK import, token from frontmatter `<meta>` tags.
37
+ */
38
+ export function astroReticleDevFile(port, projectId, uiLibrary = UiLibrary.REACT, testids = []) {
39
+ const sdk = sdkImport(uiLibrary);
40
+ const extra = port !== undefined && port !== RETICLE_DEFAULT_PORT ? `\n url: '${bridgeWsUrl(port)}',` : '';
41
+ const id = projectId !== undefined && projectId.length > 0 ? `\n projectId: '${projectId}',` : '';
42
+ return `import { reticle${sdk.usesInstall ? ', install' : ''}, registerCapabilities } from '${sdk.specifier}';
43
+
44
+ /** Dev-only: connect Reticle. Imported from a processed page script so Vite owns the SDK graph. */
45
+ export default function connectReticle() {
46
+ const token =
47
+ document.querySelector('meta[name="reticle-pairing-token"]')?.getAttribute('content') ?? '';
48
+ const root =
49
+ document.querySelector('meta[name="reticle-pairing-root"]')?.getAttribute('content') ?? '';
50
+ const url =
51
+ document.querySelector('meta[name="reticle-pairing-url"]')?.getAttribute('content') ?? '';
52
+ if (0 === token.length) {
53
+ console.warn(${JSON.stringify(ASTRO_MISSING_TOKEN_WARNING)});
54
+ }
55
+ ${sdk.usesInstall ? 'install();' : '// The sensor has no install(); that is the React adapter.'}
56
+ reticle.connect({${id}${extra}
57
+ ...(0 < token.length ? { token } : {}),
58
+ ...(0 < root.length ? { root } : {}),
59
+ ...(0 < url.length ? { url } : {}),
60
+ });
61
+ ${registerCapabilitiesCall(testids, ' ')}
62
+ }
63
+ `;
64
+ }
65
+ /** Present in a patched layout (and in an already-wired one that still has the old script). */
20
66
  const LAYOUT_MARKER = 'reticle.connect';
67
+ const LAYOUT_MODULE_MARKER = 'connectReticle';
21
68
  const BODY_CLOSE = '</body>';
69
+ const FRONTMATTER_FENCE = '---';
70
+ /**
71
+ * Written into the Astro `vite:` block so a second init reports ALREADY. The Nuxt path uses
72
+ * `__RETICLE_TOKEN__` in `define` as its marker; Astro no longer writes that name into the config.
73
+ */
74
+ export const ASTRO_VITE_OWNING_MARKER = 'reticle-vite-owning';
75
+ /**
76
+ * The one runtime condition that guarantees a refused connect, said out loud on the page.
77
+ *
78
+ * Matching the Vite plugin's missing-token wording, because Astro never loads that plugin. The
79
+ * token is read at render time now, not at config load, so a reload is enough — the user does not
80
+ * have to restart the dev server.
81
+ */
82
+ export const ASTRO_MISSING_TOKEN_WARNING = '[reticle] no pairing token was available when this page rendered, so the app will connect ' +
83
+ 'and be refused — you will see NO SESSION even though the SDK loads and the socket opens. ' +
84
+ 'The token is written by the Reticle daemon: start it (`reticle serve`, or let your agent start ' +
85
+ 'it) and reload this page.';
22
86
  /**
23
87
  * Astro's half of the Vite-owning config patch: the call to extend, and the SDK to pre-bundle.
24
88
  *
25
89
  * `@reticlehq/react` and not the detected UI library: Astro hosts islands from any framework, but
26
90
  * `frameworkPackages` gives every Astro app the React kit (see FRAMEWORK_ADAPTERS), so that is the
27
91
  * package the connect script imports and the one Vite must pre-bundle.
92
+ *
93
+ * `defineToken: false` is the #1008 fix. `build.target`, `optimizeDeps.include` and the `.reticle/`
94
+ * watcher ignore stay — each has a measured reason and none of them is the defect.
28
95
  */
29
96
  const ASTRO_VITE_CONFIG = {
30
97
  defineCall: /defineConfig\s*\(\s*\{/,
31
98
  sdkSpecifier: sdkImport(UiLibrary.REACT).specifier,
99
+ defineToken: false,
100
+ alreadyMarker: ASTRO_VITE_OWNING_MARKER,
32
101
  };
33
- export function patchAstroConfig(source) {
34
- return patchViteOwningConfig(source, ASTRO_VITE_CONFIG);
102
+ export function patchAstroConfig(source, viteMajor = null) {
103
+ return patchViteOwningConfig(source, { ...ASTRO_VITE_CONFIG, viteMajor });
35
104
  }
36
- /** The dev-only connect that goes inside the layout's `<body>`. */
37
- function astroConnectScript(port, projectId, uiLibrary, testids) {
38
- // Must match what `frameworkPackages` installed. Astro hosts islands from any framework, so an
39
- // Astro app whose islands are Vue or Svelte gets the sensor, and a script importing the React
40
- // adapter would load a package that is not in node_modules. See sdkImport.
41
- const sdk = sdkImport(uiLibrary);
42
- const url = port !== undefined && port !== RETICLE_DEFAULT_PORT
43
- ? `\n url: '${bridgeWsUrl(port)}',`
44
- : '';
45
- const id = projectId !== undefined && projectId.length > 0 ? `\n projectId: '${projectId}',` : '';
46
- return ` <script>
105
+ /**
106
+ * Aliased imports so a layout that already pulled `readFileSync` from `node:fs` does not get a
107
+ * duplicate binding. The names are the already-marker for the frontmatter half.
108
+ */
109
+ const FRONTMATTER_ADDITIONS = `import { readFileSync as reticleReadFileSync } from 'node:fs';
110
+ import { homedir as reticleHomedir } from 'node:os';
111
+ import { join as reticleJoin } from 'node:path';
112
+ const pairingToken = (() => {
113
+ if (!import.meta.env.DEV) return '';
114
+ const dir = process.env['RETICLE_PAIRING_TOKEN_DIR'] || reticleJoin(reticleHomedir(), '.reticle');
115
+ try { return reticleReadFileSync(reticleJoin(dir, 'pairing-token'), 'utf8').trim(); } catch { return ''; }
116
+ })();
117
+ const pairingRoot = import.meta.env.DEV ? process.cwd() : '';
118
+ `;
119
+ /**
120
+ * Import path from a layout/page to the generated connect module, always relative and posix.
121
+ *
122
+ * `src/layouts/Layout.astro` and `src/pages/index.astro` both resolve to `../components/ReticleDev`.
123
+ */
124
+ export function astroReticleDevImport(layoutPath) {
125
+ const fromDir = posix.dirname(layoutPath.replaceAll('\\', '/'));
126
+ const to = ASTRO_RETICLE_DEV_PATH.replace(/\.ts$/, '');
127
+ let rel = posix.relative(fromDir, to);
128
+ if (!rel.startsWith('.'))
129
+ rel = `./${rel}`;
130
+ return rel;
131
+ }
132
+ /** Meta tags plus a processed script that statically imports the local connect module. */
133
+ function astroConnectMarkup(moduleSpecifier) {
134
+ return ` <meta name="reticle-pairing-token" content={pairingToken} />
135
+ <meta name="reticle-pairing-root" content={pairingRoot} />
136
+ <script>
137
+ import connectReticle from '${moduleSpecifier}';
47
138
  if (import.meta.env.DEV) {
48
- const token = typeof __RETICLE_TOKEN__ !== 'undefined' ? __RETICLE_TOKEN__ : '';
49
- const root = typeof __RETICLE_ROOT__ !== 'undefined' ? __RETICLE_ROOT__ : '';
50
- const { reticle${sdk.usesInstall ? ', install' : ''}, registerCapabilities } = await import('${sdk.specifier}');
51
- ${sdk.usesInstall ? 'install();' : '// The sensor has no install(); that is the React adapter.'}
52
- reticle.connect({${id}${url}
53
- ...(token.length > 0 ? { token } : {}),
54
- ...(root.length > 0 ? { root } : {}),
55
- });
56
- ${registerCapabilitiesCall(testids, ' ')}
139
+ connectReticle();
57
140
  }
58
141
  </script>
59
142
  `;
60
143
  }
61
- export function patchAstroLayout(source, port, projectId, uiLibrary = UiLibrary.REACT, testids = []) {
62
- if (source.includes(LAYOUT_MARKER))
144
+ /**
145
+ * Marker for the frontmatter half. It must be a name ONLY the frontmatter writes: this ran against
146
+ * `pairingToken`, which the meta markup inserted one step earlier already contains, so the guard
147
+ * always tripped and the token read was never added — leaving every scaffolded page throwing
148
+ * `ReferenceError: pairingToken is not defined`.
149
+ */
150
+ const FRONTMATTER_MARKER = 'reticleReadFileSync';
151
+ function withFrontmatter(source) {
152
+ if (source.includes(FRONTMATTER_MARKER))
153
+ return source;
154
+ if (source.startsWith(FRONTMATTER_FENCE)) {
155
+ const closeAt = source.indexOf(`\n${FRONTMATTER_FENCE}`, FRONTMATTER_FENCE.length);
156
+ if (closeAt < 0)
157
+ return null;
158
+ const inner = source.slice(FRONTMATTER_FENCE.length + 1, closeAt);
159
+ const padded = inner.endsWith('\n') ? inner : `${inner}\n`;
160
+ return `${FRONTMATTER_FENCE}\n${padded}${FRONTMATTER_ADDITIONS}${source.slice(closeAt + 1)}`;
161
+ }
162
+ return `${FRONTMATTER_FENCE}\n${FRONTMATTER_ADDITIONS}${FRONTMATTER_FENCE}\n${source}`;
163
+ }
164
+ export function patchAstroLayout(source, layoutPath = 'src/layouts/Layout.astro') {
165
+ if (source.includes(LAYOUT_MARKER) || source.includes(LAYOUT_MODULE_MARKER)) {
63
166
  return { kind: PatchKind.ALREADY };
167
+ }
64
168
  const at = source.lastIndexOf(BODY_CLOSE);
65
169
  if (at < 0) {
66
170
  return {
@@ -68,28 +172,32 @@ export function patchAstroLayout(source, port, projectId, uiLibrary = UiLibrary.
68
172
  reason: "couldn't find a `</body>` to place the connect script before",
69
173
  };
70
174
  }
71
- return {
72
- kind: PatchKind.APPLY,
73
- code: `${source.slice(0, at)}${astroConnectScript(port, projectId, uiLibrary, testids)}${source.slice(at)}`,
74
- };
175
+ const withScripts = `${source.slice(0, at)}${astroConnectMarkup(astroReticleDevImport(layoutPath))}${source.slice(at)}`;
176
+ const withFence = withFrontmatter(withScripts);
177
+ if (null === withFence) {
178
+ return {
179
+ kind: PatchKind.MANUAL,
180
+ reason: "couldn't find a closing frontmatter fence to place the pairing-token read in",
181
+ };
182
+ }
183
+ return { kind: PatchKind.APPLY, code: withFence };
75
184
  }
76
185
  /** Where Astro keeps ambient types — `create-astro` ships this file. */
77
186
  export const ASTRO_ENV_DTS_PATH = 'src/env.d.ts';
78
187
  /**
79
- * The declarations `astro check` needs for the Vite `define` names (#677).
80
- *
81
- * Matching SvelteKit's `hooks.client.ts` typing: `string | undefined`, because the define can be
82
- * absent when the pairing token file is missing.
188
+ * The declarations `astro check` needs for the window names the inline script writes (#677, #1008).
83
189
  */
84
- export const ASTRO_ENV_DTS_DECLARES = 'declare const __RETICLE_TOKEN__: string | undefined;\n' +
85
- 'declare const __RETICLE_ROOT__: string | undefined;\n';
86
- /** Present once either declare is in the file — both are written together. */
190
+ export const ASTRO_ENV_DTS_DECLARES = 'interface Window {\n' +
191
+ ' __RETICLE_TOKEN__?: string;\n' +
192
+ ' __RETICLE_ROOT__?: string;\n' +
193
+ '}\n';
194
+ /** Present once either name is in the file — both are written together. */
87
195
  const ENV_DTS_MARKER = '__RETICLE_TOKEN__';
88
196
  /**
89
- * Append the Vite-define ambient declarations to `src/env.d.ts`, or create the file.
197
+ * Append the window ambient declarations to `src/env.d.ts`, or create the file.
90
198
  *
91
- * Without this, `create-astro`'s default `"astro check && astro build"` fails with four
92
- * `ts(2304) Cannot find name '__RETICLE_TOKEN__'` errors after a clean init (#677).
199
+ * Without this, `create-astro`'s default `"astro check && astro build"` fails with
200
+ * `ts(2339) Property '__RETICLE_TOKEN__' does not exist on type 'Window'` after a clean init.
93
201
  */
94
202
  export function patchAstroEnvDts(existing) {
95
203
  if (null !== existing && existing.includes(ENV_DTS_MARKER)) {
@@ -21,8 +21,6 @@ export declare const CRA_DEV_MODULE_IMPORT = "import './reticle-dev';";
21
21
  * makes the first compile after init fail while the plan still reports `[✓]`.
22
22
  */
23
23
  export declare function craDevModulePath(typescript: boolean): string;
24
- /** TypeScript default path — prefer `craDevModulePath` when the project's language is known. */
25
- export declare const CRA_DEV_MODULE_PATH: string;
26
24
  export declare const CRA_ENV_PATH = ".env.development.local";
27
25
  export declare const TOKEN_VAR = "REACT_APP_RETICLE_TOKEN";
28
26
  /**
package/dist/patch/cra.js CHANGED
@@ -26,8 +26,6 @@ export const CRA_DEV_MODULE_IMPORT = "import './reticle-dev';";
26
26
  export function craDevModulePath(typescript) {
27
27
  return typescript ? 'src/reticle-dev.ts' : 'src/reticle-dev.js';
28
28
  }
29
- /** TypeScript default path — prefer `craDevModulePath` when the project's language is known. */
30
- export const CRA_DEV_MODULE_PATH = craDevModulePath(true);
31
29
  export const CRA_ENV_PATH = '.env.development.local';
32
30
  export const TOKEN_VAR = 'REACT_APP_RETICLE_TOKEN';
33
31
  /**
@@ -5,7 +5,16 @@
5
5
  * Preload is mechanical — prepend one line — so it is APPLY. Main is only APPLY when the file
6
6
  * constructs exactly one BrowserWindow as `const <name> = new BrowserWindow(`; anything else
7
7
  * (zero, several, a factory) is MANUAL carrying the doctor's existing fix text, not a guess.
8
+ *
9
+ * BOTH ARE DEV-ONLY, as the adapter's README says they must be. They used to be written unguarded
10
+ * — a static import in preload, a static import and call in main — so `electron-vite build` and
11
+ * `electron-forge package` bundled the IPC shim (which patches `ipcRenderer.invoke` and exposes
12
+ * `__reticleIpc`) and the screenshot IPC handler into the shipped app. Each is now behind a guard
13
+ * the bundler replaces at build time, so a production build folds the branch away and carries
14
+ * neither. Measured on electron-vite: absent from a production build, present in
15
+ * `--mode development`, which is the build the renderer's `desktop: true` also instruments.
16
+ * A line an older `init` wrote is rewritten in place on a re-run.
8
17
  */
9
18
  import { type SourcePatch } from './patch-kind.js';
10
- export declare function patchElectronPreload(source: string, path: string): SourcePatch;
19
+ export declare function patchElectronPreload(source: string, path: string, viteTypes?: boolean): SourcePatch;
11
20
  export declare function patchElectronMain(source: string, path?: string): SourcePatch;
@@ -5,48 +5,97 @@
5
5
  * Preload is mechanical — prepend one line — so it is APPLY. Main is only APPLY when the file
6
6
  * constructs exactly one BrowserWindow as `const <name> = new BrowserWindow(`; anything else
7
7
  * (zero, several, a factory) is MANUAL carrying the doctor's existing fix text, not a guess.
8
+ *
9
+ * BOTH ARE DEV-ONLY, as the adapter's README says they must be. They used to be written unguarded
10
+ * — a static import in preload, a static import and call in main — so `electron-vite build` and
11
+ * `electron-forge package` bundled the IPC shim (which patches `ipcRenderer.invoke` and exposes
12
+ * `__reticleIpc`) and the screenshot IPC handler into the shipped app. Each is now behind a guard
13
+ * the bundler replaces at build time, so a production build folds the branch away and carries
14
+ * neither. Measured on electron-vite: absent from a production build, present in
15
+ * `--mode development`, which is the build the renderer's `desktop: true` also instruments.
16
+ * A line an older `init` wrote is rewritten in place on a re-run.
8
17
  */
9
18
  import { matchingParenEnd } from './brace-scan.js';
10
19
  import { CAPTURE_REQUIRE, ELECTRON_CAPTURE_FIX, PRELOAD_REQUIRE, } from '../diagnose/desktop-doctor.js';
11
20
  import { PatchKind } from './patch-kind.js';
12
21
  const ESM_PRELOAD_EXT = /\.(?:ts|mts|mjs)$/i;
13
22
  const WINDOW_DECL = /const\s+(\w+)\s*=\s*new\s+BrowserWindow\s*\(/g;
14
- const IMPORT_LINE = /^import\s.+from\s+['"][^'"]+['"];?\s*$/gm;
15
- const REQUIRE_LINE = /^(?:const|let|var)\s+.+=\s+require\s*\(/gm;
23
+ /** Replaced by Vite in every bundled main and preload, so a production build folds it to false. */
24
+ const BUILD_MODE_GUARD = "import.meta.env.MODE !== 'production'";
25
+ /**
26
+ * Forge's own dev-server define (`MAIN_WINDOW_VITE_DEV_SERVER_URL` in its template): a URL under
27
+ * `electron-forge start`, undefined in a package, and declared by the template — so it folds AND
28
+ * typechecks, where Forge's `tsconfig` has no types for `import.meta.env`.
29
+ */
30
+ const FORGE_DEV_SERVER_DEFINE = /\b([A-Z][A-Z0-9_]*_VITE_DEV_SERVER_URL)\b/;
31
+ /** An unbundled CommonJS main has nothing to fold, so it asks at runtime. */
32
+ const PACKAGED_GUARD = "!require('electron').app.isPackaged";
33
+ /** The unguarded lines an older `init` wrote, rewritten in place on a re-run. */
34
+ const LEGACY_PRELOAD_IMPORT = new RegExp(`^import '${PRELOAD_REQUIRE}'\\n`, 'm');
35
+ const LEGACY_MAIN_IMPORT = new RegExp(`^(?:import \\{ installReticleCapture \\} from '${CAPTURE_REQUIRE}'|const \\{ installReticleCapture \\} = require\\('${CAPTURE_REQUIRE}'\\);)\\n`, 'm');
36
+ const LEGACY_MAIN_CALL = /^([ \t]*)installReticleCapture\((\w+)\)[ \t]*;?[ \t]*$/m;
16
37
  function isEsmPath(path) {
17
38
  return ESM_PRELOAD_EXT.test(path);
18
39
  }
19
- function preloadLine(path) {
20
- return isEsmPath(path) ? `import '${PRELOAD_REQUIRE}'\n` : `require('${PRELOAD_REQUIRE}');\n`;
40
+ /**
41
+ * The shim line, first in the file.
42
+ *
43
+ * A DYNAMIC import behind the build mode, for a bundled preload. Not a `require`: the electron-vite
44
+ * template lints with `no-require-imports`, and a clean install must not fail the project's own lint.
45
+ * Not the static import either, which no guard can remove. The cost is that the shim installs a
46
+ * microtask after the preload body runs rather than before it — still before the page's own
47
+ * scripts, and every exposed API that calls `ipcRenderer.invoke` at call time (electron-toolkit's
48
+ * included) is observed; one that captured the function itself (`invoke: ipcRenderer.invoke`) is
49
+ * not. Verified end to end on electron-vite: IPC observed and screenshots saved in dev, and no
50
+ * Reticle code in a production build.
51
+ *
52
+ * `viteTypes` is for Electron Forge, whose preload is SANDBOXED by Electron's default and whose
53
+ * tsconfig has no `import.meta.env` types. The same guarded line works there because Forge builds its
54
+ * preload with `codeSplitting: false` and the real build mode: the dynamic import is inlined into the
55
+ * one file a sandbox can load under `electron-forge start`, and folds away under `package`. Measured
56
+ * on a scaffolded Forge 8 app — Forge used to get the static import instead, which no guard removes,
57
+ * so the packaged app shipped the shim. The reference line supplies the types, so `tsc` stays green.
58
+ */
59
+ const VITE_CLIENT_TYPES = '/// <reference types="vite/client" />\n';
60
+ function preloadLine(path, viteTypes) {
61
+ if (!isEsmPath(path))
62
+ return `require('${PRELOAD_REQUIRE}');\n`;
63
+ return ((viteTypes ? VITE_CLIENT_TYPES : '') +
64
+ `if (${BUILD_MODE_GUARD}) {\n` +
65
+ ` // Reticle IPC observation, dev only — written by \`reticle init\`. A production build drops it.\n` +
66
+ ` void import('${PRELOAD_REQUIRE}').catch(() => undefined)\n` +
67
+ `}\n`);
21
68
  }
22
- export function patchElectronPreload(source, path) {
69
+ export function patchElectronPreload(source, path, viteTypes = false) {
70
+ if (isEsmPath(path) && LEGACY_PRELOAD_IMPORT.test(source)) {
71
+ return {
72
+ kind: PatchKind.APPLY,
73
+ code: `${preloadLine(path, viteTypes)}${source.replace(LEGACY_PRELOAD_IMPORT, '')}`,
74
+ };
75
+ }
23
76
  if (source.includes(PRELOAD_REQUIRE)) {
24
77
  return { kind: PatchKind.ALREADY };
25
78
  }
26
- return { kind: PatchKind.APPLY, code: `${preloadLine(path)}${source}` };
79
+ return { kind: PatchKind.APPLY, code: `${preloadLine(path, viteTypes)}${source}` };
27
80
  }
28
- function insertMainImport(source, path) {
29
- if (source.includes(CAPTURE_REQUIRE))
30
- return source;
31
- if (isEsmPath(path)) {
32
- const line = `import { installReticleCapture } from '${CAPTURE_REQUIRE}'`;
33
- const matches = [...source.matchAll(IMPORT_LINE)];
34
- const last = matches[matches.length - 1];
35
- if (last?.index === undefined)
36
- return `${line}\n${source}`;
37
- const end = last.index + last[0].length;
38
- return `${source.slice(0, end)}\n${line}${source.slice(end)}`;
81
+ /** The guarded capture call for this main file. Nothing to import at the top: it loads itself. */
82
+ function captureCall(source, path, windowName) {
83
+ if (!isEsmPath(path)) {
84
+ return `if (${PACKAGED_GUARD}) require('${CAPTURE_REQUIRE}').installReticleCapture(${windowName})`;
39
85
  }
40
- const line = `const { installReticleCapture } = require('${CAPTURE_REQUIRE}');`;
41
- const matches = [...source.matchAll(REQUIRE_LINE)];
42
- const last = matches[matches.length - 1];
43
- if (last?.index === undefined)
44
- return `${line}\n${source}`;
45
- const endOfLine = source.indexOf('\n', last.index);
46
- const end = endOfLine < 0 ? source.length : endOfLine;
47
- return `${source.slice(0, end)}\n${line}${source.slice(end)}`;
86
+ const guard = FORGE_DEV_SERVER_DEFINE.exec(source)?.[1] ?? BUILD_MODE_GUARD;
87
+ return `if (${guard}) void import('${CAPTURE_REQUIRE}').then(({ installReticleCapture }) => installReticleCapture(${windowName}))`;
48
88
  }
49
89
  export function patchElectronMain(source, path = 'electron/main.cjs') {
90
+ const legacyCall = LEGACY_MAIN_CALL.exec(source);
91
+ if (LEGACY_MAIN_IMPORT.test(source) && legacyCall?.[2] !== undefined) {
92
+ const withoutImport = source.replace(LEGACY_MAIN_IMPORT, '');
93
+ const indent = legacyCall[1] ?? '';
94
+ return {
95
+ kind: PatchKind.APPLY,
96
+ code: withoutImport.replace(LEGACY_MAIN_CALL, `${indent}${captureCall(withoutImport, path, legacyCall[2])}`),
97
+ };
98
+ }
50
99
  if (source.includes(CAPTURE_REQUIRE)) {
51
100
  return { kind: PatchKind.ALREADY };
52
101
  }
@@ -65,6 +114,9 @@ export function patchElectronMain(source, path = 'electron/main.cjs') {
65
114
  insertAt += 1;
66
115
  const indentMatch = /^[ \t]*/.exec(source.slice(source.lastIndexOf('\n', matches[0].index) + 1));
67
116
  const indent = indentMatch?.[0] ?? ' ';
68
- const withCall = `${source.slice(0, insertAt)}\n${indent}installReticleCapture(${name})${source.slice(insertAt)}`;
69
- return { kind: PatchKind.APPLY, code: insertMainImport(withCall, path) };
117
+ const call = captureCall(source, path, name);
118
+ return {
119
+ kind: PatchKind.APPLY,
120
+ code: `${source.slice(0, insertAt)}\n${indent}${call}${source.slice(insertAt)}`,
121
+ };
70
122
  }
@@ -19,7 +19,7 @@ const RENDERER_KEY = /renderer\s*:\s*/;
19
19
  const NO_RENDERER_REASON = "couldn't find a `renderer` key to extend";
20
20
  const RENDERER_NOT_LITERAL_REASON = 'this config sets `renderer:` to something other than an object literal — merging into it is your call, not a text edit Reticle should make';
21
21
  const WRONG_BLOCK_REASON = 'reticle() is in this file but not in the `renderer` block — the plugin belongs where there is a document, and leaving it in `main` or `preload` would report success for an app that cannot connect';
22
- const MISSING_DESKTOP_REASON = 'reticle() is already in the `renderer` block but without `desktop: true` — a packaged renderer is a production build with no dev server, so the plugin is dropped from `vite build` and the shipped app has no connect()';
22
+ const MISSING_DESKTOP_REASON = 'reticle() is already in the `renderer` block but without `desktop: true` — a packaged renderer has no dev server and reports production, so without it the SDK refuses to start there and the packaged app never connects';
23
23
  function reticlePluginCall(port) {
24
24
  const options = [
25
25
  'desktop: true',
@@ -15,4 +15,4 @@
15
15
  import type { SourcePatch } from './patch-kind.js';
16
16
  /** The config filenames Nuxt itself accepts, in the order `init` should prefer them. */
17
17
  export declare const NUXT_CONFIG_CANDIDATES: string[];
18
- export declare function patchNuxtConfig(source: string): SourcePatch;
18
+ export declare function patchNuxtConfig(source: string, viteMajor?: number | null): SourcePatch;
@@ -28,6 +28,6 @@ const NUXT_VITE_CONFIG = {
28
28
  defineCall: /defineNuxtConfig\s*\(\s*\{/,
29
29
  sdkSpecifier: sdkImport(UiLibrary.VUE).specifier,
30
30
  };
31
- export function patchNuxtConfig(source) {
32
- return patchViteOwningConfig(source, NUXT_VITE_CONFIG);
31
+ export function patchNuxtConfig(source, viteMajor = null) {
32
+ return patchViteOwningConfig(source, { ...NUXT_VITE_CONFIG, viteMajor });
33
33
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The plugin directory Nuxt actually scans, which is not the same on 3 and 4.
3
+ *
4
+ * Nuxt 4's default `srcDir` is `app/`, so plugins live in `app/plugins/`. Nuxt 3's is the project
5
+ * root, so they live in `plugins/`. Writing to the wrong one is the silent failure this whole change
6
+ * exists to remove: the file is on disk, `init` reports it green, Nuxt never scans that directory,
7
+ * and no plugin is ever registered. Keyed on whether the app HAS an `app/` directory, which is the
8
+ * same signal Nuxt itself uses to pick its srcDir.
9
+ */
10
+ export declare function nuxtPluginPath(hasAppDir: boolean): string;
11
+ /**
12
+ * The dev-only Nuxt client plugin `init` writes.
13
+ *
14
+ * Every trap in it is one somebody actually hit. `import.meta.dev` rather than a hostname check: the
15
+ * reported failure was a snippet guarded on `window.location.hostname === 'localhost'`, which is
16
+ * false on any hosts-file alias or LAN address and throws during SSR, so the connect never ran with
17
+ * no error and no log line. `.client.ts` rather than an SSR guard, because that suffix is what keeps
18
+ * this out of the server bundle. `@reticlehq/browser` rather than the React kit, because installing
19
+ * a package named `@reticlehq/react` into a Vue codebase is the single thing most likely to make
20
+ * somebody abandon the setup.
21
+ *
22
+ * The token is inlined by `nuxt.config`'s `vite.define` (see nuxt-patch.ts). The bridge requires it
23
+ * even on localhost, and nothing in a browser can read the file it lives in.
24
+ */
25
+ export declare function nuxtPluginFile(port: number | undefined, projectId?: string, testids?: readonly string[]): string;
26
+ /**
27
+ * Said as its own NOTICE beside the write, not inside it: a running dev server does not pick up a
28
+ * new plugin, and off localhost the connect needs two more arguments.
29
+ *
30
+ * A ✓ line is one SKILL.md tells the reader to skip, so a caveat carried on the ✓ is a caveat nobody
31
+ * reads — and both of these end with an app that boots, looks correct and never pairs.
32
+ */
33
+ export declare const NUXT_PLUGIN_NOTICE: string;
34
+ /**
35
+ * The Nuxt recipe, written out in full because each trap in it is one a Nuxt 4 app actually hit:
36
+ * a Vue codebase classified as `html` and handed `@reticlehq/react`, a connect guarded on
37
+ * `window.location.hostname === 'localhost'` (false on any hosts-file alias, and `window` does not
38
+ * exist during SSR), and a running dev server that never picked up the new plugin.
39
+ *
40
+ * So: `import.meta.dev` instead of a hostname check, `.client.ts` instead of an SSR guard, the
41
+ * framework-neutral sensor instead of the React kit, the non-localhost flag named up front, and the
42
+ * restart said out loud.
43
+ */
44
+ export declare function nuxtManual(port: number | undefined, projectId?: string): string;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * The Nuxt connect plugin `reticle init` writes, split out of snippets.ts (which crossed the
3
+ * 1000-line cap): its path, the plugin file itself, the notice, and the manual fallback.
4
+ */
5
+ import { ReticleDir } from '@reticlehq/core';
6
+ import { connectArg, registerCapabilitiesCall, STATE_DOCS_URL } from './snippets.js';
7
+ /** Where a Nuxt dev-only client plugin belongs. `.client` keeps it out of SSR; Nuxt auto-registers it. */
8
+ const NUXT_PLUGIN_PATH = 'app/plugins/reticle.client.ts';
9
+ /**
10
+ * The plugin directory Nuxt actually scans, which is not the same on 3 and 4.
11
+ *
12
+ * Nuxt 4's default `srcDir` is `app/`, so plugins live in `app/plugins/`. Nuxt 3's is the project
13
+ * root, so they live in `plugins/`. Writing to the wrong one is the silent failure this whole change
14
+ * exists to remove: the file is on disk, `init` reports it green, Nuxt never scans that directory,
15
+ * and no plugin is ever registered. Keyed on whether the app HAS an `app/` directory, which is the
16
+ * same signal Nuxt itself uses to pick its srcDir.
17
+ */
18
+ export function nuxtPluginPath(hasAppDir) {
19
+ return hasAppDir ? NUXT_PLUGIN_PATH : 'plugins/reticle.client.ts';
20
+ }
21
+ /**
22
+ * The dev-only Nuxt client plugin `init` writes.
23
+ *
24
+ * Every trap in it is one somebody actually hit. `import.meta.dev` rather than a hostname check: the
25
+ * reported failure was a snippet guarded on `window.location.hostname === 'localhost'`, which is
26
+ * false on any hosts-file alias or LAN address and throws during SSR, so the connect never ran with
27
+ * no error and no log line. `.client.ts` rather than an SSR guard, because that suffix is what keeps
28
+ * this out of the server bundle. `@reticlehq/browser` rather than the React kit, because installing
29
+ * a package named `@reticlehq/react` into a Vue codebase is the single thing most likely to make
30
+ * somebody abandon the setup.
31
+ *
32
+ * The token is inlined by `nuxt.config`'s `vite.define` (see nuxt-patch.ts). The bridge requires it
33
+ * even on localhost, and nothing in a browser can read the file it lives in.
34
+ */
35
+ export function nuxtPluginFile(port, projectId, testids = []) {
36
+ const base = connectArg(port, projectId);
37
+ const fields = '' === base ? '' : `${base.slice(1, -1).trim()}, `;
38
+ return `// Dev-only: connect Reticle from a Nuxt client plugin. Nuxt owns its own Vite instance and
39
+ // renders its own HTML, so the Vite plugin's index.html injection never fires — a .client plugin is
40
+ // the path that can register a session at all.
41
+ export default defineNuxtPlugin(() => {
42
+ // import.meta.dev is resolved at BUILD time, so it does not care what hostname you develop on.
43
+ // Do NOT guard on window.location.hostname === 'localhost': that is false on any hosts-file alias
44
+ // or LAN address, and window does not exist during SSR.
45
+ if (!import.meta.dev) return;
46
+ void import('@reticlehq/browser').then(({ reticle, registerCapabilities }) => {
47
+ // Both inlined by nuxt.config's vite.define, which \`reticle init\` wrote. The bridge refuses a
48
+ // connect with no token even on localhost; the root makes source paths repo-relative.
49
+ const token = typeof __RETICLE_TOKEN__ !== 'undefined' ? __RETICLE_TOKEN__ : '';
50
+ const root = typeof __RETICLE_ROOT__ !== 'undefined' ? __RETICLE_ROOT__ : '';
51
+ reticle.connect({
52
+ ${fields}...(token.length > 0 ? { token } : {}),
53
+ ...(root.length > 0 ? { root } : {}),
54
+ });
55
+
56
+ // What the agent can drive without guessing. Add a store here too — for Pinia,
57
+ // registerStore('<key>', piniaStore(<useYourStore>())) — and the agent can check what the app
58
+ // BELIEVES, not just what it rendered. See ${STATE_DOCS_URL}
59
+ ${registerCapabilitiesCall(testids, ' ')}
60
+ });
61
+ });
62
+
63
+ declare const __RETICLE_TOKEN__: string | undefined;
64
+ declare const __RETICLE_ROOT__: string | undefined;
65
+ `;
66
+ }
67
+ /**
68
+ * Said as its own NOTICE beside the write, not inside it: a running dev server does not pick up a
69
+ * new plugin, and off localhost the connect needs two more arguments.
70
+ *
71
+ * A ✓ line is one SKILL.md tells the reader to skip, so a caveat carried on the ✓ is a caveat nobody
72
+ * reads — and both of these end with an app that boots, looks correct and never pairs.
73
+ */
74
+ export const NUXT_PLUGIN_NOTICE = 'Restart the dev server: one that is already running does not pick up a new plugin — it will not ' +
75
+ 'appear in .nuxt/plugins/client.mjs and the app comes up with no SDK at all. And if your dev host ' +
76
+ 'is anything other than localhost (a hosts-file alias, a LAN IP, a tunnel), that connect call ' +
77
+ 'needs TWO additions, not one: allowNonLocalhost: true AND the token from ~/.reticle/pairing-token. ' +
78
+ 'The flag alone is NOT sufficient off localhost.';
79
+ /**
80
+ * The Nuxt recipe, written out in full because each trap in it is one a Nuxt 4 app actually hit:
81
+ * a Vue codebase classified as `html` and handed `@reticlehq/react`, a connect guarded on
82
+ * `window.location.hostname === 'localhost'` (false on any hosts-file alias, and `window` does not
83
+ * exist during SSR), and a running dev server that never picked up the new plugin.
84
+ *
85
+ * So: `import.meta.dev` instead of a hostname check, `.client.ts` instead of an SSR guard, the
86
+ * framework-neutral sensor instead of the React kit, the non-localhost flag named up front, and the
87
+ * restart said out loud.
88
+ */
89
+ export function nuxtManual(port, projectId) {
90
+ const base = connectArg(port, projectId);
91
+ const fields = '' === base ? '' : base.slice(1, -1).trim();
92
+ const connect = '' === fields ? 'reticle.connect()' : `reticle.connect({ ${fields} })`;
93
+ return `Nuxt owns its own Vite instance and renders its own HTML, so there is no vite.config to patch
94
+ and no index.html to inject into. Wire it with a dev-only CLIENT plugin, which is Nuxt's own idiom:
95
+
96
+ 1. Create ${NUXT_PLUGIN_PATH}:
97
+
98
+ export default defineNuxtPlugin(() => {
99
+ // import.meta.dev is the correct guard: it is resolved at build time, so it does not care
100
+ // what hostname you develop on. Do NOT guard on window.location.hostname === 'localhost' —
101
+ // that is false on any hosts-file alias or LAN address, and window does not exist in SSR.
102
+ if (!import.meta.dev) return
103
+ void import('@reticlehq/browser').then(({ reticle }) => {
104
+ ${connect}
105
+ })
106
+ })
107
+
108
+ The .client.ts suffix is load-bearing: it is what keeps this out of the server bundle.
109
+
110
+ 2. Restart the dev server. A dev server that is already running does not pick up a new plugin —
111
+ it will not appear in .nuxt/plugins/client.mjs, and the app will come up with no SDK at all.
112
+
113
+ 3. If your dev host is anything other than localhost (a hosts-file alias, a LAN IP, a tunnel), that
114
+ connect call needs TWO additions, not one: allowNonLocalhost: true AND a pairing token passed as
115
+ token. The flag alone is NOT sufficient off localhost. The token is the one in
116
+ ~/.reticle/pairing-token. Without both, the SDK loads and then refuses, and the only sign is one
117
+ line in the browser console.
118
+
119
+ 4. Add this to nuxt.config, so the dev server does not watch Reticle's own journal:
120
+
121
+ vite: { server: { watch: { ignored: [/(^|[\\\\/])\\.${ReticleDir.ROOT.slice(1)}([\\\\/]|$)/] } } }
122
+
123
+ Reticle journals every session into ${ReticleDir.ROOT}/ in your project root, rewriting one file
124
+ in it continuously while a session is live. Nuxt's dev server watches that root, so without this
125
+ it sees each write as a project file changing and full-reloads the page — which reconnects the
126
+ SDK, which produces the next write. The loop runs several times a second and looks like anything
127
+ except what it is: refs go stale, actions die mid-flight, and the session appears to flap.
128
+ It is a RegExp rather than a glob on purpose — chokidar dropped glob support in v4, so a
129
+ double-star pattern here is accepted and matches nothing.
130
+
131
+ The package is @reticlehq/browser — the framework-neutral sensor. DOM, network, console, routing and
132
+ source file:line all work in Vue. What you do not get is React component identity, which is the only
133
+ thing the React adapter adds. The install gate scaffolds a Nuxt app on every release and requires a session, so this wiring
134
+ is proven; if something does not work, please open an issue.`;
135
+ }