@vruum/skills 0.2.0 → 0.4.0
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/bin/vruum-skills-update-check +120 -0
- package/install.js +142 -13
- package/package.json +3 -2
- package/skills/vruum-skills-upgrade/SKILL.md +76 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# vruum-skills-update-check — periodic version check for the @vruum/skills
|
|
3
|
+
# npm package. Ported from gstack's gstack-update-check, simplified.
|
|
4
|
+
#
|
|
5
|
+
# Output (one line, or nothing):
|
|
6
|
+
# JUST_UPGRADED <old> <new> — marker found from a recent upgrade
|
|
7
|
+
# UPGRADE_AVAILABLE <old> <new> — npm registry reports a newer version
|
|
8
|
+
# (nothing) — up to date, snoozed, or check skipped
|
|
9
|
+
#
|
|
10
|
+
# Env overrides (for testing):
|
|
11
|
+
# VRUUM_STATE_DIR — override ~/.vruum state directory
|
|
12
|
+
# VRUUM_REMOTE_URL — override npm registry URL
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
STATE_DIR="${VRUUM_STATE_DIR:-$HOME/.vruum}"
|
|
16
|
+
VERSION_FILE="$STATE_DIR/VERSION"
|
|
17
|
+
CACHE_FILE="$STATE_DIR/last-update-check"
|
|
18
|
+
MARKER_FILE="$STATE_DIR/just-upgraded-from"
|
|
19
|
+
SNOOZE_FILE="$STATE_DIR/update-snoozed"
|
|
20
|
+
CONFIG_FILE="$STATE_DIR/config.yaml"
|
|
21
|
+
REMOTE_URL="${VRUUM_REMOTE_URL:-https://registry.npmjs.org/@vruum/skills/latest}"
|
|
22
|
+
|
|
23
|
+
# Force flag busts cache + snooze (used by /vruum-skills-upgrade directly)
|
|
24
|
+
if [ "${1:-}" = "--force" ]; then
|
|
25
|
+
rm -f "$CACHE_FILE" "$SNOOZE_FILE"
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Step 0: if config says updates disabled, bail.
|
|
29
|
+
if [ -f "$CONFIG_FILE" ] && grep -qE '^[[:space:]]*update_check[[:space:]]*:[[:space:]]*false' "$CONFIG_FILE"; then
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Step 1: local version (written by install.js at install time)
|
|
34
|
+
[ -f "$VERSION_FILE" ] || exit 0
|
|
35
|
+
LOCAL="$(tr -d '[:space:]' < "$VERSION_FILE")"
|
|
36
|
+
[ -n "$LOCAL" ] || exit 0
|
|
37
|
+
|
|
38
|
+
# Step 2: just-upgraded marker — emit once, clear snooze, fall through to check
|
|
39
|
+
# remote in case another release landed since the upgrade.
|
|
40
|
+
if [ -f "$MARKER_FILE" ]; then
|
|
41
|
+
OLD="$(tr -d '[:space:]' < "$MARKER_FILE" 2>/dev/null || true)"
|
|
42
|
+
rm -f "$MARKER_FILE" "$SNOOZE_FILE"
|
|
43
|
+
[ -n "$OLD" ] && echo "JUST_UPGRADED $OLD $LOCAL"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Step 3: snooze helper — returns 0 if snoozed (stay quiet), 1 if not.
|
|
47
|
+
# Snooze file: "<version> <level> <epoch>". Levels: 1=24h, 2=48h, 3+=7d.
|
|
48
|
+
check_snooze() {
|
|
49
|
+
local remote_ver="$1"
|
|
50
|
+
[ -f "$SNOOZE_FILE" ] || return 1
|
|
51
|
+
local sv sl se
|
|
52
|
+
sv="$(awk '{print $1}' "$SNOOZE_FILE" 2>/dev/null || true)"
|
|
53
|
+
sl="$(awk '{print $2}' "$SNOOZE_FILE" 2>/dev/null || true)"
|
|
54
|
+
se="$(awk '{print $3}' "$SNOOZE_FILE" 2>/dev/null || true)"
|
|
55
|
+
[ -n "$sv" ] && [ -n "$sl" ] && [ -n "$se" ] || return 1
|
|
56
|
+
case "$sl" in *[!0-9]*) return 1 ;; esac
|
|
57
|
+
case "$se" in *[!0-9]*) return 1 ;; esac
|
|
58
|
+
# New version drop clears the snooze.
|
|
59
|
+
[ "$sv" = "$remote_ver" ] || return 1
|
|
60
|
+
local dur
|
|
61
|
+
case "$sl" in
|
|
62
|
+
1) dur=86400 ;;
|
|
63
|
+
2) dur=172800 ;;
|
|
64
|
+
*) dur=604800 ;;
|
|
65
|
+
esac
|
|
66
|
+
local now expires
|
|
67
|
+
now="$(date +%s)"
|
|
68
|
+
expires=$(( se + dur ))
|
|
69
|
+
[ "$now" -lt "$expires" ]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Step 4: cache — UP_TO_DATE 60 min, UPGRADE_AVAILABLE 12 hours.
|
|
73
|
+
if [ -f "$CACHE_FILE" ]; then
|
|
74
|
+
CACHED="$(cat "$CACHE_FILE" 2>/dev/null || true)"
|
|
75
|
+
case "$CACHED" in
|
|
76
|
+
UP_TO_DATE*) TTL=60 ;;
|
|
77
|
+
UPGRADE_AVAILABLE*) TTL=720 ;;
|
|
78
|
+
*) TTL=0 ;;
|
|
79
|
+
esac
|
|
80
|
+
STALE=$(find "$CACHE_FILE" -mmin +"$TTL" 2>/dev/null || true)
|
|
81
|
+
if [ -z "$STALE" ] && [ "$TTL" -gt 0 ]; then
|
|
82
|
+
case "$CACHED" in
|
|
83
|
+
UP_TO_DATE*)
|
|
84
|
+
CV="$(echo "$CACHED" | awk '{print $2}')"
|
|
85
|
+
[ "$CV" = "$LOCAL" ] && exit 0
|
|
86
|
+
;;
|
|
87
|
+
UPGRADE_AVAILABLE*)
|
|
88
|
+
CO="$(echo "$CACHED" | awk '{print $2}')"
|
|
89
|
+
if [ "$CO" = "$LOCAL" ]; then
|
|
90
|
+
CN="$(echo "$CACHED" | awk '{print $3}')"
|
|
91
|
+
check_snooze "$CN" && exit 0
|
|
92
|
+
echo "$CACHED"
|
|
93
|
+
exit 0
|
|
94
|
+
fi
|
|
95
|
+
;;
|
|
96
|
+
esac
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# Step 5: slow path — fetch latest from npm registry.
|
|
101
|
+
mkdir -p "$STATE_DIR"
|
|
102
|
+
RESP="$(curl -fsSL --max-time 5 "$REMOTE_URL" 2>/dev/null || true)"
|
|
103
|
+
# Extract "version":"X.Y.Z" without jq.
|
|
104
|
+
REMOTE="$(echo "$RESP" | grep -oE '"version"[[:space:]]*:[[:space:]]*"[^"]+"' | head -1 | sed -E 's/.*"([^"]+)"$/\1/')"
|
|
105
|
+
REMOTE="$(echo "$REMOTE" | tr -d '[:space:]')"
|
|
106
|
+
|
|
107
|
+
# Reject anything that doesn't look like a semver number.
|
|
108
|
+
if ! echo "$REMOTE" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
|
|
109
|
+
echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
|
|
110
|
+
exit 0
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
if [ "$LOCAL" = "$REMOTE" ]; then
|
|
114
|
+
echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
|
|
115
|
+
exit 0
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE" > "$CACHE_FILE"
|
|
119
|
+
check_snooze "$REMOTE" && exit 0
|
|
120
|
+
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE"
|
package/install.js
CHANGED
|
@@ -27,7 +27,54 @@ const os = require('node:os');
|
|
|
27
27
|
|
|
28
28
|
const VERSION = require('./package.json').version;
|
|
29
29
|
const PACKAGE_ROOT = __dirname;
|
|
30
|
-
const
|
|
30
|
+
const PKG_SKILLS = path.join(PACKAGE_ROOT, 'skills');
|
|
31
|
+
const PKG_AGENTS = path.join(PACKAGE_ROOT, 'agents');
|
|
32
|
+
|
|
33
|
+
// Installer-owned stable path. Mirrors gstack's ~/.claude/skills/gstack/
|
|
34
|
+
// approach: package contents get copied here so symlinks target a path that
|
|
35
|
+
// never moves, even when npm/npx wipes the original package directory.
|
|
36
|
+
const VRUUM_ROOT = path.join(os.homedir(), '.vruum');
|
|
37
|
+
const VRUUM_SKILLS = path.join(VRUUM_ROOT, 'skills');
|
|
38
|
+
const VRUUM_AGENTS = path.join(VRUUM_ROOT, 'agents');
|
|
39
|
+
const VRUUM_BIN = path.join(VRUUM_ROOT, 'bin');
|
|
40
|
+
const VRUUM_VERSION_FILE = path.join(VRUUM_ROOT, 'VERSION');
|
|
41
|
+
const PKG_BIN = path.join(PACKAGE_ROOT, 'bin');
|
|
42
|
+
|
|
43
|
+
// Prelude prepended to every SKILL.md as it lands in ~/.vruum/skills/. Runs
|
|
44
|
+
// an update check (1h cache), then gives Claude natural-language instructions
|
|
45
|
+
// for how to react. Mirrors gstack's preamble pattern but injected at install
|
|
46
|
+
// time rather than via a template-gen step.
|
|
47
|
+
const SKILL_PRELUDE = `## Preamble (run first)
|
|
48
|
+
|
|
49
|
+
\`\`\`bash
|
|
50
|
+
_UPD=$(~/.vruum/bin/vruum-skills-update-check 2>/dev/null || true)
|
|
51
|
+
[ -n "$_UPD" ] && echo "$_UPD" || true
|
|
52
|
+
\`\`\`
|
|
53
|
+
|
|
54
|
+
If output shows \`UPGRADE_AVAILABLE <old> <new>\`: read \`~/.vruum/skills/vruum-skills-upgrade/SKILL.md\` and follow the inline upgrade flow. If \`~/.vruum/config.yaml\` has \`auto_upgrade: true\`, run the upgrade silently; otherwise AskUserQuestion with four options — (A) upgrade now, (B) upgrade with changelog preview, (C) snooze, (D) skip this session. If user picks snooze, bump the snooze level in \`~/.vruum/update-snoozed\` (format: \`<new_version> <level> <unix_epoch>\`; level 1 = 24h, 2 = 48h, 3+ = 7d). Then continue with the skill.
|
|
55
|
+
|
|
56
|
+
If output shows \`JUST_UPGRADED <from> <to>\`: tell the user "Running @vruum/skills v{to} (just updated!)" in one line, then continue.
|
|
57
|
+
|
|
58
|
+
Otherwise proceed silently with the skill below.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
function injectPrelude(skillDir) {
|
|
64
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
65
|
+
if (!fs.existsSync(skillFile)) return;
|
|
66
|
+
const raw = fs.readFileSync(skillFile, 'utf8');
|
|
67
|
+
// Find end of YAML frontmatter. If missing, inject at top.
|
|
68
|
+
let insertAt = 0;
|
|
69
|
+
if (raw.startsWith('---\n')) {
|
|
70
|
+
const end = raw.indexOf('\n---\n', 4);
|
|
71
|
+
if (end !== -1) insertAt = end + 5;
|
|
72
|
+
}
|
|
73
|
+
const before = raw.slice(0, insertAt);
|
|
74
|
+
const after = raw.slice(insertAt);
|
|
75
|
+
const patched = before + '\n' + SKILL_PRELUDE + '\n' + after.replace(/^\n+/, '');
|
|
76
|
+
fs.writeFileSync(skillFile, patched);
|
|
77
|
+
}
|
|
31
78
|
|
|
32
79
|
// Known harness skill directories. Add a target here when a new harness
|
|
33
80
|
// lands on a stable skill-dir convention.
|
|
@@ -90,13 +137,56 @@ other assistants that don't yet support MCP prompts.`);
|
|
|
90
137
|
}
|
|
91
138
|
|
|
92
139
|
function listAvailableSkills() {
|
|
93
|
-
if (!fs.existsSync(
|
|
140
|
+
if (!fs.existsSync(PKG_SKILLS)) return [];
|
|
94
141
|
return fs
|
|
95
|
-
.readdirSync(
|
|
142
|
+
.readdirSync(PKG_SKILLS, { withFileTypes: true })
|
|
96
143
|
.filter((entry) => entry.isDirectory())
|
|
97
144
|
.map((entry) => entry.name);
|
|
98
145
|
}
|
|
99
146
|
|
|
147
|
+
function syncVruumRoot({ dryRun }) {
|
|
148
|
+
if (dryRun) {
|
|
149
|
+
const rows = [
|
|
150
|
+
`would sync ${PKG_SKILLS} -> ${VRUUM_SKILLS} (with auto-update prelude)`,
|
|
151
|
+
];
|
|
152
|
+
if (fs.existsSync(PKG_AGENTS)) rows.push(`would sync ${PKG_AGENTS} -> ${VRUUM_AGENTS}`);
|
|
153
|
+
if (fs.existsSync(PKG_BIN)) rows.push(`would sync ${PKG_BIN} -> ${VRUUM_BIN}`);
|
|
154
|
+
rows.push(`would write ${VRUUM_VERSION_FILE} = ${VERSION}`);
|
|
155
|
+
return rows;
|
|
156
|
+
}
|
|
157
|
+
fs.mkdirSync(VRUUM_ROOT, { recursive: true });
|
|
158
|
+
|
|
159
|
+
// Skills — copy then inject prelude in-place on the ~/.vruum/ copy.
|
|
160
|
+
fs.rmSync(VRUUM_SKILLS, { recursive: true, force: true });
|
|
161
|
+
fs.cpSync(PKG_SKILLS, VRUUM_SKILLS, { recursive: true });
|
|
162
|
+
for (const skillName of listAvailableSkills()) {
|
|
163
|
+
injectPrelude(path.join(VRUUM_SKILLS, skillName));
|
|
164
|
+
}
|
|
165
|
+
const rows = [`synced ${VRUUM_SKILLS} (prelude injected)`];
|
|
166
|
+
|
|
167
|
+
if (fs.existsSync(PKG_AGENTS)) {
|
|
168
|
+
fs.rmSync(VRUUM_AGENTS, { recursive: true, force: true });
|
|
169
|
+
fs.cpSync(PKG_AGENTS, VRUUM_AGENTS, { recursive: true });
|
|
170
|
+
rows.push(`synced ${VRUUM_AGENTS}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Bin scripts — update-check needs to land at a stable path and be exec.
|
|
174
|
+
if (fs.existsSync(PKG_BIN)) {
|
|
175
|
+
fs.rmSync(VRUUM_BIN, { recursive: true, force: true });
|
|
176
|
+
fs.cpSync(PKG_BIN, VRUUM_BIN, { recursive: true });
|
|
177
|
+
for (const entry of fs.readdirSync(VRUUM_BIN)) {
|
|
178
|
+
fs.chmodSync(path.join(VRUUM_BIN, entry), 0o755);
|
|
179
|
+
}
|
|
180
|
+
rows.push(`synced ${VRUUM_BIN}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// VERSION file — read by vruum-skills-update-check as the local version.
|
|
184
|
+
fs.writeFileSync(VRUUM_VERSION_FILE, VERSION + '\n');
|
|
185
|
+
rows.push(`wrote ${VRUUM_VERSION_FILE} = ${VERSION}`);
|
|
186
|
+
|
|
187
|
+
return rows;
|
|
188
|
+
}
|
|
189
|
+
|
|
100
190
|
function ensureTargetDir(target, dryRun) {
|
|
101
191
|
if (fs.existsSync(target)) return { created: false };
|
|
102
192
|
if (dryRun) return { created: 'would' };
|
|
@@ -159,7 +249,7 @@ function commandInstall({ targets: extraTargets, dryRun }) {
|
|
|
159
249
|
const skills = listAvailableSkills();
|
|
160
250
|
if (skills.length === 0) {
|
|
161
251
|
throw new Error(
|
|
162
|
-
`No skills found under ${
|
|
252
|
+
`No skills found under ${PKG_SKILLS}. This looks like a broken package.`
|
|
163
253
|
);
|
|
164
254
|
}
|
|
165
255
|
const targets = detectTargets(extraTargets);
|
|
@@ -173,6 +263,8 @@ function commandInstall({ targets: extraTargets, dryRun }) {
|
|
|
173
263
|
process.exit(1);
|
|
174
264
|
}
|
|
175
265
|
|
|
266
|
+
const syncRows = syncVruumRoot({ dryRun });
|
|
267
|
+
|
|
176
268
|
const summary = [];
|
|
177
269
|
for (const target of targets) {
|
|
178
270
|
const { created } = ensureTargetDir(target.dir, dryRun);
|
|
@@ -182,14 +274,18 @@ function commandInstall({ targets: extraTargets, dryRun }) {
|
|
|
182
274
|
summary.push({ target: target.dir, detail: 'created target dir' });
|
|
183
275
|
}
|
|
184
276
|
for (const skillName of skills) {
|
|
185
|
-
const srcAbs = path.join(
|
|
277
|
+
const srcAbs = path.join(VRUUM_SKILLS, skillName);
|
|
186
278
|
summary.push(linkSkill({ name: skillName, srcAbs, target: target.dir, dryRun }));
|
|
187
279
|
}
|
|
188
280
|
}
|
|
189
281
|
|
|
190
282
|
const prefix = dryRun ? '[dry-run] ' : '';
|
|
191
283
|
console.log(`${prefix}@vruum/skills v${VERSION}`);
|
|
192
|
-
console.log(`${prefix}
|
|
284
|
+
console.log(`${prefix}package source: ${PACKAGE_ROOT}`);
|
|
285
|
+
console.log(`${prefix}stable root: ${VRUUM_ROOT}`);
|
|
286
|
+
for (const row of syncRows) {
|
|
287
|
+
console.log(` ${prefix}${row}`);
|
|
288
|
+
}
|
|
193
289
|
for (const target of targets) {
|
|
194
290
|
console.log(`${prefix}target: ${target.dir}${target.autoDetected ? ' (auto)' : ''}`);
|
|
195
291
|
}
|
|
@@ -213,19 +309,25 @@ function commandInstall({ targets: extraTargets, dryRun }) {
|
|
|
213
309
|
function commandUninstall({ targets: extraTargets, dryRun }) {
|
|
214
310
|
const skills = listAvailableSkills();
|
|
215
311
|
const targets = detectTargets(extraTargets);
|
|
312
|
+
const prefix = dryRun ? '[dry-run] ' : '';
|
|
313
|
+
console.log(`${prefix}@vruum/skills v${VERSION} uninstall`);
|
|
314
|
+
|
|
216
315
|
if (targets.length === 0) {
|
|
217
|
-
console.error('No AI harness skill directories detected.
|
|
218
|
-
return;
|
|
316
|
+
console.error('No AI harness skill directories detected.');
|
|
219
317
|
}
|
|
220
318
|
|
|
221
|
-
|
|
222
|
-
|
|
319
|
+
// A symlink is "ours" if it points into ~/.vruum/skills/. We accept any
|
|
320
|
+
// target there (not just the current skill name) so stale links from
|
|
321
|
+
// renamed skills still get cleaned up.
|
|
322
|
+
const isOurLink = (linkTarget) => {
|
|
323
|
+
const resolved = path.resolve(linkTarget);
|
|
324
|
+
return resolved.startsWith(VRUUM_SKILLS + path.sep) || resolved === VRUUM_SKILLS;
|
|
325
|
+
};
|
|
223
326
|
|
|
224
327
|
for (const target of targets) {
|
|
225
328
|
console.log(`${prefix}target: ${target.dir}`);
|
|
226
329
|
for (const skillName of skills) {
|
|
227
330
|
const dst = path.join(target.dir, skillName);
|
|
228
|
-
const srcAbs = path.join(SKILLS_SRC, skillName);
|
|
229
331
|
let existing = null;
|
|
230
332
|
try {
|
|
231
333
|
const lstat = fs.lstatSync(dst);
|
|
@@ -242,7 +344,7 @@ function commandUninstall({ targets: extraTargets, dryRun }) {
|
|
|
242
344
|
console.log(` ${prefix}not-installed ${skillName}`);
|
|
243
345
|
continue;
|
|
244
346
|
}
|
|
245
|
-
if (existing.kind !== 'symlink' || existing.target
|
|
347
|
+
if (existing.kind !== 'symlink' || !isOurLink(existing.target)) {
|
|
246
348
|
console.log(` ${prefix}skipped ${skillName} [not our symlink]`);
|
|
247
349
|
continue;
|
|
248
350
|
}
|
|
@@ -254,6 +356,33 @@ function commandUninstall({ targets: extraTargets, dryRun }) {
|
|
|
254
356
|
}
|
|
255
357
|
}
|
|
256
358
|
}
|
|
359
|
+
|
|
360
|
+
// Only clean up our own subdirectories — ~/.vruum/ is a shared state dir
|
|
361
|
+
// (e.g. the .agents/ vruum-update-check keeps config.yaml + snooze state
|
|
362
|
+
// there, and both installers share that config).
|
|
363
|
+
for (const dir of [VRUUM_SKILLS, VRUUM_AGENTS, VRUUM_BIN]) {
|
|
364
|
+
if (!fs.existsSync(dir)) continue;
|
|
365
|
+
if (dryRun) {
|
|
366
|
+
console.log(`${prefix}would remove ${dir}`);
|
|
367
|
+
} else {
|
|
368
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
369
|
+
console.log(`${prefix}removed ${dir}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (fs.existsSync(VRUUM_VERSION_FILE)) {
|
|
373
|
+
if (dryRun) {
|
|
374
|
+
console.log(`${prefix}would remove ${VRUUM_VERSION_FILE}`);
|
|
375
|
+
} else {
|
|
376
|
+
fs.unlinkSync(VRUUM_VERSION_FILE);
|
|
377
|
+
console.log(`${prefix}removed ${VRUUM_VERSION_FILE}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Best-effort rmdir the root if now empty; ignore ENOTEMPTY.
|
|
381
|
+
if (!dryRun && fs.existsSync(VRUUM_ROOT)) {
|
|
382
|
+
try { fs.rmdirSync(VRUUM_ROOT); } catch (err) {
|
|
383
|
+
if (err.code !== 'ENOTEMPTY' && err.code !== 'EEXIST') throw err;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
257
386
|
}
|
|
258
387
|
|
|
259
388
|
function commandList({ targets: extraTargets }) {
|
|
@@ -273,7 +402,7 @@ function commandList({ targets: extraTargets }) {
|
|
|
273
402
|
}
|
|
274
403
|
for (const skillName of skills) {
|
|
275
404
|
const dst = path.join(target.dir, skillName);
|
|
276
|
-
const srcAbs = path.join(
|
|
405
|
+
const srcAbs = path.join(VRUUM_SKILLS, skillName);
|
|
277
406
|
try {
|
|
278
407
|
const lstat = fs.lstatSync(dst);
|
|
279
408
|
if (lstat.isSymbolicLink()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vruum/skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Vruum AI skills for Claude Code, Codex CLI, and any AI assistant with a skill directory. Pairs with the Vruum MCP server at https://api.vruum.ai/mcp.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"LICENSE",
|
|
21
21
|
".mcp.json",
|
|
22
22
|
"skills/",
|
|
23
|
-
"agents/"
|
|
23
|
+
"agents/",
|
|
24
|
+
"bin/"
|
|
24
25
|
],
|
|
25
26
|
"engines": {
|
|
26
27
|
"node": ">=18"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vruum-skills-upgrade
|
|
3
|
+
description: "Upgrade @vruum/skills to the latest npm version and re-sync ~/.vruum/. Use when: upgrade vruum skills, update vruum, pull latest vruum skills, or when the preamble reports UPGRADE_AVAILABLE."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /vruum-skills-upgrade
|
|
7
|
+
|
|
8
|
+
Upgrade the `@vruum/skills` npm package + re-sync `~/.vruum/` + relink all harness skill dirs.
|
|
9
|
+
|
|
10
|
+
## Inline upgrade flow (called from preamble)
|
|
11
|
+
|
|
12
|
+
If the calling skill's preamble reported `UPGRADE_AVAILABLE <old> <new>`, follow this flow. It runs inline — when done, the original skill continues.
|
|
13
|
+
|
|
14
|
+
### Step 1: decide whether to auto-upgrade
|
|
15
|
+
|
|
16
|
+
Read `~/.vruum/config.yaml`. If it contains `auto_upgrade: true`, skip to Step 3 (silent upgrade). Otherwise go to Step 2.
|
|
17
|
+
|
|
18
|
+
### Step 2: ask the user
|
|
19
|
+
|
|
20
|
+
Use AskUserQuestion with these four options:
|
|
21
|
+
|
|
22
|
+
- **A) Upgrade now** — run the upgrade, return a one-line confirmation, then continue with the original skill.
|
|
23
|
+
- **B) Show changelog first** — fetch `https://raw.githubusercontent.com/vruum-gtm/skills/main/CHANGELOG.md`, show the section for the new version, then re-ask A/C/D.
|
|
24
|
+
- **C) Snooze** — don't upgrade this session. Bump the snooze level:
|
|
25
|
+
```bash
|
|
26
|
+
# Snooze format: "<version> <level> <epoch>". Level 1=24h, 2=48h, 3+=7d.
|
|
27
|
+
OLD_LEVEL=$(awk '{print $2}' ~/.vruum/update-snoozed 2>/dev/null || echo 0)
|
|
28
|
+
NEW_LEVEL=$((OLD_LEVEL + 1))
|
|
29
|
+
[ $NEW_LEVEL -gt 3 ] && NEW_LEVEL=3
|
|
30
|
+
echo "<new> $NEW_LEVEL $(date +%s)" > ~/.vruum/update-snoozed
|
|
31
|
+
```
|
|
32
|
+
Replace `<new>` with the version the preamble reported.
|
|
33
|
+
- **D) Skip this session** — do nothing, don't write snooze. Next session will re-prompt.
|
|
34
|
+
|
|
35
|
+
### Step 3: run the upgrade
|
|
36
|
+
|
|
37
|
+
Detect how `@vruum/skills` was installed (global vs npx) and pick the right command:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# If installed globally (vruum-skills is on PATH):
|
|
41
|
+
if command -v vruum-skills >/dev/null 2>&1; then
|
|
42
|
+
OLD=$(cat ~/.vruum/VERSION 2>/dev/null || echo "unknown")
|
|
43
|
+
npm install -g @vruum/skills@latest
|
|
44
|
+
vruum-skills install
|
|
45
|
+
NEW=$(cat ~/.vruum/VERSION 2>/dev/null || echo "unknown")
|
|
46
|
+
else
|
|
47
|
+
# npx-only install — refresh the npx cache and re-run.
|
|
48
|
+
OLD=$(cat ~/.vruum/VERSION 2>/dev/null || echo "unknown")
|
|
49
|
+
npx --yes @vruum/skills@latest install
|
|
50
|
+
NEW=$(cat ~/.vruum/VERSION 2>/dev/null || echo "unknown")
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Write the just-upgraded marker so the next preamble greets with "just updated!".
|
|
54
|
+
echo "$OLD" > ~/.vruum/just-upgraded-from
|
|
55
|
+
rm -f ~/.vruum/last-update-check ~/.vruum/update-snoozed
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Step 4: report
|
|
59
|
+
|
|
60
|
+
One line: `upgraded @vruum/skills $OLD → $NEW`. Then continue with the original skill.
|
|
61
|
+
|
|
62
|
+
## Standalone mode (user invoked `/vruum-skills-upgrade` directly)
|
|
63
|
+
|
|
64
|
+
Force a fresh check first, then run the flow above starting from Step 2:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
~/.vruum/bin/vruum-skills-update-check --force
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
If the forced check returns nothing, report `already on latest (v$(cat ~/.vruum/VERSION))` and exit.
|
|
71
|
+
|
|
72
|
+
## When something goes wrong
|
|
73
|
+
|
|
74
|
+
- `npm install` fails with permissions → tell the user to re-run with `sudo` or fix their npm prefix. Don't auto-sudo.
|
|
75
|
+
- `vruum-skills install` reports skipped skills → surface the conflict message verbatim; user needs to remove conflicting files.
|
|
76
|
+
- Network failure on registry lookup → report `upgrade check failed, try again later` and continue with the original skill (don't block work on a transient fetch error).
|