@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.
Files changed (93) hide show
  1. package/dist/detect/capabilities.d.ts +4 -1
  2. package/dist/detect/capabilities.js +45 -9
  3. package/dist/detect/detect.d.ts +58 -4
  4. package/dist/detect/detect.js +117 -7
  5. package/dist/detect/dev-script.d.ts +26 -2
  6. package/dist/detect/dev-script.js +80 -7
  7. package/dist/detect/non-js-project.d.ts +1 -1
  8. package/dist/detect/non-js-project.js +6 -1
  9. package/dist/detect/workspace-apps.d.ts +21 -0
  10. package/dist/detect/workspace-apps.js +111 -12
  11. package/dist/detect/workspace-redirect.d.ts +2 -1
  12. package/dist/detect/workspace-redirect.js +28 -2
  13. package/dist/diagnose/containerised-dev-server.d.ts +12 -4
  14. package/dist/diagnose/containerised-dev-server.js +36 -4
  15. package/dist/diagnose/csp-check.d.ts +12 -6
  16. package/dist/diagnose/csp-check.js +61 -13
  17. package/dist/diagnose/csp-doctor.d.ts +33 -2
  18. package/dist/diagnose/csp-doctor.js +103 -6
  19. package/dist/diagnose/csp-step.d.ts +27 -0
  20. package/dist/diagnose/csp-step.js +86 -0
  21. package/dist/diagnose/desktop-doctor.d.ts +1 -1
  22. package/dist/diagnose/desktop-doctor.js +8 -2
  23. package/dist/diagnose/install-hint.d.ts +9 -1
  24. package/dist/diagnose/install-hint.js +17 -7
  25. package/dist/diagnose/install-retries.d.ts +18 -2
  26. package/dist/diagnose/install-retries.js +50 -3
  27. package/dist/diagnose/terminal-wrap.d.ts +18 -0
  28. package/dist/diagnose/terminal-wrap.js +63 -0
  29. package/dist/host.d.ts +6 -0
  30. package/dist/host.js +3 -0
  31. package/dist/index.d.ts +8 -3
  32. package/dist/index.js +8 -3
  33. package/dist/no-package-json.d.ts +10 -0
  34. package/dist/no-package-json.js +121 -0
  35. package/dist/node-io.d.ts +9 -5
  36. package/dist/node-io.js +25 -7
  37. package/dist/patch/angular.d.ts +68 -0
  38. package/dist/patch/angular.js +216 -0
  39. package/dist/patch/astro-patch.d.ts +48 -13
  40. package/dist/patch/astro-patch.js +151 -43
  41. package/dist/patch/cra.d.ts +0 -2
  42. package/dist/patch/cra.js +0 -2
  43. package/dist/patch/electron-patch.d.ts +10 -1
  44. package/dist/patch/electron-patch.js +79 -27
  45. package/dist/patch/electron-vite-patch.js +1 -1
  46. package/dist/patch/nuxt-patch.d.ts +1 -1
  47. package/dist/patch/nuxt-patch.js +2 -2
  48. package/dist/patch/nuxt-snippets.d.ts +44 -0
  49. package/dist/patch/nuxt-snippets.js +135 -0
  50. package/dist/patch/remix.d.ts +24 -0
  51. package/dist/patch/remix.js +59 -0
  52. package/dist/patch/snippets.d.ts +58 -57
  53. package/dist/patch/snippets.js +181 -202
  54. package/dist/patch/static-page.d.ts +39 -0
  55. package/dist/patch/static-page.js +102 -0
  56. package/dist/patch/tanstack-start.d.ts +36 -8
  57. package/dist/patch/tanstack-start.js +118 -29
  58. package/dist/patch/vite-config.d.ts +5 -0
  59. package/dist/patch/vite-config.js +8 -0
  60. package/dist/patch/vite-env-types.d.ts +9 -0
  61. package/dist/patch/vite-env-types.js +19 -0
  62. package/dist/patch/vite-owning-config.d.ts +47 -0
  63. package/dist/patch/vite-owning-config.js +104 -19
  64. package/dist/plan/connect-steps.d.ts +10 -3
  65. package/dist/plan/connect-steps.js +22 -3
  66. package/dist/plan/framework-adapter.d.ts +5 -6
  67. package/dist/plan/framework-adapter.js +105 -11
  68. package/dist/plan/notices.js +4 -3
  69. package/dist/plan/plan-angular.d.ts +6 -0
  70. package/dist/plan/plan-angular.js +89 -0
  71. package/dist/plan/plan-electron-vite.d.ts +6 -3
  72. package/dist/plan/plan-electron-vite.js +39 -8
  73. package/dist/plan/plan-framework.d.ts +9 -47
  74. package/dist/plan/plan-framework.js +114 -323
  75. package/dist/plan/plan-types.d.ts +47 -2
  76. package/dist/plan/plan-vite.d.ts +67 -0
  77. package/dist/plan/plan-vite.js +256 -0
  78. package/dist/plan/plan.js +155 -69
  79. package/dist/plan/port-steps.d.ts +45 -0
  80. package/dist/plan/port-steps.js +119 -0
  81. package/dist/plan/preflight.d.ts +17 -3
  82. package/dist/plan/preflight.js +53 -23
  83. package/dist/plan/stop-hook-step.d.ts +4 -0
  84. package/dist/plan/stop-hook-step.js +49 -0
  85. package/dist/project/agent-rules.js +2 -0
  86. package/dist/register/mcp-clients.d.ts +13 -0
  87. package/dist/register/mcp-clients.js +22 -0
  88. package/dist/register/mcp.d.ts +26 -5
  89. package/dist/register/mcp.js +64 -8
  90. package/dist/run-types.d.ts +10 -0
  91. package/dist/run.js +132 -78
  92. package/package.json +3 -4
  93. 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>): 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', useStore) // pass the store itself, not () => store.getState()",
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(), { cart, user }))"],
63
- ['valtio', "registerStore('app', valtioStore(state))"],
64
- ['mobx', "registerStore('app', mobxStore(state))"],
65
- ['xstate', "registerStore('machine', xstateStore(actor))"],
66
- ['pinia', "registerStore('cart', piniaStore(useCartStore()))"],
67
- ['svelte', "registerStore('cart', svelteStore(cartStore))"],
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
- return STORE_LIBRARIES.filter(([dep]) => deps.has(dep) && !AUTO_DISCOVERED_DEPS.has(dep)).map(([, hint]) => hint);
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`
@@ -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
- export interface FrameworkSignals {
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>): PackageManager;
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[]): InstallCommand;
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[]): 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 {};
@@ -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
- export function detectPackageManager(lockfiles, nodeModulesMarkers) {
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: [...INSTALL_ARGS[pm], ...list, ...(QUIET_INSTALL_ARGS[pm] ?? []), ...extraFlags],
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 `${pm} ${[...INSTALL_ARGS[pm], ...list].join(' ')}`;
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 planDevScript(scripts: Readonly<Record<string, string>>, packageManager: string, serving: boolean): DevScriptPlan;
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, packageManager: string): string | undefined;
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, packageManager, serving) {
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
- // `npm` needs `run`; the others take the script name directly. Getting this wrong prints a
42
- // command that fails, which is worse than printing nothing.
43
- const command = 'npm' === packageManager ? `npm run ${name}` : `${packageManager} ${name}`;
44
- return { choice: DevScriptChoice.START, script: name, command };
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, packageManager) {
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, packageManager, false).command;
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 {};