happyskills 1.16.0 → 1.16.1
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/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.16.1] - 2026-07-02
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Make the passive daily skill-update check watch **every** installed skill, not just top-level (`__root__`) ones. Bundled family satellites installed as transitive dependencies (e.g. `happyskills-design`, `happyskills-help`) are recorded in the lock as `requested_by: [<parent>]`, so the previous `__root__`-only filter silently never nudged when they fell behind the registry — even though the check already spans both the project-local and global locks. Candidate selection now mirrors `happyskills check` (every installed skill), so an outdated bundled satellite is surfaced both in the stderr nudge and in the `SKILLS_UPDATE_AVAILABLE` envelope warning.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- The "installed skills behind the registry" nudge now names the outdated skills in its suggested command (`npx happyskills update <owner/name> …`, grouped by scope so global skills get `-g`) instead of suggesting `update --all` — which only re-checks top-level skills and therefore could never refresh an outdated bundled satellite.
|
|
19
|
+
|
|
10
20
|
## [1.16.0] - 2026-07-01
|
|
11
21
|
|
|
12
22
|
### Changed
|
package/package.json
CHANGED
package/src/ui/envelope.test.js
CHANGED
|
@@ -34,7 +34,7 @@ test('drift set → appends a SKILLS_UPDATE_AVAILABLE warning without touching d
|
|
|
34
34
|
assert.strictEqual(env.warnings[0].code, 'SKILLS_UPDATE_AVAILABLE')
|
|
35
35
|
assert.match(env.warnings[0].message, /2 installed skill\(s\) behind the registry/)
|
|
36
36
|
assert.match(env.warnings[0].message, /acme\/x, acme\/y/)
|
|
37
|
-
assert.match(env.warnings[0].message, /npx happyskills update
|
|
37
|
+
assert.match(env.warnings[0].message, /npx happyskills update acme\/x acme\/y/)
|
|
38
38
|
})
|
|
39
39
|
})
|
|
40
40
|
|
|
@@ -44,6 +44,16 @@ const write_scope = (key, outdated) => {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// Selects which locked skills to check for registry drift: EVERY installed skill,
|
|
48
|
+
// not just __root__ entries. A bundled family satellite (e.g. happyskills-help) is
|
|
49
|
+
// pulled in as a transitive dependency of core — its lock entry reads
|
|
50
|
+
// requested_by:['happyskillsai/happyskills'], NOT ['__root__'] — so a __root__-only
|
|
51
|
+
// filter silently never nudged about it even when it fell behind. Mirrors the
|
|
52
|
+
// "check every installed skill" enumeration in commands/check.js. A null value is a
|
|
53
|
+
// removed-skill tombstone — skip it. Extracted as a pure function for unit testing.
|
|
54
|
+
const select_drift_candidates = (skills) =>
|
|
55
|
+
Object.entries(skills || {}).filter(([, d]) => d && typeof d === 'object')
|
|
56
|
+
|
|
47
57
|
// Background refresh — NOT awaited, all errors swallowed. Reads the lock directly
|
|
48
58
|
// (NOT via lock/reader.read_lock, which prints a stderr warning on version mismatch
|
|
49
59
|
// — unwanted noise from a silent background task), batch-checks root skills against
|
|
@@ -60,8 +70,7 @@ const refresh_cache = (key, opts) => {
|
|
|
60
70
|
const lock_path = lock_file_path(lock_root(opts.is_global, opts.project_root))
|
|
61
71
|
const [, lock_data] = await read_json(lock_path)
|
|
62
72
|
const skills = (lock_data && lock_data.skills) || {}
|
|
63
|
-
const candidates =
|
|
64
|
-
.filter(([, d]) => d && Array.isArray(d.requested_by) && d.requested_by.includes('__root__'))
|
|
73
|
+
const candidates = select_drift_candidates(skills)
|
|
65
74
|
|
|
66
75
|
if (candidates.length === 0) { write_scope(key, []); return }
|
|
67
76
|
|
|
@@ -140,11 +149,17 @@ const format_skill_drift = (verdict) => {
|
|
|
140
149
|
const items = verdict.outdated
|
|
141
150
|
const names = items.slice(0, 3).map(s => s.skill).join(', ')
|
|
142
151
|
const more = items.length > 3 ? `, +${items.length - 3} more` : ''
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
152
|
+
// Name the outdated skills explicitly rather than suggesting `update --all`.
|
|
153
|
+
// `update --all` only re-checks __root__ skills, so it can never refresh a bundled
|
|
154
|
+
// satellite installed as a transitive dependency — but `update <owner/name>` fixes
|
|
155
|
+
// ANY locked skill. Group by scope: a global skill needs -g, and one update run
|
|
156
|
+
// writes a single scope. Items with no scope default to local.
|
|
157
|
+
const local = items.filter(i => i.scope !== 'global').map(i => i.skill)
|
|
158
|
+
const global = items.filter(i => i.scope === 'global').map(i => i.skill)
|
|
159
|
+
const cmds = []
|
|
160
|
+
if (local.length) cmds.push(`npx happyskills update ${local.join(' ')}`)
|
|
161
|
+
if (global.length) cmds.push(`npx happyskills update ${global.join(' ')} -g`)
|
|
162
|
+
return `${items.length} installed skill(s) behind the registry (${names}${more}). Run: ${cmds.join(' && ')}`
|
|
148
163
|
}
|
|
149
164
|
|
|
150
|
-
module.exports = { check_skills_for_update, format_skill_drift, get_cache_path, scope_key }
|
|
165
|
+
module.exports = { check_skills_for_update, format_skill_drift, select_drift_candidates, get_cache_path, scope_key }
|
|
@@ -4,7 +4,7 @@ const fs = require('fs')
|
|
|
4
4
|
const os = require('os')
|
|
5
5
|
const path = require('path')
|
|
6
6
|
|
|
7
|
-
const { check_skills_for_update, format_skill_drift, get_cache_path, scope_key } = require('./skill_update_check')
|
|
7
|
+
const { check_skills_for_update, format_skill_drift, select_drift_candidates, get_cache_path, scope_key } = require('./skill_update_check')
|
|
8
8
|
|
|
9
9
|
const PROJECT_ROOT = '/proj'
|
|
10
10
|
const PKEY = scope_key({ is_global: false, project_root: PROJECT_ROOT })
|
|
@@ -53,7 +53,7 @@ test('project-only drift → verdict flagged local, no -g in the message', () =>
|
|
|
53
53
|
assert.strictEqual(v.outdated.length, 1)
|
|
54
54
|
assert.strictEqual(v.has_local, true)
|
|
55
55
|
assert.strictEqual(v.has_global, false)
|
|
56
|
-
assert.match(format_skill_drift(v), /update
|
|
56
|
+
assert.match(format_skill_drift(v), /Run: npx happyskills update acme\/x$/)
|
|
57
57
|
})
|
|
58
58
|
})
|
|
59
59
|
|
|
@@ -64,7 +64,7 @@ test('global-only drift → verdict flagged global, message uses -g', () => {
|
|
|
64
64
|
assert.ok(v)
|
|
65
65
|
assert.strictEqual(v.has_local, false)
|
|
66
66
|
assert.strictEqual(v.has_global, true)
|
|
67
|
-
assert.match(format_skill_drift(v), /update
|
|
67
|
+
assert.match(format_skill_drift(v), /Run: npx happyskills update happyskillsai\/happyskills -g$/)
|
|
68
68
|
})
|
|
69
69
|
})
|
|
70
70
|
|
|
@@ -75,7 +75,9 @@ test('drift in BOTH scopes → merged, both flags set, message mentions -g for g
|
|
|
75
75
|
assert.strictEqual(v.outdated.length, 2)
|
|
76
76
|
assert.strictEqual(v.has_local, true)
|
|
77
77
|
assert.strictEqual(v.has_global, true)
|
|
78
|
-
|
|
78
|
+
const msg = format_skill_drift(v)
|
|
79
|
+
assert.match(msg, /npx happyskills update acme\/x/)
|
|
80
|
+
assert.match(msg, /npx happyskills update happyskillsai\/happyskills -g/)
|
|
79
81
|
})
|
|
80
82
|
})
|
|
81
83
|
|
|
@@ -116,3 +118,38 @@ test('localhost:0 test API sentinel short-circuits to null', () => {
|
|
|
116
118
|
assert.strictEqual(check_skills_for_update({ project_root: PROJECT_ROOT }), null)
|
|
117
119
|
})
|
|
118
120
|
})
|
|
121
|
+
|
|
122
|
+
// ─── Regression: the passive check must watch the WHOLE installed family ─────────
|
|
123
|
+
// A bundled family satellite (design/publish/sync/search/help) is installed as a
|
|
124
|
+
// transitive dependency of core, so its lock entry is requested_by:[core], NOT
|
|
125
|
+
// [__root__]. A __root__-only enumeration silently never nudged about it even when it
|
|
126
|
+
// fell behind the registry — the gap this suite pins shut. See docs/cli-overview.md § 3.
|
|
127
|
+
|
|
128
|
+
test('select_drift_candidates watches transitive dependencies, not just __root__ skills', () => {
|
|
129
|
+
const skills = {
|
|
130
|
+
'happyskillsai/happyskills': { version: '2.5.4', base_commit: 'aaa', requested_by: ['__root__'] },
|
|
131
|
+
'happyskillsai/happyskills-help': { version: '0.8.2', base_commit: 'bbb', requested_by: ['happyskillsai/happyskills'] },
|
|
132
|
+
}
|
|
133
|
+
const names = select_drift_candidates(skills).map(([n]) => n)
|
|
134
|
+
assert.ok(names.includes('happyskillsai/happyskills-help'),
|
|
135
|
+
'a bundled satellite pulled in as a transitive dependency must still be checked for updates')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('select_drift_candidates keeps __root__ skills and skips null (removed-skill) tombstones', () => {
|
|
139
|
+
const skills = {
|
|
140
|
+
'acme/root': { version: '1.0.0', requested_by: ['__root__'] },
|
|
141
|
+
'acme/removed': null,
|
|
142
|
+
}
|
|
143
|
+
const names = select_drift_candidates(skills).map(([n]) => n)
|
|
144
|
+
assert.ok(names.includes('acme/root'))
|
|
145
|
+
assert.ok(!names.includes('acme/removed'))
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('format_skill_drift names the skills (works for transitive deps), never suggests update --all', () => {
|
|
149
|
+
// `update --all` only re-checks __root__ skills, so it can never refresh a bundled
|
|
150
|
+
// satellite. The remedy must name the skill: `update <owner/name>` fixes ANY locked skill.
|
|
151
|
+
const verdict = { outdated: [{ skill: 'happyskillsai/happyskills-help', installed: '0.8.2', latest: '0.9.0', scope: 'local' }], has_local: true, has_global: false }
|
|
152
|
+
const msg = format_skill_drift(verdict)
|
|
153
|
+
assert.match(msg, /npx happyskills update happyskillsai\/happyskills-help/)
|
|
154
|
+
assert.doesNotMatch(msg, /update --all/)
|
|
155
|
+
})
|