@reticlehq/init 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/detect/capabilities.d.ts +4 -1
- package/dist/detect/capabilities.js +45 -9
- package/dist/detect/detect.d.ts +58 -4
- package/dist/detect/detect.js +117 -7
- package/dist/detect/dev-script.d.ts +26 -2
- package/dist/detect/dev-script.js +80 -7
- package/dist/detect/non-js-project.d.ts +1 -1
- package/dist/detect/non-js-project.js +6 -1
- package/dist/detect/workspace-apps.d.ts +21 -0
- package/dist/detect/workspace-apps.js +111 -12
- package/dist/detect/workspace-redirect.d.ts +2 -1
- package/dist/detect/workspace-redirect.js +28 -2
- package/dist/diagnose/containerised-dev-server.d.ts +12 -4
- package/dist/diagnose/containerised-dev-server.js +36 -4
- package/dist/diagnose/csp-check.d.ts +12 -6
- package/dist/diagnose/csp-check.js +61 -13
- package/dist/diagnose/csp-doctor.d.ts +33 -2
- package/dist/diagnose/csp-doctor.js +103 -6
- package/dist/diagnose/csp-step.d.ts +27 -0
- package/dist/diagnose/csp-step.js +86 -0
- package/dist/diagnose/desktop-doctor.d.ts +1 -1
- package/dist/diagnose/desktop-doctor.js +8 -2
- package/dist/diagnose/install-hint.d.ts +9 -1
- package/dist/diagnose/install-hint.js +17 -7
- package/dist/diagnose/install-retries.d.ts +18 -2
- package/dist/diagnose/install-retries.js +50 -3
- package/dist/diagnose/terminal-wrap.d.ts +18 -0
- package/dist/diagnose/terminal-wrap.js +63 -0
- package/dist/host.d.ts +6 -0
- package/dist/host.js +3 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +8 -3
- package/dist/no-package-json.d.ts +10 -0
- package/dist/no-package-json.js +121 -0
- package/dist/node-io.d.ts +9 -5
- package/dist/node-io.js +25 -7
- package/dist/patch/angular.d.ts +68 -0
- package/dist/patch/angular.js +216 -0
- package/dist/patch/astro-patch.d.ts +48 -13
- package/dist/patch/astro-patch.js +151 -43
- package/dist/patch/cra.d.ts +0 -2
- package/dist/patch/cra.js +0 -2
- package/dist/patch/electron-patch.d.ts +10 -1
- package/dist/patch/electron-patch.js +79 -27
- package/dist/patch/electron-vite-patch.js +1 -1
- package/dist/patch/nuxt-patch.d.ts +1 -1
- package/dist/patch/nuxt-patch.js +2 -2
- package/dist/patch/nuxt-snippets.d.ts +44 -0
- package/dist/patch/nuxt-snippets.js +135 -0
- package/dist/patch/remix.d.ts +24 -0
- package/dist/patch/remix.js +59 -0
- package/dist/patch/snippets.d.ts +58 -57
- package/dist/patch/snippets.js +181 -202
- package/dist/patch/static-page.d.ts +39 -0
- package/dist/patch/static-page.js +102 -0
- package/dist/patch/tanstack-start.d.ts +36 -8
- package/dist/patch/tanstack-start.js +118 -29
- package/dist/patch/vite-config.d.ts +5 -0
- package/dist/patch/vite-config.js +8 -0
- package/dist/patch/vite-env-types.d.ts +9 -0
- package/dist/patch/vite-env-types.js +19 -0
- package/dist/patch/vite-owning-config.d.ts +47 -0
- package/dist/patch/vite-owning-config.js +104 -19
- package/dist/plan/connect-steps.d.ts +10 -3
- package/dist/plan/connect-steps.js +22 -3
- package/dist/plan/framework-adapter.d.ts +5 -6
- package/dist/plan/framework-adapter.js +105 -11
- package/dist/plan/notices.js +4 -3
- package/dist/plan/plan-angular.d.ts +6 -0
- package/dist/plan/plan-angular.js +89 -0
- package/dist/plan/plan-electron-vite.d.ts +6 -3
- package/dist/plan/plan-electron-vite.js +39 -8
- package/dist/plan/plan-framework.d.ts +9 -47
- package/dist/plan/plan-framework.js +114 -323
- package/dist/plan/plan-types.d.ts +47 -2
- package/dist/plan/plan-vite.d.ts +67 -0
- package/dist/plan/plan-vite.js +256 -0
- package/dist/plan/plan.js +155 -69
- package/dist/plan/port-steps.d.ts +45 -0
- package/dist/plan/port-steps.js +119 -0
- package/dist/plan/preflight.d.ts +17 -3
- package/dist/plan/preflight.js +53 -23
- package/dist/plan/stop-hook-step.d.ts +4 -0
- package/dist/plan/stop-hook-step.js +49 -0
- package/dist/project/agent-rules.js +2 -0
- package/dist/register/mcp-clients.d.ts +13 -0
- package/dist/register/mcp-clients.js +22 -0
- package/dist/register/mcp.d.ts +26 -5
- package/dist/register/mcp.js +64 -8
- package/dist/run-types.d.ts +10 -0
- package/dist/run.js +132 -78
- package/package.json +3 -4
- package/NOTICE +0 -5
|
@@ -17,7 +17,10 @@ export declare function scanTestids(sources: readonly string[]): string[];
|
|
|
17
17
|
* The store libraries this app depends on AND the running app cannot reveal on its own, as
|
|
18
18
|
* ready-to-uncomment registration lines.
|
|
19
19
|
*/
|
|
20
|
-
export declare function storeHints(deps: ReadonlySet<string
|
|
20
|
+
export declare function storeHints(deps: ReadonlySet<string>, files?: readonly {
|
|
21
|
+
path: string;
|
|
22
|
+
source: string;
|
|
23
|
+
}[]): string[];
|
|
21
24
|
/** A store instance we found in the app's own source, with everything needed to import and register it. */
|
|
22
25
|
export interface FoundStore {
|
|
23
26
|
/** The name it registers under — what `reticle_state` keys its output by. */
|
|
@@ -55,16 +55,42 @@ const STORE_LIBRARIES = [
|
|
|
55
55
|
['@tanstack/react-query', "registerStore('queries', tanstackQueryStore(queryClient))"],
|
|
56
56
|
[
|
|
57
57
|
'zustand',
|
|
58
|
-
"registerStore('app',
|
|
58
|
+
"registerStore('app', <yourStore>) // pass the store itself, not () => store.getState()",
|
|
59
59
|
],
|
|
60
60
|
['@reduxjs/toolkit', "registerStore('app', store)"],
|
|
61
61
|
['redux', "registerStore('app', store)"],
|
|
62
|
-
['jotai', "registerStore('app', jotaiStore(getDefaultStore(), {
|
|
63
|
-
['valtio', "registerStore('app', valtioStore(
|
|
64
|
-
['mobx', "registerStore('app', mobxStore(
|
|
65
|
-
['xstate', "registerStore('machine', xstateStore(
|
|
66
|
-
['pinia', "registerStore('
|
|
67
|
-
|
|
62
|
+
['jotai', "registerStore('app', jotaiStore(getDefaultStore(), { <yourAtoms> }))"],
|
|
63
|
+
['valtio', "registerStore('app', valtioStore(<yourProxyState>))"],
|
|
64
|
+
['mobx', "registerStore('app', mobxStore(<yourObservable>))"],
|
|
65
|
+
['xstate', "registerStore('machine', xstateStore(<yourActor>))"],
|
|
66
|
+
['pinia', "registerStore('<key>', piniaStore(<useYourStore>()))"],
|
|
67
|
+
];
|
|
68
|
+
/**
|
|
69
|
+
* The adapter-wrapped libraries whose store the SOURCE can name for us, and how.
|
|
70
|
+
*
|
|
71
|
+
* Every hint above used to carry a made-up identifier, and two of them were wrong on every app of
|
|
72
|
+
* their kind: each Svelte app was told `svelteStore(cartStore)` and a create-vue app defining
|
|
73
|
+
* `useCounterStore` was told `piniaStore(useCartStore())`. An agent that follows a hint imports what
|
|
74
|
+
* it names. So where the declaration has a shape we can read, the hint names what is actually there;
|
|
75
|
+
* where it does not, the hint says what to fill in with a placeholder instead of a guess.
|
|
76
|
+
*
|
|
77
|
+
* Svelte has no generic hint at all: every Svelte app depends on `svelte`, so the dependency says
|
|
78
|
+
* nothing about whether it has a store, and a hint on it fired the "finish the capabilities file"
|
|
79
|
+
* notice on every Svelte app there is.
|
|
80
|
+
*/
|
|
81
|
+
const SOURCE_NAMED_STORES = [
|
|
82
|
+
[
|
|
83
|
+
'pinia',
|
|
84
|
+
/export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*defineStore\s*\(\s*['"`]([^'"`]+)['"`]/g,
|
|
85
|
+
(m) => m[1] === undefined || m[2] === undefined
|
|
86
|
+
? undefined
|
|
87
|
+
: `registerStore('${m[2]}', piniaStore(${m[1]}()))`,
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
'svelte',
|
|
91
|
+
/export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:writable|readable|derived)\s*[<(]/g,
|
|
92
|
+
(m) => (m[1] === undefined ? undefined : `registerStore('${m[1]}', svelteStore(${m[1]}))`),
|
|
93
|
+
],
|
|
68
94
|
];
|
|
69
95
|
/**
|
|
70
96
|
* Libraries that hand their store to React through a context provider, and therefore need no hint.
|
|
@@ -87,8 +113,18 @@ const AUTO_DISCOVERED_DEPS = new Set([
|
|
|
87
113
|
* The store libraries this app depends on AND the running app cannot reveal on its own, as
|
|
88
114
|
* ready-to-uncomment registration lines.
|
|
89
115
|
*/
|
|
90
|
-
export function storeHints(deps) {
|
|
91
|
-
|
|
116
|
+
export function storeHints(deps, files = []) {
|
|
117
|
+
const named = new Map();
|
|
118
|
+
for (const [dep, declaration, hint] of SOURCE_NAMED_STORES) {
|
|
119
|
+
if (!deps.has(dep))
|
|
120
|
+
continue;
|
|
121
|
+
const found = files.flatMap(({ source }) => [...source.matchAll(declaration)].flatMap((m) => hint(m) ?? []));
|
|
122
|
+
if (found.length > 0)
|
|
123
|
+
named.set(dep, found);
|
|
124
|
+
}
|
|
125
|
+
return STORE_LIBRARIES.filter(([dep]) => deps.has(dep) && !AUTO_DISCOVERED_DEPS.has(dep))
|
|
126
|
+
.flatMap(([dep, hint]) => named.get(dep) ?? [hint])
|
|
127
|
+
.concat(named.get('svelte') ?? []);
|
|
92
128
|
}
|
|
93
129
|
/**
|
|
94
130
|
* Store factories whose EXPORTED result is the store instance itself — the form `registerStore`
|
package/dist/detect/detect.d.ts
CHANGED
|
@@ -19,6 +19,13 @@ export declare const Framework: {
|
|
|
19
19
|
* is main's — wiring the SDK where there is no document, and reporting success.
|
|
20
20
|
*/
|
|
21
21
|
readonly ELECTRON_VITE: "electron-vite";
|
|
22
|
+
/**
|
|
23
|
+
* Electron Forge's Vite template. Its Vite config is split three ways (`vite.main.config.*`,
|
|
24
|
+
* `vite.preload.config.*`, `vite.renderer.config.*`) and there is no `vite.config.*` at all, so it
|
|
25
|
+
* used to read as plain Vite with a missing config: a manual step, no preload shim, no capture
|
|
26
|
+
* helper, and a re-run that could never clear the ⚠. Only the renderer config has a document.
|
|
27
|
+
*/
|
|
28
|
+
readonly ELECTRON_FORGE: "electron-forge";
|
|
22
29
|
/**
|
|
23
30
|
* React Router in FRAMEWORK mode (v7's `@react-router/dev`, the successor to Remix).
|
|
24
31
|
*
|
|
@@ -36,6 +43,16 @@ export declare const Framework: {
|
|
|
36
43
|
* path.
|
|
37
44
|
*/
|
|
38
45
|
readonly REACT_ROUTER: "react-router";
|
|
46
|
+
/**
|
|
47
|
+
* Remix v2 (`@remix-run/dev`) — React Router framework mode under its previous name.
|
|
48
|
+
*
|
|
49
|
+
* On Vite (`vitePlugin` from `@remix-run/dev`) it renders its own HTML exactly as framework mode
|
|
50
|
+
* does, so it needs the same client-entry connect. It used to fall through to `Framework.VITE`:
|
|
51
|
+
* the plugin was wired, every step went green, and the page never connected, because nothing
|
|
52
|
+
* reaches a document Remix SSRs. On the classic compiler (no Vite) there is no plugin to serve the
|
|
53
|
+
* connect module at all, and the plan says so rather than wiring half of it.
|
|
54
|
+
*/
|
|
55
|
+
readonly REMIX: "remix";
|
|
39
56
|
/**
|
|
40
57
|
* TanStack Start SSRs `<html>` from `src/routes/__root.tsx` and never sends Vite's index.html, so
|
|
41
58
|
* the plugin's `transformIndexHtml` injection never fires. It used to fall through to
|
|
@@ -51,6 +68,12 @@ export declare const Framework: {
|
|
|
51
68
|
readonly TANSTACK_START: "tanstack-start";
|
|
52
69
|
readonly SVELTEKIT: "sveltekit";
|
|
53
70
|
readonly ASTRO: "astro";
|
|
71
|
+
/**
|
|
72
|
+
* Angular CLI. It has no Vite config to patch (the CLI owns its bundler) and no index.html at the
|
|
73
|
+
* root, so it used to fall through to HTML — and be handed the React kit, a ⚠ pointing at a file
|
|
74
|
+
* that does not exist, and two snippets that fail on it.
|
|
75
|
+
*/
|
|
76
|
+
readonly ANGULAR: "angular";
|
|
54
77
|
/** Create React App. No config file exists, so `react-scripts` in the dependencies is the signal. */
|
|
55
78
|
readonly CRA: "cra";
|
|
56
79
|
readonly HTML: "html";
|
|
@@ -143,9 +166,32 @@ export interface Detection {
|
|
|
143
166
|
*/
|
|
144
167
|
webGlSubtree?: boolean | undefined;
|
|
145
168
|
packageManager: PackageManager;
|
|
169
|
+
/**
|
|
170
|
+
* The prefix this run can actually invoke `packageManager` through — see `preflight.ts` (#1149) for
|
|
171
|
+
* why that can be `corepack <packageManager>` rather than the bare name. Undefined here: `detect` is
|
|
172
|
+
* pure and never probes a binary, so this is filled in by `run.ts` from `preflight`'s result, before
|
|
173
|
+
* the plan is built. Every later spawn or printed command for the package manager reads this rather
|
|
174
|
+
* than `packageManager` alone.
|
|
175
|
+
*/
|
|
176
|
+
packageManagerCommand?: string;
|
|
177
|
+
/**
|
|
178
|
+
* Every dependency this package DECLARES, by name, merged across the three blocks.
|
|
179
|
+
*
|
|
180
|
+
* So the planner can tell "not wired yet" from "wired already". Without it the install step could
|
|
181
|
+
* only ever say APPLY, and a re-run over an instrumented project therefore ran a package-manager
|
|
182
|
+
* install it did not need - which is the step that touches `node_modules`.
|
|
183
|
+
*
|
|
184
|
+
* Optional so every existing construction keeps working; absent reads as "nothing known", which
|
|
185
|
+
* installs, and installing twice is the harmless direction.
|
|
186
|
+
*/
|
|
187
|
+
dependencies?: Readonly<Record<string, string>> | undefined;
|
|
146
188
|
}
|
|
189
|
+
/** Forge's renderer build — the only one of its three Vite configs with a document. */
|
|
190
|
+
export declare const FORGE_RENDERER_CONFIGS: string[];
|
|
191
|
+
/** The Angular CLI workspace file. */
|
|
192
|
+
export declare const ANGULAR_WORKSPACE_FILE = "angular.json";
|
|
147
193
|
/** What identifies one framework in a project root: a dependency name, or a config file basename. */
|
|
148
|
-
|
|
194
|
+
interface FrameworkSignals {
|
|
149
195
|
/** `package.json` dependency names that name this framework outright. */
|
|
150
196
|
readonly deps: readonly string[];
|
|
151
197
|
/** Root config-file basenames that name it when the dependency is absent. */
|
|
@@ -196,7 +242,7 @@ export declare function parseMajor(range: string | undefined): number | undefine
|
|
|
196
242
|
* lockfile once the project's own tree can answer the question.
|
|
197
243
|
*/
|
|
198
244
|
export declare function namesAPackageManager(markers: ReadonlySet<string>): boolean;
|
|
199
|
-
export declare function detectPackageManager(lockfiles: ReadonlySet<string>, nodeModulesMarkers: ReadonlySet<string
|
|
245
|
+
export declare function detectPackageManager(lockfiles: ReadonlySet<string>, nodeModulesMarkers: ReadonlySet<string>, pkg?: unknown): PackageManager;
|
|
200
246
|
export declare function detect(input: DetectInput): Detection;
|
|
201
247
|
interface InstallCommand {
|
|
202
248
|
command: string;
|
|
@@ -212,7 +258,13 @@ export declare function installCommandParts(pm: PackageManager, pkgs: string | r
|
|
|
212
258
|
* how `npm i -D --legacy-peer-deps` would become a request to install a package called
|
|
213
259
|
* `--legacy-peer-deps` on a manager that does not recognise the flag.
|
|
214
260
|
*/
|
|
215
|
-
extraFlags?: readonly string[]
|
|
261
|
+
extraFlags?: readonly string[],
|
|
262
|
+
/**
|
|
263
|
+
* The prefix that actually invokes `pm` on this machine — `pm` itself, or `corepack ${pm}` on a
|
|
264
|
+
* corepack-only machine (see `Detection.packageManagerCommand`). Defaults to `pm` so every existing
|
|
265
|
+
* caller keeps spawning the bare binary without naming this.
|
|
266
|
+
*/
|
|
267
|
+
prefix?: string): InstallCommand;
|
|
216
268
|
/**
|
|
217
269
|
* The install command as a human reads it — and deliberately NOT `installCommandParts` joined.
|
|
218
270
|
*
|
|
@@ -221,5 +273,7 @@ extraFlags?: readonly string[]): InstallCommand;
|
|
|
221
273
|
* directly for that reason; routing it through the parts would put `--no-audit --no-fund` in front of
|
|
222
274
|
* every reader and teach them our noise-suppression as if it were part of installing Reticle.
|
|
223
275
|
*/
|
|
224
|
-
export declare function installCommand(pm: PackageManager, pkgs: string | readonly string[]
|
|
276
|
+
export declare function installCommand(pm: PackageManager, pkgs: string | readonly string[],
|
|
277
|
+
/** As in `installCommandParts` — what a corepack-only machine has to type instead of bare `pm`. */
|
|
278
|
+
prefix?: string): string;
|
|
225
279
|
export {};
|
package/dist/detect/detect.js
CHANGED
|
@@ -19,6 +19,13 @@ export const Framework = {
|
|
|
19
19
|
* is main's — wiring the SDK where there is no document, and reporting success.
|
|
20
20
|
*/
|
|
21
21
|
ELECTRON_VITE: 'electron-vite',
|
|
22
|
+
/**
|
|
23
|
+
* Electron Forge's Vite template. Its Vite config is split three ways (`vite.main.config.*`,
|
|
24
|
+
* `vite.preload.config.*`, `vite.renderer.config.*`) and there is no `vite.config.*` at all, so it
|
|
25
|
+
* used to read as plain Vite with a missing config: a manual step, no preload shim, no capture
|
|
26
|
+
* helper, and a re-run that could never clear the ⚠. Only the renderer config has a document.
|
|
27
|
+
*/
|
|
28
|
+
ELECTRON_FORGE: 'electron-forge',
|
|
22
29
|
/**
|
|
23
30
|
* React Router in FRAMEWORK mode (v7's `@react-router/dev`, the successor to Remix).
|
|
24
31
|
*
|
|
@@ -36,6 +43,16 @@ export const Framework = {
|
|
|
36
43
|
* path.
|
|
37
44
|
*/
|
|
38
45
|
REACT_ROUTER: 'react-router',
|
|
46
|
+
/**
|
|
47
|
+
* Remix v2 (`@remix-run/dev`) — React Router framework mode under its previous name.
|
|
48
|
+
*
|
|
49
|
+
* On Vite (`vitePlugin` from `@remix-run/dev`) it renders its own HTML exactly as framework mode
|
|
50
|
+
* does, so it needs the same client-entry connect. It used to fall through to `Framework.VITE`:
|
|
51
|
+
* the plugin was wired, every step went green, and the page never connected, because nothing
|
|
52
|
+
* reaches a document Remix SSRs. On the classic compiler (no Vite) there is no plugin to serve the
|
|
53
|
+
* connect module at all, and the plan says so rather than wiring half of it.
|
|
54
|
+
*/
|
|
55
|
+
REMIX: 'remix',
|
|
39
56
|
/**
|
|
40
57
|
* TanStack Start SSRs `<html>` from `src/routes/__root.tsx` and never sends Vite's index.html, so
|
|
41
58
|
* the plugin's `transformIndexHtml` injection never fires. It used to fall through to
|
|
@@ -51,6 +68,12 @@ export const Framework = {
|
|
|
51
68
|
TANSTACK_START: 'tanstack-start',
|
|
52
69
|
SVELTEKIT: 'sveltekit',
|
|
53
70
|
ASTRO: 'astro',
|
|
71
|
+
/**
|
|
72
|
+
* Angular CLI. It has no Vite config to patch (the CLI owns its bundler) and no index.html at the
|
|
73
|
+
* root, so it used to fall through to HTML — and be handed the React kit, a ⚠ pointing at a file
|
|
74
|
+
* that does not exist, and two snippets that fail on it.
|
|
75
|
+
*/
|
|
76
|
+
ANGULAR: 'angular',
|
|
54
77
|
/** Create React App. No config file exists, so `react-scripts` in the dependencies is the signal. */
|
|
55
78
|
CRA: 'cra',
|
|
56
79
|
HTML: 'html',
|
|
@@ -88,6 +111,17 @@ const REACT_ROUTER_CONFIGS = [
|
|
|
88
111
|
'react-router.config.mjs',
|
|
89
112
|
];
|
|
90
113
|
const NUXT_CONFIGS = ['nuxt.config.ts', 'nuxt.config.js', 'nuxt.config.mjs'];
|
|
114
|
+
/** The classic Remix compiler's config. Remix on Vite has none; its dependency is the signal. */
|
|
115
|
+
const REMIX_CONFIGS = ['remix.config.js', 'remix.config.mjs', 'remix.config.cjs'];
|
|
116
|
+
/** Forge's renderer build — the only one of its three Vite configs with a document. */
|
|
117
|
+
export const FORGE_RENDERER_CONFIGS = [
|
|
118
|
+
'vite.renderer.config.ts',
|
|
119
|
+
'vite.renderer.config.mts',
|
|
120
|
+
'vite.renderer.config.js',
|
|
121
|
+
'vite.renderer.config.mjs',
|
|
122
|
+
];
|
|
123
|
+
/** The Angular CLI workspace file. */
|
|
124
|
+
export const ANGULAR_WORKSPACE_FILE = 'angular.json';
|
|
91
125
|
const ASTRO_CONFIGS = [
|
|
92
126
|
'astro.config.mjs',
|
|
93
127
|
'astro.config.js',
|
|
@@ -148,6 +182,17 @@ export const FRAMEWORK_SIGNALS = {
|
|
|
148
182
|
* has a DOM, so it must never fall through to the generic Vite path.
|
|
149
183
|
*/
|
|
150
184
|
[Framework.ELECTRON_VITE]: { deps: ['electron-vite'], configs: ELECTRON_VITE_CONFIGS },
|
|
185
|
+
/**
|
|
186
|
+
* The Vite plugin, not `@electron-forge/cli`: a Forge app on the webpack template has no Vite
|
|
187
|
+
* config to patch, and claiming it here would hand it steps that cannot apply.
|
|
188
|
+
*/
|
|
189
|
+
[Framework.ELECTRON_FORGE]: {
|
|
190
|
+
deps: ['@electron-forge/plugin-vite'],
|
|
191
|
+
configs: FORGE_RENDERER_CONFIGS,
|
|
192
|
+
},
|
|
193
|
+
/** `@remix-run/dev` is both compilers' package; the plan tells them apart by the Vite config. */
|
|
194
|
+
[Framework.REMIX]: { deps: ['@remix-run/dev'], configs: REMIX_CONFIGS },
|
|
195
|
+
[Framework.ANGULAR]: { deps: ['@angular/core'], configs: [ANGULAR_WORKSPACE_FILE] },
|
|
151
196
|
/**
|
|
152
197
|
* The Start packages, never Query or Router alone — those stay on the Vite path. Start has no
|
|
153
198
|
* config file of its own to key on.
|
|
@@ -177,9 +222,17 @@ export const DETECTION_ORDER = [
|
|
|
177
222
|
Framework.SVELTEKIT,
|
|
178
223
|
Framework.ASTRO,
|
|
179
224
|
Framework.ELECTRON_VITE,
|
|
225
|
+
// Before Vite, for the reason electron-vite is: Forge's template depends on `vite` directly.
|
|
226
|
+
Framework.ELECTRON_FORGE,
|
|
180
227
|
Framework.REACT_ROUTER,
|
|
228
|
+
// After React Router, so an app half-way through the Remix -> React Router upgrade gets the
|
|
229
|
+
// successor's wiring; before Vite, because Remix on Vite depends on `vite` directly.
|
|
230
|
+
Framework.REMIX,
|
|
181
231
|
Framework.TANSTACK_START,
|
|
182
232
|
Framework.VITE,
|
|
233
|
+
// After Vite: an Analog app is Angular on Vite, and the Vite plugin's index.html injection reaches
|
|
234
|
+
// it. A CLI app never depends on `vite` itself — the CLI bundles its own.
|
|
235
|
+
Framework.ANGULAR,
|
|
183
236
|
Framework.CRA,
|
|
184
237
|
];
|
|
185
238
|
/** Extract the leading major version from a semver range like "^19.0.0" or "19.1.1". */
|
|
@@ -221,7 +274,41 @@ function packageManagerFromNodeModules(markers) {
|
|
|
221
274
|
export function namesAPackageManager(markers) {
|
|
222
275
|
return packageManagerFromNodeModules(markers) !== undefined;
|
|
223
276
|
}
|
|
224
|
-
|
|
277
|
+
/**
|
|
278
|
+
* The manager a project DECLARES, via corepack's `packageManager` field.
|
|
279
|
+
*
|
|
280
|
+
* The strongest signal there is, and stronger than anything else here: a lockfile is evidence of
|
|
281
|
+
* what was run once, an installed tree is evidence of what was run last, and this is the project
|
|
282
|
+
* SAYING which one it uses. Corepack enforces it — it refuses to run a different manager on the
|
|
283
|
+
* project's behalf — so choosing against it produces a command that cannot work.
|
|
284
|
+
*
|
|
285
|
+
* Unrecognised values fall through rather than defaulting. A field naming something we do not know
|
|
286
|
+
* must not quietly become npm; that is how a pnpm repo gets an `npm i`.
|
|
287
|
+
*/
|
|
288
|
+
function packageManagerFromField(pkg) {
|
|
289
|
+
if ('object' !== typeof pkg || null === pkg)
|
|
290
|
+
return undefined;
|
|
291
|
+
const declared = pkg['packageManager'];
|
|
292
|
+
if ('string' !== typeof declared)
|
|
293
|
+
return undefined;
|
|
294
|
+
// `name@version`, and the version is not ours to care about — only which binary runs.
|
|
295
|
+
const name = declared.split('@')[0]?.trim().toLowerCase();
|
|
296
|
+
const known = {
|
|
297
|
+
npm: PackageManager.NPM,
|
|
298
|
+
pnpm: PackageManager.PNPM,
|
|
299
|
+
yarn: PackageManager.YARN,
|
|
300
|
+
bun: PackageManager.BUN,
|
|
301
|
+
};
|
|
302
|
+
return name === undefined ? undefined : known[name];
|
|
303
|
+
}
|
|
304
|
+
export function detectPackageManager(lockfiles, nodeModulesMarkers, pkg) {
|
|
305
|
+
// What the project SAYS beats every trace of what was once run in it. Reported from the field: an
|
|
306
|
+
// already-instrumented npm workspaces project had a pnpm dependency migration run over it, which
|
|
307
|
+
// moved its `node_modules` aside and broke the dev server. Moving an installed tree is not
|
|
308
|
+
// something a scaffolder may do to a working checkout.
|
|
309
|
+
const declared = packageManagerFromField(pkg);
|
|
310
|
+
if (declared !== undefined)
|
|
311
|
+
return declared;
|
|
225
312
|
if (lockfiles.has('pnpm-lock.yaml'))
|
|
226
313
|
return PackageManager.PNPM;
|
|
227
314
|
if (lockfiles.has('yarn.lock'))
|
|
@@ -231,6 +318,10 @@ export function detectPackageManager(lockfiles, nodeModulesMarkers) {
|
|
|
231
318
|
// No lockfile is not the same as "npm". An already-installed tree says which manager built it.
|
|
232
319
|
return packageManagerFromNodeModules(nodeModulesMarkers) ?? PackageManager.NPM;
|
|
233
320
|
}
|
|
321
|
+
/** Every declared dependency, by name. Later blocks do not override earlier ones by accident. */
|
|
322
|
+
function declaredDependencies(pkg) {
|
|
323
|
+
return { ...pkg.peerDependencies, ...pkg.devDependencies, ...pkg.dependencies };
|
|
324
|
+
}
|
|
234
325
|
function detectFramework(input) {
|
|
235
326
|
for (const framework of DETECTION_ORDER) {
|
|
236
327
|
const signals = FRAMEWORK_SIGNALS[framework];
|
|
@@ -272,7 +363,8 @@ export function detect(input) {
|
|
|
272
363
|
needsSourceMapping: reactMajor !== undefined && reactMajor >= 19,
|
|
273
364
|
customReconciler: CUSTOM_RECONCILER_DEPS.some((name) => depVersion(input.pkg, name) !== undefined),
|
|
274
365
|
webGlSubtree: WEBGL_SUBTREE_DEPS.some((name) => depVersion(input.pkg, name) !== undefined),
|
|
275
|
-
packageManager: detectPackageManager(input.lockfiles, input.nodeModulesMarkers ?? new Set()),
|
|
366
|
+
packageManager: detectPackageManager(input.lockfiles, input.nodeModulesMarkers ?? new Set(), input.pkg),
|
|
367
|
+
dependencies: declaredDependencies(input.pkg),
|
|
276
368
|
};
|
|
277
369
|
}
|
|
278
370
|
const INSTALL_ARGS = {
|
|
@@ -317,11 +409,27 @@ export function installCommandParts(pm, pkgs,
|
|
|
317
409
|
* how `npm i -D --legacy-peer-deps` would become a request to install a package called
|
|
318
410
|
* `--legacy-peer-deps` on a manager that does not recognise the flag.
|
|
319
411
|
*/
|
|
320
|
-
extraFlags = []
|
|
412
|
+
extraFlags = [],
|
|
413
|
+
/**
|
|
414
|
+
* The prefix that actually invokes `pm` on this machine — `pm` itself, or `corepack ${pm}` on a
|
|
415
|
+
* corepack-only machine (see `Detection.packageManagerCommand`). Defaults to `pm` so every existing
|
|
416
|
+
* caller keeps spawning the bare binary without naming this.
|
|
417
|
+
*/
|
|
418
|
+
prefix = pm) {
|
|
321
419
|
const list = 'string' === typeof pkgs ? [pkgs] : pkgs;
|
|
420
|
+
// A bare `pm` is one word and stays the whole command; `corepack ${pm}` splits so `corepack` is the
|
|
421
|
+
// program `spawnAllowed` runs and `pm` is its first argument — the shape `RUNNABLE_COMMANDS` and the
|
|
422
|
+
// corepack probe both expect.
|
|
423
|
+
const [command, ...leadingArgs] = prefix.split(' ');
|
|
322
424
|
return {
|
|
323
|
-
command: pm,
|
|
324
|
-
args: [
|
|
425
|
+
command: command ?? pm,
|
|
426
|
+
args: [
|
|
427
|
+
...leadingArgs,
|
|
428
|
+
...INSTALL_ARGS[pm],
|
|
429
|
+
...list,
|
|
430
|
+
...(QUIET_INSTALL_ARGS[pm] ?? []),
|
|
431
|
+
...extraFlags,
|
|
432
|
+
],
|
|
325
433
|
};
|
|
326
434
|
}
|
|
327
435
|
/**
|
|
@@ -332,7 +440,9 @@ extraFlags = []) {
|
|
|
332
440
|
* directly for that reason; routing it through the parts would put `--no-audit --no-fund` in front of
|
|
333
441
|
* every reader and teach them our noise-suppression as if it were part of installing Reticle.
|
|
334
442
|
*/
|
|
335
|
-
export function installCommand(pm, pkgs
|
|
443
|
+
export function installCommand(pm, pkgs,
|
|
444
|
+
/** As in `installCommandParts` — what a corepack-only machine has to type instead of bare `pm`. */
|
|
445
|
+
prefix = pm) {
|
|
336
446
|
const list = 'string' === typeof pkgs ? [pkgs] : pkgs;
|
|
337
|
-
return `${
|
|
447
|
+
return `${prefix} ${[...INSTALL_ARGS[pm], ...list].join(' ')}`;
|
|
338
448
|
}
|
|
@@ -38,8 +38,30 @@ interface DevScriptPlan {
|
|
|
38
38
|
* `serving` is the probe result, passed in rather than measured here so the decision stays pure.
|
|
39
39
|
* When something is already answering we do not care which script exists: the rule is never to
|
|
40
40
|
* start a second server, because the one that is running may be the user's, with their state in it.
|
|
41
|
+
*
|
|
42
|
+
* `packageManagerCommand` is `Detection.packageManagerCommand` (see `preflight.ts`, #1149) — the
|
|
43
|
+
* prefix that actually invokes the package manager on this machine, which is what gets printed AND,
|
|
44
|
+
* later, spawned by `startDevServer`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function planDevScript(scripts: Readonly<Record<string, string>>, packageManagerCommand: string, serving: boolean): DevScriptPlan;
|
|
47
|
+
/** How a desktop shell is launched: through one of its scripts, or its CLI when none names it. */
|
|
48
|
+
export type DesktopLaunch = {
|
|
49
|
+
readonly script: string;
|
|
50
|
+
readonly args: string;
|
|
51
|
+
} | {
|
|
52
|
+
readonly command: string;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* The desktop shell's own launcher, when the manifest names one; undefined for anything else.
|
|
56
|
+
*
|
|
57
|
+
* A desktop project's `dev` script is its RENDERER's dev server: on the official Tauri template it
|
|
58
|
+
* is plain `vite`, so init ran it, no window ever opened, and the run waited out its whole budget
|
|
59
|
+
* for a session only the window could create. The shell's CLI is what opens the window.
|
|
60
|
+
*
|
|
61
|
+
* Exported because the daemon names a dev command too (its no-session next action), and two
|
|
62
|
+
* readers of one manifest have to give one answer.
|
|
41
63
|
*/
|
|
42
|
-
export declare function
|
|
64
|
+
export declare function desktopLaunch(pkg: unknown): DesktopLaunch | undefined;
|
|
43
65
|
/**
|
|
44
66
|
* The command to print for this project, straight from its `package.json`.
|
|
45
67
|
*
|
|
@@ -47,5 +69,7 @@ export declare function planDevScript(scripts: Readonly<Record<string, string>>,
|
|
|
47
69
|
* parsing attached, and `run.ts` is at its cohesion limit. Returns undefined for anything it cannot
|
|
48
70
|
* read: this phrasing decides nothing, so it must never be able to break an install.
|
|
49
71
|
*/
|
|
50
|
-
export declare function devCommandFrom(pkg: unknown,
|
|
72
|
+
export declare function devCommandFrom(pkg: unknown, packageManagerCommand: string): string | undefined;
|
|
73
|
+
/** The dev script's own command line (`react-router dev`, `vite`), or undefined when there is none. */
|
|
74
|
+
export declare function devScriptBody(pkg: unknown): string | undefined;
|
|
51
75
|
export {};
|
|
@@ -31,17 +31,73 @@ export const DevScriptChoice = {
|
|
|
31
31
|
* `serving` is the probe result, passed in rather than measured here so the decision stays pure.
|
|
32
32
|
* When something is already answering we do not care which script exists: the rule is never to
|
|
33
33
|
* start a second server, because the one that is running may be the user's, with their state in it.
|
|
34
|
+
*
|
|
35
|
+
* `packageManagerCommand` is `Detection.packageManagerCommand` (see `preflight.ts`, #1149) — the
|
|
36
|
+
* prefix that actually invokes the package manager on this machine, which is what gets printed AND,
|
|
37
|
+
* later, spawned by `startDevServer`.
|
|
34
38
|
*/
|
|
35
|
-
export function planDevScript(scripts,
|
|
39
|
+
export function planDevScript(scripts, packageManagerCommand, serving) {
|
|
36
40
|
if (serving)
|
|
37
41
|
return { choice: DevScriptChoice.ALREADY_SERVING };
|
|
38
42
|
const name = CANDIDATES.find((c) => 'string' === typeof scripts[c] && scripts[c] !== '');
|
|
39
43
|
if (name === undefined)
|
|
40
44
|
return { choice: DevScriptChoice.NO_SCRIPT };
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
return {
|
|
46
|
+
choice: DevScriptChoice.START,
|
|
47
|
+
script: name,
|
|
48
|
+
command: runScript(packageManagerCommand, name),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* `npm` needs `run`; the others take the script name directly. Getting this wrong prints a command
|
|
53
|
+
* that fails, which is worse than printing nothing. `npm` is never corepack-prefixed (it ships with
|
|
54
|
+
* node), so this comparison still identifies it correctly.
|
|
55
|
+
*/
|
|
56
|
+
function runScript(packageManagerCommand, script) {
|
|
57
|
+
return 'npm' === packageManagerCommand
|
|
58
|
+
? `npm run ${script}`
|
|
59
|
+
: `${packageManagerCommand} ${script}`;
|
|
60
|
+
}
|
|
61
|
+
const TAURI_CLI_PACKAGE = '@tauri-apps/cli';
|
|
62
|
+
/** The official template's script: `"tauri": "tauri"`, so `<pm> tauri dev` is its dev command. */
|
|
63
|
+
const TAURI_SCRIPT = 'tauri';
|
|
64
|
+
const TAURI_DEV_ARG = 'dev';
|
|
65
|
+
const TAURI_DIRECT = 'npx tauri dev';
|
|
66
|
+
const FORGE_CLI_PACKAGE = '@electron-forge/cli';
|
|
67
|
+
const FORGE_START = /\belectron-forge\s+start\b/;
|
|
68
|
+
const FORGE_DIRECT = 'npx electron-forge start';
|
|
69
|
+
function manifestRecord(pkg, key) {
|
|
70
|
+
const value = 'object' === typeof pkg && null !== pkg ? Reflect.get(pkg, key) : {};
|
|
71
|
+
return 'object' === typeof value && null !== value ? value : {};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The desktop shell's own launcher, when the manifest names one; undefined for anything else.
|
|
75
|
+
*
|
|
76
|
+
* A desktop project's `dev` script is its RENDERER's dev server: on the official Tauri template it
|
|
77
|
+
* is plain `vite`, so init ran it, no window ever opened, and the run waited out its whole budget
|
|
78
|
+
* for a session only the window could create. The shell's CLI is what opens the window.
|
|
79
|
+
*
|
|
80
|
+
* Exported because the daemon names a dev command too (its no-session next action), and two
|
|
81
|
+
* readers of one manifest have to give one answer.
|
|
82
|
+
*/
|
|
83
|
+
export function desktopLaunch(pkg) {
|
|
84
|
+
const scripts = manifestRecord(pkg, 'scripts');
|
|
85
|
+
const deps = {
|
|
86
|
+
...manifestRecord(pkg, 'dependencies'),
|
|
87
|
+
...manifestRecord(pkg, 'devDependencies'),
|
|
88
|
+
};
|
|
89
|
+
const tauriScript = scripts[TAURI_SCRIPT];
|
|
90
|
+
if ('string' === typeof tauriScript && 0 < tauriScript.trim().length) {
|
|
91
|
+
return { script: TAURI_SCRIPT, args: TAURI_DEV_ARG };
|
|
92
|
+
}
|
|
93
|
+
if (undefined !== deps[TAURI_CLI_PACKAGE])
|
|
94
|
+
return { command: TAURI_DIRECT };
|
|
95
|
+
const forge = Object.entries(scripts).find(([, body]) => 'string' === typeof body && FORGE_START.test(body));
|
|
96
|
+
if (undefined !== forge)
|
|
97
|
+
return { script: forge[0], args: '' };
|
|
98
|
+
if (undefined !== deps[FORGE_CLI_PACKAGE])
|
|
99
|
+
return { command: FORGE_DIRECT };
|
|
100
|
+
return undefined;
|
|
45
101
|
}
|
|
46
102
|
/**
|
|
47
103
|
* The command to print for this project, straight from its `package.json`.
|
|
@@ -50,17 +106,34 @@ export function planDevScript(scripts, packageManager, serving) {
|
|
|
50
106
|
* parsing attached, and `run.ts` is at its cohesion limit. Returns undefined for anything it cannot
|
|
51
107
|
* read: this phrasing decides nothing, so it must never be able to break an install.
|
|
52
108
|
*/
|
|
53
|
-
export function devCommandFrom(pkg,
|
|
109
|
+
export function devCommandFrom(pkg, packageManagerCommand) {
|
|
54
110
|
try {
|
|
55
111
|
// Takes the PARSED manifest. It used to take the raw string and parse it a fourth time — the
|
|
56
112
|
// caller now reads the file once, through a guard, so there is one place a malformed manifest
|
|
57
113
|
// can be noticed and it is not this one.
|
|
114
|
+
const desktop = desktopLaunch(pkg);
|
|
115
|
+
if (undefined !== desktop) {
|
|
116
|
+
if ('command' in desktop)
|
|
117
|
+
return desktop.command;
|
|
118
|
+
const run = runScript(packageManagerCommand, desktop.script);
|
|
119
|
+
return 0 < desktop.args.length ? `${run} ${desktop.args}` : run;
|
|
120
|
+
}
|
|
58
121
|
const scripts = 'object' === typeof pkg && null !== pkg
|
|
59
122
|
? (pkg.scripts ?? {})
|
|
60
123
|
: {};
|
|
61
|
-
return planDevScript(scripts,
|
|
124
|
+
return planDevScript(scripts, packageManagerCommand, false).command;
|
|
62
125
|
}
|
|
63
126
|
catch {
|
|
64
127
|
return undefined;
|
|
65
128
|
}
|
|
66
129
|
}
|
|
130
|
+
/** The dev script's own command line (`react-router dev`, `vite`), or undefined when there is none. */
|
|
131
|
+
export function devScriptBody(pkg) {
|
|
132
|
+
const scripts = 'object' === typeof pkg && null !== pkg ? pkg.scripts : undefined;
|
|
133
|
+
if ('object' !== typeof scripts || null === scripts)
|
|
134
|
+
return undefined;
|
|
135
|
+
const table = scripts;
|
|
136
|
+
const name = CANDIDATES.find((c) => 'string' === typeof table[c] && table[c] !== '');
|
|
137
|
+
const body = name === undefined ? undefined : table[name];
|
|
138
|
+
return 'string' === typeof body ? body : undefined;
|
|
139
|
+
}
|
|
@@ -41,4 +41,4 @@ export declare function detectNonJsEcosystem(exists: (file: string) => boolean):
|
|
|
41
41
|
* directory, and a developer whose project is not JavaScript at all. The first needs a path; the
|
|
42
42
|
* second needs to know that no path exists and why.
|
|
43
43
|
*/
|
|
44
|
-
export declare function noPackageJsonMessage(exists: (file: string) => boolean): string;
|
|
44
|
+
export declare function noPackageJsonMessage(exists: (file: string) => boolean, rootFiles?: readonly string[]): string;
|
|
@@ -104,9 +104,14 @@ export function detectNonJsEcosystem(exists) {
|
|
|
104
104
|
* directory, and a developer whose project is not JavaScript at all. The first needs a path; the
|
|
105
105
|
* second needs to know that no path exists and why.
|
|
106
106
|
*/
|
|
107
|
-
export function noPackageJsonMessage(exists) {
|
|
107
|
+
export function noPackageJsonMessage(exists, rootFiles = []) {
|
|
108
108
|
const ecosystem = detectNonJsEcosystem(exists);
|
|
109
109
|
if (ecosystem === undefined) {
|
|
110
|
+
if (exists('index.html') || rootFiles.some((name) => name.endsWith('.html'))) {
|
|
111
|
+
return ('This is a static page with no build step, so there is nothing for `reticle init` to wire. ' +
|
|
112
|
+
'Add the script-tag snippet below to the page while you develop, then reload it. Remove the ' +
|
|
113
|
+
'snippet before publishing: a static file has no development build to keep it out of production.');
|
|
114
|
+
}
|
|
110
115
|
return ('No package.json found here, and no app directory beneath it either. Run `reticle init` from ' +
|
|
111
116
|
"your app's directory, or from a repo root that contains it.");
|
|
112
117
|
}
|
|
@@ -35,4 +35,25 @@ export declare const APP_DEPS: readonly ["next", "vite", "electron-vite"];
|
|
|
35
35
|
* it knows it is in a workspace; this is the matching walk DOWN.
|
|
36
36
|
*/
|
|
37
37
|
export declare function findWorkspaceApps(io: Pick<InitIo, 'exists' | 'readFile' | 'listDirs'>): string[];
|
|
38
|
+
/**
|
|
39
|
+
* Every package directory the workspace declares or holds, app-shaped or not.
|
|
40
|
+
*
|
|
41
|
+
* For the questions discovery's filter must not answer — "which package does the root config name"
|
|
42
|
+
* is about identity, and a package `--app` wired is the app whether or not it looks like one.
|
|
43
|
+
*/
|
|
44
|
+
export declare function workspacePackageDirs(io: Pick<InitIo, 'readFile' | 'listDirs'>): string[];
|
|
45
|
+
/**
|
|
46
|
+
* The workspace root that DECLARES `cwd` as one of its packages, or undefined.
|
|
47
|
+
*
|
|
48
|
+
* `init` run inside `apps/web` wrote `.reticle.json` there only, and `reticle mcp` finds that file by
|
|
49
|
+
* walking UP from where the agent stands — so an agent opened at the repo root never found it, fell
|
|
50
|
+
* back to the default port, and drove whichever daemon was there. `init --app apps/web` run from the
|
|
51
|
+
* root writes the root pointer and the agent files at the root; this is what lets a run from inside
|
|
52
|
+
* the app arrive at the same state.
|
|
53
|
+
*
|
|
54
|
+
* Only the NEAREST workspace root is asked, and only its declaration counts: a directory that merely
|
|
55
|
+
* sits under a repo with a package.json is not that repo's app, and guessing it is would write
|
|
56
|
+
* config into a stranger's root.
|
|
57
|
+
*/
|
|
58
|
+
export declare function enclosingWorkspaceRoot(cwd: string, io: Pick<InitIo, 'readFile'>): string | undefined;
|
|
38
59
|
export {};
|