happyskills 0.7.3 → 0.8.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/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.0] - 2026-03-08
11
+
12
+ ### Added
13
+ - Add `refresh` command to check all installed skills for updates and upgrade outdated ones in a single step (alias: `r`); supports `-y`, `-g`, and `--json` flags
14
+
15
+ ### Changed
16
+ - Change `check` command to use a single batch API call (`POST /repos:check-updates`) instead of N sequential ref lookups, significantly reducing latency for projects with many skills
17
+
18
+ ### Fixed
19
+ - Fix `publish` not writing the commit SHA to `skills-lock.json` after publishing, causing first-publish detection to rely on stale lock data
20
+
10
21
  ## [0.7.3] - 2026-03-08
11
22
 
12
23
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
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)",
package/src/api/repos.js CHANGED
@@ -46,4 +46,10 @@ const get_repo = (owner, repo) => catch_errors(`Get repo ${owner}/${repo} failed
46
46
  return data
47
47
  })
48
48
 
49
- module.exports = { search, resolve_dependencies, clone, push, get_refs, get_repo }
49
+ const check_updates = (skills) => catch_errors('Check updates failed', async () => {
50
+ const [errors, data] = await client.post('/repos:check-updates', { skills })
51
+ if (errors) throw errors[errors.length - 1]
52
+ return data
53
+ })
54
+
55
+ module.exports = { search, resolve_dependencies, clone, push, get_refs, get_repo, check_updates }
@@ -1,7 +1,7 @@
1
1
  const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
2
2
  const { read_lock, get_all_locked_skills } = require('../lock/reader')
3
3
  const repos_api = require('../api/repos')
4
- const { satisfies, gt } = require('../utils/semver')
4
+ const { gt } = require('../utils/semver')
5
5
  const { print_help, print_table, print_json, print_info, print_success, print_hint, code } = require('../ui/output')
6
6
  const { green, yellow, red } = require('../ui/colors')
7
7
  const { exit_with_error } = require('../utils/errors')
@@ -57,29 +57,26 @@ const run = (args) => catch_errors('Check failed', async () => {
57
57
  return
58
58
  }
59
59
 
60
+ const skill_names = to_check.map(([name]) => name)
61
+ const [batch_err, batch_data] = await repos_api.check_updates(skill_names)
62
+
60
63
  const results = []
61
- for (const [name, data] of to_check) {
62
- const [owner, repo] = name.split('/')
63
- const [errors, refs] = await repos_api.get_refs(owner, repo)
64
- if (errors) {
64
+ if (batch_err) {
65
+ for (const [name, data] of to_check) {
65
66
  results.push({ skill: name, installed: data.version, latest: 'error', status: 'error' })
66
- continue
67
67
  }
68
-
69
- const versions = (refs || [])
70
- .map(r => r.name?.replace('refs/tags/v', ''))
71
- .filter(Boolean)
72
-
73
- const latest = versions.sort((a, b) => gt(a, b) ? -1 : 1)[0]
74
-
75
- if (!latest) {
76
- results.push({ skill: name, installed: data.version, latest: '-', status: 'unknown' })
77
- } else if (latest === data.version) {
78
- results.push({ skill: name, installed: data.version, latest, status: 'up-to-date' })
79
- } else if (gt(latest, data.version)) {
80
- results.push({ skill: name, installed: data.version, latest, status: 'outdated' })
81
- } else {
82
- results.push({ skill: name, installed: data.version, latest, status: 'up-to-date' })
68
+ } else {
69
+ for (const [name, data] of to_check) {
70
+ const info = batch_data?.results?.[name]
71
+ if (!info || !info.latest_version) {
72
+ results.push({ skill: name, installed: data.version, latest: '-', status: 'unknown' })
73
+ } else if (info.latest_version === data.version) {
74
+ results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'up-to-date' })
75
+ } else if (gt(info.latest_version, data.version)) {
76
+ results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'outdated' })
77
+ } else {
78
+ results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'up-to-date' })
79
+ }
83
80
  }
84
81
  }
85
82
 
@@ -0,0 +1,170 @@
1
+ const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
2
+ const { install } = require('../engine/installer')
3
+ const { read_lock, get_all_locked_skills } = require('../lock/reader')
4
+ const repos_api = require('../api/repos')
5
+ const { gt } = require('../utils/semver')
6
+ const { print_help, print_table, print_json, print_info, print_success, code } = require('../ui/output')
7
+ const { green, yellow, red } = require('../ui/colors')
8
+ const { create_spinner } = require('../ui/spinner')
9
+ const { exit_with_error } = require('../utils/errors')
10
+ const { find_project_root, lock_root } = require('../config/paths')
11
+ const { EXIT_CODES } = require('../constants')
12
+
13
+ const HELP_TEXT = `Usage: happyskills refresh [options]
14
+
15
+ Check all installed skills for updates and upgrade outdated ones.
16
+
17
+ Options:
18
+ -g, --global Refresh globally installed skills
19
+ -y, --yes Skip confirmation prompts
20
+ --json Output as JSON
21
+
22
+ Aliases: r
23
+
24
+ Examples:
25
+ happyskills refresh
26
+ happyskills refresh -y
27
+ happyskills refresh -g -y --json`
28
+
29
+ const confirm_prompt = (question) => new Promise((resolve) => {
30
+ const readline = require('readline')
31
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
32
+ rl.question(question, (answer) => {
33
+ rl.close()
34
+ resolve(answer.trim().toLowerCase())
35
+ })
36
+ })
37
+
38
+ const run = (args) => catch_errors('Refresh failed', async () => {
39
+ if (args.flags._show_help) {
40
+ print_help(HELP_TEXT)
41
+ return process.exit(EXIT_CODES.SUCCESS)
42
+ }
43
+
44
+ const is_global = args.flags.global || false
45
+ const auto_yes = args.flags.yes || false
46
+ const project_root = find_project_root()
47
+ const [, lock_data] = await read_lock(lock_root(is_global, project_root))
48
+ const skills = get_all_locked_skills(lock_data)
49
+ const entries = Object.entries(skills)
50
+
51
+ const to_check = entries.filter(([, data]) => data.requested_by?.includes('__root__'))
52
+
53
+ if (to_check.length === 0) {
54
+ if (args.flags.json) {
55
+ print_json({ data: { results: [], outdated_count: 0, up_to_date_count: 0, updated: [], already_up_to_date: [], errors: [] } })
56
+ return
57
+ }
58
+ print_info('No skills installed.')
59
+ return
60
+ }
61
+
62
+ // 1. Check for updates
63
+ const spinner = !args.flags.json ? create_spinner('Checking for updates…') : null
64
+ const skill_names = to_check.map(([name]) => name)
65
+ const [batch_err, batch_data] = await repos_api.check_updates(skill_names)
66
+
67
+ const results = []
68
+ if (batch_err) {
69
+ spinner?.fail('Failed to check for updates')
70
+ for (const [name, data] of to_check) {
71
+ results.push({ skill: name, installed: data.version, latest: 'error', status: 'error' })
72
+ }
73
+ } else {
74
+ spinner?.succeed('Checked for updates')
75
+ for (const [name, data] of to_check) {
76
+ const info = batch_data?.results?.[name]
77
+ if (!info || !info.latest_version) {
78
+ results.push({ skill: name, installed: data.version, latest: '-', status: 'unknown' })
79
+ } else if (info.latest_version === data.version) {
80
+ results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'up-to-date' })
81
+ } else if (gt(info.latest_version, data.version)) {
82
+ results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'outdated' })
83
+ } else {
84
+ results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'up-to-date' })
85
+ }
86
+ }
87
+ }
88
+
89
+ const outdated = results.filter(r => r.status === 'outdated')
90
+ const up_to_date = results.filter(r => r.status === 'up-to-date')
91
+
92
+ // 2. If nothing to update, report and exit
93
+ if (outdated.length === 0) {
94
+ if (args.flags.json) {
95
+ print_json({ data: { results, outdated_count: 0, up_to_date_count: up_to_date.length, updated: [], already_up_to_date: up_to_date.map(r => ({ skill: r.skill, version: r.installed })), errors: [] } })
96
+ return
97
+ }
98
+ print_table(['Skill', 'Installed', 'Latest', 'Status'], results.map(r => [
99
+ r.skill, r.installed, r.latest, green(r.status)
100
+ ]))
101
+ console.log()
102
+ print_success('All skills are up to date.')
103
+ return
104
+ }
105
+
106
+ // 3. Show results table (non-json)
107
+ if (!args.flags.json) {
108
+ const status_colors = { 'up-to-date': green, 'outdated': yellow, 'error': red, 'unknown': (s) => s }
109
+ print_table(['Skill', 'Installed', 'Latest', 'Status'], results.map(r => [
110
+ r.skill, r.installed, r.latest, (status_colors[r.status] || ((s) => s))(r.status)
111
+ ]))
112
+ console.log()
113
+ print_info(`${outdated.length} skill(s) can be updated.`)
114
+ }
115
+
116
+ // 4. Confirm update
117
+ const should_update = auto_yes || !process.stdin.isTTY
118
+ if (!should_update && !args.flags.json) {
119
+ const answer = await confirm_prompt(`\nUpdate ${outdated.length} skill(s)? [y/N] `)
120
+ if (answer !== 'y' && answer !== 'yes') {
121
+ print_info('Skipped.')
122
+ return
123
+ }
124
+ }
125
+
126
+ // 5. Update outdated skills
127
+ const options = { global: is_global, fresh: true, project_root }
128
+ const updated = []
129
+ const update_errors = []
130
+
131
+ for (const r of outdated) {
132
+ const update_spinner = !args.flags.json ? create_spinner(`Updating ${r.skill}…`) : null
133
+ const [errors, result] = await install(r.skill, options)
134
+ if (errors) {
135
+ update_spinner?.fail(`Failed to update ${r.skill}`)
136
+ update_errors.push({ skill: r.skill, message: errors[errors.length - 1]?.message || 'Unknown error' })
137
+ } else if (!result.no_op) {
138
+ update_spinner?.succeed(`Updated ${r.skill} ${r.installed} → ${result.version}`)
139
+ updated.push({ skill: r.skill, from: r.installed, to: result.version })
140
+ } else {
141
+ update_spinner?.succeed(`${r.skill} already up to date`)
142
+ }
143
+ }
144
+
145
+ // 6. Output results
146
+ if (args.flags.json) {
147
+ print_json({
148
+ data: {
149
+ results,
150
+ outdated_count: outdated.length,
151
+ up_to_date_count: up_to_date.length,
152
+ updated,
153
+ already_up_to_date: up_to_date.map(r => ({ skill: r.skill, version: r.installed })),
154
+ errors: update_errors
155
+ }
156
+ })
157
+ return
158
+ }
159
+
160
+ if (updated.length > 0) {
161
+ console.log()
162
+ print_success(`Updated ${updated.length} skill(s).`)
163
+ }
164
+ if (update_errors.length > 0) {
165
+ console.log()
166
+ print_info(`${update_errors.length} skill(s) failed to update. Run ${code('happyskills check')} for details.`)
167
+ }
168
+ }).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
169
+
170
+ module.exports = { run }
package/src/constants.js CHANGED
@@ -26,6 +26,7 @@ const COMMAND_ALIASES = {
26
26
  remove: 'uninstall',
27
27
  ls: 'list',
28
28
  s: 'search',
29
+ r: 'refresh',
29
30
  up: 'update',
30
31
  pub: 'publish'
31
32
  }
@@ -37,6 +38,7 @@ const COMMANDS = [
37
38
  'list',
38
39
  'search',
39
40
  'check',
41
+ 'refresh',
40
42
  'update',
41
43
  'bump',
42
44
  'publish',
package/src/index.js CHANGED
@@ -90,6 +90,7 @@ Commands:
90
90
  list List installed skills (alias: ls)
91
91
  search <query> Search the registry (alias: s)
92
92
  check [owner/skill] Check for available updates
93
+ refresh Check + update all outdated skills (alias: r)
93
94
  update [owner/skill] Upgrade to latest versions (alias: up)
94
95
  publish Push skill to registry (alias: pub)
95
96
  fork <owner/skill> Fork a skill to your workspace
@@ -92,7 +92,7 @@ describe('CLI — global flags', () => {
92
92
 
93
93
  it('--help lists all expected commands', () => {
94
94
  const { stdout } = run(['--help'])
95
- const expected_commands = ['install', 'uninstall', 'list', 'search', 'check', 'update', 'publish', 'fork', 'login', 'logout', 'whoami', 'init', 'setup', 'self-update']
95
+ const expected_commands = ['install', 'uninstall', 'list', 'search', 'check', 'refresh', 'update', 'publish', 'fork', 'login', 'logout', 'whoami', 'init', 'setup', 'self-update']
96
96
  for (const cmd of expected_commands) {
97
97
  assert.ok(stdout.includes(cmd), `help output should mention "${cmd}"`)
98
98
  }
@@ -160,6 +160,9 @@ describe('CLI — command --help', () => {
160
160
  ['search', 'Arguments:'],
161
161
  ['search', 'Aliases:'],
162
162
  ['check', 'Examples:'],
163
+ ['refresh', 'Options:'],
164
+ ['refresh', 'Examples:'],
165
+ ['refresh', 'Aliases:'],
163
166
  ['update', 'Aliases:'],
164
167
  ['publish', 'Aliases:'],
165
168
  ['fork', 'Arguments:'],
@@ -193,6 +196,7 @@ describe('CLI — command aliases', () => {
193
196
  ['remove', 'uninstall'],
194
197
  ['ls', 'list'],
195
198
  ['s', 'search'],
199
+ ['r', 'refresh'],
196
200
  ['up', 'update'],
197
201
  ['pub', 'publish'],
198
202
  ]
@@ -412,6 +416,24 @@ describe('CLI — --json: existing json commands now use { data } envelope', ()
412
416
  })
413
417
  })
414
418
 
419
+ // ─── refresh command ──────────────────────────────────────────────────────────
420
+
421
+ describe('CLI — --json: refresh command', () => {
422
+ it('refresh --json with no skills returns { data: { results, outdated_count, ... } }', () => {
423
+ const tmp = make_tmp()
424
+ try {
425
+ const { stdout, code } = run(['refresh', '--json'], {}, { cwd: tmp })
426
+ assert.strictEqual(code, 0)
427
+ const out = parse_json_output(stdout, 'refresh --json empty')
428
+ assert.ok('data' in out)
429
+ assert.ok(Array.isArray(out.data.results))
430
+ assert.strictEqual(out.data.outdated_count, 0)
431
+ } finally {
432
+ fs.rmSync(tmp, { recursive: true, force: true })
433
+ }
434
+ })
435
+ })
436
+
415
437
  // ─── setup command ─────────────────────────────────────────────────────────────
416
438
 
417
439
  describe('CLI — setup command', () => {