happyskills 0.44.0 → 0.44.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 +5 -0
- package/package.json +1 -1
- package/src/commands/diff.js +15 -11
- package/src/integration/drift.test.js +32 -8
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.44.1] - 2026-05-13
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- `diff` no longer hard-blocks when the target skill has lock-vs-disk drift. Previously, running `happyskills diff <skill>` against a drifted skill threw a `UsageError` telling the user to run `happyskills install <skill> --fresh` before diffing — but `--fresh` overwrites the on-disk content, destroying the very local state the user was trying to inspect. Drift is exactly the case where diff is most useful as a diagnostic tool. The command now prints a one-line warning (`<skill> has drift: lock <X>, disk <Y>. Diff is shown against the lock-recorded base (<X>).`) and proceeds with the diff. JSON output gains a top-level `drift` field (same `{ reason, lock_version, disk_version }` shape used elsewhere) in `local` and `full` modes, or `null` when clean. `--remote` mode skips the drift probe entirely since it reads nothing from disk. Other drift-aware commands are unchanged — `status`/`check`/`list` still surface drift in their reports, `update` still skips drifted skills to avoid clobbering local work, and the post-write self-checks in `install`/`pull`/`bump`/`publish`/`convert` still throw on inconsistency.
|
|
14
|
+
|
|
10
15
|
## [0.44.0] - 2026-05-12
|
|
11
16
|
|
|
12
17
|
### Added
|
package/package.json
CHANGED
package/src/commands/diff.js
CHANGED
|
@@ -249,16 +249,20 @@ const run = (args) => catch_errors('Diff failed', async () => {
|
|
|
249
249
|
const base_dir = skills_dir(is_global, project_root)
|
|
250
250
|
const skill_dir = skill_install_dir(base_dir, repo)
|
|
251
251
|
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
// the
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
252
|
+
// Drift (lock-vs-disk version mismatch) does NOT block diff — diff is the
|
|
253
|
+
// diagnostic tool a user reaches for precisely when things have diverged,
|
|
254
|
+
// and the obvious "remediation" (install --fresh) would destroy the local
|
|
255
|
+
// content they're trying to inspect. We probe drift only to warn the user
|
|
256
|
+
// that the "base" shown is the lock-recorded base (not the disk version's
|
|
257
|
+
// base). Skipped in --remote mode, which reads nothing from disk.
|
|
258
|
+
const drift = mode === 'remote'
|
|
259
|
+
? null
|
|
260
|
+
: (await verify_lock_disk_consistency(lock_entry, skill_dir))[1]
|
|
261
|
+
const has_drift = drift && !drift.ok
|
|
262
|
+
if (has_drift && !args.flags.json) {
|
|
263
|
+
print_warn(
|
|
260
264
|
`${skill_name} has drift: lock ${drift.expected}, disk ${drift.actual || 'none'}. ` +
|
|
261
|
-
`
|
|
265
|
+
`Diff is shown against the lock-recorded base (${lock_entry.version}).`
|
|
262
266
|
)
|
|
263
267
|
}
|
|
264
268
|
|
|
@@ -280,7 +284,7 @@ const run = (args) => catch_errors('Diff failed', async () => {
|
|
|
280
284
|
}
|
|
281
285
|
|
|
282
286
|
if (args.flags.json) {
|
|
283
|
-
print_json({ data: { mode, report } })
|
|
287
|
+
print_json({ data: { mode, report, drift: has_drift ? { reason: drift.reason, lock_version: drift.expected, disk_version: drift.actual } : null } })
|
|
284
288
|
} else {
|
|
285
289
|
print_file_table(classified)
|
|
286
290
|
print_report_diffs(report)
|
|
@@ -333,7 +337,7 @@ const run = (args) => catch_errors('Diff failed', async () => {
|
|
|
333
337
|
}
|
|
334
338
|
|
|
335
339
|
if (args.flags.json) {
|
|
336
|
-
print_json({ data: { mode, report } })
|
|
340
|
+
print_json({ data: { mode, report, drift: has_drift ? { reason: drift.reason, lock_version: drift.expected, disk_version: drift.actual } : null } })
|
|
337
341
|
} else {
|
|
338
342
|
print_file_table(classified)
|
|
339
343
|
print_report_diffs(report)
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
* - `list` reported the lock's version as the installed version (lying)
|
|
12
12
|
* - `diff` used the lock's `base_commit` as a comparison baseline that no
|
|
13
13
|
* longer matched what was on disk (incoherent diff)
|
|
14
|
+
*
|
|
15
|
+
* Note on `diff`: an earlier fix hard-blocked `diff` on drift. That was wrong
|
|
16
|
+
* — diff is the diagnostic tool a user reaches for *because* of drift, and
|
|
17
|
+
* the suggested "install --fresh" remediation would destroy the local
|
|
18
|
+
* content. The current behavior is to warn and proceed.
|
|
14
19
|
* - `update` could decide a drifted skill was up-to-date and skip it
|
|
15
20
|
*
|
|
16
21
|
* These tests reconstruct the exact linwong scenario that surfaced the bug
|
|
@@ -258,19 +263,38 @@ describe('list — drift surfacing', () => {
|
|
|
258
263
|
|
|
259
264
|
// ─── diff ─────────────────────────────────────────────────────────────────────
|
|
260
265
|
|
|
261
|
-
describe('diff — drift
|
|
262
|
-
it('diff
|
|
263
|
-
// diff
|
|
264
|
-
//
|
|
266
|
+
describe('diff — drift does not block', () => {
|
|
267
|
+
it('diff proceeds past the drift check (no UsageError) and warns the user', () => {
|
|
268
|
+
// Drift must not block diff: diff is the diagnostic tool for this case,
|
|
269
|
+
// and "install --fresh" would destroy the local content. We assert that
|
|
270
|
+
// the command moves past the drift gate — it will then fail trying to
|
|
271
|
+
// reach the fake API URL, but exit code 2 (UsageError) means the old
|
|
272
|
+
// pre-block is back.
|
|
265
273
|
const { root, cleanup } = scaffold_drifted([
|
|
266
274
|
{ full: 'acme/cant-diff', short: 'cant-diff', lock_version: '0.4.0', disk_version: '0.3.0' }
|
|
267
275
|
])
|
|
268
276
|
try {
|
|
269
277
|
const { code, stderr } = run(['diff', 'acme/cant-diff'], { cwd: root })
|
|
270
|
-
assert.
|
|
271
|
-
assert.ok(
|
|
272
|
-
|
|
273
|
-
|
|
278
|
+
assert.notStrictEqual(code, 2, 'must not exit with the old drift UsageError')
|
|
279
|
+
assert.ok(
|
|
280
|
+
stderr.toLowerCase().includes('drift') && stderr.includes('0.4.0') && stderr.includes('0.3.0'),
|
|
281
|
+
'should warn about drift with both versions'
|
|
282
|
+
)
|
|
283
|
+
assert.ok(
|
|
284
|
+
!/before diffing/i.test(stderr),
|
|
285
|
+
'must not tell the user to fix drift before diffing'
|
|
286
|
+
)
|
|
287
|
+
} finally { cleanup() }
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('diff --remote skips the drift probe entirely (drift is irrelevant when no disk is read)', () => {
|
|
291
|
+
const { root, cleanup } = scaffold_drifted([
|
|
292
|
+
{ full: 'acme/cant-diff', short: 'cant-diff', lock_version: '0.4.0', disk_version: '0.3.0' }
|
|
293
|
+
])
|
|
294
|
+
try {
|
|
295
|
+
const { code, stderr } = run(['diff', 'acme/cant-diff', '--remote'], { cwd: root })
|
|
296
|
+
assert.notStrictEqual(code, 2, 'must not exit with the old drift UsageError')
|
|
297
|
+
assert.ok(!/drift/i.test(stderr), '--remote mode should not surface drift at all')
|
|
274
298
|
} finally { cleanup() }
|
|
275
299
|
})
|
|
276
300
|
})
|