@cuewright/skills 0.2.1 → 0.2.3
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/package.json +1 -1
- package/src/commands/doctor.js +13 -2
- package/src/utils/symlink.js +87 -6
package/package.json
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -76,8 +76,8 @@ export async function doctor() {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
// 4b. Inventory skills directory — detect legacy and orphaned entries
|
|
80
|
-
const { legacy: legacyFound, orphaned: orphanedFound } = await inventorySkillsDir(skillsDir, skills);
|
|
79
|
+
// 4b. Inventory skills directory — detect legacy, dead framework, and orphaned entries
|
|
80
|
+
const { legacy: legacyFound, deadFramework: deadFound, orphaned: orphanedFound } = await inventorySkillsDir(skillsDir, skills);
|
|
81
81
|
|
|
82
82
|
if (legacyFound.length > 0) {
|
|
83
83
|
allPassed = false;
|
|
@@ -90,6 +90,17 @@ export async function doctor() {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
if (deadFound.length > 0) {
|
|
94
|
+
allPassed = false;
|
|
95
|
+
checks.push(fail(
|
|
96
|
+
`${deadFound.length} broken framework symlink${deadFound.length === 1 ? '' : 's'} found (retired skills with no target)`,
|
|
97
|
+
'Run `cuewright update` to clean up automatically.'
|
|
98
|
+
));
|
|
99
|
+
for (const name of deadFound) {
|
|
100
|
+
checks.push(fail(` .claude/skills/${name}`, null));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
93
104
|
if (orphanedFound.length > 0) {
|
|
94
105
|
checks.push(warn(
|
|
95
106
|
`${orphanedFound.length} unknown director${orphanedFound.length === 1 ? 'y' : 'ies'} in .claude/skills/ (third-party or project-local — no action needed)`
|
package/src/utils/symlink.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { symlink, mkdir, access, lstat, readFile, writeFile, readdir, unlink, rename } from 'fs/promises';
|
|
1
|
+
import { symlink, mkdir, access, lstat, readFile, writeFile, readdir, unlink, rename, readlink } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -190,7 +190,7 @@ export async function cleanupLegacyDirectories(skillsDir, manifestSkillNames, pr
|
|
|
190
190
|
const referenceFiles = entries.filter(e => e !== 'SKILL.md');
|
|
191
191
|
|
|
192
192
|
if (referenceFiles.length > 0 && !dryRun) {
|
|
193
|
-
const refDir = join(projectRoot, '.claude', 'skills', 'arch', 'references',
|
|
193
|
+
const refDir = join(projectRoot, '.claude', 'skills', 'arch', 'references', cwName);
|
|
194
194
|
try {
|
|
195
195
|
await mkdir(refDir, { recursive: true });
|
|
196
196
|
for (const file of referenceFiles) {
|
|
@@ -220,6 +220,61 @@ export async function cleanupLegacyDirectories(skillsDir, manifestSkillNames, pr
|
|
|
220
220
|
cleaned.push(unprefixed);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
// Second pass: remove broken symlinks pointing into _framework/ with no manifest match.
|
|
224
|
+
// These are retired framework skills whose target directory no longer exists.
|
|
225
|
+
const { join: pathJoin } = await import('path');
|
|
226
|
+
const RESERVED = new Set(['_framework', '_baseline', 'arch', 'worktrees']);
|
|
227
|
+
const cwNameSet = new Set(manifestSkillNames);
|
|
228
|
+
const unprefixedSet = new Set(
|
|
229
|
+
manifestSkillNames.filter(n => n.startsWith('cw-')).map(n => n.slice(3))
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
let allEntries;
|
|
233
|
+
try {
|
|
234
|
+
allEntries = await readdir(skillsDir, { withFileTypes: true });
|
|
235
|
+
} catch {
|
|
236
|
+
return { cleaned, errors };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const entry of allEntries) {
|
|
240
|
+
const name = entry.name;
|
|
241
|
+
if (RESERVED.has(name)) continue;
|
|
242
|
+
if (cwNameSet.has(name)) continue; // active framework skill
|
|
243
|
+
if (unprefixedSet.has(name)) continue; // already handled in first pass
|
|
244
|
+
if (!entry.isSymbolicLink()) continue;
|
|
245
|
+
|
|
246
|
+
const linkPath = pathJoin(skillsDir, name);
|
|
247
|
+
let target;
|
|
248
|
+
try {
|
|
249
|
+
target = await readlink(linkPath);
|
|
250
|
+
} catch {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Only touch symlinks whose target is inside _framework/skills/
|
|
255
|
+
if (!target.includes('_framework/skills/') && !target.includes('_framework\\skills\\')) continue;
|
|
256
|
+
|
|
257
|
+
// Check if the target resolves — if not, it's dead
|
|
258
|
+
let broken = false;
|
|
259
|
+
try {
|
|
260
|
+
await access(linkPath);
|
|
261
|
+
} catch {
|
|
262
|
+
broken = true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!broken) continue;
|
|
266
|
+
|
|
267
|
+
if (!dryRun) {
|
|
268
|
+
try {
|
|
269
|
+
await unlink(linkPath);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
errors.push(`${name}: could not remove broken framework symlink — ${err.message}`);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
cleaned.push(name);
|
|
276
|
+
}
|
|
277
|
+
|
|
223
278
|
return { cleaned, errors };
|
|
224
279
|
}
|
|
225
280
|
|
|
@@ -239,6 +294,7 @@ export async function cleanupLegacyDirectories(skillsDir, manifestSkillNames, pr
|
|
|
239
294
|
export async function inventorySkillsDir(skillsDir, manifestSkillNames) {
|
|
240
295
|
const { join } = await import('path');
|
|
241
296
|
const legacy = [];
|
|
297
|
+
const deadFramework = [];
|
|
242
298
|
const orphaned = [];
|
|
243
299
|
|
|
244
300
|
const RESERVED = new Set(['_framework', '_baseline', 'arch', 'worktrees']);
|
|
@@ -251,7 +307,7 @@ export async function inventorySkillsDir(skillsDir, manifestSkillNames) {
|
|
|
251
307
|
try {
|
|
252
308
|
entries = await readdir(skillsDir, { withFileTypes: true });
|
|
253
309
|
} catch {
|
|
254
|
-
return { legacy, orphaned };
|
|
310
|
+
return { legacy, deadFramework, orphaned };
|
|
255
311
|
}
|
|
256
312
|
|
|
257
313
|
for (const entry of entries) {
|
|
@@ -262,10 +318,35 @@ export async function inventorySkillsDir(skillsDir, manifestSkillNames) {
|
|
|
262
318
|
|
|
263
319
|
if (unprefixedNames.has(name)) {
|
|
264
320
|
legacy.push(name);
|
|
265
|
-
|
|
266
|
-
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (entry.isSymbolicLink()) {
|
|
325
|
+
// Check if this is a broken symlink into _framework/
|
|
326
|
+
let target;
|
|
327
|
+
try {
|
|
328
|
+
target = await readlink(join(skillsDir, name));
|
|
329
|
+
} catch {
|
|
330
|
+
orphaned.push(name);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (target.includes('_framework/skills/') || target.includes('_framework\\skills\\')) {
|
|
335
|
+
let broken = false;
|
|
336
|
+
try {
|
|
337
|
+
await access(join(skillsDir, name));
|
|
338
|
+
} catch {
|
|
339
|
+
broken = true;
|
|
340
|
+
}
|
|
341
|
+
if (broken) {
|
|
342
|
+
deadFramework.push(name);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
267
346
|
}
|
|
347
|
+
|
|
348
|
+
orphaned.push(name);
|
|
268
349
|
}
|
|
269
350
|
|
|
270
|
-
return { legacy, orphaned };
|
|
351
|
+
return { legacy, deadFramework, orphaned };
|
|
271
352
|
}
|