@reticlehq/server 2.5.0 → 2.7.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/SKILL.md +226 -26
- package/dist/bridge/bridge.d.ts +18 -1
- package/dist/bridge/bridge.js +59 -6
- package/dist/bridge/bridge.test-harness.d.ts +8 -0
- package/dist/bridge/bridge.test-harness.js +9 -1
- package/dist/cli/chromium-hint.d.ts +40 -0
- package/dist/cli/chromium-hint.js +75 -0
- package/dist/cli/cli-doctor.d.ts +0 -5
- package/dist/cli/cli-doctor.js +74 -14
- package/dist/cli/cli-launch.d.ts +1 -0
- package/dist/cli/cli-launch.js +4 -1
- package/dist/cli/cli-parse-feedback.d.ts +31 -0
- package/dist/cli/cli-parse-feedback.js +56 -0
- package/dist/cli/cli-parse.d.ts +5 -9
- package/dist/cli/cli-parse.js +7 -23
- package/dist/cli/cli-port.js +20 -1
- package/dist/cli/doctor-daemon-line.d.ts +37 -0
- package/dist/cli/doctor-daemon-line.js +44 -0
- package/dist/cli/port-holder.d.ts +43 -0
- package/dist/cli/port-holder.js +99 -0
- package/dist/cli.js +170 -19
- package/dist/daemon/daemon-resilience.d.ts +24 -0
- package/dist/daemon/daemon-resilience.js +105 -17
- package/dist/daemon/daemon.js +26 -1
- package/dist/daemon/heartbeat.d.ts +115 -0
- package/dist/daemon/heartbeat.js +141 -0
- package/dist/daemon/idle-shutdown.d.ts +13 -5
- package/dist/daemon/idle-shutdown.js +14 -6
- package/dist/daemon/loopback-alias.d.ts +32 -0
- package/dist/daemon/loopback-alias.js +80 -0
- package/dist/daemon/port-presence.d.ts +76 -0
- package/dist/daemon/port-presence.js +93 -0
- package/dist/daemon/state-dir.d.ts +7 -0
- package/dist/daemon/state-dir.js +59 -0
- package/dist/daemon/unclean-predecessor.d.ts +23 -0
- package/dist/daemon/unclean-predecessor.js +27 -0
- package/dist/events/predicate-eval.d.ts +17 -3
- package/dist/events/predicate-eval.js +183 -96
- package/dist/events/predicate-parse.d.ts +23 -0
- package/dist/events/predicate-parse.js +68 -0
- package/dist/events/predicate.d.ts +5 -0
- package/dist/events/predicate.js +17 -3
- package/dist/events/ring-buffer.d.ts +8 -0
- package/dist/events/ring-buffer.js +35 -17
- package/dist/flows/flow-replay.d.ts +1 -1
- package/dist/flows/flow-replay.js +19 -2
- package/dist/flows/flow-step-runners.js +2 -3
- package/dist/flows/flow-tools.js +10 -14
- package/dist/flows/role-drift-reason.d.ts +20 -0
- package/dist/flows/role-drift-reason.js +39 -0
- package/dist/flows/suite-flakes.d.ts +33 -0
- package/dist/flows/suite-flakes.js +32 -0
- package/dist/honesty/blind-spots.d.ts +19 -0
- package/dist/honesty/blind-spots.js +33 -0
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +9 -1
- package/dist/honesty/verified.d.ts +10 -1
- package/dist/honesty/verified.js +42 -3
- package/dist/http-server.js +20 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +36 -3
- package/dist/init/agent-rules.d.ts +18 -0
- package/dist/init/agent-rules.js +28 -5
- package/dist/init/cra.d.ts +13 -0
- package/dist/init/cra.js +16 -0
- package/dist/init/detect.d.ts +14 -0
- package/dist/init/detect.js +23 -0
- package/dist/init/mcp-clients.d.ts +81 -0
- package/dist/init/mcp-clients.js +248 -0
- package/dist/init/node-io.js +8 -2
- package/dist/init/non-js-project.d.ts +27 -0
- package/dist/init/non-js-project.js +60 -0
- package/dist/init/plan-framework.d.ts +9 -0
- package/dist/init/plan-framework.js +55 -3
- package/dist/init/plan.d.ts +14 -0
- package/dist/init/plan.js +93 -12
- package/dist/init/run.d.ts +18 -1
- package/dist/init/run.js +169 -23
- package/dist/init/snippets.d.ts +20 -1
- package/dist/init/snippets.js +71 -6
- package/dist/init/windows-quote.d.ts +38 -0
- package/dist/init/windows-quote.js +70 -0
- package/dist/mcp/mcp-outage.d.ts +1 -0
- package/dist/mcp/mcp-outage.js +8 -1
- package/dist/mcp/mcp-proxy.d.ts +11 -1
- package/dist/mcp/mcp-proxy.js +39 -9
- package/dist/mcp/mcp.js +64 -2
- package/dist/mcp/proxy-lifecycle.d.ts +19 -0
- package/dist/mcp/proxy-lifecycle.js +21 -0
- package/dist/pool/browser-pool.d.ts +9 -0
- package/dist/pool/browser-pool.js +38 -9
- package/dist/pool/playwright-launcher.js +10 -2
- package/dist/session/attachment-history.d.ts +46 -0
- package/dist/session/attachment-history.js +65 -0
- package/dist/session/live-control-tools.js +39 -2
- package/dist/session/live-control.d.ts +7 -9
- package/dist/session/live-control.js +34 -1
- package/dist/session/no-session-diagnosis.d.ts +11 -4
- package/dist/session/no-session-diagnosis.js +95 -14
- package/dist/session/no-session-watch.d.ts +3 -1
- package/dist/session/no-session-watch.js +10 -2
- package/dist/session/pending-commands.js +1 -0
- package/dist/session/review-store.d.ts +19 -0
- package/dist/session/review-store.js +34 -5
- package/dist/session/session-info.d.ts +17 -1
- package/dist/session/session-info.js +5 -1
- package/dist/session/session-manager.d.ts +12 -0
- package/dist/session/session-manager.js +83 -3
- package/dist/session/session.d.ts +9 -0
- package/dist/session/session.js +20 -0
- package/dist/status-payload.d.ts +13 -1
- package/dist/status-payload.js +6 -1
- package/dist/telemetry/app-instrumented.d.ts +37 -0
- package/dist/telemetry/app-instrumented.js +54 -0
- package/dist/telemetry/crash-cause.d.ts +47 -0
- package/dist/telemetry/crash-cause.js +104 -0
- package/dist/telemetry/daemon-telemetry.d.ts +25 -7
- package/dist/telemetry/daemon-telemetry.js +32 -13
- package/dist/telemetry/error-class.d.ts +26 -0
- package/dist/telemetry/error-class.js +52 -0
- package/dist/telemetry/feedback-cli.d.ts +7 -2
- package/dist/telemetry/feedback-cli.js +49 -5
- package/dist/telemetry/feedback-context.d.ts +1 -6
- package/dist/telemetry/feedback-context.js +64 -4
- package/dist/telemetry/git-facts.d.ts +7 -0
- package/dist/telemetry/git-facts.js +4 -4
- package/dist/telemetry/project-profile.js +2 -1
- package/dist/telemetry/session-metrics.d.ts +45 -3
- package/dist/telemetry/session-metrics.js +247 -9
- package/dist/telemetry/telemetry.d.ts +21 -1
- package/dist/telemetry/telemetry.js +34 -9
- package/dist/telemetry/verification-of.js +26 -1
- package/dist/tools/act-tools.js +63 -14
- package/dist/tools/assert-verdict.d.ts +7 -1
- package/dist/tools/assert-verdict.js +22 -3
- package/dist/tools/browser-tools.js +8 -2
- package/dist/tools/contract-tools.d.ts +0 -5
- package/dist/tools/contract-tools.js +26 -0
- package/dist/tools/dynamic-tools.js +6 -1
- package/dist/tools/error-recovery.d.ts +14 -0
- package/dist/tools/error-recovery.js +89 -8
- package/dist/tools/feedback-invite.d.ts +48 -0
- package/dist/tools/feedback-invite.js +71 -0
- package/dist/tools/invoke-tool.d.ts +0 -6
- package/dist/tools/invoke-tool.js +76 -7
- package/dist/tools/navigate-arrival.d.ts +26 -0
- package/dist/tools/navigate-arrival.js +60 -0
- package/dist/tools/navigate-result.d.ts +15 -1
- package/dist/tools/navigate-result.js +17 -10
- package/dist/tools/observe-tools.js +22 -4
- package/dist/tools/real-input-attempt.js +10 -5
- package/dist/tools/tool-kit.d.ts +60 -5
- package/dist/tools/tool-kit.js +55 -24
- package/dist/tools/tool-surface.js +13 -0
- package/dist/tools/tools.js +22 -0
- package/dist/visual/capture-cleanup.d.ts +16 -0
- package/dist/visual/capture-cleanup.js +39 -0
- package/dist/visual/visual-tools.d.ts +18 -0
- package/dist/visual/visual-tools.js +24 -5
- package/docs/README.md +55 -0
- package/docs/agent-cheatsheet.md +15 -3
- package/docs/benchmarks.md +1 -1
- package/docs/fixtures-dispatch-receiver.yml +76 -0
- package/docs/fixtures.md +17 -0
- package/docs/gate-plan.md +98 -0
- package/docs/gates.md +117 -0
- package/docs/local-registry.md +1 -1
- package/docs/matrix/2.5.0/claude-code-machine.json +19 -0
- package/docs/matrix/2.5.0/codex-machine.json +20 -0
- package/docs/matrix/2.5.0/cursor-machine.json +22 -0
- package/docs/matrix/2.5.0/gemini-machine.json +22 -0
- package/docs/matrix/2.5.0/opencode-machine.json +22 -0
- package/docs/matrix/2.5.0/vscode-machine.json +22 -0
- package/docs/matrix/2.5.0/windsurf-machine.json +22 -0
- package/docs/matrix/MATRIX.md +26 -0
- package/docs/matrix/README.md +59 -0
- package/docs/mint.json +1 -1
- package/docs/system-map.md +152 -0
- package/docs/telemetry-contract.md +15 -3
- package/docs/telemetry.md +16 -1
- package/docs/token-efficiency.md +2 -2
- package/package.json +2 -2
package/dist/init/plan.js
CHANGED
|
@@ -6,15 +6,18 @@
|
|
|
6
6
|
import { Framework, PackageManager, UiLibrary, installCommand, installCommandParts, } from './detect.js';
|
|
7
7
|
import { claudeAddCommand, mcpManual } from './mcp.js';
|
|
8
8
|
import { mergeCursorConfig, CursorMergeStatus, cursorServerEntry } from './cursor.js';
|
|
9
|
+
import { mergeClientConfig, ClientMergeStatus, clientSnippet, clientSpec, } from './mcp-clients.js';
|
|
9
10
|
import { CLAUDE_COMMAND_PATH, CURSOR_COMMAND_PATH, SLASH_COMMAND_BODY, SLASH_COMMAND_SIGNATURE, } from './slash-command.js';
|
|
10
11
|
import { mergeMarkedInstruction, cursorRuleFile, AgentRuleStatus, CLAUDE_MD_PATH, AGENTS_MD_PATH, CURSOR_RULE_PATH, } from './agent-rules.js';
|
|
11
|
-
import { viteSteps, nextSteps, craSteps, svelteKitSteps, astroSteps, VITE_PLUGIN_DETAIL, } from './plan-framework.js';
|
|
12
|
+
import { viteSteps, nextSteps, craSteps, svelteKitSteps, nuxtSteps, astroSteps, VITE_PLUGIN_DETAIL, } from './plan-framework.js';
|
|
12
13
|
import { htmlManual, reticleConfigContent, unverifiedUiLibraryNote } from './snippets.js';
|
|
13
14
|
// An app dev installs exactly the audience-scoped browser-side dependencies — never the retired
|
|
14
15
|
// `@reticlehq/core` umbrella (which dragged the Node MCP server + ws into every app). The kit is the
|
|
15
16
|
// framework adapter (it re-exports the browser sensor), paired with that framework's dev-only build
|
|
16
17
|
// plugin for source mapping + connect injection.
|
|
17
18
|
const RETICLE_REACT_KIT = '@reticlehq/react';
|
|
19
|
+
/** The framework-neutral sensor, for stacks the React adapter has nothing to attach to. */
|
|
20
|
+
const RETICLE_BROWSER_SDK = '@reticlehq/browser';
|
|
18
21
|
const RETICLE_VITE_PLUGIN = '@reticlehq/vite-plugin';
|
|
19
22
|
const RETICLE_NEXT_PLUGIN = '@reticlehq/next';
|
|
20
23
|
/**
|
|
@@ -32,7 +35,7 @@ export function pinnedPackages(packages, version) {
|
|
|
32
35
|
return packages.map((p) => `${p}@${version}`);
|
|
33
36
|
}
|
|
34
37
|
/** The dev-dependencies `reticle init` installs for a given framework — kit first, build plugin next. */
|
|
35
|
-
function frameworkPackages(framework) {
|
|
38
|
+
export function frameworkPackages(framework) {
|
|
36
39
|
switch (framework) {
|
|
37
40
|
case Framework.NEXT:
|
|
38
41
|
return [RETICLE_REACT_KIT, RETICLE_NEXT_PLUGIN];
|
|
@@ -40,6 +43,11 @@ function frameworkPackages(framework) {
|
|
|
40
43
|
case Framework.SVELTEKIT:
|
|
41
44
|
// SvelteKit builds on Vite; until a dedicated Svelte kit exists it uses the Vite build plugin.
|
|
42
45
|
return [RETICLE_REACT_KIT, RETICLE_VITE_PLUGIN];
|
|
46
|
+
case Framework.NUXT:
|
|
47
|
+
// The framework-neutral sensor, NOT the React kit. Nuxt renders Vue, and installing a package
|
|
48
|
+
// named @reticlehq/react — with `react` in its peer dependencies — into a Vue codebase is the
|
|
49
|
+
// single thing most likely to make someone abandon the setup, whether or not it works.
|
|
50
|
+
return [RETICLE_BROWSER_SDK];
|
|
43
51
|
case Framework.ASTRO:
|
|
44
52
|
// Astro owns its own Vite instance and renders its own HTML, so there is no config for the
|
|
45
53
|
// plugin to attach to — the kit alone, connected from a page <script> (see astroManual).
|
|
@@ -67,6 +75,7 @@ const CONNECT_STEP_TITLES = new Set([
|
|
|
67
75
|
'Connect snippet',
|
|
68
76
|
'Connect snippet (CRA)',
|
|
69
77
|
'Connect snippet (Astro)',
|
|
78
|
+
'Connect snippet (Nuxt)',
|
|
70
79
|
'Reticle client hook',
|
|
71
80
|
'Reticle connect module',
|
|
72
81
|
'ReticleDev component',
|
|
@@ -142,6 +151,49 @@ function cursorMcpStep(input) {
|
|
|
142
151
|
write: { path: input.cursorConfigPath, content: r.content },
|
|
143
152
|
};
|
|
144
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* One step per OTHER detected client.
|
|
156
|
+
*
|
|
157
|
+
* The shapes differ enough that a shared "write mcp.json" would be wrong for three of them — see
|
|
158
|
+
* mcp-clients.ts. What is shared is the DECISION: already-correct is left alone, a file we cannot
|
|
159
|
+
* parse is reported with a paste-able block rather than overwritten, and everything else is written.
|
|
160
|
+
*/
|
|
161
|
+
function otherClientSteps(input) {
|
|
162
|
+
const steps = [];
|
|
163
|
+
for (const detected of input.detectedClients ?? []) {
|
|
164
|
+
const spec = clientSpec(detected.id);
|
|
165
|
+
const merged = mergeClientConfig(spec, detected.existing);
|
|
166
|
+
const title = `MCP server (${spec.label})`;
|
|
167
|
+
if (merged.status === ClientMergeStatus.ALREADY) {
|
|
168
|
+
steps.push({
|
|
169
|
+
title,
|
|
170
|
+
target: detected.configPath,
|
|
171
|
+
status: StepStatus.ALREADY,
|
|
172
|
+
detail: `reticle already registered with ${spec.label}`,
|
|
173
|
+
});
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (merged.status === ClientMergeStatus.MANUAL) {
|
|
177
|
+
// Either the file did not parse, or the format is one we refuse to edit blind (TOML). Both
|
|
178
|
+
// end the same way: say so, and hand over the exact block.
|
|
179
|
+
steps.push({
|
|
180
|
+
title,
|
|
181
|
+
target: detected.configPath,
|
|
182
|
+
status: StepStatus.MANUAL,
|
|
183
|
+
detail: `add this to ${detected.configPath} by hand:\n${clientSnippet(spec)}`,
|
|
184
|
+
});
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
steps.push({
|
|
188
|
+
title,
|
|
189
|
+
target: detected.configPath,
|
|
190
|
+
status: StepStatus.APPLY,
|
|
191
|
+
detail: `register reticle with ${spec.label}`,
|
|
192
|
+
write: { path: detected.configPath, content: merged.content },
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return steps;
|
|
196
|
+
}
|
|
145
197
|
/** One global registration per detected agent (Claude + Cursor). Falls back to a manual note. */
|
|
146
198
|
function mcpSteps(input) {
|
|
147
199
|
if (!input.options.mcp) {
|
|
@@ -157,7 +209,10 @@ function mcpSteps(input) {
|
|
|
157
209
|
},
|
|
158
210
|
];
|
|
159
211
|
}
|
|
160
|
-
|
|
212
|
+
// Claude and Cursor first (they have their own registration paths), then every other detected
|
|
213
|
+
// client. A machine with Cursor AND Windsurf gets both — registering only the first one found is
|
|
214
|
+
// how a user ends up with Reticle in the editor they were not using.
|
|
215
|
+
const steps = [...stepsForAgents(input, (a) => a.mcpStep), ...otherClientSteps(input)];
|
|
161
216
|
if (steps.length > 0)
|
|
162
217
|
return steps;
|
|
163
218
|
// No supported agent detected — print the one-time global instructions. Reached when the `claude`
|
|
@@ -348,13 +403,30 @@ function agentRuleSteps(input) {
|
|
|
348
403
|
* with nothing naming a version. Pinning turns that into this loud failure, which is the better
|
|
349
404
|
* trade — but only if the message says what to do about it.
|
|
350
405
|
*/
|
|
351
|
-
/**
|
|
352
|
-
|
|
406
|
+
/**
|
|
407
|
+
* Said out loud when the exact-version install failed and the unpinned one worked.
|
|
408
|
+
*
|
|
409
|
+
* It used to assert a cause it cannot know. Every one of nine fixture apps got the same sentence —
|
|
410
|
+
* "the registry refused 2.5.0 (pnpm's minimumReleaseAge holds new releases back)" — when the actual
|
|
411
|
+
* cause on that run was that the version did not exist yet, and the remedy offered was a `pnpm
|
|
412
|
+
* config` command handed to a yarn 1 project that will never read it.
|
|
413
|
+
*
|
|
414
|
+
* This note is built at PLAN time, before anything runs, and `io.exec` returns a bare boolean, so
|
|
415
|
+
* the apply layer has no failure text to hand back either. The honest move is therefore to report
|
|
416
|
+
* the CONSEQUENCE (which is certain and is the part that bites) and offer the remedy that belongs to
|
|
417
|
+
* the manager actually in use — rather than name a cause that is one possibility among several.
|
|
418
|
+
*/
|
|
419
|
+
function unpinnedRetryNote(version, pm) {
|
|
353
420
|
const wanted = version === undefined ? 'the exact version' : version;
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
421
|
+
// Kept verbatim for pnpm, where minimumReleaseAge is a real and common cause with a real remedy.
|
|
422
|
+
const remedy = pm === PackageManager.PNPM
|
|
423
|
+
? ' One common cause on pnpm is minimumReleaseAge holding a new release back; if pnpm ' +
|
|
424
|
+
'reported ERR_PNPM_NO_MATURE_MATCHING_VERSION, either wait out the window or allow these ' +
|
|
425
|
+
'packages: pnpm config set minimumReleaseAgeExclude "@reticlehq/*"'
|
|
426
|
+
: '';
|
|
427
|
+
return (`the pinned install of ${wanted} failed, so the newest version the registry WOULD accept was ` +
|
|
428
|
+
`installed instead. That may not match the daemon — if the agent reports protocol errors, check ` +
|
|
429
|
+
`\`versionSkew\` in reticle_sessions.${remedy}`);
|
|
358
430
|
}
|
|
359
431
|
function installFailureHint(pm) {
|
|
360
432
|
if (pm !== PackageManager.PNPM)
|
|
@@ -392,7 +464,7 @@ function installStep(input) {
|
|
|
392
464
|
// release-age hold gets a working install instead of no install.
|
|
393
465
|
retry: {
|
|
394
466
|
...installCommandParts(pm, frameworkPackages(input.detection.framework)),
|
|
395
|
-
note: unpinnedRetryNote(input.options.sdkVersion),
|
|
467
|
+
note: unpinnedRetryNote(input.options.sdkVersion, pm),
|
|
396
468
|
},
|
|
397
469
|
};
|
|
398
470
|
}
|
|
@@ -420,8 +492,14 @@ function reticleConfigStep(input) {
|
|
|
420
492
|
*/
|
|
421
493
|
function uiLibraryStep(input) {
|
|
422
494
|
const lib = input.detection.uiLibrary;
|
|
423
|
-
|
|
495
|
+
// Nuxt and SvelteKit each carry their own unverified wording inside the recipe, so a second,
|
|
496
|
+
// more generic notice beside it would only argue with the specific one.
|
|
497
|
+
const framework = input.detection.framework;
|
|
498
|
+
if (lib === UiLibrary.REACT ||
|
|
499
|
+
framework === Framework.SVELTEKIT ||
|
|
500
|
+
framework === Framework.NUXT) {
|
|
424
501
|
return [];
|
|
502
|
+
}
|
|
425
503
|
if (lib === UiLibrary.UNKNOWN)
|
|
426
504
|
return [];
|
|
427
505
|
return [
|
|
@@ -454,6 +532,9 @@ export function buildPlan(input) {
|
|
|
454
532
|
else if (input.detection.framework === Framework.CRA) {
|
|
455
533
|
steps.push(...craSteps(input));
|
|
456
534
|
}
|
|
535
|
+
else if (input.detection.framework === Framework.NUXT) {
|
|
536
|
+
steps.push(...nuxtSteps(input));
|
|
537
|
+
}
|
|
457
538
|
else if (input.detection.framework === Framework.SVELTEKIT) {
|
|
458
539
|
steps.push(...svelteKitSteps(input));
|
|
459
540
|
// The Vite plugin as well as the client hook. `init` already INSTALLS @reticlehq/vite-plugin for
|
|
@@ -466,7 +547,7 @@ export function buildPlan(input) {
|
|
|
466
547
|
title: 'Connect snippet',
|
|
467
548
|
target: 'index.html',
|
|
468
549
|
status: StepStatus.MANUAL,
|
|
469
|
-
detail: htmlManual(input.options.port, input.options.projectId),
|
|
550
|
+
detail: htmlManual(input.options.port, input.options.projectId, input.pairingToken),
|
|
470
551
|
});
|
|
471
552
|
}
|
|
472
553
|
return { framework: input.detection.framework, steps };
|
package/dist/init/run.d.ts
CHANGED
|
@@ -7,8 +7,15 @@
|
|
|
7
7
|
* Resolve the lockfiles set used to pick the package manager. A lockfile in the project root wins;
|
|
8
8
|
* otherwise we walk UP the directory tree (monorepos keep the lockfile at the workspace root, not in
|
|
9
9
|
* each package) so `reticle init` in a sub-package suggests `pnpm add` instead of defaulting to `npm i`.
|
|
10
|
+
*
|
|
11
|
+
* The walk is skipped when the project has its own installed tree, because an INHERITED lockfile is
|
|
12
|
+
* weaker evidence than a `node_modules` sitting right there — the ancestor describes the workspace,
|
|
13
|
+
* the tree describes THIS package. Reported from the field: `init` in a `frontend/` app installed
|
|
14
|
+
* with npm emitted `pnpm add -D` off a repo-root `pnpm-lock.yaml`, pnpm was not on PATH, and the
|
|
15
|
+
* failed install took every downstream wiring step with it. A LOCAL lockfile still wins over the
|
|
16
|
+
* tree — it is a deliberate statement about this package, not an inheritance.
|
|
10
17
|
*/
|
|
11
|
-
export declare function resolveLockfiles(rootFiles: ReadonlySet<string>, cwd: string, io: Pick<InitIo, 'exists'>): Set<string>;
|
|
18
|
+
export declare function resolveLockfiles(rootFiles: ReadonlySet<string>, cwd: string, io: Pick<InitIo, 'exists'>, nodeModulesMarkers?: ReadonlySet<string>): Set<string>;
|
|
12
19
|
export interface InitOptions {
|
|
13
20
|
cwd: string;
|
|
14
21
|
port: number | undefined;
|
|
@@ -59,5 +66,15 @@ interface InitResult {
|
|
|
59
66
|
* it knows it is in a workspace; this is the matching walk DOWN.
|
|
60
67
|
*/
|
|
61
68
|
export declare function findWorkspaceApps(io: Pick<InitIo, 'exists' | 'readFile' | 'listDirs'>): string[];
|
|
69
|
+
/**
|
|
70
|
+
* The standing ask, printed at the end of every `init`.
|
|
71
|
+
*
|
|
72
|
+
* Addressed to the agent as much as the human: whichever of them just ran this command is the one
|
|
73
|
+
* holding the experience, and neither has a Reticle tool surface to file through at this point.
|
|
74
|
+
*
|
|
75
|
+
* Printed by the `runInit` wrapper, NOT by `report()` — see the comment there for why, and for how
|
|
76
|
+
* the workspace redirect is kept from asking twice.
|
|
77
|
+
*/
|
|
78
|
+
export declare const FEEDBACK_HINT: string;
|
|
62
79
|
export declare function runInit(options: InitOptions, io: InitIo): InitResult;
|
|
63
80
|
export {};
|
package/dist/init/run.js
CHANGED
|
@@ -4,16 +4,17 @@
|
|
|
4
4
|
* access goes through `InitIo` so the orchestration is unit-testable with an in-memory IO.
|
|
5
5
|
*/
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
|
+
import { noPackageJsonMessage } from './non-js-project.js';
|
|
7
8
|
import { spanSync } from '../trace.js';
|
|
8
9
|
import { readFileSync } from 'node:fs';
|
|
9
10
|
import { homedir } from 'node:os';
|
|
10
|
-
import { detect, Framework } from './detect.js';
|
|
11
|
+
import { detect, Framework, namesAPackageManager } from './detect.js';
|
|
11
12
|
import { wasMcpRegistered } from './mcp-registered.js';
|
|
12
13
|
import { pickAstroHost } from './astro-host.js';
|
|
13
14
|
import { workspaceParents } from './workspace-apps.js';
|
|
14
15
|
import { chooseWorkspaceApp } from './app-choice.js';
|
|
15
16
|
import { isConnectStep } from './plan.js';
|
|
16
|
-
import { CURSOR_RULE_PATH } from './agent-rules.js';
|
|
17
|
+
import { CLI, CURSOR_RULE_PATH } from './agent-rules.js';
|
|
17
18
|
import { CRA_ENV_PATH } from './cra.js';
|
|
18
19
|
/** CRA's bundled entry, in the order create-react-app itself generates them. */
|
|
19
20
|
const CRA_ENTRY_CANDIDATES = ['src/index.tsx', 'src/index.jsx', 'src/index.ts', 'src/index.js'];
|
|
@@ -40,11 +41,12 @@ function readPairingToken() {
|
|
|
40
41
|
return '';
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
|
-
import { DEPS_TARGET, MCP_TARGET, buildPlan, StepStatus, } from './plan.js';
|
|
44
|
+
import { DEPS_TARGET, frameworkPackages, MCP_TARGET, buildPlan, StepStatus, } from './plan.js';
|
|
44
45
|
import { claudeAvailableProbe, claudeExistsProbe } from './mcp.js';
|
|
45
46
|
import { reticleDevLocation } from './next-patch.js';
|
|
46
47
|
import { scanTestids, storeHints } from './capabilities.js';
|
|
47
48
|
import { CURSOR_DIR_RELPATH, CURSOR_MCP_RELPATH } from './cursor.js';
|
|
49
|
+
import { fileBackedClients, clientMarkerRelPath, ConfigScope, McpClient } from './mcp-clients.js';
|
|
48
50
|
import { deriveProjectId, packageName } from './project-id.js';
|
|
49
51
|
import { VITE_DEV_MODULE_PATH } from './snippets.js';
|
|
50
52
|
import { CLAUDE_COMMAND_PATH, CURSOR_COMMAND_PATH } from './slash-command.js';
|
|
@@ -62,11 +64,20 @@ const LOCKFILE_NAMES = [
|
|
|
62
64
|
* Resolve the lockfiles set used to pick the package manager. A lockfile in the project root wins;
|
|
63
65
|
* otherwise we walk UP the directory tree (monorepos keep the lockfile at the workspace root, not in
|
|
64
66
|
* each package) so `reticle init` in a sub-package suggests `pnpm add` instead of defaulting to `npm i`.
|
|
67
|
+
*
|
|
68
|
+
* The walk is skipped when the project has its own installed tree, because an INHERITED lockfile is
|
|
69
|
+
* weaker evidence than a `node_modules` sitting right there — the ancestor describes the workspace,
|
|
70
|
+
* the tree describes THIS package. Reported from the field: `init` in a `frontend/` app installed
|
|
71
|
+
* with npm emitted `pnpm add -D` off a repo-root `pnpm-lock.yaml`, pnpm was not on PATH, and the
|
|
72
|
+
* failed install took every downstream wiring step with it. A LOCAL lockfile still wins over the
|
|
73
|
+
* tree — it is a deliberate statement about this package, not an inheritance.
|
|
65
74
|
*/
|
|
66
|
-
export function resolveLockfiles(rootFiles, cwd, io) {
|
|
75
|
+
export function resolveLockfiles(rootFiles, cwd, io, nodeModulesMarkers = new Set()) {
|
|
67
76
|
const set = new Set(rootFiles);
|
|
68
77
|
if (LOCKFILE_NAMES.some((name) => set.has(name)))
|
|
69
78
|
return set; // local lockfile is authoritative
|
|
79
|
+
if (namesAPackageManager(nodeModulesMarkers))
|
|
80
|
+
return set;
|
|
70
81
|
let dir = cwd;
|
|
71
82
|
for (let depth = 0; depth < 50; depth++) {
|
|
72
83
|
for (const name of LOCKFILE_NAMES) {
|
|
@@ -83,6 +94,7 @@ export function resolveLockfiles(rootFiles, cwd, io) {
|
|
|
83
94
|
return set;
|
|
84
95
|
}
|
|
85
96
|
const PACKAGE_JSON = 'package.json';
|
|
97
|
+
const NODE_MODULES_DIR = 'node_modules';
|
|
86
98
|
/**
|
|
87
99
|
* Root-layout candidates, App Router only. `--src-dir` apps keep theirs under `src/app`, and the
|
|
88
100
|
* ReticleDev component has to land NEXT TO the layout or the relative import it generates is dead.
|
|
@@ -197,13 +209,15 @@ function gatherPlanInput(options, io, pkgRaw) {
|
|
|
197
209
|
// Stable identity derived from the app's package.json name + root, so it survives port changes.
|
|
198
210
|
const projectId = deriveProjectId(packageName(pkg), options.cwd);
|
|
199
211
|
const rootFiles = new Set(io.rootFiles());
|
|
212
|
+
const nodeModulesMarkers = new Set(io.listFiles('node_modules'));
|
|
200
213
|
const detectInput = {
|
|
201
214
|
pkg: 'object' === typeof pkg && pkg !== null ? pkg : {},
|
|
202
215
|
configFiles: rootFiles,
|
|
203
|
-
// Walk up for the lockfile so a monorepo sub-package picks the workspace's package manager
|
|
204
|
-
|
|
216
|
+
// Walk up for the lockfile so a monorepo sub-package picks the workspace's package manager —
|
|
217
|
+
// unless this package's own installed tree already answers it, which outranks an inherited one.
|
|
218
|
+
lockfiles: resolveLockfiles(rootFiles, options.cwd, io, nodeModulesMarkers),
|
|
205
219
|
// An already-installed tree names its own manager, which matters when no lockfile is committed.
|
|
206
|
-
nodeModulesMarkers
|
|
220
|
+
nodeModulesMarkers,
|
|
207
221
|
};
|
|
208
222
|
const detection = detect(detectInput);
|
|
209
223
|
const vitePath = firstPresent(rootFiles, VITE_CONFIG_CANDIDATES);
|
|
@@ -225,6 +239,27 @@ function gatherPlanInput(options, io, pkgRaw) {
|
|
|
225
239
|
// project-level .cursor/ is unambiguous evidence somebody uses it here.
|
|
226
240
|
const cursorPresent = options.mcp && (io.exists(cursorDir) || io.exists(CURSOR_DIR_RELPATH));
|
|
227
241
|
const cursorConfig = cursorPresent ? io.readFile(cursorConfigPath) : null;
|
|
242
|
+
// Every OTHER MCP client this machine shows evidence of. Conservative and one-directional: we
|
|
243
|
+
// write into a config a client ALREADY has, and never create ~/.gemini or ~/.codeium for somebody
|
|
244
|
+
// who does not use them. Cursor and Claude are handled above by their own steps.
|
|
245
|
+
const detectedClients = options.mcp
|
|
246
|
+
? fileBackedClients()
|
|
247
|
+
.filter((spec) => spec.id !== McpClient.CURSOR)
|
|
248
|
+
.map((spec) => {
|
|
249
|
+
const marker = clientMarkerRelPath(spec);
|
|
250
|
+
const absolute = spec.scope === ConfigScope.HOME ? join(io.homeDir(), spec.relPath) : spec.relPath;
|
|
251
|
+
const markerPath = spec.scope === ConfigScope.HOME ? join(io.homeDir(), marker) : marker;
|
|
252
|
+
if (!io.exists(markerPath))
|
|
253
|
+
return null;
|
|
254
|
+
return {
|
|
255
|
+
id: spec.id,
|
|
256
|
+
configPath: absolute,
|
|
257
|
+
// io.readFile passes absolute paths through unchanged, so one call covers both scopes.
|
|
258
|
+
existing: io.readFile(absolute),
|
|
259
|
+
};
|
|
260
|
+
})
|
|
261
|
+
.filter((entry) => entry !== null)
|
|
262
|
+
: [];
|
|
228
263
|
const astroPath = firstPresent(rootFiles, ASTRO_CONFIG_CANDIDATES);
|
|
229
264
|
const astroSource = null === astroPath ? null : io.readFile(astroPath);
|
|
230
265
|
// Which file owns the DOCUMENT, not how many files sit in a directory. The old rule ("exactly one
|
|
@@ -253,6 +288,7 @@ function gatherPlanInput(options, io, pkgRaw) {
|
|
|
253
288
|
claudeCli,
|
|
254
289
|
mcpExists,
|
|
255
290
|
cursorPresent,
|
|
291
|
+
detectedClients,
|
|
256
292
|
cursorProjectPresent: io.exists(CURSOR_DIR_RELPATH),
|
|
257
293
|
cursorConfig,
|
|
258
294
|
cursorConfigPath,
|
|
@@ -354,21 +390,62 @@ export function findWorkspaceApps(io) {
|
|
|
354
390
|
}
|
|
355
391
|
return [...new Set(found)];
|
|
356
392
|
}
|
|
357
|
-
|
|
393
|
+
/**
|
|
394
|
+
* The standing ask, printed at the end of every `init`.
|
|
395
|
+
*
|
|
396
|
+
* Addressed to the agent as much as the human: whichever of them just ran this command is the one
|
|
397
|
+
* holding the experience, and neither has a Reticle tool surface to file through at this point.
|
|
398
|
+
*
|
|
399
|
+
* Printed by the `runInit` wrapper, NOT by `report()` — see the comment there for why, and for how
|
|
400
|
+
* the workspace redirect is kept from asking twice.
|
|
401
|
+
*/
|
|
402
|
+
export const FEEDBACK_HINT = 'Anything wrong, missing, or awkward — in this setup or in Reticle itself? Tell us; it is the ' +
|
|
403
|
+
'one thing that decides what gets fixed:\n' +
|
|
404
|
+
` ${CLI} feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement> "what happened" (agents)\n` +
|
|
405
|
+
` ${CLI} feedback "what worked, what didn't" (humans)`;
|
|
406
|
+
function devServerRestart(framework) {
|
|
358
407
|
if (framework === Framework.NEXT)
|
|
359
|
-
return 'Restart `next dev
|
|
408
|
+
return 'Restart `next dev`';
|
|
360
409
|
if (framework === Framework.VITE)
|
|
361
|
-
return 'Restart `vite
|
|
410
|
+
return 'Restart `vite`';
|
|
362
411
|
if (framework === Framework.ASTRO)
|
|
363
|
-
return 'Restart `astro dev
|
|
412
|
+
return 'Restart `astro dev`';
|
|
364
413
|
if (framework === Framework.SVELTEKIT)
|
|
365
|
-
return 'Restart your dev server (`npm run dev`)
|
|
366
|
-
return 'Reload your app on localhost
|
|
414
|
+
return 'Restart your dev server (`npm run dev`)';
|
|
415
|
+
return 'Reload your app on localhost';
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* The last two lines a user reads, and the order matters.
|
|
419
|
+
*
|
|
420
|
+
* This used to say only "Restart <dev server>, then ask your agent: List Reticle sessions" — and
|
|
421
|
+
* asking the agent was the one thing that could not work yet. `init` registers the MCP server, but
|
|
422
|
+
* an agent client reads its tool list when it STARTS and never re-reads it, so the session that
|
|
423
|
+
* just ran `init` has no `reticle_*` tools however clean the install was. The user follows the
|
|
424
|
+
* instruction, the agent answers "unknown tool", and the obvious conclusion is that the install
|
|
425
|
+
* failed.
|
|
426
|
+
*
|
|
427
|
+
* So the reload is named FIRST, before the sentence that depends on it. Omitted entirely when this
|
|
428
|
+
* run did not register MCP (`--no-mcp`), where it would be advice about something we did not do.
|
|
429
|
+
*/
|
|
430
|
+
function restartHint(framework, mcpRegistered) {
|
|
431
|
+
const dev = `${devServerRestart(framework)}.`;
|
|
432
|
+
if (!mcpRegistered)
|
|
433
|
+
return `${dev} Then ask your agent: "List Reticle sessions".`;
|
|
434
|
+
return (`${dev}\n` +
|
|
435
|
+
"Then reload your agent's MCP tools — `/mcp` in Claude Code, or reload the window in " +
|
|
436
|
+
'Cursor/VS Code.\n' +
|
|
437
|
+
'The tools only appear after that: your agent read its tool list before Reticle existed.\n' +
|
|
438
|
+
'Then ask it: "List Reticle sessions".');
|
|
367
439
|
}
|
|
368
440
|
const SKIPPED_DETAIL = 'skipped — the dependency install above failed, and wiring the app to a package that is not ' +
|
|
369
441
|
'installed stops it booting. Run that install, then re-run `reticle init`.';
|
|
370
|
-
function report(plan, dryRun, failed, skipped, degraded, io) {
|
|
442
|
+
function report(plan, dryRun, failed, skipped, degraded, io, projectDir) {
|
|
371
443
|
io.print(dryRun ? 'reticle init (dry run — no files written)' : 'reticle init');
|
|
444
|
+
// Every path below is printed RELATIVE, and until now nothing said what to. Reported from the
|
|
445
|
+
// field as "[✓] Reticle config → .reticle.json" followed by the file not being there: the app was
|
|
446
|
+
// in `frontend/`, init redirected into it, and the report's `.reticle.json` was true about a
|
|
447
|
+
// directory the reader was not standing in. One line makes every path in the report unambiguous.
|
|
448
|
+
io.print(` in ${projectDir}`);
|
|
372
449
|
io.print('');
|
|
373
450
|
let applied = 0;
|
|
374
451
|
let manual = 0;
|
|
@@ -417,7 +494,7 @@ function report(plan, dryRun, failed, skipped, degraded, io) {
|
|
|
417
494
|
'"no browser session connected" until then. Everything else is already in place.');
|
|
418
495
|
io.print('');
|
|
419
496
|
}
|
|
420
|
-
io.print(restartHint(plan.framework));
|
|
497
|
+
io.print(restartHint(plan.framework, wasMcpRegistered(resolvedStatus(plan, MCP_TARGET, failed, skipped))));
|
|
421
498
|
return { ok: !connectPending, applied, manual };
|
|
422
499
|
}
|
|
423
500
|
/**
|
|
@@ -429,6 +506,25 @@ function report(plan, dryRun, failed, skipped, degraded, io) {
|
|
|
429
506
|
* MODULE_NOT_FOUND, so the app stops booting *because* Reticle was installed. A skipped step is a
|
|
430
507
|
* message; a half-wired app is a broken project.
|
|
431
508
|
*/
|
|
509
|
+
/**
|
|
510
|
+
* Are the SDK packages actually on disk, whatever the install step reported?
|
|
511
|
+
*
|
|
512
|
+
* `dependsOnInstall` exists to stop init writing a `next.config.ts` that imports a package which is
|
|
513
|
+
* not there — that took a dev server down once, and installing Reticle must never be why an app
|
|
514
|
+
* stops booting. The invariant it protects is "the import RESOLVES", but it was gated on "our
|
|
515
|
+
* install subprocess exited 0", and those come apart in exactly the situation the failure creates:
|
|
516
|
+
* init tells the user to install by hand, they do, they re-run init, the install step fails again
|
|
517
|
+
* (wrong package manager, not on PATH) and every wiring step is skipped a second time. Reported from
|
|
518
|
+
* a Next 16 app where npm had already installed both packages successfully — leaving an init that
|
|
519
|
+
* could not be retried into working, which is the shape this guard was written to prevent.
|
|
520
|
+
*
|
|
521
|
+
* Reading node_modules answers the real question and costs one `exists` call per package.
|
|
522
|
+
*/
|
|
523
|
+
function sdkPackagesPresent(framework, io) {
|
|
524
|
+
const packages = frameworkPackages(framework);
|
|
525
|
+
return (packages.length > 0 &&
|
|
526
|
+
packages.every((p) => io.exists(`${NODE_MODULES_DIR}/${p}/${PACKAGE_JSON}`)));
|
|
527
|
+
}
|
|
432
528
|
function applyEffects(plan, io) {
|
|
433
529
|
const failed = new Set();
|
|
434
530
|
const skipped = new Set();
|
|
@@ -443,9 +539,24 @@ function applyEffects(plan, io) {
|
|
|
443
539
|
}
|
|
444
540
|
const write = s.write;
|
|
445
541
|
if (write !== undefined) {
|
|
446
|
-
|
|
447
|
-
|
|
542
|
+
// Confirm the EFFECT, not the intention. Reported from the field (#160): init printed
|
|
543
|
+
// `[✓] Reticle config → .reticle.json` and the file was not there afterward. Nothing checked —
|
|
544
|
+
// no arrangement of the filesystem (a read-only mount, a full disk, an antivirus quarantining
|
|
545
|
+
// a new dotfile) could have turned that tick into anything else. A checkmark that cannot fail
|
|
546
|
+
// is decoration, and this one is the first thing a new user reads. Same shape as #139.
|
|
547
|
+
const wrote = spanSync('init.write', { target: s.target, path: write.path }, () => {
|
|
548
|
+
try {
|
|
549
|
+
io.writeFile(write.path, write.content);
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
return false; // a throw is the loud version of the same failure
|
|
553
|
+
}
|
|
554
|
+
return io.exists(write.path);
|
|
448
555
|
});
|
|
556
|
+
if (!wrote) {
|
|
557
|
+
failed.add(s.target);
|
|
558
|
+
continue; // do not run this step's exec against a file that is not there
|
|
559
|
+
}
|
|
449
560
|
}
|
|
450
561
|
// Bound once so the traced call cannot need a `?? ''` fallback — a default there would turn a
|
|
451
562
|
// narrowing mistake into an empty command that silently "succeeds".
|
|
@@ -469,7 +580,9 @@ function applyEffects(plan, io) {
|
|
|
469
580
|
continue;
|
|
470
581
|
}
|
|
471
582
|
failed.add(s.target);
|
|
472
|
-
|
|
583
|
+
// A failed install only blocks the wiring when the packages are genuinely ABSENT. See
|
|
584
|
+
// sdkPackagesPresent: the guard protects "the import resolves", not "our subprocess exited 0".
|
|
585
|
+
if (s.target === DEPS_TARGET && !sdkPackagesPresent(plan.framework, io))
|
|
473
586
|
installFailed = true;
|
|
474
587
|
}
|
|
475
588
|
}
|
|
@@ -534,17 +647,50 @@ function redirectToWorkspaceApp(options, io, pkgRaw) {
|
|
|
534
647
|
return runInit({ ...options, cwd: join(options.cwd, target), redirected: true }, io.scoped(target));
|
|
535
648
|
}
|
|
536
649
|
export function runInit(options, io) {
|
|
650
|
+
const result = runInitSteps(options, io);
|
|
651
|
+
// The ask goes here, not in report(): report() is only the success-shaped path, and the exits that
|
|
652
|
+
// matter most are the ones that never reach it — no package.json, an ambiguous workspace — where
|
|
653
|
+
// setup died before anything ran and the person holding the report has the least to go on. This
|
|
654
|
+
// wrapper is the one point every exit passes through.
|
|
655
|
+
//
|
|
656
|
+
// `redirected` is what keeps it to ONE print: wiring an app in a monorepo re-enters runInit for the
|
|
657
|
+
// chosen directory, and the inner call must not ask again.
|
|
658
|
+
if (true !== options.redirected) {
|
|
659
|
+
io.print('');
|
|
660
|
+
io.print(FEEDBACK_HINT);
|
|
661
|
+
}
|
|
662
|
+
return result;
|
|
663
|
+
}
|
|
664
|
+
function runInitSteps(options, io) {
|
|
537
665
|
const pkgRaw = io.readFile(PACKAGE_JSON);
|
|
666
|
+
// Look for the app BEFORE concluding there isn't one.
|
|
667
|
+
//
|
|
668
|
+
// A root with no package.json is not a dead end — it is the ordinary shape of a repo whose app
|
|
669
|
+
// lives one directory down (`frontend/`, `web/`, `client/`) with no manifest at the top. Bailing
|
|
670
|
+
// first made that repo un-instrumentable: `reticle init` said "No package.json found", and
|
|
671
|
+
// `--app frontend`, the flag that exists for exactly this, was never read because the bail came
|
|
672
|
+
// first. Reported twice by the same user, who tried the documented workaround and hit the same
|
|
673
|
+
// wall. Discovery already handles the case (it scans top-level directories, not just declared
|
|
674
|
+
// workspaces); it was simply unreachable.
|
|
675
|
+
//
|
|
676
|
+
// `'{}'` because the redirect only needs the manifest to ask "is THIS directory the app", and a
|
|
677
|
+
// directory with no package.json is definitively not.
|
|
678
|
+
const redirectedEarly = redirectToWorkspaceApp(options, io, pkgRaw ?? '{}');
|
|
679
|
+
if (redirectedEarly !== null)
|
|
680
|
+
return redirectedEarly;
|
|
538
681
|
if (null === pkgRaw) {
|
|
539
|
-
io.print(
|
|
682
|
+
io.print(
|
|
683
|
+
// Two genuinely different situations used to share one sentence: a JS developer in the wrong
|
|
684
|
+
// directory, and a project that is not JavaScript at all. The second reads the old wording as
|
|
685
|
+
// a path problem and goes looking for a directory that cannot exist — reported from a
|
|
686
|
+
// Streamlit app, where the search continued into hunting for a browser bundle to inject by
|
|
687
|
+
// hand before the real answer surfaced.
|
|
688
|
+
noPackageJsonMessage((file) => io.exists(join(options.cwd, file))));
|
|
540
689
|
// The onboarding funnel had NO instrumentation, so a setup that died here was indistinguishable
|
|
541
690
|
// from someone who never ran the command — the two failure modes with the most different fixes.
|
|
542
691
|
reportInitOutcome({ ok: false, reason: InitFailure.NO_PACKAGE_JSON });
|
|
543
692
|
return { ok: false, applied: 0, manual: 0 };
|
|
544
693
|
}
|
|
545
|
-
const redirected = redirectToWorkspaceApp(options, io, pkgRaw);
|
|
546
|
-
if (redirected !== null)
|
|
547
|
-
return redirected;
|
|
548
694
|
// Init is the flow a user experiences the wait of personally, and the fixture gate measures it at
|
|
549
695
|
// 1–6s per app with no explanation of the spread. These three spans split that number into detect
|
|
550
696
|
// (filesystem probing), plan (pure), and apply (writes + package-manager and CLI subprocesses).
|
|
@@ -553,7 +699,7 @@ export function runInit(options, io) {
|
|
|
553
699
|
? { failed: new Set(), skipped: new Set(), degraded: new Map() }
|
|
554
700
|
: spanSync('init.apply', { steps: plan.steps.length }, () => applyEffects(plan, io));
|
|
555
701
|
const { failed, skipped, degraded } = effects;
|
|
556
|
-
const result = report(plan, options.dryRun, failed, skipped, degraded, io);
|
|
702
|
+
const result = report(plan, options.dryRun, failed, skipped, degraded, io, options.cwd);
|
|
557
703
|
// A dry run is a preview, not an outcome — reporting it would inflate both success and failure.
|
|
558
704
|
if (!options.dryRun) {
|
|
559
705
|
reportInitOutcome({
|
package/dist/init/snippets.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export declare const NEXT_LAYOUT_MANUAL = "Mount <ReticleDev /> in your root lay
|
|
|
47
47
|
* where a bare `@reticlehq/react` import resolves. A bare import in a plain index.html does NOT resolve in
|
|
48
48
|
* the browser, so we never tell a bundled app to do that (the old advice silently failed for CRA).
|
|
49
49
|
*/
|
|
50
|
-
export declare function htmlManual(port: number | undefined, projectId?: string): string;
|
|
50
|
+
export declare function htmlManual(port: number | undefined, projectId?: string, pairingToken?: string): string;
|
|
51
51
|
export declare const NEXT_RETICLE_DEV_PATH = "app/reticle-dev.tsx";
|
|
52
52
|
export declare const SVELTEKIT_HOOKS_PATH = "src/hooks.client.ts";
|
|
53
53
|
/**
|
|
@@ -68,6 +68,25 @@ export declare const UNVERIFIED_FRAMEWORK_NOTE = "Reticle has no SvelteKit app a
|
|
|
68
68
|
* a client hook is the reliable path. SvelteKit runs src/hooks.client.ts on the client at startup.
|
|
69
69
|
*/
|
|
70
70
|
export declare function svelteKitHooksFile(port: number | undefined, projectId?: string): string;
|
|
71
|
+
/** Where a Nuxt dev-only client plugin belongs. `.client` keeps it out of SSR; Nuxt auto-registers it. */
|
|
72
|
+
export declare const NUXT_PLUGIN_PATH = "app/plugins/reticle.client.ts";
|
|
73
|
+
/**
|
|
74
|
+
* The Nuxt recipe, written out in full because every trap in it is one somebody actually hit.
|
|
75
|
+
*
|
|
76
|
+
* Reported from the field, in the order they were hit: `init` classified a Nuxt 4 app as `html`, so
|
|
77
|
+
* it installed a package named `@reticlehq/react` (with `react` in its peer dependencies) into a Vue
|
|
78
|
+
* codebase — the reporter only continued after auditing our dist to confirm there are no React
|
|
79
|
+
* imports at runtime, which most people will not do. It then handed over a snippet guarded on
|
|
80
|
+
* `window.location.hostname === 'localhost'`, which fails twice over in Nuxt: `window` does not
|
|
81
|
+
* exist during SSR, and the dev host here was a hosts-file alias (required for the backend's
|
|
82
|
+
* white-label origin detection), so the guard was false and the connect never ran — no error, no log
|
|
83
|
+
* line, nothing to debug. And nothing said a running dev server does not pick up a new plugin.
|
|
84
|
+
*
|
|
85
|
+
* So: `import.meta.dev` (build-time, host-independent) instead of a hostname check, `.client.ts`
|
|
86
|
+
* instead of an SSR guard, the framework-neutral sensor instead of the React kit, the non-localhost
|
|
87
|
+
* flag named up front, and the restart said out loud.
|
|
88
|
+
*/
|
|
89
|
+
export declare function nuxtManual(port: number | undefined, projectId?: string): string;
|
|
71
90
|
/**
|
|
72
91
|
* Root-level project config for Reticle. Written by `reticle init`; read by `reticle mcp` for the port
|
|
73
92
|
* and by tooling for the stable projectId (the app's identity across port changes).
|