@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cuewright/skills",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "CLI installer for the Cuewright skills framework",
5
5
  "type": "module",
6
6
  "engines": {
@@ -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)`
@@ -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', unprefixed);
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
- } else {
266
- orphaned.push(name);
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
  }