@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.
- package/install.js +87 -21
- 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
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
|
|
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
|
-
|
|
336
|
-
|
|
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
|
-
|
|
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 ${
|
|
413
|
+
console.log(` ${prefix}would-remove ${entry}`);
|
|
359
414
|
} else {
|
|
360
415
|
fs.unlinkSync(dst);
|
|
361
|
-
console.log(` ${prefix}removed ${
|
|
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.
|
|
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": "
|
|
42
|
+
"contentHash": "753b0d94ad80acaf1986cadaa6502df2a6b6f32a913ec9b39349922331378345"
|
|
40
43
|
}
|