happyskills 0.31.0 → 0.31.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
+ ## [0.31.1] - 2026-04-07
11
+
12
+ ### Fixed
13
+ - Fix `check` and `update --all` ignoring kit dependencies and transitive dependencies — previously only root-level skills were checked, hiding available updates for skills installed via kits or as dependencies of other skills
14
+ - Fix pre-existing test failure in lock reader caused by lazy `require` of `print_warn` breaking under Jest environment teardown
15
+
16
+ ### Added
17
+ - Add `via` field to `check` and `update` JSON output showing which parent skill or kit pulled in each dependency (`null` for directly installed skills)
18
+ - Add "Via" column to `check` table output (only shown when dependencies are present)
19
+
10
20
  ## [0.31.0] - 2026-04-06
11
21
 
12
22
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "0.31.0",
3
+ "version": "0.31.1",
4
4
  "description": "Package manager for AI agent skills",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Nicolas Dao <nic@cloudlesslabs.com> (https://cloudlesslabs.com)",
@@ -1,5 +1,5 @@
1
1
  const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
2
- const { read_lock, get_all_locked_skills } = require('../lock/reader')
2
+ const { read_lock, get_all_locked_skills, get_via } = require('../lock/reader')
3
3
  const repos_api = require('../api/repos')
4
4
  const { print_help, print_table, print_json, print_info, print_success, print_warn, print_hint, code } = require('../ui/output')
5
5
  const { green, yellow, red } = require('../ui/colors')
@@ -44,15 +44,15 @@ const run = (args) => catch_errors('Check failed', async () => {
44
44
 
45
45
  const target_skill = args._[0]
46
46
  const to_check = target_skill
47
- ? entries.filter(([name]) => name === target_skill)
48
- : entries.filter(([, data]) => data.requested_by?.includes('__root__'))
47
+ ? entries.filter(([name, data]) => name === target_skill && data !== null)
48
+ : entries.filter(([, data]) => data !== null)
49
49
 
50
50
  if (to_check.length === 0) {
51
51
  if (args.flags.json) {
52
52
  print_json({ data: { results: [], outdated_count: 0, up_to_date_count: 0 } })
53
53
  return
54
54
  }
55
- print_info(target_skill ? `${target_skill} is not installed.` : 'No directly installed skills found.')
55
+ print_info(target_skill ? `${target_skill} is not installed.` : 'No installed skills found.')
56
56
  return
57
57
  }
58
58
 
@@ -62,25 +62,26 @@ const run = (args) => catch_errors('Check failed', async () => {
62
62
  const results = []
63
63
  if (batch_err) {
64
64
  for (const [name, data] of to_check) {
65
- results.push({ skill: name, installed: data.version, latest: 'error', status: 'error' })
65
+ results.push({ skill: name, installed: data.version, latest: 'error', status: 'error', via: get_via(data) })
66
66
  }
67
67
  } else {
68
68
  for (const [name, data] of to_check) {
69
69
  const info = batch_data?.results?.[name]
70
+ const via = get_via(data)
70
71
  const has_conflicts = (data.conflict_files || []).length > 0
71
72
  if (has_conflicts) {
72
- results.push({ skill: name, installed: data.version, latest: info?.latest_version || '-', status: 'conflicts' })
73
+ results.push({ skill: name, installed: data.version, latest: info?.latest_version || '-', status: 'conflicts', via })
73
74
  } else if (info?.access_denied) {
74
- results.push({ skill: name, installed: data.version, latest: '-', status: 'no-access' })
75
+ results.push({ skill: name, installed: data.version, latest: '-', status: 'no-access', via })
75
76
  } else if (!info || !info.latest_version) {
76
- results.push({ skill: name, installed: data.version, latest: '-', status: 'unknown' })
77
+ results.push({ skill: name, installed: data.version, latest: '-', status: 'unknown', via })
77
78
  } else if (data.base_commit && info.commit && data.base_commit !== info.commit) {
78
- results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'outdated' })
79
+ results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'outdated', via })
79
80
  } else if (!data.base_commit && info.latest_version !== data.version) {
80
81
  // Fallback to version comparison for old lock files without base_commit
81
- results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'outdated' })
82
+ results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'outdated', via })
82
83
  } else {
83
- results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'up-to-date' })
84
+ results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'up-to-date', via })
84
85
  }
85
86
  }
86
87
  }
@@ -102,14 +103,21 @@ const run = (args) => catch_errors('Check failed', async () => {
102
103
  'unknown': (s) => s
103
104
  }
104
105
 
105
- const rows = results.map(r => [
106
- r.skill,
107
- r.installed,
108
- r.latest,
109
- (status_colors[r.status] || ((s) => s))(r.status)
110
- ])
111
-
112
- print_table(['Skill', 'Installed', 'Latest', 'Status'], rows)
106
+ const has_via = results.some(r => r.via)
107
+ const rows = results.map(r => {
108
+ const row = [
109
+ r.skill,
110
+ r.installed,
111
+ r.latest,
112
+ (status_colors[r.status] || ((s) => s))(r.status)
113
+ ]
114
+ if (has_via) row.push(r.via || '-')
115
+ return row
116
+ })
117
+
118
+ const headers = ['Skill', 'Installed', 'Latest', 'Status']
119
+ if (has_via) headers.push('Via')
120
+ print_table(headers, rows)
113
121
 
114
122
  const outdated = results.filter(r => r.status === 'outdated')
115
123
  const conflicts = results.filter(r => r.status === 'conflicts')
@@ -121,7 +129,7 @@ const run = (args) => catch_errors('Check failed', async () => {
121
129
  }
122
130
  if (outdated.length > 0) {
123
131
  console.log()
124
- print_info(`Run ${code('happyskills update')} to upgrade ${outdated.length} skill(s).`)
132
+ print_info(`Run ${code('happyskills update --all')} to upgrade ${outdated.length} skill(s).`)
125
133
  } else if (conflicts.length === 0 && results.every(r => r.status === 'up-to-date')) {
126
134
  console.log()
127
135
  print_success('All skills are up to date.')
@@ -1,7 +1,6 @@
1
- const path = require('path')
2
1
  const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
3
2
  const { install } = require('../engine/installer')
4
- const { read_lock, get_all_locked_skills } = require('../lock/reader')
3
+ const { read_lock, get_all_locked_skills, get_via } = require('../lock/reader')
5
4
  const { detect_status } = require('../merge/detector')
6
5
  const { print_help, print_success, print_info, print_warn, print_json } = require('../ui/output')
7
6
  const { exit_with_error, UsageError } = require('../utils/errors')
@@ -48,7 +47,7 @@ const run = (args) => catch_errors('Update failed', async () => {
48
47
 
49
48
  const to_update = target_skill
50
49
  ? [[target_skill, skills[target_skill]]]
51
- : Object.entries(skills).filter(([, data]) => data.requested_by?.includes('__root__'))
50
+ : Object.entries(skills).filter(([, data]) => data !== null)
52
51
 
53
52
  if (to_update.length === 0) {
54
53
  if (args.flags.json) {
@@ -96,9 +95,9 @@ const run = (args) => catch_errors('Update failed', async () => {
96
95
  const [errors, result] = await install(name, options)
97
96
  if (errors) throw e(`Update ${name} failed`, errors)
98
97
  if (!result.no_op) {
99
- updated.push({ skill: name, from: before_version, to: result.version })
98
+ updated.push({ skill: name, from: before_version, to: result.version, via: get_via(data) })
100
99
  } else {
101
- already_up_to_date.push({ skill: name, version: result.version })
100
+ already_up_to_date.push({ skill: name, version: result.version, via: get_via(data) })
102
101
  }
103
102
  }
104
103
 
@@ -2,6 +2,7 @@ const { error: { catch_errors } } = require('puffy-core')
2
2
  const { read_json } = require('../utils/fs')
3
3
  const { lock_file_path } = require('../config/paths')
4
4
  const { LOCK_VERSION } = require('../constants')
5
+ const { print_warn } = require('../ui/output')
5
6
 
6
7
  const read_lock = (project_root) => catch_errors('Failed to read lock file', async () => {
7
8
  const lock_path = lock_file_path(project_root)
@@ -9,7 +10,6 @@ const read_lock = (project_root) => catch_errors('Failed to read lock file', asy
9
10
  if (errors) return null
10
11
 
11
12
  if (data.lockVersion !== LOCK_VERSION) {
12
- const { print_warn } = require('../ui/output')
13
13
  print_warn(`Lock file version mismatch (found ${data.lockVersion}, expected ${LOCK_VERSION}). Consider running with --fresh.`)
14
14
  }
15
15
 
@@ -26,4 +26,10 @@ const get_all_locked_skills = (lock_data) => {
26
26
  return lock_data.skills
27
27
  }
28
28
 
29
- module.exports = { read_lock, get_locked_skill, get_all_locked_skills }
29
+ const get_via = (data) => {
30
+ const requester = data?.requested_by?.[0]
31
+ if (!requester || requester === '__root__') return null
32
+ return requester
33
+ }
34
+
35
+ module.exports = { read_lock, get_locked_skill, get_all_locked_skills, get_via }
@@ -5,7 +5,7 @@ const os = require('os')
5
5
  const path = require('path')
6
6
  const fs = require('fs')
7
7
 
8
- const { read_lock, get_locked_skill, get_all_locked_skills } = require('./reader')
8
+ const { read_lock, get_locked_skill, get_all_locked_skills, get_via } = require('./reader')
9
9
  const { LOCK_VERSION } = require('../constants')
10
10
 
11
11
  // ─── helpers ──────────────────────────────────────────────────────────────────
@@ -126,3 +126,39 @@ describe('get_all_locked_skills', () => {
126
126
  assert.deepEqual(get_all_locked_skills({ skills: {} }), {})
127
127
  })
128
128
  })
129
+
130
+ // ─── get_via ────────────────────────────────────────────────────────────────
131
+
132
+ describe('get_via', () => {
133
+ it('returns null for a directly installed skill (__root__)', () => {
134
+ assert.strictEqual(get_via({ requested_by: ['__root__'] }), null)
135
+ })
136
+
137
+ it('returns the parent skill name for a dependency', () => {
138
+ assert.strictEqual(get_via({ requested_by: ['acme/deploy-aws'] }), 'acme/deploy-aws')
139
+ })
140
+
141
+ it('returns the kit name for a kit dependency', () => {
142
+ assert.strictEqual(get_via({ requested_by: ['acme/_kit-react'] }), 'acme/_kit-react')
143
+ })
144
+
145
+ it('returns the first requester when multiple exist', () => {
146
+ assert.strictEqual(get_via({ requested_by: ['acme/foo', 'acme/bar'] }), 'acme/foo')
147
+ })
148
+
149
+ it('returns null when requested_by is empty', () => {
150
+ assert.strictEqual(get_via({ requested_by: [] }), null)
151
+ })
152
+
153
+ it('returns null when requested_by is missing', () => {
154
+ assert.strictEqual(get_via({}), null)
155
+ })
156
+
157
+ it('returns null when data is null', () => {
158
+ assert.strictEqual(get_via(null), null)
159
+ })
160
+
161
+ it('returns null when data is undefined', () => {
162
+ assert.strictEqual(get_via(undefined), null)
163
+ })
164
+ })