@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
package/dist/node-io.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * `runInit` stays testable with an in-memory IO. Prints to stdout — `init` is a one-shot CLI
4
4
  * command, not the MCP stdio transport.
5
5
  */
6
+ import { terminalWidth, wrapForTerminal } from './diagnose/terminal-wrap.js';
6
7
  import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, accessSync, constants, } from 'node:fs';
7
8
  import { NodePlatform } from './detect/platform.js';
8
9
  import { join, dirname, isAbsolute } from 'node:path';
@@ -23,17 +24,29 @@ function shellSafe(args) {
23
24
  /**
24
25
  * Every program `init` is ever allowed to run.
25
26
  *
26
- * The set is closed and tiny — the four package managers, `npx`, and the Claude CLI — because those
27
- * are the only things the plan can ask for. It is a named constant for the same reason every other
28
- * wire string here is one, and it is CHECKED because of the shell: on Windows these spawn through
29
- * a shell, and a shell turns the command name into something the shell parses rather than a file it
30
- * executes. Arguments are quoted by `shellSafe`; the command name was the half nothing covered.
27
+ * The set is closed and tiny — the four package managers, `npx`, `corepack`, and the Claude CLI —
28
+ * because those are the only things the plan can ask for. It is a named constant for the same reason
29
+ * every other wire string here is one, and it is CHECKED because of the shell: on Windows these spawn
30
+ * through a shell, and a shell turns the command name into something the shell parses rather than a
31
+ * file it executes. Arguments are quoted by `shellSafe`; the command name was the half nothing
32
+ * covered.
33
+ *
34
+ * `corepack` runs the package manager, never IS it (see `preflight.ts`, #1149) — the allowlist has to
35
+ * name the actual program being spawned.
31
36
  *
32
37
  * A command outside this set is a programming error — the plan built something no branch of this
33
38
  * package can produce — so it throws rather than returning `false`, which would read to the caller
34
39
  * as "the command ran and failed" and hide the bug in a retry.
35
40
  */
36
- export const RUNNABLE_COMMANDS = ['pnpm', 'yarn', 'bun', 'npm', 'npx', 'claude'];
41
+ export const RUNNABLE_COMMANDS = [
42
+ 'pnpm',
43
+ 'yarn',
44
+ 'bun',
45
+ 'npm',
46
+ 'npx',
47
+ 'corepack',
48
+ 'claude',
49
+ ];
37
50
  /** The command, proven to be one of `RUNNABLE_COMMANDS` — never the caller's string. */
38
51
  function runnable(command) {
39
52
  const found = RUNNABLE_COMMANDS.find((allowed) => allowed === command);
@@ -155,6 +168,11 @@ export function buildNodeIo(cwd, host) {
155
168
  const result = spawnAllowed(command, args, { cwd, stdio: 'inherit' });
156
169
  return 0 === result.status;
157
170
  },
171
+ capture(command, args) {
172
+ const result = spawnAllowed(command, args, { cwd, stdio: 'pipe', encoding: 'utf8' });
173
+ const text = (part) => ('string' === typeof part ? part : '');
174
+ return { ok: 0 === result.status, output: `${text(result.stdout)}${text(result.stderr)}` };
175
+ },
158
176
  /** One access check, rather than discovering it as an EACCES stack four phases later. */
159
177
  canWrite() {
160
178
  try {
@@ -171,7 +189,7 @@ export function buildNodeIo(cwd, host) {
171
189
  return 0 === result.status;
172
190
  },
173
191
  print(line) {
174
- process.stdout.write(`${line}\n`);
192
+ process.stdout.write(`${wrapForTerminal(line, terminalWidth())}\n`);
175
193
  },
176
194
  host,
177
195
  };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Angular CLI: a dev-only connect in the browser entry, and a way for the pairing token to reach it.
3
+ *
4
+ * The connect is the part proven by hand on Angular 22, with and without SSR: a dynamic
5
+ * `import('@reticlehq/browser')` guarded on `ngDevMode` (see DEV_GUARD for why not `isDevMode()`),
6
+ * which the production build folds to `false` and drops together with the chunk it would load.
7
+ *
8
+ * The token is the hard part, and why this is not one line. The bridge refuses a hello without the
9
+ * machine's pairing token even on localhost, and every other stack inlines it from Node at build
10
+ * time — the Vite plugin's `define`, Next's `NEXT_PUBLIC_*`, CRA's gitignored `.env.development.local`.
11
+ * The Angular CLI has no config file that runs code, so each of those has a failure here, all
12
+ * measured on a scaffolded Angular 21 app before this was written:
13
+ *
14
+ * - A literal in `angular.json` (`define`, `fileReplacements`) commits a per-machine secret.
15
+ * - A gitignored module imported from `main.ts` is bundled by `ng build` too: with the file present
16
+ * the production output held a lazy chunk carrying the token behind `isDevMode()`, which the
17
+ * build cannot fold — and a token must not depend on a guard being written exactly right.
18
+ * - Making that module a dev-only `fileReplacements` target breaks `ng serve` for every teammate
19
+ * who has not run `init` ("path in file replacements does not exist").
20
+ *
21
+ * What is left is the serve target, which `ng build` never reads. Its `proxyConfig` is a JavaScript
22
+ * module that `ng serve` loads in Node, so `reticle.proxy.mjs` reads `~/.reticle/pairing-token` at
23
+ * request time — the same file the Vite and Next plugins read — and answers one same-origin route
24
+ * with it. The token is never written into the project, and a production build has no route to it.
25
+ */
26
+ import { type SourcePatch } from './patch-kind.js';
27
+ /** The generated dev-server module, beside `angular.json`. */
28
+ export declare const ANGULAR_PROXY_PATH = "reticle.proxy.mjs";
29
+ /** The one route that module answers, fetched by the entry's dev-only connect. */
30
+ export declare const ANGULAR_TOKEN_PATH = "/__reticle/pairing-token";
31
+ /** What an Angular CLI app's browser entry is when `angular.json` does not say. */
32
+ export declare const ANGULAR_DEFAULT_ENTRY = "src/main.ts";
33
+ /**
34
+ * The browser entry `angular.json` names — `browser` on the application builder (v17+), `main` on
35
+ * the webpack one — or the CLI's default when it cannot be read.
36
+ */
37
+ export declare function angularEntry(source: string | null): string;
38
+ /**
39
+ * Point the one app's serve target at `reticle.proxy.mjs`.
40
+ *
41
+ * An existing `proxyConfig` is the app's, and is never replaced: the step goes MANUAL naming the
42
+ * entry to add to it. The file is rewritten through `JSON.stringify` with the CLI's own two-space
43
+ * indent, which is how `ng new` writes it, so the diff is the one key.
44
+ */
45
+ export declare function patchAngularJson(source: string): SourcePatch;
46
+ /**
47
+ * The dev-server module. Plain ESM, no dependencies, because it runs in the app's `ng serve`.
48
+ *
49
+ * Two refusals, both about who may read a token: a request that did not come over loopback (an
50
+ * `ng serve --host 0.0.0.0` on a shared network), and a browser request from another origin — a
51
+ * different localhost app — which `Sec-Fetch-Site` names on every current browser.
52
+ *
53
+ * `bypass` answers the request itself and returns `false`, so the dummy `target` is never dialled.
54
+ * ponytail: relies on the Vite-based dev server (Angular 17+) ignoring the 404 it sets after the
55
+ * response has already ended; the older webpack dev server calls `next()` instead, and if it ever
56
+ * matters there the upgrade is a `configure` hook instead of `bypass`.
57
+ */
58
+ export declare function angularProxyFile(): string;
59
+ /**
60
+ * Add the dev-only connect to the browser entry, or ALREADY when it is there.
61
+ *
62
+ * Appended after the bootstrap rather than before it: the app must never wait on Reticle, and a
63
+ * connect that fails (no daemon yet) must not be able to stop Angular from starting.
64
+ *
65
+ * An entry wired by the previous version (`if (isDevMode())`) is rewritten in place, so a re-run is
66
+ * what takes the connect out of an existing app's production bundle.
67
+ */
68
+ export declare function patchAngularMain(source: string, port: number | undefined, projectId: string | undefined): SourcePatch;
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Angular CLI: a dev-only connect in the browser entry, and a way for the pairing token to reach it.
3
+ *
4
+ * The connect is the part proven by hand on Angular 22, with and without SSR: a dynamic
5
+ * `import('@reticlehq/browser')` guarded on `ngDevMode` (see DEV_GUARD for why not `isDevMode()`),
6
+ * which the production build folds to `false` and drops together with the chunk it would load.
7
+ *
8
+ * The token is the hard part, and why this is not one line. The bridge refuses a hello without the
9
+ * machine's pairing token even on localhost, and every other stack inlines it from Node at build
10
+ * time — the Vite plugin's `define`, Next's `NEXT_PUBLIC_*`, CRA's gitignored `.env.development.local`.
11
+ * The Angular CLI has no config file that runs code, so each of those has a failure here, all
12
+ * measured on a scaffolded Angular 21 app before this was written:
13
+ *
14
+ * - A literal in `angular.json` (`define`, `fileReplacements`) commits a per-machine secret.
15
+ * - A gitignored module imported from `main.ts` is bundled by `ng build` too: with the file present
16
+ * the production output held a lazy chunk carrying the token behind `isDevMode()`, which the
17
+ * build cannot fold — and a token must not depend on a guard being written exactly right.
18
+ * - Making that module a dev-only `fileReplacements` target breaks `ng serve` for every teammate
19
+ * who has not run `init` ("path in file replacements does not exist").
20
+ *
21
+ * What is left is the serve target, which `ng build` never reads. Its `proxyConfig` is a JavaScript
22
+ * module that `ng serve` loads in Node, so `reticle.proxy.mjs` reads `~/.reticle/pairing-token` at
23
+ * request time — the same file the Vite and Next plugins read — and answers one same-origin route
24
+ * with it. The token is never written into the project, and a production build has no route to it.
25
+ */
26
+ import { connectArg } from './snippets.js';
27
+ import { PatchKind } from './patch-kind.js';
28
+ /** The generated dev-server module, beside `angular.json`. */
29
+ export const ANGULAR_PROXY_PATH = 'reticle.proxy.mjs';
30
+ /** The one route that module answers, fetched by the entry's dev-only connect. */
31
+ export const ANGULAR_TOKEN_PATH = '/__reticle/pairing-token';
32
+ /** What an Angular CLI app's browser entry is when `angular.json` does not say. */
33
+ export const ANGULAR_DEFAULT_ENTRY = 'src/main.ts';
34
+ const SENSOR = '@reticlehq/browser';
35
+ const DEV_SERVER_BUILDER = /:dev-server$/;
36
+ const UNPARSEABLE_REASON = 'angular.json is not plain JSON, so it is not safe to rewrite by hand';
37
+ const NO_SERVE_TARGET_REASON = 'no project in angular.json has a `serve` target on a dev-server builder, so there is nowhere ' +
38
+ 'for `ng serve` to pick up the token route';
39
+ const SEVERAL_APPS_REASON = 'angular.json has several projects with a `serve` target, and which one is the app is not ours ' +
40
+ 'to guess';
41
+ function parseWorkspace(source) {
42
+ try {
43
+ const parsed = JSON.parse(source);
44
+ if ('object' !== typeof parsed || null === parsed)
45
+ return undefined;
46
+ const root = parsed;
47
+ const projects = root['projects'];
48
+ if ('object' !== typeof projects || null === projects)
49
+ return undefined;
50
+ return { root, projects: projects };
51
+ }
52
+ catch {
53
+ return undefined;
54
+ }
55
+ }
56
+ function targetsOf(project) {
57
+ return project.architect ?? project.targets ?? {};
58
+ }
59
+ /** The projects `ng serve` can serve — the only ones with a page to connect. */
60
+ function servedProjects(projects) {
61
+ return Object.values(projects).filter((p) => DEV_SERVER_BUILDER.test(targetsOf(p)['serve']?.builder ?? ''));
62
+ }
63
+ /**
64
+ * The browser entry `angular.json` names — `browser` on the application builder (v17+), `main` on
65
+ * the webpack one — or the CLI's default when it cannot be read.
66
+ */
67
+ export function angularEntry(source) {
68
+ const workspace = null === source ? undefined : parseWorkspace(source);
69
+ const [only, ...rest] = workspace === undefined ? [] : servedProjects(workspace.projects);
70
+ if (only === undefined || rest.length > 0)
71
+ return ANGULAR_DEFAULT_ENTRY;
72
+ const options = targetsOf(only)['build']?.options ?? {};
73
+ const named = options['browser'] ?? options['main'];
74
+ return 'string' === typeof named && named.length > 0 ? named : ANGULAR_DEFAULT_ENTRY;
75
+ }
76
+ /**
77
+ * Point the one app's serve target at `reticle.proxy.mjs`.
78
+ *
79
+ * An existing `proxyConfig` is the app's, and is never replaced: the step goes MANUAL naming the
80
+ * entry to add to it. The file is rewritten through `JSON.stringify` with the CLI's own two-space
81
+ * indent, which is how `ng new` writes it, so the diff is the one key.
82
+ */
83
+ export function patchAngularJson(source) {
84
+ const workspace = parseWorkspace(source);
85
+ if (workspace === undefined)
86
+ return { kind: PatchKind.MANUAL, reason: UNPARSEABLE_REASON };
87
+ const served = servedProjects(workspace.projects);
88
+ const [only, ...rest] = served;
89
+ if (only === undefined)
90
+ return { kind: PatchKind.MANUAL, reason: NO_SERVE_TARGET_REASON };
91
+ if (rest.length > 0)
92
+ return { kind: PatchKind.MANUAL, reason: SEVERAL_APPS_REASON };
93
+ const serve = targetsOf(only)['serve'];
94
+ if (serve === undefined)
95
+ return { kind: PatchKind.MANUAL, reason: NO_SERVE_TARGET_REASON };
96
+ const existing = serve.options?.['proxyConfig'];
97
+ if (ANGULAR_PROXY_PATH === existing)
98
+ return { kind: PatchKind.ALREADY };
99
+ if (existing !== undefined) {
100
+ return {
101
+ kind: PatchKind.MANUAL,
102
+ reason: `the serve target already uses ${JSON.stringify(existing)} as its proxyConfig, and it is yours — ` +
103
+ `add the \`${ANGULAR_TOKEN_PATH}\` entry from ${ANGULAR_PROXY_PATH} to it`,
104
+ };
105
+ }
106
+ serve.options = { ...serve.options, proxyConfig: ANGULAR_PROXY_PATH };
107
+ return { kind: PatchKind.APPLY, code: `${JSON.stringify(workspace.root, null, 2)}\n` };
108
+ }
109
+ /**
110
+ * The dev-server module. Plain ESM, no dependencies, because it runs in the app's `ng serve`.
111
+ *
112
+ * Two refusals, both about who may read a token: a request that did not come over loopback (an
113
+ * `ng serve --host 0.0.0.0` on a shared network), and a browser request from another origin — a
114
+ * different localhost app — which `Sec-Fetch-Site` names on every current browser.
115
+ *
116
+ * `bypass` answers the request itself and returns `false`, so the dummy `target` is never dialled.
117
+ * ponytail: relies on the Vite-based dev server (Angular 17+) ignoring the 404 it sets after the
118
+ * response has already ended; the older webpack dev server calls `next()` instead, and if it ever
119
+ * matters there the upgrade is a `configure` hook instead of `bypass`.
120
+ */
121
+ export function angularProxyFile() {
122
+ return `// Written by \`reticle init\`. Read by \`ng serve\` only — \`ng build\` never loads a proxy config.
123
+ //
124
+ // Hands the page this machine's Reticle pairing token, read at request time from the file the
125
+ // Reticle daemon keeps (~/.reticle/pairing-token). The token is per-machine and never written into
126
+ // this project, so nothing here needs to be kept out of version control.
127
+ import { readFileSync } from 'node:fs';
128
+ import { homedir } from 'node:os';
129
+ import { join } from 'node:path';
130
+
131
+ const LOOPBACK = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
132
+
133
+ function pairingToken() {
134
+ const dir = process.env.RETICLE_PAIRING_TOKEN_DIR || join(homedir(), '.reticle');
135
+ try {
136
+ return readFileSync(join(dir, 'pairing-token'), 'utf8').trim();
137
+ } catch {
138
+ return '';
139
+ }
140
+ }
141
+
142
+ export default {
143
+ '${ANGULAR_TOKEN_PATH}': {
144
+ target: 'http://127.0.0.1:9',
145
+ bypass(req, res) {
146
+ const site = req.headers['sec-fetch-site'];
147
+ const allowed =
148
+ LOOPBACK.has(req.socket.remoteAddress ?? '') && (site === undefined || site === 'same-origin');
149
+ res.writeHead(allowed ? 200 : 403, { 'content-type': 'text/plain', 'cache-control': 'no-store' });
150
+ res.end(allowed ? pairingToken() : '');
151
+ return false;
152
+ },
153
+ },
154
+ };
155
+ `;
156
+ }
157
+ /** Everything `connect()` is called with, as the literal's inner fields — `url` only off 4400. */
158
+ function connectFields(port, projectId) {
159
+ const literal = connectArg(port, projectId);
160
+ return literal.length > 0 ? `${literal.slice(1, -1).trim()}, ` : '';
161
+ }
162
+ /**
163
+ * The guard the connect sits under: Angular's own `ngDevMode` global, not `isDevMode()`.
164
+ *
165
+ * `isDevMode()` is a runtime call, so `ng build` cannot fold it: the production bundle shipped the
166
+ * whole connect — the bridge URL included — plus ~300KB of lazy Reticle chunks that nothing
167
+ * would ever load. The Angular CLI defines `ngDevMode` to `false` in a production build, which lets
168
+ * esbuild drop the branch and, with it, the dynamic import; in dev it is undefined or an object, so
169
+ * this reads exactly as `isDevMode()` did there. It is the check Angular's own sources use.
170
+ */
171
+ const DEV_GUARD = "if (typeof ngDevMode === 'undefined' || ngDevMode) {";
172
+ /** Declared locally so strict TypeScript accepts the global without the app importing anything. */
173
+ const NG_DEV_MODE_DECLARATION = 'declare const ngDevMode: unknown;';
174
+ /** The guard the previous version wrote, which is rewritten in place on a re-run. */
175
+ const LEGACY_GUARD = /^if \(isDevMode\(\)\) \{(?=\s*void Promise\.all\(\[\s*import\('@reticlehq\/browser'\))/m;
176
+ /** The comment the previous version wrote above it, which named the guard it no longer uses. */
177
+ const LEGACY_COMMENT = /^\/\/ Reticle, dev only — written by `reticle init`\. `isDevMode\(\)` is false in a production build,\n\/\/ so this never runs there\./m;
178
+ const HEADER = '// Reticle, dev only — written by `reticle init`. `ngDevMode` is false in a production build,\n' +
179
+ '// so the build drops this block.';
180
+ /**
181
+ * Add the dev-only connect to the browser entry, or ALREADY when it is there.
182
+ *
183
+ * Appended after the bootstrap rather than before it: the app must never wait on Reticle, and a
184
+ * connect that fails (no daemon yet) must not be able to stop Angular from starting.
185
+ *
186
+ * An entry wired by the previous version (`if (isDevMode())`) is rewritten in place, so a re-run is
187
+ * what takes the connect out of an existing app's production bundle.
188
+ */
189
+ export function patchAngularMain(source, port, projectId) {
190
+ if (LEGACY_GUARD.test(source)) {
191
+ const guarded = source
192
+ .replace(LEGACY_COMMENT, HEADER)
193
+ .replace(LEGACY_GUARD, `${NG_DEV_MODE_DECLARATION}\n${DEV_GUARD}`);
194
+ return { kind: PatchKind.APPLY, code: guarded };
195
+ }
196
+ if (source.includes(SENSOR))
197
+ return { kind: PatchKind.ALREADY };
198
+ const declaration = source.includes(NG_DEV_MODE_DECLARATION)
199
+ ? ''
200
+ : `${NG_DEV_MODE_DECLARATION}\n`;
201
+ const block = `
202
+ ${HEADER} The pairing token comes from \`ng serve\` (${ANGULAR_PROXY_PATH}), so it
203
+ // is never in a source file or a bundle.
204
+ ${declaration}${DEV_GUARD}
205
+ void Promise.all([
206
+ import('${SENSOR}'),
207
+ fetch('${ANGULAR_TOKEN_PATH}')
208
+ .then((res) => (res.ok ? res.text() : ''))
209
+ .catch(() => ''),
210
+ ]).then(([{ reticle }, token]) => {
211
+ reticle.connect({ ${connectFields(port, projectId)}...(token ? { token } : {}) });
212
+ });
213
+ }
214
+ `;
215
+ return { kind: PatchKind.APPLY, code: `${source.trimEnd()}\n${block}` };
216
+ }
@@ -1,33 +1,68 @@
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
  */
14
21
  import { type SourcePatch } from './patch-kind.js';
15
22
  import { UiLibrary } from '../detect/detect.js';
16
- export declare function patchAstroConfig(source: string): SourcePatch;
17
- export declare function patchAstroLayout(source: string, port: number | undefined, projectId: string | undefined, uiLibrary?: UiLibrary, testids?: readonly string[]): SourcePatch;
23
+ /**
24
+ * Local module `init` writes for Astro. A processed page `<script>` statically imports this file
25
+ * so Vite owns the SDK the same way it owns any other project module.
26
+ *
27
+ * `.ts`, not `.tsx`: the install-gate scaffold is `create-astro --template minimal`, which has no
28
+ * React integration. The example app uses the same static import in a processed page `<script>`.
29
+ */
30
+ export declare const ASTRO_RETICLE_DEV_PATH = "src/components/ReticleDev.ts";
31
+ /**
32
+ * Dev-only Astro connect module. Static SDK import, token from frontmatter `<meta>` tags.
33
+ */
34
+ export declare function astroReticleDevFile(port: number | undefined, projectId: string | undefined, uiLibrary?: UiLibrary, testids?: readonly string[]): string;
35
+ /**
36
+ * Written into the Astro `vite:` block so a second init reports ALREADY. The Nuxt path uses
37
+ * `__RETICLE_TOKEN__` in `define` as its marker; Astro no longer writes that name into the config.
38
+ */
39
+ export declare const ASTRO_VITE_OWNING_MARKER = "reticle-vite-owning";
40
+ /**
41
+ * The one runtime condition that guarantees a refused connect, said out loud on the page.
42
+ *
43
+ * Matching the Vite plugin's missing-token wording, because Astro never loads that plugin. The
44
+ * token is read at render time now, not at config load, so a reload is enough — the user does not
45
+ * have to restart the dev server.
46
+ */
47
+ export declare const ASTRO_MISSING_TOKEN_WARNING: string;
48
+ export declare function patchAstroConfig(source: string, viteMajor?: number | null): SourcePatch;
49
+ /**
50
+ * Import path from a layout/page to the generated connect module, always relative and posix.
51
+ *
52
+ * `src/layouts/Layout.astro` and `src/pages/index.astro` both resolve to `../components/ReticleDev`.
53
+ */
54
+ export declare function astroReticleDevImport(layoutPath: string): string;
55
+ export declare function patchAstroLayout(source: string, layoutPath?: string): SourcePatch;
18
56
  /** Where Astro keeps ambient types — `create-astro` ships this file. */
19
57
  export declare const ASTRO_ENV_DTS_PATH = "src/env.d.ts";
20
58
  /**
21
- * The declarations `astro check` needs for the Vite `define` names (#677).
22
- *
23
- * Matching SvelteKit's `hooks.client.ts` typing: `string | undefined`, because the define can be
24
- * absent when the pairing token file is missing.
59
+ * The declarations `astro check` needs for the window names the inline script writes (#677, #1008).
25
60
  */
26
61
  export declare const ASTRO_ENV_DTS_DECLARES: string;
27
62
  /**
28
- * Append the Vite-define ambient declarations to `src/env.d.ts`, or create the file.
63
+ * Append the window ambient declarations to `src/env.d.ts`, or create the file.
29
64
  *
30
- * Without this, `create-astro`'s default `"astro check && astro build"` fails with four
31
- * `ts(2304) Cannot find name '__RETICLE_TOKEN__'` errors after a clean init (#677).
65
+ * Without this, `create-astro`'s default `"astro check && astro build"` fails with
66
+ * `ts(2339) Property '__RETICLE_TOKEN__' does not exist on type 'Window'` after a clean init.
32
67
  */
33
68
  export declare function patchAstroEnvDts(existing: string | null): SourcePatch;