happyskills 0.27.1 → 0.28.0
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 +11 -0
- package/package.json +1 -1
- package/src/api/push.js +49 -3
- package/src/commands/publish.js +4 -2
- package/src/commands/refresh.js +23 -0
- package/src/utils/archive.js +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.28.0] - 2026-04-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- 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
|
|
14
|
+
|
|
15
|
+
## [0.27.2] - 2026-04-02
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Fix `publish` not syncing dependency declarations to the lock file when publishing an existing skill — the lock entry's `dependencies` field was stale after publish
|
|
19
|
+
- Fix `refresh` not detecting dependency drift — skills whose lock `dependencies` diverged from the installed `skill.json` were incorrectly reported as up-to-date because the staleness check was purely commit-based
|
|
20
|
+
|
|
10
21
|
## [0.27.1] - 2026-04-02
|
|
11
22
|
|
|
12
23
|
### 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
|
|
@@ -229,7 +230,8 @@ const run = (args) => catch_errors('Publish failed', async () => {
|
|
|
229
230
|
ref: push_data?.ref || `refs/tags/v${manifest.version}`,
|
|
230
231
|
commit: push_data?.commit || null,
|
|
231
232
|
base_commit: push_data?.commit || null,
|
|
232
|
-
base_integrity: (!hash_err && integrity) ? integrity : null
|
|
233
|
+
base_integrity: (!hash_err && integrity) ? integrity : null,
|
|
234
|
+
dependencies: manifest.dependencies || {}
|
|
233
235
|
}
|
|
234
236
|
if (!hash_err && integrity) updated_entry.integrity = integrity
|
|
235
237
|
delete updated_entry.merge_parents
|
package/src/commands/refresh.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
|
|
2
2
|
const { install } = require('../engine/installer')
|
|
3
3
|
const { read_lock, get_all_locked_skills } = require('../lock/reader')
|
|
4
|
+
const { read_manifest } = require('../manifest/reader')
|
|
4
5
|
const { detect_status } = require('../merge/detector')
|
|
5
6
|
const repos_api = require('../api/repos')
|
|
6
7
|
const { print_help, print_table, print_json, print_info, print_success, print_warn, print_hint, code } = require('../ui/output')
|
|
@@ -26,6 +27,16 @@ Examples:
|
|
|
26
27
|
happyskills refresh -y
|
|
27
28
|
happyskills refresh -g -y --json`
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Returns true if the lock entry's dependencies differ from the installed skill.json's dependencies.
|
|
32
|
+
*/
|
|
33
|
+
const has_dependency_drift = (lock_entry, manifest) => {
|
|
34
|
+
if (!manifest) return false
|
|
35
|
+
const lock_deps = JSON.stringify(lock_entry.dependencies || {})
|
|
36
|
+
const manifest_deps = JSON.stringify(manifest.dependencies || {})
|
|
37
|
+
return lock_deps !== manifest_deps
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
const confirm_prompt = (question) => new Promise((resolve) => {
|
|
30
41
|
const readline = require('readline')
|
|
31
42
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
|
|
@@ -72,6 +83,15 @@ const run = (args) => catch_errors('Refresh failed', async () => {
|
|
|
72
83
|
}
|
|
73
84
|
} else {
|
|
74
85
|
spinner?.succeed('Checked for updates')
|
|
86
|
+
const base_dir = skills_dir(is_global, project_root)
|
|
87
|
+
// Read all installed manifests in parallel to check for dependency drift
|
|
88
|
+
const manifest_map = {}
|
|
89
|
+
await Promise.all(to_check.map(async ([name]) => {
|
|
90
|
+
const short_name = name.split('/')[1] || name
|
|
91
|
+
const dir = skill_install_dir(base_dir, short_name)
|
|
92
|
+
const [, manifest] = await read_manifest(dir)
|
|
93
|
+
if (manifest) manifest_map[name] = manifest
|
|
94
|
+
}))
|
|
75
95
|
for (const [name, data] of to_check) {
|
|
76
96
|
const info = batch_data?.results?.[name]
|
|
77
97
|
if (info?.access_denied) {
|
|
@@ -83,6 +103,9 @@ const run = (args) => catch_errors('Refresh failed', async () => {
|
|
|
83
103
|
} else if (!data.base_commit && info.latest_version !== data.version) {
|
|
84
104
|
// Fallback to version comparison for old lock files without base_commit
|
|
85
105
|
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'outdated' })
|
|
106
|
+
} else if (has_dependency_drift(data, manifest_map[name])) {
|
|
107
|
+
// Lock dependencies are stale compared to installed skill.json
|
|
108
|
+
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'outdated' })
|
|
86
109
|
} else {
|
|
87
110
|
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'up-to-date' })
|
|
88
111
|
}
|
|
@@ -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, '-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 }
|