happyskills 0.39.1 → 0.40.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 +9 -0
- package/package.json +1 -1
- package/src/api/push.js +8 -3
- package/src/commands/publish.js +11 -1
- package/src/commands/publish.test.js +21 -0
- package/src/manifest/validator.js +5 -1
- package/src/manifest/validator.test.js +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.40.0] - 2026-05-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Propagate the fork relationship to the server on the first `publish` after `happyskills fork`. The CLI now reads `manifest.forked_from.repo` (an `owner/name` string written by the fork command), splits it into `{ workspace, name }`, and includes it in the push payload across all three transport modes: direct push, archive upload, and per-file presigned upload. The API resolves the parent and persists `repos.forked_from`, which feeds the duplicate-detection heuristic (forks-of-active-parents are ineligible to be elected source-of-truth, and the publish-time `duplicates` warning is suppressed when the only match is the fork's parent). Subsequent publishes of an existing repo are unaffected — the field is honored only on the create-repo path. Requires API ≥ 2.4.0.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Fix `publish` crashing with a `ReferenceError`/TDZ on `visibility` when validation runs before the spinner update — the `const visibility = args.flags.public ? 'public' : 'private'` declaration was below its first use. Move the declaration ahead of all reads.
|
|
17
|
+
- Fix `validate_manifest` rejecting kit names like `_kit-foo` ("Invalid name … Must start with a letter or number"). Kits are required by `init --kit` and `validate_skill_json` to start with `_kit-`, so the leading-character rule is now carved out for `manifest.type === 'kit'` via a separate `KIT_NAME_PATTERN`. Bumping a kit no longer fails its own pre-publish manifest validation.
|
|
18
|
+
|
|
10
19
|
## [0.39.1] - 2026-05-01
|
|
11
20
|
|
|
12
21
|
### Fixed
|
package/package.json
CHANGED
package/src/api/push.js
CHANGED
|
@@ -13,7 +13,7 @@ const estimate_payload_size = (files) => {
|
|
|
13
13
|
return size
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const archive_push = (owner, repo, { version, message, files, visibility, base_commit, force, parent_shas, source_dir }, on_progress) =>
|
|
16
|
+
const archive_push = (owner, repo, { version, message, files, visibility, base_commit, force, parent_shas, forked_from, source_dir }, on_progress) =>
|
|
17
17
|
catch_errors('Archive push failed', async () => {
|
|
18
18
|
const file_meta = files.map(f => ({ path: f.path, sha: f.sha, size: f.size }))
|
|
19
19
|
|
|
@@ -28,6 +28,7 @@ const archive_push = (owner, repo, { version, message, files, visibility, base_c
|
|
|
28
28
|
archive: true, archive_sha: archive.sha, archive_size: archive.size
|
|
29
29
|
}
|
|
30
30
|
if (parent_shas) init_body.parent_shas = parent_shas
|
|
31
|
+
if (forked_from) init_body.forked_from = forked_from
|
|
31
32
|
const [init_err, init_data] = await initiate_upload(owner, repo, init_body)
|
|
32
33
|
if (init_err) throw e('Upload initiation failed', init_err)
|
|
33
34
|
|
|
@@ -44,6 +45,7 @@ const archive_push = (owner, repo, { version, message, files, visibility, base_c
|
|
|
44
45
|
version, message, files: file_meta, base_commit, force
|
|
45
46
|
}
|
|
46
47
|
if (parent_shas) complete_body.parent_shas = parent_shas
|
|
48
|
+
if (forked_from) complete_body.forked_from = forked_from
|
|
47
49
|
const [complete_err, complete_data] = await complete_upload(owner, repo, complete_body)
|
|
48
50
|
if (complete_err) throw e('Upload completion failed', complete_err)
|
|
49
51
|
|
|
@@ -62,7 +64,7 @@ const archive_push = (owner, repo, { version, message, files, visibility, base_c
|
|
|
62
64
|
}
|
|
63
65
|
})
|
|
64
66
|
|
|
65
|
-
const smart_push = (owner, repo, { version, message, files, visibility, base_commit, force, parent_shas, source_dir }, on_progress) =>
|
|
67
|
+
const smart_push = (owner, repo, { version, message, files, visibility, base_commit, force, parent_shas, forked_from, source_dir }, on_progress) =>
|
|
66
68
|
catch_errors('Smart push failed', async () => {
|
|
67
69
|
const payload_size = estimate_payload_size(files)
|
|
68
70
|
|
|
@@ -70,6 +72,7 @@ const smart_push = (owner, repo, { version, message, files, visibility, base_com
|
|
|
70
72
|
// Small payload — use direct push
|
|
71
73
|
const body = { version, message, files, visibility, base_commit, force }
|
|
72
74
|
if (parent_shas) body.parent_shas = parent_shas
|
|
75
|
+
if (forked_from) body.forked_from = forked_from
|
|
73
76
|
const [err, data] = await repos_api.push(owner, repo, body)
|
|
74
77
|
if (err) throw e('Direct push failed', err)
|
|
75
78
|
return data
|
|
@@ -77,7 +80,7 @@ const smart_push = (owner, repo, { version, message, files, visibility, base_com
|
|
|
77
80
|
|
|
78
81
|
// Large payload — prefer archive upload if source_dir is available
|
|
79
82
|
if (source_dir) {
|
|
80
|
-
return await archive_push(owner, repo, { version, message, files, visibility, base_commit, force, parent_shas, source_dir }, on_progress)
|
|
83
|
+
return await archive_push(owner, repo, { version, message, files, visibility, base_commit, force, parent_shas, forked_from, source_dir }, on_progress)
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
// Fallback: per-file presigned upload
|
|
@@ -86,6 +89,7 @@ const smart_push = (owner, repo, { version, message, files, visibility, base_com
|
|
|
86
89
|
// Step 1: Initiate
|
|
87
90
|
const init_body = { version, message, files: file_meta, visibility, base_commit, force }
|
|
88
91
|
if (parent_shas) init_body.parent_shas = parent_shas
|
|
92
|
+
if (forked_from) init_body.forked_from = forked_from
|
|
89
93
|
const [init_err, init_data] = await initiate_upload(owner, repo, init_body)
|
|
90
94
|
if (init_err) throw e('Upload initiation failed', init_err)
|
|
91
95
|
|
|
@@ -111,6 +115,7 @@ const smart_push = (owner, repo, { version, message, files, visibility, base_com
|
|
|
111
115
|
// Step 3: Complete
|
|
112
116
|
const complete_body = { upload_id, version, message, files: file_meta, base_commit, force }
|
|
113
117
|
if (parent_shas) complete_body.parent_shas = parent_shas
|
|
118
|
+
if (forked_from) complete_body.forked_from = forked_from
|
|
114
119
|
const [complete_err, complete_data] = await complete_upload(owner, repo, complete_body)
|
|
115
120
|
if (complete_err) throw e('Upload completion failed', complete_err)
|
|
116
121
|
|
package/src/commands/publish.js
CHANGED
|
@@ -135,6 +135,8 @@ const run = (args) => catch_errors('Publish failed', async () => {
|
|
|
135
135
|
|
|
136
136
|
const spinner = create_spinner('Preparing to publish...')
|
|
137
137
|
|
|
138
|
+
const visibility = args.flags.public ? 'public' : 'private'
|
|
139
|
+
|
|
138
140
|
let owner = args.flags.workspace
|
|
139
141
|
if (!owner) {
|
|
140
142
|
const [, resolved_owner] = await resolve_skill_owner(skill_name)
|
|
@@ -221,7 +223,6 @@ const run = (args) => catch_errors('Publish failed', async () => {
|
|
|
221
223
|
spinner.update(`Uploading files (${completed}/${total})...`)
|
|
222
224
|
}
|
|
223
225
|
}
|
|
224
|
-
const visibility = args.flags.public ? 'public' : 'private'
|
|
225
226
|
const push_options = {
|
|
226
227
|
version: manifest.version,
|
|
227
228
|
message: `Release ${manifest.version}`,
|
|
@@ -234,6 +235,15 @@ const run = (args) => catch_errors('Publish failed', async () => {
|
|
|
234
235
|
if (merge_parents && !force) {
|
|
235
236
|
push_options.parent_shas = merge_parents
|
|
236
237
|
}
|
|
238
|
+
|
|
239
|
+
// Propagate fork relationship to the server on the first publish.
|
|
240
|
+
// Only the create-repo path uses this; existing repos already have forked_from set.
|
|
241
|
+
if (manifest.forked_from && typeof manifest.forked_from.repo === 'string' && manifest.forked_from.repo.includes('/')) {
|
|
242
|
+
const [parent_workspace, parent_name] = manifest.forked_from.repo.split('/')
|
|
243
|
+
if (parent_workspace && parent_name) {
|
|
244
|
+
push_options.forked_from = { workspace: parent_workspace, name: parent_name }
|
|
245
|
+
}
|
|
246
|
+
}
|
|
237
247
|
const [push_err, push_data] = await smart_push(workspace.slug, manifest.name, push_options, on_progress)
|
|
238
248
|
if (push_err) {
|
|
239
249
|
const last = push_err[push_err.length - 1]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { describe, it } = require('node:test')
|
|
2
|
+
const assert = require('node:assert')
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
|
|
6
|
+
describe('publish.js — Bug 1 regression: visibility temporal-dead-zone', () => {
|
|
7
|
+
const src = fs.readFileSync(path.join(__dirname, 'publish.js'), 'utf8')
|
|
8
|
+
|
|
9
|
+
it('declares `visibility` before passing it to validate_dependencies', () => {
|
|
10
|
+
const decl_idx = src.search(/const\s+visibility\s*=\s*args\.flags\.public/)
|
|
11
|
+
const use_idx = src.search(/validate_dependencies\([\s\S]*?visibility/)
|
|
12
|
+
assert.notStrictEqual(decl_idx, -1, '`const visibility = args.flags.public ? ...` declaration not found')
|
|
13
|
+
assert.notStrictEqual(use_idx, -1, '`validate_dependencies(... visibility ...)` call not found')
|
|
14
|
+
assert.ok(
|
|
15
|
+
decl_idx < use_idx,
|
|
16
|
+
`visibility must be declared (idx ${decl_idx}) before it is read inside validate_dependencies (idx ${use_idx}). ` +
|
|
17
|
+
'The current ordering causes `ReferenceError: Cannot access \'visibility\' before initialization` ' +
|
|
18
|
+
'whenever skill.json declares one or more dependencies.'
|
|
19
|
+
)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -2,6 +2,9 @@ const { valid } = require('../utils/semver')
|
|
|
2
2
|
|
|
3
3
|
const REQUIRED_FIELDS = ['name', 'version']
|
|
4
4
|
const NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/
|
|
5
|
+
// Kits are required to start with `_kit-` (enforced by `init --kit` and
|
|
6
|
+
// validate_skill_json). Carve out the leading-character rule for them.
|
|
7
|
+
const KIT_NAME_PATTERN = /^_kit-[a-z0-9][a-z0-9-]*$/
|
|
5
8
|
|
|
6
9
|
const validate_manifest = (manifest) => {
|
|
7
10
|
const errors = []
|
|
@@ -12,7 +15,8 @@ const validate_manifest = (manifest) => {
|
|
|
12
15
|
}
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
const name_pattern = manifest.type === 'kit' ? KIT_NAME_PATTERN : NAME_PATTERN
|
|
19
|
+
if (manifest.name && !name_pattern.test(manifest.name)) {
|
|
16
20
|
errors.push(`Invalid name "${manifest.name}". Use lowercase letters, numbers, hyphens, and underscores. Must start with a letter or number.`)
|
|
17
21
|
}
|
|
18
22
|
|
|
@@ -98,4 +98,26 @@ describe('validate_manifest', () => {
|
|
|
98
98
|
const result = validate_manifest({ name: 'my-skill', version: '1.0.0' })
|
|
99
99
|
assert.strictEqual(result.valid, true)
|
|
100
100
|
})
|
|
101
|
+
|
|
102
|
+
// Bug 2 regression — kit names start with `_kit-` (enforced by `init --kit` and
|
|
103
|
+
// `validate_skill_json` for type==='kit'). The shared `validate_manifest` used
|
|
104
|
+
// by `bump` must accept this convention or the standard release workflow breaks.
|
|
105
|
+
describe('kit name convention', () => {
|
|
106
|
+
it('accepts a kit name beginning with _kit- when type is "kit"', () => {
|
|
107
|
+
const result = validate_manifest({ name: '_kit-mykit', version: '1.0.0', type: 'kit' })
|
|
108
|
+
assert.strictEqual(result.valid, true, `expected valid, got errors: ${result.errors.join(', ')}`)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('still rejects a non-kit manifest whose name starts with _', () => {
|
|
112
|
+
const result = validate_manifest({ name: '_oops', version: '1.0.0' })
|
|
113
|
+
assert.strictEqual(result.valid, false)
|
|
114
|
+
assert.ok(result.errors.some(e => e.includes('Invalid name')))
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('still rejects a non-kit manifest whose name starts with _ even if type=skill', () => {
|
|
118
|
+
const result = validate_manifest({ name: '_oops', version: '1.0.0', type: 'skill' })
|
|
119
|
+
assert.strictEqual(result.valid, false)
|
|
120
|
+
assert.ok(result.errors.some(e => e.includes('Invalid name')))
|
|
121
|
+
})
|
|
122
|
+
})
|
|
101
123
|
})
|