@reticlehq/init 3.1.0 → 3.3.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 (39) hide show
  1. package/dist/detect/detect.d.ts +13 -2
  2. package/dist/detect/detect.js +46 -3
  3. package/dist/detect/workspace-redirect.d.ts +2 -1
  4. package/dist/diagnose/csp-doctor.d.ts +20 -1
  5. package/dist/diagnose/csp-doctor.js +101 -4
  6. package/dist/diagnose/desktop-doctor.d.ts +1 -1
  7. package/dist/diagnose/install-retries.d.ts +2 -1
  8. package/dist/host.d.ts +6 -0
  9. package/dist/host.js +3 -0
  10. package/dist/index.d.ts +11 -1
  11. package/dist/index.js +11 -1
  12. package/dist/patch/astro-patch.d.ts +47 -12
  13. package/dist/patch/astro-patch.js +149 -41
  14. package/dist/patch/nuxt-snippets.d.ts +44 -0
  15. package/dist/patch/nuxt-snippets.js +135 -0
  16. package/dist/patch/snippets.d.ts +1 -48
  17. package/dist/patch/snippets.js +68 -178
  18. package/dist/patch/vite-env-types.d.ts +9 -0
  19. package/dist/patch/vite-env-types.js +19 -0
  20. package/dist/patch/vite-owning-config.d.ts +13 -0
  21. package/dist/patch/vite-owning-config.js +43 -16
  22. package/dist/plan/connect-steps.d.ts +3 -2
  23. package/dist/plan/connect-steps.js +4 -2
  24. package/dist/plan/framework-adapter.d.ts +2 -5
  25. package/dist/plan/framework-adapter.js +6 -5
  26. package/dist/plan/plan-framework.d.ts +1 -1
  27. package/dist/plan/plan-framework.js +33 -11
  28. package/dist/plan/plan-types.d.ts +18 -2
  29. package/dist/plan/plan.js +117 -35
  30. package/dist/plan/stop-hook-step.d.ts +4 -0
  31. package/dist/plan/stop-hook-step.js +49 -0
  32. package/dist/project/agent-rules.js +2 -0
  33. package/dist/register/mcp-clients.d.ts +13 -0
  34. package/dist/register/mcp-clients.js +22 -0
  35. package/dist/register/mcp.d.ts +8 -0
  36. package/dist/register/mcp.js +16 -0
  37. package/dist/run-types.d.ts +12 -0
  38. package/dist/run.js +42 -6
  39. package/package.json +2 -2
@@ -2,9 +2,10 @@
2
2
  * Generated file contents and copy-paste snippets for `reticle init`. Kept as named constants/builders
3
3
  * so the runner never inlines free strings.
4
4
  */
5
- import { RETICLE_DEFAULT_PORT, ReticleDir, bridgeWsUrl, RETICLE_CLIENT_HOST, RETICLE_WS_PATH, } from '@reticlehq/core';
5
+ import { RETICLE_DEFAULT_PORT, bridgeWsUrl, RETICLE_CLIENT_HOST, RETICLE_WS_PATH, } from '@reticlehq/core';
6
6
  import { UiLibrary } from '../detect/detect.js';
7
7
  import { RETICLE_VERSION } from '../version.js';
8
+ import { VITE_ENV_DEV_DECLARATION } from './vite-env-types.js';
8
9
  /**
9
10
  * The SDK as one import a plain page can actually resolve.
10
11
  *
@@ -113,11 +114,13 @@ pointers, not the app.`;
113
114
  import { reticle } from '@reticlehq/vite-plugin';
114
115
 
115
116
  export default defineConfig({
116
- plugins: [${frameworkPluginExample(uiLibrary)}, ${call}],
117
+ plugins: [${call}, ${frameworkPluginExample(uiLibrary)}],
117
118
  });
118
119
 
119
- Keep \`reticle()\` LAST so it sees the output of your other plugins. It only applies during \`vite\`
120
- (dev) — it is dropped from \`vite build\`.${note}`;
120
+ The position in the array does not matter: the plugin declares \`enforce: 'pre'\`, so Vite runs it
121
+ before every normal plugin wherever you put it. Shown first only because that is where \`init\`
122
+ writes it when it patches the config for you. It applies during \`vite\` (dev) only — it is dropped
123
+ from \`vite build\`.${note}`;
121
124
  }
122
125
  /**
123
126
  * The electron-vite recipe: the plugin belongs in `renderer`, never in `main` or `preload`.
@@ -202,10 +205,13 @@ export function ReticleDev() {
202
205
  // withReticle() finds the daemon serving this project on every dev-server start. It wins over
203
206
  // any port written into this file at install time, so moving the daemon needs no edit here.
204
207
  const url = process.env.NEXT_PUBLIC_RETICLE_URL;
208
+ // So a version-skewed pair names the SDK's version instead of "an unknown older wire".
209
+ const sdkVersion = process.env.NEXT_PUBLIC_RETICLE_SDK_VERSION;
205
210
  reticle.connect({
206
211
  ${fields}...(url ? { url } : {}),
207
212
  ...(token ? { token } : {}),
208
213
  ...(root ? { root } : {}),
214
+ ...(sdkVersion ? { sdkVersion } : {}),
209
215
  });
210
216
 
211
217
  // ── Start with ONE flow. ────────────────────────────────────────────────────────────────
@@ -261,10 +267,9 @@ layoutPath) {
261
267
  ? `\n url: '${bridgeWsUrl(port)}',`
262
268
  : '';
263
269
  const id = projectId !== undefined && projectId.length > 0 ? `\n projectId: '${projectId}',` : '';
264
- // Astro owns its Vite instance, so it has a build-time channel of its own: the same `define` that
265
- // already inlines the token can inline the daemon URL, resolved every time the config is read —
266
- // that is, on every `astro dev`. The port above is written once at install time and goes stale the
267
- // moment the daemon moves; this does not.
270
+ // Astro owns its Vite instance, so the layout can resolve the daemon URL in frontmatter on every
271
+ // `astro dev` and put it on a <meta> the client script reads. The port above is written once at
272
+ // install time and goes stale the moment the daemon moves; this does not.
268
273
  //
269
274
  // Needs the projectId to know WHICH daemon is ours. Without one there is nothing to match on, and
270
275
  // adopting a daemon serving another project would report its state as this app's, so the whole
@@ -274,10 +279,8 @@ layoutPath) {
274
279
  // `bridgeWsUrl` builds, and a generator that spells it out by hand is exactly how the four call
275
280
  // sites that constant exists to unify drifted apart in the first place.
276
281
  const RETICLE_CLIENT_HOST_LITERAL = `ws://${RETICLE_CLIENT_HOST}:`;
277
- const urlHelper = canDiscover
282
+ const urlFrontmatter = canDiscover
278
283
  ? `
279
- // The live daemon serving THIS project, re-read on every \`astro dev\`. Same rule as the Vite and
280
- // Next plugins: skip dead daemons, match on projectId, lowest port wins, '' when none matches.
281
284
  function reticleUrl() {
282
285
  const dir = process.env['RETICLE_PAIRING_TOKEN_DIR'] || join(homedir(), '.reticle');
283
286
  let best;
@@ -293,63 +296,77 @@ layoutPath) {
293
296
  } catch { return ''; }
294
297
  return best === undefined ? '' : '${RETICLE_CLIENT_HOST_LITERAL}' + best + '${RETICLE_WS_PATH}';
295
298
  }
299
+ const pairingUrl = import.meta.env.DEV ? reticleUrl() : '';
296
300
  `
297
301
  : '';
298
- const urlDefine = canDiscover ? `\n __RETICLE_URL__: JSON.stringify(reticleUrl()),` : '';
302
+ const urlMeta = canDiscover ? `\n <meta name="reticle-pairing-url" content={pairingUrl} />` : '';
299
303
  const urlRead = canDiscover
300
- ? `\n const url = typeof __RETICLE_URL__ !== 'undefined' ? __RETICLE_URL__ : '';`
304
+ ? `\n const url = document.querySelector('meta[name="reticle-pairing-url"]')?.getAttribute('content') ?? '';`
301
305
  : '';
302
- // After the baked port, so the discovered one wins: later key in an object literal.
303
- const urlSpread = canDiscover ? `\n ...(url.length > 0 ? { url } : {}),` : '';
304
- return `Astro renders its own HTML, so the connect goes in a page <script> and the pairing token is inlined by the config.
306
+ const urlSpread = canDiscover ? `\n ...(url.length > 0 ? { url } : {}),` : '';
307
+ return `Astro renders its own HTML, so the connect goes in a local module the page statically imports. Do not put the pairing token through \`vite.define\`: on Astro 7.2+ that substitution never reaches the client, the identifier stays literal, and the bridge refuses the dial. Read the token in frontmatter and put it on a <meta> the module can query. Reading it per request also means a token written after the dev server started is picked up on the next load instead of needing a restart.
305
308
 
306
- 1. In astro.config.mjs — inline the daemon's token and raise the build target:
309
+ 1. In astro.config.mjs, raise the build target. The token does NOT belong here:
307
310
 
308
- import { readFileSync, readdirSync } from 'node:fs';
309
- import { homedir } from 'node:os';
310
- import { join } from 'node:path';
311
-
312
- function reticleToken() {
313
- const dir = process.env['RETICLE_PAIRING_TOKEN_DIR'] || join(homedir(), '.reticle');
314
- try { return readFileSync(join(dir, 'pairing-token'), 'utf8').trim(); } catch { return ''; }
315
- }
316
- ${urlHelper}
317
311
  export default defineConfig({
318
312
  vite: {
319
313
  // Astro's default target down-levels the modern SDK bundle and fails on a destructuring transform.
320
314
  build: { target: 'es2022' },
321
- optimizeDeps: { esbuildOptions: { target: 'es2022' } },
322
- define: {
323
- __RETICLE_TOKEN__: JSON.stringify(reticleToken()),${urlDefine}
324
- // Without this, source pointers come back as absolute paths from YOUR machine — useless in a
325
- // report. Every other framework gets it from its build plugin; Astro owns its Vite instance.
326
- __RETICLE_ROOT__: JSON.stringify(process.cwd()),
327
- },
315
+ optimizeDeps: { include: ['@reticlehq/react'] },
328
316
  },
329
317
  });
330
318
 
331
319
  ${layoutHost(layoutPath)}
332
320
 
321
+ ---
322
+ import { readFileSync${canDiscover ? ', readdirSync' : ''} } from 'node:fs';
323
+ import { homedir } from 'node:os';
324
+ import { join } from 'node:path';
325
+ const pairingToken = (() => {
326
+ if (!import.meta.env.DEV) return '';
327
+ const dir = process.env['RETICLE_PAIRING_TOKEN_DIR'] || join(homedir(), '.reticle');
328
+ try { return readFileSync(join(dir, 'pairing-token'), 'utf8').trim(); } catch { return ''; }
329
+ })();
330
+ const pairingRoot = import.meta.env.DEV ? process.cwd() : '';${urlFrontmatter} ---
331
+ <meta name="reticle-pairing-token" content={pairingToken} />
332
+ <meta name="reticle-pairing-root" content={pairingRoot} />${urlMeta}
333
333
  <script>
334
+ import connectReticle from '../components/ReticleDev';
334
335
  if (import.meta.env.DEV) {
335
- const token = typeof __RETICLE_TOKEN__ !== 'undefined' ? __RETICLE_TOKEN__ : '';
336
- const root = typeof __RETICLE_ROOT__ !== 'undefined' ? __RETICLE_ROOT__ : '';${urlRead}
337
- const { reticle, install } = await import('@reticlehq/react');
338
- install();
339
- reticle.connect({${id}${extra}${urlSpread}
340
- ...(token.length > 0 ? { token } : {}),
341
- ...(root.length > 0 ? { root } : {}),
342
- });
336
+ connectReticle();
343
337
  }
344
338
  </script>
345
339
 
346
- 3. In src/env.d.ts — declare the Vite define names so \`astro check\` can see them (create-astro's
347
- default build runs check first, and without this it fails with Cannot find name '__RETICLE_TOKEN__'):
340
+ The <script> tag must not sit inside a conditional. Astro hoists them statically, so
341
+ \`{isDev && (<script />)}\` is never hoisted and the module 404s.
342
+ The dev gate belongs in the imported module.
348
343
 
349
- declare const __RETICLE_TOKEN__: string | undefined;
350
- declare const __RETICLE_ROOT__: string | undefined;
344
+ 3. Create src/components/ReticleDev.ts — Vite owns this file, so the SDK is a static import:
351
345
 
352
- Start the daemon BEFORE \`astro dev\`, so the token file exists when the config is read. Until it does the token is empty and the page reloads once the daemon is up.`;
346
+ import { reticle, install } from '@reticlehq/react';
347
+ export default function connectReticle() {
348
+ const token = document.querySelector('meta[name="reticle-pairing-token"]')?.getAttribute('content') ?? '';
349
+ const root = document.querySelector('meta[name="reticle-pairing-root"]')?.getAttribute('content') ?? '';${urlRead}
350
+ if (token.length === 0) {
351
+ console.warn('[reticle] no pairing token was available when this page rendered, so the app will connect and be refused — you will see NO SESSION even though the SDK loads and the socket opens. The token is written by the Reticle daemon: start it and reload this page.');
352
+ }
353
+ install();
354
+ reticle.connect({${id}${extra}${urlSpread}
355
+ ...(token.length > 0 ? { token } : {}),
356
+ ...(root.length > 0 ? { root } : {}),
357
+ });
358
+ }
359
+
360
+ 4. In src/env.d.ts — declare the window names so \`astro check\` can see them (create-astro's
361
+ default build runs check first):
362
+
363
+ interface Window {
364
+ __RETICLE_TOKEN__?: string;
365
+ __RETICLE_ROOT__?: string;
366
+ }
367
+
368
+ Start the daemon, then load the page. The token is read when the page renders, so a reload is enough
369
+ once the token file exists — you do not have to restart \`astro dev\`.`;
353
370
  }
354
371
  /**
355
372
  * The `registerCapabilities` call every generated connect carries.
@@ -422,7 +439,8 @@ ${storeBlock}
422
439
  stores: [${found.map((s) => `'${s.key}'`).join(', ')}], // the keys you registered above
423
440
  });
424
441
  }
425
- `;
442
+
443
+ ${VITE_ENV_DEV_DECLARATION}`;
426
444
  }
427
445
  /** Where that module goes. Matches @reticlehq/vite-plugin's convention list. */
428
446
  export const VITE_DEV_MODULE_PATH = 'src/reticle-dev.ts';
@@ -502,7 +520,7 @@ st.html(
502
520
  * `<head>`, so the SDK sees a parsed document.
503
521
  */
504
522
  export function djangoMiddlewareSnippet(connectArgLiteral) {
505
- return `# reticle_dev.py — add to MIDDLEWARE while DEBUG is on:
523
+ return `# reticle_dev.py: add to MIDDLEWARE while DEBUG is on:
506
524
  # MIDDLEWARE = [..., "reticle_dev.ReticleDevMiddleware"]
507
525
  from django.conf import settings
508
526
 
@@ -682,7 +700,7 @@ export const REACT_ROUTER_ENTRY_PATH = 'app/entry.client.tsx';
682
700
  /** The dev-only module @reticlehq/vite-plugin serves: connect() with the token already in it. */
683
701
  const RETICLE_CONNECT_MODULE = '/@reticle-connect';
684
702
  /** The one line that puts Reticle in a React Router client entry. */
685
- export const REACT_ROUTER_CONNECT_LINE = `if (import.meta.env.DEV) void import('/@reticle-connect');`;
703
+ const REACT_ROUTER_CONNECT_LINE = `if (import.meta.env.DEV) void import('/@reticle-connect');`;
686
704
  /**
687
705
  * The client entry `init` writes when React Router framework mode has none.
688
706
  *
@@ -735,135 +753,7 @@ export function reactRouterEntryPatch(source) {
735
753
  lines.splice(lastImport + 1, 0, REACT_ROUTER_CONNECT_LINE);
736
754
  return lines.join('\n');
737
755
  }
738
- /** Where a Nuxt dev-only client plugin belongs. `.client` keeps it out of SSR; Nuxt auto-registers it. */
739
- export const NUXT_PLUGIN_PATH = 'app/plugins/reticle.client.ts';
740
- /**
741
- * The plugin directory Nuxt actually scans, which is not the same on 3 and 4.
742
- *
743
- * Nuxt 4's default `srcDir` is `app/`, so plugins live in `app/plugins/`. Nuxt 3's is the project
744
- * root, so they live in `plugins/`. Writing to the wrong one is the silent failure this whole change
745
- * exists to remove: the file is on disk, `init` reports it green, Nuxt never scans that directory,
746
- * and no plugin is ever registered. Keyed on whether the app HAS an `app/` directory, which is the
747
- * same signal Nuxt itself uses to pick its srcDir.
748
- */
749
- export function nuxtPluginPath(hasAppDir) {
750
- return hasAppDir ? NUXT_PLUGIN_PATH : 'plugins/reticle.client.ts';
751
- }
752
- /**
753
- * The dev-only Nuxt client plugin `init` writes.
754
- *
755
- * Every trap in it is one somebody actually hit. `import.meta.dev` rather than a hostname check: the
756
- * reported failure was a snippet guarded on `window.location.hostname === 'localhost'`, which is
757
- * false on any hosts-file alias or LAN address and throws during SSR, so the connect never ran with
758
- * no error and no log line. `.client.ts` rather than an SSR guard, because that suffix is what keeps
759
- * this out of the server bundle. `@reticlehq/browser` rather than the React kit, because installing
760
- * a package named `@reticlehq/react` into a Vue codebase is the single thing most likely to make
761
- * somebody abandon the setup.
762
- *
763
- * The token is inlined by `nuxt.config`'s `vite.define` (see nuxt-patch.ts). The bridge requires it
764
- * even on localhost, and nothing in a browser can read the file it lives in.
765
- */
766
- export function nuxtPluginFile(port, projectId, testids = []) {
767
- const base = connectArg(port, projectId);
768
- const fields = '' === base ? '' : `${base.slice(1, -1).trim()}, `;
769
- return `// Dev-only: connect Reticle from a Nuxt client plugin. Nuxt owns its own Vite instance and
770
- // renders its own HTML, so the Vite plugin's index.html injection never fires — a .client plugin is
771
- // the path that can register a session at all.
772
- export default defineNuxtPlugin(() => {
773
- // import.meta.dev is resolved at BUILD time, so it does not care what hostname you develop on.
774
- // Do NOT guard on window.location.hostname === 'localhost': that is false on any hosts-file alias
775
- // or LAN address, and window does not exist during SSR.
776
- if (!import.meta.dev) return;
777
- void import('@reticlehq/browser').then(({ reticle, registerCapabilities }) => {
778
- // Both inlined by nuxt.config's vite.define, which \`reticle init\` wrote. The bridge refuses a
779
- // connect with no token even on localhost; the root makes source paths repo-relative.
780
- const token = typeof __RETICLE_TOKEN__ !== 'undefined' ? __RETICLE_TOKEN__ : '';
781
- const root = typeof __RETICLE_ROOT__ !== 'undefined' ? __RETICLE_ROOT__ : '';
782
- reticle.connect({
783
- ${fields}...(token.length > 0 ? { token } : {}),
784
- ...(root.length > 0 ? { root } : {}),
785
- });
786
-
787
- // What the agent can drive without guessing. Add a store here too — registerStore('cart',
788
- // piniaStore(useCartStore())) — and the agent can check what the app BELIEVES, not just what it
789
- // rendered. See node_modules/@reticlehq/server/docs/usage.md.
790
- ${registerCapabilitiesCall(testids, ' ')}
791
- });
792
- });
793
-
794
- declare const __RETICLE_TOKEN__: string | undefined;
795
- declare const __RETICLE_ROOT__: string | undefined;
796
- `;
797
- }
798
- /**
799
- * Said as its own NOTICE beside the write, not inside it: a running dev server does not pick up a
800
- * new plugin, and off localhost the connect needs two more arguments.
801
- *
802
- * A ✓ line is one SKILL.md tells the reader to skip, so a caveat carried on the ✓ is a caveat nobody
803
- * reads — and both of these end with an app that boots, looks correct and never pairs.
804
- */
805
- export const NUXT_PLUGIN_NOTICE = 'Restart the dev server: one that is already running does not pick up a new plugin — it will not ' +
806
- 'appear in .nuxt/plugins/client.mjs and the app comes up with no SDK at all. And if your dev host ' +
807
- 'is anything other than localhost (a hosts-file alias, a LAN IP, a tunnel), that connect call ' +
808
- 'needs TWO additions, not one: allowNonLocalhost: true AND the token from ~/.reticle/pairing-token. ' +
809
- 'The flag alone is NOT sufficient off localhost.';
810
- /**
811
- * The Nuxt recipe, written out in full because each trap in it is one a Nuxt 4 app actually hit:
812
- * a Vue codebase classified as `html` and handed `@reticlehq/react`, a connect guarded on
813
- * `window.location.hostname === 'localhost'` (false on any hosts-file alias, and `window` does not
814
- * exist during SSR), and a running dev server that never picked up the new plugin.
815
- *
816
- * So: `import.meta.dev` instead of a hostname check, `.client.ts` instead of an SSR guard, the
817
- * framework-neutral sensor instead of the React kit, the non-localhost flag named up front, and the
818
- * restart said out loud.
819
- */
820
- export function nuxtManual(port, projectId) {
821
- const base = connectArg(port, projectId);
822
- const fields = '' === base ? '' : base.slice(1, -1).trim();
823
- const connect = '' === fields ? 'reticle.connect()' : `reticle.connect({ ${fields} })`;
824
- return `Nuxt owns its own Vite instance and renders its own HTML, so there is no vite.config to patch
825
- and no index.html to inject into. Wire it with a dev-only CLIENT plugin, which is Nuxt's own idiom:
826
-
827
- 1. Create ${NUXT_PLUGIN_PATH}:
828
-
829
- export default defineNuxtPlugin(() => {
830
- // import.meta.dev is the correct guard: it is resolved at build time, so it does not care
831
- // what hostname you develop on. Do NOT guard on window.location.hostname === 'localhost' —
832
- // that is false on any hosts-file alias or LAN address, and window does not exist in SSR.
833
- if (!import.meta.dev) return
834
- void import('@reticlehq/browser').then(({ reticle }) => {
835
- ${connect}
836
- })
837
- })
838
-
839
- The .client.ts suffix is load-bearing: it is what keeps this out of the server bundle.
840
-
841
- 2. Restart the dev server. A dev server that is already running does not pick up a new plugin —
842
- it will not appear in .nuxt/plugins/client.mjs, and the app will come up with no SDK at all.
843
-
844
- 3. If your dev host is anything other than localhost (a hosts-file alias, a LAN IP, a tunnel), that
845
- connect call needs TWO additions, not one: allowNonLocalhost: true AND a pairing token passed as
846
- token. The flag alone is NOT sufficient off localhost. The token is the one in
847
- ~/.reticle/pairing-token. Without both, the SDK loads and then refuses, and the only sign is one
848
- line in the browser console.
849
-
850
- 4. Add this to nuxt.config, so the dev server does not watch Reticle's own journal:
851
-
852
- vite: { server: { watch: { ignored: [/(^|[\\\\/])\\.${ReticleDir.ROOT.slice(1)}([\\\\/]|$)/] } } }
853
-
854
- Reticle journals every session into ${ReticleDir.ROOT}/ in your project root, rewriting one file
855
- in it continuously while a session is live. Nuxt's dev server watches that root, so without this
856
- it sees each write as a project file changing and full-reloads the page — which reconnects the
857
- SDK, which produces the next write. The loop runs several times a second and looks like anything
858
- except what it is: refs go stale, actions die mid-flight, and the session appears to flap.
859
- It is a RegExp rather than a glob on purpose — chokidar dropped glob support in v4, so a
860
- double-star pattern here is accepted and matches nothing.
861
-
862
- The package is @reticlehq/browser — the framework-neutral sensor. DOM, network, console, routing and
863
- source file:line all work in Vue. What you do not get is React component identity, which is the only
864
- thing the React adapter adds. There is no Nuxt app in this project's CI, so this path is UNVERIFIED:
865
- if something does not work, please open an issue.`;
866
- }
756
+ export { nuxtPluginPath, nuxtPluginFile, NUXT_PLUGIN_NOTICE, nuxtManual } from './nuxt-snippets.js';
867
757
  /**
868
758
  * Root-level project config for Reticle. Written by `reticle init`; read by `reticle mcp` for the port
869
759
  * and by tooling for the stable projectId (the app's identity across port changes).
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The generated dev module must compile even when the host excludes `vite/client` ambient types.
3
+ * Keep the guard itself as `import.meta.env.DEV` so Vite can remove dev-only code from production.
4
+ *
5
+ * These declarations must merge with Vite's existing interfaces: DEV is mutable there, while env
6
+ * is readonly and refers to the named ImportMetaEnv interface. A triple-slash package reference
7
+ * would also require vite to be directly resolvable, which electron-vite-only projects need not have.
8
+ */
9
+ export declare const VITE_ENV_DEV_DECLARATION = "// Supply the dev flag type when the host does not include vite/client.\n// These interfaces merge with vite/client when it is already present.\ndeclare global {\n interface ImportMetaEnv {\n DEV: boolean;\n }\n interface ImportMeta {\n readonly env: ImportMetaEnv;\n }\n}\n";
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The generated dev module must compile even when the host excludes `vite/client` ambient types.
3
+ * Keep the guard itself as `import.meta.env.DEV` so Vite can remove dev-only code from production.
4
+ *
5
+ * These declarations must merge with Vite's existing interfaces: DEV is mutable there, while env
6
+ * is readonly and refers to the named ImportMetaEnv interface. A triple-slash package reference
7
+ * would also require vite to be directly resolvable, which electron-vite-only projects need not have.
8
+ */
9
+ export const VITE_ENV_DEV_DECLARATION = `// Supply the dev flag type when the host does not include vite/client.
10
+ // These interfaces merge with vite/client when it is already present.
11
+ declare global {
12
+ interface ImportMetaEnv {
13
+ DEV: boolean;
14
+ }
15
+ interface ImportMeta {
16
+ readonly env: ImportMetaEnv;
17
+ }
18
+ }
19
+ `;
@@ -21,5 +21,18 @@ export interface ViteOwningConfig {
21
21
  readonly defineCall: RegExp;
22
22
  /** The SDK package this framework's connect imports, declared so Vite pre-bundles it. */
23
23
  readonly sdkSpecifier: string;
24
+ /**
25
+ * When true (default), inline the pairing token and project root via `vite.define`.
26
+ *
27
+ * Nuxt still needs this: its client plugin reads `__RETICLE_TOKEN__` from the define. Astro 7.2+
28
+ * no longer substitutes `define` into the client pipeline (#1008), so Astro sets this false and
29
+ * puts the token on a frontmatter `<meta>` instead.
30
+ */
31
+ readonly defineToken?: boolean;
32
+ /**
33
+ * Present once this patch has applied. Defaults to `__RETICLE_TOKEN__`, which is what the define
34
+ * path writes. Astro's path never writes that name into the config, so it supplies its own.
35
+ */
36
+ readonly alreadyMarker?: string;
24
37
  }
25
38
  export declare function patchViteOwningConfig(source: string, config: ViteOwningConfig): SourcePatch;
@@ -18,7 +18,13 @@ import { ReticleDir } from '@reticlehq/core';
18
18
  import { blockAfter } from './brace-scan.js';
19
19
  import { PatchKind } from './patch-kind.js';
20
20
  /** Present in a patched config AND in a hand-followed recipe — so both count as already wired. */
21
- const CONFIG_MARKER = '__RETICLE_TOKEN__';
21
+ const DEFINE_TOKEN_MARKER = '__RETICLE_TOKEN__';
22
+ function usesDefineToken(config) {
23
+ return config.defineToken !== false;
24
+ }
25
+ function alreadyMarker(config) {
26
+ return config.alreadyMarker ?? DEFINE_TOKEN_MARKER;
27
+ }
22
28
  /**
23
29
  * The SDK, declared so Vite pre-bundles it BEFORE the first page load.
24
30
  *
@@ -82,30 +88,36 @@ import { join } from 'node:path';
82
88
  * cosmetic: Astro's default down-levels the modern SDK bundle and dies on a destructuring transform.
83
89
  */
84
90
  function viteKeys(config) {
85
- return [
91
+ const keys = [
86
92
  { key: 'build', inner: `\n target: 'es2022',` },
87
93
  {
88
94
  key: 'optimizeDeps',
89
95
  inner: `\n include: [${sdkInclude(config)}],\n esbuildOptions: { target: 'es2022' },`,
90
96
  },
91
- {
92
- key: 'define',
93
- inner: `\n __RETICLE_TOKEN__: JSON.stringify(reticleToken()),\n __RETICLE_ROOT__: JSON.stringify(process.cwd()),`,
94
- },
95
97
  // Merged in first, so a `server: { port }` the app already set keeps its port. An app that
96
98
  // already sets `server.watch` itself is the one shape this loses to — the inner `watch` key would
97
99
  // be duplicated and the app's would win — which is the same nested-merge ceiling `build` has.
98
100
  { key: 'server', inner: `\n watch: { ignored: [${WATCH_IGNORE_LITERAL}] },` },
99
101
  ];
102
+ if (usesDefineToken(config)) {
103
+ keys.splice(2, 0, {
104
+ key: 'define',
105
+ inner: `\n __RETICLE_TOKEN__: JSON.stringify(reticleToken()),\n __RETICLE_ROOT__: JSON.stringify(process.cwd()),`,
106
+ });
107
+ }
108
+ return keys;
100
109
  }
101
110
  /** Whole-key form, for a `vite:` block that does not have the key at all. */
102
111
  function wholeKeys(config) {
103
- return {
112
+ const keys = {
104
113
  build: `\n build: { target: 'es2022' },`,
105
114
  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 },`,
107
115
  server: `\n server: { watch: { ignored: [${WATCH_IGNORE_LITERAL}] } },`,
108
116
  };
117
+ if (usesDefineToken(config)) {
118
+ keys.define = `\n define: {\n __RETICLE_TOKEN__: JSON.stringify(reticleToken()),\n __RETICLE_ROOT__: JSON.stringify(process.cwd()),\n },`;
119
+ }
120
+ return keys;
109
121
  }
110
122
  /**
111
123
  * Add our keys to an existing `vite: { ... }` block, merging into any the app already set.
@@ -160,19 +172,34 @@ function withoutInclude(inner, config) {
160
172
  * that ran `init`.
161
173
  */
162
174
  function viteBlock(config) {
163
- return ` vite: {
164
- build: { target: 'es2022' },
165
- optimizeDeps: { include: [${sdkInclude(config)}], esbuildOptions: { target: 'es2022' } },
166
- define: {
175
+ const marker = usesDefineToken(config) ? '' : ` /* ${alreadyMarker(config)} */\n`;
176
+ const define = usesDefineToken(config)
177
+ ? ` define: {
167
178
  __RETICLE_TOKEN__: JSON.stringify(reticleToken()),
168
179
  __RETICLE_ROOT__: JSON.stringify(process.cwd()),
169
180
  },
170
- server: { watch: { ignored: [${WATCH_IGNORE_LITERAL}] } },
181
+ `
182
+ : '';
183
+ return ` vite: {
184
+ ${marker} build: { target: 'es2022' },
185
+ optimizeDeps: { include: [${sdkInclude(config)}], esbuildOptions: { target: 'es2022' } },
186
+ ${define} server: { watch: { ignored: [${WATCH_IGNORE_LITERAL}] } },
171
187
  },
172
188
  `;
173
189
  }
190
+ function withHelper(source, config) {
191
+ if (!usesDefineToken(config)) {
192
+ return source.endsWith('\n') ? source : `${source}\n`;
193
+ }
194
+ return `${HELPER_IMPORTS}${source.trimStart()}\n${HELPER}`.trimEnd() + '\n';
195
+ }
196
+ function withOwningMarker(source, config, braceAt) {
197
+ if (usesDefineToken(config) || source.includes(alreadyMarker(config)))
198
+ return source;
199
+ return insertAt(source, braceAt, `\n /* ${alreadyMarker(config)} */`);
200
+ }
174
201
  export function patchViteOwningConfig(source, config) {
175
- if (source.includes(CONFIG_MARKER))
202
+ if (source.includes(alreadyMarker(config)))
176
203
  return { kind: PatchKind.ALREADY };
177
204
  // A `vite: { ... }` block is an object literal, so our keys can go straight after the brace and
178
205
  // whatever is already in there is untouched. Refusing outright was the only genuine install defect
@@ -186,7 +213,7 @@ export function patchViteOwningConfig(source, config) {
186
213
  if (merged !== null) {
187
214
  return {
188
215
  kind: PatchKind.APPLY,
189
- code: `${HELPER_IMPORTS}${merged.trimStart()}\n${HELPER}`.trimEnd() + '\n',
216
+ code: withHelper(withOwningMarker(merged, config, at), config),
190
217
  };
191
218
  }
192
219
  return {
@@ -214,6 +241,6 @@ export function patchViteOwningConfig(source, config) {
214
241
  const withBlock = `${source.slice(0, at)}\n${viteBlock(config)}${source.slice(at)}`;
215
242
  return {
216
243
  kind: PatchKind.APPLY,
217
- code: `${HELPER_IMPORTS}${withBlock.trimStart()}\n${HELPER}`.trimEnd() + '\n',
244
+ code: withHelper(withBlock, config),
218
245
  };
219
246
  }
@@ -34,8 +34,9 @@ export declare const StepTitle: {
34
34
  readonly SVELTEKIT_UNVERIFIED: "SvelteKit is UNVERIFIED";
35
35
  readonly CLIENT_HOOK: "Reticle client hook";
36
36
  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)";
37
+ readonly ASTRO_RETICLE_DEV: "Astro ReticleDev module";
38
+ readonly ASTRO_CONFIG: "Astro config (build target)";
39
+ readonly ASTRO_ENV_DTS: "Astro env types (window token)";
39
40
  readonly CSP: "Content-Security-Policy";
40
41
  readonly CONNECT_SNIPPET: "Connect snippet";
41
42
  };
@@ -35,8 +35,9 @@ export const StepTitle = {
35
35
  SVELTEKIT_UNVERIFIED: 'SvelteKit is UNVERIFIED',
36
36
  CLIENT_HOOK: 'Reticle client hook',
37
37
  CONNECT_SNIPPET_ASTRO: 'Connect snippet (Astro)',
38
- ASTRO_CONFIG: 'Astro config (token + build target)',
39
- ASTRO_ENV_DTS: 'Astro env types (Vite defines)',
38
+ ASTRO_RETICLE_DEV: 'Astro ReticleDev module',
39
+ ASTRO_CONFIG: 'Astro config (build target)',
40
+ ASTRO_ENV_DTS: 'Astro env types (window token)',
40
41
  CSP: CSP_STEP_TITLE,
41
42
  CONNECT_SNIPPET: 'Connect snippet',
42
43
  };
@@ -54,6 +55,7 @@ export const CONNECT_STEP_TITLES = new Set([
54
55
  StepTitle.CONNECT_SNIPPET,
55
56
  StepTitle.CONNECT_SNIPPET_CRA,
56
57
  StepTitle.CONNECT_SNIPPET_ASTRO,
58
+ StepTitle.ASTRO_RETICLE_DEV,
57
59
  StepTitle.CONNECT_SNIPPET_NUXT,
58
60
  // The config is the other half of the Nuxt connect, not a nicety: it is the only thing in a Nuxt
59
61
  // app that can inline the pairing token, and the bridge refuses a connect without one even on
@@ -24,11 +24,7 @@ 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
@@ -67,3 +63,4 @@ export declare const FRAMEWORK_ADAPTERS: Record<Framework, FrameworkAdapter>;
67
63
  * for the packages, the connect-step titles and the unverified-note flag in the same edit.
68
64
  */
69
65
  export declare function frameworkSteps(input: PlanInput): Step[];
66
+ export {};
@@ -29,10 +29,10 @@ import { electronViteSteps } from './plan-electron-vite.js';
29
29
  export const RETICLE_REACT_KIT = '@reticlehq/react';
30
30
  /** The framework-neutral sensor, for stacks the React adapter has nothing to attach to. */
31
31
  export const RETICLE_BROWSER_SDK = '@reticlehq/browser';
32
- export const RETICLE_VITE_PLUGIN = '@reticlehq/vite-plugin';
33
- export const RETICLE_NEXT_PLUGIN = '@reticlehq/next';
32
+ const RETICLE_VITE_PLUGIN = '@reticlehq/vite-plugin';
33
+ const RETICLE_NEXT_PLUGIN = '@reticlehq/next';
34
34
  /** The Electron main/preload helper — what makes IPC and screenshots exist at all. */
35
- export const RETICLE_ELECTRON = '@reticlehq/electron';
35
+ const RETICLE_ELECTRON = '@reticlehq/electron';
36
36
  export const FRAMEWORK_ADAPTERS = {
37
37
  [Framework.NEXT]: {
38
38
  // Next is React by construction, so the detection cannot disagree in a way worth honouring.
@@ -92,10 +92,11 @@ export const FRAMEWORK_ADAPTERS = {
92
92
  },
93
93
  [Framework.ASTRO]: {
94
94
  // 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).
95
+ // plugin to attach to — the kit alone, connected from a local module the page imports
96
+ // (see astroManual).
96
97
  packages: (kit) => [kit],
97
98
  steps: astroSteps,
98
- connectStepTitles: [StepTitle.CONNECT_SNIPPET_ASTRO],
99
+ connectStepTitles: [StepTitle.CONNECT_SNIPPET_ASTRO, StepTitle.ASTRO_RETICLE_DEV],
99
100
  carriesOwnUnverifiedNote: false,
100
101
  },
101
102
  [Framework.ELECTRON_VITE]: {
@@ -85,7 +85,7 @@ export declare function reactRouterSteps(input: PlanInput): Step[];
85
85
  export declare function tanstackStartSteps(input: PlanInput): Step[];
86
86
  export declare function svelteKitSteps(input: PlanInput): Step[];
87
87
  /**
88
- * Astro: the config define + build target, and the connect script in ONE layout.
88
+ * Astro: the config build target, and the connect script (with the pairing token) in ONE layout.
89
89
  *
90
90
  * Astro was the last gated stack left printing a recipe it did not apply. It still falls back to the
91
91
  * printed one whenever the choice is not obvious — no config, no single layout, or a shape the