@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.
- package/LICENSE +105 -0
- package/NOTICE +5 -0
- package/README.md +29 -0
- package/dist/agent-rules.d.ts +116 -0
- package/dist/agent-rules.js +279 -0
- package/dist/app-choice.d.ts +29 -0
- package/dist/app-choice.js +39 -0
- package/dist/astro-host.d.ts +21 -0
- package/dist/astro-host.js +27 -0
- package/dist/astro-patch.d.ts +33 -0
- package/dist/astro-patch.js +103 -0
- package/dist/brace-scan.d.ts +24 -0
- package/dist/brace-scan.js +46 -0
- package/dist/capabilities.d.ts +40 -0
- package/dist/capabilities.js +167 -0
- package/dist/closing-hint.d.ts +35 -0
- package/dist/closing-hint.js +122 -0
- package/dist/codex-toml.d.ts +10 -0
- package/dist/codex-toml.js +144 -0
- package/dist/connect-steps.d.ts +55 -0
- package/dist/connect-steps.js +92 -0
- package/dist/cra.d.ts +82 -0
- package/dist/cra.js +183 -0
- package/dist/csp-check.d.ts +49 -0
- package/dist/csp-check.js +168 -0
- package/dist/csp-doctor.d.ts +32 -0
- package/dist/csp-doctor.js +62 -0
- package/dist/desktop-doctor.d.ts +46 -0
- package/dist/desktop-doctor.js +201 -0
- package/dist/detect.d.ts +225 -0
- package/dist/detect.js +334 -0
- package/dist/dev-script.d.ts +51 -0
- package/dist/dev-script.js +66 -0
- package/dist/dev-server-ports.d.ts +20 -0
- package/dist/dev-server-ports.js +45 -0
- package/dist/electron-patch.d.ts +11 -0
- package/dist/electron-patch.js +70 -0
- package/dist/electron-vite-patch.d.ts +12 -0
- package/dist/electron-vite-patch.js +100 -0
- package/dist/existing-config.d.ts +31 -0
- package/dist/existing-config.js +63 -0
- package/dist/format-generated.d.ts +18 -0
- package/dist/format-generated.js +48 -0
- package/dist/framework-adapter.d.ts +60 -0
- package/dist/framework-adapter.js +138 -0
- package/dist/host.d.ts +41 -0
- package/dist/host.js +21 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +25 -0
- package/dist/init-failure.d.ts +19 -0
- package/dist/init-failure.js +18 -0
- package/dist/init-opt-out.d.ts +17 -0
- package/dist/init-opt-out.js +30 -0
- package/dist/install-hint.d.ts +9 -0
- package/dist/install-hint.js +41 -0
- package/dist/install-retries.d.ts +22 -0
- package/dist/install-retries.js +65 -0
- package/dist/install-source-config.d.ts +18 -0
- package/dist/install-source-config.js +34 -0
- package/dist/lockfiles.d.ts +14 -0
- package/dist/lockfiles.js +49 -0
- package/dist/mcp-clients.d.ts +94 -0
- package/dist/mcp-clients.js +327 -0
- package/dist/mcp-registered.d.ts +12 -0
- package/dist/mcp-registered.js +14 -0
- package/dist/mcp.d.ts +89 -0
- package/dist/mcp.js +129 -0
- package/dist/next-patch.d.ts +49 -0
- package/dist/next-patch.js +237 -0
- package/dist/node-io.d.ts +22 -0
- package/dist/node-io.js +150 -0
- package/dist/non-js-project.d.ts +44 -0
- package/dist/non-js-project.js +137 -0
- package/dist/nuxt-patch.d.ts +18 -0
- package/dist/nuxt-patch.js +33 -0
- package/dist/patch-kind.d.ts +20 -0
- package/dist/patch-kind.js +10 -0
- package/dist/plan-electron-vite.d.ts +16 -0
- package/dist/plan-electron-vite.js +72 -0
- package/dist/plan-framework.d.ts +122 -0
- package/dist/plan-framework.js +773 -0
- package/dist/plan.d.ts +312 -0
- package/dist/plan.js +650 -0
- package/dist/platform.d.ts +16 -0
- package/dist/platform.js +16 -0
- package/dist/preflight.d.ts +30 -0
- package/dist/preflight.js +52 -0
- package/dist/project-id.d.ts +12 -0
- package/dist/project-id.js +25 -0
- package/dist/refresh-rules.d.ts +40 -0
- package/dist/refresh-rules.js +51 -0
- package/dist/remember-project.d.ts +24 -0
- package/dist/remember-project.js +40 -0
- package/dist/run.d.ts +122 -0
- package/dist/run.js +762 -0
- package/dist/slash-command.d.ts +27 -0
- package/dist/slash-command.js +132 -0
- package/dist/snippets.d.ts +267 -0
- package/dist/snippets.js +898 -0
- package/dist/tanstack-start.d.ts +18 -0
- package/dist/tanstack-start.js +57 -0
- package/dist/version.d.ts +18 -0
- package/dist/version.js +23 -0
- package/dist/vite-config.d.ts +18 -0
- package/dist/vite-config.js +111 -0
- package/dist/vite-owning-config.d.ts +25 -0
- package/dist/vite-owning-config.js +219 -0
- package/dist/windows-quote.d.ts +38 -0
- package/dist/windows-quote.js +70 -0
- package/dist/workspace-apps.d.ts +40 -0
- package/dist/workspace-apps.js +188 -0
- package/dist/workspace-redirect.d.ts +22 -0
- package/dist/workspace-redirect.js +97 -0
- package/package.json +53 -0
package/dist/detect.d.ts
ADDED
|
@@ -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 {};
|