happyskills 0.9.0 → 0.9.1

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,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.1] - 2026-03-10
11
+
12
+ ### Changed
13
+ - Change `publish` to run full skill validation (SKILL.md + skill.json + cross-file rules) before publishing, blocking on errors with structured `VALIDATION_FAILED` output in JSON mode — replaces the previous ad-hoc manifest validation and frontmatter warnings
14
+
10
15
  ## [0.9.0] - 2026-03-10
11
16
 
12
17
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
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)",
@@ -1,9 +1,7 @@
1
- const fs = require('fs')
2
1
  const path = require('path')
3
2
  const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
4
3
  const { read_manifest } = require('../manifest/reader')
5
4
  const { write_manifest } = require('../manifest/writer')
6
- const { validate_manifest } = require('../manifest/validator')
7
5
  const { require_token } = require('../auth/token_store')
8
6
  const repos_api = require('../api/repos')
9
7
  const workspaces_api = require('../api/workspaces')
@@ -15,11 +13,13 @@ const { find_project_root } = require('../config/paths')
15
13
  const { read_lock, get_all_locked_skills } = require('../lock/reader')
16
14
  const { write_lock, update_lock_skills } = require('../lock/writer')
17
15
  const { hash_directory } = require('../lock/integrity')
18
- const { parse_frontmatter } = require('../utils/skill_scanner')
16
+ const { validate_skill_md } = require('../validation/skill_md_rules')
17
+ const { validate_skill_json } = require('../validation/skill_json_rules')
18
+ const { validate_cross } = require('../validation/cross_rules')
19
19
  const { create_spinner } = require('../ui/spinner')
20
20
  const { print_help, print_success, print_error, print_warn, print_hint, print_json, code } = require('../ui/output')
21
21
  const { exit_with_error, UsageError, CliError } = require('../utils/errors')
22
- const { EXIT_CODES, SKILL_MD } = require('../constants')
22
+ const { EXIT_CODES } = require('../constants')
23
23
 
24
24
  const HELP_TEXT = `Usage: happyskills publish <skill-name> [options]
25
25
 
@@ -85,34 +85,39 @@ const run = (args) => catch_errors('Publish failed', async () => {
85
85
  if (write_err) throw e('Failed to update manifest version', write_err)
86
86
  }
87
87
 
88
- const validation = validate_manifest(manifest)
89
- if (!validation.valid) {
90
- throw new CliError(`Invalid skill.json: ${validation.errors.join(', ')}`, EXIT_CODES.ERROR)
91
- }
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
- }
88
+ // Run full validation block on errors, show warnings
89
+ const dir_name = path.basename(dir)
90
+ const [md_err, md_data] = await validate_skill_md(dir, dir_name)
91
+ if (md_err) throw md_err
92
+ const [json_err, json_data] = await validate_skill_json(dir)
93
+ if (json_err) throw json_err
94
+ const [cross_err, cross_results] = await validate_cross(dir, md_data.frontmatter, json_data.manifest, md_data.content)
95
+ if (cross_err) throw cross_err
96
+
97
+ const all_results = [...md_data.results, ...json_data.results, ...cross_results]
98
+ const validation_errors = all_results.filter(r => r.severity === 'error')
99
+ const validation_warnings = all_results.filter(r => r.severity === 'warning')
100
+
101
+ if (validation_errors.length > 0) {
102
+ const { is_json_mode } = require('../state')
103
+ if (is_json_mode()) {
104
+ const structured_errors = validation_errors.map(({ severity, ...rest }) => rest)
105
+ print_json({
106
+ error: {
107
+ code: 'VALIDATION_FAILED',
108
+ message: `Skill failed validation with ${validation_errors.length} error(s). Fix these issues and try again.`,
109
+ exit_code: EXIT_CODES.ERROR,
110
+ validation_errors: structured_errors
111
+ }
112
+ })
113
+ return process.exit(EXIT_CODES.ERROR)
107
114
  }
108
- } catch {
109
- frontmatter_warnings.push(`No ${SKILL_MD} found. The skill may not work correctly.`)
115
+ const error_msgs = validation_errors.map(r => `${r.file || 'General'}${r.field ? ` (${r.field})` : ''}: ${r.message}`)
116
+ throw new CliError(`Skill failed validation with ${validation_errors.length} error(s):\n ${error_msgs.join('\n ')}\n\nRun \`happyskills validate ${skill_name}\` for full details.`, EXIT_CODES.ERROR)
110
117
  }
111
118
 
112
- if (frontmatter_warnings.length > 0) {
113
- for (const warning of frontmatter_warnings) {
114
- print_warn(warning)
115
- }
119
+ for (const w of validation_warnings) {
120
+ print_warn(`${w.file || 'General'}${w.field ? ` (${w.field})` : ''}: ${w.message}`)
116
121
  }
117
122
 
118
123
  const spinner = create_spinner('Preparing to publish...')
@@ -199,7 +204,7 @@ const run = (args) => catch_errors('Publish failed', async () => {
199
204
  commit: push_data?.commit || null,
200
205
  bumped_from
201
206
  }
202
- if (frontmatter_warnings.length > 0) json_data.warnings = frontmatter_warnings
207
+ if (validation_warnings.length > 0) json_data.warnings = validation_warnings.map(w => w.message)
203
208
  print_json({ data: json_data })
204
209
  return
205
210
  }