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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "0.7.3",
3
+ "version": "0.9.0",
4
4
  "description": "Package manager for AI agent skills",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Nicolas Dao <nic@cloudlesslabs.com> (https://cloudlesslabs.com)",
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
- module.exports = { search, resolve_dependencies, clone, push, get_refs, get_repo }
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 }
@@ -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 { satisfies, gt } = require('../utils/semver')
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
- for (const [name, data] of to_check) {
62
- const [owner, repo] = name.split('/')
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 versions = (refs || [])
70
- .map(r => r.name?.replace('refs/tags/v', ''))
71
- .filter(Boolean)
72
-
73
- const latest = versions.sort((a, b) => gt(a, b) ? -1 : 1)[0]
74
-
75
- if (!latest) {
76
- results.push({ skill: name, installed: data.version, latest: '-', status: 'unknown' })
77
- } else if (latest === data.version) {
78
- results.push({ skill: name, installed: data.version, latest, status: 'up-to-date' })
79
- } else if (gt(latest, data.version)) {
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
 
@@ -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
- print_json({ data: {
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
 
@@ -24,11 +24,12 @@ Examples:
24
24
  happyskills init
25
25
  happyskills init my-deploy-skill`
26
26
 
27
- const create_skill_md = (name) => `# ${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
- ## Description
30
-
31
- Describe what this skill does for the AI agent.
32
+ # ${name}
32
33
 
33
34
  ## Instructions
34
35
 
@@ -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
- print_json({ data: {
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