happyskills 0.46.0 → 0.46.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,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.46.1] - 2026-05-20
11
+
12
+ ### Fixed
13
+ - Fix `happyskills agents add` ignoring two categories of skills that should be mirrored into the newly added agent's folder: **kits** (skills with `type: kit` in the lock file — previously excluded by a copy-pasted "not agent-invocable" filter that conflated *invocation routing* with *filesystem presence*) and **locally-authored skills that were never published** (skills scaffolded via `happyskills init` and kept under source control but not pushed to the registry — they exist on disk in `.agents/skills/<name>/` but have no lock entry, so the lock-based enumeration missed them). The `add` subcommand now enumerates `.agents/skills/<name>/` directories directly, so anything physically present in the canonical location — regardless of whether it's lock-managed, a kit, or a private local-only skill — is symlinked into the new agent's folder.
14
+ - Fix `happyskills agents remove` ignoring the same two categories — it previously read short names from the lock file, so a project containing kits or unpublished skills would leave dangling symlinks in the removed agent's folder. The subcommand now reads canonical names from `.agents/skills/<name>/` so every skill that could have been mirrored can also be unlinked.
15
+ - Fix `happyskills agents list` undercounting the `Linked` column for the same reason — count is now against the canonical on-disk list so kits and unpublished skills are included.
16
+
10
17
  ## [0.46.0] - 2026-05-20
11
18
 
12
19
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "0.46.0",
3
+ "version": "0.46.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)",
@@ -11,7 +11,7 @@ const { find_project_root, lock_root, skills_dir, skill_install_dir, agent_skill
11
11
  const { print_help, print_json, print_success, print_warn, print_info, print_table, code } = require('../ui/output')
12
12
  const { green, dim } = require('../ui/colors')
13
13
  const { exit_with_error, UsageError } = require('../utils/errors')
14
- const { EXIT_CODES, SKILL_TYPES } = require('../constants')
14
+ const { EXIT_CODES } = require('../constants')
15
15
 
16
16
  const HELP_TEXT = `Usage: happyskills agents <subcommand> [args] [options]
17
17
 
@@ -63,26 +63,70 @@ const _parse_agent_ids = (raw_args) => {
63
63
  }
64
64
 
65
65
  /**
66
- * For each installed skill, decide whether it should be mirrored into the
67
- * newly added agent. A skill is mirrored when it is currently enabled — i.e.,
68
- * a symlink for it exists in at least one *other* already-configured agent.
66
+ * Enumerate every skill physically present in the canonical .agents/skills/
67
+ * directory for the given scope. Covers all three categories of "skill that
68
+ * lives in this project":
69
+ * 1. Lock-managed skills (installed from the registry)
70
+ * 2. Kits (lock-managed or not — kits have a SKILL.md without frontmatter,
71
+ * so a frontmatter-aware scan would miss them; we enumerate directories
72
+ * directly to catch them)
73
+ * 3. Locally-authored skills that were created via `happyskills init` but
74
+ * never published — they exist on disk but have no lock entry
69
75
  *
70
- * When no other agent folders exist yet (fresh project bootstrap), every
71
- * non-kit installed skill is mirrored.
76
+ * Returns short directory names (e.g. "deploy-aws"), not owner-qualified names.
77
+ */
78
+ const _list_canonical_skill_names = async (is_global, project_root) => {
79
+ const base_dir = skills_dir(is_global, project_root)
80
+ try {
81
+ const entries = await fs.promises.readdir(base_dir, { withFileTypes: true })
82
+ return entries
83
+ .filter(e => (e.isDirectory() || e.isSymbolicLink()) && !e.name.startsWith('.'))
84
+ .map(e => e.name)
85
+ } catch {
86
+ return []
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Build a short-name → display-name map from the lock file so on-disk skills
92
+ * that happen to be lock-managed are shown with their owner-qualified name.
93
+ * Skills not in the lock fall back to their short directory name.
94
+ */
95
+ const _build_display_name_map = (lock_data) => {
96
+ const lock_skills = get_all_locked_skills(lock_data)
97
+ const map = new Map()
98
+ for (const full_name of Object.keys(lock_skills)) {
99
+ const short = full_name.split('/')[1] || full_name
100
+ map.set(short, full_name)
101
+ }
102
+ return map
103
+ }
104
+
105
+ /**
106
+ * For each skill physically present in the canonical .agents/skills/ directory,
107
+ * decide whether it should be mirrored into the newly added agent. A skill is
108
+ * mirrored when it is currently enabled — i.e., a symlink for it exists in at
109
+ * least one *other* already-configured agent.
110
+ *
111
+ * When no other agent folders exist yet (fresh project bootstrap), every skill
112
+ * present on disk is mirrored.
113
+ *
114
+ * Includes kits (which have SKILL.md without frontmatter — still need agent
115
+ * symlinks for filesystem parity) AND locally-authored skills that were never
116
+ * published (no lock entry, but legitimate project skills).
72
117
  */
73
118
  const _select_skills_to_mirror = async (lock_data, is_global, project_root, new_agent) => {
74
- const all = get_all_locked_skills(lock_data)
75
- const installed = Object.entries(all).filter(([, data]) => data && data.type !== SKILL_TYPES.KIT)
119
+ const display_names = _build_display_name_map(lock_data)
120
+ const candidates = await _list_canonical_skill_names(is_global, project_root)
76
121
 
77
- // Find any *other* agents that are already configured in this scope
78
122
  const [, all_detected] = await detect_agents({ global: is_global, project_root })
79
123
  const other_detected = (all_detected || []).filter(a => a.id !== new_agent.id)
80
124
 
81
125
  const selected = []
82
126
  const skipped_disabled = []
83
127
 
84
- for (const [full_name, data] of installed) {
85
- const short = full_name.split('/')[1] || full_name
128
+ for (const short of candidates) {
129
+ const full_name = display_names.get(short) || short
86
130
 
87
131
  if (other_detected.length === 0) {
88
132
  selected.push({ full_name, short })
@@ -105,7 +149,6 @@ const _add = async (raw_args, args) => {
105
149
  const base_dir = skills_dir(is_global, project_root)
106
150
 
107
151
  const [, lock_data] = await read_lock(lock_root(is_global, project_root))
108
- const has_lock = !!(lock_data && lock_data.skills && Object.keys(lock_data.skills).length > 0)
109
152
 
110
153
  const per_agent = []
111
154
 
@@ -113,11 +156,6 @@ const _add = async (raw_args, args) => {
113
156
  const target_root = agent_skills_dir(agent, is_global, project_root)
114
157
  await fs.promises.mkdir(target_root, { recursive: true })
115
158
 
116
- if (!has_lock) {
117
- per_agent.push({ agent_id: agent.id, status: 'configured', linked: [], skipped_disabled: [] })
118
- continue
119
- }
120
-
121
159
  const { selected, skipped_disabled } = await _select_skills_to_mirror(lock_data, is_global, project_root, agent)
122
160
  const linked = []
123
161
 
@@ -160,9 +198,7 @@ const _remove = async (raw_args, args) => {
160
198
  const project_root = find_project_root()
161
199
  const is_json = args.flags.json || false
162
200
 
163
- const [, lock_data] = await read_lock(lock_root(is_global, project_root))
164
- const all = get_all_locked_skills(lock_data)
165
- const short_names = Object.keys(all).map(k => k.split('/')[1] || k)
201
+ const short_names = await _list_canonical_skill_names(is_global, project_root)
166
202
 
167
203
  const per_agent = []
168
204
 
@@ -235,9 +271,7 @@ const _list = async (args) => {
235
271
  const [, detected] = await detect_agents({ global: is_global, project_root })
236
272
  const detected_ids = new Set((detected || []).map(a => a.id))
237
273
 
238
- const [, lock_data] = await read_lock(lock_root(is_global, project_root))
239
- const all = get_all_locked_skills(lock_data)
240
- const short_names = Object.keys(all).map(k => k.split('/')[1] || k)
274
+ const short_names = await _list_canonical_skill_names(is_global, project_root)
241
275
 
242
276
  const rows = []
243
277
  for (const agent of AGENTS) {