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 +7 -0
- package/package.json +1 -1
- package/src/commands/agents.js +57 -23
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
package/src/commands/agents.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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
|
-
*
|
|
71
|
-
|
|
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
|
|
75
|
-
const
|
|
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
|
|
85
|
-
const
|
|
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
|
|
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
|
|
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) {
|