@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,225 @@
1
+ /**
2
+ * Pure framework + toolchain detection for `reticle init`. No filesystem access — callers pass in
3
+ * the parsed package.json and the set of config/lock filenames present in the project root.
4
+ */
5
+ export declare const Framework: {
6
+ readonly NEXT: "next";
7
+ /**
8
+ * Nuxt owns its own Vite instance and has no `vite.config`, so it used to fall all the way through
9
+ * to HTML — and then be handed a React kit and a connect snippet guarded on
10
+ * `window.location.hostname === 'localhost'`, which cannot work in a Vue app (the guard throws
11
+ * during SSR, and never runs at all on a non-localhost dev host). Detected in its own right so it
12
+ * gets the package and the recipe that actually fit it.
13
+ */
14
+ readonly NUXT: "nuxt";
15
+ readonly VITE: "vite";
16
+ /**
17
+ * electron-vite is Vite-based but its config holds three build configs (main/preload/renderer)
18
+ * and only the renderer has a DOM. The generic Vite path patches the FIRST plugins array, which
19
+ * is main's — wiring the SDK where there is no document, and reporting success.
20
+ */
21
+ readonly ELECTRON_VITE: "electron-vite";
22
+ /**
23
+ * React Router in FRAMEWORK mode (v7's `@react-router/dev`, the successor to Remix).
24
+ *
25
+ * Vite-based, and it renders HTML through its own request handler — so the Vite plugin's
26
+ * `transformIndexHtml` injection never fires and the connect script never reaches the page. It
27
+ * used to fall through to `Framework.VITE`, where `init` wired the plugin, reported every step
28
+ * green, and produced zero sessions: confirmed by a reporter curling the SSR'd HTML (no
29
+ * `reticle-connect` anywhere) with the daemon showing no session for 20+ minutes (#678).
30
+ *
31
+ * The same class as SvelteKit and Astro below, and detected in the same place and for the same
32
+ * reason: a framework that owns its own HTML rendering is invisible to the injection hook.
33
+ *
34
+ * Library mode — `react-router` as a plain dependency with no `@react-router/dev` — is NOT this.
35
+ * That app renders through its own `index.html` and the plugin works, so it stays on the Vite
36
+ * path.
37
+ */
38
+ readonly REACT_ROUTER: "react-router";
39
+ /**
40
+ * TanStack Start SSRs `<html>` from `src/routes/__root.tsx` and never sends Vite's index.html, so
41
+ * the plugin's `transformIndexHtml` injection never fires. It used to fall through to
42
+ * `Framework.VITE`, where `init` wired the plugin, reported every step green ("also injects
43
+ * connect()"), and produced zero sessions — confirmed in the field as ~13 minutes of "still
44
+ * verifying" against a daemon showing none (#773).
45
+ *
46
+ * Keyed on `@tanstack/react-start` or the older `@tanstack/start`, never on
47
+ * `@tanstack/react-query` or `@tanstack/react-router` alone — those are libraries on a Vite SPA
48
+ * whose index.html the plugin does reach. Not `@tanstack/solid-start` either: that would install
49
+ * the React kit into a Solid app.
50
+ */
51
+ readonly TANSTACK_START: "tanstack-start";
52
+ readonly SVELTEKIT: "sveltekit";
53
+ readonly ASTRO: "astro";
54
+ /** Create React App. No config file exists, so `react-scripts` in the dependencies is the signal. */
55
+ readonly CRA: "cra";
56
+ readonly HTML: "html";
57
+ };
58
+ export type Framework = (typeof Framework)[keyof typeof Framework];
59
+ export declare const PackageManager: {
60
+ readonly PNPM: "pnpm";
61
+ readonly YARN: "yarn";
62
+ readonly BUN: "bun";
63
+ readonly NPM: "npm";
64
+ };
65
+ export type PackageManager = (typeof PackageManager)[keyof typeof PackageManager];
66
+ interface PackageJsonLike {
67
+ dependencies?: Record<string, string>;
68
+ devDependencies?: Record<string, string>;
69
+ peerDependencies?: Record<string, string>;
70
+ }
71
+ export interface DetectInput {
72
+ pkg: PackageJsonLike;
73
+ /** Basenames of config files present in the project root (e.g. 'next.config.mjs'). */
74
+ configFiles: ReadonlySet<string>;
75
+ /** Lockfile basenames present in the project root. */
76
+ lockfiles: ReadonlySet<string>;
77
+ /** Marker basenames inside `node_modules` (`.modules.yaml`, `.yarn-state.yml`, …), if installed. */
78
+ nodeModulesMarkers?: ReadonlySet<string>;
79
+ }
80
+ /**
81
+ * The UI library the app renders with. Detection used to key on "vite is in package.json" and stop
82
+ * there, so a Vue or Preact app got `@reticlehq/react` installed and an all-green report with no
83
+ * mention that the React adapter has nothing to attach to.
84
+ */
85
+ export declare const UiLibrary: {
86
+ readonly REACT: "react";
87
+ readonly PREACT: "preact";
88
+ readonly VUE: "vue";
89
+ readonly SVELTE: "svelte";
90
+ readonly UNKNOWN: "unknown";
91
+ };
92
+ export type UiLibrary = (typeof UiLibrary)[keyof typeof UiLibrary];
93
+ export interface Detection {
94
+ framework: Framework;
95
+ uiLibrary: UiLibrary;
96
+ /**
97
+ * Whether the project is TypeScript. Not cosmetic: generating a `.tsx` file into a JavaScript
98
+ * project makes Next auto-install TypeScript on the next `next dev`, which on Next 13 takes its
99
+ * require-hook down with it and the dev server never starts.
100
+ */
101
+ typescript: boolean;
102
+ reactMajor: number | undefined;
103
+ /**
104
+ * `react-scripts`' major, when the project has it. `undefined` on every other stack.
105
+ *
106
+ * Load-bearing below 5: react-scripts 4 runs webpack 4, whose parser predates optional chaining
107
+ * and logical assignment. `@reticlehq/browser` ships both untranspiled, and react-scripts excludes
108
+ * `node_modules` from Babel, so the build dies inside our `dist/` before a session can exist
109
+ * (#680). The failure has no diagnostic of its own -- the app simply does not compile.
110
+ *
111
+ * Optional so every existing fixture keeps compiling without naming it; `detect` always sets it.
112
+ */
113
+ reactScriptsMajor?: number | undefined;
114
+ /** React 19 dropped _debugSource, so it needs the build-time source-map stamp. */
115
+ needsSourceMapping: boolean;
116
+ /**
117
+ * Whether this project renders through a NON-DOM React reconciler, in which case the
118
+ * `data-reticle-source` stamp must be switched off for the whole app.
119
+ *
120
+ * React is a reconciler interface, not a renderer. A lowercase JSX tag is a host element in
121
+ * every renderer, but only in React DOM is a host element a node that takes attributes. The
122
+ * babel plugin's allowlist keeps the stamp off `<mesh>` and `<group>`, and it cannot help with
123
+ * the tags that COLLIDE: `<line>` is both SVG's and `THREE.Line`, `<audio>` is both. R3F's
124
+ * `applyProps` reads any dashed prop as a pierced property path, walks `data` -> `reticle` on a
125
+ * three.js instance that has no `data`, and throws from inside the commit phase — which unmounts
126
+ * the entire tree to a white screen, long after the app looked fine.
127
+ *
128
+ * A tag name alone cannot separate the two, and a lexical "is it under a <Canvas>" walk only sees
129
+ * the file being transformed. The manifest can: an app that depends on one of these renderers has
130
+ * the collision, so `init` writes `sourceMapping: false` rather than shipping a crash. The cost is
131
+ * source pointers on that app; the alternative cost is the app.
132
+ *
133
+ * Optional so every existing fixture keeps compiling without naming it, in the one direction a
134
+ * default here can be wrong safely: absent means an ordinary React DOM app, which is what an
135
+ * unstated fixture is. `detect` always sets it.
136
+ */
137
+ customReconciler?: boolean | undefined;
138
+ /**
139
+ * Whether this project depends on react-three-fiber (or the legacy package name). A WebGL /
140
+ * R3F subtree is a blank `<canvas>` to Reticle: DOM, state and network around it still work;
141
+ * picking and observing inside the scene do not. Optional for the same reason as
142
+ * `customReconciler` — absent means an ordinary DOM app.
143
+ */
144
+ webGlSubtree?: boolean | undefined;
145
+ packageManager: PackageManager;
146
+ }
147
+ /** What identifies one framework in a project root: a dependency name, or a config file basename. */
148
+ export interface FrameworkSignals {
149
+ /** `package.json` dependency names that name this framework outright. */
150
+ readonly deps: readonly string[];
151
+ /** Root config-file basenames that name it when the dependency is absent. */
152
+ readonly configs: readonly string[];
153
+ /**
154
+ * Root files whose PRESENCE disproves the config-file signal (the dependency signal is
155
+ * conclusive either way).
156
+ *
157
+ * SvelteKit is the only user: a plain Svelte + Vite SPA ships `svelte.config.js` too, for
158
+ * `@sveltejs/vite-plugin-svelte`'s preprocessor options and with no `kit` block. Reading the
159
+ * config file alone misclassified a real project, and `init` wrote a SvelteKit-only
160
+ * `src/hooks.client.ts` bootstrap that nothing on that project could ever import. SvelteKit
161
+ * renders through `src/app.html` and never ships a root `index.html`, where a plain Vite SPA
162
+ * always has one.
163
+ */
164
+ readonly configsUnless?: readonly string[];
165
+ }
166
+ /**
167
+ * The detection half of the per-framework table.
168
+ *
169
+ * It is a `Record<Framework, …>` for the reason `frameworkPackages` and `FRAMEWORK_ADAPTERS` are:
170
+ * a member added to `Framework` with no signals here is a compile error rather than a framework
171
+ * that is quietly never detected. The globs used to be seven loose `const` arrays read by an
172
+ * if/else chain, so a new framework with no branch was classified as whatever matched next and
173
+ * wired for THAT — the SvelteKit and React Router failure, one step upstream of the plan.
174
+ *
175
+ * Deliberately NOT merged into `FRAMEWORK_ADAPTERS`: that record holds the plan's step builders,
176
+ * which import this module transitively, so folding detection into it would make "what is this
177
+ * project" depend on "how do we wire it". Both halves are completed by the compiler; the seam is
178
+ * asserted in `framework-adapter.test.ts`.
179
+ */
180
+ export declare const FRAMEWORK_SIGNALS: Record<Framework, FrameworkSignals>;
181
+ /**
182
+ * The precedence chain, and every line of it is load-bearing.
183
+ *
184
+ * Nuxt, SvelteKit, Astro and React Router all come BEFORE Vite: each is Vite-based, so the generic
185
+ * Vite branch would match them, and each renders its own HTML — so the plugin's `transformIndexHtml`
186
+ * injection never fires and `init` would report every step green over an app that never connects.
187
+ * CRA comes AFTER Vite, because a project migrating off CRA carries both and the Vite path is the
188
+ * one that works. `Framework.HTML` is the fall-through and so is absent here.
189
+ */
190
+ export declare const DETECTION_ORDER: readonly Framework[];
191
+ /** Extract the leading major version from a semver range like "^19.0.0" or "19.1.1". */
192
+ export declare function parseMajor(range: string | undefined): number | undefined;
193
+ /**
194
+ * Whether an installed tree identifies the manager that built it. Callers use this to decide whether
195
+ * they still need weaker evidence — see `resolveLockfiles`, which stops inheriting an ancestor
196
+ * lockfile once the project's own tree can answer the question.
197
+ */
198
+ export declare function namesAPackageManager(markers: ReadonlySet<string>): boolean;
199
+ export declare function detectPackageManager(lockfiles: ReadonlySet<string>, nodeModulesMarkers: ReadonlySet<string>): PackageManager;
200
+ export declare function detect(input: DetectInput): Detection;
201
+ interface InstallCommand {
202
+ command: string;
203
+ args: string[];
204
+ }
205
+ /** Build a dev-dependency install command for one or more packages (e.g. the kit + its build plugin). */
206
+ export declare function installCommandParts(pm: PackageManager, pkgs: string | readonly string[],
207
+ /**
208
+ * Extra flags for a RETRY, never for the first attempt.
209
+ *
210
+ * Appended after the quiet flags so a caller cannot accidentally displace them, and typed
211
+ * separately from `pkgs` so a flag can never be mistaken for a package name — which is exactly
212
+ * how `npm i -D --legacy-peer-deps` would become a request to install a package called
213
+ * `--legacy-peer-deps` on a manager that does not recognise the flag.
214
+ */
215
+ extraFlags?: readonly string[]): InstallCommand;
216
+ /**
217
+ * The install command as a human reads it — and deliberately NOT `installCommandParts` joined.
218
+ *
219
+ * This string is what the plan prints and what a user retypes when running the step by hand, so it
220
+ * must not carry the flags we add only to quieten our own child process. Built from `INSTALL_ARGS`
221
+ * directly for that reason; routing it through the parts would put `--no-audit --no-fund` in front of
222
+ * every reader and teach them our noise-suppression as if it were part of installing Reticle.
223
+ */
224
+ export declare function installCommand(pm: PackageManager, pkgs: string | readonly string[]): string;
225
+ export {};
package/dist/detect.js ADDED
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Pure framework + toolchain detection for `reticle init`. No filesystem access — callers pass in
3
+ * the parsed package.json and the set of config/lock filenames present in the project root.
4
+ */
5
+ export const Framework = {
6
+ NEXT: 'next',
7
+ /**
8
+ * Nuxt owns its own Vite instance and has no `vite.config`, so it used to fall all the way through
9
+ * to HTML — and then be handed a React kit and a connect snippet guarded on
10
+ * `window.location.hostname === 'localhost'`, which cannot work in a Vue app (the guard throws
11
+ * during SSR, and never runs at all on a non-localhost dev host). Detected in its own right so it
12
+ * gets the package and the recipe that actually fit it.
13
+ */
14
+ NUXT: 'nuxt',
15
+ VITE: 'vite',
16
+ /**
17
+ * electron-vite is Vite-based but its config holds three build configs (main/preload/renderer)
18
+ * and only the renderer has a DOM. The generic Vite path patches the FIRST plugins array, which
19
+ * is main's — wiring the SDK where there is no document, and reporting success.
20
+ */
21
+ ELECTRON_VITE: 'electron-vite',
22
+ /**
23
+ * React Router in FRAMEWORK mode (v7's `@react-router/dev`, the successor to Remix).
24
+ *
25
+ * Vite-based, and it renders HTML through its own request handler — so the Vite plugin's
26
+ * `transformIndexHtml` injection never fires and the connect script never reaches the page. It
27
+ * used to fall through to `Framework.VITE`, where `init` wired the plugin, reported every step
28
+ * green, and produced zero sessions: confirmed by a reporter curling the SSR'd HTML (no
29
+ * `reticle-connect` anywhere) with the daemon showing no session for 20+ minutes (#678).
30
+ *
31
+ * The same class as SvelteKit and Astro below, and detected in the same place and for the same
32
+ * reason: a framework that owns its own HTML rendering is invisible to the injection hook.
33
+ *
34
+ * Library mode — `react-router` as a plain dependency with no `@react-router/dev` — is NOT this.
35
+ * That app renders through its own `index.html` and the plugin works, so it stays on the Vite
36
+ * path.
37
+ */
38
+ REACT_ROUTER: 'react-router',
39
+ /**
40
+ * TanStack Start SSRs `<html>` from `src/routes/__root.tsx` and never sends Vite's index.html, so
41
+ * the plugin's `transformIndexHtml` injection never fires. It used to fall through to
42
+ * `Framework.VITE`, where `init` wired the plugin, reported every step green ("also injects
43
+ * connect()"), and produced zero sessions — confirmed in the field as ~13 minutes of "still
44
+ * verifying" against a daemon showing none (#773).
45
+ *
46
+ * Keyed on `@tanstack/react-start` or the older `@tanstack/start`, never on
47
+ * `@tanstack/react-query` or `@tanstack/react-router` alone — those are libraries on a Vite SPA
48
+ * whose index.html the plugin does reach. Not `@tanstack/solid-start` either: that would install
49
+ * the React kit into a Solid app.
50
+ */
51
+ TANSTACK_START: 'tanstack-start',
52
+ SVELTEKIT: 'sveltekit',
53
+ ASTRO: 'astro',
54
+ /** Create React App. No config file exists, so `react-scripts` in the dependencies is the signal. */
55
+ CRA: 'cra',
56
+ HTML: 'html',
57
+ };
58
+ export const PackageManager = {
59
+ PNPM: 'pnpm',
60
+ YARN: 'yarn',
61
+ BUN: 'bun',
62
+ NPM: 'npm',
63
+ };
64
+ /**
65
+ * The UI library the app renders with. Detection used to key on "vite is in package.json" and stop
66
+ * there, so a Vue or Preact app got `@reticlehq/react` installed and an all-green report with no
67
+ * mention that the React adapter has nothing to attach to.
68
+ */
69
+ export const UiLibrary = {
70
+ REACT: 'react',
71
+ PREACT: 'preact',
72
+ VUE: 'vue',
73
+ SVELTE: 'svelte',
74
+ UNKNOWN: 'unknown',
75
+ };
76
+ const NEXT_CONFIGS = ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.cjs'];
77
+ const VITE_CONFIGS = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs', 'vite.config.mts'];
78
+ const ELECTRON_VITE_CONFIGS = [
79
+ 'electron.vite.config.ts',
80
+ 'electron.vite.config.js',
81
+ 'electron.vite.config.mjs',
82
+ 'electron.vite.config.mts',
83
+ ];
84
+ const SVELTE_CONFIGS = ['svelte.config.js', 'svelte.config.ts', 'svelte.config.mjs'];
85
+ const REACT_ROUTER_CONFIGS = [
86
+ 'react-router.config.ts',
87
+ 'react-router.config.js',
88
+ 'react-router.config.mjs',
89
+ ];
90
+ const NUXT_CONFIGS = ['nuxt.config.ts', 'nuxt.config.js', 'nuxt.config.mjs'];
91
+ const ASTRO_CONFIGS = [
92
+ 'astro.config.mjs',
93
+ 'astro.config.js',
94
+ 'astro.config.ts',
95
+ 'astro.config.cjs',
96
+ ];
97
+ function depVersion(pkg, name) {
98
+ return pkg.dependencies?.[name] ?? pkg.devDependencies?.[name] ?? pkg.peerDependencies?.[name];
99
+ }
100
+ /**
101
+ * React renderers whose host instances are not DOM nodes. Presence of any one of them means a
102
+ * lowercase JSX tag in this project may not be an element. See `Detection.customReconciler`.
103
+ */
104
+ const CUSTOM_RECONCILER_DEPS = [
105
+ '@react-three/fiber',
106
+ 'react-three-fiber',
107
+ '@react-pdf/renderer',
108
+ 'ink',
109
+ 'react-native',
110
+ ];
111
+ /** Packages whose presence means the product's main surface is a WebGL canvas, not the DOM. */
112
+ const WEBGL_SUBTREE_DEPS = ['@react-three/fiber', 'react-three-fiber'];
113
+ function hasAnyConfig(files, candidates) {
114
+ return candidates.some((c) => files.has(c));
115
+ }
116
+ /** The root document a plain Vite SPA serves — and a framework that SSRs its own HTML never has. */
117
+ const VITE_INDEX_HTML = 'index.html';
118
+ /**
119
+ * The detection half of the per-framework table.
120
+ *
121
+ * It is a `Record<Framework, …>` for the reason `frameworkPackages` and `FRAMEWORK_ADAPTERS` are:
122
+ * a member added to `Framework` with no signals here is a compile error rather than a framework
123
+ * that is quietly never detected. The globs used to be seven loose `const` arrays read by an
124
+ * if/else chain, so a new framework with no branch was classified as whatever matched next and
125
+ * wired for THAT — the SvelteKit and React Router failure, one step upstream of the plan.
126
+ *
127
+ * Deliberately NOT merged into `FRAMEWORK_ADAPTERS`: that record holds the plan's step builders,
128
+ * which import this module transitively, so folding detection into it would make "what is this
129
+ * project" depend on "how do we wire it". Both halves are completed by the compiler; the seam is
130
+ * asserted in `framework-adapter.test.ts`.
131
+ */
132
+ export const FRAMEWORK_SIGNALS = {
133
+ [Framework.NEXT]: { deps: ['next'], configs: NEXT_CONFIGS },
134
+ [Framework.NUXT]: { deps: ['nuxt'], configs: NUXT_CONFIGS },
135
+ [Framework.SVELTEKIT]: {
136
+ deps: ['@sveltejs/kit'],
137
+ configs: SVELTE_CONFIGS,
138
+ configsUnless: [VITE_INDEX_HTML],
139
+ },
140
+ [Framework.ASTRO]: { deps: ['astro'], configs: ASTRO_CONFIGS },
141
+ /**
142
+ * `@react-router/dev` or a `react-router.config.*`, never `react-router` itself — library mode is
143
+ * a plain Vite app whose index.html the plugin does reach.
144
+ */
145
+ [Framework.REACT_ROUTER]: { deps: ['@react-router/dev'], configs: REACT_ROUTER_CONFIGS },
146
+ /**
147
+ * electron-vite's config holds three build configs (main/preload/renderer) and only the renderer
148
+ * has a DOM, so it must never fall through to the generic Vite path.
149
+ */
150
+ [Framework.ELECTRON_VITE]: { deps: ['electron-vite'], configs: ELECTRON_VITE_CONFIGS },
151
+ /**
152
+ * The Start packages, never Query or Router alone — those stay on the Vite path. Start has no
153
+ * config file of its own to key on.
154
+ */
155
+ [Framework.TANSTACK_START]: {
156
+ deps: ['@tanstack/react-start', '@tanstack/start'],
157
+ configs: [],
158
+ },
159
+ [Framework.VITE]: { deps: ['vite'], configs: VITE_CONFIGS },
160
+ /** CRA has no config file at all, so the dependency is the only signal. */
161
+ [Framework.CRA]: { deps: ['react-scripts'], configs: [] },
162
+ /** Nothing identifies plain HTML; it is where the chain below ends. */
163
+ [Framework.HTML]: { deps: [], configs: [] },
164
+ };
165
+ /**
166
+ * The precedence chain, and every line of it is load-bearing.
167
+ *
168
+ * Nuxt, SvelteKit, Astro and React Router all come BEFORE Vite: each is Vite-based, so the generic
169
+ * Vite branch would match them, and each renders its own HTML — so the plugin's `transformIndexHtml`
170
+ * injection never fires and `init` would report every step green over an app that never connects.
171
+ * CRA comes AFTER Vite, because a project migrating off CRA carries both and the Vite path is the
172
+ * one that works. `Framework.HTML` is the fall-through and so is absent here.
173
+ */
174
+ export const DETECTION_ORDER = [
175
+ Framework.NEXT,
176
+ Framework.NUXT,
177
+ Framework.SVELTEKIT,
178
+ Framework.ASTRO,
179
+ Framework.ELECTRON_VITE,
180
+ Framework.REACT_ROUTER,
181
+ Framework.TANSTACK_START,
182
+ Framework.VITE,
183
+ Framework.CRA,
184
+ ];
185
+ /** Extract the leading major version from a semver range like "^19.0.0" or "19.1.1". */
186
+ export function parseMajor(range) {
187
+ if (range === undefined)
188
+ return undefined;
189
+ const match = range.match(/(\d+)/);
190
+ if (null === match || match[1] === undefined)
191
+ return undefined;
192
+ const major = parseInt(match[1], 10);
193
+ return isNaN(major) ? undefined : major;
194
+ }
195
+ /**
196
+ * Markers each package manager leaves INSIDE `node_modules`. An installed tree is the strongest
197
+ * evidence there is — stronger than a lockfile, which may simply not be committed.
198
+ *
199
+ * Without this, a pnpm-installed project with no committed lockfile was read as npm, and `npm i -D`
200
+ * then died on pnpm's symlink layout with `Cannot read properties of null (reading 'matches')`. Worse,
201
+ * it left the package present in `node_modules` but absent from `package.json`, so every later run
202
+ * reported the same failure — a setup that could not be retried into working.
203
+ */
204
+ const NODE_MODULES_MARKERS = [
205
+ ['.modules.yaml', PackageManager.PNPM],
206
+ ['.yarn-state.yml', PackageManager.YARN],
207
+ ['.package-lock.json', PackageManager.NPM],
208
+ ];
209
+ /** Marker basenames present inside the project's `node_modules`, if it has one. */
210
+ function packageManagerFromNodeModules(markers) {
211
+ for (const [name, pm] of NODE_MODULES_MARKERS)
212
+ if (markers.has(name))
213
+ return pm;
214
+ return undefined;
215
+ }
216
+ /**
217
+ * Whether an installed tree identifies the manager that built it. Callers use this to decide whether
218
+ * they still need weaker evidence — see `resolveLockfiles`, which stops inheriting an ancestor
219
+ * lockfile once the project's own tree can answer the question.
220
+ */
221
+ export function namesAPackageManager(markers) {
222
+ return packageManagerFromNodeModules(markers) !== undefined;
223
+ }
224
+ export function detectPackageManager(lockfiles, nodeModulesMarkers) {
225
+ if (lockfiles.has('pnpm-lock.yaml'))
226
+ return PackageManager.PNPM;
227
+ if (lockfiles.has('yarn.lock'))
228
+ return PackageManager.YARN;
229
+ if (lockfiles.has('bun.lockb') || lockfiles.has('bun.lock'))
230
+ return PackageManager.BUN;
231
+ // No lockfile is not the same as "npm". An already-installed tree says which manager built it.
232
+ return packageManagerFromNodeModules(nodeModulesMarkers) ?? PackageManager.NPM;
233
+ }
234
+ function detectFramework(input) {
235
+ for (const framework of DETECTION_ORDER) {
236
+ const signals = FRAMEWORK_SIGNALS[framework];
237
+ if (signals.deps.some((d) => depVersion(input.pkg, d) !== undefined))
238
+ return framework;
239
+ if (hasAnyConfig(input.configFiles, signals.configs) &&
240
+ !hasAnyConfig(input.configFiles, signals.configsUnless ?? [])) {
241
+ return framework;
242
+ }
243
+ }
244
+ return Framework.HTML;
245
+ }
246
+ /**
247
+ * React first: a Preact app using preact/compat aliases React, and Next/Remix apps list both. The
248
+ * order is the precedence — whichever the app actually renders through is the one it depends on
249
+ * directly.
250
+ */
251
+ function detectUiLibrary(pkg) {
252
+ if (depVersion(pkg, 'react') !== undefined)
253
+ return UiLibrary.REACT;
254
+ if (depVersion(pkg, 'preact') !== undefined)
255
+ return UiLibrary.PREACT;
256
+ if (depVersion(pkg, 'vue') !== undefined)
257
+ return UiLibrary.VUE;
258
+ if (depVersion(pkg, 'svelte') !== undefined)
259
+ return UiLibrary.SVELTE;
260
+ return UiLibrary.UNKNOWN;
261
+ }
262
+ const TS_CONFIGS = ['tsconfig.json'];
263
+ export function detect(input) {
264
+ const reactMajor = parseMajor(depVersion(input.pkg, 'react'));
265
+ return {
266
+ framework: detectFramework(input),
267
+ reactScriptsMajor: parseMajor(depVersion(input.pkg, 'react-scripts')),
268
+ uiLibrary: detectUiLibrary(input.pkg),
269
+ typescript: hasAnyConfig(input.configFiles, TS_CONFIGS) ||
270
+ depVersion(input.pkg, 'typescript') !== undefined,
271
+ reactMajor,
272
+ needsSourceMapping: reactMajor !== undefined && reactMajor >= 19,
273
+ customReconciler: CUSTOM_RECONCILER_DEPS.some((name) => depVersion(input.pkg, name) !== undefined),
274
+ webGlSubtree: WEBGL_SUBTREE_DEPS.some((name) => depVersion(input.pkg, name) !== undefined),
275
+ packageManager: detectPackageManager(input.lockfiles, input.nodeModulesMarkers ?? new Set()),
276
+ };
277
+ }
278
+ const INSTALL_ARGS = {
279
+ [PackageManager.PNPM]: ['add', '-D'],
280
+ [PackageManager.YARN]: ['add', '-D'],
281
+ [PackageManager.BUN]: ['add', '-d'],
282
+ [PackageManager.NPM]: ['i', '-D'],
283
+ };
284
+ /**
285
+ * Flags added only to the install we RUN, never to the one we print.
286
+ *
287
+ * The install is a child process whose output lands above ours. Measured on a real install: the run
288
+ * opened with "added 602 packages", a funding notice, and "14 vulnerabilities (7 moderate, 7 high)"
289
+ * with `npm audit fix` advice, before one line of Reticle output. A user's first impression of a
290
+ * verification tool was a wall of somebody else's security warnings, at exactly the moment they are
291
+ * deciding whether this tool is careful — and that audit summary describes their existing dependency
292
+ * tree, which our two dev packages neither caused nor can fix.
293
+ *
294
+ * Kept off `installCommand`, which is the string shown in the plan and the one a user copies when
295
+ * running it by hand: nobody should be taught to type our noise-suppression flags.
296
+ *
297
+ * Quieted, never silenced — the exit code and stderr are untouched, so a real failure of the step
298
+ * everything downstream depends on stays as loud as it was.
299
+ *
300
+ * npm only: pnpm, yarn and bun reject these, and an unknown flag would turn a working install into a
301
+ * hard failure, which is the opposite of the problem being fixed.
302
+ */
303
+ const QUIET_INSTALL_ARGS = {
304
+ [PackageManager.NPM]: ['--no-audit', '--no-fund'],
305
+ };
306
+ /** Build a dev-dependency install command for one or more packages (e.g. the kit + its build plugin). */
307
+ export function installCommandParts(pm, pkgs,
308
+ /**
309
+ * Extra flags for a RETRY, never for the first attempt.
310
+ *
311
+ * Appended after the quiet flags so a caller cannot accidentally displace them, and typed
312
+ * separately from `pkgs` so a flag can never be mistaken for a package name — which is exactly
313
+ * how `npm i -D --legacy-peer-deps` would become a request to install a package called
314
+ * `--legacy-peer-deps` on a manager that does not recognise the flag.
315
+ */
316
+ extraFlags = []) {
317
+ const list = 'string' === typeof pkgs ? [pkgs] : pkgs;
318
+ return {
319
+ command: pm,
320
+ args: [...INSTALL_ARGS[pm], ...list, ...(QUIET_INSTALL_ARGS[pm] ?? []), ...extraFlags],
321
+ };
322
+ }
323
+ /**
324
+ * The install command as a human reads it — and deliberately NOT `installCommandParts` joined.
325
+ *
326
+ * This string is what the plan prints and what a user retypes when running the step by hand, so it
327
+ * must not carry the flags we add only to quieten our own child process. Built from `INSTALL_ARGS`
328
+ * directly for that reason; routing it through the parts would put `--no-audit --no-fund` in front of
329
+ * every reader and teach them our noise-suppression as if it were part of installing Reticle.
330
+ */
331
+ export function installCommand(pm, pkgs) {
332
+ const list = 'string' === typeof pkgs ? [pkgs] : pkgs;
333
+ return `${pm} ${[...INSTALL_ARGS[pm], ...list].join(' ')}`;
334
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Which command starts this project, and should we start it ourselves?
3
+ *
4
+ * `init` finishes by telling the human to start or restart their dev server, in a closing
5
+ * paragraph, in a terminal that is about to be cleared by the client restart it also asks for.
6
+ * That is the second unmarked hand-off in the install, after the capabilities file, and it lands
7
+ * on the same surface: prose nobody re-reads.
8
+ *
9
+ * Every fact needed to remove it is already in the project. `package.json` names the script; the
10
+ * framework tells us the conventional port; a probe tells us whether something already answers
11
+ * there. Nothing here needs a human.
12
+ *
13
+ * Kept as a PURE decision, separate from spawning, because the interesting part is the judgement —
14
+ * which script, and whether to touch anything at all — and that is what wants testing. Spawning a
15
+ * process is not.
16
+ */
17
+ /** Script names a JS project uses to run itself, in the order we would pick them. */
18
+ export declare const DEV_SCRIPT_NAMES: readonly ["dev", "start", "serve"];
19
+ export declare const DevScriptChoice: {
20
+ /** Something already answers on the port. Use it; never replace a server the user is running. */
21
+ readonly ALREADY_SERVING: "already-serving";
22
+ /** We know the command and nothing is listening. Start it. */
23
+ readonly START: "start";
24
+ /** No script we recognise. Say so rather than guess a command. */
25
+ readonly NO_SCRIPT: "no-script";
26
+ };
27
+ export type DevScriptChoice = (typeof DevScriptChoice)[keyof typeof DevScriptChoice];
28
+ interface DevScriptPlan {
29
+ choice: DevScriptChoice;
30
+ /** The npm script name, when there is one. */
31
+ script?: string;
32
+ /** What a human would type, for the line we print. */
33
+ command?: string;
34
+ }
35
+ /**
36
+ * Pick the script and decide whether to run it.
37
+ *
38
+ * `serving` is the probe result, passed in rather than measured here so the decision stays pure.
39
+ * When something is already answering we do not care which script exists: the rule is never to
40
+ * start a second server, because the one that is running may be the user's, with their state in it.
41
+ */
42
+ export declare function planDevScript(scripts: Readonly<Record<string, string>>, packageManager: string, serving: boolean): DevScriptPlan;
43
+ /**
44
+ * The command to print for this project, straight from its `package.json`.
45
+ *
46
+ * Lives here rather than in `run.ts` because it is the same judgement as `planDevScript` with the
47
+ * parsing attached, and `run.ts` is at its cohesion limit. Returns undefined for anything it cannot
48
+ * read: this phrasing decides nothing, so it must never be able to break an install.
49
+ */
50
+ export declare function devCommandFrom(pkg: unknown, packageManager: string): string | undefined;
51
+ export {};