happyskills 0.20.0 → 0.21.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 +9 -0
- package/package.json +1 -1
- package/src/commands/check.js +18 -7
- package/src/commands/refresh.js +35 -8
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.21.0] - 2026-03-30
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Upgrade `check` and `refresh` from semver-only to commit-level comparison (`base_commit` vs remote head) for accurate divergence detection — catches divergence even when version strings match (e.g., after force-publish). Falls back to version comparison for old lock files without `base_commit`.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- Add `conflicts` status to `check` command — detects unresolved merge conflicts from the lock file's `conflict_files` field without disk I/O. JSON output now includes `conflicts_count`.
|
|
17
|
+
- Add local modification protection to `refresh` — skills with local edits are skipped with a warning and `happyskills pull` suggestion instead of being silently overwritten. JSON output includes `skipped` array with `{ skill, reason, suggestion }` per entry.
|
|
18
|
+
|
|
10
19
|
## [0.20.0] - 2026-03-30
|
|
11
20
|
|
|
12
21
|
### Added
|
package/package.json
CHANGED
package/src/commands/check.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
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 { gt } = require('../utils/semver')
|
|
5
4
|
const { print_help, print_table, print_json, print_info, print_success, print_warn, print_hint, code } = require('../ui/output')
|
|
6
5
|
const { green, yellow, red } = require('../ui/colors')
|
|
7
6
|
const { exit_with_error } = require('../utils/errors')
|
|
@@ -68,13 +67,17 @@ const run = (args) => catch_errors('Check failed', async () => {
|
|
|
68
67
|
} else {
|
|
69
68
|
for (const [name, data] of to_check) {
|
|
70
69
|
const info = batch_data?.results?.[name]
|
|
71
|
-
|
|
70
|
+
const has_conflicts = (data.conflict_files || []).length > 0
|
|
71
|
+
if (has_conflicts) {
|
|
72
|
+
results.push({ skill: name, installed: data.version, latest: info?.latest_version || '-', status: 'conflicts' })
|
|
73
|
+
} else if (info?.access_denied) {
|
|
72
74
|
results.push({ skill: name, installed: data.version, latest: '-', status: 'no-access' })
|
|
73
75
|
} else if (!info || !info.latest_version) {
|
|
74
76
|
results.push({ skill: name, installed: data.version, latest: '-', status: 'unknown' })
|
|
75
|
-
} else if (info.
|
|
76
|
-
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: '
|
|
77
|
-
} else if (
|
|
77
|
+
} 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
|
+
} else if (!data.base_commit && info.latest_version !== data.version) {
|
|
80
|
+
// Fallback to version comparison for old lock files without base_commit
|
|
78
81
|
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'outdated' })
|
|
79
82
|
} else {
|
|
80
83
|
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'up-to-date' })
|
|
@@ -85,13 +88,15 @@ const run = (args) => catch_errors('Check failed', async () => {
|
|
|
85
88
|
if (args.flags.json) {
|
|
86
89
|
const outdated_count = results.filter(r => r.status === 'outdated').length
|
|
87
90
|
const up_to_date_count = results.filter(r => r.status === 'up-to-date').length
|
|
88
|
-
|
|
91
|
+
const conflicts_count = results.filter(r => r.status === 'conflicts').length
|
|
92
|
+
print_json({ data: { results, outdated_count, up_to_date_count, conflicts_count } })
|
|
89
93
|
return
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
const status_colors = {
|
|
93
97
|
'up-to-date': green,
|
|
94
98
|
'outdated': yellow,
|
|
99
|
+
'conflicts': red,
|
|
95
100
|
'no-access': yellow,
|
|
96
101
|
'error': red,
|
|
97
102
|
'unknown': (s) => s
|
|
@@ -107,11 +112,17 @@ const run = (args) => catch_errors('Check failed', async () => {
|
|
|
107
112
|
print_table(['Skill', 'Installed', 'Latest', 'Status'], rows)
|
|
108
113
|
|
|
109
114
|
const outdated = results.filter(r => r.status === 'outdated')
|
|
115
|
+
const conflicts = results.filter(r => r.status === 'conflicts')
|
|
110
116
|
const no_access = results.filter(r => r.status === 'no-access')
|
|
117
|
+
if (conflicts.length > 0) {
|
|
118
|
+
console.log()
|
|
119
|
+
print_warn(`${conflicts.length} skill(s) have unresolved merge conflicts.`)
|
|
120
|
+
print_hint(`Run ${code('happyskills status')} for details.`)
|
|
121
|
+
}
|
|
111
122
|
if (outdated.length > 0) {
|
|
112
123
|
console.log()
|
|
113
124
|
print_info(`Run ${code('happyskills update')} to upgrade ${outdated.length} skill(s).`)
|
|
114
|
-
} else if (results.every(r => r.status === 'up-to-date')) {
|
|
125
|
+
} else if (conflicts.length === 0 && results.every(r => r.status === 'up-to-date')) {
|
|
115
126
|
console.log()
|
|
116
127
|
print_success('All skills are up to date.')
|
|
117
128
|
}
|
package/src/commands/refresh.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
|
|
2
2
|
const { install } = require('../engine/installer')
|
|
3
3
|
const { read_lock, get_all_locked_skills } = require('../lock/reader')
|
|
4
|
+
const { detect_status } = require('../merge/detector')
|
|
4
5
|
const repos_api = require('../api/repos')
|
|
5
|
-
const { gt } = require('../utils/semver')
|
|
6
6
|
const { print_help, print_table, print_json, print_info, print_success, print_warn, print_hint, code } = require('../ui/output')
|
|
7
7
|
const { green, yellow, red } = require('../ui/colors')
|
|
8
8
|
const { create_spinner } = require('../ui/spinner')
|
|
9
9
|
const { exit_with_error } = require('../utils/errors')
|
|
10
|
-
const { find_project_root, lock_root } = require('../config/paths')
|
|
10
|
+
const { find_project_root, lock_root, skills_dir, skill_install_dir } = require('../config/paths')
|
|
11
11
|
const { EXIT_CODES } = require('../constants')
|
|
12
12
|
|
|
13
13
|
const HELP_TEXT = `Usage: happyskills refresh [options]
|
|
@@ -78,9 +78,10 @@ const run = (args) => catch_errors('Refresh failed', async () => {
|
|
|
78
78
|
results.push({ skill: name, installed: data.version, latest: '-', status: 'no-access' })
|
|
79
79
|
} else if (!info || !info.latest_version) {
|
|
80
80
|
results.push({ skill: name, installed: data.version, latest: '-', status: 'unknown' })
|
|
81
|
-
} else if (info.
|
|
82
|
-
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: '
|
|
83
|
-
} else if (
|
|
81
|
+
} else if (data.base_commit && info.commit && data.base_commit !== info.commit) {
|
|
82
|
+
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'outdated' })
|
|
83
|
+
} else if (!data.base_commit && info.latest_version !== data.version) {
|
|
84
|
+
// Fallback to version comparison for old lock files without base_commit
|
|
84
85
|
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'outdated' })
|
|
85
86
|
} else {
|
|
86
87
|
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'up-to-date' })
|
|
@@ -125,12 +126,29 @@ const run = (args) => catch_errors('Refresh failed', async () => {
|
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
// 5.
|
|
129
|
+
// 5. Detect local modifications before updating
|
|
130
|
+
const base_dir = skills_dir(is_global, project_root)
|
|
131
|
+
const skipped = []
|
|
132
|
+
const safe_to_update = []
|
|
133
|
+
|
|
134
|
+
for (const r of outdated) {
|
|
135
|
+
const lock_entry = skills[r.skill]
|
|
136
|
+
const short_name = r.skill.split('/')[1] || r.skill
|
|
137
|
+
const dir = skill_install_dir(base_dir, short_name)
|
|
138
|
+
const [, det] = await detect_status(lock_entry, dir)
|
|
139
|
+
if (det?.local_modified) {
|
|
140
|
+
skipped.push({ skill: r.skill, reason: 'local_modifications', suggestion: `happyskills pull ${r.skill}` })
|
|
141
|
+
} else {
|
|
142
|
+
safe_to_update.push(r)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 6. Update safe skills
|
|
129
147
|
const options = { global: is_global, fresh: true, agents: args.flags.agents || undefined, project_root }
|
|
130
148
|
const updated = []
|
|
131
149
|
const update_errors = []
|
|
132
150
|
|
|
133
|
-
for (const r of
|
|
151
|
+
for (const r of safe_to_update) {
|
|
134
152
|
const update_spinner = !args.flags.json ? create_spinner(`Updating ${r.skill}…`) : null
|
|
135
153
|
const [errors, result] = await install(r.skill, options)
|
|
136
154
|
if (errors) {
|
|
@@ -144,7 +162,7 @@ const run = (args) => catch_errors('Refresh failed', async () => {
|
|
|
144
162
|
}
|
|
145
163
|
}
|
|
146
164
|
|
|
147
|
-
//
|
|
165
|
+
// 7. Output results
|
|
148
166
|
if (args.flags.json) {
|
|
149
167
|
print_json({
|
|
150
168
|
data: {
|
|
@@ -152,6 +170,7 @@ const run = (args) => catch_errors('Refresh failed', async () => {
|
|
|
152
170
|
outdated_count: outdated.length,
|
|
153
171
|
up_to_date_count: up_to_date.length,
|
|
154
172
|
updated,
|
|
173
|
+
skipped,
|
|
155
174
|
already_up_to_date: up_to_date.map(r => ({ skill: r.skill, version: r.installed })),
|
|
156
175
|
errors: update_errors
|
|
157
176
|
}
|
|
@@ -159,6 +178,14 @@ const run = (args) => catch_errors('Refresh failed', async () => {
|
|
|
159
178
|
return
|
|
160
179
|
}
|
|
161
180
|
|
|
181
|
+
if (skipped.length > 0) {
|
|
182
|
+
console.log()
|
|
183
|
+
print_warn(`Skipped ${skipped.length} skill(s) with local modifications:`)
|
|
184
|
+
for (const s of skipped) {
|
|
185
|
+
print_info(` ${s.skill}`)
|
|
186
|
+
}
|
|
187
|
+
print_hint(`Use ${code('happyskills pull <skill>')} to merge remote changes.`)
|
|
188
|
+
}
|
|
162
189
|
if (updated.length > 0) {
|
|
163
190
|
console.log()
|
|
164
191
|
print_success(`Updated ${updated.length} skill(s).`)
|