@reticlehq/init 3.3.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 (81) 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 +45 -2
  4. package/dist/detect/detect.js +76 -5
  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.js +28 -2
  12. package/dist/diagnose/containerised-dev-server.d.ts +12 -4
  13. package/dist/diagnose/containerised-dev-server.js +36 -4
  14. package/dist/diagnose/csp-check.d.ts +12 -6
  15. package/dist/diagnose/csp-check.js +61 -13
  16. package/dist/diagnose/csp-doctor.d.ts +13 -1
  17. package/dist/diagnose/csp-doctor.js +2 -2
  18. package/dist/diagnose/csp-step.d.ts +27 -0
  19. package/dist/diagnose/csp-step.js +86 -0
  20. package/dist/diagnose/desktop-doctor.js +8 -2
  21. package/dist/diagnose/install-hint.d.ts +9 -1
  22. package/dist/diagnose/install-hint.js +17 -7
  23. package/dist/diagnose/install-retries.d.ts +16 -1
  24. package/dist/diagnose/install-retries.js +50 -3
  25. package/dist/diagnose/terminal-wrap.d.ts +18 -0
  26. package/dist/diagnose/terminal-wrap.js +63 -0
  27. package/dist/index.d.ts +6 -2
  28. package/dist/index.js +6 -2
  29. package/dist/no-package-json.d.ts +10 -0
  30. package/dist/no-package-json.js +121 -0
  31. package/dist/node-io.d.ts +9 -5
  32. package/dist/node-io.js +25 -7
  33. package/dist/patch/angular.d.ts +68 -0
  34. package/dist/patch/angular.js +216 -0
  35. package/dist/patch/astro-patch.d.ts +1 -1
  36. package/dist/patch/astro-patch.js +2 -2
  37. package/dist/patch/cra.d.ts +0 -2
  38. package/dist/patch/cra.js +0 -2
  39. package/dist/patch/electron-patch.d.ts +10 -1
  40. package/dist/patch/electron-patch.js +79 -27
  41. package/dist/patch/electron-vite-patch.js +1 -1
  42. package/dist/patch/nuxt-patch.d.ts +1 -1
  43. package/dist/patch/nuxt-patch.js +2 -2
  44. package/dist/patch/nuxt-snippets.js +6 -6
  45. package/dist/patch/remix.d.ts +24 -0
  46. package/dist/patch/remix.js +59 -0
  47. package/dist/patch/snippets.d.ts +57 -9
  48. package/dist/patch/snippets.js +119 -28
  49. package/dist/patch/static-page.d.ts +39 -0
  50. package/dist/patch/static-page.js +102 -0
  51. package/dist/patch/tanstack-start.d.ts +36 -8
  52. package/dist/patch/tanstack-start.js +118 -29
  53. package/dist/patch/vite-config.d.ts +5 -0
  54. package/dist/patch/vite-config.js +8 -0
  55. package/dist/patch/vite-owning-config.d.ts +34 -0
  56. package/dist/patch/vite-owning-config.js +63 -5
  57. package/dist/plan/connect-steps.d.ts +7 -1
  58. package/dist/plan/connect-steps.js +18 -1
  59. package/dist/plan/framework-adapter.d.ts +3 -1
  60. package/dist/plan/framework-adapter.js +99 -6
  61. package/dist/plan/notices.js +4 -3
  62. package/dist/plan/plan-angular.d.ts +6 -0
  63. package/dist/plan/plan-angular.js +89 -0
  64. package/dist/plan/plan-electron-vite.d.ts +6 -3
  65. package/dist/plan/plan-electron-vite.js +39 -8
  66. package/dist/plan/plan-framework.d.ts +8 -46
  67. package/dist/plan/plan-framework.js +81 -312
  68. package/dist/plan/plan-types.d.ts +29 -0
  69. package/dist/plan/plan-vite.d.ts +67 -0
  70. package/dist/plan/plan-vite.js +256 -0
  71. package/dist/plan/plan.js +38 -34
  72. package/dist/plan/port-steps.d.ts +45 -0
  73. package/dist/plan/port-steps.js +119 -0
  74. package/dist/plan/preflight.d.ts +17 -3
  75. package/dist/plan/preflight.js +53 -23
  76. package/dist/register/mcp.d.ts +18 -5
  77. package/dist/register/mcp.js +48 -8
  78. package/dist/run-types.d.ts +8 -0
  79. package/dist/run.js +111 -78
  80. package/package.json +3 -4
  81. 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,6 +166,14 @@ 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;
146
177
  /**
147
178
  * Every dependency this package DECLARES, by name, merged across the three blocks.
148
179
  *
@@ -155,6 +186,10 @@ export interface Detection {
155
186
  */
156
187
  dependencies?: Readonly<Record<string, string>> | undefined;
157
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";
158
193
  /** What identifies one framework in a project root: a dependency name, or a config file basename. */
159
194
  interface FrameworkSignals {
160
195
  /** `package.json` dependency names that name this framework outright. */
@@ -223,7 +258,13 @@ export declare function installCommandParts(pm: PackageManager, pkgs: string | r
223
258
  * how `npm i -D --legacy-peer-deps` would become a request to install a package called
224
259
  * `--legacy-peer-deps` on a manager that does not recognise the flag.
225
260
  */
226
- 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;
227
268
  /**
228
269
  * The install command as a human reads it — and deliberately NOT `installCommandParts` joined.
229
270
  *
@@ -232,5 +273,7 @@ extraFlags?: readonly string[]): InstallCommand;
232
273
  * directly for that reason; routing it through the parts would put `--no-audit --no-fund` in front of
233
274
  * every reader and teach them our noise-suppression as if it were part of installing Reticle.
234
275
  */
235
- 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;
236
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". */
@@ -356,11 +409,27 @@ export function installCommandParts(pm, pkgs,
356
409
  * how `npm i -D --legacy-peer-deps` would become a request to install a package called
357
410
  * `--legacy-peer-deps` on a manager that does not recognise the flag.
358
411
  */
359
- 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) {
360
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(' ');
361
424
  return {
362
- command: pm,
363
- 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
+ ],
364
433
  };
365
434
  }
366
435
  /**
@@ -371,7 +440,9 @@ extraFlags = []) {
371
440
  * directly for that reason; routing it through the parts would put `--no-audit --no-fund` in front of
372
441
  * every reader and teach them our noise-suppression as if it were part of installing Reticle.
373
442
  */
374
- 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) {
375
446
  const list = 'string' === typeof pkgs ? [pkgs] : pkgs;
376
- return `${pm} ${[...INSTALL_ARGS[pm], ...list].join(' ')}`;
447
+ return `${prefix} ${[...INSTALL_ARGS[pm], ...list].join(' ')}`;
377
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 {};