@open-agent-toolkit/cli 0.0.30 → 0.0.32
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 +5 -0
- package/assets/docs/cli-utilities/config-and-local-state.md +12 -4
- package/assets/docs/provider-sync/commands.md +13 -6
- package/assets/docs/provider-sync/index.md +2 -0
- package/assets/docs/provider-sync/instruction-sync.md +146 -0
- package/assets/docs/provider-sync/scope-and-surface.md +2 -2
- package/assets/docs/reference/troubleshooting.md +10 -5
- package/assets/public-package-versions.json +4 -4
- package/dist/app/create-program.d.ts.map +1 -1
- package/dist/app/create-program.js +2 -2
- package/dist/commands/docs/init/scaffold.d.ts.map +1 -1
- package/dist/commands/docs/init/scaffold.js +12 -11
- package/dist/commands/instructions/instructions.types.d.ts +17 -4
- package/dist/commands/instructions/instructions.types.d.ts.map +1 -1
- package/dist/commands/instructions/instructions.types.js +5 -1
- package/dist/commands/instructions/instructions.utils.d.ts +4 -2
- package/dist/commands/instructions/instructions.utils.d.ts.map +1 -1
- package/dist/commands/instructions/instructions.utils.js +238 -25
- package/dist/commands/instructions/sync/sync.d.ts +3 -1
- package/dist/commands/instructions/sync/sync.d.ts.map +1 -1
- package/dist/commands/instructions/sync/sync.js +176 -16
- package/dist/commands/instructions/validate/validate.d.ts +1 -1
- package/dist/commands/instructions/validate/validate.d.ts.map +1 -1
- package/dist/commands/instructions/validate/validate.js +16 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -4
- package/dist/manifest/manager.d.ts.map +1 -1
- package/dist/manifest/manager.js +1 -1
- package/dist/shared/oat-version.d.ts +2 -0
- package/dist/shared/oat-version.d.ts.map +1 -0
- package/dist/shared/oat-version.js +3 -0
- package/package.json +2 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
|
-
import { dirname, join, relative } from 'node:path';
|
|
1
|
+
import { lstat, readdir, readFile, readlink, realpath, stat, } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
3
3
|
export const EXPECTED_CLAUDE_CONTENT = '@AGENTS.md\n';
|
|
4
|
+
export const DEFAULT_INSTRUCTION_SYNC_STRATEGY = 'pointer';
|
|
4
5
|
const ROOT_EXCLUDED_DIRECTORIES = new Set(['.git', '.oat', '.worktrees']);
|
|
5
6
|
const GLOBAL_EXCLUDED_DIRECTORIES = new Set(['node_modules']);
|
|
6
7
|
function getErrorCode(error) {
|
|
@@ -11,12 +12,37 @@ function getErrorCode(error) {
|
|
|
11
12
|
function normalizeLineEndings(content) {
|
|
12
13
|
return content.replaceAll('\r\n', '\n');
|
|
13
14
|
}
|
|
15
|
+
export function resolveInstructionSyncStrategy(strategy) {
|
|
16
|
+
return strategy ?? DEFAULT_INSTRUCTION_SYNC_STRATEGY;
|
|
17
|
+
}
|
|
18
|
+
function getValidInstructionDetail(strategy) {
|
|
19
|
+
switch (strategy) {
|
|
20
|
+
case 'symlink':
|
|
21
|
+
return 'symlink valid';
|
|
22
|
+
case 'copy':
|
|
23
|
+
return 'copy valid';
|
|
24
|
+
default:
|
|
25
|
+
return 'pointer valid';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function getInvalidInstructionDetail(strategy) {
|
|
29
|
+
switch (strategy) {
|
|
30
|
+
case 'symlink':
|
|
31
|
+
return 'expected symlink to AGENTS.md';
|
|
32
|
+
case 'copy':
|
|
33
|
+
return 'expected hard copy of AGENTS.md content';
|
|
34
|
+
default:
|
|
35
|
+
return `expected ${JSON.stringify(EXPECTED_CLAUDE_CONTENT)}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
14
38
|
function toPosixPath(pathValue) {
|
|
15
39
|
return pathValue.replaceAll('\\', '/');
|
|
16
40
|
}
|
|
17
41
|
function normalizeEntries(entries) {
|
|
18
42
|
return [...entries].sort((left, right) => {
|
|
19
|
-
|
|
43
|
+
const leftSortPath = left.agentsPath ?? left.claudePath;
|
|
44
|
+
const rightSortPath = right.agentsPath ?? right.claudePath;
|
|
45
|
+
return (leftSortPath.localeCompare(rightSortPath) ||
|
|
20
46
|
left.claudePath.localeCompare(right.claudePath) ||
|
|
21
47
|
left.status.localeCompare(right.status) ||
|
|
22
48
|
left.detail.localeCompare(right.detail));
|
|
@@ -30,9 +56,23 @@ function normalizeActions(actions) {
|
|
|
30
56
|
left.reason.localeCompare(right.reason));
|
|
31
57
|
});
|
|
32
58
|
}
|
|
33
|
-
|
|
59
|
+
function recordInstructionFile(directoryEntries, directoryPath, entryName, entryPath) {
|
|
60
|
+
const current = directoryEntries.get(directoryPath) ?? {};
|
|
61
|
+
if (entryName === 'AGENTS.md') {
|
|
62
|
+
current.agentsPath = entryPath;
|
|
63
|
+
current.brokenAgentsPath = undefined;
|
|
64
|
+
current.brokenAgentsErrorCode = undefined;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
current.claudePath = entryPath;
|
|
68
|
+
current.brokenClaudePath = undefined;
|
|
69
|
+
current.brokenClaudeErrorCode = undefined;
|
|
70
|
+
}
|
|
71
|
+
directoryEntries.set(directoryPath, current);
|
|
72
|
+
}
|
|
73
|
+
async function scanInstructionDirectories(repoRoot, dependencies, debug) {
|
|
34
74
|
const queue = [repoRoot];
|
|
35
|
-
const
|
|
75
|
+
const directoryEntries = new Map();
|
|
36
76
|
while (queue.length > 0) {
|
|
37
77
|
const currentDirectory = queue.shift();
|
|
38
78
|
if (!currentDirectory) {
|
|
@@ -46,7 +86,7 @@ async function scanAgentsFiles(repoRoot, dependencies) {
|
|
|
46
86
|
}
|
|
47
87
|
catch (error) {
|
|
48
88
|
const errorCode = getErrorCode(error);
|
|
49
|
-
|
|
89
|
+
debug?.(`Skipping directory scan for ${toPosixPath(currentDirectory)} (${errorCode ?? 'unknown error'})`);
|
|
50
90
|
continue;
|
|
51
91
|
}
|
|
52
92
|
for (const entry of entries) {
|
|
@@ -63,8 +103,8 @@ async function scanAgentsFiles(repoRoot, dependencies) {
|
|
|
63
103
|
continue;
|
|
64
104
|
}
|
|
65
105
|
if (entry.isFile()) {
|
|
66
|
-
if (entry.name === 'AGENTS.md') {
|
|
67
|
-
|
|
106
|
+
if (entry.name === 'AGENTS.md' || entry.name === 'CLAUDE.md') {
|
|
107
|
+
recordInstructionFile(directoryEntries, currentDirectory, entry.name, entryPath);
|
|
68
108
|
}
|
|
69
109
|
continue;
|
|
70
110
|
}
|
|
@@ -77,38 +117,157 @@ async function scanAgentsFiles(repoRoot, dependencies) {
|
|
|
77
117
|
}
|
|
78
118
|
catch (error) {
|
|
79
119
|
const errorCode = getErrorCode(error);
|
|
80
|
-
|
|
120
|
+
if (entry.name === 'CLAUDE.md') {
|
|
121
|
+
const current = directoryEntries.get(currentDirectory) ?? {};
|
|
122
|
+
current.claudePath = entryPath;
|
|
123
|
+
current.brokenClaudePath = entryPath;
|
|
124
|
+
current.brokenClaudeErrorCode = errorCode ?? 'unknown error';
|
|
125
|
+
directoryEntries.set(currentDirectory, current);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (entry.name === 'AGENTS.md') {
|
|
129
|
+
const current = directoryEntries.get(currentDirectory) ?? {};
|
|
130
|
+
current.brokenAgentsPath = entryPath;
|
|
131
|
+
current.brokenAgentsErrorCode = errorCode ?? 'unknown error';
|
|
132
|
+
directoryEntries.set(currentDirectory, current);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
debug?.(`Skipping symlink target stat for ${toPosixPath(entryPath)} (${errorCode ?? 'unknown error'})`);
|
|
81
136
|
continue;
|
|
82
137
|
}
|
|
83
138
|
if (entryStats.isDirectory()) {
|
|
84
139
|
continue;
|
|
85
140
|
}
|
|
86
|
-
if (entryStats.isFile() &&
|
|
87
|
-
|
|
141
|
+
if (entryStats.isFile() &&
|
|
142
|
+
(entry.name === 'AGENTS.md' || entry.name === 'CLAUDE.md')) {
|
|
143
|
+
recordInstructionFile(directoryEntries, currentDirectory, entry.name, entryPath);
|
|
88
144
|
}
|
|
89
145
|
}
|
|
90
146
|
}
|
|
91
|
-
return
|
|
147
|
+
return directoryEntries;
|
|
92
148
|
}
|
|
93
|
-
export async function scanInstructionFiles(repoRoot, overrides = {}) {
|
|
149
|
+
export async function scanInstructionFiles(repoRoot, options = {}, overrides = {}) {
|
|
150
|
+
const strategy = resolveInstructionSyncStrategy(options.strategy);
|
|
94
151
|
const dependencies = {
|
|
152
|
+
lstat,
|
|
153
|
+
realpath,
|
|
95
154
|
readdir,
|
|
96
155
|
readFile,
|
|
156
|
+
readlink,
|
|
97
157
|
stat,
|
|
98
158
|
...overrides,
|
|
99
159
|
};
|
|
100
|
-
const
|
|
160
|
+
const instructionDirectories = await scanInstructionDirectories(repoRoot, dependencies, options.debug);
|
|
101
161
|
const entries = [];
|
|
102
|
-
for (const
|
|
103
|
-
const
|
|
162
|
+
for (const [directoryPath, directoryEntry] of instructionDirectories) {
|
|
163
|
+
const agentsPath = directoryEntry.agentsPath ?? null;
|
|
164
|
+
const brokenAgentsPath = directoryEntry.brokenAgentsPath ?? null;
|
|
165
|
+
const brokenAgentsErrorCode = directoryEntry.brokenAgentsErrorCode ?? 'unknown error';
|
|
166
|
+
const brokenClaudePath = directoryEntry.brokenClaudePath ?? null;
|
|
167
|
+
const brokenClaudeErrorCode = directoryEntry.brokenClaudeErrorCode ?? 'unknown error';
|
|
168
|
+
const claudePath = directoryEntry.claudePath ?? join(directoryPath, 'CLAUDE.md');
|
|
169
|
+
if (!agentsPath && brokenAgentsPath) {
|
|
170
|
+
entries.push({
|
|
171
|
+
agentsPath: brokenAgentsPath,
|
|
172
|
+
claudePath,
|
|
173
|
+
status: 'content_mismatch',
|
|
174
|
+
detail: brokenAgentsErrorCode === 'ENOENT'
|
|
175
|
+
? 'broken AGENTS.md symlink'
|
|
176
|
+
: `unreadable AGENTS.md symlink target (${brokenAgentsErrorCode})`,
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (brokenClaudePath) {
|
|
181
|
+
entries.push({
|
|
182
|
+
agentsPath,
|
|
183
|
+
claudePath,
|
|
184
|
+
status: 'content_mismatch',
|
|
185
|
+
detail: brokenClaudeErrorCode === 'ENOENT'
|
|
186
|
+
? 'broken CLAUDE.md symlink'
|
|
187
|
+
: `unreadable CLAUDE.md symlink target (${brokenClaudeErrorCode})`,
|
|
188
|
+
});
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (!agentsPath) {
|
|
192
|
+
try {
|
|
193
|
+
await dependencies.readFile(claudePath, 'utf8');
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
entries.push({
|
|
197
|
+
agentsPath: null,
|
|
198
|
+
claudePath,
|
|
199
|
+
status: 'content_mismatch',
|
|
200
|
+
detail: `unable to read CLAUDE.md (${getErrorCode(error) ?? 'unknown'})`,
|
|
201
|
+
});
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
entries.push({
|
|
205
|
+
agentsPath: null,
|
|
206
|
+
claudePath,
|
|
207
|
+
status: 'stray',
|
|
208
|
+
detail: 'CLAUDE.md found without AGENTS.md',
|
|
209
|
+
});
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
let claudeStats;
|
|
104
213
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
214
|
+
claudeStats = await dependencies.lstat(claudePath);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
const errorCode = getErrorCode(error);
|
|
218
|
+
if (errorCode === 'ENOENT') {
|
|
219
|
+
entries.push({
|
|
220
|
+
agentsPath,
|
|
221
|
+
claudePath,
|
|
222
|
+
status: 'missing',
|
|
223
|
+
detail: 'CLAUDE.md missing',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
entries.push({
|
|
228
|
+
agentsPath,
|
|
229
|
+
claudePath,
|
|
230
|
+
status: 'content_mismatch',
|
|
231
|
+
detail: `unable to read CLAUDE.md (${errorCode ?? 'unknown error'})`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (strategy === 'symlink') {
|
|
237
|
+
if (!claudeStats.isSymbolicLink()) {
|
|
238
|
+
entries.push({
|
|
239
|
+
agentsPath,
|
|
240
|
+
claudePath,
|
|
241
|
+
status: 'content_mismatch',
|
|
242
|
+
detail: getInvalidInstructionDetail(strategy),
|
|
243
|
+
});
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
let claudeTarget;
|
|
247
|
+
try {
|
|
248
|
+
claudeTarget = await dependencies.readlink(claudePath);
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
const errorCode = getErrorCode(error);
|
|
252
|
+
entries.push({
|
|
253
|
+
agentsPath,
|
|
254
|
+
claudePath,
|
|
255
|
+
status: 'content_mismatch',
|
|
256
|
+
detail: `unable to read CLAUDE.md symlink target (${errorCode ?? 'unknown error'})`,
|
|
257
|
+
});
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const resolvedTarget = resolve(dirname(claudePath), claudeTarget);
|
|
261
|
+
const [canonicalTarget, canonicalAgentsPath] = await Promise.all([
|
|
262
|
+
dependencies.realpath(resolvedTarget).catch(() => resolvedTarget),
|
|
263
|
+
dependencies.realpath(agentsPath).catch(() => agentsPath),
|
|
264
|
+
]);
|
|
265
|
+
if (canonicalTarget === canonicalAgentsPath) {
|
|
107
266
|
entries.push({
|
|
108
267
|
agentsPath,
|
|
109
268
|
claudePath,
|
|
110
269
|
status: 'ok',
|
|
111
|
-
detail:
|
|
270
|
+
detail: getValidInstructionDetail(strategy),
|
|
112
271
|
});
|
|
113
272
|
}
|
|
114
273
|
else {
|
|
@@ -116,9 +275,23 @@ export async function scanInstructionFiles(repoRoot, overrides = {}) {
|
|
|
116
275
|
agentsPath,
|
|
117
276
|
claudePath,
|
|
118
277
|
status: 'content_mismatch',
|
|
119
|
-
detail:
|
|
278
|
+
detail: getInvalidInstructionDetail(strategy),
|
|
120
279
|
});
|
|
121
280
|
}
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (claudeStats.isSymbolicLink()) {
|
|
284
|
+
entries.push({
|
|
285
|
+
agentsPath,
|
|
286
|
+
claudePath,
|
|
287
|
+
status: 'content_mismatch',
|
|
288
|
+
detail: getInvalidInstructionDetail(strategy),
|
|
289
|
+
});
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
let claudeContent;
|
|
293
|
+
try {
|
|
294
|
+
claudeContent = await dependencies.readFile(claudePath, 'utf8');
|
|
122
295
|
}
|
|
123
296
|
catch (error) {
|
|
124
297
|
const errorCode = getErrorCode(error);
|
|
@@ -138,6 +311,44 @@ export async function scanInstructionFiles(repoRoot, overrides = {}) {
|
|
|
138
311
|
detail: `unable to read CLAUDE.md (${errorCode ?? 'unknown error'})`,
|
|
139
312
|
});
|
|
140
313
|
}
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const expectedContent = strategy === 'copy'
|
|
317
|
+
? await (async () => {
|
|
318
|
+
try {
|
|
319
|
+
return await dependencies.readFile(agentsPath, 'utf8');
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
const errorCode = getErrorCode(error);
|
|
323
|
+
entries.push({
|
|
324
|
+
agentsPath,
|
|
325
|
+
claudePath,
|
|
326
|
+
status: 'content_mismatch',
|
|
327
|
+
detail: `unable to read AGENTS.md (${errorCode ?? 'unknown error'})`,
|
|
328
|
+
});
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
})()
|
|
332
|
+
: EXPECTED_CLAUDE_CONTENT;
|
|
333
|
+
if (expectedContent === null) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (normalizeLineEndings(claudeContent) ===
|
|
337
|
+
normalizeLineEndings(expectedContent)) {
|
|
338
|
+
entries.push({
|
|
339
|
+
agentsPath,
|
|
340
|
+
claudePath,
|
|
341
|
+
status: 'ok',
|
|
342
|
+
detail: getValidInstructionDetail(strategy),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
entries.push({
|
|
347
|
+
agentsPath,
|
|
348
|
+
claudePath,
|
|
349
|
+
status: 'content_mismatch',
|
|
350
|
+
detail: getInvalidInstructionDetail(strategy),
|
|
351
|
+
});
|
|
141
352
|
}
|
|
142
353
|
}
|
|
143
354
|
return normalizeEntries(entries);
|
|
@@ -151,6 +362,7 @@ export function buildInstructionsSummary(entries, actions) {
|
|
|
151
362
|
missing: normalizedEntries.filter((entry) => entry.status === 'missing')
|
|
152
363
|
.length,
|
|
153
364
|
contentMismatch: normalizedEntries.filter((entry) => entry.status === 'content_mismatch').length,
|
|
365
|
+
stray: normalizedEntries.filter((entry) => entry.status === 'stray').length,
|
|
154
366
|
created: normalizedActions.filter((action) => action.type === 'create')
|
|
155
367
|
.length,
|
|
156
368
|
updated: normalizedActions.filter((action) => action.type === 'update')
|
|
@@ -181,7 +393,7 @@ export function formatInstructionsReport(payload, repoRoot) {
|
|
|
181
393
|
const lines = [
|
|
182
394
|
`instructions ${payload.mode}`,
|
|
183
395
|
`status: ${payload.status}`,
|
|
184
|
-
`summary: scanned=${payload.summary.scanned}, ok=${payload.summary.ok}, missing=${payload.summary.missing}, content_mismatch=${payload.summary.contentMismatch}, created=${payload.summary.created}, updated=${payload.summary.updated}, skipped=${payload.summary.skipped}`,
|
|
396
|
+
`summary: scanned=${payload.summary.scanned}, ok=${payload.summary.ok}, missing=${payload.summary.missing}, content_mismatch=${payload.summary.contentMismatch}, stray=${payload.summary.stray}, created=${payload.summary.created}, updated=${payload.summary.updated}, skipped=${payload.summary.skipped}`,
|
|
185
397
|
];
|
|
186
398
|
if (payload.entries.length === 0) {
|
|
187
399
|
lines.push('entries: none');
|
|
@@ -189,10 +401,11 @@ export function formatInstructionsReport(payload, repoRoot) {
|
|
|
189
401
|
else {
|
|
190
402
|
lines.push('entries:');
|
|
191
403
|
for (const entry of payload.entries) {
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
404
|
+
const displayPath = entry.agentsPath ?? entry.claudePath;
|
|
405
|
+
const relativePath = repoRoot
|
|
406
|
+
? toPosixPath(relative(repoRoot, displayPath)) || '.'
|
|
407
|
+
: toPosixPath(displayPath);
|
|
408
|
+
lines.push(`- ${relativePath} -> ${entry.status} (${entry.detail})`);
|
|
196
409
|
}
|
|
197
410
|
}
|
|
198
411
|
if (payload.actions.length === 0) {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { rm } from 'node:fs/promises';
|
|
2
|
+
import { type InstructionsSyncCommandDependencies } from '../../instructions/instructions.types.js';
|
|
2
3
|
import { Command } from 'commander';
|
|
4
|
+
export declare function removeInstructionFile(path: string, remove?: typeof rm): Promise<void>;
|
|
3
5
|
export declare function createInstructionsSyncCommand(overrides?: Partial<InstructionsSyncCommandDependencies>): Command;
|
|
4
6
|
//# sourceMappingURL=sync.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../../src/commands/instructions/sync/sync.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../../src/commands/instructions/sync/sync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,EAAE,EAAsB,MAAM,kBAAkB,CAAC;AAI3E,OAAO,EAKL,KAAK,mCAAmC,EACzC,MAAM,2CAA2C,CAAC;AAYnD,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAQ5C,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,OAAO,EAAO,GACrB,OAAO,CAAC,IAAI,CAAC,CAEf;AA+SD,wBAAgB,6BAA6B,CAC3C,SAAS,GAAE,OAAO,CAAC,mCAAmC,CAAM,GAC3D,OAAO,CA6FT"}
|
|
@@ -1,26 +1,96 @@
|
|
|
1
|
-
import { writeFile } from 'node:fs/promises';
|
|
1
|
+
import { lstat, readFile, rm, symlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, relative } from 'node:path';
|
|
2
3
|
import { buildCommandContext } from '../../../app/command-context.js';
|
|
3
|
-
import {
|
|
4
|
+
import { INSTRUCTION_SYNC_STRATEGIES, } from '../../instructions/instructions.types.js';
|
|
5
|
+
import { buildInstructionsPayload, DEFAULT_INSTRUCTION_SYNC_STRATEGY, EXPECTED_CLAUDE_CONTENT, formatInstructionsReport, resolveInstructionSyncStrategy, scanInstructionFiles, } from '../../instructions/instructions.utils.js';
|
|
4
6
|
import { readGlobalOptions } from '../../shared/shared.utils.js';
|
|
5
7
|
import { CliError } from '../../../errors/cli-error.js';
|
|
6
8
|
import { resolveProjectRoot } from '../../../fs/paths.js';
|
|
7
|
-
import { Command } from 'commander';
|
|
9
|
+
import { Command, Option } from 'commander';
|
|
10
|
+
export async function removeInstructionFile(path, remove = rm) {
|
|
11
|
+
await remove(path, { force: true });
|
|
12
|
+
}
|
|
8
13
|
function defaultDependencies() {
|
|
9
14
|
return {
|
|
10
15
|
buildCommandContext,
|
|
16
|
+
lstat,
|
|
17
|
+
readFile,
|
|
18
|
+
removeFile: removeInstructionFile,
|
|
11
19
|
resolveProjectRoot,
|
|
12
20
|
scanInstructionFiles,
|
|
21
|
+
symlinkFile: async (target, path) => {
|
|
22
|
+
await symlink(target, path, 'file');
|
|
23
|
+
},
|
|
13
24
|
writeFile,
|
|
14
25
|
};
|
|
15
26
|
}
|
|
16
|
-
function
|
|
27
|
+
function getSyncReason(actionType, strategy) {
|
|
28
|
+
const label = strategy === 'symlink'
|
|
29
|
+
? 'symlink'
|
|
30
|
+
: strategy === 'copy'
|
|
31
|
+
? 'hard copy'
|
|
32
|
+
: 'pointer file';
|
|
33
|
+
return actionType === 'create'
|
|
34
|
+
? `missing CLAUDE.md ${label}`
|
|
35
|
+
: `overwrite CLAUDE.md with canonical ${label}`;
|
|
36
|
+
}
|
|
37
|
+
function getSyncedDetail(strategy) {
|
|
38
|
+
switch (strategy) {
|
|
39
|
+
case 'symlink':
|
|
40
|
+
return 'symlink synced';
|
|
41
|
+
case 'copy':
|
|
42
|
+
return 'copy synced';
|
|
43
|
+
default:
|
|
44
|
+
return 'pointer synced';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function getAgentsPath(entry) {
|
|
48
|
+
return entry.agentsPath ?? join(dirname(entry.claudePath), 'AGENTS.md');
|
|
49
|
+
}
|
|
50
|
+
function getErrorCode(error) {
|
|
51
|
+
return error && typeof error === 'object' && 'code' in error
|
|
52
|
+
? String(error.code)
|
|
53
|
+
: null;
|
|
54
|
+
}
|
|
55
|
+
function hasUnreadableCanonicalAgents(entry) {
|
|
56
|
+
return (entry.agentsPath !== null &&
|
|
57
|
+
(entry.detail === 'broken AGENTS.md symlink' ||
|
|
58
|
+
entry.detail.startsWith('unreadable AGENTS.md symlink target') ||
|
|
59
|
+
entry.detail.startsWith('unable to read AGENTS.md')));
|
|
60
|
+
}
|
|
61
|
+
function hasUnreadableClaudeSource(entry) {
|
|
62
|
+
return (entry.agentsPath === null &&
|
|
63
|
+
(entry.detail === 'broken CLAUDE.md symlink' ||
|
|
64
|
+
entry.detail.startsWith('unreadable CLAUDE.md symlink target') ||
|
|
65
|
+
entry.detail.startsWith('unable to read CLAUDE.md')));
|
|
66
|
+
}
|
|
67
|
+
function wrapStrayResyncError(error, agentsPath, claudePath) {
|
|
68
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
69
|
+
return new CliError(`Adopted stray instructions into ${agentsPath}, but failed to regenerate ${claudePath}: ${message}`, error instanceof CliError ? error.exitCode : 2);
|
|
70
|
+
}
|
|
71
|
+
function planSyncActions({ entries, force, strategy, }) {
|
|
17
72
|
const actions = [];
|
|
18
73
|
for (const entry of entries) {
|
|
74
|
+
if (entry.status === 'stray') {
|
|
75
|
+
actions.push({
|
|
76
|
+
type: 'create',
|
|
77
|
+
target: getAgentsPath(entry),
|
|
78
|
+
reason: 'adopt stray CLAUDE.md into canonical AGENTS.md',
|
|
79
|
+
result: 'planned',
|
|
80
|
+
});
|
|
81
|
+
actions.push({
|
|
82
|
+
type: 'update',
|
|
83
|
+
target: entry.claudePath,
|
|
84
|
+
reason: getSyncReason('update', strategy),
|
|
85
|
+
result: 'planned',
|
|
86
|
+
});
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
19
89
|
if (entry.status === 'missing') {
|
|
20
90
|
actions.push({
|
|
21
91
|
type: 'create',
|
|
22
92
|
target: entry.claudePath,
|
|
23
|
-
reason: '
|
|
93
|
+
reason: getSyncReason('create', strategy),
|
|
24
94
|
result: 'planned',
|
|
25
95
|
});
|
|
26
96
|
continue;
|
|
@@ -28,6 +98,28 @@ function planSyncActions({ entries, force, }) {
|
|
|
28
98
|
if (entry.status !== 'content_mismatch') {
|
|
29
99
|
continue;
|
|
30
100
|
}
|
|
101
|
+
if (hasUnreadableClaudeSource(entry)) {
|
|
102
|
+
actions.push({
|
|
103
|
+
type: 'skip',
|
|
104
|
+
target: entry.claudePath,
|
|
105
|
+
reason: 'CLAUDE.md unreadable; repair manually',
|
|
106
|
+
result: 'skipped',
|
|
107
|
+
});
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (hasUnreadableCanonicalAgents(entry)) {
|
|
111
|
+
const unreadableAgentsPath = entry.agentsPath;
|
|
112
|
+
if (!unreadableAgentsPath) {
|
|
113
|
+
throw new CliError(`Unable to resolve unreadable AGENTS.md path for ${entry.claudePath}`, 2);
|
|
114
|
+
}
|
|
115
|
+
actions.push({
|
|
116
|
+
type: 'skip',
|
|
117
|
+
target: unreadableAgentsPath,
|
|
118
|
+
reason: 'canonical AGENTS.md unreadable; repair manually',
|
|
119
|
+
result: 'skipped',
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
31
123
|
if (!force) {
|
|
32
124
|
actions.push({
|
|
33
125
|
type: 'skip',
|
|
@@ -40,20 +132,76 @@ function planSyncActions({ entries, force, }) {
|
|
|
40
132
|
actions.push({
|
|
41
133
|
type: 'update',
|
|
42
134
|
target: entry.claudePath,
|
|
43
|
-
reason: '
|
|
135
|
+
reason: getSyncReason('update', strategy),
|
|
44
136
|
result: 'planned',
|
|
45
137
|
});
|
|
46
138
|
}
|
|
47
139
|
return actions;
|
|
48
140
|
}
|
|
49
|
-
async function applySyncActions(actions, dependencies) {
|
|
141
|
+
async function applySyncActions(actions, entries, dependencies, strategy) {
|
|
50
142
|
const appliedActions = [];
|
|
143
|
+
const entriesByTarget = new Map();
|
|
144
|
+
for (const entry of entries) {
|
|
145
|
+
entriesByTarget.set(entry.claudePath, entry);
|
|
146
|
+
entriesByTarget.set(getAgentsPath(entry), entry);
|
|
147
|
+
}
|
|
51
148
|
for (const action of actions) {
|
|
52
149
|
if (action.result !== 'planned') {
|
|
53
150
|
appliedActions.push(action);
|
|
54
151
|
continue;
|
|
55
152
|
}
|
|
56
|
-
|
|
153
|
+
const entry = entriesByTarget.get(action.target);
|
|
154
|
+
if (!entry) {
|
|
155
|
+
throw new CliError(`Unable to resolve instruction entry for ${action.target}`, 2);
|
|
156
|
+
}
|
|
157
|
+
const agentsPath = getAgentsPath(entry);
|
|
158
|
+
const isAgentsAction = action.target === agentsPath;
|
|
159
|
+
if (isAgentsAction) {
|
|
160
|
+
try {
|
|
161
|
+
await dependencies.lstat(agentsPath);
|
|
162
|
+
throw new CliError(`Canonical AGENTS.md appeared during sync at ${agentsPath}; re-run to reclassify before adopting stray CLAUDE.md`, 2);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (error instanceof CliError) {
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
if (getErrorCode(error) !== 'ENOENT') {
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const adoptedContent = await dependencies.readFile(entry.claudePath, 'utf8');
|
|
173
|
+
await dependencies.writeFile(agentsPath, adoptedContent, 'utf8');
|
|
174
|
+
appliedActions.push({
|
|
175
|
+
...action,
|
|
176
|
+
result: 'applied',
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (!entry.agentsPath && action.type !== 'update') {
|
|
181
|
+
throw new CliError(`Unable to resolve AGENTS.md for ${action.target}`, 2);
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
if (action.type === 'update') {
|
|
185
|
+
await dependencies.removeFile(action.target);
|
|
186
|
+
}
|
|
187
|
+
if (strategy === 'symlink') {
|
|
188
|
+
const symlinkTarget = relative(dirname(action.target), agentsPath);
|
|
189
|
+
await dependencies.symlinkFile(symlinkTarget, action.target);
|
|
190
|
+
}
|
|
191
|
+
else if (strategy === 'copy') {
|
|
192
|
+
const agentsContent = await dependencies.readFile(agentsPath, 'utf8');
|
|
193
|
+
await dependencies.writeFile(action.target, agentsContent, 'utf8');
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
await dependencies.writeFile(action.target, EXPECTED_CLAUDE_CONTENT, 'utf8');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (entry.status === 'stray') {
|
|
201
|
+
throw wrapStrayResyncError(error, agentsPath, action.target);
|
|
202
|
+
}
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
57
205
|
appliedActions.push({
|
|
58
206
|
...action,
|
|
59
207
|
result: 'applied',
|
|
@@ -61,18 +209,21 @@ async function applySyncActions(actions, dependencies) {
|
|
|
61
209
|
}
|
|
62
210
|
return appliedActions;
|
|
63
211
|
}
|
|
64
|
-
function getPostSyncEntries(entries, actions) {
|
|
212
|
+
function getPostSyncEntries(entries, actions, strategy) {
|
|
65
213
|
const actionByTarget = new Map(actions.map((action) => [action.target, action]));
|
|
66
214
|
return entries.map((entry) => {
|
|
67
215
|
const action = actionByTarget.get(entry.claudePath);
|
|
68
|
-
|
|
216
|
+
const adoptedAction = actionByTarget.get(getAgentsPath(entry));
|
|
217
|
+
if (!action && !adoptedAction) {
|
|
69
218
|
return entry;
|
|
70
219
|
}
|
|
71
|
-
if (action
|
|
220
|
+
if (action?.result === 'applied' &&
|
|
221
|
+
(entry.status !== 'stray' || adoptedAction?.result === 'applied')) {
|
|
72
222
|
return {
|
|
73
223
|
...entry,
|
|
224
|
+
agentsPath: getAgentsPath(entry),
|
|
74
225
|
status: 'ok',
|
|
75
|
-
detail:
|
|
226
|
+
detail: getSyncedDetail(strategy),
|
|
76
227
|
};
|
|
77
228
|
}
|
|
78
229
|
return entry;
|
|
@@ -87,25 +238,34 @@ export function createInstructionsSyncCommand(overrides = {}) {
|
|
|
87
238
|
...overrides,
|
|
88
239
|
};
|
|
89
240
|
return new Command('sync')
|
|
90
|
-
.description('Repair AGENTS.md
|
|
241
|
+
.description('Repair AGENTS.md/CLAUDE.md sync drift using the selected strategy')
|
|
91
242
|
.option('--dry-run', 'Preview sync changes without applying')
|
|
92
243
|
.option('--force', 'Overwrite mismatched CLAUDE.md files')
|
|
244
|
+
.addOption(new Option('--strategy <strategy>', 'Sync strategy')
|
|
245
|
+
.choices([...INSTRUCTION_SYNC_STRATEGIES])
|
|
246
|
+
.default(DEFAULT_INSTRUCTION_SYNC_STRATEGY))
|
|
93
247
|
.action(async (options, command) => {
|
|
94
248
|
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
95
249
|
try {
|
|
96
250
|
const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
|
|
97
|
-
const
|
|
251
|
+
const strategy = resolveInstructionSyncStrategy(options.strategy);
|
|
252
|
+
const entries = await dependencies.scanInstructionFiles(repoRoot, {
|
|
253
|
+
strategy,
|
|
254
|
+
});
|
|
98
255
|
const plannedActions = planSyncActions({
|
|
99
256
|
entries,
|
|
100
257
|
force: options.force ?? false,
|
|
258
|
+
strategy,
|
|
101
259
|
});
|
|
102
260
|
const dryRun = options.dryRun ?? false;
|
|
103
261
|
const actions = dryRun
|
|
104
262
|
? plannedActions
|
|
105
|
-
: await applySyncActions(plannedActions, dependencies);
|
|
263
|
+
: await applySyncActions(plannedActions, entries, dependencies, strategy);
|
|
106
264
|
const payload = buildInstructionsPayload({
|
|
107
265
|
mode: dryRun ? 'dry-run' : 'apply',
|
|
108
|
-
entries: dryRun
|
|
266
|
+
entries: dryRun
|
|
267
|
+
? entries
|
|
268
|
+
: getPostSyncEntries(entries, actions, strategy),
|
|
109
269
|
actions,
|
|
110
270
|
});
|
|
111
271
|
if (context.json) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type InstructionsValidateCommandDependencies } from '../../instructions/instructions.types.js';
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
export declare function createInstructionsValidateCommand(overrides?: Partial<InstructionsValidateCommandDependencies>): Command;
|
|
4
4
|
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../../src/commands/instructions/validate/validate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../../src/commands/instructions/validate/validate.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,uCAAuC,EAC7C,MAAM,2CAA2C,CAAC;AAWnD,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAU5C,wBAAgB,iCAAiC,CAC/C,SAAS,GAAE,OAAO,CAAC,uCAAuC,CAAM,GAC/D,OAAO,CA2DT"}
|