happyskills 0.11.0 → 0.12.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
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.12.0] - 2026-03-12
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Add `_kit-` naming convention for kits — `init --kit` and `fork` auto-prepend the `_kit-` prefix to the name if not already present
|
|
14
|
+
- Add `KIT_PREFIX` constant (`_kit-`) exported from `constants.js`
|
|
15
|
+
- Add `kit_prefix` validation rule — errors when a kit name is missing the `_kit-` prefix, or when a non-kit name uses the reserved prefix
|
|
16
|
+
- Add `KIT_NAME_PATTERN` regex (`/^_kit-[a-z0-9][a-z0-9-]*$/`) for validating kit names
|
|
17
|
+
|
|
18
|
+
## [0.11.1] - 2026-03-12
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- Fix `init` not setting `type: "skill"` in skill.json for regular skills — only kits were getting the `type` field; now all scaffolded skills explicitly set `type`
|
|
22
|
+
- Fix `convert` not setting `type` in skill.json — converted skills now preserve existing `type` or default to `"skill"`
|
|
23
|
+
- Fix `fork` not setting `type` in skill.json — forked skills now preserve the original skill's `type` or default to `"skill"`
|
|
24
|
+
|
|
10
25
|
## [0.11.0] - 2026-03-11
|
|
11
26
|
|
|
12
27
|
### Added
|
package/package.json
CHANGED
package/src/commands/convert.js
CHANGED
|
@@ -16,7 +16,7 @@ const { file_exists } = require('../utils/fs')
|
|
|
16
16
|
const { create_spinner } = require('../ui/spinner')
|
|
17
17
|
const { print_help, print_success, print_error, print_info, print_warn, print_label, print_json } = require('../ui/output')
|
|
18
18
|
const { exit_with_error, UsageError, CliError } = require('../utils/errors')
|
|
19
|
-
const { EXIT_CODES, SKILL_MD } = require('../constants')
|
|
19
|
+
const { EXIT_CODES, SKILL_MD, SKILL_TYPES } = require('../constants')
|
|
20
20
|
|
|
21
21
|
const HELP_TEXT = `Usage: happyskills convert <skill-name> [options]
|
|
22
22
|
|
|
@@ -162,6 +162,7 @@ const run = (args) => catch_errors('Convert failed', async () => {
|
|
|
162
162
|
...(existing_manifest || {}),
|
|
163
163
|
name: skill_name,
|
|
164
164
|
version: merged_version,
|
|
165
|
+
type: (existing_manifest?.type) || SKILL_TYPES.SKILL,
|
|
165
166
|
description: merged_description,
|
|
166
167
|
keywords: merged_keywords
|
|
167
168
|
}
|
package/src/commands/fork.js
CHANGED
|
@@ -8,7 +8,8 @@ const { write_manifest } = require('../manifest/writer')
|
|
|
8
8
|
const { create_spinner } = require('../ui/spinner')
|
|
9
9
|
const { print_help, print_success, print_hint, print_json, code } = require('../ui/output')
|
|
10
10
|
const { exit_with_error, UsageError } = require('../utils/errors')
|
|
11
|
-
const { EXIT_CODES } = require('../constants')
|
|
11
|
+
const { EXIT_CODES, SKILL_TYPES, KIT_PREFIX } = require('../constants')
|
|
12
|
+
const { read_manifest } = require('../manifest/reader')
|
|
12
13
|
|
|
13
14
|
const HELP_TEXT = `Usage: happyskills fork <owner/skill> [options]
|
|
14
15
|
|
|
@@ -79,9 +80,13 @@ const run = (args) => catch_errors('Fork failed', async () => {
|
|
|
79
80
|
const [ext_err] = await extract(clone_data, dest)
|
|
80
81
|
if (ext_err) { spinner.fail('Extract failed'); throw e('Extract failed', ext_err) }
|
|
81
82
|
|
|
83
|
+
const [, original_manifest] = await read_manifest(dest)
|
|
84
|
+
const is_kit = (original_manifest?.type) === SKILL_TYPES.KIT
|
|
85
|
+
const forked_name = is_kit && !name.startsWith(KIT_PREFIX) ? `${KIT_PREFIX}${name}` : name
|
|
82
86
|
const forked_manifest = {
|
|
83
|
-
name,
|
|
87
|
+
name: forked_name,
|
|
84
88
|
version: '0.1.0',
|
|
89
|
+
type: is_kit ? SKILL_TYPES.KIT : SKILL_TYPES.SKILL,
|
|
85
90
|
description: '',
|
|
86
91
|
keywords: [],
|
|
87
92
|
forked_from: {
|
package/src/commands/init.js
CHANGED
|
@@ -4,7 +4,7 @@ const { write_manifest } = require('../manifest/writer')
|
|
|
4
4
|
const { write_file, file_exists, ensure_dir } = require('../utils/fs')
|
|
5
5
|
const { print_success, print_error, print_help, print_hint, print_json, code } = require('../ui/output')
|
|
6
6
|
const { exit_with_error, CliError, UsageError } = require('../utils/errors')
|
|
7
|
-
const { SKILL_JSON, SKILL_MD, EXIT_CODES, SKILL_TYPES } = require('../constants')
|
|
7
|
+
const { SKILL_JSON, SKILL_MD, EXIT_CODES, SKILL_TYPES, KIT_PREFIX } = require('../constants')
|
|
8
8
|
const { find_project_root, skills_dir } = require('../config/paths')
|
|
9
9
|
|
|
10
10
|
const HELP_TEXT = `Usage: happyskills init <name> [options]
|
|
@@ -58,7 +58,7 @@ const create_manifest = (name, is_kit = false) => ({
|
|
|
58
58
|
version: '0.1.0',
|
|
59
59
|
description: '',
|
|
60
60
|
keywords: [],
|
|
61
|
-
|
|
61
|
+
type: is_kit ? SKILL_TYPES.KIT : SKILL_TYPES.SKILL,
|
|
62
62
|
dependencies: {}
|
|
63
63
|
})
|
|
64
64
|
|
|
@@ -75,9 +75,10 @@ const run = (args) => catch_errors('Init failed', async () => {
|
|
|
75
75
|
|
|
76
76
|
const is_global = args.flags.global || false
|
|
77
77
|
const is_kit = args.flags.kit || false
|
|
78
|
+
const final_name = is_kit && !name.startsWith(KIT_PREFIX) ? `${KIT_PREFIX}${name}` : name
|
|
78
79
|
const project_root = find_project_root()
|
|
79
80
|
const base_skills_dir = skills_dir(is_global, project_root)
|
|
80
|
-
const dir = path.join(base_skills_dir,
|
|
81
|
+
const dir = path.join(base_skills_dir, final_name)
|
|
81
82
|
|
|
82
83
|
const [, json_exists] = await file_exists(path.join(dir, SKILL_JSON))
|
|
83
84
|
if (json_exists) {
|
|
@@ -87,7 +88,7 @@ const run = (args) => catch_errors('Init failed', async () => {
|
|
|
87
88
|
const [dir_err] = await ensure_dir(dir)
|
|
88
89
|
if (dir_err) throw e('Failed to create skill directory', dir_err)
|
|
89
90
|
|
|
90
|
-
const manifest = create_manifest(
|
|
91
|
+
const manifest = create_manifest(final_name, is_kit)
|
|
91
92
|
const [write_err] = await write_manifest(dir, manifest)
|
|
92
93
|
if (write_err) throw e('Failed to write manifest', write_err)
|
|
93
94
|
|
|
@@ -95,7 +96,7 @@ const run = (args) => catch_errors('Init failed', async () => {
|
|
|
95
96
|
|
|
96
97
|
const [, md_exists] = await file_exists(path.join(dir, SKILL_MD))
|
|
97
98
|
if (!md_exists) {
|
|
98
|
-
const md_content = is_kit ? create_kit_md(
|
|
99
|
+
const md_content = is_kit ? create_kit_md(final_name) : create_skill_md(final_name)
|
|
99
100
|
const [md_err] = await write_file(path.join(dir, SKILL_MD), md_content)
|
|
100
101
|
if (md_err) throw e('Failed to write SKILL.md', md_err)
|
|
101
102
|
files_created.push(SKILL_MD)
|
|
@@ -104,11 +105,11 @@ const run = (args) => catch_errors('Init failed', async () => {
|
|
|
104
105
|
const label = is_kit ? 'kit' : 'skill'
|
|
105
106
|
|
|
106
107
|
if (args.flags.json) {
|
|
107
|
-
print_json({ data: { name, type: is_kit ? SKILL_TYPES.KIT : SKILL_TYPES.SKILL, files_created, directory: dir } })
|
|
108
|
+
print_json({ data: { name: final_name, type: is_kit ? SKILL_TYPES.KIT : SKILL_TYPES.SKILL, files_created, directory: dir } })
|
|
108
109
|
return
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
print_success(`Initialized ${label} "${
|
|
112
|
+
print_success(`Initialized ${label} "${final_name}" at ${dir}`)
|
|
112
113
|
console.log(` ${SKILL_JSON} — manifest`)
|
|
113
114
|
console.log(` ${SKILL_MD} — ${is_kit ? 'kit description' : 'instructions'}`)
|
|
114
115
|
console.log()
|
package/src/constants.js
CHANGED
|
@@ -13,6 +13,7 @@ const EXIT_CODES = {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const SKILL_TYPES = { SKILL: 'skill', KIT: 'kit' }
|
|
16
|
+
const KIT_PREFIX = '_kit-'
|
|
16
17
|
const VALID_SKILL_TYPES = ['skill', 'kit']
|
|
17
18
|
|
|
18
19
|
const LOCK_VERSION = 1
|
|
@@ -63,6 +64,7 @@ module.exports = {
|
|
|
63
64
|
CLI_VERSION,
|
|
64
65
|
EXIT_CODES,
|
|
65
66
|
SKILL_TYPES,
|
|
67
|
+
KIT_PREFIX,
|
|
66
68
|
VALID_SKILL_TYPES,
|
|
67
69
|
LOCK_VERSION,
|
|
68
70
|
SKILL_JSON,
|
|
@@ -2,9 +2,10 @@ const path = require('path')
|
|
|
2
2
|
const { error: { catch_errors } } = require('puffy-core')
|
|
3
3
|
const { file_exists, read_file } = require('../utils/fs')
|
|
4
4
|
const { valid } = require('../utils/semver')
|
|
5
|
-
const { SKILL_JSON, VALID_SKILL_TYPES, SKILL_TYPES } = require('../constants')
|
|
5
|
+
const { SKILL_JSON, VALID_SKILL_TYPES, SKILL_TYPES, KIT_PREFIX } = require('../constants')
|
|
6
6
|
|
|
7
7
|
const NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/
|
|
8
|
+
const KIT_NAME_PATTERN = /^_kit-[a-z0-9][a-z0-9-]*$/
|
|
8
9
|
const CANONICAL_SLUGS = ['deployment', 'database', 'security', 'ai', 'api', 'monitoring', 'testing', 'devops', 'cloud', 'analytics']
|
|
9
10
|
const REQUIRED_PLATFORMS = ['darwin', 'linux', 'win32']
|
|
10
11
|
|
|
@@ -15,6 +16,7 @@ const result = (field, rule, severity, message, value) => ({
|
|
|
15
16
|
const validate_name = (manifest) => {
|
|
16
17
|
const results = []
|
|
17
18
|
const name = manifest.name
|
|
19
|
+
const is_kit = manifest.type === SKILL_TYPES.KIT
|
|
18
20
|
|
|
19
21
|
if (!name) {
|
|
20
22
|
results.push(result('name', 'present', 'error', 'skill.json must include a "name" field'))
|
|
@@ -23,10 +25,26 @@ const validate_name = (manifest) => {
|
|
|
23
25
|
|
|
24
26
|
results.push(result('name', 'present', 'pass', `name: "${name}"`, name))
|
|
25
27
|
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
+
if (is_kit) {
|
|
29
|
+
if (!KIT_NAME_PATTERN.test(name)) {
|
|
30
|
+
results.push(result('name', 'format', 'error', `Kit name must match ${KIT_NAME_PATTERN}`, name))
|
|
31
|
+
} else {
|
|
32
|
+
results.push(result('name', 'format', 'pass', 'Name format valid', name))
|
|
33
|
+
}
|
|
34
|
+
if (!name.startsWith(KIT_PREFIX)) {
|
|
35
|
+
results.push(result('name', 'kit_prefix', 'error', `Kit name "${name}" must start with "${KIT_PREFIX}". Expected: "${KIT_PREFIX}${name}"`, name))
|
|
36
|
+
} else {
|
|
37
|
+
results.push(result('name', 'kit_prefix', 'pass', `Name starts with "${KIT_PREFIX}"`, name))
|
|
38
|
+
}
|
|
28
39
|
} else {
|
|
29
|
-
|
|
40
|
+
if (!NAME_PATTERN.test(name)) {
|
|
41
|
+
results.push(result('name', 'format', 'error', 'Name must match /^[a-z0-9][a-z0-9_-]*$/', name))
|
|
42
|
+
} else {
|
|
43
|
+
results.push(result('name', 'format', 'pass', 'Name format valid', name))
|
|
44
|
+
}
|
|
45
|
+
if (name.startsWith(KIT_PREFIX)) {
|
|
46
|
+
results.push(result('name', 'kit_prefix', 'error', `Name "${name}" uses the reserved "${KIT_PREFIX}" prefix but type is "${manifest.type || 'skill'}". The "${KIT_PREFIX}" prefix is reserved for kits (type: "kit")`, name))
|
|
47
|
+
}
|
|
30
48
|
}
|
|
31
49
|
|
|
32
50
|
return results
|
|
@@ -205,7 +205,7 @@ describe('validate_skill_json — type', () => {
|
|
|
205
205
|
})
|
|
206
206
|
|
|
207
207
|
it('passes for type "kit"', async () => {
|
|
208
|
-
write_json(tmp, { name: 'my-
|
|
208
|
+
write_json(tmp, { name: '_kit-my-bundle', version: '1.0.0', type: 'kit' })
|
|
209
209
|
const [err, data] = await validate_skill_json(tmp)
|
|
210
210
|
assert.ifError(err)
|
|
211
211
|
const check = data.results.find(r => r.field === 'type' && r.rule === 'valid_value')
|
|
@@ -224,7 +224,7 @@ describe('validate_skill_json — type', () => {
|
|
|
224
224
|
|
|
225
225
|
describe('validate_skill_json — kit dependencies', () => {
|
|
226
226
|
it('warns when kit has no dependencies', async () => {
|
|
227
|
-
write_json(tmp, { name: 'my-
|
|
227
|
+
write_json(tmp, { name: '_kit-my-bundle', version: '1.0.0', type: 'kit' })
|
|
228
228
|
const [err, data] = await validate_skill_json(tmp)
|
|
229
229
|
assert.ifError(err)
|
|
230
230
|
const check = data.results.find(r => r.rule === 'kit_has_deps')
|
|
@@ -233,7 +233,7 @@ describe('validate_skill_json — kit dependencies', () => {
|
|
|
233
233
|
})
|
|
234
234
|
|
|
235
235
|
it('warns when kit has empty dependencies object', async () => {
|
|
236
|
-
write_json(tmp, { name: 'my-
|
|
236
|
+
write_json(tmp, { name: '_kit-my-bundle', version: '1.0.0', type: 'kit', dependencies: {} })
|
|
237
237
|
const [err, data] = await validate_skill_json(tmp)
|
|
238
238
|
assert.ifError(err)
|
|
239
239
|
const check = data.results.find(r => r.rule === 'kit_has_deps')
|
|
@@ -241,7 +241,7 @@ describe('validate_skill_json — kit dependencies', () => {
|
|
|
241
241
|
})
|
|
242
242
|
|
|
243
243
|
it('does not warn when kit has dependencies', async () => {
|
|
244
|
-
write_json(tmp, { name: 'my-
|
|
244
|
+
write_json(tmp, { name: '_kit-my-bundle', version: '1.0.0', type: 'kit', dependencies: { 'acme/deploy': '^1.0.0' } })
|
|
245
245
|
const [err, data] = await validate_skill_json(tmp)
|
|
246
246
|
assert.ifError(err)
|
|
247
247
|
assert.ok(!data.results.some(r => r.rule === 'kit_has_deps'))
|
|
@@ -255,6 +255,61 @@ describe('validate_skill_json — kit dependencies', () => {
|
|
|
255
255
|
})
|
|
256
256
|
})
|
|
257
257
|
|
|
258
|
+
describe('validate_skill_json — kit prefix', () => {
|
|
259
|
+
it('passes for kit with _kit- prefix', async () => {
|
|
260
|
+
write_json(tmp, { name: '_kit-my-bundle', version: '1.0.0', type: 'kit' })
|
|
261
|
+
const [err, data] = await validate_skill_json(tmp)
|
|
262
|
+
assert.ifError(err)
|
|
263
|
+
const format_check = data.results.find(r => r.field === 'name' && r.rule === 'format')
|
|
264
|
+
assert.strictEqual(format_check.severity, 'pass')
|
|
265
|
+
const prefix_check = data.results.find(r => r.field === 'name' && r.rule === 'kit_prefix')
|
|
266
|
+
assert.strictEqual(prefix_check.severity, 'pass')
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('errors for kit without _kit- prefix', async () => {
|
|
270
|
+
write_json(tmp, { name: 'my-bundle', version: '1.0.0', type: 'kit' })
|
|
271
|
+
const [err, data] = await validate_skill_json(tmp)
|
|
272
|
+
assert.ifError(err)
|
|
273
|
+
const check = data.results.find(r => r.field === 'name' && r.rule === 'kit_prefix')
|
|
274
|
+
assert.strictEqual(check.severity, 'error')
|
|
275
|
+
assert.ok(check.message.includes('must start with'))
|
|
276
|
+
assert.ok(check.message.includes('_kit-my-bundle'))
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('errors for non-kit with _kit- prefix', async () => {
|
|
280
|
+
write_json(tmp, { name: '_kit-foo', version: '1.0.0', type: 'skill' })
|
|
281
|
+
const [err, data] = await validate_skill_json(tmp)
|
|
282
|
+
assert.ifError(err)
|
|
283
|
+
const check = data.results.find(r => r.field === 'name' && r.rule === 'kit_prefix')
|
|
284
|
+
assert.strictEqual(check.severity, 'error')
|
|
285
|
+
assert.ok(check.message.includes('reserved'))
|
|
286
|
+
assert.ok(check.message.includes('type is "skill"'))
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it('errors for _kit- alone as kit name', async () => {
|
|
290
|
+
write_json(tmp, { name: '_kit-', version: '1.0.0', type: 'kit' })
|
|
291
|
+
const [err, data] = await validate_skill_json(tmp)
|
|
292
|
+
assert.ifError(err)
|
|
293
|
+
const check = data.results.find(r => r.field === 'name' && r.rule === 'format')
|
|
294
|
+
assert.strictEqual(check.severity, 'error')
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('errors for kit name with uppercase after prefix', async () => {
|
|
298
|
+
write_json(tmp, { name: '_kit-A', version: '1.0.0', type: 'kit' })
|
|
299
|
+
const [err, data] = await validate_skill_json(tmp)
|
|
300
|
+
assert.ifError(err)
|
|
301
|
+
const check = data.results.find(r => r.field === 'name' && r.rule === 'format')
|
|
302
|
+
assert.strictEqual(check.severity, 'error')
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('does not produce kit_prefix error for non-kit without _kit- prefix', async () => {
|
|
306
|
+
write_json(tmp, { name: 'my-skill', version: '1.0.0', type: 'skill' })
|
|
307
|
+
const [err, data] = await validate_skill_json(tmp)
|
|
308
|
+
assert.ifError(err)
|
|
309
|
+
assert.ok(!data.results.some(r => r.rule === 'kit_prefix'))
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
258
313
|
describe('validate_skill_json — systemDependencies', () => {
|
|
259
314
|
it('errors when systemDependencies is an array', async () => {
|
|
260
315
|
write_json(tmp, { name: 'my-skill', version: '1.0.0', systemDependencies: [] })
|