happyskills 0.18.0 → 0.18.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,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.18.1] - 2026-03-29
11
+
12
+ ### Fixed
13
+ - Fix `pull` and `diff` recording stale commit SHA from CloudFront-cached clone responses — now clones at the specific head commit from the compare endpoint, ensuring the lock file matches the remote after pull
14
+
10
15
  ## [0.18.0] - 2026-03-29
11
16
 
12
17
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "0.18.0",
3
+ "version": "0.18.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)",
@@ -136,15 +136,18 @@ const run = (args) => catch_errors('Diff failed', async () => {
136
136
  }
137
137
 
138
138
  if (mode === 'remote') {
139
- // Base vs remote — use classify_changes with base as the "local" side
140
- const [remote_err, remote_clone] = await repos_api.clone(owner, repo, null)
139
+ // Base vs remote — get head commit via compare, clone at that commit
140
+ const [cmp_err, cmp_data] = await repos_api.compare(owner, repo, lock_entry.base_commit)
141
+ if (cmp_err) throw e('Compare failed', cmp_err)
142
+
143
+ const [remote_err, remote_clone] = await repos_api.clone(owner, repo, null, { commit: cmp_data.head_commit })
141
144
  if (remote_err) throw e('Failed to fetch remote files', remote_err)
142
145
  const remote_files = (remote_clone.files || []).map(f => ({ path: f.path, sha: f.sha }))
143
146
 
144
147
  const classified = classify_changes(base_files, base_files, remote_files)
145
148
 
146
149
  if (args.flags.json) {
147
- const report = build_report(skill_name, lock_entry.version, null, classified)
150
+ const report = build_report(skill_name, lock_entry.version, cmp_data.head_version, classified)
148
151
  print_json({ data: { mode, report } })
149
152
  } else {
150
153
  print_file_table(classified)
@@ -152,16 +155,19 @@ const run = (args) => catch_errors('Diff failed', async () => {
152
155
  return
153
156
  }
154
157
 
155
- // Full three-way diff
158
+ // Full three-way diff — get head commit via compare
159
+ const [cmp_err, cmp_data] = await repos_api.compare(owner, repo, lock_entry.base_commit)
160
+ if (cmp_err) throw e('Compare failed', cmp_err)
161
+
156
162
  const [local_err, local_files] = await build_local_entries(skill_dir)
157
163
  if (local_err) throw e('Failed to read local files', local_err)
158
164
 
159
- const [remote_err, remote_clone] = await repos_api.clone(owner, repo, null)
165
+ const [remote_err, remote_clone] = await repos_api.clone(owner, repo, null, { commit: cmp_data.head_commit })
160
166
  if (remote_err) throw e('Failed to fetch remote files', remote_err)
161
167
  const remote_files = (remote_clone.files || []).map(f => ({ path: f.path, sha: f.sha }))
162
168
 
163
169
  const classified = classify_changes(base_files, local_files, remote_files)
164
- const report = build_report(skill_name, lock_entry.version, null, classified)
170
+ const report = build_report(skill_name, lock_entry.version, cmp_data.head_version, classified)
165
171
 
166
172
  if (args.flags.json) {
167
173
  print_json({ data: { mode, report } })
@@ -142,7 +142,8 @@ const run = (args) => catch_errors('Pull failed', async () => {
142
142
  // 5. No local mods → fast-forward
143
143
  if (!det.local_modified || strategy === 'force') {
144
144
  spinner.update('Fast-forwarding...')
145
- const [clone_err, clone_data] = await repos_api.clone(owner, repo, null)
145
+ // Clone at the specific head commit from compare (bypasses CDN cache)
146
+ const [clone_err, clone_data] = await repos_api.clone(owner, repo, null, { commit: cmp_data.head_commit })
146
147
  if (clone_err) { spinner.fail('Clone failed'); throw clone_err[0] }
147
148
 
148
149
  // Remove existing files and write fresh
@@ -159,12 +160,12 @@ const run = (args) => catch_errors('Pull failed', async () => {
159
160
  const [wf_err] = await write_files_from_clone(clone_data, skill_dir)
160
161
  if (wf_err) { spinner.fail('Failed to write files'); throw wf_err[0] }
161
162
 
162
- // Update lock
163
+ // Update lock — use head_commit from compare (authoritative, not cached)
163
164
  const [hash_err, new_integrity] = await hash_directory(skill_dir)
164
165
  const updated_entry = {
165
166
  ...lock_entry,
166
- commit: clone_data.commit,
167
- base_commit: clone_data.commit,
167
+ commit: cmp_data.head_commit,
168
+ base_commit: cmp_data.head_commit,
168
169
  integrity: new_integrity || lock_entry.integrity,
169
170
  base_integrity: new_integrity || lock_entry.base_integrity,
170
171
  version: cmp_data.head_version || lock_entry.version,
@@ -196,8 +197,8 @@ const run = (args) => catch_errors('Pull failed', async () => {
196
197
  const [local_err, local_files] = await build_local_entries(skill_dir)
197
198
  if (local_err) { spinner.fail('Failed to read local files'); throw local_err[0] }
198
199
 
199
- // Get remote files (clone latest)
200
- const [remote_err, remote_clone] = await repos_api.clone(owner, repo, null)
200
+ // Get remote files (clone at head commit — bypasses CDN cache)
201
+ const [remote_err, remote_clone] = await repos_api.clone(owner, repo, null, { commit: cmp_data.head_commit })
201
202
  if (remote_err) { spinner.fail('Failed to fetch remote files'); throw remote_err[0] }
202
203
  const remote_files = (remote_clone.files || []).map(f => ({ path: f.path, sha: f.sha }))
203
204
 
@@ -262,12 +263,12 @@ const run = (args) => catch_errors('Pull failed', async () => {
262
263
  }
263
264
  // strategy === 'ours' → keep local files as-is (no action needed)
264
265
 
265
- // Update lock
266
+ // Update lock — use head_commit from compare (authoritative, not cached)
266
267
  const [hash_err, new_integrity] = await hash_directory(skill_dir)
267
268
  const updated_entry = {
268
269
  ...lock_entry,
269
- commit: remote_clone.commit,
270
- base_commit: remote_clone.commit,
270
+ commit: cmp_data.head_commit,
271
+ base_commit: cmp_data.head_commit,
271
272
  integrity: new_integrity || lock_entry.integrity,
272
273
  base_integrity: new_integrity || lock_entry.base_integrity,
273
274
  version: cmp_data.head_version || lock_entry.version,