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 +11 -0
- package/package.json +1 -1
- package/src/api/repos.js +7 -1
- package/src/commands/check.js +18 -21
- package/src/commands/refresh.js +170 -0
- package/src/constants.js +2 -0
- package/src/index.js +1 -0
- package/src/integration/cli.test.js +23 -1
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
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
|
-
|
|
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 }
|
package/src/commands/check.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
62
|
-
const [
|
|
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
|
|
70
|
-
|
|
71
|
-
.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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', () => {
|