@dalzoubi/dev-agents-sync 1.0.26 → 2.0.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/README.md +14 -0
- package/eslint.config.mjs +9 -0
- package/package.json +1 -1
- package/src/claudeMd.mjs +220 -0
- package/src/commands/check.mjs +66 -1
- package/src/commands/init.mjs +26 -0
- package/src/commands/update.mjs +24 -0
- package/tests/checkClaudeMd.test.mjs +656 -0
- package/tests/claudeMd.test.mjs +910 -0
- package/tests/e2e/claudeMd-injection.test.mjs +846 -0
- package/tests/e2e/readme-coverage.test.mjs +183 -0
package/README.md
CHANGED
|
@@ -41,18 +41,32 @@ npx --yes @dalzoubi/dev-agents-sync@1 init --targets claude
|
|
|
41
41
|
This writes managed files into `.claude/` and creates `.dev-agents-sync.json` with the resolved content version.
|
|
42
42
|
Use `--targets cursor` or `--targets claude,cursor` to install Cursor project agents, rules, and slash commands into `.cursor/agents/`, `.cursor/rules/`, and `.cursor/commands/`.
|
|
43
43
|
|
|
44
|
+
As part of `init`, the CLI injects an auto-routing fenced block into `CLAUDE.md` (creating the file if it does not exist). The block tells Claude Code how to route tasks to the correct agent. It is delimited by:
|
|
45
|
+
|
|
46
|
+
```html
|
|
47
|
+
<!-- dev-agents:auto-route:start -->
|
|
48
|
+
...auto-generated routing instructions...
|
|
49
|
+
<!-- dev-agents:auto-route:end -->
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Do not hand-edit content inside those markers — it will be overwritten on the next `update`.
|
|
53
|
+
|
|
44
54
|
Update to the latest matching content version:
|
|
45
55
|
|
|
46
56
|
```bash
|
|
47
57
|
npx --yes @dalzoubi/dev-agents-sync@1 update
|
|
48
58
|
```
|
|
49
59
|
|
|
60
|
+
`update` refreshes managed agent files **and** replaces the `CLAUDE.md` auto-routing block if it is stale, so routing instructions always match the installed content version.
|
|
61
|
+
|
|
50
62
|
Check whether managed files are in sync:
|
|
51
63
|
|
|
52
64
|
```bash
|
|
53
65
|
npx --yes @dalzoubi/dev-agents-sync@1 check
|
|
54
66
|
```
|
|
55
67
|
|
|
68
|
+
`check` verifies that managed agent files and the `CLAUDE.md` auto-routing block are both up to date. If the routing block is absent or stale, `check` exits with code 1 and names `CLAUDE.md` in the output. This makes it safe to use in CI to detect drift.
|
|
69
|
+
|
|
56
70
|
Show the expected diff without writing files:
|
|
57
71
|
|
|
58
72
|
```bash
|
package/package.json
CHANGED
package/src/claudeMd.mjs
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claudeMd.mjs
|
|
3
|
+
*
|
|
4
|
+
* Helper for injecting and updating the auto-route fenced block in a
|
|
5
|
+
* consumer's CLAUDE.md file.
|
|
6
|
+
*
|
|
7
|
+
* @internal — not part of the public package API
|
|
8
|
+
*
|
|
9
|
+
* Fenced block format:
|
|
10
|
+
* <!-- dev-agents:auto-route:start managed-by: dev-agents-sync vX.Y.Z -->
|
|
11
|
+
* ...routing rules content...
|
|
12
|
+
* <!-- dev-agents:auto-route:end -->
|
|
13
|
+
*
|
|
14
|
+
* Design notes:
|
|
15
|
+
* - The version in the start marker is the CLI package version, not the
|
|
16
|
+
* resolved content version. This lets consumers detect that their routing
|
|
17
|
+
* rules were written by a specific CLI release.
|
|
18
|
+
* - We never clobber a malformed block (start present, end missing). Doing so
|
|
19
|
+
* would silently destroy user content in the region below the orphaned
|
|
20
|
+
* start marker. Warn and skip instead.
|
|
21
|
+
* - Deterministic output: same inputs → same file content (§9 CONSTITUTION).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Exported constants — load-bearing: tests and callers grep for these exact
|
|
29
|
+
// literal strings, so do not change them without a migration story.
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export const START_MARKER_PREFIX = '<!-- dev-agents:auto-route:start managed-by: dev-agents-sync v';
|
|
33
|
+
export const END_MARKER = '<!-- dev-agents:auto-route:end -->';
|
|
34
|
+
|
|
35
|
+
// Regex that matches the full start marker and captures the version.
|
|
36
|
+
// Must stay in sync with START_MARKER_PREFIX above.
|
|
37
|
+
const START_MARKER_RE = /<!--\s*dev-agents:auto-route:start\s+managed-by:\s*dev-agents-sync\s+v([^\s]+)\s*-->/;
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// buildFencedBlock
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Builds the full fenced block string.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} version CLI package version (e.g. "1.0.25")
|
|
47
|
+
* @param {string} content Routing rules content to embed between the markers
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
export function buildFencedBlock(version, content) {
|
|
51
|
+
return [
|
|
52
|
+
`${START_MARKER_PREFIX}${version} -->`,
|
|
53
|
+
content,
|
|
54
|
+
END_MARKER,
|
|
55
|
+
].join('\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// parseExistingBlock
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Scans `fileContent` for an existing auto-route fenced block.
|
|
64
|
+
*
|
|
65
|
+
* Returns:
|
|
66
|
+
* null — no block present (safe to append)
|
|
67
|
+
* { version, block } — well-formed block found; `block` is the
|
|
68
|
+
* full text from start marker to end marker
|
|
69
|
+
* (inclusive, with the newline between them)
|
|
70
|
+
* { malformed: true } — start marker found but end marker is absent
|
|
71
|
+
*
|
|
72
|
+
* @param {string} fileContent
|
|
73
|
+
* @returns {null | { version: string, block: string } | { malformed: true }}
|
|
74
|
+
*/
|
|
75
|
+
export function parseExistingBlock(fileContent) {
|
|
76
|
+
if (!fileContent) return null;
|
|
77
|
+
|
|
78
|
+
const startMatch = fileContent.match(START_MARKER_RE);
|
|
79
|
+
if (!startMatch) return null;
|
|
80
|
+
|
|
81
|
+
const startIndex = fileContent.indexOf(startMatch[0]);
|
|
82
|
+
const version = startMatch[1];
|
|
83
|
+
|
|
84
|
+
const endIndex = fileContent.indexOf(END_MARKER, startIndex + startMatch[0].length);
|
|
85
|
+
if (endIndex === -1) {
|
|
86
|
+
return { malformed: true };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Include the full block from start marker through end marker (no trailing newline).
|
|
90
|
+
const block = fileContent.slice(startIndex, endIndex + END_MARKER.length);
|
|
91
|
+
return { version, block };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// injectClaudeMd
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Injects or replaces the auto-route fenced block in a CLAUDE.md file.
|
|
100
|
+
*
|
|
101
|
+
* Behaviour:
|
|
102
|
+
* - File does not exist → create it with just the fenced block
|
|
103
|
+
* - File exists, no block → append the fenced block (preserving existing prose)
|
|
104
|
+
* - File exists, well-formed block, same version+content → skip silently
|
|
105
|
+
* - File exists, well-formed block, stale → replace in-place; prose untouched
|
|
106
|
+
* - File exists, malformed block (start, no end) → warn stderr, skip
|
|
107
|
+
* - dryRun: true → return plan, write nothing
|
|
108
|
+
*
|
|
109
|
+
* @param {string} absPath Absolute path to CLAUDE.md
|
|
110
|
+
* @param {string} version CLI package version string
|
|
111
|
+
* @param {string} content Routing rules content to embed
|
|
112
|
+
* @param {{ dryRun?: boolean }} [opts]
|
|
113
|
+
* @returns {{ skipped?: boolean, malformed?: boolean, dryRun?: boolean,
|
|
114
|
+
* action?: string, plannedContent?: string }}
|
|
115
|
+
*/
|
|
116
|
+
export function injectClaudeMd(absPath, version, content, opts = {}) {
|
|
117
|
+
const { dryRun = false } = opts;
|
|
118
|
+
|
|
119
|
+
const newBlock = buildFencedBlock(version, content);
|
|
120
|
+
|
|
121
|
+
// -------------------------------------------------------------------------
|
|
122
|
+
// Determine current state
|
|
123
|
+
// -------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
let existingContent = null;
|
|
126
|
+
if (existsSync(absPath)) {
|
|
127
|
+
existingContent = readFileSync(absPath, 'utf8');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Empty string is treated the same as absent — nothing to preserve.
|
|
131
|
+
const hasExistingContent = existingContent !== null && existingContent.length > 0;
|
|
132
|
+
|
|
133
|
+
let parsed = null;
|
|
134
|
+
if (hasExistingContent) {
|
|
135
|
+
parsed = parseExistingBlock(existingContent);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// -------------------------------------------------------------------------
|
|
139
|
+
// Malformed block — warn and skip
|
|
140
|
+
// -------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
if (parsed && parsed.malformed) {
|
|
143
|
+
process.stderr.write(
|
|
144
|
+
`warn: dev-agents-sync: CLAUDE.md has a malformed auto-route block ` +
|
|
145
|
+
`(start marker present but end marker is missing). ` +
|
|
146
|
+
`Skipping injection to avoid corrupting the file. ` +
|
|
147
|
+
`Remove or repair the block manually and re-run.\n`,
|
|
148
|
+
);
|
|
149
|
+
return { skipped: true, malformed: true };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// -------------------------------------------------------------------------
|
|
153
|
+
// Skip when current — version AND content are identical
|
|
154
|
+
// -------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
if (parsed && parsed.version === version && parsed.block === newBlock) {
|
|
157
|
+
return { skipped: true };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// -------------------------------------------------------------------------
|
|
161
|
+
// Dry run — compute planned content but do not write
|
|
162
|
+
// -------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
if (dryRun) {
|
|
165
|
+
const plannedContent = computeNewContent(existingContent, parsed, newBlock);
|
|
166
|
+
return { dryRun: true, action: parsed ? 'replace' : 'inject', plannedContent };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// -------------------------------------------------------------------------
|
|
170
|
+
// Write
|
|
171
|
+
// -------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
const newContent = computeNewContent(existingContent, parsed, newBlock);
|
|
174
|
+
|
|
175
|
+
mkdirSync(path.dirname(absPath), { recursive: true });
|
|
176
|
+
try {
|
|
177
|
+
writeFileSync(absPath, newContent, 'utf8');
|
|
178
|
+
} catch (err) {
|
|
179
|
+
const wrapped = new Error('Cannot write to CLAUDE.md — check file permissions.');
|
|
180
|
+
wrapped.cause = err;
|
|
181
|
+
wrapped.exitCode = 2;
|
|
182
|
+
throw wrapped;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { action: parsed ? 'replace' : 'inject' };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Internal helper
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Computes the new full file content by either:
|
|
194
|
+
* - Replacing the existing block in-place (preserving surrounding prose), or
|
|
195
|
+
* - Appending the block to whatever content exists (possibly empty/absent)
|
|
196
|
+
*
|
|
197
|
+
* @param {string | null} existingContent Current file content, or null if absent
|
|
198
|
+
* @param {{ block: string } | null} parsed parseExistingBlock result (non-malformed)
|
|
199
|
+
* @param {string} newBlock The fully-formed fenced block to inject
|
|
200
|
+
* @returns {string}
|
|
201
|
+
*/
|
|
202
|
+
function computeNewContent(existingContent, parsed, newBlock) {
|
|
203
|
+
// Case 1: file absent or empty — write just the fenced block
|
|
204
|
+
if (!existingContent || existingContent.length === 0) {
|
|
205
|
+
return newBlock + '\n';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Case 2: existing block to replace in-place
|
|
209
|
+
if (parsed && parsed.block) {
|
|
210
|
+
// Use the function form of replace to avoid $-sequence corruption
|
|
211
|
+
// (e.g. `$$`, `$&`, `$1` in newBlock would otherwise be misinterpreted
|
|
212
|
+
// as replacement patterns by the String.prototype.replace algorithm).
|
|
213
|
+
return existingContent.replace(parsed.block, () => newBlock);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Case 3: file has prose but no block — append
|
|
217
|
+
// Ensure a blank line separates the existing prose from the fenced block.
|
|
218
|
+
const trimmed = existingContent.trimEnd();
|
|
219
|
+
return trimmed + '\n\n' + newBlock + '\n';
|
|
220
|
+
}
|
package/src/commands/check.mjs
CHANGED
|
@@ -7,9 +7,15 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
10
12
|
|
|
11
13
|
import { readLockfile } from '../lockfile.mjs';
|
|
12
14
|
import { resolveConsumerPath, normalizeFileMap } from '../writer.mjs';
|
|
15
|
+
import { parseExistingBlock, buildFencedBlock } from '../claudeMd.mjs';
|
|
16
|
+
|
|
17
|
+
const _require = createRequire(import.meta.url);
|
|
18
|
+
const CLI_VERSION = _require('../../package.json').version;
|
|
13
19
|
|
|
14
20
|
function filterFileMapByTargets(fileMap, targets) {
|
|
15
21
|
const out = {};
|
|
@@ -46,7 +52,8 @@ export async function runCheck(consumerCwd, opts = {}) {
|
|
|
46
52
|
throw wrapped;
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
const
|
|
55
|
+
const normalized = normalizeFileMap(fileMap);
|
|
56
|
+
const scoped = filterFileMapByTargets(normalized, lock.targets);
|
|
50
57
|
|
|
51
58
|
const drifted = [];
|
|
52
59
|
|
|
@@ -65,6 +72,64 @@ export async function runCheck(consumerCwd, opts = {}) {
|
|
|
65
72
|
}
|
|
66
73
|
}
|
|
67
74
|
|
|
75
|
+
// -------------------------------------------------------------------------
|
|
76
|
+
// CLAUDE.md fenced block drift check
|
|
77
|
+
//
|
|
78
|
+
// Only applies when the `claude` target is active. We look for the routing
|
|
79
|
+
// content at `claude/skills/auto-route.md` in the fetched file map and
|
|
80
|
+
// compare the consumer's CLAUDE.md fenced block against the expected block
|
|
81
|
+
// built from the current CLI version + that routing content.
|
|
82
|
+
//
|
|
83
|
+
// All three problem states are treated as drift (not tooling error):
|
|
84
|
+
// - CLAUDE.md absent
|
|
85
|
+
// - fenced block absent (CLAUDE.md has prose but no block)
|
|
86
|
+
// - fenced block malformed (start marker, no end marker)
|
|
87
|
+
// - fenced block stale (version or content differs from expected)
|
|
88
|
+
// -------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
const claudeTargetActive = lock.targets.includes('claude');
|
|
91
|
+
if (claudeTargetActive) {
|
|
92
|
+
const routingContent = normalized['claude/skills/auto-route.md'] ?? null;
|
|
93
|
+
|
|
94
|
+
if (routingContent === null) {
|
|
95
|
+
// Skip is intentional when the remote skill is absent — the consumer may
|
|
96
|
+
// be pinned to a version that predates the auto-route skill file. This
|
|
97
|
+
// is not drift (no expected block to compare against), but it should not
|
|
98
|
+
// be silent either, so we emit a structured warning.
|
|
99
|
+
process.stderr.write(
|
|
100
|
+
'warn: CLAUDE.md drift check skipped — claude/skills/auto-route.md not present in fetched content\n',
|
|
101
|
+
);
|
|
102
|
+
} else {
|
|
103
|
+
const claudeMdPath = path.join(consumerCwd, 'CLAUDE.md');
|
|
104
|
+
const expectedBlock = buildFencedBlock(CLI_VERSION, routingContent);
|
|
105
|
+
|
|
106
|
+
let isDrifted = false;
|
|
107
|
+
if (!existsSync(claudeMdPath)) {
|
|
108
|
+
// Absence is drift — CLAUDE.md should always carry the routing block
|
|
109
|
+
// after init/update has run.
|
|
110
|
+
isDrifted = true;
|
|
111
|
+
} else {
|
|
112
|
+
const claudeMdContent = readFileSync(claudeMdPath, 'utf8');
|
|
113
|
+
const parsed = parseExistingBlock(claudeMdContent);
|
|
114
|
+
|
|
115
|
+
if (parsed === null) {
|
|
116
|
+
// No block present at all.
|
|
117
|
+
isDrifted = true;
|
|
118
|
+
} else if (parsed.malformed) {
|
|
119
|
+
// Start marker with no end marker — treat as drift, not a crash.
|
|
120
|
+
isDrifted = true;
|
|
121
|
+
} else if (parsed.block !== expectedBlock) {
|
|
122
|
+
// Block present but version or content differs.
|
|
123
|
+
isDrifted = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (isDrifted) {
|
|
128
|
+
drifted.push('CLAUDE.md');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
68
133
|
if (drifted.length === 0) {
|
|
69
134
|
return { inSync: true, resolvedVersion: lock.resolvedVersion };
|
|
70
135
|
}
|
package/src/commands/init.mjs
CHANGED
|
@@ -8,11 +8,17 @@
|
|
|
8
8
|
|
|
9
9
|
import { existsSync, readFileSync } from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
11
13
|
|
|
12
14
|
import { writeLockfile, DEFAULT_SOURCE } from '../lockfile.mjs';
|
|
13
15
|
import { resolveRange } from '../range.mjs';
|
|
14
16
|
import { resolveConsumerPath, writeManagedFile, normalizeFileMap } from '../writer.mjs';
|
|
15
17
|
import { hasMarker, hasMarkerOnFirstLine } from '../marker.mjs';
|
|
18
|
+
import { injectClaudeMd } from '../claudeMd.mjs';
|
|
19
|
+
|
|
20
|
+
const _require = createRequire(import.meta.url);
|
|
21
|
+
const CLI_VERSION = _require('../../package.json').version;
|
|
16
22
|
|
|
17
23
|
const DEFAULT_RANGE = '^1';
|
|
18
24
|
const ALL_TARGETS = ['claude', 'cursor'];
|
|
@@ -114,13 +120,28 @@ export async function runInit(consumerCwd, opts = {}) {
|
|
|
114
120
|
plannedWrites.push({ relKey, absPath, content, isNew });
|
|
115
121
|
}
|
|
116
122
|
|
|
123
|
+
// Extract auto-route content for CLAUDE.md injection (claude target only).
|
|
124
|
+
// We read from the normalised map so the content is available regardless of
|
|
125
|
+
// whether the `skills` dir ends up in the consumer's scoped file set.
|
|
126
|
+
const claudeTargetActive = resolvedTargets.includes('claude');
|
|
127
|
+
const routingContent = claudeTargetActive
|
|
128
|
+
? (normalized['claude/skills/auto-route.md'] ?? null)
|
|
129
|
+
: null;
|
|
130
|
+
const claudeMdPath = path.join(consumerCwd, 'CLAUDE.md');
|
|
131
|
+
|
|
117
132
|
if (dryRun) {
|
|
133
|
+
// Run inject in dry-run mode to compute the planned change.
|
|
134
|
+
const claudeMdResult = routingContent
|
|
135
|
+
? injectClaudeMd(claudeMdPath, CLI_VERSION, routingContent, { dryRun: true })
|
|
136
|
+
: null;
|
|
137
|
+
|
|
118
138
|
return {
|
|
119
139
|
dryRun: true,
|
|
120
140
|
resolvedVersion,
|
|
121
141
|
targets: resolvedTargets,
|
|
122
142
|
plannedWrites: plannedWrites.map(({ relKey, absPath }) => ({ relKey, absPath })),
|
|
123
143
|
files: plannedWrites.map(({ relKey }) => relKey),
|
|
144
|
+
claudeMd: claudeMdResult,
|
|
124
145
|
};
|
|
125
146
|
}
|
|
126
147
|
|
|
@@ -153,6 +174,11 @@ export async function runInit(consumerCwd, opts = {}) {
|
|
|
153
174
|
}
|
|
154
175
|
}
|
|
155
176
|
|
|
177
|
+
// Inject or update the auto-route fenced block in CLAUDE.md.
|
|
178
|
+
if (routingContent) {
|
|
179
|
+
injectClaudeMd(claudeMdPath, CLI_VERSION, routingContent);
|
|
180
|
+
}
|
|
181
|
+
|
|
156
182
|
writeLockfile(consumerCwd, {
|
|
157
183
|
source,
|
|
158
184
|
range,
|
package/src/commands/update.mjs
CHANGED
|
@@ -6,11 +6,17 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
9
11
|
|
|
10
12
|
import { readLockfile, writeLockfile } from '../lockfile.mjs';
|
|
11
13
|
import { resolveRange } from '../range.mjs';
|
|
12
14
|
import { resolveConsumerPath, writeManagedFile, normalizeFileMap } from '../writer.mjs';
|
|
13
15
|
import { hasMarker, hasMarkerOnFirstLine } from '../marker.mjs';
|
|
16
|
+
import { injectClaudeMd } from '../claudeMd.mjs';
|
|
17
|
+
|
|
18
|
+
const _require = createRequire(import.meta.url);
|
|
19
|
+
const CLI_VERSION = _require('../../package.json').version;
|
|
14
20
|
|
|
15
21
|
function filterFileMapByTargets(fileMap, targets) {
|
|
16
22
|
const out = {};
|
|
@@ -61,12 +67,25 @@ export async function runUpdate(consumerCwd, opts = {}) {
|
|
|
61
67
|
plannedWrites.push({ relKey, absPath, content, isNew });
|
|
62
68
|
}
|
|
63
69
|
|
|
70
|
+
// Auto-route content for CLAUDE.md injection (claude target only).
|
|
71
|
+
const claudeTargetActive = lock.targets.includes('claude');
|
|
72
|
+
const routingContent = claudeTargetActive
|
|
73
|
+
? (normalized['claude/skills/auto-route.md'] ?? null)
|
|
74
|
+
: null;
|
|
75
|
+
const claudeMdPath = path.join(consumerCwd, 'CLAUDE.md');
|
|
76
|
+
|
|
64
77
|
if (dryRun) {
|
|
78
|
+
// Compute the planned CLAUDE.md change so callers can inspect it without
|
|
79
|
+
// the result shape silently diverging from the init --dry-run contract.
|
|
80
|
+
const claudeMdResult = routingContent
|
|
81
|
+
? injectClaudeMd(claudeMdPath, CLI_VERSION, routingContent, { dryRun: true })
|
|
82
|
+
: null;
|
|
65
83
|
return {
|
|
66
84
|
dryRun: true,
|
|
67
85
|
resolvedVersion: targetVersion,
|
|
68
86
|
previousVersion: lock.resolvedVersion,
|
|
69
87
|
files: plannedWrites.map(({ relKey }) => relKey),
|
|
88
|
+
claudeMd: claudeMdResult,
|
|
70
89
|
};
|
|
71
90
|
}
|
|
72
91
|
|
|
@@ -98,6 +117,11 @@ export async function runUpdate(consumerCwd, opts = {}) {
|
|
|
98
117
|
}
|
|
99
118
|
}
|
|
100
119
|
|
|
120
|
+
// Inject or update the auto-route fenced block in CLAUDE.md.
|
|
121
|
+
if (routingContent) {
|
|
122
|
+
injectClaudeMd(claudeMdPath, CLI_VERSION, routingContent);
|
|
123
|
+
}
|
|
124
|
+
|
|
101
125
|
writeLockfile(consumerCwd, {
|
|
102
126
|
source: lock.source,
|
|
103
127
|
range: lock.range,
|