@reticlehq/init 3.6.0 → 3.8.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 (64) hide show
  1. package/dist/detect/detect.d.ts +9 -0
  2. package/dist/detect/detect.js +5 -1
  3. package/dist/detect/dev-script.d.ts +17 -0
  4. package/dist/detect/dev-script.js +128 -0
  5. package/dist/detect/dev-server-ports.d.ts +2 -0
  6. package/dist/detect/dev-server-ports.js +20 -0
  7. package/dist/detect/prettier-config.d.ts +21 -0
  8. package/dist/detect/prettier-config.js +38 -0
  9. package/dist/detect/workspace-apps.d.ts +11 -0
  10. package/dist/detect/workspace-apps.js +63 -0
  11. package/dist/detect/workspace-redirect.js +69 -10
  12. package/dist/diagnose/closing-hint.d.ts +5 -0
  13. package/dist/diagnose/closing-hint.js +18 -11
  14. package/dist/diagnose/init-failure.d.ts +2 -0
  15. package/dist/diagnose/init-failure.js +2 -0
  16. package/dist/gather-plan-input.d.ts +11 -0
  17. package/dist/gather-plan-input.js +468 -0
  18. package/dist/host.d.ts +6 -0
  19. package/dist/index.d.ts +11 -4
  20. package/dist/index.js +14 -4
  21. package/dist/no-package-json.js +62 -14
  22. package/dist/node-io.d.ts +1 -1
  23. package/dist/node-io.js +11 -2
  24. package/dist/patch/next-patch.d.ts +45 -9
  25. package/dist/patch/next-patch.js +93 -0
  26. package/dist/patch/nuxt-snippets.js +1 -1
  27. package/dist/patch/prettier-ignore.d.ts +34 -0
  28. package/dist/patch/prettier-ignore.js +49 -0
  29. package/dist/patch/python-snippets.d.ts +22 -0
  30. package/dist/patch/python-snippets.js +113 -0
  31. package/dist/patch/snippets.d.ts +10 -31
  32. package/dist/patch/snippets.js +42 -105
  33. package/dist/patch/static-page.d.ts +1 -0
  34. package/dist/patch/static-page.js +6 -8
  35. package/dist/patch/vite-config.js +11 -0
  36. package/dist/plan/connect-steps.d.ts +2 -0
  37. package/dist/plan/connect-steps.js +6 -0
  38. package/dist/plan/framework-adapter.d.ts +10 -0
  39. package/dist/plan/framework-adapter.js +122 -3
  40. package/dist/plan/notices.js +1 -1
  41. package/dist/plan/plan-angular.js +1 -1
  42. package/dist/plan/plan-framework.d.ts +1 -1
  43. package/dist/plan/plan-framework.js +140 -37
  44. package/dist/plan/plan-types.d.ts +50 -2
  45. package/dist/plan/plan-vite.js +15 -0
  46. package/dist/plan/plan.d.ts +1 -10
  47. package/dist/plan/plan.js +181 -126
  48. package/dist/plan/stop-hook-step.d.ts +30 -0
  49. package/dist/plan/stop-hook-step.js +154 -5
  50. package/dist/project/agent-rules.d.ts +1 -1
  51. package/dist/project/agent-rules.js +22 -17
  52. package/dist/project/refresh-rules.js +2 -1
  53. package/dist/register/codex-toml.d.ts +12 -0
  54. package/dist/register/codex-toml.js +44 -0
  55. package/dist/register/mcp.d.ts +15 -2
  56. package/dist/register/mcp.js +16 -0
  57. package/dist/register/prompt-hooks.d.ts +123 -0
  58. package/dist/register/prompt-hooks.js +449 -0
  59. package/dist/register/slash-command.d.ts +1 -1
  60. package/dist/register/slash-command.js +5 -4
  61. package/dist/run-types.d.ts +10 -0
  62. package/dist/run.d.ts +1 -1
  63. package/dist/run.js +15 -390
  64. package/package.json +2 -2
package/dist/run.js CHANGED
@@ -4,177 +4,25 @@
4
4
  * access goes through `InitIo` so the orchestration is unit-testable with an in-memory IO.
5
5
  */
6
6
  import { OnboardingPhase, OnboardingStepStatus } from '@reticlehq/core/telemetry';
7
- import { detectMcpClients } from './register/detect-clients.js';
8
- import { dirname, join } from 'node:path';
9
- import { CONTAINER_MARKERS, runsDevServer } from './diagnose/containerised-dev-server.js';
10
7
  import { explainInstallFailure } from './diagnose/install-retries.js';
11
- import { CSP_FILES } from './diagnose/csp-doctor.js';
12
8
  import { preflight } from './plan/preflight.js';
13
- import { devCommandFrom, devScriptBody } from './detect/dev-script.js';
14
- import { restartHint, FEEDBACK_HINT } from './diagnose/closing-hint.js';
9
+ import { devCommandFrom } from './detect/dev-script.js';
10
+ import { restartHint, COMMUNITY_HINT, FEEDBACK_HINT } from './diagnose/closing-hint.js';
15
11
  import { projectIdOf, rememberProjectOnDisk } from './project/remember-project.js';
16
- import { ANGULAR_WORKSPACE_FILE, detect, FORGE_RENDERER_CONFIGS, Framework, } from './detect/detect.js';
17
- import { ANGULAR_PROXY_PATH, angularEntry } from './patch/angular.js';
18
- import { HTML_INDEX_PATH } from './patch/static-page.js';
19
12
  import { initWithoutPackageJson } from './no-package-json.js';
20
13
  import { sdkPackagesDeclared, sdkPackagesPresent } from './sdk-packages.js';
21
14
  import { wasMcpRegistered } from './register/mcp-registered.js';
22
- import { pickAstroHost } from './patch/astro-host.js';
23
- import { ELECTRON_VITE_CONFIG_CANDIDATES, NEXT_CONFIG_CANDIDATES, PACKAGE_JSON, VITE_CONFIG_CANDIDATES, } from './detect/workspace-apps.js';
15
+ import { PACKAGE_JSON } from './detect/workspace-apps.js';
24
16
  import { redirectToWorkspaceApp } from './detect/workspace-redirect.js';
25
- import { enclosingWorkspaceRoot } from './detect/workspace-apps.js';
17
+ import { appsAt, enclosingWorkspaceRoot } from './detect/workspace-apps.js';
26
18
  import { isConnectStep } from './plan/connect-steps.js';
27
- import { CURSOR_RULE_PATH, RETICLE_MD_PATH } from './project/agent-rules.js';
28
- import { CLAUDE_SETTINGS_PATH } from './plan/stop-hook-step.js';
29
- import { CRA_ENV_PATH } from './patch/cra.js';
30
- import { NEXT_LAYOUT_CANDIDATES, NEXT_PAGES_APP_CANDIDATES } from './patch/next-patch.js';
31
19
  import { formatGeneratedSource } from './patch/format-generated.js';
32
- import { tanstackStartConnectPath } from './patch/tanstack-start.js';
33
- import { installedViteMajor } from './patch/vite-owning-config.js';
34
- /** CRA's bundled entry, in the order create-react-app itself generates them. */
35
- const CRA_ENTRY_CANDIDATES = ['src/index.tsx', 'src/index.jsx', 'src/index.ts', 'src/index.js'];
36
- /** Preview snippets must not mint a real machine credential or imply this token is usable. */
37
- const DRY_RUN_PAIRING_TOKEN = 'DRY_RUN_TOKEN_NOT_FOR_USE';
38
- function craEntryOf(io) {
39
- for (const path of CRA_ENTRY_CANDIDATES) {
40
- const source = io.readFile(path);
41
- if (source !== null)
42
- return { path, source };
43
- }
44
- return null;
45
- }
46
20
  import { DEPS_TARGET, RETICLE_CONFIG_FILE, MCP_TARGET, buildPlan, StepStatus, } from './plan/plan.js';
47
- import { claudeAvailableProbe, claudeHasReticle } from './register/mcp.js';
48
- import { reticleDevLocation } from './patch/next-patch.js';
49
- import { scanTestids, storeHints, scanStores } from './detect/capabilities.js';
50
- import { CLAUDE_PROJECT_CONFIG, CURSOR_PROJECT_MARKER } from './register/mcp-clients.js';
51
- import { deriveProjectId, packageName } from './project/project-id.js';
52
- import { VITE_DEV_MODULE_PATH, ELECTRON_VITE_DEV_MODULE_PATH, nuxtPluginPath, } from './patch/snippets.js';
53
- import { NUXT_CONFIG_CANDIDATES } from './patch/nuxt-patch.js';
54
- import { CLAUDE_COMMAND_PATH, CURSOR_COMMAND_PATH } from './register/slash-command.js';
55
- import { RETICLE_VERSION } from './version.js';
56
21
  import { InitFailure } from './diagnose/init-failure.js';
57
22
  import { resolveLockfiles } from './detect/lockfiles.js';
23
+ import { agentRootOf, gatherPlanInput } from './gather-plan-input.js';
58
24
  // Re-exported: it moved to its own module, and every existing importer says `run.js`.
59
25
  export { resolveLockfiles };
60
- const SVELTEKIT_HOOKS = 'src/hooks.client.ts';
61
- const REACT_ROUTER_ENTRY = 'app/entry.client.tsx';
62
- const TANSTACK_START_ROOT_CANDIDATES = [
63
- 'src/routes/__root.tsx',
64
- 'src/routes/__root.jsx',
65
- 'app/routes/__root.tsx',
66
- 'app/routes/__root.jsx',
67
- ];
68
- const SOURCE_FILE = /\.(tsx|jsx|ts|js|svelte|vue|astro)$/;
69
- /** Files read for the testid scan. A capabilities block is a hint; reading a whole repo for it is not. */
70
- const MAX_SCANNED_FILES = 200;
71
- /** Directories that never hold the app's own source, and are expensive or misleading to read. */
72
- const NOT_SOURCE_DIRS = new Set([
73
- 'node_modules',
74
- 'dist',
75
- 'build',
76
- 'out',
77
- 'coverage',
78
- 'target',
79
- 'public',
80
- 'static',
81
- '.next',
82
- '.svelte-kit',
83
- '.nuxt',
84
- ]);
85
- /** How far below the app root to look. Deep enough for `modules/users/UserList.tsx`, not a repo crawl. */
86
- const MAX_SCAN_DEPTH = 5;
87
- /**
88
- * Read a bounded set of the app's source files, for the `data-testid` scan.
89
- *
90
- * This used to walk the fixed list `src, src/components, src/pages, app, components`. Reported from
91
- * the field (#318) by a repo whose app is at `src/admin`: the scan read directories that were not
92
- * the app's and correctly reported finding nothing in them, so `init` said "no data-testid values
93
- * yet" about an app with several — which makes an agent go and write the ones already there. That
94
- * list had already grown once for a `frontend/` app, and a third report of the same shape is what
95
- * says the answer is not another name in a list. So: walk the app root, bounded by depth and by a
96
- * file count.
97
- */
98
- function readSourceFiles(io) {
99
- const out = [];
100
- const read = (path) => {
101
- const content = io.readFile(path);
102
- if (content !== null)
103
- out.push({ path, source: content });
104
- };
105
- for (const name of io.rootFiles()) {
106
- if (out.length >= MAX_SCANNED_FILES)
107
- return out;
108
- if (SOURCE_FILE.test(name))
109
- read(name);
110
- }
111
- const walk = (dir, depth) => {
112
- for (const name of io.listFiles(dir)) {
113
- if (out.length >= MAX_SCANNED_FILES)
114
- return;
115
- if (SOURCE_FILE.test(name))
116
- read(`${dir}/${name}`);
117
- }
118
- if (depth >= MAX_SCAN_DEPTH)
119
- return;
120
- for (const sub of io.listDirs(dir)) {
121
- if (out.length >= MAX_SCANNED_FILES)
122
- return;
123
- if (sub.startsWith('.') || NOT_SOURCE_DIRS.has(sub))
124
- continue;
125
- walk(`${dir}/${sub}`, depth + 1);
126
- }
127
- };
128
- for (const dir of io.listDirs('.')) {
129
- if (out.length >= MAX_SCANNED_FILES)
130
- return out;
131
- if (dir.startsWith('.') || NOT_SOURCE_DIRS.has(dir))
132
- continue;
133
- walk(dir, 1);
134
- }
135
- return out;
136
- }
137
- /** Direct dependency names, for naming the state libraries an app actually has. */
138
- function dependencyNames(pkg) {
139
- const p = (pkg ?? {});
140
- return new Set([
141
- ...Object.keys(p['dependencies'] ?? {}),
142
- ...Object.keys(p['devDependencies'] ?? {}),
143
- ]);
144
- }
145
- /** electron-vite and the unbundled Electron layouts this repo already ships. */
146
- const ELECTRON_MAIN_SOURCES = [
147
- 'src/main/index.ts',
148
- 'src/main/index.js',
149
- 'src/main/index.mts',
150
- 'src/main/index.mjs',
151
- 'electron/main.cjs',
152
- 'electron/main.js',
153
- 'electron/main.ts',
154
- 'src/main.ts',
155
- 'src/main.js',
156
- ];
157
- const ELECTRON_PRELOAD_SOURCES = [
158
- 'src/preload/index.ts',
159
- 'src/preload/index.js',
160
- 'src/preload/index.mts',
161
- 'src/preload/index.mjs',
162
- 'electron/preload.cjs',
163
- 'electron/preload.js',
164
- 'electron/preload.ts',
165
- 'src/preload.ts',
166
- 'src/preload.js',
167
- ];
168
- const ASTRO_CONFIG_CANDIDATES = [
169
- 'astro.config.mjs',
170
- 'astro.config.js',
171
- 'astro.config.ts',
172
- 'astro.config.cjs',
173
- ];
174
- /** Where a conventional Astro app keeps the layout every page wraps itself in. */
175
- const ASTRO_LAYOUTS_DIR = 'src/layouts';
176
- /** Also searched: an app with no layouts directory renders the document straight from a page. */
177
- const ASTRO_PAGES_DIR = 'src/pages';
178
26
  /**
179
27
  * `⚠` means WORK LEFT TO DO and nothing else — it is what an agent (and the release gate) counts to
180
28
  * decide whether the install finished. A notice gets its own mark so "steps remaining" can reach zero
@@ -204,234 +52,6 @@ const STATUS_SYMBOL = {
204
52
  [StepStatus.SKIP]: '–',
205
53
  [StepStatus.NOTICE]: 'ℹ',
206
54
  };
207
- function firstPresent(files, candidates) {
208
- for (const c of candidates)
209
- if (files.has(c))
210
- return c;
211
- return null;
212
- }
213
- function firstReadable(io, candidates) {
214
- for (const path of candidates) {
215
- const source = io.readFile(path);
216
- if (source !== null)
217
- return { path, source };
218
- }
219
- return null;
220
- }
221
- /**
222
- * The directory the human's agent runs in, when it is not the app's directory.
223
- *
224
- * `undefined` for a single-package repo, which keeps every agent-file path project-relative exactly
225
- * as it was — the two roots are the same there, which is why writing them beside the app went
226
- * unnoticed until a repo with its app at `src/admin` reported it.
227
- */
228
- function agentRootOf(options) {
229
- const root = options.agentRoot;
230
- return root === undefined || root === options.cwd ? undefined : root;
231
- }
232
- /** What the Angular steps read: the workspace file, the entry it names, and our proxy module. */
233
- function angularInputs(io) {
234
- const workspace = io.readFile(ANGULAR_WORKSPACE_FILE);
235
- const entryPath = angularEntry(workspace);
236
- const entry = io.readFile(entryPath);
237
- return {
238
- angularWorkspace: null === workspace ? null : { path: ANGULAR_WORKSPACE_FILE, source: workspace },
239
- angularEntry: null === entry ? null : { path: entryPath, source: entry },
240
- angularProxySource: io.readFile(ANGULAR_PROXY_PATH),
241
- };
242
- }
243
- function gatherPlanInput(options, io, pkg) {
244
- // Stable identity derived from the app's package.json name + root, so it survives port changes.
245
- const projectId = deriveProjectId(packageName(pkg), options.cwd);
246
- const rootFiles = new Set(io.rootFiles());
247
- const nodeModulesMarkers = new Set(io.listFiles('node_modules'));
248
- const detectInput = {
249
- pkg: 'object' === typeof pkg && pkg !== null ? pkg : {},
250
- configFiles: rootFiles,
251
- // Walk up for the lockfile so a monorepo sub-package picks the workspace's package manager —
252
- // unless this package's own installed tree already answers it, which outranks an inherited one.
253
- lockfiles: resolveLockfiles(rootFiles, options.cwd, io, nodeModulesMarkers),
254
- // An already-installed tree names its own manager, which matters when no lockfile is committed.
255
- nodeModulesMarkers,
256
- };
257
- const detection = detect(detectInput);
258
- // Forge has no `vite.config.*`; its renderer config IS the Vite config the plugin belongs in, and
259
- // handing it over under that name is what lets the ordinary Vite patcher do the work.
260
- const vitePath = firstPresent(rootFiles, Framework.ELECTRON_FORGE === detection.framework
261
- ? FORGE_RENDERER_CONFIGS
262
- : VITE_CONFIG_CANDIDATES);
263
- const viteSource = null === vitePath ? null : io.readFile(vitePath);
264
- const viteConfig = vitePath !== null && viteSource !== null ? { path: vitePath, source: viteSource } : null;
265
- const electronVitePath = firstPresent(rootFiles, ELECTRON_VITE_CONFIG_CANDIDATES);
266
- const electronViteSource = null === electronVitePath ? null : io.readFile(electronVitePath);
267
- const electronViteConfig = electronVitePath !== null && electronViteSource !== null
268
- ? { path: electronVitePath, source: electronViteSource }
269
- : null;
270
- const electronMain = firstReadable(io, ELECTRON_MAIN_SOURCES);
271
- const electronPreload = firstReadable(io, ELECTRON_PRELOAD_SOURCES);
272
- // Global MCP registration targets each agent that's present: Claude via its CLI, Cursor via its
273
- // global config file. Only probe when the MCP step is in play.
274
- const availableProbe = claudeAvailableProbe();
275
- const claudeCli = options.mcp ? io.probe(availableProbe.command, availableProbe.args) : false;
276
- // Read from Claude's config, never `claude mcp get`: that launches the server — see claudeHasReticle.
277
- const mcpExists = claudeCli ? claudeHasReticle(io, io.cwd()) : false;
278
- // Every MCP client this machine shows evidence of. Conservative and one-directional: we write
279
- // into a config a client ALREADY has, and never create ~/.gemini or ~/.codeium for somebody who
280
- // does not use them.
281
- const detectedClients = options.mcp ? detectMcpClients(io) : [];
282
- const astroPath = firstPresent(rootFiles, ASTRO_CONFIG_CANDIDATES);
283
- const astroSource = null === astroPath ? null : io.readFile(astroPath);
284
- // Which file owns the DOCUMENT, not how many files sit in a directory. The old rule ("exactly one
285
- // .astro in src/layouts") fired on neither real Astro app: one has no layouts directory and
286
- // renders from src/pages/index.astro, the other has three files there of which two are partials.
287
- // `</body>` is the discriminator — see astro-host.
288
- const astroCandidates = [ASTRO_LAYOUTS_DIR, ASTRO_PAGES_DIR].flatMap((dir) => io
289
- .listFiles(dir)
290
- .filter((f) => f.endsWith('.astro'))
291
- .map((f) => ({ path: `${dir}/${f}`, source: io.readFile(`${dir}/${f}`) }))
292
- .filter((c) => c.source !== null));
293
- const astroHost = pickAstroHost(astroCandidates);
294
- const layoutRelPath = astroHost?.path ?? null;
295
- const astroLayoutSource = astroHost?.source ?? null;
296
- const nuxtConfigPath = firstPresent(rootFiles, NUXT_CONFIG_CANDIDATES);
297
- const nuxtConfigSource = null === nuxtConfigPath ? null : io.readFile(nuxtConfigPath);
298
- // Nuxt 4's default srcDir is `app/`, Nuxt 3's is the root — and a plugin written into the
299
- // directory Nuxt does not scan is never registered, silently.
300
- const nuxtHasAppDir = io.listDirs('.').includes('app');
301
- const nextConfigFile = firstPresent(rootFiles, NEXT_CONFIG_CANDIDATES);
302
- // App Router first; a Pages Router app has no layout, and its mount point is pages/_app.
303
- const layoutPath = NEXT_LAYOUT_CANDIDATES.find((p) => io.exists(p)) ??
304
- NEXT_PAGES_APP_CANDIDATES.find((p) => io.exists(p)) ??
305
- null;
306
- const layoutSource = null === layoutPath ? null : io.readFile(layoutPath);
307
- // Where the component goes depends on WHICH router mounts it: `pages/` routes on presence, so a
308
- // component there becomes a broken route; `app/` routes on filename, so a sibling is inert.
309
- const devLocation = reticleDevLocation(layoutPath ?? 'app/layout.tsx', detection.typescript);
310
- // Read once: both the testid scan and the store scan want the same bounded set of files.
311
- const sourceFiles = readSourceFiles(io);
312
- const agentRoot = agentRootOf(options);
313
- const agentFile = (relPath) => agentRoot === undefined ? relPath : join(agentRoot, relPath);
314
- const cspSources = {};
315
- for (const file of CSP_FILES) {
316
- const source = io.readFile(file);
317
- if (null !== source)
318
- cspSources[file] = source;
319
- }
320
- return {
321
- detection,
322
- captureBodies: options.captureBodies,
323
- hooks: options.hooks,
324
- cspSources,
325
- claudeCli,
326
- mcpExists,
327
- insideClaudeCode: io.host.insideClaudeCode(),
328
- claudeProjectConfig: options.mcp ? io.readFile(agentFile(CLAUDE_PROJECT_CONFIG)) : undefined,
329
- platform: process.platform,
330
- detectedClients,
331
- cursorProjectPresent: io.exists(CURSOR_PROJECT_MARKER),
332
- // Looked for beside the app AND one level up, because the app is routinely a subdirectory of the
333
- // repo that containerises it — `frontend/` under a root `docker-compose.yml` is the shape this
334
- // came from. Only the first match is reported; the note is the same whichever file found it.
335
- ...(() => {
336
- // Only a file that RUNS the dev server: a production-only image is the common case, and
337
- // React Router's template ships one. See runsDevServer.
338
- const devScript = devScriptBody(pkg);
339
- const marker = CONTAINER_MARKERS.find((name) => [name, join('..', name)].some((path) => {
340
- const content = io.readFile(path);
341
- return null !== content && runsDevServer(name, content, devScript);
342
- }));
343
- return marker === undefined ? {} : { containerMarker: marker };
344
- })(),
345
- viteConfig,
346
- electronViteConfig,
347
- electronPreload,
348
- electronMain,
349
- astroConfig: astroPath !== null && astroSource !== null ? { path: astroPath, source: astroSource } : null,
350
- astroLayout: layoutRelPath !== null && astroLayoutSource !== null
351
- ? { path: layoutRelPath, source: astroLayoutSource }
352
- : null,
353
- astroEnvDts: io.readFile('src/env.d.ts'),
354
- astroReticleDev: io.readFile('src/components/ReticleDev.ts'),
355
- nextConfigFile,
356
- nextConfigSource: null === nextConfigFile ? null : io.readFile(nextConfigFile),
357
- nextLayout: layoutPath !== null && layoutSource !== null
358
- ? { path: layoutPath, source: layoutSource }
359
- : null,
360
- // Capabilities: scanned, never asked for. Bounded — a hint for the agent, not a repo index.
361
- testids: scanTestids(sourceFiles.map((f) => f.source)),
362
- storeHints: storeHints(dependencyNames(pkg), sourceFiles),
363
- foundStores: scanStores(sourceFiles, dependencyNames(pkg)),
364
- nextFoundStores: scanStores(sourceFiles, dependencyNames(pkg), dirname(devLocation.path)),
365
- viteDevModuleExists: io.exists(detection.framework === Framework.ELECTRON_VITE
366
- ? ELECTRON_VITE_DEV_MODULE_PATH
367
- : VITE_DEV_MODULE_PATH),
368
- nextReticleDevPath: devLocation.path,
369
- nextReticleDevImport: devLocation.importSpecifier,
370
- nextReticleDevExists: io.exists(devLocation.path),
371
- nextReticleDevSource: io.readFile(devLocation.path),
372
- svelteKitHooksExists: io.exists(SVELTEKIT_HOOKS),
373
- svelteKitHooksSource: io.readFile(SVELTEKIT_HOOKS),
374
- reactRouterEntryExists: io.exists(REACT_ROUTER_ENTRY),
375
- // The SOURCE, so an entry the app already owns is ADDED TO rather than replaced — it is an
376
- // override of React Router's default, and everything in it is load-bearing.
377
- reactRouterEntrySource: io.readFile(REACT_ROUTER_ENTRY),
378
- nuxtConfig: nuxtConfigPath !== null && nuxtConfigSource !== null
379
- ? { path: nuxtConfigPath, source: nuxtConfigSource }
380
- : null,
381
- nuxtHasAppDir,
382
- viteMajor: installedViteMajor(io, detectInput.pkg),
383
- nuxtPluginExists: io.exists(nuxtPluginPath(nuxtHasAppDir)),
384
- nuxtPluginSource: io.readFile(nuxtPluginPath(nuxtHasAppDir)),
385
- ...(() => {
386
- const root = TANSTACK_START_ROOT_CANDIDATES.find((file) => io.exists(file));
387
- return root === undefined
388
- ? {}
389
- : {
390
- tanstackStartRoot: root,
391
- tanstackStartRootSource: io.readFile(root),
392
- tanstackStartConnectExists: io.exists(tanstackStartConnectPath(root)),
393
- tanstackStartConnectSource: io.readFile(tanstackStartConnectPath(root)),
394
- };
395
- })(),
396
- ...(Framework.ANGULAR === detection.framework ? angularInputs(io) : {}),
397
- ...(Framework.HTML === detection.framework
398
- ? { htmlIndexSource: io.readFile(HTML_INDEX_PATH) }
399
- : {}),
400
- craEntry: craEntryOf(io),
401
- craEnv: io.readFile(CRA_ENV_PATH),
402
- pairingToken: options.dryRun ? DRY_RUN_PAIRING_TOKEN : io.host.pairingToken(),
403
- installSource: io.host.installSource(),
404
- reticleConfigExists: io.exists(RETICLE_CONFIG_FILE),
405
- // The CONTENT, so a config that exists can be checked rather than trusted — a `"port"` set to
406
- // the app's own dev-server port used to survive every re-run of `init`.
407
- reticleConfigSource: io.readFile(RETICLE_CONFIG_FILE),
408
- // Read the agent instruction files so the rule merge stays idempotent across re-runs — from the
409
- // agent's own root, or the merge would idempotently check a file it is not going to write.
410
- claudeMdContent: io.readFile(agentFile('CLAUDE.md')),
411
- agentsMdContent: io.readFile(agentFile('AGENTS.md')),
412
- reticleMdContent: io.readFile(agentFile(RETICLE_MD_PATH)),
413
- cursorRuleContent: io.readFile(agentFile(CURSOR_RULE_PATH)),
414
- claudeCommandContent: io.readFile(agentFile(CLAUDE_COMMAND_PATH)),
415
- cursorCommandContent: io.readFile(agentFile(CURSOR_COMMAND_PATH)),
416
- // Read only to MERGE into: the settings file is the user's, and other tools write in it too.
417
- claudeSettingsContent: true === options.hooks ? io.readFile(agentFile(CLAUDE_SETTINGS_PATH)) : undefined,
418
- ...(agentRoot === undefined
419
- ? {}
420
- : {
421
- agentFileRoot: agentRoot,
422
- // The config the AGENT's cwd already has, if any — see agentRootConfigStep.
423
- agentRootConfigSource: io.readFile(agentFile(RETICLE_CONFIG_FILE)),
424
- }),
425
- options: {
426
- port: options.port,
427
- mcp: options.mcp,
428
- install: options.install,
429
- projectId,
430
- // The SDK must match the CLI asking for it — see pinnedPackages.
431
- sdkVersion: RETICLE_VERSION,
432
- },
433
- };
434
- }
435
55
  /** Where workspace tooling conventionally puts packages. */
436
56
  /** pnpm's workspace declaration, read when present — it is authoritative about where packages live. */
437
57
  const SKIPPED_DETAIL = 'skipped — the dependency install above failed, and wiring the app to a package that is not ' +
@@ -678,7 +298,9 @@ function withWorkspaceRoot(options, io) {
678
298
  if (options.agentRoot !== undefined || true === options.redirected)
679
299
  return options;
680
300
  const root = enclosingWorkspaceRoot(options.cwd, io);
681
- return root === undefined ? options : { ...options, agentRoot: root };
301
+ if (root === undefined)
302
+ return options;
303
+ return { ...options, agentRoot: root, severalApps: 1 < appsAt(root, io).length };
682
304
  }
683
305
  export function runInit(options, io) {
684
306
  const result = runInitSteps(withWorkspaceRoot(options, io), io);
@@ -695,6 +317,9 @@ export function runInit(options, io) {
695
317
  if (true !== options.redirected && true !== options.continuesToRuntime) {
696
318
  io.print('');
697
319
  io.print(FEEDBACK_HINT);
320
+ if (result.ok && true !== options.dryRun) {
321
+ io.print(COMMUNITY_HINT);
322
+ }
698
323
  }
699
324
  return result;
700
325
  }
@@ -743,11 +368,11 @@ function runInitSteps(options, io) {
743
368
  // `--app frontend`, the flag that exists for exactly this, was never read because the bail came
744
369
  // first. Reported twice by the same user, who tried the documented workaround and hit the same
745
370
  // wall. Discovery already handles the case (it scans top-level directories, not just declared
746
- // workspaces); it was simply unreachable.
747
- //
748
- // `'{}'` because the redirect only needs the manifest to ask "is THIS directory the app", and a
749
- // directory with no package.json is definitively not.
371
+ // workspaces); it was simply unreachable. `'{}'`: a directory with no manifest is not the app.
750
372
  const redirectedEarly = redirectToWorkspaceApp(options, io, pkgRaw ?? {}, runInit);
373
+ // Listing the apps below still counts as "no app here", in the same series as before.
374
+ if (true === redirectedEarly?.listedAppsBelow && !options.dryRun)
375
+ io.host.reportOutcome({ ok: false, reason: InitFailure.NO_PACKAGE_JSON });
751
376
  if (redirectedEarly !== null)
752
377
  return redirectedEarly;
753
378
  if (null === pkgRaw)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reticlehq/init",
3
- "version": "3.6.0",
3
+ "version": "3.8.0",
4
4
  "description": "Reticle project scaffolder — detects the framework, writes the config, patches the build and registers the MCP server. The build-time half of `reticle init`.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,7 +13,7 @@
13
13
  "./package.json": "./package.json"
14
14
  },
15
15
  "dependencies": {
16
- "@reticlehq/core": "3.6.0"
16
+ "@reticlehq/core": "3.8.0"
17
17
  },
18
18
  "license": "FSL-1.1-ALv2",
19
19
  "author": "Reticle contributors",