@reticlehq/init 2.14.0 → 3.2.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 (125) hide show
  1. package/dist/{detect.d.ts → detect/detect.d.ts} +1 -1
  2. package/dist/{detect.js → detect/detect.js} +6 -2
  3. package/dist/{dev-server-ports.js → detect/dev-server-ports.js} +8 -0
  4. package/dist/{existing-config.d.ts → detect/existing-config.d.ts} +2 -4
  5. package/dist/{existing-config.js → detect/existing-config.js} +2 -4
  6. package/dist/{lockfiles.d.ts → detect/lockfiles.d.ts} +1 -1
  7. package/dist/{lockfiles.js → detect/lockfiles.js} +1 -2
  8. package/dist/{workspace-apps.d.ts → detect/workspace-apps.d.ts} +3 -5
  9. package/dist/{workspace-apps.js → detect/workspace-apps.js} +2 -4
  10. package/dist/{workspace-redirect.d.ts → detect/workspace-redirect.d.ts} +3 -5
  11. package/dist/{workspace-redirect.js → detect/workspace-redirect.js} +8 -12
  12. package/dist/{closing-hint.d.ts → diagnose/closing-hint.d.ts} +3 -4
  13. package/dist/{closing-hint.js → diagnose/closing-hint.js} +14 -19
  14. package/dist/diagnose/containerised-dev-server.d.ts +32 -0
  15. package/dist/diagnose/containerised-dev-server.js +56 -0
  16. package/dist/{csp-check.d.ts → diagnose/csp-check.d.ts} +2 -2
  17. package/dist/{csp-check.js → diagnose/csp-check.js} +2 -2
  18. package/dist/{desktop-doctor.js → diagnose/desktop-doctor.js} +12 -6
  19. package/dist/{init-failure.d.ts → diagnose/init-failure.d.ts} +1 -2
  20. package/dist/{init-failure.js → diagnose/init-failure.js} +1 -2
  21. package/dist/diagnose/install-hint.d.ts +8 -0
  22. package/dist/{install-hint.js → diagnose/install-hint.js} +3 -4
  23. package/dist/{install-retries.d.ts → diagnose/install-retries.d.ts} +3 -4
  24. package/dist/{install-retries.js → diagnose/install-retries.js} +6 -8
  25. package/dist/host.d.ts +14 -1
  26. package/dist/host.js +3 -0
  27. package/dist/index.d.ts +32 -12
  28. package/dist/index.js +36 -12
  29. package/dist/node-io.d.ts +12 -1
  30. package/dist/node-io.js +53 -25
  31. package/dist/{astro-patch.d.ts → patch/astro-patch.d.ts} +1 -1
  32. package/dist/{astro-patch.js → patch/astro-patch.js} +1 -1
  33. package/dist/{cra.js → patch/cra.js} +1 -1
  34. package/dist/{electron-patch.js → patch/electron-patch.js} +1 -1
  35. package/dist/{next-patch.d.ts → patch/next-patch.d.ts} +2 -2
  36. package/dist/{next-patch.js → patch/next-patch.js} +2 -2
  37. package/dist/{nuxt-patch.js → patch/nuxt-patch.js} +1 -1
  38. package/dist/{snippets.d.ts → patch/snippets.d.ts} +11 -21
  39. package/dist/{snippets.js → patch/snippets.js} +20 -29
  40. package/dist/{tanstack-start.d.ts → patch/tanstack-start.d.ts} +2 -3
  41. package/dist/{tanstack-start.js → patch/tanstack-start.js} +3 -4
  42. package/dist/{vite-config.js → patch/vite-config.js} +15 -13
  43. package/dist/{connect-steps.d.ts → plan/connect-steps.d.ts} +3 -3
  44. package/dist/{connect-steps.js → plan/connect-steps.js} +4 -4
  45. package/dist/{framework-adapter.d.ts → plan/framework-adapter.d.ts} +11 -2
  46. package/dist/{framework-adapter.js → plan/framework-adapter.js} +12 -1
  47. package/dist/plan/notices.d.ts +23 -0
  48. package/dist/plan/notices.js +94 -0
  49. package/dist/{plan-electron-vite.d.ts → plan/plan-electron-vite.d.ts} +3 -4
  50. package/dist/{plan-electron-vite.js → plan/plan-electron-vite.js} +7 -8
  51. package/dist/{plan-framework.d.ts → plan/plan-framework.d.ts} +4 -13
  52. package/dist/{plan-framework.js → plan/plan-framework.js} +14 -34
  53. package/dist/{plan.d.ts → plan/plan-types.d.ts} +14 -17
  54. package/dist/plan/plan-types.js +24 -0
  55. package/dist/plan/plan.d.ts +20 -0
  56. package/dist/{plan.js → plan/plan.js} +27 -88
  57. package/dist/{agent-rules.d.ts → project/agent-rules.d.ts} +2 -2
  58. package/dist/{agent-rules.js → project/agent-rules.js} +17 -21
  59. package/dist/project/install-source-config.d.ts +14 -0
  60. package/dist/{install-source-config.js → project/install-source-config.js} +3 -7
  61. package/dist/project/remember-project.js +74 -0
  62. package/dist/register/detect-clients.d.ts +23 -0
  63. package/dist/register/detect-clients.js +28 -0
  64. package/dist/{mcp-clients.js → register/mcp-clients.js} +4 -8
  65. package/dist/{mcp-registered.d.ts → register/mcp-registered.d.ts} +1 -4
  66. package/dist/{mcp-registered.js → register/mcp-registered.js} +1 -4
  67. package/dist/{mcp.d.ts → register/mcp.d.ts} +5 -8
  68. package/dist/{mcp.js → register/mcp.js} +6 -9
  69. package/dist/register/slash-command.d.ts +27 -0
  70. package/dist/{slash-command.js → register/slash-command.js} +10 -10
  71. package/dist/register/windows-quote.d.ts +17 -0
  72. package/dist/register/windows-quote.js +49 -0
  73. package/dist/run-types.d.ts +131 -0
  74. package/dist/run-types.js +8 -0
  75. package/dist/run.d.ts +3 -120
  76. package/dist/run.js +111 -56
  77. package/package.json +5 -5
  78. package/dist/install-hint.d.ts +0 -9
  79. package/dist/install-source-config.d.ts +0 -18
  80. package/dist/remember-project.js +0 -40
  81. package/dist/slash-command.d.ts +0 -27
  82. package/dist/windows-quote.d.ts +0 -38
  83. package/dist/windows-quote.js +0 -70
  84. /package/dist/{capabilities.d.ts → detect/capabilities.d.ts} +0 -0
  85. /package/dist/{capabilities.js → detect/capabilities.js} +0 -0
  86. /package/dist/{app-choice.d.ts → detect/declared/app-choice.d.ts} +0 -0
  87. /package/dist/{app-choice.js → detect/declared/app-choice.js} +0 -0
  88. /package/dist/{init-opt-out.d.ts → detect/declared/init-opt-out.d.ts} +0 -0
  89. /package/dist/{init-opt-out.js → detect/declared/init-opt-out.js} +0 -0
  90. /package/dist/{dev-script.d.ts → detect/dev-script.d.ts} +0 -0
  91. /package/dist/{dev-script.js → detect/dev-script.js} +0 -0
  92. /package/dist/{dev-server-ports.d.ts → detect/dev-server-ports.d.ts} +0 -0
  93. /package/dist/{non-js-project.d.ts → detect/non-js-project.d.ts} +0 -0
  94. /package/dist/{non-js-project.js → detect/non-js-project.js} +0 -0
  95. /package/dist/{platform.d.ts → detect/platform.d.ts} +0 -0
  96. /package/dist/{platform.js → detect/platform.js} +0 -0
  97. /package/dist/{csp-doctor.d.ts → diagnose/csp-doctor.d.ts} +0 -0
  98. /package/dist/{csp-doctor.js → diagnose/csp-doctor.js} +0 -0
  99. /package/dist/{desktop-doctor.d.ts → diagnose/desktop-doctor.d.ts} +0 -0
  100. /package/dist/{astro-host.d.ts → patch/astro-host.d.ts} +0 -0
  101. /package/dist/{astro-host.js → patch/astro-host.js} +0 -0
  102. /package/dist/{brace-scan.d.ts → patch/brace-scan.d.ts} +0 -0
  103. /package/dist/{brace-scan.js → patch/brace-scan.js} +0 -0
  104. /package/dist/{cra.d.ts → patch/cra.d.ts} +0 -0
  105. /package/dist/{electron-patch.d.ts → patch/electron-patch.d.ts} +0 -0
  106. /package/dist/{electron-vite-patch.d.ts → patch/electron-vite-patch.d.ts} +0 -0
  107. /package/dist/{electron-vite-patch.js → patch/electron-vite-patch.js} +0 -0
  108. /package/dist/{format-generated.d.ts → patch/format-generated.d.ts} +0 -0
  109. /package/dist/{format-generated.js → patch/format-generated.js} +0 -0
  110. /package/dist/{nuxt-patch.d.ts → patch/nuxt-patch.d.ts} +0 -0
  111. /package/dist/{patch-kind.d.ts → patch/patch-kind.d.ts} +0 -0
  112. /package/dist/{patch-kind.js → patch/patch-kind.js} +0 -0
  113. /package/dist/{vite-config.d.ts → patch/vite-config.d.ts} +0 -0
  114. /package/dist/{vite-owning-config.d.ts → patch/vite-owning-config.d.ts} +0 -0
  115. /package/dist/{vite-owning-config.js → patch/vite-owning-config.js} +0 -0
  116. /package/dist/{preflight.d.ts → plan/preflight.d.ts} +0 -0
  117. /package/dist/{preflight.js → plan/preflight.js} +0 -0
  118. /package/dist/{project-id.d.ts → project/project-id.d.ts} +0 -0
  119. /package/dist/{project-id.js → project/project-id.js} +0 -0
  120. /package/dist/{refresh-rules.d.ts → project/refresh-rules.d.ts} +0 -0
  121. /package/dist/{refresh-rules.js → project/refresh-rules.js} +0 -0
  122. /package/dist/{remember-project.d.ts → project/remember-project.d.ts} +0 -0
  123. /package/dist/{codex-toml.d.ts → register/codex-toml.d.ts} +0 -0
  124. /package/dist/{codex-toml.js → register/codex-toml.js} +0 -0
  125. /package/dist/{mcp-clients.d.ts → register/mcp-clients.d.ts} +0 -0
package/dist/index.js CHANGED
@@ -9,17 +9,41 @@
9
9
  * ends up unable to move a file.
10
10
  */
11
11
  export { runInit, resolveLockfiles, } from './run.js';
12
- export { buildNodeIo } from './node-io.js';
12
+ /**
13
+ * Exported for the ONE other place in this repo that spawns a package manager: `reticle update`.
14
+ *
15
+ * It is a pure string rule with its own tests, and it was private while a second caller needed it
16
+ * -- which is exactly the shape that let `reticle setup mcp` reimplement the spawn as a bare
17
+ * `execFileSync` and break on Windows. Two copies of a quoting rule is the version of this bug that
18
+ * is hardest to see, so there is one.
19
+ */
20
+ export { windowsShellArg } from './register/windows-quote.js';
21
+ export { buildNodeIo, probeCli } from './node-io.js';
13
22
  export { SILENT_HOST } from './host.js';
14
23
  export { RETICLE_VERSION, RETICLE_NPM_PACKAGE } from './version.js';
15
- export { InitFailure } from './init-failure.js';
16
- export { NodePlatform } from './platform.js';
17
- export { DEV_SERVER_PORTS, isLikelyDevServerPort, devServerPortWarning, } from './dev-server-ports.js';
18
- export { configWithInstallSource } from './install-source-config.js';
19
- export { projectIdOf, rememberProjectOnDisk } from './remember-project.js';
20
- export { detectPackageManager, installCommandParts, parseMajor, PackageManager } from './detect.js';
21
- export { findWorkspaceApps } from './workspace-apps.js';
22
- export { refreshAgentRules } from './refresh-rules.js';
23
- export { diagnoseDesktop, isDesktopProject } from './desktop-doctor.js';
24
- export { diagnoseWebCsp } from './csp-doctor.js';
25
- export { reticleConfigContent } from './snippets.js';
24
+ export { InitFailure } from './diagnose/init-failure.js';
25
+ export { NodePlatform } from './detect/platform.js';
26
+ export { DEV_SERVER_PORTS, isLikelyDevServerPort, devServerPortWarning, } from './detect/dev-server-ports.js';
27
+ export { configWithInstallSource } from './project/install-source-config.js';
28
+ export { projectIdOf, rememberProjectOnDisk } from './project/remember-project.js';
29
+ export { detectPackageManager, installCommandParts, parseMajor, PackageManager, } from './detect/detect.js';
30
+ export { findWorkspaceApps } from './detect/workspace-apps.js';
31
+ export { refreshAgentRules } from './project/refresh-rules.js';
32
+ export { diagnoseDesktop, isDesktopProject } from './diagnose/desktop-doctor.js';
33
+ export { diagnoseWebCsp } from './diagnose/csp-doctor.js';
34
+ export { reticleConfigContent } from './patch/snippets.js';
35
+ /**
36
+ * MCP registration, for a caller that has no project.
37
+ *
38
+ * `reticle setup mcp` registers the server for the USER across every agent on the machine, which is
39
+ * what the one-line installer runs before any project exists. It needs the same client table and
40
+ * the same detection `init` uses — a second copy would be a second answer to "which agents are
41
+ * here", and the two would disagree the first time a client was added to one of them.
42
+ */
43
+ export { McpClient, ConfigScope, MCP_CLIENTS, ClientMergeStatus, clientSpec, mergeClientConfig, } from './register/mcp-clients.js';
44
+ export { claudeAddCommand,
45
+ // The two probes `run.ts` uses to decide about Claude Code. Exported because `reticle setup mcp`
46
+ // asks the same question and had no way to ask it: Claude Code keeps no config file, so the
47
+ // file-reading detector cannot see it, and the installer silently skipped the commonest client.
48
+ claudeAvailableProbe, claudeExistsProbe, } from './register/mcp.js';
49
+ export { detectMcpClients } from './register/detect-clients.js';
package/dist/node-io.d.ts CHANGED
@@ -10,7 +10,7 @@ import type { InitHost } from './host.js';
10
10
  *
11
11
  * The set is closed and tiny — the four package managers, `npx`, and the Claude CLI — because those
12
12
  * are the only things the plan can ask for. It is a named constant for the same reason every other
13
- * wire string here is one, and it is CHECKED because of `shellOpt`: on Windows these spawn through
13
+ * wire string here is one, and it is CHECKED because of the shell: on Windows these spawn through
14
14
  * a shell, and a shell turns the command name into something the shell parses rather than a file it
15
15
  * executes. Arguments are quoted by `shellSafe`; the command name was the half nothing covered.
16
16
  *
@@ -19,4 +19,15 @@ import type { InitHost } from './host.js';
19
19
  * as "the command ran and failed" and hide the bug in a retry.
20
20
  */
21
21
  export declare const RUNNABLE_COMMANDS: readonly string[];
22
+ /**
23
+ * Run one allowed CLI quietly and say whether it succeeded — the `probe` above, without an `InitIo`.
24
+ *
25
+ * Exported because `reticle setup mcp` needs exactly this and had reimplemented it as a bare
26
+ * `execFileSync(command, args)`. That works everywhere except Windows, where `claude` is a `.cmd`
27
+ * shim that cannot be spawned without a shell: the probe threw ENOENT, the installer concluded the
28
+ * machine had no Claude Code on it, and said so. The three rules that make this correct —
29
+ * `runnable`, `shellSafe` and the shell decision — already lived here and were private, which is the whole
30
+ * reason a second, broken copy existed.
31
+ */
32
+ export declare function probeCli(command: string, args: readonly string[]): boolean;
22
33
  export declare function buildNodeIo(cwd: string, host: InitHost): InitIo;
package/dist/node-io.js CHANGED
@@ -4,25 +4,14 @@
4
4
  * command, not the MCP stdio transport.
5
5
  */
6
6
  import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, accessSync, constants, } from 'node:fs';
7
- import { NodePlatform } from './platform.js';
7
+ import { NodePlatform } from './detect/platform.js';
8
8
  import { join, dirname, isAbsolute } from 'node:path';
9
9
  import { homedir } from 'node:os';
10
10
  import { spawnSync } from 'node:child_process';
11
- import { windowsShellArg } from './windows-quote.js';
12
- /**
13
- * `shell: true` ONLY where it earns its keep.
14
- *
15
- * It exists so package-manager shims (`pnpm.cmd`, `npx.cmd`) resolve on Windows. On POSIX it buys
16
- * nothing and costs correctness: under a shell, arguments are re-parsed, so a path containing a
17
- * space — `/Users/ada/My Projects/app` — silently becomes two arguments and registration fails with
18
- * no error anyone can read.
19
- */
20
- function shellOpt() {
21
- return NodePlatform.WINDOWS === process.platform ? { shell: true } : {};
22
- }
11
+ import { windowsShellArg } from './register/windows-quote.js';
23
12
  /**
24
13
  * Under a shell, quote what the shell would otherwise split or interpret. Windows only, where
25
- * `shell: true` is required (see shellOpt). The rule itself lives in windows-quote.ts as a pure
14
+ * `shell: true` is required (see spawnAllowed). The rule itself lives in windows-quote.ts as a pure
26
15
  * function so it can be tested on every platform — keeping it inside this branch is why it was
27
16
  * wrong for as long as it was.
28
17
  */
@@ -36,7 +25,7 @@ function shellSafe(args) {
36
25
  *
37
26
  * The set is closed and tiny — the four package managers, `npx`, and the Claude CLI — because those
38
27
  * are the only things the plan can ask for. It is a named constant for the same reason every other
39
- * wire string here is one, and it is CHECKED because of `shellOpt`: on Windows these spawn through
28
+ * wire string here is one, and it is CHECKED because of the shell: on Windows these spawn through
40
29
  * a shell, and a shell turns the command name into something the shell parses rather than a file it
41
30
  * executes. Arguments are quoted by `shellSafe`; the command name was the half nothing covered.
42
31
  *
@@ -53,6 +42,53 @@ function runnable(command) {
53
42
  }
54
43
  return found;
55
44
  }
45
+ /**
46
+ * Every spawn `init` makes, in the one shape Node does not deprecate.
47
+ *
48
+ * Passing an args ARRAY together with `shell: true` is DEP0190, and on Windows — the only platform
49
+ * where this turns the shell on — every single `init` and `curl | sh` install printed this
50
+ * into the middle of its output:
51
+ *
52
+ * [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead
53
+ * to security vulnerabilities, as the arguments are not escaped, only concatenated.
54
+ *
55
+ * A security warning in the middle of an install is a bad first thing to show somebody, and on the
56
+ * platform with the most users it was every install. The deprecation is also RIGHT about the
57
+ * reason: Node does not escape, it concatenates. We already escape — `windowsShellArg` implements
58
+ * the `CommandLineToArgvW` rule and is tested on every platform — so the fix is to own the
59
+ * concatenation rather than hand Node an array it will join unsafely. Same command line, same
60
+ * quoting, no warning.
61
+ *
62
+ * POSIX keeps the array and no shell, where argv is passed through untouched and a path with a
63
+ * space cannot be re-split.
64
+ */
65
+ function spawnAllowed(command, args, options) {
66
+ const name = runnable(command);
67
+ if (NodePlatform.WINDOWS !== process.platform) {
68
+ return spawnSync(name, [...args], options);
69
+ }
70
+ // The name is one of RUNNABLE_COMMANDS, so it never needs quoting itself.
71
+ return spawnSync([name, ...shellSafe(args)].join(' '), [], { ...options, shell: true });
72
+ }
73
+ /**
74
+ * Run one allowed CLI quietly and say whether it succeeded — the `probe` above, without an `InitIo`.
75
+ *
76
+ * Exported because `reticle setup mcp` needs exactly this and had reimplemented it as a bare
77
+ * `execFileSync(command, args)`. That works everywhere except Windows, where `claude` is a `.cmd`
78
+ * shim that cannot be spawned without a shell: the probe threw ENOENT, the installer concluded the
79
+ * machine had no Claude Code on it, and said so. The three rules that make this correct —
80
+ * `runnable`, `shellSafe` and the shell decision — already lived here and were private, which is the whole
81
+ * reason a second, broken copy existed.
82
+ */
83
+ export function probeCli(command, args) {
84
+ try {
85
+ const result = spawnAllowed(command, args, { stdio: 'ignore' });
86
+ return 0 === result.status;
87
+ }
88
+ catch {
89
+ return false;
90
+ }
91
+ }
56
92
  export function buildNodeIo(cwd, host) {
57
93
  // Project-relative by default; absolute paths (e.g. ~/.cursor/mcp.json) pass through unchanged.
58
94
  const abs = (rel) => (isAbsolute(rel) ? rel : join(cwd, rel));
@@ -116,11 +152,7 @@ export function buildNodeIo(cwd, host) {
116
152
  },
117
153
  exec(command, args) {
118
154
  // Inherit stdio so the install's own progress is visible to the user.
119
- const result = spawnSync(runnable(command), shellSafe(args), {
120
- cwd,
121
- stdio: 'inherit',
122
- ...shellOpt(),
123
- });
155
+ const result = spawnAllowed(command, args, { cwd, stdio: 'inherit' });
124
156
  return 0 === result.status;
125
157
  },
126
158
  /** One access check, rather than discovering it as an EACCES stack four phases later. */
@@ -135,11 +167,7 @@ export function buildNodeIo(cwd, host) {
135
167
  },
136
168
  probe(command, args) {
137
169
  // Quiet yes/no check (CLI availability, existing registration). Never throws.
138
- const result = spawnSync(runnable(command), shellSafe(args), {
139
- cwd,
140
- stdio: 'ignore',
141
- ...shellOpt(),
142
- });
170
+ const result = spawnAllowed(command, args, { cwd, stdio: 'ignore' });
143
171
  return 0 === result.status;
144
172
  },
145
173
  print(line) {
@@ -12,7 +12,7 @@
12
12
  * Half-editing a build config is worse than a documented manual step.
13
13
  */
14
14
  import { type SourcePatch } from './patch-kind.js';
15
- import { UiLibrary } from './detect.js';
15
+ import { UiLibrary } from '../detect/detect.js';
16
16
  export declare function patchAstroConfig(source: string): SourcePatch;
17
17
  export declare function patchAstroLayout(source: string, port: number | undefined, projectId: string | undefined, uiLibrary?: UiLibrary, testids?: readonly string[]): SourcePatch;
18
18
  /** Where Astro keeps ambient types — `create-astro` ships this file. */
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import { RETICLE_DEFAULT_PORT, bridgeWsUrl } from '@reticlehq/core';
15
15
  import { PatchKind } from './patch-kind.js';
16
- import { UiLibrary } from './detect.js';
16
+ import { UiLibrary } from '../detect/detect.js';
17
17
  import { registerCapabilitiesCall, sdkImport } from './snippets.js';
18
18
  import { patchViteOwningConfig } from './vite-owning-config.js';
19
19
  /** Present in a patched layout and in the printed recipe's script. */
@@ -13,7 +13,7 @@
13
13
  * mechanism and is gitignored by CRA's own template.
14
14
  */
15
15
  import { bridgeWsUrl } from '@reticlehq/core';
16
- import { CLI } from './agent-rules.js';
16
+ import { CLI } from '../project/agent-rules.js';
17
17
  import { registerCapabilitiesCall } from './snippets.js';
18
18
  /** The line added to `src/index.tsx` / `src/index.js`. Side-effect import: the module guards itself on NODE_ENV. */
19
19
  export const CRA_DEV_MODULE_IMPORT = "import './reticle-dev';";
@@ -7,7 +7,7 @@
7
7
  * (zero, several, a factory) is MANUAL carrying the doctor's existing fix text, not a guess.
8
8
  */
9
9
  import { matchingParenEnd } from './brace-scan.js';
10
- import { CAPTURE_REQUIRE, ELECTRON_CAPTURE_FIX, PRELOAD_REQUIRE } from './desktop-doctor.js';
10
+ import { CAPTURE_REQUIRE, ELECTRON_CAPTURE_FIX, PRELOAD_REQUIRE, } from '../diagnose/desktop-doctor.js';
11
11
  import { PatchKind } from './patch-kind.js';
12
12
  const ESM_PRELOAD_EXT = /\.(?:ts|mts|mjs)$/i;
13
13
  const WINDOW_DECL = /const\s+(\w+)\s*=\s*new\s+BrowserWindow\s*\(/g;
@@ -2,8 +2,8 @@
2
2
  * Pure, conservative patchers for the two files a Next app needed edited BY HAND: `next.config.*`
3
3
  * (wrap the export in `withReticle`) and `app/layout.tsx` (mount `<ReticleDev />`). Same contract as
4
4
  * the Vite patcher — recognise the obvious shape, bail to `manual` on anything ambiguous, never
5
- * half-edit. Leaving these manual is why Next connected 0% of the time: both edits are silent when
6
- * skipped, and one of them is JSX.
5
+ * half-edit. These must not be left manual: both edits fail silently when skipped, and one of them
6
+ * is JSX.
7
7
  */
8
8
  import { type SourcePatch } from './patch-kind.js';
9
9
  /** Where the generated connect component goes, and how the mount file should import it. */
@@ -2,8 +2,8 @@
2
2
  * Pure, conservative patchers for the two files a Next app needed edited BY HAND: `next.config.*`
3
3
  * (wrap the export in `withReticle`) and `app/layout.tsx` (mount `<ReticleDev />`). Same contract as
4
4
  * the Vite patcher — recognise the obvious shape, bail to `manual` on anything ambiguous, never
5
- * half-edit. Leaving these manual is why Next connected 0% of the time: both edits are silent when
6
- * skipped, and one of them is JSX.
5
+ * half-edit. These must not be left manual: both edits fail silently when skipped, and one of them
6
+ * is JSX.
7
7
  */
8
8
  import { PatchKind } from './patch-kind.js';
9
9
  const RETICLE_NEXT_PACKAGE = '@reticlehq/next';
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { patchViteOwningConfig } from './vite-owning-config.js';
16
16
  import { sdkImport } from './snippets.js';
17
- import { UiLibrary } from './detect.js';
17
+ import { UiLibrary } from '../detect/detect.js';
18
18
  /** The config filenames Nuxt itself accepts, in the order `init` should prefer them. */
19
19
  export const NUXT_CONFIG_CANDIDATES = ['nuxt.config.ts', 'nuxt.config.js', 'nuxt.config.mjs'];
20
20
  /**
@@ -2,8 +2,8 @@
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 { UiLibrary } from './detect.js';
6
- import type { FoundStore } from './capabilities.js';
5
+ import { UiLibrary } from '../detect/detect.js';
6
+ import type { FoundStore } from '../detect/capabilities.js';
7
7
  /**
8
8
  * The connect argument literal: a non-default port adds a `url`, and a projectId is always passed
9
9
  * (so the app is identifiable across port changes). Empty string only when neither applies.
@@ -140,15 +140,11 @@ export declare function djangoMiddlewareSnippet(connectArgLiteral: string): stri
140
140
  export declare function htmlManual(port: number | undefined, projectId?: string, pairingToken?: string): string;
141
141
  export declare const NEXT_RETICLE_DEV_PATH = "app/reticle-dev.tsx";
142
142
  export declare const SVELTEKIT_HOOKS_PATH = "src/hooks.client.ts";
143
- /**
144
- * Said to the user's face rather than discovered later. React, Next, Remix and Astro each have an
145
- * app and a CI gate; SvelteKit has neither, so "it generated some wiring" is not evidence it works.
146
- */
147
143
  /**
148
144
  * Said out loud when the app does not render through React. The SDK is framework-agnostic — DOM,
149
145
  * network, console and routing all still work — but `@reticlehq/react` is a React adapter, so
150
- * component names and source mapping do not, and no CI gate covers this stack. Reporting all-green
151
- * here is the one thing this project exists not to do.
146
+ * component names and source mapping do not, and no CI gate covers this stack. Saying so beats
147
+ * reporting all-green.
152
148
  */
153
149
  export declare function unverifiedUiLibraryNote(library: string): string;
154
150
  export declare const UNVERIFIED_FRAMEWORK_NOTE = "Reticle has no SvelteKit app and no CI gate for one, so this wiring is untested \u2014 it may work, but nothing proves it and nothing will tell us if it breaks. Supported and gated today: Vite + React, Next.js, Remix and Astro. If the hook does not register a session, please open an issue.";
@@ -244,20 +240,14 @@ export declare function nuxtPluginFile(port: number | undefined, projectId?: str
244
240
  */
245
241
  export declare const NUXT_PLUGIN_NOTICE: string;
246
242
  /**
247
- * The Nuxt recipe, written out in full because every trap in it is one somebody actually hit.
248
- *
249
- * Reported from the field, in the order they were hit: `init` classified a Nuxt 4 app as `html`, so
250
- * it installed a package named `@reticlehq/react` (with `react` in its peer dependencies) into a Vue
251
- * codebase — the reporter only continued after auditing our dist to confirm there are no React
252
- * imports at runtime, which most people will not do. It then handed over a snippet guarded on
253
- * `window.location.hostname === 'localhost'`, which fails twice over in Nuxt: `window` does not
254
- * exist during SSR, and the dev host here was a hosts-file alias (required for the backend's
255
- * white-label origin detection), so the guard was false and the connect never ran — no error, no log
256
- * line, nothing to debug. And nothing said a running dev server does not pick up a new plugin.
243
+ * The Nuxt recipe, written out in full because each trap in it is one a Nuxt 4 app actually hit:
244
+ * a Vue codebase classified as `html` and handed `@reticlehq/react`, a connect guarded on
245
+ * `window.location.hostname === 'localhost'` (false on any hosts-file alias, and `window` does not
246
+ * exist during SSR), and a running dev server that never picked up the new plugin.
257
247
  *
258
- * So: `import.meta.dev` (build-time, host-independent) instead of a hostname check, `.client.ts`
259
- * instead of an SSR guard, the framework-neutral sensor instead of the React kit, the non-localhost
260
- * flag named up front, and the restart said out loud.
248
+ * So: `import.meta.dev` instead of a hostname check, `.client.ts` instead of an SSR guard, the
249
+ * framework-neutral sensor instead of the React kit, the non-localhost flag named up front, and the
250
+ * restart said out loud.
261
251
  */
262
252
  export declare function nuxtManual(port: number | undefined, projectId?: string): string;
263
253
  /**
@@ -3,8 +3,8 @@
3
3
  * so the runner never inlines free strings.
4
4
  */
5
5
  import { RETICLE_DEFAULT_PORT, ReticleDir, bridgeWsUrl, RETICLE_CLIENT_HOST, RETICLE_WS_PATH, } from '@reticlehq/core';
6
- import { UiLibrary } from './detect.js';
7
- import { RETICLE_VERSION } from './version.js';
6
+ import { UiLibrary } from '../detect/detect.js';
7
+ import { RETICLE_VERSION } from '../version.js';
8
8
  /**
9
9
  * The SDK as one import a plain page can actually resolve.
10
10
  *
@@ -113,11 +113,13 @@ pointers, not the app.`;
113
113
  import { reticle } from '@reticlehq/vite-plugin';
114
114
 
115
115
  export default defineConfig({
116
- plugins: [${frameworkPluginExample(uiLibrary)}, ${call}],
116
+ plugins: [${call}, ${frameworkPluginExample(uiLibrary)}],
117
117
  });
118
118
 
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}`;
119
+ The position in the array does not matter: the plugin declares \`enforce: 'pre'\`, so Vite runs it
120
+ before every normal plugin wherever you put it. Shown first only because that is where \`init\`
121
+ writes it when it patches the config for you. It applies during \`vite\` (dev) only — it is dropped
122
+ from \`vite build\`.${note}`;
121
123
  }
122
124
  /**
123
125
  * The electron-vite recipe: the plugin belongs in `renderer`, never in `main` or `preload`.
@@ -573,15 +575,11 @@ ${staticPageSnippet(withToken)}
573
575
  }
574
576
  export const NEXT_RETICLE_DEV_PATH = 'app/reticle-dev.tsx';
575
577
  export const SVELTEKIT_HOOKS_PATH = 'src/hooks.client.ts';
576
- /**
577
- * Said to the user's face rather than discovered later. React, Next, Remix and Astro each have an
578
- * app and a CI gate; SvelteKit has neither, so "it generated some wiring" is not evidence it works.
579
- */
580
578
  /**
581
579
  * Said out loud when the app does not render through React. The SDK is framework-agnostic — DOM,
582
580
  * network, console and routing all still work — but `@reticlehq/react` is a React adapter, so
583
- * component names and source mapping do not, and no CI gate covers this stack. Reporting all-green
584
- * here is the one thing this project exists not to do.
581
+ * component names and source mapping do not, and no CI gate covers this stack. Saying so beats
582
+ * reporting all-green.
585
583
  */
586
584
  export function unverifiedUiLibraryNote(library) {
587
585
  // Preact is not in the same position as Vue or Svelte and must not be told it is. The React
@@ -812,20 +810,14 @@ export const NUXT_PLUGIN_NOTICE = 'Restart the dev server: one that is already r
812
810
  'needs TWO additions, not one: allowNonLocalhost: true AND the token from ~/.reticle/pairing-token. ' +
813
811
  'The flag alone is NOT sufficient off localhost.';
814
812
  /**
815
- * The Nuxt recipe, written out in full because every trap in it is one somebody actually hit.
816
- *
817
- * Reported from the field, in the order they were hit: `init` classified a Nuxt 4 app as `html`, so
818
- * it installed a package named `@reticlehq/react` (with `react` in its peer dependencies) into a Vue
819
- * codebase — the reporter only continued after auditing our dist to confirm there are no React
820
- * imports at runtime, which most people will not do. It then handed over a snippet guarded on
821
- * `window.location.hostname === 'localhost'`, which fails twice over in Nuxt: `window` does not
822
- * exist during SSR, and the dev host here was a hosts-file alias (required for the backend's
823
- * white-label origin detection), so the guard was false and the connect never ran — no error, no log
824
- * line, nothing to debug. And nothing said a running dev server does not pick up a new plugin.
813
+ * The Nuxt recipe, written out in full because each trap in it is one a Nuxt 4 app actually hit:
814
+ * a Vue codebase classified as `html` and handed `@reticlehq/react`, a connect guarded on
815
+ * `window.location.hostname === 'localhost'` (false on any hosts-file alias, and `window` does not
816
+ * exist during SSR), and a running dev server that never picked up the new plugin.
825
817
  *
826
- * So: `import.meta.dev` (build-time, host-independent) instead of a hostname check, `.client.ts`
827
- * instead of an SSR guard, the framework-neutral sensor instead of the React kit, the non-localhost
828
- * flag named up front, and the restart said out loud.
818
+ * So: `import.meta.dev` instead of a hostname check, `.client.ts` instead of an SSR guard, the
819
+ * framework-neutral sensor instead of the React kit, the non-localhost flag named up front, and the
820
+ * restart said out loud.
829
821
  */
830
822
  export function nuxtManual(port, projectId) {
831
823
  const base = connectArg(port, projectId);
@@ -884,11 +876,10 @@ export function reticleConfigContent(framework, port, projectId, installSource)
884
876
  fields['projectId'] = projectId;
885
877
  if (port !== undefined && port !== RETICLE_DEFAULT_PORT)
886
878
  fields['port'] = port;
887
- // How this install arrived, recorded HERE because it is a property of the install and the only
888
- // moment anything knows it is the moment it happens. It reaches us as an environment variable set
889
- // by whichever channel ran the install, and an environment variable is gone by the next command,
890
- // so every event after this one reported `unknown` and the question "which channel actually
891
- // converts" could not be asked at all. Written once, read for the life of the project.
879
+ // How this install arrived, recorded HERE because this is the only moment anything knows it. It
880
+ // arrives as an environment variable set by whichever channel ran the install, and an environment
881
+ // variable is gone by the next command, so every later event reported `unknown`. Written once,
882
+ // read for the life of the project.
892
883
  //
893
884
  // A closed vocabulary, narrowed before it gets here, so this can never carry a path or a URL.
894
885
  if (installSource !== undefined && installSource.length > 0) {
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * The TanStack Start connect recipe.
3
3
  *
4
- * Its own module rather than another entry in `snippets.ts`, which was at the 1000-line backstop:
5
- * Start is the one framework whose document module `init` deliberately refuses to write, so the
6
- * recipe and the reasoning behind that refusal are a unit.
4
+ * Its own module because Start is the one framework whose document module `init` deliberately
5
+ * refuses to write, so the recipe and the reasoning behind that refusal are a unit.
7
6
  */
8
7
  /** TanStack Start's document module — the file that SSRs `<html>`. */
9
8
  export declare const TANSTACK_START_ROOT_PATH = "src/routes/__root.tsx";
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * The TanStack Start connect recipe.
3
3
  *
4
- * Its own module rather than another entry in `snippets.ts`, which was at the 1000-line backstop:
5
- * Start is the one framework whose document module `init` deliberately refuses to write, so the
6
- * recipe and the reasoning behind that refusal are a unit.
4
+ * Its own module because Start is the one framework whose document module `init` deliberately
5
+ * refuses to write, so the recipe and the reasoning behind that refusal are a unit.
7
6
  */
8
- import { UiLibrary } from './detect.js';
7
+ import { UiLibrary } from '../detect/detect.js';
9
8
  import { connectArg, sdkImport, UNVERIFIED_TANSTACK_START_NOTE } from './snippets.js';
10
9
  /** TanStack Start's document module — the file that SSRs `<html>`. */
11
10
  export const TANSTACK_START_ROOT_PATH = 'src/routes/__root.tsx';
@@ -10,19 +10,18 @@ const RETICLE_MARKER = '@reticlehq/vite-plugin';
10
10
  /**
11
11
  * The `reticle(...)` call — the bridge port so the injected connect targets it.
12
12
  *
13
- * **`captureNetworkBodies` is OPT-IN.** It was written in by default, and the argument for that was
14
- * a good one: without a body, a write that answers 2xx grades `unknown / outcome_unread`, because a
15
- * 200 describes the transport and not the result. Measured on a real payments UI — a refund posted
16
- * rupees into a paise field, the server answered 200 having refunded a hundredth of it, the page
17
- * rendered the amount the user had typed, and every DOM-level check passed.
13
+ * **`captureNetworkBodies` is OPT-IN.**
18
14
  *
19
- * That argument justifies the CAPABILITY. It does not justify the default, and the distinction is
20
- * what #705 is about. A healthcare workspace proxying authenticated API traffic through Vite ran
21
- * `init`, and on the first drive their login tokens and patient payloads were in the daemon's
22
- * buffer. The old comment here conceded the exact gap — "the credential classes are redacted ... but
23
- * an address or an email is not, and nothing here should decide that for someone silently" — and
24
- * then decided it for them, because writing the line into their config IS deciding. `init` is run
25
- * unattended by an agent; the person who knows the data is sensitive is not in the room.
15
+ * The case FOR capturing bodies is real: without one, a write that answers 2xx grades
16
+ * `unknown / outcome_unread`, because a 200 describes the transport and not the result. A request
17
+ * that posts a wrong amount can answer 200, render the amount the user typed, and pass every
18
+ * DOM-level check.
19
+ *
20
+ * That justifies the CAPABILITY, not the default. Any app proxying authenticated API traffic through
21
+ * Vite puts request bodies in the daemon's buffer on the first drive, and the credential classes are
22
+ * redacted while an address or an email is not. Writing the line into someone's config IS deciding
23
+ * that for them — and `init` is run unattended by an agent, so the person who knows whether the data
24
+ * is sensitive is not in the room.
26
25
  *
27
26
  * So the default is off and the ways in are all deliberate:
28
27
  * - `reticle init --capture-bodies` writes the line, for someone who has decided
@@ -77,7 +76,10 @@ function insertImport(source) {
77
76
  * single-line array needs the space, or the result reads `[reticle(),react()]`.
78
77
  */
79
78
  function insertPlugin(source, call) {
80
- return source.replace(PLUGINS_ARRAY, (match, _g, offset) => {
79
+ // `(match, offset, source)` — PLUGINS_ARRAY has no capture group. Reading the second argument as
80
+ // one put the whole source string in `offset`, so the index below was `undefined` and the
81
+ // separator was empty every single time.
82
+ return source.replace(PLUGINS_ARRAY, (match, offset) => {
81
83
  const next = source[offset + match.length] ?? '';
82
84
  const separator = '' === next || /\s/.test(next) ? '' : ' ';
83
85
  return `${match}${call},${separator}`;
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * The step titles `init` emits, and which of them decide whether the app can dial the daemon.
3
3
  *
4
- * Lifted out of `plan.ts` when that file reached the size backstop. A cohesive unit on its own
5
- * terms: one question, asked of a step title, with one consequence — a manual step in this set makes
6
- * `init` exit non-zero instead of reporting a success over an app that can never connect.
4
+ * A cohesive unit on its own terms: one question, asked of a step title, with one consequence — a
5
+ * manual step in this set makes `init` exit non-zero instead of reporting a success over an app that
6
+ * can never connect.
7
7
  *
8
8
  * The membership used to be matched against free strings written out a second time here, so
9
9
  * renaming a step in `plan-framework.ts` silently dropped it out of the set and nothing went red.
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * The step titles `init` emits, and which of them decide whether the app can dial the daemon.
3
3
  *
4
- * Lifted out of `plan.ts` when that file reached the size backstop. A cohesive unit on its own
5
- * terms: one question, asked of a step title, with one consequence — a manual step in this set makes
6
- * `init` exit non-zero instead of reporting a success over an app that can never connect.
4
+ * A cohesive unit on its own terms: one question, asked of a step title, with one consequence — a
5
+ * manual step in this set makes `init` exit non-zero instead of reporting a success over an app that
6
+ * can never connect.
7
7
  *
8
8
  * The membership used to be matched against free strings written out a second time here, so
9
9
  * renaming a step in `plan-framework.ts` silently dropped it out of the set and nothing went red.
10
10
  * `StepTitle` is now the one place a title is spelled: both files import it, so a rename is a
11
11
  * compile error and `plan-framework.test.ts` pins that no builder has drifted back to a literal.
12
12
  */
13
- import { CSP_STEP_TITLE } from './csp-check.js';
13
+ import { CSP_STEP_TITLE } from '../diagnose/csp-check.js';
14
14
  /** Every fixed title a plan step can carry. The one spelling; nothing writes a title inline. */
15
15
  export const StepTitle = {
16
16
  VITE_PLUGIN: 'Vite plugin',
@@ -18,9 +18,9 @@
18
18
  *
19
19
  * `framework-adapter.test.ts` asserts the seam between the two halves.
20
20
  */
21
- import { Framework } from './detect.js';
21
+ import { Framework } from '../detect/detect.js';
22
22
  import { StepTitle } from './connect-steps.js';
23
- import type { PlanInput, Step } from './plan.js';
23
+ 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";
@@ -58,3 +58,12 @@ export interface FrameworkAdapter {
58
58
  readonly carriesOwnUnverifiedNote: boolean;
59
59
  }
60
60
  export declare const FRAMEWORK_ADAPTERS: Record<Framework, FrameworkAdapter>;
61
+ /**
62
+ * The per-framework half of the plan, looked up rather than switched.
63
+ *
64
+ * `FRAMEWORK_ADAPTERS` is a `Record<Framework, FrameworkAdapter>`, so a member added to `Framework`
65
+ * is a compile error in that one table instead of a framework the plan quietly does not serve. The
66
+ * exhaustive switch this replaced gave the same guarantee for the steps alone; the record gives it
67
+ * for the packages, the connect-step titles and the unverified-note flag in the same edit.
68
+ */
69
+ export declare function frameworkSteps(input: PlanInput): Step[];
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * `framework-adapter.test.ts` asserts the seam between the two halves.
20
20
  */
21
- import { Framework } from './detect.js';
21
+ import { Framework } from '../detect/detect.js';
22
22
  import { StepTitle } from './connect-steps.js';
23
23
  import { VITE_PLUGIN_DETAIL, astroSteps, craSteps, htmlSteps, nextSteps, nuxtSteps, reactRouterSteps, svelteKitSteps, tanstackStartSteps, viteSteps, } from './plan-framework.js';
24
24
  import { electronViteSteps } from './plan-electron-vite.js';
@@ -136,3 +136,14 @@ export const FRAMEWORK_ADAPTERS = {
136
136
  carriesOwnUnverifiedNote: false,
137
137
  },
138
138
  };
139
+ /**
140
+ * The per-framework half of the plan, looked up rather than switched.
141
+ *
142
+ * `FRAMEWORK_ADAPTERS` is a `Record<Framework, FrameworkAdapter>`, so a member added to `Framework`
143
+ * is a compile error in that one table instead of a framework the plan quietly does not serve. The
144
+ * exhaustive switch this replaced gave the same guarantee for the steps alone; the record gives it
145
+ * for the packages, the connect-step titles and the unverified-note flag in the same edit.
146
+ */
147
+ export function frameworkSteps(input) {
148
+ return FRAMEWORK_ADAPTERS[input.detection.framework].steps(input);
149
+ }
@@ -0,0 +1,23 @@
1
+ import type { PlanInput, Step } from './plan-types.js';
2
+ /**
3
+ * The two things a containerised dev server does differently, said before they go wrong.
4
+ *
5
+ * Null for every project with no container marker near it, which is almost all of them — so this
6
+ * adds nothing to the ordinary plan and does not move the install baseline.
7
+ */
8
+ export declare function containerisedStep(input: PlanInput): Step | null;
9
+ /** Windows only. The reported install never reached mcpManual because `claude mcp add` succeeded. */
10
+ export declare function windowsMcpNoteStep(input: PlanInput): Step | null;
11
+ /**
12
+ * A step that says out loud when the app isn't React. SvelteKit already carries its own unverified
13
+ * note, so it isn't doubled up here.
14
+ */
15
+ export declare function uiLibraryStep(input: PlanInput): Step[];
16
+ /**
17
+ * A WebGL subtree is pixels, and Reticle reads the DOM.
18
+ *
19
+ * react-three-fiber and its relatives render into a canvas, so the tools that find and drive
20
+ * elements have nothing to find. Disclosed rather than blocked: the rest of the app is still
21
+ * verifiable and a NOTICE says which part is not.
22
+ */
23
+ export declare function webGlCanvasStep(input: PlanInput): Step[];