happyskills 0.7.3 → 0.9.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 +20 -0
- package/package.json +1 -1
- package/src/api/repos.js +7 -1
- package/src/commands/check.js +18 -21
- package/src/commands/convert.js +24 -3
- package/src/commands/init.js +5 -4
- package/src/commands/publish.js +33 -3
- package/src/commands/refresh.js +170 -0
- package/src/commands/validate.js +148 -0
- package/src/constants.js +5 -1
- package/src/index.js +2 -0
- package/src/integration/cli.test.js +151 -1
- package/src/validation/cross_rules.js +76 -0
- package/src/validation/cross_rules.test.js +88 -0
- package/src/validation/skill_json_rules.js +202 -0
- package/src/validation/skill_json_rules.test.js +239 -0
- package/src/validation/skill_md_rules.js +169 -0
- package/src/validation/skill_md_rules.test.js +201 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.0] - 2026-03-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Add `validate` command to check a skill against all rules from the agentskills.io spec in one pass — validates SKILL.md frontmatter (name, description, optional fields, line count), skill.json (name, version, description, keywords, dependencies, systemDependencies), and cross-file consistency (name match, executable code detection); supports `--json` and `-g` flags (alias: `v`)
|
|
14
|
+
- Add SKILL.md frontmatter validation warnings to `publish` and `convert` commands — warn about missing `name` or `description` fields before publishing
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Change `init` scaffolding to generate SKILL.md with proper YAML frontmatter (`name` and `description` fields) instead of a bare markdown heading
|
|
18
|
+
|
|
19
|
+
## [0.8.0] - 2026-03-08
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- Add `refresh` command to check all installed skills for updates and upgrade outdated ones in a single step (alias: `r`); supports `-y`, `-g`, and `--json` flags
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Change `check` command to use a single batch API call (`POST /repos:check-updates`) instead of N sequential ref lookups, significantly reducing latency for projects with many skills
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Fix `publish` not writing the commit SHA to `skills-lock.json` after publishing, causing first-publish detection to rely on stale lock data
|
|
29
|
+
|
|
10
30
|
## [0.7.3] - 2026-03-08
|
|
11
31
|
|
|
12
32
|
### Fixed
|
package/package.json
CHANGED
package/src/api/repos.js
CHANGED
|
@@ -46,4 +46,10 @@ const get_repo = (owner, repo) => catch_errors(`Get repo ${owner}/${repo} failed
|
|
|
46
46
|
return data
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
const check_updates = (skills) => catch_errors('Check updates failed', async () => {
|
|
50
|
+
const [errors, data] = await client.post('/repos:check-updates', { skills })
|
|
51
|
+
if (errors) throw errors[errors.length - 1]
|
|
52
|
+
return data
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
module.exports = { search, resolve_dependencies, clone, push, get_refs, get_repo, check_updates }
|
package/src/commands/check.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
|
|
2
2
|
const { read_lock, get_all_locked_skills } = require('../lock/reader')
|
|
3
3
|
const repos_api = require('../api/repos')
|
|
4
|
-
const {
|
|
4
|
+
const { gt } = require('../utils/semver')
|
|
5
5
|
const { print_help, print_table, print_json, print_info, print_success, print_hint, code } = require('../ui/output')
|
|
6
6
|
const { green, yellow, red } = require('../ui/colors')
|
|
7
7
|
const { exit_with_error } = require('../utils/errors')
|
|
@@ -57,29 +57,26 @@ const run = (args) => catch_errors('Check failed', async () => {
|
|
|
57
57
|
return
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
const skill_names = to_check.map(([name]) => name)
|
|
61
|
+
const [batch_err, batch_data] = await repos_api.check_updates(skill_names)
|
|
62
|
+
|
|
60
63
|
const results = []
|
|
61
|
-
|
|
62
|
-
const [
|
|
63
|
-
const [errors, refs] = await repos_api.get_refs(owner, repo)
|
|
64
|
-
if (errors) {
|
|
64
|
+
if (batch_err) {
|
|
65
|
+
for (const [name, data] of to_check) {
|
|
65
66
|
results.push({ skill: name, installed: data.version, latest: 'error', status: 'error' })
|
|
66
|
-
continue
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
results.push({ skill: name, installed: data.version, latest, status: 'outdated' })
|
|
81
|
-
} else {
|
|
82
|
-
results.push({ skill: name, installed: data.version, latest, status: 'up-to-date' })
|
|
68
|
+
} else {
|
|
69
|
+
for (const [name, data] of to_check) {
|
|
70
|
+
const info = batch_data?.results?.[name]
|
|
71
|
+
if (!info || !info.latest_version) {
|
|
72
|
+
results.push({ skill: name, installed: data.version, latest: '-', status: 'unknown' })
|
|
73
|
+
} else if (info.latest_version === data.version) {
|
|
74
|
+
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'up-to-date' })
|
|
75
|
+
} else if (gt(info.latest_version, data.version)) {
|
|
76
|
+
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'outdated' })
|
|
77
|
+
} else {
|
|
78
|
+
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'up-to-date' })
|
|
79
|
+
}
|
|
83
80
|
}
|
|
84
81
|
}
|
|
85
82
|
|
package/src/commands/convert.js
CHANGED
|
@@ -13,7 +13,7 @@ const { hash_directory } = require('../lock/integrity')
|
|
|
13
13
|
const { skills_dir, find_project_root, lock_root } = require('../config/paths')
|
|
14
14
|
const { file_exists } = require('../utils/fs')
|
|
15
15
|
const { create_spinner } = require('../ui/spinner')
|
|
16
|
-
const { print_help, print_success, print_error, print_info, print_label, print_json } = require('../ui/output')
|
|
16
|
+
const { print_help, print_success, print_error, print_info, print_warn, print_label, print_json } = require('../ui/output')
|
|
17
17
|
const { exit_with_error, UsageError, CliError } = require('../utils/errors')
|
|
18
18
|
const { EXIT_CODES, SKILL_MD } = require('../constants')
|
|
19
19
|
|
|
@@ -99,6 +99,25 @@ const run = (args) => catch_errors('Convert failed', async () => {
|
|
|
99
99
|
? args.flags.keywords.split(',').map(k => k.trim()).filter(Boolean)
|
|
100
100
|
: fm_keywords
|
|
101
101
|
|
|
102
|
+
// Warn about missing frontmatter fields that affect auto-invocation
|
|
103
|
+
const frontmatter_warnings = []
|
|
104
|
+
if (!frontmatter) {
|
|
105
|
+
frontmatter_warnings.push('SKILL.md has no YAML frontmatter (---). Without frontmatter, Claude cannot auto-invoke this skill. Add a frontmatter block with name and description fields.')
|
|
106
|
+
} else {
|
|
107
|
+
if (!frontmatter.name) {
|
|
108
|
+
frontmatter_warnings.push('SKILL.md frontmatter is missing "name". Add name: <skill-name> to the frontmatter.')
|
|
109
|
+
}
|
|
110
|
+
if (!frontmatter.description) {
|
|
111
|
+
frontmatter_warnings.push('SKILL.md frontmatter is missing "description". The description is the #1 factor for Claude auto-invocation quality. Without it, the skill will not be auto-invoked.')
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (frontmatter_warnings.length > 0) {
|
|
116
|
+
for (const warning of frontmatter_warnings) {
|
|
117
|
+
print_warn(warning)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
102
121
|
const spinner = create_spinner('Resolving workspace...')
|
|
103
122
|
|
|
104
123
|
const [ws_err, workspaces] = await workspaces_api.list_workspaces()
|
|
@@ -160,12 +179,14 @@ const run = (args) => catch_errors('Convert failed', async () => {
|
|
|
160
179
|
pub_spinner.succeed(`Converted ${full_name}@${version}`)
|
|
161
180
|
|
|
162
181
|
if (args.flags.json) {
|
|
163
|
-
|
|
182
|
+
const json_data = {
|
|
164
183
|
skill: full_name,
|
|
165
184
|
version,
|
|
166
185
|
workspace: workspace.slug,
|
|
167
186
|
description: description || ''
|
|
168
|
-
}
|
|
187
|
+
}
|
|
188
|
+
if (frontmatter_warnings.length > 0) json_data.warnings = frontmatter_warnings
|
|
189
|
+
print_json({ data: json_data })
|
|
169
190
|
return
|
|
170
191
|
}
|
|
171
192
|
|
package/src/commands/init.js
CHANGED
|
@@ -24,11 +24,12 @@ Examples:
|
|
|
24
24
|
happyskills init
|
|
25
25
|
happyskills init my-deploy-skill`
|
|
26
26
|
|
|
27
|
-
const create_skill_md = (name) =>
|
|
27
|
+
const create_skill_md = (name) => `---
|
|
28
|
+
name: ${name}
|
|
29
|
+
description: Describe what this skill does and when to invoke it
|
|
30
|
+
---
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Describe what this skill does for the AI agent.
|
|
32
|
+
# ${name}
|
|
32
33
|
|
|
33
34
|
## Instructions
|
|
34
35
|
|
package/src/commands/publish.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
1
3
|
const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
|
|
2
4
|
const { read_manifest } = require('../manifest/reader')
|
|
3
5
|
const { write_manifest } = require('../manifest/writer')
|
|
@@ -13,10 +15,11 @@ const { find_project_root } = require('../config/paths')
|
|
|
13
15
|
const { read_lock, get_all_locked_skills } = require('../lock/reader')
|
|
14
16
|
const { write_lock, update_lock_skills } = require('../lock/writer')
|
|
15
17
|
const { hash_directory } = require('../lock/integrity')
|
|
18
|
+
const { parse_frontmatter } = require('../utils/skill_scanner')
|
|
16
19
|
const { create_spinner } = require('../ui/spinner')
|
|
17
20
|
const { print_help, print_success, print_error, print_warn, print_hint, print_json, code } = require('../ui/output')
|
|
18
21
|
const { exit_with_error, UsageError, CliError } = require('../utils/errors')
|
|
19
|
-
const { EXIT_CODES } = require('../constants')
|
|
22
|
+
const { EXIT_CODES, SKILL_MD } = require('../constants')
|
|
20
23
|
|
|
21
24
|
const HELP_TEXT = `Usage: happyskills publish <skill-name> [options]
|
|
22
25
|
|
|
@@ -87,6 +90,31 @@ const run = (args) => catch_errors('Publish failed', async () => {
|
|
|
87
90
|
throw new CliError(`Invalid skill.json: ${validation.errors.join(', ')}`, EXIT_CODES.ERROR)
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
// Validate SKILL.md frontmatter — warn about missing name/description
|
|
94
|
+
const frontmatter_warnings = []
|
|
95
|
+
try {
|
|
96
|
+
const skill_md_content = await fs.promises.readFile(path.join(dir, SKILL_MD), 'utf-8')
|
|
97
|
+
const frontmatter = parse_frontmatter(skill_md_content)
|
|
98
|
+
if (!frontmatter) {
|
|
99
|
+
frontmatter_warnings.push('SKILL.md has no YAML frontmatter (---). Without frontmatter, Claude cannot auto-invoke this skill.')
|
|
100
|
+
} else {
|
|
101
|
+
if (!frontmatter.name) {
|
|
102
|
+
frontmatter_warnings.push('SKILL.md frontmatter is missing "name". Add name: <skill-name> to the frontmatter.')
|
|
103
|
+
}
|
|
104
|
+
if (!frontmatter.description) {
|
|
105
|
+
frontmatter_warnings.push('SKILL.md frontmatter is missing "description". The description is the #1 factor for Claude auto-invocation quality. Without it, the skill will not be auto-invoked.')
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
frontmatter_warnings.push(`No ${SKILL_MD} found. The skill may not work correctly.`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (frontmatter_warnings.length > 0) {
|
|
113
|
+
for (const warning of frontmatter_warnings) {
|
|
114
|
+
print_warn(warning)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
90
118
|
const spinner = create_spinner('Preparing to publish...')
|
|
91
119
|
|
|
92
120
|
let owner = args.flags.workspace
|
|
@@ -164,13 +192,15 @@ const run = (args) => catch_errors('Publish failed', async () => {
|
|
|
164
192
|
}
|
|
165
193
|
|
|
166
194
|
if (args.flags.json) {
|
|
167
|
-
|
|
195
|
+
const json_data = {
|
|
168
196
|
skill: full_name,
|
|
169
197
|
version: manifest.version,
|
|
170
198
|
ref: push_data?.ref || `refs/tags/v${manifest.version}`,
|
|
171
199
|
commit: push_data?.commit || null,
|
|
172
200
|
bumped_from
|
|
173
|
-
}
|
|
201
|
+
}
|
|
202
|
+
if (frontmatter_warnings.length > 0) json_data.warnings = frontmatter_warnings
|
|
203
|
+
print_json({ data: json_data })
|
|
174
204
|
return
|
|
175
205
|
}
|
|
176
206
|
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
|
|
2
|
+
const { install } = require('../engine/installer')
|
|
3
|
+
const { read_lock, get_all_locked_skills } = require('../lock/reader')
|
|
4
|
+
const repos_api = require('../api/repos')
|
|
5
|
+
const { gt } = require('../utils/semver')
|
|
6
|
+
const { print_help, print_table, print_json, print_info, print_success, code } = require('../ui/output')
|
|
7
|
+
const { green, yellow, red } = require('../ui/colors')
|
|
8
|
+
const { create_spinner } = require('../ui/spinner')
|
|
9
|
+
const { exit_with_error } = require('../utils/errors')
|
|
10
|
+
const { find_project_root, lock_root } = require('../config/paths')
|
|
11
|
+
const { EXIT_CODES } = require('../constants')
|
|
12
|
+
|
|
13
|
+
const HELP_TEXT = `Usage: happyskills refresh [options]
|
|
14
|
+
|
|
15
|
+
Check all installed skills for updates and upgrade outdated ones.
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
-g, --global Refresh globally installed skills
|
|
19
|
+
-y, --yes Skip confirmation prompts
|
|
20
|
+
--json Output as JSON
|
|
21
|
+
|
|
22
|
+
Aliases: r
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
happyskills refresh
|
|
26
|
+
happyskills refresh -y
|
|
27
|
+
happyskills refresh -g -y --json`
|
|
28
|
+
|
|
29
|
+
const confirm_prompt = (question) => new Promise((resolve) => {
|
|
30
|
+
const readline = require('readline')
|
|
31
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
|
|
32
|
+
rl.question(question, (answer) => {
|
|
33
|
+
rl.close()
|
|
34
|
+
resolve(answer.trim().toLowerCase())
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const run = (args) => catch_errors('Refresh failed', async () => {
|
|
39
|
+
if (args.flags._show_help) {
|
|
40
|
+
print_help(HELP_TEXT)
|
|
41
|
+
return process.exit(EXIT_CODES.SUCCESS)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const is_global = args.flags.global || false
|
|
45
|
+
const auto_yes = args.flags.yes || false
|
|
46
|
+
const project_root = find_project_root()
|
|
47
|
+
const [, lock_data] = await read_lock(lock_root(is_global, project_root))
|
|
48
|
+
const skills = get_all_locked_skills(lock_data)
|
|
49
|
+
const entries = Object.entries(skills)
|
|
50
|
+
|
|
51
|
+
const to_check = entries.filter(([, data]) => data.requested_by?.includes('__root__'))
|
|
52
|
+
|
|
53
|
+
if (to_check.length === 0) {
|
|
54
|
+
if (args.flags.json) {
|
|
55
|
+
print_json({ data: { results: [], outdated_count: 0, up_to_date_count: 0, updated: [], already_up_to_date: [], errors: [] } })
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
print_info('No skills installed.')
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 1. Check for updates
|
|
63
|
+
const spinner = !args.flags.json ? create_spinner('Checking for updates…') : null
|
|
64
|
+
const skill_names = to_check.map(([name]) => name)
|
|
65
|
+
const [batch_err, batch_data] = await repos_api.check_updates(skill_names)
|
|
66
|
+
|
|
67
|
+
const results = []
|
|
68
|
+
if (batch_err) {
|
|
69
|
+
spinner?.fail('Failed to check for updates')
|
|
70
|
+
for (const [name, data] of to_check) {
|
|
71
|
+
results.push({ skill: name, installed: data.version, latest: 'error', status: 'error' })
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
spinner?.succeed('Checked for updates')
|
|
75
|
+
for (const [name, data] of to_check) {
|
|
76
|
+
const info = batch_data?.results?.[name]
|
|
77
|
+
if (!info || !info.latest_version) {
|
|
78
|
+
results.push({ skill: name, installed: data.version, latest: '-', status: 'unknown' })
|
|
79
|
+
} else if (info.latest_version === data.version) {
|
|
80
|
+
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'up-to-date' })
|
|
81
|
+
} else if (gt(info.latest_version, data.version)) {
|
|
82
|
+
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'outdated' })
|
|
83
|
+
} else {
|
|
84
|
+
results.push({ skill: name, installed: data.version, latest: info.latest_version, status: 'up-to-date' })
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const outdated = results.filter(r => r.status === 'outdated')
|
|
90
|
+
const up_to_date = results.filter(r => r.status === 'up-to-date')
|
|
91
|
+
|
|
92
|
+
// 2. If nothing to update, report and exit
|
|
93
|
+
if (outdated.length === 0) {
|
|
94
|
+
if (args.flags.json) {
|
|
95
|
+
print_json({ data: { results, outdated_count: 0, up_to_date_count: up_to_date.length, updated: [], already_up_to_date: up_to_date.map(r => ({ skill: r.skill, version: r.installed })), errors: [] } })
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
print_table(['Skill', 'Installed', 'Latest', 'Status'], results.map(r => [
|
|
99
|
+
r.skill, r.installed, r.latest, green(r.status)
|
|
100
|
+
]))
|
|
101
|
+
console.log()
|
|
102
|
+
print_success('All skills are up to date.')
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 3. Show results table (non-json)
|
|
107
|
+
if (!args.flags.json) {
|
|
108
|
+
const status_colors = { 'up-to-date': green, 'outdated': yellow, 'error': red, 'unknown': (s) => s }
|
|
109
|
+
print_table(['Skill', 'Installed', 'Latest', 'Status'], results.map(r => [
|
|
110
|
+
r.skill, r.installed, r.latest, (status_colors[r.status] || ((s) => s))(r.status)
|
|
111
|
+
]))
|
|
112
|
+
console.log()
|
|
113
|
+
print_info(`${outdated.length} skill(s) can be updated.`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 4. Confirm update
|
|
117
|
+
const should_update = auto_yes || !process.stdin.isTTY
|
|
118
|
+
if (!should_update && !args.flags.json) {
|
|
119
|
+
const answer = await confirm_prompt(`\nUpdate ${outdated.length} skill(s)? [y/N] `)
|
|
120
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
121
|
+
print_info('Skipped.')
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 5. Update outdated skills
|
|
127
|
+
const options = { global: is_global, fresh: true, project_root }
|
|
128
|
+
const updated = []
|
|
129
|
+
const update_errors = []
|
|
130
|
+
|
|
131
|
+
for (const r of outdated) {
|
|
132
|
+
const update_spinner = !args.flags.json ? create_spinner(`Updating ${r.skill}…`) : null
|
|
133
|
+
const [errors, result] = await install(r.skill, options)
|
|
134
|
+
if (errors) {
|
|
135
|
+
update_spinner?.fail(`Failed to update ${r.skill}`)
|
|
136
|
+
update_errors.push({ skill: r.skill, message: errors[errors.length - 1]?.message || 'Unknown error' })
|
|
137
|
+
} else if (!result.no_op) {
|
|
138
|
+
update_spinner?.succeed(`Updated ${r.skill} ${r.installed} → ${result.version}`)
|
|
139
|
+
updated.push({ skill: r.skill, from: r.installed, to: result.version })
|
|
140
|
+
} else {
|
|
141
|
+
update_spinner?.succeed(`${r.skill} already up to date`)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 6. Output results
|
|
146
|
+
if (args.flags.json) {
|
|
147
|
+
print_json({
|
|
148
|
+
data: {
|
|
149
|
+
results,
|
|
150
|
+
outdated_count: outdated.length,
|
|
151
|
+
up_to_date_count: up_to_date.length,
|
|
152
|
+
updated,
|
|
153
|
+
already_up_to_date: up_to_date.map(r => ({ skill: r.skill, version: r.installed })),
|
|
154
|
+
errors: update_errors
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (updated.length > 0) {
|
|
161
|
+
console.log()
|
|
162
|
+
print_success(`Updated ${updated.length} skill(s).`)
|
|
163
|
+
}
|
|
164
|
+
if (update_errors.length > 0) {
|
|
165
|
+
console.log()
|
|
166
|
+
print_info(`${update_errors.length} skill(s) failed to update. Run ${code('happyskills check')} for details.`)
|
|
167
|
+
}
|
|
168
|
+
}).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
|
|
169
|
+
|
|
170
|
+
module.exports = { run }
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const { error: { catch_errors } } = require('puffy-core')
|
|
3
|
+
const { validate_skill_md } = require('../validation/skill_md_rules')
|
|
4
|
+
const { validate_skill_json } = require('../validation/skill_json_rules')
|
|
5
|
+
const { validate_cross } = require('../validation/cross_rules')
|
|
6
|
+
const { file_exists } = require('../utils/fs')
|
|
7
|
+
const { skills_dir, find_project_root } = require('../config/paths')
|
|
8
|
+
const { print_help, print_json } = require('../ui/output')
|
|
9
|
+
const { bold, green, yellow, red, dim } = require('../ui/colors')
|
|
10
|
+
const { exit_with_error, UsageError } = require('../utils/errors')
|
|
11
|
+
const { EXIT_CODES, SKILL_MD } = require('../constants')
|
|
12
|
+
|
|
13
|
+
const HELP_TEXT = `Usage: happyskills validate <skill-name> [options]
|
|
14
|
+
|
|
15
|
+
Validate a skill against all rules from the agentskills.io spec.
|
|
16
|
+
|
|
17
|
+
Arguments:
|
|
18
|
+
skill-name Name of the skill to validate
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
-g, --global Look in global skills (~/.claude/skills/)
|
|
22
|
+
--json Output as JSON
|
|
23
|
+
|
|
24
|
+
Aliases: v
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
happyskills validate my-skill
|
|
28
|
+
happyskills validate deploy-aws -g
|
|
29
|
+
happyskills v my-skill --json`
|
|
30
|
+
|
|
31
|
+
const resolve_validate_dir = async (skill_name, is_global) => {
|
|
32
|
+
const project_root = find_project_root()
|
|
33
|
+
const base = skills_dir(is_global, project_root)
|
|
34
|
+
const dir = path.join(base, skill_name)
|
|
35
|
+
|
|
36
|
+
// Check if directory exists (don't require skill.json — that's what validate checks)
|
|
37
|
+
const md_path = path.join(dir, SKILL_MD)
|
|
38
|
+
const json_path = path.join(dir, 'skill.json')
|
|
39
|
+
|
|
40
|
+
const [, md_exists] = await file_exists(md_path)
|
|
41
|
+
const [, json_exists] = await file_exists(json_path)
|
|
42
|
+
|
|
43
|
+
if (!md_exists && !json_exists) {
|
|
44
|
+
throw new UsageError(`Skill "${skill_name}" not found at ${dir}`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return dir
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const format_human = (skill_name, all_results) => {
|
|
51
|
+
console.log(`\nValidating skill "${bold(skill_name)}"...\n`)
|
|
52
|
+
|
|
53
|
+
const groups = {}
|
|
54
|
+
for (const r of all_results) {
|
|
55
|
+
const key = r.file || 'General'
|
|
56
|
+
if (!groups[key]) groups[key] = []
|
|
57
|
+
groups[key].push(r)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const [file, items] of Object.entries(groups)) {
|
|
61
|
+
console.log(` ${bold(file)}`)
|
|
62
|
+
for (const item of items) {
|
|
63
|
+
if (item.severity === 'pass') {
|
|
64
|
+
console.log(` ${green('✓')} ${item.message}`)
|
|
65
|
+
} else if (item.severity === 'warning') {
|
|
66
|
+
console.log(` ${yellow('⚠')} ${item.message}`)
|
|
67
|
+
} else {
|
|
68
|
+
console.log(` ${red('✗')} ${item.message}`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
console.log()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const errors = all_results.filter(r => r.severity === 'error')
|
|
75
|
+
const warnings = all_results.filter(r => r.severity === 'warning')
|
|
76
|
+
const passed = all_results.filter(r => r.severity === 'pass')
|
|
77
|
+
|
|
78
|
+
const parts = []
|
|
79
|
+
if (warnings.length > 0) parts.push(yellow(`${warnings.length} warning${warnings.length === 1 ? '' : 's'}`))
|
|
80
|
+
if (errors.length > 0) parts.push(red(`${errors.length} error${errors.length === 1 ? '' : 's'}`))
|
|
81
|
+
if (errors.length === 0 && warnings.length === 0) parts.push(green('all checks passed'))
|
|
82
|
+
|
|
83
|
+
console.log(dim(`${passed.length} passed, `) + parts.join(', '))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const format_json = (skill_name, all_results) => {
|
|
87
|
+
const errors = all_results.filter(r => r.severity === 'error').map(({ severity, ...rest }) => rest)
|
|
88
|
+
const warnings = all_results.filter(r => r.severity === 'warning').map(({ severity, ...rest }) => rest)
|
|
89
|
+
const passed = all_results.filter(r => r.severity === 'pass')
|
|
90
|
+
const failed = all_results.filter(r => r.severity === 'error')
|
|
91
|
+
const warned = all_results.filter(r => r.severity === 'warning')
|
|
92
|
+
|
|
93
|
+
print_json({
|
|
94
|
+
data: {
|
|
95
|
+
skill: skill_name,
|
|
96
|
+
valid: failed.length === 0,
|
|
97
|
+
errors,
|
|
98
|
+
warnings,
|
|
99
|
+
checks_passed: passed.length,
|
|
100
|
+
checks_failed: failed.length,
|
|
101
|
+
checks_warned: warned.length
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const run = (args) => catch_errors('Validate failed', async () => {
|
|
107
|
+
if (args.flags._show_help) {
|
|
108
|
+
print_help(HELP_TEXT)
|
|
109
|
+
return process.exit(EXIT_CODES.SUCCESS)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const skill_name = args._[0]
|
|
113
|
+
if (!skill_name) {
|
|
114
|
+
throw new UsageError('Skill name required. Usage: happyskills validate <skill-name>')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const is_global = !!args.flags.global
|
|
118
|
+
const skill_dir = await resolve_validate_dir(skill_name, is_global)
|
|
119
|
+
const dir_name = path.basename(skill_dir)
|
|
120
|
+
|
|
121
|
+
// Run all rule modules
|
|
122
|
+
const [md_err, md_data] = await validate_skill_md(skill_dir, dir_name)
|
|
123
|
+
if (md_err) throw md_err
|
|
124
|
+
|
|
125
|
+
const [json_err, json_data] = await validate_skill_json(skill_dir)
|
|
126
|
+
if (json_err) throw json_err
|
|
127
|
+
|
|
128
|
+
const [cross_err, cross_results] = await validate_cross(
|
|
129
|
+
skill_dir,
|
|
130
|
+
md_data.frontmatter,
|
|
131
|
+
json_data.manifest,
|
|
132
|
+
md_data.content
|
|
133
|
+
)
|
|
134
|
+
if (cross_err) throw cross_err
|
|
135
|
+
|
|
136
|
+
const all_results = [...md_data.results, ...json_data.results, ...cross_results]
|
|
137
|
+
|
|
138
|
+
if (args.flags.json) {
|
|
139
|
+
format_json(skill_name, all_results)
|
|
140
|
+
} else {
|
|
141
|
+
format_human(skill_name, all_results)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const has_errors = all_results.some(r => r.severity === 'error')
|
|
145
|
+
process.exit(has_errors ? EXIT_CODES.ERROR : EXIT_CODES.SUCCESS)
|
|
146
|
+
}).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
|
|
147
|
+
|
|
148
|
+
module.exports = { run }
|
package/src/constants.js
CHANGED
|
@@ -26,8 +26,10 @@ const COMMAND_ALIASES = {
|
|
|
26
26
|
remove: 'uninstall',
|
|
27
27
|
ls: 'list',
|
|
28
28
|
s: 'search',
|
|
29
|
+
r: 'refresh',
|
|
29
30
|
up: 'update',
|
|
30
|
-
pub: 'publish'
|
|
31
|
+
pub: 'publish',
|
|
32
|
+
v: 'validate'
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
const COMMANDS = [
|
|
@@ -37,6 +39,7 @@ const COMMANDS = [
|
|
|
37
39
|
'list',
|
|
38
40
|
'search',
|
|
39
41
|
'check',
|
|
42
|
+
'refresh',
|
|
40
43
|
'update',
|
|
41
44
|
'bump',
|
|
42
45
|
'publish',
|
|
@@ -46,6 +49,7 @@ const COMMANDS = [
|
|
|
46
49
|
'whoami',
|
|
47
50
|
'fork',
|
|
48
51
|
'setup',
|
|
52
|
+
'validate',
|
|
49
53
|
'self-update'
|
|
50
54
|
]
|
|
51
55
|
|
package/src/index.js
CHANGED
|
@@ -90,8 +90,10 @@ Commands:
|
|
|
90
90
|
list List installed skills (alias: ls)
|
|
91
91
|
search <query> Search the registry (alias: s)
|
|
92
92
|
check [owner/skill] Check for available updates
|
|
93
|
+
refresh Check + update all outdated skills (alias: r)
|
|
93
94
|
update [owner/skill] Upgrade to latest versions (alias: up)
|
|
94
95
|
publish Push skill to registry (alias: pub)
|
|
96
|
+
validate <skill-name> Validate skill against all rules (alias: v)
|
|
95
97
|
fork <owner/skill> Fork a skill to your workspace
|
|
96
98
|
setup Install the HappySkills CLI skill globally
|
|
97
99
|
self-update Upgrade happyskills CLI to latest version
|