@reticlehq/init 3.6.0 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/detect/detect.d.ts +9 -0
- package/dist/detect/detect.js +5 -1
- package/dist/detect/dev-script.d.ts +17 -0
- package/dist/detect/dev-script.js +128 -0
- package/dist/detect/dev-server-ports.d.ts +2 -0
- package/dist/detect/dev-server-ports.js +20 -0
- package/dist/detect/prettier-config.d.ts +21 -0
- package/dist/detect/prettier-config.js +38 -0
- package/dist/detect/workspace-apps.d.ts +11 -0
- package/dist/detect/workspace-apps.js +63 -0
- package/dist/detect/workspace-redirect.js +69 -10
- package/dist/diagnose/closing-hint.d.ts +5 -0
- package/dist/diagnose/closing-hint.js +18 -11
- package/dist/diagnose/init-failure.d.ts +2 -0
- package/dist/diagnose/init-failure.js +2 -0
- package/dist/gather-plan-input.d.ts +11 -0
- package/dist/gather-plan-input.js +468 -0
- package/dist/host.d.ts +6 -0
- package/dist/index.d.ts +11 -4
- package/dist/index.js +14 -4
- package/dist/no-package-json.js +62 -14
- package/dist/node-io.d.ts +1 -1
- package/dist/node-io.js +11 -2
- package/dist/patch/next-patch.d.ts +45 -9
- package/dist/patch/next-patch.js +93 -0
- package/dist/patch/nuxt-snippets.js +1 -1
- package/dist/patch/prettier-ignore.d.ts +34 -0
- package/dist/patch/prettier-ignore.js +49 -0
- package/dist/patch/python-snippets.d.ts +22 -0
- package/dist/patch/python-snippets.js +113 -0
- package/dist/patch/snippets.d.ts +10 -31
- package/dist/patch/snippets.js +42 -105
- package/dist/patch/static-page.d.ts +1 -0
- package/dist/patch/static-page.js +6 -8
- package/dist/patch/vite-config.js +11 -0
- package/dist/plan/connect-steps.d.ts +2 -0
- package/dist/plan/connect-steps.js +6 -0
- package/dist/plan/framework-adapter.d.ts +10 -0
- package/dist/plan/framework-adapter.js +122 -3
- package/dist/plan/notices.js +1 -1
- package/dist/plan/plan-angular.js +1 -1
- package/dist/plan/plan-framework.d.ts +1 -1
- package/dist/plan/plan-framework.js +140 -37
- package/dist/plan/plan-types.d.ts +50 -2
- package/dist/plan/plan-vite.js +15 -0
- package/dist/plan/plan.d.ts +1 -10
- package/dist/plan/plan.js +181 -126
- package/dist/plan/stop-hook-step.d.ts +30 -0
- package/dist/plan/stop-hook-step.js +154 -5
- package/dist/project/agent-rules.d.ts +1 -1
- package/dist/project/agent-rules.js +22 -17
- package/dist/project/refresh-rules.js +2 -1
- package/dist/register/codex-toml.d.ts +12 -0
- package/dist/register/codex-toml.js +44 -0
- package/dist/register/mcp.d.ts +15 -2
- package/dist/register/mcp.js +16 -0
- package/dist/register/prompt-hooks.d.ts +123 -0
- package/dist/register/prompt-hooks.js +449 -0
- package/dist/register/slash-command.d.ts +1 -1
- package/dist/register/slash-command.js +5 -4
- package/dist/run-types.d.ts +10 -0
- package/dist/run.d.ts +1 -1
- package/dist/run.js +15 -390
- package/package.json +2 -2
package/dist/plan/plan.js
CHANGED
|
@@ -3,53 +3,28 @@
|
|
|
3
3
|
* contents, produce an ordered list of steps — each marked apply / manual / already / skip. The
|
|
4
4
|
* runner performs the `write` side-effects; this module decides *what* should happen.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { installRetries } from '../diagnose/install-retries.js';
|
|
9
|
-
import { claudeAddCommand, claudeProjectMcpJson, mcpManual } from '../register/mcp.js';
|
|
6
|
+
import { claudeAddCommand, claudeProjectMcpJson, codexAddCommand, mcpManual, } from '../register/mcp.js';
|
|
7
|
+
import { codexNamesOurServer } from '../register/codex-toml.js';
|
|
10
8
|
import { mergeClientConfig, ClientMergeStatus, clientSnippet, clientSpec, CLAUDE_PROJECT_CONFIG, CLAUDE_PROJECT_SPEC, McpClient, } from '../register/mcp-clients.js';
|
|
11
9
|
import { CLAUDE_COMMAND_PATH, CURSOR_COMMAND_PATH, SLASH_COMMAND_BODY, SLASH_COMMAND_SIGNATURE, } from '../register/slash-command.js';
|
|
12
10
|
import { mergeMarkedInstruction, reticleMdFile, cursorRuleFile, AgentRuleStatus, CLAUDE_MD_PATH, markedBlock, AGENTS_MD_PATH, RETICLE_MD_PATH, CURSOR_RULE_PATH, } from '../project/agent-rules.js';
|
|
13
11
|
import { cspStep } from '../diagnose/csp-step.js';
|
|
12
|
+
import { HTML_INDEX_PATH } from '../patch/static-page.js';
|
|
14
13
|
import { frameworkSteps } from './framework-adapter.js';
|
|
15
|
-
import {
|
|
14
|
+
import { installStep } from './framework-adapter.js';
|
|
16
15
|
import { MCP_TARGET, StepStatus } from './plan-types.js';
|
|
17
16
|
export { DEPS_TARGET, MCP_TARGET, StepStatus, } from './plan-types.js';
|
|
18
17
|
import { join } from 'node:path';
|
|
19
|
-
import { reticleConfigContent
|
|
18
|
+
import { reticleConfigContent } from '../patch/snippets.js';
|
|
20
19
|
import { configWithInstallSource } from '../project/install-source-config.js';
|
|
21
20
|
import { portMoveDetail, retargetConfigPort } from './port-steps.js';
|
|
22
21
|
import { containerisedStep, uiLibraryStep, webGlCanvasStep, windowsMcpNoteStep, } from './notices.js';
|
|
23
22
|
import { existingConfigProblem, projectIdOf, RETICLE_CONFIG_FILE, } from '../detect/existing-config.js';
|
|
24
|
-
import { CLAUDE_SETTINGS_PATH,
|
|
23
|
+
import { CLAUDE_SETTINGS_PATH, claudeSettingsSteps, promptHookSteps } from './stop-hook-step.js';
|
|
24
|
+
import { PRETTIERIGNORE_PATH, RETICLE_IGNORE_ENTRY, ignoresReticle, withReticleIgnored, } from '../patch/prettier-ignore.js';
|
|
25
25
|
// Re-exported: it moved to the module that reads it, and every existing importer says `plan.js`.
|
|
26
26
|
export { RETICLE_CONFIG_FILE };
|
|
27
|
-
|
|
28
|
-
* Pin the SDK to the CLI's own version.
|
|
29
|
-
*
|
|
30
|
-
* `pnpm add -D @reticlehq/react` once installed an older release in one project while npm and yarn
|
|
31
|
-
* took the current one in the next — a stale registry metadata cache, invisible to everyone. A
|
|
32
|
-
* version-skewed SDK talking to a newer daemon is the `-32000` failure path: the app connects, the
|
|
33
|
-
* protocol disagrees, and nothing on either side names a version. Asking for the CLI's exact version
|
|
34
|
-
* makes the cache irrelevant, and a skewed pair impossible to install by accident.
|
|
35
|
-
*/
|
|
36
|
-
function pinnedPackages(packages, version) {
|
|
37
|
-
if (version === undefined || 0 === version.length)
|
|
38
|
-
return packages;
|
|
39
|
-
return packages.map((p) => `${p}@${version}`);
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* The dev-dependencies `reticle init` installs — kit (or sensor) first, build plugin next.
|
|
43
|
-
*
|
|
44
|
-
* `uiLibrary` matters because the framework does not always name the renderer. The rule below was
|
|
45
|
-
* already written for Nuxt, correctly, and applied only there: a Vue app on plain Vite is
|
|
46
|
-
* `Framework.VITE` and a SvelteKit app renders Svelte, and both were being handed the React kit
|
|
47
|
-
* right after `init` had detected the real UI library and said so on screen.
|
|
48
|
-
*/
|
|
49
|
-
export function frameworkPackages(framework, uiLibrary = UiLibrary.UNKNOWN) {
|
|
50
|
-
const kit = usesReactKit(uiLibrary, framework) ? RETICLE_REACT_KIT : RETICLE_BROWSER_SDK;
|
|
51
|
-
return FRAMEWORK_ADAPTERS[framework].packages(kit);
|
|
52
|
-
}
|
|
27
|
+
export { frameworkPackages } from './framework-adapter.js';
|
|
53
28
|
/**
|
|
54
29
|
* Where the agent-facing files go, which is not always where the app is.
|
|
55
30
|
*
|
|
@@ -65,8 +40,9 @@ function agentFile(input, relPath) {
|
|
|
65
40
|
}
|
|
66
41
|
const CLAUDE_MCP_TITLE = 'MCP server (Claude, global)';
|
|
67
42
|
function claudeMcpStep(input) {
|
|
68
|
-
if (!input.claudeCli)
|
|
43
|
+
if (!input.claudeCli) {
|
|
69
44
|
return null;
|
|
45
|
+
}
|
|
70
46
|
if (input.mcpExists) {
|
|
71
47
|
return {
|
|
72
48
|
title: CLAUDE_MCP_TITLE,
|
|
@@ -92,7 +68,36 @@ function claudeMcpStep(input) {
|
|
|
92
68
|
* parse is reported with a paste-able block rather than overwritten, and everything else is written.
|
|
93
69
|
*/
|
|
94
70
|
function otherClientSteps(input) {
|
|
95
|
-
return (input.detectedClients ?? []).map((detected) =>
|
|
71
|
+
return (input.detectedClients ?? []).map((detected) => {
|
|
72
|
+
const spec = clientSpec(detected.id);
|
|
73
|
+
return (codexCliStep(spec, detected.configPath, detected.existing, input) ??
|
|
74
|
+
clientStep(spec, detected.configPath, detected.existing));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Codex, registered through its own CLI instead of a printed block (#1238).
|
|
79
|
+
*
|
|
80
|
+
* `mergeClientConfig` answers MANUAL for TOML, since we never edit it blind. `codex mcp add` does
|
|
81
|
+
* the editing, so with the binary on PATH nothing is left for a human. It REPLACES an entry of the
|
|
82
|
+
* same name and exits 0, so this answers only when the config names no `reticle` server at all, in
|
|
83
|
+
* any shape. An entry somebody wrote — a local build, a `url` — falls through to `clientStep`, which
|
|
84
|
+
* leaves it alone (ALREADY) or hands over the block (MANUAL), exactly as before.
|
|
85
|
+
*/
|
|
86
|
+
function codexCliStep(spec, configPath, existing, input) {
|
|
87
|
+
if (McpClient.CODEX !== spec.id || true !== input.codexCli) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (codexNamesOurServer(existing, spec.serversKey)) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const cmd = codexAddCommand();
|
|
94
|
+
return {
|
|
95
|
+
title: `MCP server (${spec.label})`,
|
|
96
|
+
target: configPath,
|
|
97
|
+
status: StepStatus.APPLY,
|
|
98
|
+
detail: `register reticle with ${spec.label} (${cmd.display})`,
|
|
99
|
+
exec: { command: cmd.command, args: cmd.args, fallback: cmd.display },
|
|
100
|
+
};
|
|
96
101
|
}
|
|
97
102
|
/** One client's step: already-correct is left alone, an unparseable file gets a paste-able block. */
|
|
98
103
|
function clientStep(spec, configPath, existing) {
|
|
@@ -131,8 +136,9 @@ function clientStep(spec, configPath, existing) {
|
|
|
131
136
|
* they get the notice instead.
|
|
132
137
|
*/
|
|
133
138
|
function claudeProjectStep(input) {
|
|
134
|
-
if (input.claudeCli || true !== input.insideClaudeCode)
|
|
139
|
+
if (input.claudeCli || true !== input.insideClaudeCode) {
|
|
135
140
|
return null;
|
|
141
|
+
}
|
|
136
142
|
const path = agentFile(input, CLAUDE_PROJECT_CONFIG);
|
|
137
143
|
return clientStep(CLAUDE_PROJECT_SPEC, path, input.claudeProjectConfig ?? null);
|
|
138
144
|
}
|
|
@@ -198,8 +204,9 @@ function mcpSteps(input) {
|
|
|
198
204
|
});
|
|
199
205
|
}
|
|
200
206
|
const windowsNote = windowsMcpNoteStep(input);
|
|
201
|
-
if (windowsNote !== null)
|
|
207
|
+
if (windowsNote !== null) {
|
|
202
208
|
steps.push(windowsNote);
|
|
209
|
+
}
|
|
203
210
|
return steps;
|
|
204
211
|
}
|
|
205
212
|
const SLASH_COMMAND_TITLE = 'The /reticle command';
|
|
@@ -211,8 +218,9 @@ const SLASH_COMMAND_TITLE = 'The /reticle command';
|
|
|
211
218
|
* our signature is left exactly as it is.
|
|
212
219
|
*/
|
|
213
220
|
function commandIsSettled(content) {
|
|
214
|
-
if (null === content || content === undefined)
|
|
221
|
+
if (null === content || content === undefined) {
|
|
215
222
|
return false;
|
|
223
|
+
}
|
|
216
224
|
return content === SLASH_COMMAND_BODY || !content.includes(SLASH_COMMAND_SIGNATURE);
|
|
217
225
|
}
|
|
218
226
|
/**
|
|
@@ -224,8 +232,9 @@ function commandIsSettled(content) {
|
|
|
224
232
|
* whole file is Reticle's, so a stale one is rewritten.
|
|
225
233
|
*/
|
|
226
234
|
function commandStepFor(path, present, content) {
|
|
227
|
-
if (!present)
|
|
235
|
+
if (!present) {
|
|
228
236
|
return null;
|
|
237
|
+
}
|
|
229
238
|
return commandIsSettled(content)
|
|
230
239
|
? {
|
|
231
240
|
title: SLASH_COMMAND_TITLE,
|
|
@@ -268,18 +277,34 @@ const AGENT_RULE_DETAIL = 'teach the agent to verify features with Reticle after
|
|
|
268
277
|
*/
|
|
269
278
|
function ruleWriteDetail(existing, file) {
|
|
270
279
|
const hasContent = existing !== null && existing !== undefined && existing.trim().length > 0;
|
|
271
|
-
if (!hasContent)
|
|
280
|
+
if (!hasContent) {
|
|
272
281
|
return `${AGENT_RULE_DETAIL} — creates ${file}`;
|
|
282
|
+
}
|
|
273
283
|
const added = markedBlock().split('\n').length - 1;
|
|
274
284
|
return (`${AGENT_RULE_DETAIL} — APPENDS ${String(added)} lines to your existing ${file}, ` +
|
|
275
285
|
'inside `reticle:begin`/`reticle:end` markers; delete the marked block to undo');
|
|
276
286
|
}
|
|
277
287
|
const RETICLE_MD_TITLE = 'Full agent rules';
|
|
278
288
|
const RETICLE_MD_DETAIL = 'the reference the always-loaded rule points at (when NOT to verify, recovery, feedback)';
|
|
289
|
+
/**
|
|
290
|
+
* A line that is only an import of AGENTS.md (`@AGENTS.md`, `@./AGENTS.md`). Claude Code reads the
|
|
291
|
+
* imported file as part of CLAUDE.md, so the rule `agentsMdStep` writes there already reaches it, and
|
|
292
|
+
* a second copy in CLAUDE.md would load the rule twice on every turn (#1680).
|
|
293
|
+
*/
|
|
294
|
+
const IMPORTS_AGENTS_MD = /^[ \t]*@(?:\.\/)?AGENTS\.md[ \t]*\r?$/m;
|
|
279
295
|
function claudeRuleStep(input) {
|
|
280
|
-
if (!input.claudeCli)
|
|
296
|
+
if (!input.claudeCli) {
|
|
281
297
|
return null;
|
|
298
|
+
}
|
|
282
299
|
const path = agentFile(input, CLAUDE_MD_PATH);
|
|
300
|
+
if (IMPORTS_AGENTS_MD.test(input.claudeMdContent ?? '')) {
|
|
301
|
+
return {
|
|
302
|
+
title: AGENT_RULE_TITLE,
|
|
303
|
+
target: path,
|
|
304
|
+
status: StepStatus.ALREADY,
|
|
305
|
+
detail: `CLAUDE.md imports ${AGENTS_MD_PATH}, so the Reticle rule written there reaches Claude Code through the import; no second copy`,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
283
308
|
const r = mergeMarkedInstruction(input.claudeMdContent);
|
|
284
309
|
if (r.status === AgentRuleStatus.ALREADY) {
|
|
285
310
|
return {
|
|
@@ -305,10 +330,12 @@ function claudeRuleStep(input) {
|
|
|
305
330
|
*/
|
|
306
331
|
function cursorRuleStep(input) {
|
|
307
332
|
const cursorGlobal = (input.detectedClients ?? []).some((c) => c.id === McpClient.CURSOR);
|
|
308
|
-
if (!cursorGlobal && true !== input.cursorProjectPresent)
|
|
333
|
+
if (!cursorGlobal && true !== input.cursorProjectPresent) {
|
|
309
334
|
return null;
|
|
310
|
-
|
|
335
|
+
}
|
|
336
|
+
if (input.cursorProjectPresent !== true && input.claudeCli) {
|
|
311
337
|
return null;
|
|
338
|
+
}
|
|
312
339
|
// The whole file is Reticle's — init created it — so a stale one is REWRITTEN rather than merged.
|
|
313
340
|
// Comparing content is what makes the rule updatable; comparing existence made it permanent.
|
|
314
341
|
const path = agentFile(input, CURSOR_RULE_PATH);
|
|
@@ -429,85 +456,44 @@ function reticleMdStep(input) {
|
|
|
429
456
|
* noise.
|
|
430
457
|
*/
|
|
431
458
|
function agentRuleSteps(input) {
|
|
432
|
-
if (!input.options.mcp)
|
|
459
|
+
if (!input.options.mcp) {
|
|
433
460
|
return [];
|
|
461
|
+
}
|
|
434
462
|
return [...stepsForAgents(input, (a) => a.ruleStep), agentsMdStep(input), reticleMdStep(input)];
|
|
435
463
|
}
|
|
436
464
|
/**
|
|
437
|
-
*
|
|
465
|
+
* Keep `.reticle/` out of the project's `prettier --check`.
|
|
438
466
|
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
* with nothing naming a version. Pinning turns that into this loud failure, which is the better
|
|
444
|
-
* trade — but only if the message says what to do about it.
|
|
445
|
-
*/
|
|
446
|
-
/**
|
|
447
|
-
* Is this package already declared at the version we would pin?
|
|
448
|
-
*
|
|
449
|
-
* The caret is the point. `npm i -D pkg@3.2.0` WRITES `^3.2.0`, so on every later run the pinned
|
|
450
|
-
* `3.2.0` failed a string comparison against the range npm had just created, and init re-installed
|
|
451
|
-
* for ever. Only the two forms a package manager writes by itself are read - an exact version, and
|
|
452
|
-
* `^`/`~` over that same version.
|
|
467
|
+
* When the project uses Prettier, `init` adds `.reticle/` to the project's `.prettierignore`.
|
|
468
|
+
* The nested `.reticle/.gitignore` does not help — Prettier only reads the ignore file of the
|
|
469
|
+
* directory it runs from — and matching Prettier's exact JSON output is a losing game, so
|
|
470
|
+
* excluding the directory is the root-cause fix (#1560).
|
|
453
471
|
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
472
|
+
* SKIP when the project does not use Prettier. ALREADY when `.reticle/` is already excluded.
|
|
473
|
+
* Otherwise APPLY: create the file, or append the entry to the existing one.
|
|
456
474
|
*/
|
|
457
|
-
function
|
|
458
|
-
if (
|
|
459
|
-
return
|
|
460
|
-
if (pinned === undefined || 0 === pinned.length)
|
|
461
|
-
return true;
|
|
462
|
-
return declared === pinned || `^${pinned}` === declared || `~${pinned}` === declared;
|
|
463
|
-
}
|
|
464
|
-
function installStep(input) {
|
|
465
|
-
const pm = input.detection.packageManager;
|
|
466
|
-
// `Detection.packageManagerCommand` (see `preflight.ts`, #1149). Falling back to `pm` covers every
|
|
467
|
-
// caller that never went through preflight (tests, and any future one).
|
|
468
|
-
const pmCommand = input.detection.packageManagerCommand ?? pm;
|
|
469
|
-
const packages = pinnedPackages(frameworkPackages(input.detection.framework, input.detection.uiLibrary), input.options.sdkVersion);
|
|
470
|
-
const command = installCommand(pm, packages, pmCommand);
|
|
471
|
-
/*
|
|
472
|
-
* A re-run over an already-wired project does NO dependency work.
|
|
473
|
-
*
|
|
474
|
-
* Reported from the field: `init` on an instrumented npm-workspaces project ran a pnpm dependency
|
|
475
|
-
* migration, which moved the existing `node_modules` aside and broke the dev server. Whatever
|
|
476
|
-
* manager gets chosen, the redundant install is the step that touches the tree — so the fix that
|
|
477
|
-
* matters is not running one that nothing needs. `init` is documented as idempotent; this is the
|
|
478
|
-
* step that was not.
|
|
479
|
-
*/
|
|
480
|
-
const declared = input.detection.dependencies ?? {};
|
|
481
|
-
const version = input.options.sdkVersion;
|
|
482
|
-
const names = frameworkPackages(input.detection.framework, input.detection.uiLibrary);
|
|
483
|
-
if (names.length > 0 && names.every((n) => alreadyDeclared(declared[n], version))) {
|
|
484
|
-
return {
|
|
485
|
-
title: 'Install dependencies',
|
|
486
|
-
target: 'package.json',
|
|
487
|
-
status: StepStatus.ALREADY,
|
|
488
|
-
detail: `${names.join(', ')} already declared${version === undefined || 0 === version.length ? '' : ` at ${version}`}`,
|
|
489
|
-
};
|
|
475
|
+
function prettierIgnoreStep(input) {
|
|
476
|
+
if (true !== input.detection.usesPrettier) {
|
|
477
|
+
return null;
|
|
490
478
|
}
|
|
491
|
-
|
|
479
|
+
const existing = input.prettierIgnoreSource ?? null;
|
|
480
|
+
if (null !== existing && ignoresReticle(existing)) {
|
|
492
481
|
return {
|
|
493
|
-
title: '
|
|
494
|
-
target:
|
|
495
|
-
status: StepStatus.
|
|
496
|
-
detail:
|
|
482
|
+
title: 'Prettier ignore',
|
|
483
|
+
target: PRETTIERIGNORE_PATH,
|
|
484
|
+
status: StepStatus.ALREADY,
|
|
485
|
+
detail: '.reticle/ already excluded from Prettier',
|
|
497
486
|
};
|
|
498
487
|
}
|
|
499
|
-
const
|
|
488
|
+
const content = withReticleIgnored(existing);
|
|
500
489
|
return {
|
|
501
|
-
title: '
|
|
502
|
-
target:
|
|
490
|
+
title: 'Prettier ignore',
|
|
491
|
+
target: PRETTIERIGNORE_PATH,
|
|
503
492
|
status: StepStatus.APPLY,
|
|
504
|
-
detail:
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
fallback: `${command}\n\n${installFailureHint(pm, pmCommand)}`,
|
|
509
|
-
},
|
|
510
|
-
retries: installRetries(pm, packages, frameworkPackages(input.detection.framework, input.detection.uiLibrary), input.options.sdkVersion, pmCommand),
|
|
493
|
+
detail: null === existing
|
|
494
|
+
? 'create .prettierignore excluding .reticle/ so `prettier --check` passes'
|
|
495
|
+
: 'add .reticle/ to .prettierignore so `prettier --check` passes',
|
|
496
|
+
write: { path: PRETTIERIGNORE_PATH, content, expect: [RETICLE_IGNORE_ENTRY] },
|
|
511
497
|
};
|
|
512
498
|
}
|
|
513
499
|
const RETICLE_CONFIG_TITLE = 'Reticle config';
|
|
@@ -526,12 +512,15 @@ const AGENT_ROOT_CONFIG_TITLE = 'Reticle config (agent root)';
|
|
|
526
512
|
* and the two files are byte-identical, so neither can disagree with the other about the port or the
|
|
527
513
|
* project identity.
|
|
528
514
|
*/
|
|
529
|
-
function agentRootConfigStep(input, content) {
|
|
515
|
+
function agentRootConfigStep(input, content, renamedFrom) {
|
|
530
516
|
const root = input.agentFileRoot;
|
|
531
|
-
if (root === undefined || 0 === root.length)
|
|
517
|
+
if (root === undefined || 0 === root.length) {
|
|
532
518
|
return [];
|
|
519
|
+
}
|
|
533
520
|
const path = agentFile(input, RETICLE_CONFIG_FILE);
|
|
534
|
-
const
|
|
521
|
+
const declared = projectIdOf(input.agentRootConfigSource);
|
|
522
|
+
// The root's copy of the id `--new-id` just replaced is this checkout's, not another app's.
|
|
523
|
+
const existingProject = declared === renamedFrom ? undefined : declared;
|
|
535
524
|
const wantedProject = projectIdOf(content);
|
|
536
525
|
/**
|
|
537
526
|
* A monorepo has more than one app and the root can only point at one of them.
|
|
@@ -561,6 +550,19 @@ function agentRootConfigStep(input, content) {
|
|
|
561
550
|
},
|
|
562
551
|
];
|
|
563
552
|
}
|
|
553
|
+
// Several apps: a root config names one and is wrong for the rest. Not written; an agent at the
|
|
554
|
+
// root is served whichever app's daemon is live, and one started in an app reads that app's.
|
|
555
|
+
if (true === input.agentRootSeveralApps && existingProject === undefined) {
|
|
556
|
+
return [
|
|
557
|
+
{
|
|
558
|
+
title: AGENT_ROOT_CONFIG_TITLE,
|
|
559
|
+
target: path,
|
|
560
|
+
status: StepStatus.NOTICE,
|
|
561
|
+
detail: 'not written: this workspace has several apps and the root can name only one. Run the ' +
|
|
562
|
+
"agent from an app's directory to pin it to that app.",
|
|
563
|
+
},
|
|
564
|
+
];
|
|
565
|
+
}
|
|
564
566
|
if (input.agentRootConfigSource === content) {
|
|
565
567
|
return [
|
|
566
568
|
{
|
|
@@ -582,7 +584,7 @@ function agentRootConfigStep(input, content) {
|
|
|
582
584
|
},
|
|
583
585
|
];
|
|
584
586
|
}
|
|
585
|
-
function reticleConfigStep(input, content, onDisk, movedFrom) {
|
|
587
|
+
function reticleConfigStep(input, content, onDisk, movedFrom, renamedFrom) {
|
|
586
588
|
if (true === input.reticleConfigExists) {
|
|
587
589
|
const problem = existingConfigProblem(onDisk);
|
|
588
590
|
if (problem !== undefined) {
|
|
@@ -598,15 +600,17 @@ function reticleConfigStep(input, content, onDisk, movedFrom) {
|
|
|
598
600
|
// The one thing a re-run can still learn: which channel the user actually arrived through.
|
|
599
601
|
// See configWithInstallSource — it only ever ADDS a field that is absent.
|
|
600
602
|
const backfilled = configWithInstallSource(onDisk, input.installSource);
|
|
601
|
-
if (backfilled !== undefined || movedFrom !== undefined) {
|
|
603
|
+
if (backfilled !== undefined || movedFrom !== undefined || renamedFrom !== undefined) {
|
|
602
604
|
const port = input.options.port;
|
|
603
605
|
return {
|
|
604
606
|
title: RETICLE_CONFIG_TITLE,
|
|
605
607
|
target: RETICLE_CONFIG_FILE,
|
|
606
608
|
status: StepStatus.APPLY,
|
|
607
|
-
detail:
|
|
608
|
-
?
|
|
609
|
-
:
|
|
609
|
+
detail: renamedFrom !== undefined
|
|
610
|
+
? `this checkout's own project id: ${String(input.options.projectId)} (was ${renamedFrom}, the id it was copied with)`
|
|
611
|
+
: movedFrom !== undefined && port !== undefined
|
|
612
|
+
? portMoveDetail(movedFrom, port)
|
|
613
|
+
: 'record which install route this project came through',
|
|
610
614
|
write: { path: RETICLE_CONFIG_FILE, content: backfilled ?? onDisk ?? content },
|
|
611
615
|
};
|
|
612
616
|
}
|
|
@@ -634,11 +638,27 @@ function reticleConfigStep(input, content, onDisk, movedFrom) {
|
|
|
634
638
|
* requested port, and a config still naming the old one is half of a split brain (see
|
|
635
639
|
* port-steps.ts). Only the port moves; the identity stays the one already on disk.
|
|
636
640
|
*/
|
|
641
|
+
/**
|
|
642
|
+
* `--new-id`: the config with `projectId` replaced by the one derived for THIS checkout, or null when
|
|
643
|
+
* there is nothing to replace. A copied repository keeps the original's `.reticle.json`, and init
|
|
644
|
+
* otherwise keeps a config on disk as it is, so nothing else gives the copy an id of its own.
|
|
645
|
+
*/
|
|
646
|
+
function withOwnProjectId(source, options) {
|
|
647
|
+
const id = options.projectId;
|
|
648
|
+
const from = projectIdOf(source);
|
|
649
|
+
if (true !== options.newId || id === undefined || from === undefined || from === id) {
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
const fields = JSON.parse(source);
|
|
653
|
+
return { code: `${JSON.stringify({ ...fields, projectId: id }, null, 2)}\n`, from };
|
|
654
|
+
}
|
|
637
655
|
function reticleConfigSteps(input) {
|
|
638
656
|
const existing = input.reticleConfigSource;
|
|
639
|
-
const
|
|
657
|
+
const copied = true === input.reticleConfigExists && null !== existing && existing !== undefined
|
|
640
658
|
? existing
|
|
641
659
|
: undefined;
|
|
660
|
+
const renamed = copied === undefined ? null : withOwnProjectId(copied, input.options);
|
|
661
|
+
const onDisk = renamed?.code ?? copied;
|
|
642
662
|
const moved = onDisk === undefined ? null : retargetConfigPort(onDisk, input.options.port);
|
|
643
663
|
const content = moved?.code ??
|
|
644
664
|
onDisk ??
|
|
@@ -647,29 +667,64 @@ function reticleConfigSteps(input) {
|
|
|
647
667
|
// config written before this field existed, and the two mean different things.
|
|
648
668
|
input.installSource);
|
|
649
669
|
return [
|
|
650
|
-
reticleConfigStep(input, content, moved?.code ??
|
|
651
|
-
...agentRootConfigStep(input, content),
|
|
670
|
+
reticleConfigStep(input, content, moved?.code ?? onDisk, moved?.from, renamed?.from),
|
|
671
|
+
...agentRootConfigStep(input, content, renamed?.from),
|
|
672
|
+
...(null === renamed ? [] : copiedIdSteps(input, renamed.from)),
|
|
652
673
|
];
|
|
653
674
|
}
|
|
675
|
+
/**
|
|
676
|
+
* `--new-id`: the old id where the app quotes it by hand, as in `connect({ projectId: '…' })`. Left
|
|
677
|
+
* there, the copy's evidence goes on into the original checkout. Only the whole quoted literal moves:
|
|
678
|
+
* the id is a hash-suffixed name nothing else spells.
|
|
679
|
+
*/
|
|
680
|
+
function copiedIdSteps(input, from) {
|
|
681
|
+
const id = input.options.projectId;
|
|
682
|
+
if (id === undefined) {
|
|
683
|
+
return [];
|
|
684
|
+
}
|
|
685
|
+
return (input.copiedIdSources ?? []).map(({ path, source }) => ({
|
|
686
|
+
title: RETICLE_CONFIG_TITLE,
|
|
687
|
+
target: path,
|
|
688
|
+
status: StepStatus.APPLY,
|
|
689
|
+
detail: `the project id the app names by hand: ${id} (was ${from})`,
|
|
690
|
+
write: {
|
|
691
|
+
path,
|
|
692
|
+
content: ['"', "'", '`'].reduce((code, q) => code.replaceAll(`${q}${from}${q}`, `${q}${id}${q}`), source),
|
|
693
|
+
expect: [id],
|
|
694
|
+
},
|
|
695
|
+
}));
|
|
696
|
+
}
|
|
654
697
|
export function buildPlan(input) {
|
|
698
|
+
const cspSteps = cspStep(input);
|
|
699
|
+
const prettierIgnore = prettierIgnoreStep(input);
|
|
655
700
|
const steps = [
|
|
656
|
-
...
|
|
701
|
+
...cspSteps,
|
|
657
702
|
...mcpSteps(input),
|
|
658
703
|
...agentRuleSteps(input),
|
|
659
704
|
...slashCommandSteps(input),
|
|
660
|
-
...(
|
|
661
|
-
|
|
662
|
-
:
|
|
705
|
+
...claudeSettingsSteps(agentFile(input, CLAUDE_SETTINGS_PATH), input.claudeSettingsContent, {
|
|
706
|
+
stop: true === input.hooks,
|
|
707
|
+
// Beside the CLAUDE.md rule: the same agent, the same consent to write its instruction files.
|
|
708
|
+
compact: input.options.mcp && input.claudeCli,
|
|
709
|
+
prompt: true === input.claudePromptHook,
|
|
710
|
+
bin: input.hookBin,
|
|
711
|
+
}),
|
|
712
|
+
...promptHookSteps(input.promptHooks ?? [], true === input.claudePromptHook, input.hookBin),
|
|
663
713
|
...uiLibraryStep(input),
|
|
664
714
|
...webGlCanvasStep(input),
|
|
665
715
|
installStep(input),
|
|
666
716
|
...reticleConfigSteps(input),
|
|
717
|
+
...(prettierIgnore !== null ? [prettierIgnore] : []),
|
|
667
718
|
];
|
|
668
|
-
steps.
|
|
719
|
+
// Both steps can patch index.html: the connect write must keep the CSP edit that runs first.
|
|
720
|
+
const patchedIndex = cspSteps.find((step) => step.write?.path === HTML_INDEX_PATH)?.write
|
|
721
|
+
?.content;
|
|
722
|
+
steps.push(...frameworkSteps(patchedIndex === undefined ? input : { ...input, htmlIndexSource: patchedIndex }));
|
|
669
723
|
// LAST, and a notice rather than an action: it is a statement about this machine's shape, and it
|
|
670
724
|
// only matters once every file above has been written. See containerised-dev-server.ts.
|
|
671
725
|
const container = containerisedStep(input);
|
|
672
|
-
if (container !== null)
|
|
726
|
+
if (container !== null) {
|
|
673
727
|
steps.push(container);
|
|
728
|
+
}
|
|
674
729
|
return { framework: input.detection.framework, uiLibrary: input.detection.uiLibrary, steps };
|
|
675
730
|
}
|
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
import { type Step } from './plan-types.js';
|
|
2
|
+
import { type PromptHookTarget } from '../register/prompt-hooks.js';
|
|
2
3
|
export declare const CLAUDE_SETTINGS_PATH = ".claude/settings.json";
|
|
3
4
|
export declare const STOP_HOOK_COMMAND = "npx @reticlehq/server report --hook";
|
|
4
5
|
export declare function stopHookStep(path: string, existing: string | null | undefined): Step;
|
|
6
|
+
/**
|
|
7
|
+
* The Claude Code `SessionStart` hook with matcher `compact`: right after a compaction it prints
|
|
8
|
+
* `reticle context --compact` — what this session proved, what failed, what is still owed — back
|
|
9
|
+
* into the agent's fresh context. Reticle holds the memory; this is how it is handed back without
|
|
10
|
+
* the agent having to remember to ask.
|
|
11
|
+
*
|
|
12
|
+
* Written by default wherever the CLAUDE.md rule is (it only reads the project's own `.reticle/`, and
|
|
13
|
+
* prints nothing where there is none). Removable like any hook: delete the entry.
|
|
14
|
+
*/
|
|
15
|
+
export declare const COMPACT_HOOK_COMMAND = "npx @reticlehq/server context --compact";
|
|
16
|
+
export declare function compactHookStep(path: string, existing: string | null | undefined): Step;
|
|
17
|
+
/**
|
|
18
|
+
* Every hook this run writes into `.claude/settings.json`, each merged on top of the one before, so
|
|
19
|
+
* two steps writing the same file cannot undo each other.
|
|
20
|
+
*/
|
|
21
|
+
export declare function claudeSettingsSteps(path: string, existing: string | null | undefined, options: {
|
|
22
|
+
stop: boolean;
|
|
23
|
+
compact: boolean;
|
|
24
|
+
prompt?: boolean;
|
|
25
|
+
bin?: string | undefined;
|
|
26
|
+
}): Step[];
|
|
27
|
+
/**
|
|
28
|
+
* The consent line. Names all that the agent hooks keep, what leaves with a linked run and under
|
|
29
|
+
* which setting, and the one switch that stops all of it.
|
|
30
|
+
*/
|
|
31
|
+
export declare const PROMPT_CAPTURE_NOTICE: string;
|
|
32
|
+
export declare function promptHookStep(target: PromptHookTarget): Step;
|
|
33
|
+
/** The steps for every detected agent, then the one consent line when any hook is in place. */
|
|
34
|
+
export declare function promptHookSteps(targets: readonly PromptHookTarget[], claude: boolean, bin?: string): Step[];
|