happyskills 1.7.0 → 1.7.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,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.7.1] - 2026-06-04
11
+
12
+ ### Fixed
13
+
14
+ - Stop double-counting a single install. The install pipeline tries an archive download first and falls back to a JSON clone when the archive is missing or unusable; both hit the clone endpoint, which counts (`repo.clone` + `download_count`) per request. The fallback now sends `?no_count=1` so one install registers exactly once. Only the install fallback sets it — `fork`, `pull`, and normal installs are unchanged. Takes effect against API v5.4.1+ (older APIs ignore the flag and still count both); forward-only.
15
+
10
16
  ## [1.7.0] - 2026-06-04
11
17
 
12
18
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "1.7.0",
3
+ "version": "1.7.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)",
package/src/api/repos.js CHANGED
@@ -61,6 +61,9 @@ const clone = (owner, repo, ref, options = {}) => catch_errors(`Clone ${owner}/$
61
61
  if (options.commit) params.set('commit', options.commit)
62
62
  else if (ref) params.set('ref', ref)
63
63
  if (options.format) params.set('format', options.format)
64
+ // Fallback retries (install archive→JSON) mark themselves so the server
65
+ // counts the install only once (the primary attempt already counted).
66
+ if (options.no_count) params.set('no_count', '1')
64
67
  const qs = params.toString() ? `?${params}` : ''
65
68
  const [errors, data] = await client.get(`/repos/${owner}/${repo}/clone${qs}`)
66
69
  if (errors) throw errors[errors.length - 1]
@@ -217,3 +217,19 @@ describe('repos.star / repos.unstar — endpoint + path', () => {
217
217
  assert.strictEqual(calls[0][1], `/repos/${encodeURIComponent('acme corp')}/${encodeURIComponent('deploy/aws')}/star`)
218
218
  })
219
219
  })
220
+
221
+ // Bug B fix: a fallback retry marks itself no_count so the server counts the
222
+ // install only once (the primary archive attempt already counted).
223
+ describe('repos.clone — no_count flag (double-count prevention)', () => {
224
+ afterEach(restore)
225
+
226
+ it('adds ?no_count=1 only when options.no_count is set', async () => {
227
+ const { repos, calls } = stub_client_capture()
228
+ await repos.clone('acme', 'deploy', 'refs/tags/v1.0.0', { no_count: true })
229
+ await repos.clone('acme', 'deploy', 'refs/tags/v1.0.0', {})
230
+ await repos.clone('acme', 'deploy', 'refs/tags/v1.0.0', { format: 'archive' })
231
+ assert.match(calls[0][1], /[?&]no_count=1/)
232
+ assert.doesNotMatch(calls[1][1], /no_count/)
233
+ assert.doesNotMatch(calls[2][1], /no_count/) // archive is the primary attempt — counts
234
+ })
235
+ })
@@ -1,8 +1,8 @@
1
1
  const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
2
2
  const repos_api = require('../api/repos')
3
3
 
4
- const download = (owner, repo, ref) => catch_errors(`Download ${owner}/${repo} failed`, async () => {
5
- const [errors, data] = await repos_api.clone(owner, repo, ref)
4
+ const download = (owner, repo, ref, options = {}) => catch_errors(`Download ${owner}/${repo} failed`, async () => {
5
+ const [errors, data] = await repos_api.clone(owner, repo, ref, options)
6
6
  if (errors) throw e(`Failed to clone ${owner}/${repo}`, errors)
7
7
  return data
8
8
  })
@@ -204,8 +204,11 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
204
204
  clone_ref = arch_result.ref
205
205
  clone_commit = arch_result.commit
206
206
  } else {
207
- // Fall back to JSON clone
208
- const [dl_errors, clone_data] = await download(owner, name, pkg.ref)
207
+ // Fall back to JSON clone. The archive attempt above already hit the
208
+ // clone endpoint (and counted the install), so mark this retry
209
+ // no_count to avoid double-counting one install (download_count +
210
+ // repo.clone). See repos.js clone handler.
211
+ const [dl_errors, clone_data] = await download(owner, name, pkg.ref, { no_count: true })
209
212
  if (dl_errors) { spinner.fail(`Download failed: ${pkg.skill}`); throw e(`Download ${pkg.skill} failed`, dl_errors) }
210
213
 
211
214
  const [ext_errors] = await extract(clone_data, pkg_tmp)