happyskills 0.35.4 → 0.35.5

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,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.35.5] - 2026-04-11
11
+
12
+ ### Fixed
13
+ - Fix kits being symlinked to agent folders during `install`, `refresh`, and `enable` — kits are meta-packages not invocable by agents, only their dependency skills should be linked
14
+ - Block `enable` command for kits with a clear warning instead of silently creating useless symlinks
15
+
10
16
  ## [0.35.4] - 2026-04-11
11
17
 
12
18
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "0.35.4",
3
+ "version": "0.35.5",
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)",
@@ -6,7 +6,7 @@ const { is_skill_enabled } = require('../agents/status')
6
6
  const { file_exists } = require('../utils/fs')
7
7
  const { print_help, print_json, print_success, print_warn, print_info } = require('../ui/output')
8
8
  const { exit_with_error, UsageError } = require('../utils/errors')
9
- const { EXIT_CODES } = require('../constants')
9
+ const { EXIT_CODES, SKILL_TYPES } = require('../constants')
10
10
 
11
11
  const HELP_TEXT = `Usage: happyskills enable <skill> [skill2 ...] [options]
12
12
 
@@ -77,6 +77,13 @@ const run = (args) => catch_errors('Enable failed', async () => {
77
77
 
78
78
  const { full, short } = resolved
79
79
 
80
+ // Kits are not agent-invocable — skip linking
81
+ if (locked_skills[full]?.type === SKILL_TYPES.KIT) {
82
+ print_warn(`${full} is a kit — kits are not linked to agent folders`)
83
+ results.push({ skill: full, status: 'kit_skipped' })
84
+ continue
85
+ }
86
+
80
87
  // Verify the skill directory exists on disk
81
88
  const dir = skill_install_dir(base_dir, short)
82
89
  const [, exists] = await file_exists(dir)
@@ -9,7 +9,7 @@ const { green, yellow, red } = require('../ui/colors')
9
9
  const { create_spinner } = require('../ui/spinner')
10
10
  const { exit_with_error } = require('../utils/errors')
11
11
  const { find_project_root, lock_root, skills_dir, skill_install_dir } = require('../config/paths')
12
- const { EXIT_CODES } = require('../constants')
12
+ const { EXIT_CODES, SKILL_TYPES } = require('../constants')
13
13
  const { resolve_agents, verify_and_repair_symlinks } = require('../agents')
14
14
  const { is_skill_enabled } = require('../agents/status')
15
15
 
@@ -123,7 +123,8 @@ const run = (args) => catch_errors('Refresh failed', async () => {
123
123
  let symlink_repairs = []
124
124
  if (detected_agents.length > 0) {
125
125
  const skills_to_check = []
126
- for (const [name] of entries) {
126
+ for (const [name, data] of entries) {
127
+ if (data.type === SKILL_TYPES.KIT) continue
127
128
  const short_name = name.split('/')[1] || name
128
129
  const source_dir = skill_install_dir(base_dir, short_name)
129
130
  const [, enabled] = await is_skill_enabled(short_name, detected_agents, is_global, project_root)
@@ -10,7 +10,7 @@ const { read_lock, get_locked_skill } = require('../lock/reader')
10
10
  const { write_lock, update_lock_skills } = require('../lock/writer')
11
11
  const { skills_dir, tmp_dir, skill_install_dir, lock_root } = require('../config/paths')
12
12
  const { ensure_dir, remove_dir, file_exists, read_json } = require('../utils/fs')
13
- const { SKILL_JSON } = require('../constants')
13
+ const { SKILL_JSON, SKILL_TYPES } = require('../constants')
14
14
  const { resolve_agents, link_to_agents, verify_and_repair_symlinks } = require('../agents')
15
15
  const { is_skill_enabled } = require('../agents/status')
16
16
  const { create_spinner } = require('../ui/spinner')
@@ -42,8 +42,8 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
42
42
  if (exists) {
43
43
  const [, valid] = locked.integrity ? await verify_integrity(install_dir, locked.integrity) : [null, true]
44
44
  if (valid !== false) {
45
- // Verify and repair symlinks even for already-installed skills
46
- if (agents.length > 0) {
45
+ // Verify and repair symlinks even for already-installed skills (skip kits — not agent-invocable)
46
+ if (agents.length > 0 && locked.type !== SKILL_TYPES.KIT) {
47
47
  const name = skill.split('/')[1]
48
48
  const [, enabled] = await is_skill_enabled(name, agents, is_global, project_root)
49
49
  if (enabled) {
@@ -103,10 +103,12 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
103
103
  }
104
104
 
105
105
  if (packages_to_install.length === 0) {
106
- // Verify and repair symlinks for all skipped packages
106
+ // Verify and repair symlinks for all skipped packages (skip kits — not agent-invocable)
107
107
  if (agents.length > 0) {
108
108
  const skills_to_verify = []
109
109
  for (const pkg of packages) {
110
+ const locked = get_locked_skill(lock_data, pkg.skill)
111
+ if (locked?.type === SKILL_TYPES.KIT) continue
110
112
  const name = pkg.skill.split('/')[1]
111
113
  const [, enabled] = await is_skill_enabled(name, agents, is_global, project_root)
112
114
  if (enabled) {
@@ -190,10 +192,22 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
190
192
  await fs.promises.rename(tmp_path, final_dir)
191
193
  }
192
194
 
195
+ // Read type from each installed package's skill.json (used for linking + lock writing)
196
+ const pkg_types = {}
197
+ for (const { pkg } of downloaded) {
198
+ const name = pkg.skill.split('/')[1]
199
+ const final_dir = skill_install_dir(base_dir, name)
200
+ const pkg_json_path = path.join(final_dir, SKILL_JSON)
201
+ const [, pkg_manifest] = await read_json(pkg_json_path)
202
+ if (pkg_manifest?.type) pkg_types[pkg.skill] = pkg_manifest.type
203
+ }
204
+
193
205
  // Link to detected agents (non-fatal — warnings only)
194
- // Skip linking for skills that were disabled before this install/update
206
+ // Skip linking for kits (not invocable by agents) and skills that were disabled before this install/update
195
207
  if (agents.length > 0) {
196
- const to_link = downloaded.filter(({ pkg }) => !disabled_skills.has(pkg.skill.split('/')[1]))
208
+ const to_link = downloaded.filter(({ pkg }) =>
209
+ pkg_types[pkg.skill] !== SKILL_TYPES.KIT && !disabled_skills.has(pkg.skill.split('/')[1])
210
+ )
197
211
  if (to_link.length > 0) {
198
212
  spinner.update(`Linking to ${agents.length} agent(s)...`)
199
213
  for (const { pkg } of to_link) {
@@ -220,12 +234,7 @@ const install = (skill, options = {}) => catch_errors('Install failed', async ()
220
234
  const final_dir = skill_install_dir(base_dir, name)
221
235
  const [, integrity] = await hash_directory(final_dir)
222
236
 
223
- // Read type from the installed package's skill.json
224
- let pkg_type
225
- const pkg_json_path = path.join(final_dir, SKILL_JSON)
226
- const [, pkg_manifest] = await read_json(pkg_json_path)
227
- if (pkg_manifest?.type) pkg_type = pkg_manifest.type
228
-
237
+ const pkg_type = pkg_types[pkg.skill]
229
238
  updates[pkg.skill] = {
230
239
  version: pkg.version,
231
240
  ref: pkg.ref,