@reticlehq/init 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/detect/capabilities.d.ts +4 -1
- package/dist/detect/capabilities.js +45 -9
- package/dist/detect/detect.d.ts +58 -4
- package/dist/detect/detect.js +117 -7
- package/dist/detect/dev-script.d.ts +26 -2
- package/dist/detect/dev-script.js +80 -7
- package/dist/detect/non-js-project.d.ts +1 -1
- package/dist/detect/non-js-project.js +6 -1
- package/dist/detect/workspace-apps.d.ts +21 -0
- package/dist/detect/workspace-apps.js +111 -12
- package/dist/detect/workspace-redirect.d.ts +2 -1
- package/dist/detect/workspace-redirect.js +28 -2
- package/dist/diagnose/containerised-dev-server.d.ts +12 -4
- package/dist/diagnose/containerised-dev-server.js +36 -4
- package/dist/diagnose/csp-check.d.ts +12 -6
- package/dist/diagnose/csp-check.js +61 -13
- package/dist/diagnose/csp-doctor.d.ts +33 -2
- package/dist/diagnose/csp-doctor.js +103 -6
- package/dist/diagnose/csp-step.d.ts +27 -0
- package/dist/diagnose/csp-step.js +86 -0
- package/dist/diagnose/desktop-doctor.d.ts +1 -1
- package/dist/diagnose/desktop-doctor.js +8 -2
- package/dist/diagnose/install-hint.d.ts +9 -1
- package/dist/diagnose/install-hint.js +17 -7
- package/dist/diagnose/install-retries.d.ts +18 -2
- package/dist/diagnose/install-retries.js +50 -3
- package/dist/diagnose/terminal-wrap.d.ts +18 -0
- package/dist/diagnose/terminal-wrap.js +63 -0
- package/dist/host.d.ts +6 -0
- package/dist/host.js +3 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +8 -3
- package/dist/no-package-json.d.ts +10 -0
- package/dist/no-package-json.js +121 -0
- package/dist/node-io.d.ts +9 -5
- package/dist/node-io.js +25 -7
- package/dist/patch/angular.d.ts +68 -0
- package/dist/patch/angular.js +216 -0
- package/dist/patch/astro-patch.d.ts +48 -13
- package/dist/patch/astro-patch.js +151 -43
- package/dist/patch/cra.d.ts +0 -2
- package/dist/patch/cra.js +0 -2
- package/dist/patch/electron-patch.d.ts +10 -1
- package/dist/patch/electron-patch.js +79 -27
- package/dist/patch/electron-vite-patch.js +1 -1
- package/dist/patch/nuxt-patch.d.ts +1 -1
- package/dist/patch/nuxt-patch.js +2 -2
- package/dist/patch/nuxt-snippets.d.ts +44 -0
- package/dist/patch/nuxt-snippets.js +135 -0
- package/dist/patch/remix.d.ts +24 -0
- package/dist/patch/remix.js +59 -0
- package/dist/patch/snippets.d.ts +58 -57
- package/dist/patch/snippets.js +181 -202
- package/dist/patch/static-page.d.ts +39 -0
- package/dist/patch/static-page.js +102 -0
- package/dist/patch/tanstack-start.d.ts +36 -8
- package/dist/patch/tanstack-start.js +118 -29
- package/dist/patch/vite-config.d.ts +5 -0
- package/dist/patch/vite-config.js +8 -0
- package/dist/patch/vite-env-types.d.ts +9 -0
- package/dist/patch/vite-env-types.js +19 -0
- package/dist/patch/vite-owning-config.d.ts +47 -0
- package/dist/patch/vite-owning-config.js +104 -19
- package/dist/plan/connect-steps.d.ts +10 -3
- package/dist/plan/connect-steps.js +22 -3
- package/dist/plan/framework-adapter.d.ts +5 -6
- package/dist/plan/framework-adapter.js +105 -11
- package/dist/plan/notices.js +4 -3
- package/dist/plan/plan-angular.d.ts +6 -0
- package/dist/plan/plan-angular.js +89 -0
- package/dist/plan/plan-electron-vite.d.ts +6 -3
- package/dist/plan/plan-electron-vite.js +39 -8
- package/dist/plan/plan-framework.d.ts +9 -47
- package/dist/plan/plan-framework.js +114 -323
- package/dist/plan/plan-types.d.ts +47 -2
- package/dist/plan/plan-vite.d.ts +67 -0
- package/dist/plan/plan-vite.js +256 -0
- package/dist/plan/plan.js +155 -69
- package/dist/plan/port-steps.d.ts +45 -0
- package/dist/plan/port-steps.js +119 -0
- package/dist/plan/preflight.d.ts +17 -3
- package/dist/plan/preflight.js +53 -23
- package/dist/plan/stop-hook-step.d.ts +4 -0
- package/dist/plan/stop-hook-step.js +49 -0
- package/dist/project/agent-rules.js +2 -0
- package/dist/register/mcp-clients.d.ts +13 -0
- package/dist/register/mcp-clients.js +22 -0
- package/dist/register/mcp.d.ts +26 -5
- package/dist/register/mcp.js +64 -8
- package/dist/run-types.d.ts +10 -0
- package/dist/run.js +132 -78
- package/package.json +3 -4
- package/NOTICE +0 -5
package/dist/run.js
CHANGED
|
@@ -6,23 +6,30 @@
|
|
|
6
6
|
import { OnboardingPhase, OnboardingStepStatus } from '@reticlehq/core/telemetry';
|
|
7
7
|
import { detectMcpClients } from './register/detect-clients.js';
|
|
8
8
|
import { dirname, join } from 'node:path';
|
|
9
|
-
import { CONTAINER_MARKERS } from './diagnose/containerised-dev-server.js';
|
|
9
|
+
import { CONTAINER_MARKERS, runsDevServer } from './diagnose/containerised-dev-server.js';
|
|
10
|
+
import { explainInstallFailure } from './diagnose/install-retries.js';
|
|
10
11
|
import { CSP_FILES } from './diagnose/csp-doctor.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { devCommandFrom } from './detect/dev-script.js';
|
|
12
|
+
import { preflight } from './plan/preflight.js';
|
|
13
|
+
import { devCommandFrom, devScriptBody } from './detect/dev-script.js';
|
|
14
14
|
import { restartHint, FEEDBACK_HINT } from './diagnose/closing-hint.js';
|
|
15
15
|
import { projectIdOf, rememberProjectOnDisk } from './project/remember-project.js';
|
|
16
|
-
import { detect, Framework, UiLibrary } from './detect/detect.js';
|
|
16
|
+
import { ANGULAR_WORKSPACE_FILE, detect, FORGE_RENDERER_CONFIGS, Framework, UiLibrary, } 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
|
+
import { initWithoutPackageJson } from './no-package-json.js';
|
|
17
20
|
import { wasMcpRegistered } from './register/mcp-registered.js';
|
|
18
21
|
import { pickAstroHost } from './patch/astro-host.js';
|
|
19
22
|
import { ELECTRON_VITE_CONFIG_CANDIDATES, NEXT_CONFIG_CANDIDATES, PACKAGE_JSON, VITE_CONFIG_CANDIDATES, } from './detect/workspace-apps.js';
|
|
20
23
|
import { redirectToWorkspaceApp } from './detect/workspace-redirect.js';
|
|
24
|
+
import { enclosingWorkspaceRoot } from './detect/workspace-apps.js';
|
|
21
25
|
import { isConnectStep } from './plan/connect-steps.js';
|
|
22
26
|
import { CURSOR_RULE_PATH, RETICLE_MD_PATH } from './project/agent-rules.js';
|
|
27
|
+
import { CLAUDE_SETTINGS_PATH } from './plan/stop-hook-step.js';
|
|
23
28
|
import { CRA_ENV_PATH } from './patch/cra.js';
|
|
24
29
|
import { NEXT_LAYOUT_CANDIDATES, NEXT_PAGES_APP_CANDIDATES } from './patch/next-patch.js';
|
|
25
30
|
import { formatGeneratedSource } from './patch/format-generated.js';
|
|
31
|
+
import { tanstackStartConnectPath } from './patch/tanstack-start.js';
|
|
32
|
+
import { installedViteMajor } from './patch/vite-owning-config.js';
|
|
26
33
|
/** CRA's bundled entry, in the order create-react-app itself generates them. */
|
|
27
34
|
const CRA_ENTRY_CANDIDATES = ['src/index.tsx', 'src/index.jsx', 'src/index.ts', 'src/index.js'];
|
|
28
35
|
function craEntryOf(io) {
|
|
@@ -34,12 +41,12 @@ function craEntryOf(io) {
|
|
|
34
41
|
return null;
|
|
35
42
|
}
|
|
36
43
|
import { DEPS_TARGET, RETICLE_CONFIG_FILE, frameworkPackages, MCP_TARGET, buildPlan, StepStatus, } from './plan/plan.js';
|
|
37
|
-
import { claudeAvailableProbe,
|
|
44
|
+
import { claudeAvailableProbe, claudeHasReticle } from './register/mcp.js';
|
|
38
45
|
import { reticleDevLocation } from './patch/next-patch.js';
|
|
39
46
|
import { scanTestids, storeHints, scanStores } from './detect/capabilities.js';
|
|
40
|
-
import { CURSOR_PROJECT_MARKER } from './register/mcp-clients.js';
|
|
47
|
+
import { CLAUDE_PROJECT_CONFIG, CURSOR_PROJECT_MARKER } from './register/mcp-clients.js';
|
|
41
48
|
import { deriveProjectId, packageName } from './project/project-id.js';
|
|
42
|
-
import { VITE_DEV_MODULE_PATH, ELECTRON_VITE_DEV_MODULE_PATH,
|
|
49
|
+
import { VITE_DEV_MODULE_PATH, ELECTRON_VITE_DEV_MODULE_PATH, nuxtPluginPath, } from './patch/snippets.js';
|
|
43
50
|
import { NUXT_CONFIG_CANDIDATES } from './patch/nuxt-patch.js';
|
|
44
51
|
import { CLAUDE_COMMAND_PATH, CURSOR_COMMAND_PATH } from './register/slash-command.js';
|
|
45
52
|
import { RETICLE_VERSION } from './version.js';
|
|
@@ -218,6 +225,17 @@ function agentRootOf(options) {
|
|
|
218
225
|
const root = options.agentRoot;
|
|
219
226
|
return root === undefined || root === options.cwd ? undefined : root;
|
|
220
227
|
}
|
|
228
|
+
/** What the Angular steps read: the workspace file, the entry it names, and our proxy module. */
|
|
229
|
+
function angularInputs(io) {
|
|
230
|
+
const workspace = io.readFile(ANGULAR_WORKSPACE_FILE);
|
|
231
|
+
const entryPath = angularEntry(workspace);
|
|
232
|
+
const entry = io.readFile(entryPath);
|
|
233
|
+
return {
|
|
234
|
+
angularWorkspace: null === workspace ? null : { path: ANGULAR_WORKSPACE_FILE, source: workspace },
|
|
235
|
+
angularEntry: null === entry ? null : { path: entryPath, source: entry },
|
|
236
|
+
angularProxySource: io.readFile(ANGULAR_PROXY_PATH),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
221
239
|
function gatherPlanInput(options, io, pkg) {
|
|
222
240
|
// Stable identity derived from the app's package.json name + root, so it survives port changes.
|
|
223
241
|
const projectId = deriveProjectId(packageName(pkg), options.cwd);
|
|
@@ -233,7 +251,11 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
233
251
|
nodeModulesMarkers,
|
|
234
252
|
};
|
|
235
253
|
const detection = detect(detectInput);
|
|
236
|
-
|
|
254
|
+
// Forge has no `vite.config.*`; its renderer config IS the Vite config the plugin belongs in, and
|
|
255
|
+
// handing it over under that name is what lets the ordinary Vite patcher do the work.
|
|
256
|
+
const vitePath = firstPresent(rootFiles, Framework.ELECTRON_FORGE === detection.framework
|
|
257
|
+
? FORGE_RENDERER_CONFIGS
|
|
258
|
+
: VITE_CONFIG_CANDIDATES);
|
|
237
259
|
const viteSource = null === vitePath ? null : io.readFile(vitePath);
|
|
238
260
|
const viteConfig = vitePath !== null && viteSource !== null ? { path: vitePath, source: viteSource } : null;
|
|
239
261
|
const electronVitePath = firstPresent(rootFiles, ELECTRON_VITE_CONFIG_CANDIDATES);
|
|
@@ -247,8 +269,8 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
247
269
|
// global config file. Only probe when the MCP step is in play.
|
|
248
270
|
const availableProbe = claudeAvailableProbe();
|
|
249
271
|
const claudeCli = options.mcp ? io.probe(availableProbe.command, availableProbe.args) : false;
|
|
250
|
-
|
|
251
|
-
const mcpExists = claudeCli ? io
|
|
272
|
+
// Read from Claude's config, never `claude mcp get`: that launches the server — see claudeHasReticle.
|
|
273
|
+
const mcpExists = claudeCli ? claudeHasReticle(io, io.cwd()) : false;
|
|
252
274
|
// Every MCP client this machine shows evidence of. Conservative and one-directional: we write
|
|
253
275
|
// into a config a client ALREADY has, and never create ~/.gemini or ~/.codeium for somebody who
|
|
254
276
|
// does not use them.
|
|
@@ -294,9 +316,12 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
294
316
|
return {
|
|
295
317
|
detection,
|
|
296
318
|
captureBodies: options.captureBodies,
|
|
319
|
+
hooks: options.hooks,
|
|
297
320
|
cspSources,
|
|
298
321
|
claudeCli,
|
|
299
322
|
mcpExists,
|
|
323
|
+
insideClaudeCode: io.host.insideClaudeCode(),
|
|
324
|
+
claudeProjectConfig: options.mcp ? io.readFile(agentFile(CLAUDE_PROJECT_CONFIG)) : undefined,
|
|
300
325
|
platform: process.platform,
|
|
301
326
|
detectedClients,
|
|
302
327
|
cursorProjectPresent: io.exists(CURSOR_PROJECT_MARKER),
|
|
@@ -304,7 +329,13 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
304
329
|
// repo that containerises it — `frontend/` under a root `docker-compose.yml` is the shape this
|
|
305
330
|
// came from. Only the first match is reported; the note is the same whichever file found it.
|
|
306
331
|
...(() => {
|
|
307
|
-
|
|
332
|
+
// Only a file that RUNS the dev server: a production-only image is the common case, and
|
|
333
|
+
// React Router's template ships one. See runsDevServer.
|
|
334
|
+
const devScript = devScriptBody(pkg);
|
|
335
|
+
const marker = CONTAINER_MARKERS.find((name) => [name, join('..', name)].some((path) => {
|
|
336
|
+
const content = io.readFile(path);
|
|
337
|
+
return null !== content && runsDevServer(name, content, devScript);
|
|
338
|
+
}));
|
|
308
339
|
return marker === undefined ? {} : { containerMarker: marker };
|
|
309
340
|
})(),
|
|
310
341
|
viteConfig,
|
|
@@ -316,6 +347,7 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
316
347
|
? { path: layoutRelPath, source: astroLayoutSource }
|
|
317
348
|
: null,
|
|
318
349
|
astroEnvDts: io.readFile('src/env.d.ts'),
|
|
350
|
+
astroReticleDev: io.readFile('src/components/ReticleDev.ts'),
|
|
319
351
|
nextConfigFile,
|
|
320
352
|
nextConfigSource: null === nextConfigFile ? null : io.readFile(nextConfigFile),
|
|
321
353
|
nextLayout: layoutPath !== null && layoutSource !== null
|
|
@@ -323,7 +355,7 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
323
355
|
: null,
|
|
324
356
|
// Capabilities: scanned, never asked for. Bounded — a hint for the agent, not a repo index.
|
|
325
357
|
testids: scanTestids(sourceFiles.map((f) => f.source)),
|
|
326
|
-
storeHints: storeHints(dependencyNames(pkg)),
|
|
358
|
+
storeHints: storeHints(dependencyNames(pkg), sourceFiles),
|
|
327
359
|
foundStores: scanStores(sourceFiles, dependencyNames(pkg)),
|
|
328
360
|
nextFoundStores: scanStores(sourceFiles, dependencyNames(pkg), dirname(devLocation.path)),
|
|
329
361
|
viteDevModuleExists: io.exists(detection.framework === Framework.ELECTRON_VITE
|
|
@@ -334,6 +366,7 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
334
366
|
nextReticleDevExists: io.exists(devLocation.path),
|
|
335
367
|
nextReticleDevSource: io.readFile(devLocation.path),
|
|
336
368
|
svelteKitHooksExists: io.exists(SVELTEKIT_HOOKS),
|
|
369
|
+
svelteKitHooksSource: io.readFile(SVELTEKIT_HOOKS),
|
|
337
370
|
reactRouterEntryExists: io.exists(REACT_ROUTER_ENTRY),
|
|
338
371
|
// The SOURCE, so an entry the app already owns is ADDED TO rather than replaced — it is an
|
|
339
372
|
// override of React Router's default, and everything in it is load-bearing.
|
|
@@ -342,8 +375,24 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
342
375
|
? { path: nuxtConfigPath, source: nuxtConfigSource }
|
|
343
376
|
: null,
|
|
344
377
|
nuxtHasAppDir,
|
|
378
|
+
viteMajor: installedViteMajor(io, detectInput.pkg),
|
|
345
379
|
nuxtPluginExists: io.exists(nuxtPluginPath(nuxtHasAppDir)),
|
|
346
|
-
|
|
380
|
+
nuxtPluginSource: io.readFile(nuxtPluginPath(nuxtHasAppDir)),
|
|
381
|
+
...(() => {
|
|
382
|
+
const root = TANSTACK_START_ROOT_CANDIDATES.find((file) => io.exists(file));
|
|
383
|
+
return root === undefined
|
|
384
|
+
? {}
|
|
385
|
+
: {
|
|
386
|
+
tanstackStartRoot: root,
|
|
387
|
+
tanstackStartRootSource: io.readFile(root),
|
|
388
|
+
tanstackStartConnectExists: io.exists(tanstackStartConnectPath(root)),
|
|
389
|
+
tanstackStartConnectSource: io.readFile(tanstackStartConnectPath(root)),
|
|
390
|
+
};
|
|
391
|
+
})(),
|
|
392
|
+
...(Framework.ANGULAR === detection.framework ? angularInputs(io) : {}),
|
|
393
|
+
...(Framework.HTML === detection.framework
|
|
394
|
+
? { htmlIndexSource: io.readFile(HTML_INDEX_PATH) }
|
|
395
|
+
: {}),
|
|
347
396
|
craEntry: craEntryOf(io),
|
|
348
397
|
craEnv: io.readFile(CRA_ENV_PATH),
|
|
349
398
|
pairingToken: io.host.pairingToken(),
|
|
@@ -360,6 +409,8 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
360
409
|
cursorRuleContent: io.readFile(agentFile(CURSOR_RULE_PATH)),
|
|
361
410
|
claudeCommandContent: io.readFile(agentFile(CLAUDE_COMMAND_PATH)),
|
|
362
411
|
cursorCommandContent: io.readFile(agentFile(CURSOR_COMMAND_PATH)),
|
|
412
|
+
// Read only to MERGE into: the settings file is the user's, and other tools write in it too.
|
|
413
|
+
claudeSettingsContent: true === options.hooks ? io.readFile(agentFile(CLAUDE_SETTINGS_PATH)) : undefined,
|
|
363
414
|
...(agentRoot === undefined
|
|
364
415
|
? {}
|
|
365
416
|
: {
|
|
@@ -381,7 +432,7 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
381
432
|
/** pnpm's workspace declaration, read when present — it is authoritative about where packages live. */
|
|
382
433
|
const SKIPPED_DETAIL = 'skipped — the dependency install above failed, and wiring the app to a package that is not ' +
|
|
383
434
|
'installed stops it booting. Run that install, then re-run `reticle init`.';
|
|
384
|
-
function report(plan, dryRun, failed, skipped, degraded, io, projectDir, agentRoot,
|
|
435
|
+
function report(plan, dryRun, failed, skipped, degraded, why, io, projectDir, agentRoot,
|
|
385
436
|
/** The project's own dev command, so the closing line names what a human would type. */
|
|
386
437
|
devCommand,
|
|
387
438
|
/**
|
|
@@ -393,6 +444,20 @@ devCommand,
|
|
|
393
444
|
*/
|
|
394
445
|
continuesToRuntime = false) {
|
|
395
446
|
io.print(dryRun ? 'reticle init (dry run, no files written)' : 'reticle init');
|
|
447
|
+
/*
|
|
448
|
+
* The escape hatches, named by the command that needs them, at the moment it needs them.
|
|
449
|
+
*
|
|
450
|
+
* `--dry-run`, `--app` and `--no-mcp` all exist and all work, and they were discoverable only
|
|
451
|
+
* from `--help`. A prospective user's agent refused to run this at all — correctly, since it was
|
|
452
|
+
* being asked to let a third-party package edit a build config and register itself with every
|
|
453
|
+
* agent on the machine, and nothing it could see offered a way to look first. It had already
|
|
454
|
+
* decided by the time `--help` would have told it.
|
|
455
|
+
*
|
|
456
|
+
* On the REAL run only. In a dry run the reader has already found the flag.
|
|
457
|
+
*/
|
|
458
|
+
if (!dryRun) {
|
|
459
|
+
io.print(' --dry-run previews · --app <dir> picks the app · --no-mcp skips agent registration');
|
|
460
|
+
}
|
|
396
461
|
// Every path below is printed RELATIVE, and until now nothing said what to. Reported from the
|
|
397
462
|
// field as "[✓] Reticle config → .reticle.json" followed by the file not being there: the app was
|
|
398
463
|
// in `frontend/`, init redirected into it, and the report's `.reticle.json` was true about a
|
|
@@ -426,7 +491,7 @@ continuesToRuntime = false) {
|
|
|
426
491
|
const detail = skipped.has(s.target)
|
|
427
492
|
? SKIPPED_DETAIL
|
|
428
493
|
: downgraded && s.exec !== undefined
|
|
429
|
-
? `step failed — run manually: ${s.exec.fallback}`
|
|
494
|
+
? (why.get(s.target) ?? `step failed — run manually: ${s.exec.fallback}`)
|
|
430
495
|
: s.detail;
|
|
431
496
|
io.print(` [${STATUS_SYMBOL[status]}] ${s.title} → ${s.target}`);
|
|
432
497
|
if (status === StepStatus.APPLY)
|
|
@@ -499,7 +564,8 @@ function sdkPackagesPresent(framework, uiLibrary, io) {
|
|
|
499
564
|
return (packages.length > 0 &&
|
|
500
565
|
packages.every((p) => io.exists(`${NODE_MODULES_DIR}/${p}/${PACKAGE_JSON}`)));
|
|
501
566
|
}
|
|
502
|
-
function applyEffects(plan, io) {
|
|
567
|
+
function applyEffects(plan, io, packageManager) {
|
|
568
|
+
const why = new Map();
|
|
503
569
|
const failed = new Set();
|
|
504
570
|
const skipped = new Set();
|
|
505
571
|
const degraded = new Map();
|
|
@@ -573,8 +639,12 @@ function applyEffects(plan, io) {
|
|
|
573
639
|
failed.add(s.target);
|
|
574
640
|
// A failed install only blocks the wiring when the packages are genuinely ABSENT. See
|
|
575
641
|
// sdkPackagesPresent: the guard protects "the import resolves", not "our subprocess exited 0".
|
|
576
|
-
if (s.target === DEPS_TARGET)
|
|
642
|
+
if (s.target === DEPS_TARGET) {
|
|
577
643
|
installFailed = true;
|
|
644
|
+
const explained = explainInstallFailure(io, packageManager, exec, s.detail);
|
|
645
|
+
if (explained !== undefined)
|
|
646
|
+
why.set(s.target, explained);
|
|
647
|
+
}
|
|
578
648
|
}
|
|
579
649
|
}
|
|
580
650
|
// Where this project lives, remembered for a daemon that will be started somewhere else.
|
|
@@ -588,7 +658,7 @@ function applyEffects(plan, io) {
|
|
|
588
658
|
if (projectId !== undefined) {
|
|
589
659
|
rememberProjectOnDisk(io, projectId, io.cwd(), Date.now());
|
|
590
660
|
}
|
|
591
|
-
return { failed, skipped, degraded };
|
|
661
|
+
return { failed, skipped, degraded, why };
|
|
592
662
|
}
|
|
593
663
|
/**
|
|
594
664
|
* Which STEP failed, from the step targets — not from an error string, which would carry paths.
|
|
@@ -605,8 +675,20 @@ function classifyInitFailure(failed) {
|
|
|
605
675
|
return InitFailure.MCP_REGISTRATION;
|
|
606
676
|
return InitFailure.OTHER;
|
|
607
677
|
}
|
|
678
|
+
/**
|
|
679
|
+
* Where the agent stands when `init` runs inside a workspace package: the workspace root.
|
|
680
|
+
*
|
|
681
|
+
* The same answer `--app` from the root already gives (see enterApp), so both routes into one app
|
|
682
|
+
* leave the root in one state — a pointer config and the agent files an agent opened there reads.
|
|
683
|
+
*/
|
|
684
|
+
function withWorkspaceRoot(options, io) {
|
|
685
|
+
if (options.agentRoot !== undefined || true === options.redirected)
|
|
686
|
+
return options;
|
|
687
|
+
const root = enclosingWorkspaceRoot(options.cwd, io);
|
|
688
|
+
return root === undefined ? options : { ...options, agentRoot: root };
|
|
689
|
+
}
|
|
608
690
|
export function runInit(options, io) {
|
|
609
|
-
const result = runInitSteps(options, io);
|
|
691
|
+
const result = runInitSteps(withWorkspaceRoot(options, io), io);
|
|
610
692
|
// The ask goes here, not in report(): report() is only the success-shaped path, and the exits that
|
|
611
693
|
// matter most are the ones that never reach it — no package.json, an ambiguous workspace — where
|
|
612
694
|
// setup died before anything ran and the person holding the report has the least to go on. This
|
|
@@ -614,7 +696,10 @@ export function runInit(options, io) {
|
|
|
614
696
|
//
|
|
615
697
|
// `redirected` is what keeps it to ONE print: wiring an app in a monorepo re-enters runInit for the
|
|
616
698
|
// chosen directory, and the inner call must not ask again.
|
|
617
|
-
|
|
699
|
+
//
|
|
700
|
+
// And not when the runtime follows: it asks once it has finished, as the last thing printed,
|
|
701
|
+
// rather than here in the middle, before the dev server has even started.
|
|
702
|
+
if (true !== options.redirected && true !== options.continuesToRuntime) {
|
|
618
703
|
io.print('');
|
|
619
704
|
io.print(FEEDBACK_HINT);
|
|
620
705
|
}
|
|
@@ -671,52 +756,8 @@ function runInitSteps(options, io) {
|
|
|
671
756
|
const redirectedEarly = redirectToWorkspaceApp(options, io, pkgRaw ?? {}, runInit);
|
|
672
757
|
if (redirectedEarly !== null)
|
|
673
758
|
return redirectedEarly;
|
|
674
|
-
if (null === pkgRaw)
|
|
675
|
-
|
|
676
|
-
// Asked only when Streamlit already said no, so the two can never both claim the page.
|
|
677
|
-
const django = !streamlit &&
|
|
678
|
-
detectDjangoProject((file) => io.exists(join(options.cwd, file)), io.rootFiles());
|
|
679
|
-
io.print(
|
|
680
|
-
// Two genuinely different situations used to share one sentence: a JS developer in the wrong
|
|
681
|
-
// directory, and a project that is not JavaScript at all. The second reads the old wording as
|
|
682
|
-
// a path problem and goes looking for a directory that cannot exist — reported from a
|
|
683
|
-
// Streamlit app, where the search continued into hunting for a browser bundle to inject by
|
|
684
|
-
// hand before the real answer surfaced.
|
|
685
|
-
streamlit
|
|
686
|
-
? streamlitSetupMessage()
|
|
687
|
-
: django
|
|
688
|
-
? djangoSetupMessage()
|
|
689
|
-
: noPackageJsonMessage((file) => io.exists(join(options.cwd, file))));
|
|
690
|
-
// The message says "add the snippet below". Print the snippet, or the message is the same
|
|
691
|
-
// broken promise in the other direction. `connectArg` carries the port; there is no projectId
|
|
692
|
-
// to bake, because a projectId is derived from the package.json that does not exist here.
|
|
693
|
-
// Scope the project on disk before printing anything, so the daemon can serve this page.
|
|
694
|
-
//
|
|
695
|
-
// This path used to print and exit, leaving no `.reticle.json` at all — and the reporter who
|
|
696
|
-
// asked for the Django path had to hand-write one alongside the middleware. Without it the
|
|
697
|
-
// daemon has no config in its own directory, refuses the page's dial, and every downstream
|
|
698
|
-
// symptom points somewhere other than the cause (see #685). The projectId derivation already
|
|
699
|
-
// handles the no-package case: it falls back to the root folder name, fingerprinted by the
|
|
700
|
-
// absolute path so two checkouts stay distinct.
|
|
701
|
-
//
|
|
702
|
-
// Written only when absent. A config a user or an earlier run already placed here is theirs.
|
|
703
|
-
const nonJsProjectId = deriveProjectId(undefined, io.cwd());
|
|
704
|
-
if (!io.exists(RETICLE_CONFIG_FILE)) {
|
|
705
|
-
io.writeFile(RETICLE_CONFIG_FILE, reticleConfigContent(Framework.HTML, options.port, nonJsProjectId, io.host.installSource()));
|
|
706
|
-
rememberProjectOnDisk(io, nonJsProjectId, io.cwd(), Date.now());
|
|
707
|
-
io.print(`Wrote ${RETICLE_CONFIG_FILE} (project "${nonJsProjectId}").`);
|
|
708
|
-
}
|
|
709
|
-
const connect = connectArgWithToken(options.port, nonJsProjectId, io.host.pairingToken());
|
|
710
|
-
io.print(streamlit
|
|
711
|
-
? streamlitPageSnippet(connect)
|
|
712
|
-
: django
|
|
713
|
-
? djangoMiddlewareSnippet(connect)
|
|
714
|
-
: staticPageSnippet(connect));
|
|
715
|
-
// The onboarding funnel had NO instrumentation, so a setup that died here was indistinguishable
|
|
716
|
-
// from someone who never ran the command — the two failure modes with the most different fixes.
|
|
717
|
-
io.host.reportOutcome({ ok: false, reason: InitFailure.NO_PACKAGE_JSON });
|
|
718
|
-
return { ok: false, applied: 0, manual: 0 };
|
|
719
|
-
}
|
|
759
|
+
if (null === pkgRaw)
|
|
760
|
+
return initWithoutPackageJson(options, io);
|
|
720
761
|
// Init is the flow a user experiences the wait of personally, and the fixture gate measures it at
|
|
721
762
|
// 1–6s per app with no explanation of the spread. These three spans split that number into detect
|
|
722
763
|
// (filesystem probing), plan (pure), and apply (writes + package-manager and CLI subprocesses).
|
|
@@ -726,23 +767,32 @@ function runInitSteps(options, io) {
|
|
|
726
767
|
// the app in frontend/ uses pnpm, and checking the file refused a scaffold the install gate proves
|
|
727
768
|
// must succeed. Both conditions make every later phase fail, and each arrives far from its cause
|
|
728
769
|
// when it is not checked here. See preflight.ts.
|
|
729
|
-
const
|
|
770
|
+
const preflightIo = {
|
|
730
771
|
cwd: () => io.cwd(),
|
|
731
772
|
canWrite: () => io.canWrite(),
|
|
732
773
|
probe: (command, args) => io.probe(command, args),
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
|
|
774
|
+
};
|
|
775
|
+
const resolved = preflight(preflightIo, planInput.detection.packageManager, {
|
|
776
|
+
alreadyServed: options.url !== undefined && '' !== options.url,
|
|
777
|
+
});
|
|
778
|
+
if (resolved.refusal !== undefined) {
|
|
779
|
+
io.print(resolved.refusal);
|
|
736
780
|
return { ok: false, applied: 0, manual: 1 };
|
|
737
781
|
}
|
|
782
|
+
// The prefix that can actually run it on THIS machine — `pnpm`, or `corepack pnpm` where preflight
|
|
783
|
+
// just proved only corepack can. Threaded onto detection (never recomputed by `detect`, which is
|
|
784
|
+
// pure and never probes) so the install step, its retries and the dev command all spawn and print
|
|
785
|
+
// the same thing preflight just confirmed works, instead of a bare binary it already ruled out.
|
|
786
|
+
const packageManagerCommand = resolved.command;
|
|
787
|
+
planInput.detection = { ...planInput.detection, packageManagerCommand };
|
|
738
788
|
const plan = io.host.span('init.plan', {}, () => buildPlan(planInput));
|
|
739
789
|
const effects = options.dryRun
|
|
740
|
-
? { failed: new Set(), skipped: new Set(), degraded: new Map() }
|
|
741
|
-
: io.host.span('init.apply', { steps: plan.steps.length }, () => applyEffects(plan, io));
|
|
742
|
-
const { failed, skipped, degraded } = effects;
|
|
790
|
+
? { failed: new Set(), skipped: new Set(), degraded: new Map(), why: new Map() }
|
|
791
|
+
: io.host.span('init.apply', { steps: plan.steps.length }, () => applyEffects(plan, io, planInput.detection.packageManager));
|
|
792
|
+
const { failed, skipped, degraded, why } = effects;
|
|
743
793
|
// The project's own dev command, so the closing line names what a human would actually type.
|
|
744
|
-
const devCommand = devCommandFrom(pkgRaw,
|
|
745
|
-
const result = report(plan, options.dryRun, failed, skipped, degraded, io, options.cwd, agentRootOf(options), devCommand, true === options.continuesToRuntime);
|
|
794
|
+
const devCommand = devCommandFrom(pkgRaw, packageManagerCommand);
|
|
795
|
+
const result = report(plan, options.dryRun, failed, skipped, degraded, why, io, options.cwd, agentRootOf(options), devCommand, true === options.continuesToRuntime);
|
|
746
796
|
// A dry run is a preview, not an outcome — reporting it would inflate both success and failure.
|
|
747
797
|
if (options.dryRun)
|
|
748
798
|
return result;
|
|
@@ -810,8 +860,12 @@ function runInitSteps(options, io) {
|
|
|
810
860
|
stack: plan.framework,
|
|
811
861
|
...(result.ok ? {} : { reason: classifyInitFailure(failed) }),
|
|
812
862
|
});
|
|
863
|
+
// A failed dependency install skipped every wiring step, so there is no instrumented app to boot:
|
|
864
|
+
// without a context the runtime stops at this report, rather than starting the dev server and
|
|
865
|
+
// diagnosing an SDK that was never installed as a dev server that needs restarting.
|
|
866
|
+
const handover = failed.has(DEPS_TARGET) ? {} : { context };
|
|
813
867
|
if (true === options.deferOutcome)
|
|
814
|
-
return { ...result,
|
|
868
|
+
return { ...result, ...handover, outcome };
|
|
815
869
|
io.host.reportOutcome(outcome);
|
|
816
|
-
return { ...result,
|
|
870
|
+
return { ...result, ...handover };
|
|
817
871
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/init",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.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.
|
|
16
|
+
"@reticlehq/core": "3.4.0"
|
|
17
17
|
},
|
|
18
18
|
"license": "FSL-1.1-ALv2",
|
|
19
19
|
"author": "Reticle contributors",
|
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"dist",
|
|
29
29
|
"README.md",
|
|
30
|
-
"LICENSE"
|
|
31
|
-
"NOTICE"
|
|
30
|
+
"LICENSE"
|
|
32
31
|
],
|
|
33
32
|
"sideEffects": false,
|
|
34
33
|
"keywords": [
|