@reticlehq/init 3.2.0 → 3.3.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/detect.d.ts +13 -2
- package/dist/detect/detect.js +41 -2
- package/dist/detect/workspace-redirect.d.ts +2 -1
- package/dist/diagnose/csp-doctor.d.ts +20 -1
- package/dist/diagnose/csp-doctor.js +101 -4
- package/dist/diagnose/desktop-doctor.d.ts +1 -1
- package/dist/diagnose/install-retries.d.ts +2 -1
- package/dist/host.d.ts +6 -0
- package/dist/host.js +3 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/patch/astro-patch.d.ts +47 -12
- package/dist/patch/astro-patch.js +149 -41
- package/dist/patch/nuxt-snippets.d.ts +44 -0
- package/dist/patch/nuxt-snippets.js +135 -0
- package/dist/patch/snippets.d.ts +1 -48
- package/dist/patch/snippets.js +63 -175
- 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 +13 -0
- package/dist/patch/vite-owning-config.js +43 -16
- package/dist/plan/connect-steps.d.ts +3 -2
- package/dist/plan/connect-steps.js +4 -2
- package/dist/plan/framework-adapter.d.ts +2 -5
- package/dist/plan/framework-adapter.js +6 -5
- package/dist/plan/plan-framework.d.ts +1 -1
- package/dist/plan/plan-framework.js +33 -11
- package/dist/plan/plan-types.d.ts +18 -2
- package/dist/plan/plan.js +117 -35
- 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 +8 -0
- package/dist/register/mcp.js +16 -0
- package/dist/run-types.d.ts +2 -0
- package/dist/run.js +22 -1
- package/package.json +2 -2
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { bridgeWsUrl } from '@reticlehq/core';
|
|
7
7
|
import { patchViteConfig, VitePatchKind, VITE_IMPORT } from '../patch/vite-config.js';
|
|
8
8
|
import { patchNextConfig, patchRootLayout, patchPagesApp } from '../patch/next-patch.js';
|
|
9
|
-
import { ASTRO_ENV_DTS_PATH, patchAstroConfig, patchAstroEnvDts, patchAstroLayout, } from '../patch/astro-patch.js';
|
|
9
|
+
import { ASTRO_ENV_DTS_PATH, ASTRO_RETICLE_DEV_PATH, astroReticleDevFile, patchAstroConfig, patchAstroEnvDts, patchAstroLayout, } from '../patch/astro-patch.js';
|
|
10
10
|
import { CRA_DEV_MODULE_IMPORT, CRA_ENV_PATH, CRA_TOKEN_PER_MACHINE_NOTICE, TOKEN_VAR, craDevModuleFile, craDevModulePath, craEnvPatch, craImportPatch, } from '../patch/cra.js';
|
|
11
11
|
import { PatchKind } from '../patch/patch-kind.js';
|
|
12
12
|
import { viteManual, NEXT_LAYOUT_MANUAL, NEXT_LAYOUT_PATH, viteDevModuleFile, VITE_DEV_MODULE_PATH, nextReticleDevFile, NEXT_RETICLE_DEV_PATH, nextConfigManual, svelteKitHooksFile, SVELTEKIT_HOOKS_PATH, UNVERIFIED_FRAMEWORK_NOTE, astroManual, nuxtManual, nuxtPluginFile, nuxtPluginPath, NUXT_PLUGIN_NOTICE, webpack4TranspileNote, WEBPACK4_REACT_SCRIPTS_MAJOR, reactRouterEntryFile, reactRouterEntryPatch, REACT_ROUTER_ENTRY_PATH, UNVERIFIED_TANSTACK_START_NOTE, htmlManual, } from '../patch/snippets.js';
|
|
@@ -636,7 +636,7 @@ export function svelteKitSteps(input) {
|
|
|
636
636
|
];
|
|
637
637
|
}
|
|
638
638
|
/**
|
|
639
|
-
* Astro: the config
|
|
639
|
+
* Astro: the config build target, and the connect script (with the pairing token) in ONE layout.
|
|
640
640
|
*
|
|
641
641
|
* Astro was the last gated stack left printing a recipe it did not apply. It still falls back to the
|
|
642
642
|
* printed one whenever the choice is not obvious — no config, no single layout, or a shape the
|
|
@@ -659,14 +659,15 @@ export function astroSteps(input) {
|
|
|
659
659
|
},
|
|
660
660
|
];
|
|
661
661
|
}
|
|
662
|
-
// ATOMIC. The connect snippet is useless without the config:
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
662
|
+
// ATOMIC. The connect snippet is useless without the config: without `build.target: 'es2022'`
|
|
663
|
+
// Astro down-levels the SDK and the dynamic import dies, and without `optimizeDeps.include` the
|
|
664
|
+
// first load 404s the hashed module. The token itself now lives in the layout frontmatter (#1008),
|
|
665
|
+
// but a layout patched on its own still cannot connect. Measured on a real fixture — config ⚠,
|
|
666
|
+
// layout ✓ — which reads as one step done and one caveat when it is actually a guaranteed
|
|
667
|
+
// non-connection. If either half cannot be applied, BOTH go manual with the single recipe.
|
|
667
668
|
const manualWithLayout = astroManual(input.options.port, input.options.projectId, layout.path);
|
|
668
669
|
const configPatch = patchAstroConfig(config.source);
|
|
669
|
-
const layoutPatch = patchAstroLayout(layout.source,
|
|
670
|
+
const layoutPatch = patchAstroLayout(layout.source, layout.path);
|
|
670
671
|
if (configPatch.kind === PatchKind.MANUAL || layoutPatch.kind === PatchKind.MANUAL) {
|
|
671
672
|
return [
|
|
672
673
|
{
|
|
@@ -678,13 +679,34 @@ export function astroSteps(input) {
|
|
|
678
679
|
];
|
|
679
680
|
}
|
|
680
681
|
const envPatch = patchAstroEnvDts(input.astroEnvDts ?? null);
|
|
682
|
+
const existingDev = input.astroReticleDev ?? null;
|
|
683
|
+
const devAlready = 'string' === typeof existingDev && existingDev.includes('reticle.connect');
|
|
684
|
+
const devStep = devAlready
|
|
685
|
+
? {
|
|
686
|
+
title: StepTitle.ASTRO_RETICLE_DEV,
|
|
687
|
+
target: ASTRO_RETICLE_DEV_PATH,
|
|
688
|
+
status: StepStatus.ALREADY,
|
|
689
|
+
detail: 'file exists, left alone',
|
|
690
|
+
}
|
|
691
|
+
: {
|
|
692
|
+
title: StepTitle.ASTRO_RETICLE_DEV,
|
|
693
|
+
target: ASTRO_RETICLE_DEV_PATH,
|
|
694
|
+
status: StepStatus.APPLY,
|
|
695
|
+
detail: 'write the local connect module (static SDK import, token from <meta>)',
|
|
696
|
+
write: {
|
|
697
|
+
path: ASTRO_RETICLE_DEV_PATH,
|
|
698
|
+
content: astroReticleDevFile(input.options.port, input.options.projectId, input.detection.uiLibrary, input.testids ?? []),
|
|
699
|
+
},
|
|
700
|
+
dependsOnInstall: true,
|
|
701
|
+
};
|
|
681
702
|
return [
|
|
682
|
-
patchStep(StepTitle.ASTRO_CONFIG, config.path, configPatch, '
|
|
683
|
-
patchStep(StepTitle.CONNECT_SNIPPET_ASTRO, layout.path, layoutPatch, 'add the
|
|
703
|
+
patchStep(StepTitle.ASTRO_CONFIG, config.path, configPatch, 'raise build.target to es2022 and keep .reticle/ out of the watcher', manualWithLayout),
|
|
704
|
+
patchStep(StepTitle.CONNECT_SNIPPET_ASTRO, layout.path, layoutPatch, 'add the pairing-token <meta> and a script that statically imports ReticleDev', manualWithLayout),
|
|
705
|
+
devStep,
|
|
684
706
|
// Declares the Vite define names so `astro check` (create-astro's default build) can see them
|
|
685
707
|
// (#677). Independent of the two halves above: even an ALREADY config/layout still needs this
|
|
686
708
|
// when the env file was never written.
|
|
687
|
-
patchStep(StepTitle.ASTRO_ENV_DTS, ASTRO_ENV_DTS_PATH, envPatch, 'declare __RETICLE_TOKEN__ / __RETICLE_ROOT__ for astro check', manualWithLayout),
|
|
709
|
+
patchStep(StepTitle.ASTRO_ENV_DTS, ASTRO_ENV_DTS_PATH, envPatch, 'declare window.__RETICLE_TOKEN__ / __RETICLE_ROOT__ for astro check', manualWithLayout),
|
|
688
710
|
];
|
|
689
711
|
}
|
|
690
712
|
/**
|
|
@@ -114,6 +114,13 @@ export interface PlanInput {
|
|
|
114
114
|
claudeCli: boolean;
|
|
115
115
|
/** Whether an `reticle` MCP server is already registered with Claude (any scope) — idempotency. */
|
|
116
116
|
mcpExists: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Whether `init` is running inside Claude Code itself, which is the one case where writing its
|
|
119
|
+
* project-scope `.mcp.json` is known to be wanted. See `CLAUDE_PROJECT_SPEC`.
|
|
120
|
+
*/
|
|
121
|
+
insideClaudeCode?: boolean | undefined;
|
|
122
|
+
/** The project `.mcp.json` at the agent's root, read so the merge stays idempotent. */
|
|
123
|
+
claudeProjectConfig?: string | null | undefined;
|
|
117
124
|
/** `process.platform`. Injected so this module stays pure. Windows is the only branch. */
|
|
118
125
|
platform?: string;
|
|
119
126
|
/**
|
|
@@ -175,10 +182,15 @@ export interface PlanInput {
|
|
|
175
182
|
source: string;
|
|
176
183
|
} | null | undefined;
|
|
177
184
|
/**
|
|
178
|
-
* Existing `src/env.d.ts` content, when present — where the
|
|
179
|
-
* so `astro check` can see `__RETICLE_TOKEN__` / `__RETICLE_ROOT__` (#677).
|
|
185
|
+
* Existing `src/env.d.ts` content, when present — where the window ambient declarations go
|
|
186
|
+
* so `astro check` can see `__RETICLE_TOKEN__` / `__RETICLE_ROOT__` (#677, #1008).
|
|
180
187
|
*/
|
|
181
188
|
astroEnvDts?: string | null | undefined;
|
|
189
|
+
/**
|
|
190
|
+
* Existing `src/components/ReticleDev.ts` content, when present — the local module whose
|
|
191
|
+
* static SDK import is what actually dials the daemon.
|
|
192
|
+
*/
|
|
193
|
+
astroReticleDev?: string | null | undefined;
|
|
182
194
|
/** Discovered Next config filename (e.g. 'next.config.mjs'), or null. */
|
|
183
195
|
nextConfigFile: string | null;
|
|
184
196
|
/** Source of that Next config, so the export can be wrapped in withReticle. */
|
|
@@ -283,6 +295,10 @@ export interface PlanInput {
|
|
|
283
295
|
claudeCommandContent?: string | null | undefined;
|
|
284
296
|
/** Current .cursor/commands/reticle.md content, or null when absent. */
|
|
285
297
|
cursorCommandContent?: string | null | undefined;
|
|
298
|
+
/** `--hooks`: install the print-only Claude Code Stop hook. Off unless asked. */
|
|
299
|
+
hooks?: boolean | undefined;
|
|
300
|
+
/** Current .claude/settings.json content, or null when absent (the hook is MERGED into it). */
|
|
301
|
+
claudeSettingsContent?: string | null | undefined;
|
|
286
302
|
/**
|
|
287
303
|
* Absolute directory the AGENT runs in, when that is not the app's directory.
|
|
288
304
|
*
|
package/dist/plan/plan.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
import { Framework, UiLibrary, installCommand, installCommandParts, } from '../detect/detect.js';
|
|
7
7
|
import { installFailureHint } from '../diagnose/install-hint.js';
|
|
8
8
|
import { installRetries } from '../diagnose/install-retries.js';
|
|
9
|
-
import { claudeAddCommand, mcpManual } from '../register/mcp.js';
|
|
10
|
-
import { mergeClientConfig, ClientMergeStatus, clientSnippet, clientSpec, McpClient, } from '../register/mcp-clients.js';
|
|
9
|
+
import { claudeAddCommand, claudeProjectMcpJson, mcpManual } from '../register/mcp.js';
|
|
10
|
+
import { mergeClientConfig, ClientMergeStatus, clientSnippet, clientSpec, CLAUDE_PROJECT_CONFIG, CLAUDE_PROJECT_SPEC, McpClient, } from '../register/mcp-clients.js';
|
|
11
11
|
import { CLAUDE_COMMAND_PATH, CURSOR_COMMAND_PATH, SLASH_COMMAND_BODY, SLASH_COMMAND_SIGNATURE, } from '../register/slash-command.js';
|
|
12
12
|
import { mergeMarkedInstruction, reticleMdFile, cursorRuleFile, AgentRuleStatus, CLAUDE_MD_PATH, markedBlock, AGENTS_MD_PATH, RETICLE_MD_PATH, CURSOR_RULE_PATH, } from '../project/agent-rules.js';
|
|
13
13
|
import { cspStep } from './plan-framework.js';
|
|
@@ -20,6 +20,7 @@ import { reticleConfigContent } from '../patch/snippets.js';
|
|
|
20
20
|
import { configWithInstallSource } from '../project/install-source-config.js';
|
|
21
21
|
import { containerisedStep, uiLibraryStep, webGlCanvasStep, windowsMcpNoteStep } from './notices.js';
|
|
22
22
|
import { existingConfigProblem, projectIdOf, RETICLE_CONFIG_FILE } from '../detect/existing-config.js';
|
|
23
|
+
import { CLAUDE_SETTINGS_PATH, stopHookStep } from './stop-hook-step.js';
|
|
23
24
|
// Re-exported: it moved to the module that reads it, and every existing importer says `plan.js`.
|
|
24
25
|
export { RETICLE_CONFIG_FILE };
|
|
25
26
|
/**
|
|
@@ -104,40 +105,49 @@ function claudeMcpStep(input) {
|
|
|
104
105
|
* parse is reported with a paste-able block rather than overwritten, and everything else is written.
|
|
105
106
|
*/
|
|
106
107
|
function otherClientSteps(input) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
target: detected.configPath,
|
|
116
|
-
status: StepStatus.ALREADY,
|
|
117
|
-
detail: `reticle already registered with ${spec.label}`,
|
|
118
|
-
});
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
if (merged.status === ClientMergeStatus.MANUAL) {
|
|
122
|
-
// Either the file did not parse, or the format is one we refuse to edit blind (TOML). Both
|
|
123
|
-
// end the same way: say so, and hand over the exact block.
|
|
124
|
-
steps.push({
|
|
125
|
-
title,
|
|
126
|
-
target: detected.configPath,
|
|
127
|
-
status: StepStatus.MANUAL,
|
|
128
|
-
detail: `add this to ${detected.configPath} by hand:\n${clientSnippet(spec)}`,
|
|
129
|
-
});
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
steps.push({
|
|
108
|
+
return (input.detectedClients ?? []).map((detected) => clientStep(clientSpec(detected.id), detected.configPath, detected.existing));
|
|
109
|
+
}
|
|
110
|
+
/** One client's step: already-correct is left alone, an unparseable file gets a paste-able block. */
|
|
111
|
+
function clientStep(spec, configPath, existing) {
|
|
112
|
+
const merged = mergeClientConfig(spec, existing);
|
|
113
|
+
const title = `MCP server (${spec.label})`;
|
|
114
|
+
if (merged.status === ClientMergeStatus.ALREADY) {
|
|
115
|
+
return {
|
|
133
116
|
title,
|
|
134
|
-
target:
|
|
135
|
-
status: StepStatus.
|
|
136
|
-
detail: `
|
|
137
|
-
|
|
138
|
-
});
|
|
117
|
+
target: configPath,
|
|
118
|
+
status: StepStatus.ALREADY,
|
|
119
|
+
detail: `reticle already registered with ${spec.label}`,
|
|
120
|
+
};
|
|
139
121
|
}
|
|
140
|
-
|
|
122
|
+
if (merged.status === ClientMergeStatus.MANUAL) {
|
|
123
|
+
// Either the file did not parse, or the format is one we refuse to edit blind (TOML). Both
|
|
124
|
+
// end the same way: say so, and hand over the exact block.
|
|
125
|
+
return {
|
|
126
|
+
title,
|
|
127
|
+
target: configPath,
|
|
128
|
+
status: StepStatus.MANUAL,
|
|
129
|
+
detail: `add this to ${configPath} by hand:\n${clientSnippet(spec)}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
title,
|
|
134
|
+
target: configPath,
|
|
135
|
+
status: StepStatus.APPLY,
|
|
136
|
+
detail: `register reticle with ${spec.label}`,
|
|
137
|
+
write: { path: configPath, content: merged.content },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Claude Code's project `.mcp.json`, written only when `init` runs inside Claude Code with no CLI on
|
|
142
|
+
* PATH (a VS Code extension session). There the reporter had to write this file by hand (#1071,
|
|
143
|
+
* #1078); anywhere else a `.mcp.json` is a file in somebody's repo for a client they may not use, so
|
|
144
|
+
* they get the notice instead.
|
|
145
|
+
*/
|
|
146
|
+
function claudeProjectStep(input) {
|
|
147
|
+
if (input.claudeCli || true !== input.insideClaudeCode)
|
|
148
|
+
return null;
|
|
149
|
+
const path = agentFile(input, CLAUDE_PROJECT_CONFIG);
|
|
150
|
+
return clientStep(CLAUDE_PROJECT_SPEC, path, input.claudeProjectConfig ?? null);
|
|
141
151
|
}
|
|
142
152
|
/** One global registration per detected agent (Claude + Cursor). Falls back to a manual note. */
|
|
143
153
|
function mcpSteps(input) {
|
|
@@ -157,7 +167,12 @@ function mcpSteps(input) {
|
|
|
157
167
|
// Claude and Cursor first (they have their own registration paths), then every other detected
|
|
158
168
|
// client. A machine with Cursor AND Windsurf gets both — registering only the first one found is
|
|
159
169
|
// how a user ends up with Reticle in the editor they were not using.
|
|
160
|
-
const
|
|
170
|
+
const claudeProject = claudeProjectStep(input);
|
|
171
|
+
const steps = [
|
|
172
|
+
...stepsForAgents(input, (a) => a.mcpStep),
|
|
173
|
+
...(null === claudeProject ? [] : [claudeProject]),
|
|
174
|
+
...otherClientSteps(input),
|
|
175
|
+
];
|
|
161
176
|
if (0 === steps.length) {
|
|
162
177
|
// No agent detected. mcpManual already carries the Windows cmd fallback — do not append it again.
|
|
163
178
|
return [
|
|
@@ -169,6 +184,32 @@ function mcpSteps(input) {
|
|
|
169
184
|
},
|
|
170
185
|
];
|
|
171
186
|
}
|
|
187
|
+
/*
|
|
188
|
+
* Claude Code never leaves the plan without a word (#1071).
|
|
189
|
+
*
|
|
190
|
+
* It is the only client detected by CLI-on-PATH rather than by its config, so inside a Claude Code
|
|
191
|
+
* VS Code extension session — where `claude` is not on PATH — it reads as absent while the user is
|
|
192
|
+
* sitting in it. `claudeMcpStep` then returned null, and the manual fallback below only fires when
|
|
193
|
+
* NO agent at all was found. With Gemini and Codex present, Claude Code vanished from the plan
|
|
194
|
+
* entirely and nothing said so. A step that disappears is exactly what the install gate's baseline
|
|
195
|
+
* diff exists to catch, and the plan is the one artifact a person reads to learn what init did.
|
|
196
|
+
*
|
|
197
|
+
* Only when something else WAS found. With nothing found, the generic note below already says how
|
|
198
|
+
* to register, so nothing has silently vanished — and leaving that path untouched keeps every
|
|
199
|
+
* pristine scaffold in the install baseline reading exactly as it did.
|
|
200
|
+
*/
|
|
201
|
+
if (!input.claudeCli && null === claudeProject && steps.length > 0) {
|
|
202
|
+
steps.push({
|
|
203
|
+
title: CLAUDE_MCP_TITLE,
|
|
204
|
+
target: MCP_TARGET,
|
|
205
|
+
// NOTICE, not MANUAL. Nothing here FAILED and the reader may not use Claude Code at all, so
|
|
206
|
+
// this is something to know rather than work owed — and the install gate asserts zero `⚠`,
|
|
207
|
+
// which would turn an informational line into a gate failure on any machine that happens to
|
|
208
|
+
// have another agent's config. A notice prints in full exactly like a manual step.
|
|
209
|
+
status: StepStatus.NOTICE,
|
|
210
|
+
detail: `no \`claude\` on PATH, so it could not be registered from here. Inside a Claude Code VS Code extension session the CLI is genuinely absent while the editor is not — add ${claudeProjectMcpJson()}`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
172
213
|
const windowsNote = windowsMcpNoteStep(input);
|
|
173
214
|
if (windowsNote !== null)
|
|
174
215
|
steps.push(windowsNote);
|
|
@@ -415,10 +456,48 @@ function agentRuleSteps(input) {
|
|
|
415
456
|
* with nothing naming a version. Pinning turns that into this loud failure, which is the better
|
|
416
457
|
* trade — but only if the message says what to do about it.
|
|
417
458
|
*/
|
|
459
|
+
/**
|
|
460
|
+
* Is this package already declared at the version we would pin?
|
|
461
|
+
*
|
|
462
|
+
* The caret is the point. `npm i -D pkg@3.2.0` WRITES `^3.2.0`, so on every later run the pinned
|
|
463
|
+
* `3.2.0` failed a string comparison against the range npm had just created, and init re-installed
|
|
464
|
+
* for ever. Only the two forms a package manager writes by itself are read - an exact version, and
|
|
465
|
+
* `^`/`~` over that same version.
|
|
466
|
+
*
|
|
467
|
+
* Everything else installs. A false "already" leaves somebody with no SDK and an install that
|
|
468
|
+
* claims to have run; a redundant install only costs time, so the unreadable cases go the safe way.
|
|
469
|
+
*/
|
|
470
|
+
function alreadyDeclared(declared, pinned) {
|
|
471
|
+
if (declared === undefined)
|
|
472
|
+
return false;
|
|
473
|
+
if (pinned === undefined || 0 === pinned.length)
|
|
474
|
+
return true;
|
|
475
|
+
return declared === pinned || `^${pinned}` === declared || `~${pinned}` === declared;
|
|
476
|
+
}
|
|
418
477
|
function installStep(input) {
|
|
419
478
|
const pm = input.detection.packageManager;
|
|
420
479
|
const packages = pinnedPackages(frameworkPackages(input.detection.framework, input.detection.uiLibrary), input.options.sdkVersion);
|
|
421
480
|
const command = installCommand(pm, packages);
|
|
481
|
+
/*
|
|
482
|
+
* A re-run over an already-wired project does NO dependency work.
|
|
483
|
+
*
|
|
484
|
+
* Reported from the field: `init` on an instrumented npm-workspaces project ran a pnpm dependency
|
|
485
|
+
* migration, which moved the existing `node_modules` aside and broke the dev server. Whatever
|
|
486
|
+
* manager gets chosen, the redundant install is the step that touches the tree — so the fix that
|
|
487
|
+
* matters is not running one that nothing needs. `init` is documented as idempotent; this is the
|
|
488
|
+
* step that was not.
|
|
489
|
+
*/
|
|
490
|
+
const declared = input.detection.dependencies ?? {};
|
|
491
|
+
const version = input.options.sdkVersion;
|
|
492
|
+
const names = frameworkPackages(input.detection.framework, input.detection.uiLibrary);
|
|
493
|
+
if (names.length > 0 && names.every((n) => alreadyDeclared(declared[n], version))) {
|
|
494
|
+
return {
|
|
495
|
+
title: 'Install dependencies',
|
|
496
|
+
target: 'package.json',
|
|
497
|
+
status: StepStatus.ALREADY,
|
|
498
|
+
detail: `${names.join(', ')} already declared${version === undefined || 0 === version.length ? '' : ` at ${version}`}`,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
422
501
|
if (!input.options.install) {
|
|
423
502
|
return {
|
|
424
503
|
title: 'Install dependencies',
|
|
@@ -574,6 +653,9 @@ export function buildPlan(input) {
|
|
|
574
653
|
...mcpSteps(input),
|
|
575
654
|
...agentRuleSteps(input),
|
|
576
655
|
...slashCommandSteps(input),
|
|
656
|
+
...(true === input.hooks
|
|
657
|
+
? [stopHookStep(agentFile(input, CLAUDE_SETTINGS_PATH), input.claudeSettingsContent)]
|
|
658
|
+
: []),
|
|
577
659
|
...uiLibraryStep(input),
|
|
578
660
|
...webGlCanvasStep(input),
|
|
579
661
|
installStep(input),
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type Step } from './plan-types.js';
|
|
2
|
+
export declare const CLAUDE_SETTINGS_PATH = ".claude/settings.json";
|
|
3
|
+
export declare const STOP_HOOK_COMMAND = "npx @reticlehq/server report --hook";
|
|
4
|
+
export declare function stopHookStep(path: string, existing: string | null | undefined): Step;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `init --hooks`: the print-only Claude Code Stop hook, merged into `.claude/settings.json`.
|
|
3
|
+
*
|
|
4
|
+
* Opt-in, because it writes into a file the user and other tools own. When the agent stops without a
|
|
5
|
+
* single claim having held, the hook prints one line saying so; it never blocks the agent. The line
|
|
6
|
+
* itself is `reticle report --hook`, which reads the session journal on disk.
|
|
7
|
+
*/
|
|
8
|
+
import { RETICLE_NPM_PACKAGE } from '../version.js';
|
|
9
|
+
import { StepStatus } from './plan-types.js';
|
|
10
|
+
export const CLAUDE_SETTINGS_PATH = '.claude/settings.json';
|
|
11
|
+
export const STOP_HOOK_COMMAND = `npx ${RETICLE_NPM_PACKAGE} report --hook`;
|
|
12
|
+
const TITLE = 'Claude Code Stop hook';
|
|
13
|
+
const STOP_EVENT = 'Stop';
|
|
14
|
+
const HOOK_ENTRY = { hooks: [{ type: 'command', command: STOP_HOOK_COMMAND }] };
|
|
15
|
+
const isObject = (value) => 'object' === typeof value && null !== value && !Array.isArray(value);
|
|
16
|
+
/** The settings as an object, or undefined when the text is not one. */
|
|
17
|
+
function parseSettings(text) {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(text);
|
|
20
|
+
return isObject(parsed) ? parsed : undefined;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function stopHookStep(path, existing) {
|
|
27
|
+
const settings = null === existing || undefined === existing ? {} : parseSettings(existing);
|
|
28
|
+
if (settings === undefined) {
|
|
29
|
+
return {
|
|
30
|
+
title: TITLE,
|
|
31
|
+
target: path,
|
|
32
|
+
status: StepStatus.MANUAL,
|
|
33
|
+
detail: `${CLAUDE_SETTINGS_PATH} is not a JSON object, so it was left alone. Add a Stop hook running \`${STOP_HOOK_COMMAND}\``,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const hooks = isObject(settings['hooks']) ? settings['hooks'] : {};
|
|
37
|
+
const stop = Array.isArray(hooks[STOP_EVENT]) ? hooks[STOP_EVENT] : [];
|
|
38
|
+
if (JSON.stringify(stop).includes(STOP_HOOK_COMMAND)) {
|
|
39
|
+
return { title: TITLE, target: path, status: StepStatus.ALREADY, detail: 'hook already installed' };
|
|
40
|
+
}
|
|
41
|
+
const merged = { ...settings, hooks: { ...hooks, [STOP_EVENT]: [...stop, HOOK_ENTRY] } };
|
|
42
|
+
return {
|
|
43
|
+
title: TITLE,
|
|
44
|
+
target: path,
|
|
45
|
+
status: StepStatus.APPLY,
|
|
46
|
+
detail: 'print one line when the agent stops without a claim that held; never blocks it',
|
|
47
|
+
write: { path, content: `${JSON.stringify(merged, null, 2)}\n`, expect: [STOP_HOOK_COMMAND] },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { NPX } from '../register/mcp.js';
|
|
21
21
|
import { RETICLE_NPM_PACKAGE } from '../version.js';
|
|
22
|
+
import { DISCOVERY_CALL_URL, DISCOVERY_EMAIL_COMMAND } from '@reticlehq/core';
|
|
22
23
|
/** Project-root instruction files the agent re-reads every session. */
|
|
23
24
|
export const CLAUDE_MD_PATH = 'CLAUDE.md';
|
|
24
25
|
export const AGENTS_MD_PATH = 'AGENTS.md';
|
|
@@ -206,6 +207,7 @@ Finish that file when you see it. It is what lets you check what the app BELIEVE
|
|
|
206
207
|
- **Working around a Reticle defect in silence is the one thing not to do.** A silent workaround is how the same defect survives to the next user.
|
|
207
208
|
- **Report defects in *Reticle*, not in the app under test.** A bug you find in this app is Reticle working as intended; that one belongs in your report to the user.
|
|
208
209
|
- **Never include app source, secrets, user data, or customer records** in a report.
|
|
210
|
+
- **When the person you work with is stuck, or Reticle got in their way, offer them a call with the founder** (${DISCOVERY_CALL_URL}) or leaving an email with \`${DISCOVERY_EMAIL_COMMAND}\`. That conversation is where Reticle's direction comes from. Offer it; never book a call or submit an email for them.
|
|
209
211
|
|
|
210
212
|
## More
|
|
211
213
|
|
|
@@ -37,6 +37,19 @@ export interface ClientSpec {
|
|
|
37
37
|
/** Where the shape came from, so the next person can re-check it rather than trust this file. */
|
|
38
38
|
docs: string;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Claude Code's project-scope MCP config, which needs no CLI on PATH.
|
|
42
|
+
*
|
|
43
|
+
* Named here beside the client specs because it IS one of Claude Code's two config routes, and a
|
|
44
|
+
* free string for it is the reason nobody noticed the other route existed when the CLI one failed.
|
|
45
|
+
*/
|
|
46
|
+
export declare const CLAUDE_PROJECT_CONFIG = ".mcp.json";
|
|
47
|
+
/**
|
|
48
|
+
* Claude Code's project-scope registration, as a client spec so it goes through the same merge
|
|
49
|
+
* (already / manual / apply) as every other client. Not in `MCP_CLIENTS`: that list is DETECTED by
|
|
50
|
+
* its config existing, and this one is only ever written from inside Claude Code (see plan.ts).
|
|
51
|
+
*/
|
|
52
|
+
export declare const CLAUDE_PROJECT_SPEC: ClientSpec;
|
|
40
53
|
export declare const MCP_CLIENTS: readonly ClientSpec[];
|
|
41
54
|
export declare function clientSpec(id: McpClient): ClientSpec;
|
|
42
55
|
export declare const ClientMergeStatus: {
|
|
@@ -53,6 +53,28 @@ function commandArgsEntry() {
|
|
|
53
53
|
function openCodeEntry() {
|
|
54
54
|
return { type: 'local', command: [NPX, ...npxServerArgs()], enabled: true };
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Claude Code's project-scope MCP config, which needs no CLI on PATH.
|
|
58
|
+
*
|
|
59
|
+
* Named here beside the client specs because it IS one of Claude Code's two config routes, and a
|
|
60
|
+
* free string for it is the reason nobody noticed the other route existed when the CLI one failed.
|
|
61
|
+
*/
|
|
62
|
+
export const CLAUDE_PROJECT_CONFIG = '.mcp.json';
|
|
63
|
+
/**
|
|
64
|
+
* Claude Code's project-scope registration, as a client spec so it goes through the same merge
|
|
65
|
+
* (already / manual / apply) as every other client. Not in `MCP_CLIENTS`: that list is DETECTED by
|
|
66
|
+
* its config existing, and this one is only ever written from inside Claude Code (see plan.ts).
|
|
67
|
+
*/
|
|
68
|
+
export const CLAUDE_PROJECT_SPEC = {
|
|
69
|
+
id: McpClient.CLAUDE_CODE,
|
|
70
|
+
label: 'Claude Code, project',
|
|
71
|
+
scope: ConfigScope.PROJECT,
|
|
72
|
+
relPath: CLAUDE_PROJECT_CONFIG,
|
|
73
|
+
format: ConfigFormat.JSON,
|
|
74
|
+
serversKey: 'mcpServers',
|
|
75
|
+
entry: commandArgsEntry,
|
|
76
|
+
docs: 'https://docs.claude.com/en/docs/claude-code/mcp: project scope, .mcp.json at the project root',
|
|
77
|
+
};
|
|
56
78
|
export const MCP_CLIENTS = [
|
|
57
79
|
{
|
|
58
80
|
id: McpClient.CLAUDE_CODE,
|
package/dist/register/mcp.d.ts
CHANGED
|
@@ -83,4 +83,12 @@ export declare function claudeAvailableProbe(): {
|
|
|
83
83
|
*/
|
|
84
84
|
export declare function mcpWindowsNote(): string;
|
|
85
85
|
/** Printed when the `claude` CLI isn't available — register Reticle globally once, by hand. */
|
|
86
|
+
/**
|
|
87
|
+
* Claude Code's PROJECT-scope config, which needs no CLI on PATH.
|
|
88
|
+
*
|
|
89
|
+
* The remedy a reporter had to work out for themselves: inside a Claude Code VS Code extension
|
|
90
|
+
* session the `claude` binary is not on PATH, so every CLI-based route is shut and `.mcp.json` in
|
|
91
|
+
* the project root is the one that still works.
|
|
92
|
+
*/
|
|
93
|
+
export declare function claudeProjectMcpJson(): string;
|
|
86
94
|
export declare function mcpManual(): string;
|
package/dist/register/mcp.js
CHANGED
|
@@ -16,6 +16,7 @@ import { RETICLE_NPM_PACKAGE } from '../version.js';
|
|
|
16
16
|
// below (and their tests) keep reading it from the module that USES it.
|
|
17
17
|
export { MCP_SERVER_NAME } from '@reticlehq/core';
|
|
18
18
|
import { MCP_SERVER_NAME } from '@reticlehq/core';
|
|
19
|
+
import { CLAUDE_PROJECT_CONFIG } from './mcp-clients.js';
|
|
19
20
|
/**
|
|
20
21
|
* The registered command is STILL bare `npx` on every platform, deliberately.
|
|
21
22
|
*
|
|
@@ -112,12 +113,27 @@ does not gate:
|
|
|
112
113
|
"${MCP_SERVER_NAME}": { "command": "${WINDOWS_SHELL}", "args": ${JSON.stringify([WINDOWS_SHELL_C, NPX, ...serverInvocation().slice(1)])} }`;
|
|
113
114
|
}
|
|
114
115
|
/** Printed when the `claude` CLI isn't available — register Reticle globally once, by hand. */
|
|
116
|
+
/**
|
|
117
|
+
* Claude Code's PROJECT-scope config, which needs no CLI on PATH.
|
|
118
|
+
*
|
|
119
|
+
* The remedy a reporter had to work out for themselves: inside a Claude Code VS Code extension
|
|
120
|
+
* session the `claude` binary is not on PATH, so every CLI-based route is shut and `.mcp.json` in
|
|
121
|
+
* the project root is the one that still works.
|
|
122
|
+
*/
|
|
123
|
+
export function claudeProjectMcpJson() {
|
|
124
|
+
const [command, ...args] = serverInvocation();
|
|
125
|
+
return `${CLAUDE_PROJECT_CONFIG} (project root):
|
|
126
|
+
|
|
127
|
+
{ "mcpServers": { "${MCP_SERVER_NAME}": { "command": "${command ?? NPX}", "args": ${JSON.stringify(args)} } } }`;
|
|
128
|
+
}
|
|
115
129
|
export function mcpManual() {
|
|
116
130
|
const tail = serverInvocation().join(' ');
|
|
117
131
|
return `Register the Reticle MCP server ONCE, globally (so every project gets it):
|
|
118
132
|
|
|
119
133
|
${CLAUDE_CLI} ${MCP_SUBCOMMAND} add ${MCP_SERVER_NAME} -s user -- ${tail}
|
|
120
134
|
|
|
135
|
+
Or, in Claude Code with no \`claude\` on PATH (a VS Code extension session), write ${claudeProjectMcpJson()}
|
|
136
|
+
|
|
121
137
|
Or, for another agent, add this to its global MCP config (e.g. Cursor's ~/.cursor/mcp.json):
|
|
122
138
|
|
|
123
139
|
"${MCP_SERVER_NAME}": { "command": "${NPX}", "args": ${JSON.stringify(serverInvocation().slice(1))} }
|
package/dist/run-types.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ import type { InitOutcome } from '@reticlehq/core/telemetry';
|
|
|
10
10
|
export interface InitOptions {
|
|
11
11
|
/** `--capture-bodies`: write `captureNetworkBodies: true` into the app's config. Off by default (#705). */
|
|
12
12
|
captureBodies?: boolean | undefined;
|
|
13
|
+
/** `--hooks`: also install the print-only Claude Code Stop hook. Off by default. */
|
|
14
|
+
hooks?: boolean | undefined;
|
|
13
15
|
cwd: string;
|
|
14
16
|
port: number | undefined;
|
|
15
17
|
mcp: boolean;
|
package/dist/run.js
CHANGED
|
@@ -20,6 +20,7 @@ import { ELECTRON_VITE_CONFIG_CANDIDATES, NEXT_CONFIG_CANDIDATES, PACKAGE_JSON,
|
|
|
20
20
|
import { redirectToWorkspaceApp } from './detect/workspace-redirect.js';
|
|
21
21
|
import { isConnectStep } from './plan/connect-steps.js';
|
|
22
22
|
import { CURSOR_RULE_PATH, RETICLE_MD_PATH } from './project/agent-rules.js';
|
|
23
|
+
import { CLAUDE_SETTINGS_PATH } from './plan/stop-hook-step.js';
|
|
23
24
|
import { CRA_ENV_PATH } from './patch/cra.js';
|
|
24
25
|
import { NEXT_LAYOUT_CANDIDATES, NEXT_PAGES_APP_CANDIDATES } from './patch/next-patch.js';
|
|
25
26
|
import { formatGeneratedSource } from './patch/format-generated.js';
|
|
@@ -37,7 +38,7 @@ import { DEPS_TARGET, RETICLE_CONFIG_FILE, frameworkPackages, MCP_TARGET, buildP
|
|
|
37
38
|
import { claudeAvailableProbe, claudeExistsProbe } from './register/mcp.js';
|
|
38
39
|
import { reticleDevLocation } from './patch/next-patch.js';
|
|
39
40
|
import { scanTestids, storeHints, scanStores } from './detect/capabilities.js';
|
|
40
|
-
import { CURSOR_PROJECT_MARKER } from './register/mcp-clients.js';
|
|
41
|
+
import { CLAUDE_PROJECT_CONFIG, CURSOR_PROJECT_MARKER } from './register/mcp-clients.js';
|
|
41
42
|
import { deriveProjectId, packageName } from './project/project-id.js';
|
|
42
43
|
import { VITE_DEV_MODULE_PATH, ELECTRON_VITE_DEV_MODULE_PATH, connectArgWithToken, nuxtPluginPath, staticPageSnippet, djangoMiddlewareSnippet, reticleConfigContent, streamlitPageSnippet, } from './patch/snippets.js';
|
|
43
44
|
import { NUXT_CONFIG_CANDIDATES } from './patch/nuxt-patch.js';
|
|
@@ -294,9 +295,12 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
294
295
|
return {
|
|
295
296
|
detection,
|
|
296
297
|
captureBodies: options.captureBodies,
|
|
298
|
+
hooks: options.hooks,
|
|
297
299
|
cspSources,
|
|
298
300
|
claudeCli,
|
|
299
301
|
mcpExists,
|
|
302
|
+
insideClaudeCode: io.host.insideClaudeCode(),
|
|
303
|
+
claudeProjectConfig: options.mcp ? io.readFile(agentFile(CLAUDE_PROJECT_CONFIG)) : undefined,
|
|
300
304
|
platform: process.platform,
|
|
301
305
|
detectedClients,
|
|
302
306
|
cursorProjectPresent: io.exists(CURSOR_PROJECT_MARKER),
|
|
@@ -316,6 +320,7 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
316
320
|
? { path: layoutRelPath, source: astroLayoutSource }
|
|
317
321
|
: null,
|
|
318
322
|
astroEnvDts: io.readFile('src/env.d.ts'),
|
|
323
|
+
astroReticleDev: io.readFile('src/components/ReticleDev.ts'),
|
|
319
324
|
nextConfigFile,
|
|
320
325
|
nextConfigSource: null === nextConfigFile ? null : io.readFile(nextConfigFile),
|
|
321
326
|
nextLayout: layoutPath !== null && layoutSource !== null
|
|
@@ -360,6 +365,8 @@ function gatherPlanInput(options, io, pkg) {
|
|
|
360
365
|
cursorRuleContent: io.readFile(agentFile(CURSOR_RULE_PATH)),
|
|
361
366
|
claudeCommandContent: io.readFile(agentFile(CLAUDE_COMMAND_PATH)),
|
|
362
367
|
cursorCommandContent: io.readFile(agentFile(CURSOR_COMMAND_PATH)),
|
|
368
|
+
// Read only to MERGE into: the settings file is the user's, and other tools write in it too.
|
|
369
|
+
claudeSettingsContent: true === options.hooks ? io.readFile(agentFile(CLAUDE_SETTINGS_PATH)) : undefined,
|
|
363
370
|
...(agentRoot === undefined
|
|
364
371
|
? {}
|
|
365
372
|
: {
|
|
@@ -393,6 +400,20 @@ devCommand,
|
|
|
393
400
|
*/
|
|
394
401
|
continuesToRuntime = false) {
|
|
395
402
|
io.print(dryRun ? 'reticle init (dry run, no files written)' : 'reticle init');
|
|
403
|
+
/*
|
|
404
|
+
* The escape hatches, named by the command that needs them, at the moment it needs them.
|
|
405
|
+
*
|
|
406
|
+
* `--dry-run`, `--app` and `--no-mcp` all exist and all work, and they were discoverable only
|
|
407
|
+
* from `--help`. A prospective user's agent refused to run this at all — correctly, since it was
|
|
408
|
+
* being asked to let a third-party package edit a build config and register itself with every
|
|
409
|
+
* agent on the machine, and nothing it could see offered a way to look first. It had already
|
|
410
|
+
* decided by the time `--help` would have told it.
|
|
411
|
+
*
|
|
412
|
+
* On the REAL run only. In a dry run the reader has already found the flag.
|
|
413
|
+
*/
|
|
414
|
+
if (!dryRun) {
|
|
415
|
+
io.print(' --dry-run shows this plan and writes nothing · --app <dir> picks the app · --no-mcp skips agent registration');
|
|
416
|
+
}
|
|
396
417
|
// Every path below is printed RELATIVE, and until now nothing said what to. Reported from the
|
|
397
418
|
// field as "[✓] Reticle config → .reticle.json" followed by the file not being there: the app was
|
|
398
419
|
// in `frontend/`, init redirected into it, and the report's `.reticle.json` was true about a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/init",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.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.3.0"
|
|
17
17
|
},
|
|
18
18
|
"license": "FSL-1.1-ALv2",
|
|
19
19
|
"author": "Reticle contributors",
|