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 +10 -0
- package/package.json +1 -1
- package/src/api/push.js +49 -3
- package/src/commands/publish.js +2 -1
- package/src/utils/archive.js +24 -0
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
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
|
|
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 —
|
|
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
|
package/src/commands/publish.js
CHANGED
|
@@ -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 }
|