@reticlehq/init 2.14.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 (114) hide show
  1. package/LICENSE +105 -0
  2. package/NOTICE +5 -0
  3. package/README.md +29 -0
  4. package/dist/agent-rules.d.ts +116 -0
  5. package/dist/agent-rules.js +279 -0
  6. package/dist/app-choice.d.ts +29 -0
  7. package/dist/app-choice.js +39 -0
  8. package/dist/astro-host.d.ts +21 -0
  9. package/dist/astro-host.js +27 -0
  10. package/dist/astro-patch.d.ts +33 -0
  11. package/dist/astro-patch.js +103 -0
  12. package/dist/brace-scan.d.ts +24 -0
  13. package/dist/brace-scan.js +46 -0
  14. package/dist/capabilities.d.ts +40 -0
  15. package/dist/capabilities.js +167 -0
  16. package/dist/closing-hint.d.ts +35 -0
  17. package/dist/closing-hint.js +122 -0
  18. package/dist/codex-toml.d.ts +10 -0
  19. package/dist/codex-toml.js +144 -0
  20. package/dist/connect-steps.d.ts +55 -0
  21. package/dist/connect-steps.js +92 -0
  22. package/dist/cra.d.ts +82 -0
  23. package/dist/cra.js +183 -0
  24. package/dist/csp-check.d.ts +49 -0
  25. package/dist/csp-check.js +168 -0
  26. package/dist/csp-doctor.d.ts +32 -0
  27. package/dist/csp-doctor.js +62 -0
  28. package/dist/desktop-doctor.d.ts +46 -0
  29. package/dist/desktop-doctor.js +201 -0
  30. package/dist/detect.d.ts +225 -0
  31. package/dist/detect.js +334 -0
  32. package/dist/dev-script.d.ts +51 -0
  33. package/dist/dev-script.js +66 -0
  34. package/dist/dev-server-ports.d.ts +20 -0
  35. package/dist/dev-server-ports.js +45 -0
  36. package/dist/electron-patch.d.ts +11 -0
  37. package/dist/electron-patch.js +70 -0
  38. package/dist/electron-vite-patch.d.ts +12 -0
  39. package/dist/electron-vite-patch.js +100 -0
  40. package/dist/existing-config.d.ts +31 -0
  41. package/dist/existing-config.js +63 -0
  42. package/dist/format-generated.d.ts +18 -0
  43. package/dist/format-generated.js +48 -0
  44. package/dist/framework-adapter.d.ts +60 -0
  45. package/dist/framework-adapter.js +138 -0
  46. package/dist/host.d.ts +41 -0
  47. package/dist/host.js +21 -0
  48. package/dist/index.d.ts +25 -0
  49. package/dist/index.js +25 -0
  50. package/dist/init-failure.d.ts +19 -0
  51. package/dist/init-failure.js +18 -0
  52. package/dist/init-opt-out.d.ts +17 -0
  53. package/dist/init-opt-out.js +30 -0
  54. package/dist/install-hint.d.ts +9 -0
  55. package/dist/install-hint.js +41 -0
  56. package/dist/install-retries.d.ts +22 -0
  57. package/dist/install-retries.js +65 -0
  58. package/dist/install-source-config.d.ts +18 -0
  59. package/dist/install-source-config.js +34 -0
  60. package/dist/lockfiles.d.ts +14 -0
  61. package/dist/lockfiles.js +49 -0
  62. package/dist/mcp-clients.d.ts +94 -0
  63. package/dist/mcp-clients.js +327 -0
  64. package/dist/mcp-registered.d.ts +12 -0
  65. package/dist/mcp-registered.js +14 -0
  66. package/dist/mcp.d.ts +89 -0
  67. package/dist/mcp.js +129 -0
  68. package/dist/next-patch.d.ts +49 -0
  69. package/dist/next-patch.js +237 -0
  70. package/dist/node-io.d.ts +22 -0
  71. package/dist/node-io.js +150 -0
  72. package/dist/non-js-project.d.ts +44 -0
  73. package/dist/non-js-project.js +137 -0
  74. package/dist/nuxt-patch.d.ts +18 -0
  75. package/dist/nuxt-patch.js +33 -0
  76. package/dist/patch-kind.d.ts +20 -0
  77. package/dist/patch-kind.js +10 -0
  78. package/dist/plan-electron-vite.d.ts +16 -0
  79. package/dist/plan-electron-vite.js +72 -0
  80. package/dist/plan-framework.d.ts +122 -0
  81. package/dist/plan-framework.js +773 -0
  82. package/dist/plan.d.ts +312 -0
  83. package/dist/plan.js +650 -0
  84. package/dist/platform.d.ts +16 -0
  85. package/dist/platform.js +16 -0
  86. package/dist/preflight.d.ts +30 -0
  87. package/dist/preflight.js +52 -0
  88. package/dist/project-id.d.ts +12 -0
  89. package/dist/project-id.js +25 -0
  90. package/dist/refresh-rules.d.ts +40 -0
  91. package/dist/refresh-rules.js +51 -0
  92. package/dist/remember-project.d.ts +24 -0
  93. package/dist/remember-project.js +40 -0
  94. package/dist/run.d.ts +122 -0
  95. package/dist/run.js +762 -0
  96. package/dist/slash-command.d.ts +27 -0
  97. package/dist/slash-command.js +132 -0
  98. package/dist/snippets.d.ts +267 -0
  99. package/dist/snippets.js +898 -0
  100. package/dist/tanstack-start.d.ts +18 -0
  101. package/dist/tanstack-start.js +57 -0
  102. package/dist/version.d.ts +18 -0
  103. package/dist/version.js +23 -0
  104. package/dist/vite-config.d.ts +18 -0
  105. package/dist/vite-config.js +111 -0
  106. package/dist/vite-owning-config.d.ts +25 -0
  107. package/dist/vite-owning-config.js +219 -0
  108. package/dist/windows-quote.d.ts +38 -0
  109. package/dist/windows-quote.js +70 -0
  110. package/dist/workspace-apps.d.ts +40 -0
  111. package/dist/workspace-apps.js +188 -0
  112. package/dist/workspace-redirect.d.ts +22 -0
  113. package/dist/workspace-redirect.js +97 -0
  114. package/package.json +53 -0
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Does this app's Content-Security-Policy let the browser reach the Reticle bridge?
3
+ *
4
+ * Two independent field reports, both Next: `init` reported every step successful, the SDK mounted,
5
+ * the dial URL was correct, and the app never connected because a strict `connect-src` excluded
6
+ * `ws://localhost:<port>`. The browser blocks the WebSocket and says so in its own console, which
7
+ * nothing on the Reticle side reads — so `status` and `doctor` reported perfect health at an app
8
+ * that could not connect at all, forever. A setup step that exits 0 while leaving the user broken is
9
+ * the worst failure mode this repo has.
10
+ *
11
+ * A TEXT SCAN, deliberately. `headers()` is a function; `init` cannot execute a user's Next config
12
+ * without importing their whole app, and the thing worth finding is the policy string a developer
13
+ * typed — in `next.config.*`, in middleware, or in a `<meta http-equiv>` tag. The scan finds the
14
+ * string wherever it is written.
15
+ *
16
+ * NARROW on purpose: it fires only when a `connect-src` exists AND does not admit the bridge. No
17
+ * CSP, or one that already covers us, produces nothing. A warning that fires on a working setup
18
+ * costs precisely what a green check on a broken one costs, and this file exists because of the
19
+ * second kind.
20
+ */
21
+ /** Sources that admit any ws origin, so the bridge is already reachable. */
22
+ const WILDCARDS = ['*', 'ws:', 'wss:', 'ws://*', 'https:'];
23
+ /** The two hosts the SDK may dial. `localhost` and the IPv4 loopback are different CSP sources. */
24
+ function bridgeOrigins(port) {
25
+ return [`ws://localhost:${String(port)}`, `ws://127.0.0.1:${String(port)}`];
26
+ }
27
+ /**
28
+ * The `connect-src` directive's source list, or undefined when the text declares none.
29
+ *
30
+ * Matches to the end of the directive (`;`) or the end of the enclosing quoted string, which is how
31
+ * these are written in both a Next `headers()` value and a `<meta content="...">`.
32
+ */
33
+ function directiveSources(text, directive) {
34
+ // Stops at the directive separator or the closing double quote of the enclosing string. Single
35
+ // quotes are NOT terminators: `'self'` is a source, not the end of the list.
36
+ const match = new RegExp(`${directive}([^;"]*)`, 'i').exec(text);
37
+ const list = match?.[1];
38
+ if (list === undefined)
39
+ return undefined;
40
+ return list
41
+ .split(/\s+/)
42
+ .map((source) => source.trim())
43
+ .filter((source) => source.length > 0);
44
+ }
45
+ /**
46
+ * What this policy allows a WebSocket to reach — `connect-src`, or `default-src` when it is absent.
47
+ *
48
+ * The fallback is the CSP spec's, and leaving it out made the check blind to the commonest strict
49
+ * policy there is. MarkText, a production Electron editor, declares
50
+ * `default-src 'self'; script-src 'self'; …` with NO `connect-src`: its WebSockets are restricted to
51
+ * 'self', the bridge is blocked, and this returned "no problem" because it was looking for a
52
+ * directive that was not written. Every fetch-directive falls back to `default-src`; that is what
53
+ * `default` means, and it is why an author does not repeat themselves.
54
+ *
55
+ * Still undefined when NEITHER is present — a policy that constrains neither is not blocking us.
56
+ */
57
+ function connectSrcSources(text) {
58
+ return directiveSources(text, 'connect-src') ?? directiveSources(text, 'default-src');
59
+ }
60
+ /**
61
+ * The problem with this app's `connect-src`, or undefined if there is not one.
62
+ *
63
+ * `https:` counts as a wildcard here only in the sense that a policy written that loosely will not
64
+ * be the thing blocking a dev WebSocket; the check stays quiet rather than arguing about it.
65
+ */
66
+ export function cspConnectSrcProblem(text, port) {
67
+ const sources = connectSrcSources(text);
68
+ if (sources === undefined || 0 === sources.length)
69
+ return undefined;
70
+ if (sources.some((source) => WILDCARDS.includes(source)))
71
+ return undefined;
72
+ const wanted = bridgeOrigins(port);
73
+ // BOTH, not either: the SDK picks its host from how the page was served, and a policy that admits
74
+ // one is a coin flip. A coin flip that fails is indistinguishable from every other silent
75
+ // non-connect, which is the whole cost being avoided here.
76
+ if (wanted.every((origin) => sources.includes(origin)))
77
+ return undefined;
78
+ const missing = wanted.filter((origin) => !sources.includes(origin));
79
+ return (`this app declares a Content-Security-Policy whose \`connect-src\` does not admit the Reticle ` +
80
+ `bridge: ${missing.join(' and ')} ${1 === missing.length ? 'is' : 'are'} missing. The browser ` +
81
+ `will block the WebSocket and report it in ITS console only — every check on the Reticle side ` +
82
+ `will pass while the app never connects. ${devCspAddition(port)}`);
83
+ }
84
+ /**
85
+ * What this policy allows a SCRIPT to be — `script-src`, or `default-src` when it is absent.
86
+ *
87
+ * Same fallback and same reason as {@link connectSrcSources}: every fetch-directive falls back to
88
+ * `default-src`, which is why an author writing `default-src 'self'` does not repeat themselves.
89
+ */
90
+ function scriptSrcSources(text) {
91
+ return directiveSources(text, 'script-src') ?? directiveSources(text, 'default-src');
92
+ }
93
+ /**
94
+ * Would this policy stop the pasted connect snippet from RUNNING?
95
+ *
96
+ * The `connect-src` check above assumes the SDK got as far as opening a socket. Under a policy
97
+ * without `'unsafe-inline'` it never does: the snippet `init` prints is an inline
98
+ * `<script type="module">`, the browser refuses to execute it, and there is no SDK, no socket and
99
+ * nothing for `connect-src` to block. `reticle open` then reports "no session / app carries no SDK",
100
+ * which sends diagnosis at the wrong cause entirely (#679).
101
+ *
102
+ * The rule is the spec's, not a guess:
103
+ *
104
+ * - `'unsafe-inline'` is the ONLY source that admits an inline script. `*` does not — a host
105
+ * wildcard says nothing about inline code, and reading it as permission is the commonest CSP
106
+ * misconception there is.
107
+ * - a nonce, a hash, or `'strict-dynamic'` makes browsers IGNORE `'unsafe-inline'` entirely. A
108
+ * policy carrying both is a policy that blocks inline scripts, and one carrying a nonce blocks
109
+ * ours specifically: the nonce is minted for the app's own tags, not for a snippet pasted in by
110
+ * hand.
111
+ */
112
+ function blocksInlineScript(sources) {
113
+ const overridesUnsafeInline = sources.some((source) => source.startsWith("'nonce-") || source.startsWith("'sha") || "'strict-dynamic'" === source);
114
+ if (overridesUnsafeInline)
115
+ return true;
116
+ return !sources.includes("'unsafe-inline'");
117
+ }
118
+ /**
119
+ * The problem with this app's `script-src`, or undefined if there is not one.
120
+ *
121
+ * Reports the remedy that works under `'self'` — serve the connect code as an external module file —
122
+ * rather than telling anyone to weaken their policy with `'unsafe-inline'`.
123
+ */
124
+ export function cspInlineScriptProblem(text, port) {
125
+ const sources = scriptSrcSources(text);
126
+ if (sources === undefined || 0 === sources.length)
127
+ return undefined;
128
+ if (!blocksInlineScript(sources))
129
+ return undefined;
130
+ return (`this app declares a Content-Security-Policy whose \`script-src\` does not admit an inline ` +
131
+ `script, so the connect snippet never executes. Nothing on the Reticle side can see this: with ` +
132
+ `no SDK there is no socket, and \`reticle open\` reports "no session / app carries no SDK" — ` +
133
+ `the wrong cause. ${externalScriptRemedy()} ${devCspAddition(port)}`);
134
+ }
135
+ /** Where the external connect module goes, for an app served from a static directory. */
136
+ export const EXTERNAL_CONNECT_PATH = 'public/reticle-connect.js';
137
+ /** The remedy that works under `script-src 'self'`, as text to copy. */
138
+ export function externalScriptRemedy() {
139
+ return (`Serve the connect code as an EXTERNAL module instead: put it in ` +
140
+ `\`${EXTERNAL_CONNECT_PATH}\` and reference it with ` +
141
+ `\`<script type="module" src="/reticle-connect.js"></script>\`, which \`script-src 'self'\` ` +
142
+ `already allows. Note that an external module is DEFERRED, so the app's own classic scripts run ` +
143
+ `first and requests they fire before the SDK attaches are not observed.`);
144
+ }
145
+ /** The exact addition to paste. Text to copy, not advice to interpret. */
146
+ export function devCspAddition(port) {
147
+ return (`Add to \`connect-src\` in development only (e.g. behind ` +
148
+ `\`process.env.NODE_ENV === 'development'\`): ` +
149
+ `${bridgeOrigins(port).join(' ')}`);
150
+ }
151
+ /** The step title, named here so `doctor` and the plan cannot drift apart on what this check is called. */
152
+ export const CSP_STEP_TITLE = 'Content-Security-Policy';
153
+ /**
154
+ * The first blocking policy among the sources `init` already has in hand, or undefined.
155
+ *
156
+ * At most one finding: a second copy of the same policy in a second file is the same problem, and
157
+ * two identical notices read as two problems.
158
+ */
159
+ export function cspPlanProblem(sources, port) {
160
+ for (const source of sources) {
161
+ if ('string' !== typeof source)
162
+ continue;
163
+ const problem = cspConnectSrcProblem(source, port);
164
+ if (problem !== undefined)
165
+ return problem;
166
+ }
167
+ return undefined;
168
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `doctor`'s web CSP check — the sibling of the desktop one next door.
3
+ *
4
+ * The desktop findings exist because a Tauri app with the default CSP runs perfectly and never
5
+ * connects. The WEB version of that failure had no check at all, and it arrived twice in one batch
6
+ * of field reports: two Next apps where `init` printed success for every step, the SDK mounted, the
7
+ * dial URL was right, and the browser silently refused the WebSocket because `connect-src` excluded
8
+ * the bridge. The violation is reported in the browser's console and nowhere else, so every check on
9
+ * this side stayed green while the app was permanently unreachable.
10
+ *
11
+ * Reads the files a CSP is actually written in, by name — a full source scan would be slower and
12
+ * would find policy strings in test fixtures and documentation.
13
+ */
14
+ /** Read a project-relative file, or undefined when it is absent/unreadable. */
15
+ type ReadFile = (relative: string) => string | undefined;
16
+ interface CspDiagnosis {
17
+ /** Which file to look at. */
18
+ file: string;
19
+ /** What is wrong, in one line. */
20
+ problem: string;
21
+ /** Exactly what to add — copy-pasteable, with the port actually in use. */
22
+ fix: string;
23
+ }
24
+ /**
25
+ * The places a Content-Security-Policy gets written in a JS app.
26
+ *
27
+ * Next config and middleware cover `headers()` and the edge-middleware style; the layouts and the
28
+ * plain `index.html` cover the `<meta http-equiv>` style. Both reported cases were in this list.
29
+ */
30
+ export declare const CSP_FILES: readonly string[];
31
+ export declare function diagnoseWebCsp(read: ReadFile, port: number, alsoCheck?: readonly string[]): CspDiagnosis[];
32
+ export {};
@@ -0,0 +1,62 @@
1
+ /**
2
+ * `doctor`'s web CSP check — the sibling of the desktop one next door.
3
+ *
4
+ * The desktop findings exist because a Tauri app with the default CSP runs perfectly and never
5
+ * connects. The WEB version of that failure had no check at all, and it arrived twice in one batch
6
+ * of field reports: two Next apps where `init` printed success for every step, the SDK mounted, the
7
+ * dial URL was right, and the browser silently refused the WebSocket because `connect-src` excluded
8
+ * the bridge. The violation is reported in the browser's console and nowhere else, so every check on
9
+ * this side stayed green while the app was permanently unreachable.
10
+ *
11
+ * Reads the files a CSP is actually written in, by name — a full source scan would be slower and
12
+ * would find policy strings in test fixtures and documentation.
13
+ */
14
+ import { cspConnectSrcProblem, cspInlineScriptProblem, devCspAddition, externalScriptRemedy, } from './csp-check.js';
15
+ /**
16
+ * The places a Content-Security-Policy gets written in a JS app.
17
+ *
18
+ * Next config and middleware cover `headers()` and the edge-middleware style; the layouts and the
19
+ * plain `index.html` cover the `<meta http-equiv>` style. Both reported cases were in this list.
20
+ */
21
+ export const CSP_FILES = [
22
+ 'next.config.js',
23
+ 'next.config.mjs',
24
+ 'next.config.ts',
25
+ 'middleware.ts',
26
+ 'middleware.js',
27
+ 'src/middleware.ts',
28
+ 'app/layout.tsx',
29
+ 'app/layout.js',
30
+ 'pages/_document.tsx',
31
+ 'pages/_document.js',
32
+ 'index.html',
33
+ 'public/index.html',
34
+ // Vite puts the entry HTML at the project root; electron-vite and Tauri put the RENDERER's one
35
+ // under src/. MarkText declares its policy in `src/renderer/index.html`, and a desktop app is the
36
+ // likeliest place to meet a strict CSP at all — Electron's own security guidance asks for one.
37
+ 'src/index.html',
38
+ 'src/renderer/index.html',
39
+ 'vercel.json',
40
+ 'netlify.toml',
41
+ ];
42
+ export function diagnoseWebCsp(read, port, alsoCheck = []) {
43
+ const findings = [];
44
+ for (const file of [...alsoCheck, ...CSP_FILES]) {
45
+ const source = read(file);
46
+ if (source === undefined)
47
+ continue;
48
+ // `script-src` first, because it is the earlier failure. A policy that blocks the inline
49
+ // snippet means there is no SDK at all, so a `connect-src` finding on the same file would be
50
+ // describing a socket that is never opened -- true, and the wrong thing to fix first (#679).
51
+ const inline = cspInlineScriptProblem(source, port);
52
+ if (inline !== undefined) {
53
+ findings.push({ file, problem: inline, fix: externalScriptRemedy() });
54
+ continue;
55
+ }
56
+ const problem = cspConnectSrcProblem(source, port);
57
+ if (problem === undefined)
58
+ continue;
59
+ findings.push({ file, problem, fix: devCspAddition(port) });
60
+ }
61
+ return findings;
62
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Setup RCA for desktop apps.
3
+ *
4
+ * Every failure this catches is SILENT. A Tauri app with the default CSP runs perfectly and simply
5
+ * never appears in `reticle status` — the WebSocket is blocked before it opens, and nothing in the
6
+ * app's own console says so. An Electron app missing the preload line works fine and reports zero
7
+ * network activity forever, which reads as "the app makes no backend calls" rather than "you are
8
+ * blind to all of them".
9
+ *
10
+ * Silent misconfiguration is the same disease as a false green: the tool answers confidently and the
11
+ * answer is wrong. So the diagnosis names the file, the reason, and the exact line to add.
12
+ *
13
+ * Pure: a `read` function in, findings out. No fs, no cwd, no process.
14
+ */
15
+ export declare const DesktopFinding: {
16
+ /** Tauri's CSP will block the bridge WebSocket — the app will never connect, with no error. */
17
+ readonly TAURI_CSP_BLOCKS_BRIDGE: "tauri-csp-blocks-bridge";
18
+ /** A custom CSP dropped the `ipc:` source Tauri needs for `invoke` itself. */
19
+ readonly TAURI_CSP_BLOCKS_IPC: "tauri-csp-blocks-ipc";
20
+ /** The preload never requires the shim, so every IPC call is invisible. */
21
+ readonly ELECTRON_PRELOAD_MISSING: "electron-preload-missing";
22
+ /** The main process never installs the capture helper, so screenshots are unavailable. */
23
+ readonly ELECTRON_CAPTURE_MISSING: "electron-capture-missing";
24
+ };
25
+ export type DesktopFinding = (typeof DesktopFinding)[keyof typeof DesktopFinding];
26
+ export interface DesktopDiagnosis {
27
+ code: DesktopFinding;
28
+ /** Which file to look at. */
29
+ file: string;
30
+ /** What is wrong, in one line. */
31
+ problem: string;
32
+ /** Exactly what to add — copy-pasteable, with the port actually in use. */
33
+ fix: string;
34
+ }
35
+ /** Reads a project-relative path. Undefined means the file does not exist. */
36
+ type ReadFile = (path: string) => string | undefined;
37
+ export declare const PRELOAD_REQUIRE = "@reticlehq/electron/preload";
38
+ export declare const CAPTURE_REQUIRE = "@reticlehq/electron/main";
39
+ /** The copy-pasteable line `diagnoseDesktop` already prints; the main patcher reuses it on MANUAL. */
40
+ export declare const ELECTRON_CAPTURE_FIX = "add: const { installReticleCapture } = require('@reticlehq/electron/main') then installReticleCapture(win)";
41
+ export declare const ELECTRON_PRELOAD_FIX = "add as the FIRST line: require('@reticlehq/electron/preload')";
42
+ /** Is this project a desktop shell at all? Used to print a positive result instead of silence. */
43
+ export declare function isDesktopProject(read: ReadFile): boolean;
44
+ /** Which desktop shell (if any) this project is, and what is missing from its Reticle wiring. */
45
+ export declare function diagnoseDesktop(read: ReadFile, port: number): DesktopDiagnosis[];
46
+ export {};
@@ -0,0 +1,201 @@
1
+ import { RETICLE_IPC_GLOBAL } from '@reticlehq/core';
2
+ /**
3
+ * Setup RCA for desktop apps.
4
+ *
5
+ * Every failure this catches is SILENT. A Tauri app with the default CSP runs perfectly and simply
6
+ * never appears in `reticle status` — the WebSocket is blocked before it opens, and nothing in the
7
+ * app's own console says so. An Electron app missing the preload line works fine and reports zero
8
+ * network activity forever, which reads as "the app makes no backend calls" rather than "you are
9
+ * blind to all of them".
10
+ *
11
+ * Silent misconfiguration is the same disease as a false green: the tool answers confidently and the
12
+ * answer is wrong. So the diagnosis names the file, the reason, and the exact line to add.
13
+ *
14
+ * Pure: a `read` function in, findings out. No fs, no cwd, no process.
15
+ */
16
+ export const DesktopFinding = {
17
+ /** Tauri's CSP will block the bridge WebSocket — the app will never connect, with no error. */
18
+ TAURI_CSP_BLOCKS_BRIDGE: 'tauri-csp-blocks-bridge',
19
+ /** A custom CSP dropped the `ipc:` source Tauri needs for `invoke` itself. */
20
+ TAURI_CSP_BLOCKS_IPC: 'tauri-csp-blocks-ipc',
21
+ /** The preload never requires the shim, so every IPC call is invisible. */
22
+ ELECTRON_PRELOAD_MISSING: 'electron-preload-missing',
23
+ /** The main process never installs the capture helper, so screenshots are unavailable. */
24
+ ELECTRON_CAPTURE_MISSING: 'electron-capture-missing',
25
+ };
26
+ const TAURI_CONF = 'src-tauri/tauri.conf.json';
27
+ export const PRELOAD_REQUIRE = '@reticlehq/electron/preload';
28
+ export const CAPTURE_REQUIRE = '@reticlehq/electron/main';
29
+ /** The copy-pasteable line `diagnoseDesktop` already prints; the main patcher reuses it on MANUAL. */
30
+ export const ELECTRON_CAPTURE_FIX = `add: const { installReticleCapture } = require('${CAPTURE_REQUIRE}') then installReticleCapture(win)`;
31
+ export const ELECTRON_PRELOAD_FIX = `add as the FIRST line: require('${PRELOAD_REQUIRE}')`;
32
+ function parseJson(text) {
33
+ if (text === undefined)
34
+ return undefined;
35
+ try {
36
+ const parsed = JSON.parse(text);
37
+ return 'object' === typeof parsed && parsed !== null
38
+ ? parsed
39
+ : undefined;
40
+ }
41
+ catch {
42
+ return undefined; // a malformed config is the app's problem, not something to crash the doctor on
43
+ }
44
+ }
45
+ function record(value) {
46
+ return 'object' === typeof value && value !== null ? value : {};
47
+ }
48
+ /**
49
+ * Does this CSP permit the bridge WebSocket on `port`?
50
+ *
51
+ * A MISSING csp key is treated as blocking, not permissive: Tauri applies a restrictive default, so
52
+ * reporting "no CSP, therefore fine" would be precisely the false green this tool exists to prevent.
53
+ */
54
+ function cspAllowsBridge(csp, port) {
55
+ if (csp === undefined)
56
+ return false;
57
+ const exact = `ws://localhost:${String(port)}`;
58
+ const loopback = `ws://127.0.0.1:${String(port)}`;
59
+ // A BARE `ws:` scheme source allows any ws origin. It must be matched as a whole source, not as a
60
+ // substring: `includes('ws:')` also matches `ws://localhost:4400`, which would make every port
61
+ // look allowed and silently defeat the port check this function exists to perform.
62
+ const bareScheme = /(^|[\s;])ws:(?!\/\/)/.test(csp);
63
+ return (csp.includes(exact) ||
64
+ csp.includes(loopback) ||
65
+ csp.includes('ws://localhost:*') ||
66
+ csp.includes('ws://127.0.0.1:*') ||
67
+ bareScheme);
68
+ }
69
+ /** Tauri v2 needs `ipc:`/`http://ipc.localhost` in connect-src for `invoke` to work at all. */
70
+ function cspAllowsIpc(csp) {
71
+ return csp.includes('ipc:') || csp.includes('ipc.localhost');
72
+ }
73
+ function diagnoseTauri(read, port) {
74
+ const conf = parseJson(read(TAURI_CONF));
75
+ if (conf === undefined)
76
+ return [];
77
+ const security = record(record(conf['app'])['security']);
78
+ const csp = 'string' === typeof security['csp'] ? security['csp'] : undefined;
79
+ const findings = [];
80
+ if (!cspAllowsBridge(csp, port)) {
81
+ findings.push({
82
+ code: DesktopFinding.TAURI_CSP_BLOCKS_BRIDGE,
83
+ file: TAURI_CONF,
84
+ problem: csp === undefined
85
+ ? "no `app.security.csp` set, so Tauri's restrictive default applies and blocks the Reticle bridge — the app will run normally and never connect"
86
+ : 'the CSP `connect-src` does not allow the Reticle bridge WebSocket — the app will run normally and never connect',
87
+ fix: `add to app.security.csp connect-src: ws://localhost:${String(port)}`,
88
+ });
89
+ }
90
+ if (csp !== undefined && !cspAllowsIpc(csp)) {
91
+ findings.push({
92
+ code: DesktopFinding.TAURI_CSP_BLOCKS_IPC,
93
+ file: TAURI_CONF,
94
+ problem: 'the CSP dropped the `ipc:` source Tauri needs — `invoke` itself will fail',
95
+ fix: 'add to app.security.csp connect-src: ipc: http://ipc.localhost',
96
+ });
97
+ }
98
+ return findings;
99
+ }
100
+ /** The directory portion of a path, without pulling in node:path (this module stays pure). */
101
+ function dirOf(path) {
102
+ const cut = path.lastIndexOf('/');
103
+ return cut < 0 ? '' : path.slice(0, cut + 1);
104
+ }
105
+ /**
106
+ * Where a preload SOURCE might live, in the order worth trying.
107
+ *
108
+ * Beside `main` first (the unbundled layout), then the usual source directories — a bundled setup
109
+ * points `main` at `out/` or `dist/` while the real preload stays in `src/` or `electron/`.
110
+ */
111
+ function preloadCandidates(mainDir) {
112
+ const names = ['preload.cjs', 'preload.js', 'preload.mjs', 'preload.ts', 'preload.mts'];
113
+ const dirs = [mainDir, 'electron/', 'src/', 'src/preload/', 'src/main/', ''];
114
+ return dirs.flatMap((dir) => names.map((name) => `${dir}${name}`));
115
+ }
116
+ /**
117
+ * Where a BUNDLED preload's output lands. electron-vite's default layout mirrors `out/main` with
118
+ * `out/preload`; Forge and hand-rolled setups favour `dist`/`build`.
119
+ */
120
+ function bundledPreloadCandidates(mainDir) {
121
+ const roots = [mainDir.replace(/main\/?$/, ''), 'out/', 'dist/', 'build/', '.vite/build/'];
122
+ const names = [
123
+ 'preload/index.js',
124
+ 'preload/index.mjs',
125
+ 'preload/index.cjs',
126
+ 'preload.js',
127
+ 'preload.mjs',
128
+ 'preload.cjs',
129
+ ];
130
+ return roots.flatMap((root) => names.map((name) => `${root}${name}`));
131
+ }
132
+ /**
133
+ * Is the preload wired, judged from whatever evidence exists?
134
+ *
135
+ * Source first (the require is right there). Failing that, the BUILD OUTPUT: bundling inlines the
136
+ * shim, and the shim carries the contract's own window global — finding that string in the artifact
137
+ * proves the preload is wired even though the require has been compiled away. Undefined means no
138
+ * evidence either way, and an unknown must never be reported as a fault.
139
+ */
140
+ function preloadWired(read, mainDir) {
141
+ for (const candidate of preloadCandidates(mainDir)) {
142
+ const source = read(candidate);
143
+ if (source !== undefined)
144
+ return { wired: source.includes(PRELOAD_REQUIRE), file: candidate };
145
+ }
146
+ for (const candidate of bundledPreloadCandidates(mainDir)) {
147
+ const built = read(candidate);
148
+ if (built !== undefined) {
149
+ return { wired: built.includes(RETICLE_IPC_GLOBAL), file: candidate };
150
+ }
151
+ }
152
+ return undefined;
153
+ }
154
+ function diagnoseElectron(read, main) {
155
+ const findings = [];
156
+ const dir = dirOf(main);
157
+ // Undefined = no source AND no build output to judge from. An unknown is not a fault.
158
+ const preload = preloadWired(read, dir);
159
+ if (preload !== undefined && !preload.wired) {
160
+ findings.push({
161
+ code: DesktopFinding.ELECTRON_PRELOAD_MISSING,
162
+ file: preload.file,
163
+ problem: 'the preload never installs the Reticle IPC shim, so every ipcRenderer.invoke is invisible — reticle_network will report nothing and read as "this app makes no backend calls"',
164
+ fix: ELECTRON_PRELOAD_FIX,
165
+ });
166
+ }
167
+ const mainSource = read(main);
168
+ if (mainSource !== undefined && !mainSource.includes(CAPTURE_REQUIRE)) {
169
+ findings.push({
170
+ code: DesktopFinding.ELECTRON_CAPTURE_MISSING,
171
+ file: main,
172
+ problem: 'the main process never installs the capture helper, so screenshots are unavailable',
173
+ fix: ELECTRON_CAPTURE_FIX,
174
+ });
175
+ }
176
+ return findings;
177
+ }
178
+ /** Is this project a desktop shell at all? Used to print a positive result instead of silence. */
179
+ export function isDesktopProject(read) {
180
+ if (read(TAURI_CONF) !== undefined)
181
+ return true;
182
+ const pkg = parseJson(read('package.json'));
183
+ if (pkg === undefined)
184
+ return false;
185
+ const deps = { ...record(pkg['dependencies']), ...record(pkg['devDependencies']) };
186
+ return deps['electron'] !== undefined;
187
+ }
188
+ /** Which desktop shell (if any) this project is, and what is missing from its Reticle wiring. */
189
+ export function diagnoseDesktop(read, port) {
190
+ const tauri = diagnoseTauri(read, port);
191
+ if (tauri.length > 0 || read(TAURI_CONF) !== undefined)
192
+ return tauri;
193
+ const pkg = parseJson(read('package.json'));
194
+ if (pkg === undefined)
195
+ return [];
196
+ const deps = { ...record(pkg['dependencies']), ...record(pkg['devDependencies']) };
197
+ if (deps['electron'] === undefined)
198
+ return [];
199
+ const main = 'string' === typeof pkg['main'] ? pkg['main'] : undefined;
200
+ return main === undefined ? [] : diagnoseElectron(read, main);
201
+ }