@vruum/skills 0.6.1 → 0.6.2

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.
Files changed (2) hide show
  1. package/install.js +87 -21
  2. package/package.json +5 -2
package/install.js CHANGED
@@ -191,6 +191,59 @@ function syncVruumRoot({ dryRun }) {
191
191
  return rows;
192
192
  }
193
193
 
194
+ // A symlink is "ours" if its target resolves into ~/.vruum/skills/. Resolve
195
+ // textually from the readlink string (relative links against the link's own
196
+ // dir) — never realpathSync, which THROWS on a dangling link, i.e. exactly the
197
+ // renamed/removed-skill bug case we need to prune. The trailing path.sep
198
+ // boundary matters: ~/.vruum/skills is a string prefix of
199
+ // ~/.vruum/skills-operator, so a bare startsWith would wrongly claim the
200
+ // operator installer's links. Equality covers a link straight at the dir.
201
+ function isOurLink(dst, linkTarget) {
202
+ const resolved = path.isAbsolute(linkTarget)
203
+ ? path.resolve(linkTarget)
204
+ : path.resolve(path.dirname(dst), linkTarget);
205
+ return resolved === VRUUM_SKILLS || resolved.startsWith(VRUUM_SKILLS + path.sep);
206
+ }
207
+
208
+ // Prune pass — after relinking current skills, remove links for skills no
209
+ // longer in the package (renamed or deleted), but ONLY symlinks this installer
210
+ // owns (target under ~/.vruum/skills/). Non-symlinks (real user dirs/files) and
211
+ // foreign symlinks (the operator installer's links, arbitrary user links) are
212
+ // never touched, so the two npm installers + the bash setup coexist in the same
213
+ // harness dir without pruning each other.
214
+ function pruneStaleLinks({ target, skills, dryRun }) {
215
+ const results = [];
216
+ let entries;
217
+ try {
218
+ entries = fs.readdirSync(target);
219
+ } catch (err) {
220
+ if (err.code === 'ENOENT') return results;
221
+ throw err;
222
+ }
223
+ const current = new Set(skills);
224
+ for (const entry of entries) {
225
+ if (current.has(entry)) continue; // current skill — keep
226
+ const dst = path.join(target, entry);
227
+ let linkTarget;
228
+ try {
229
+ const lstat = fs.lstatSync(dst);
230
+ if (!lstat.isSymbolicLink()) continue; // never touch non-symlinks
231
+ linkTarget = fs.readlinkSync(dst);
232
+ } catch (err) {
233
+ if (err.code === 'ENOENT') continue; // raced away
234
+ throw err;
235
+ }
236
+ if (!isOurLink(dst, linkTarget)) continue; // foreign symlink — keep
237
+ if (dryRun) {
238
+ results.push({ name: entry, target, action: 'would-prune' });
239
+ } else {
240
+ fs.unlinkSync(dst);
241
+ results.push({ name: entry, target, action: 'pruned' });
242
+ }
243
+ }
244
+ return results;
245
+ }
246
+
194
247
  function ensureTargetDir(target, dryRun) {
195
248
  if (fs.existsSync(target)) return { created: false };
196
249
  if (dryRun) return { created: 'would' };
@@ -283,6 +336,11 @@ function commandInstall({ targets: extraTargets, dryRun }) {
283
336
  const srcAbs = path.join(VRUUM_SKILLS, skillName);
284
337
  summary.push(linkSkill({ name: skillName, srcAbs, target: target.dir, dryRun }));
285
338
  }
339
+ // Prune links we own for skills no longer in the package (renamed/removed),
340
+ // so installed clients don't keep a broken slash command.
341
+ for (const row of pruneStaleLinks({ target: target.dir, skills, dryRun })) {
342
+ summary.push(row);
343
+ }
286
344
  }
287
345
 
288
346
  const prefix = dryRun ? '[dry-run] ' : '';
@@ -313,7 +371,6 @@ function commandInstall({ targets: extraTargets, dryRun }) {
313
371
  }
314
372
 
315
373
  function commandUninstall({ targets: extraTargets, dryRun }) {
316
- const skills = listAvailableSkills();
317
374
  const targets = detectTargets(extraTargets);
318
375
  const prefix = dryRun ? '[dry-run] ' : '';
319
376
  console.log(`${prefix}@vruum/skills v${VERSION} uninstall`);
@@ -322,18 +379,21 @@ function commandUninstall({ targets: extraTargets, dryRun }) {
322
379
  console.error('No AI harness skill directories detected.');
323
380
  }
324
381
 
325
- // A symlink is "ours" if it points into ~/.vruum/skills/. We accept any
326
- // target there (not just the current skill name) so stale links from
327
- // renamed skills still get cleaned up.
328
- const isOurLink = (linkTarget) => {
329
- const resolved = path.resolve(linkTarget);
330
- return resolved.startsWith(VRUUM_SKILLS + path.sep) || resolved === VRUUM_SKILLS;
331
- };
332
-
382
+ // Scan each target dir and remove every symlink we own not just the
383
+ // current skill names so stale links left by renamed/removed skills are
384
+ // cleaned up too. Foreign symlinks and non-symlinks are left untouched
385
+ // (shared `isOurLink` ownership predicate).
333
386
  for (const target of targets) {
334
387
  console.log(`${prefix}target: ${target.dir}`);
335
- for (const skillName of skills) {
336
- const dst = path.join(target.dir, skillName);
388
+ let entries;
389
+ try {
390
+ entries = fs.readdirSync(target.dir);
391
+ } catch (err) {
392
+ if (err.code === 'ENOENT') continue;
393
+ throw err;
394
+ }
395
+ for (const entry of entries) {
396
+ const dst = path.join(target.dir, entry);
337
397
  let existing = null;
338
398
  try {
339
399
  const lstat = fs.lstatSync(dst);
@@ -346,19 +406,14 @@ function commandUninstall({ targets: extraTargets, dryRun }) {
346
406
  if (err.code !== 'ENOENT') throw err;
347
407
  }
348
408
 
349
- if (!existing) {
350
- console.log(` ${prefix}not-installed ${skillName}`);
351
- continue;
352
- }
353
- if (existing.kind !== 'symlink' || !isOurLink(existing.target)) {
354
- console.log(` ${prefix}skipped ${skillName} [not our symlink]`);
355
- continue;
409
+ if (!existing || existing.kind !== 'symlink' || !isOurLink(dst, existing.target)) {
410
+ continue; // not our symlink — leave it
356
411
  }
357
412
  if (dryRun) {
358
- console.log(` ${prefix}would-remove ${skillName}`);
413
+ console.log(` ${prefix}would-remove ${entry}`);
359
414
  } else {
360
415
  fs.unlinkSync(dst);
361
- console.log(` ${prefix}removed ${skillName}`);
416
+ console.log(` ${prefix}removed ${entry}`);
362
417
  }
363
418
  }
364
419
  }
@@ -461,4 +516,15 @@ function main() {
461
516
  }
462
517
  }
463
518
 
464
- main();
519
+ if (require.main === module) main();
520
+
521
+ module.exports = {
522
+ parseArgs,
523
+ listAvailableSkills,
524
+ linkSkill,
525
+ isOurLink,
526
+ pruneStaleLinks,
527
+ commandInstall,
528
+ commandUninstall,
529
+ commandList,
530
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vruum/skills",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Vruum AI skills for Claude Code, Claude Desktop, Codex CLI, and any AI assistant with a skill directory. Slash commands for outreach triage, engagement triage, pipeline filling, prospect enrichment, and reply diagnosis. Pairs with the Vruum MCP server at https://api.vruum.ai/mcp.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,6 +26,9 @@
26
26
  "engines": {
27
27
  "node": ">=18"
28
28
  },
29
+ "scripts": {
30
+ "test": "node --test __tests__/*.test.js"
31
+ },
29
32
  "keywords": [
30
33
  "vruum",
31
34
  "mcp",
@@ -36,5 +39,5 @@
36
39
  "outreach",
37
40
  "gtm"
38
41
  ],
39
- "contentHash": "4bcdec8d3e471c8e08561b920faafca0c170aa2966ff5683576545aa2723b2f1"
42
+ "contentHash": "753b0d94ad80acaf1986cadaa6502df2a6b6f32a913ec9b39349922331378345"
40
43
  }