happyskills 0.27.2 → 0.28.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,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.28.1] - 2026-04-02
11
+
12
+ ### Fixed
13
+ - Exclude macOS metadata files (`._*`, `.DS_Store`) from tar archive during `publish` to prevent server-side extraction failures
14
+
15
+ ## [0.28.0] - 2026-04-02
16
+
17
+ ### Added
18
+ - Add archive-based upload for large skill publishing — bundles all files into a single `.tar.gz` archive and uploads once via presigned URL, replacing hundreds of individual S3 PUTs for dramatically faster publishes of skills with many files
19
+
10
20
  ## [0.27.2] - 2026-04-02
11
21
 
12
22
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "0.27.2",
3
+ "version": "0.28.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/push.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
2
2
  const repos_api = require('./repos')
3
- const { initiate_upload, complete_upload, upload_files_parallel } = require('./upload')
3
+ const { initiate_upload, complete_upload, upload_file_to_s3, upload_files_parallel } = require('./upload')
4
+ const { create_archive, cleanup_archive } = require('../utils/archive')
4
5
 
5
6
  const DIRECT_PUSH_THRESHOLD = 5 * 1024 * 1024 // 5MB
6
7
 
@@ -12,7 +13,47 @@ const estimate_payload_size = (files) => {
12
13
  return size
13
14
  }
14
15
 
15
- const smart_push = (owner, repo, { version, message, files, visibility, base_commit, force, parent_shas }, on_progress) =>
16
+ const archive_push = (owner, repo, { version, message, files, visibility, base_commit, force, parent_shas, source_dir }, on_progress) =>
17
+ catch_errors('Archive push failed', async () => {
18
+ const file_meta = files.map(f => ({ path: f.path, sha: f.sha, size: f.size }))
19
+
20
+ // Step 1: Create tar.gz archive
21
+ const [arch_err, archive] = await create_archive(source_dir)
22
+ if (arch_err) throw e('Failed to create archive', arch_err)
23
+
24
+ try {
25
+ // Step 2: Initiate with archive mode (single presigned URL)
26
+ const init_body = {
27
+ version, message, files: file_meta, visibility, base_commit, force,
28
+ archive: true, archive_sha: archive.sha, archive_size: archive.size
29
+ }
30
+ if (parent_shas) init_body.parent_shas = parent_shas
31
+ const [init_err, init_data] = await initiate_upload(owner, repo, init_body)
32
+ if (init_err) throw e('Upload initiation failed', init_err)
33
+
34
+ // Step 3: Upload single archive
35
+ const url = init_data.presigned_urls[archive.sha]
36
+ if (!url) throw new Error('No presigned URL returned for archive')
37
+ const [upload_err] = await upload_file_to_s3(url, archive.buffer)
38
+ if (upload_err) throw e('Archive upload failed', upload_err)
39
+ if (on_progress) on_progress(1, 1)
40
+
41
+ // Step 4: Complete
42
+ const complete_body = {
43
+ upload_id: init_data.upload_id, archive_sha: archive.sha,
44
+ version, message, files: file_meta, base_commit, force
45
+ }
46
+ if (parent_shas) complete_body.parent_shas = parent_shas
47
+ const [complete_err, complete_data] = await complete_upload(owner, repo, complete_body)
48
+ if (complete_err) throw e('Upload completion failed', complete_err)
49
+
50
+ return complete_data
51
+ } finally {
52
+ cleanup_archive(archive.path)
53
+ }
54
+ })
55
+
56
+ const smart_push = (owner, repo, { version, message, files, visibility, base_commit, force, parent_shas, source_dir }, on_progress) =>
16
57
  catch_errors('Smart push failed', async () => {
17
58
  const payload_size = estimate_payload_size(files)
18
59
 
@@ -25,7 +66,12 @@ const smart_push = (owner, repo, { version, message, files, visibility, base_com
25
66
  return data
26
67
  }
27
68
 
28
- // Large payload — use presigned upload flow
69
+ // Large payload — prefer archive upload if source_dir is available
70
+ if (source_dir) {
71
+ return await archive_push(owner, repo, { version, message, files, visibility, base_commit, force, parent_shas, source_dir }, on_progress)
72
+ }
73
+
74
+ // Fallback: per-file presigned upload
29
75
  const file_meta = files.map(f => ({ path: f.path, sha: f.sha, size: f.size }))
30
76
 
31
77
  // Step 1: Initiate
@@ -195,7 +195,8 @@ const run = (args) => catch_errors('Publish failed', async () => {
195
195
  files: skill_files,
196
196
  visibility,
197
197
  base_commit: force ? null : base_commit,
198
- force
198
+ force,
199
+ source_dir: dir
199
200
  }
200
201
  if (merge_parents && !force) {
201
202
  push_options.parent_shas = merge_parents
@@ -0,0 +1,24 @@
1
+ const { execFile } = require('child_process')
2
+ const { promisify } = require('util')
3
+ const crypto = require('crypto')
4
+ const fs = require('fs')
5
+ const os = require('os')
6
+ const path = require('path')
7
+ const { error: { catch_errors } } = require('puffy-core')
8
+
9
+ const exec_file = promisify(execFile)
10
+
11
+ const create_archive = (source_dir) =>
12
+ catch_errors('Failed to create archive', async () => {
13
+ const archive_path = path.join(os.tmpdir(), `happyskills-${crypto.randomUUID()}.tar.gz`)
14
+ await exec_file('tar', ['czf', archive_path, '--exclude', '._*', '--exclude', '.DS_Store', '-C', source_dir, '.'])
15
+ const buffer = await fs.promises.readFile(archive_path)
16
+ const sha = crypto.createHash('sha256').update(buffer).digest('hex')
17
+ return { path: archive_path, buffer, sha, size: buffer.length }
18
+ })
19
+
20
+ const cleanup_archive = (archive_path) => {
21
+ fs.promises.unlink(archive_path).catch(() => {})
22
+ }
23
+
24
+ module.exports = { create_archive, cleanup_archive }