happyskills 0.33.1 → 0.35.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 +19 -0
- package/package.json +1 -1
- package/src/commands/install.js +59 -30
- package/src/commands/search.js +21 -15
- package/src/commands/uninstall.js +35 -12
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.35.0] - 2026-04-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Add multi-skill `install` — pass multiple skills in one command (e.g., `happyskills install acme/foo acme/bar@1.0 teamb/baz`)
|
|
14
|
+
- Add multi-skill `uninstall` — remove multiple skills in one command (e.g., `happyskills uninstall acme/foo acme/bar`)
|
|
15
|
+
- Add per-skill inline `@version` pinning for multi-skill install (e.g., `acme/foo@1.2.0 acme/bar@latest`)
|
|
16
|
+
- Add graceful error handling for batch operations — individual failures print a warning and do not interrupt remaining skills
|
|
17
|
+
- Add `--version` flag guard — rejects ambiguous usage with multiple skills, directs to inline `@version` syntax
|
|
18
|
+
- Add `errors` array in JSON output for partial failures alongside successful `data`
|
|
19
|
+
|
|
20
|
+
## [0.34.0] - 2026-04-09
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- Make smart search (semantic + quality ranking) the default when a query is provided — `search "query"` now uses hybrid vector + full-text matching instead of keyword-only
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- Add `--exact` flag to `search` command to opt out of smart search and use keyword-only matching
|
|
27
|
+
- Add `hint` field to `--exact --json` responses nudging users toward smart search for better results
|
|
28
|
+
|
|
10
29
|
## [0.33.1] - 2026-04-08
|
|
11
30
|
|
|
12
31
|
### Fixed
|
package/package.json
CHANGED
package/src/commands/install.js
CHANGED
|
@@ -2,14 +2,14 @@ const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
|
|
|
2
2
|
const { install, install_from_manifest, install_from_lock } = require('../engine/installer')
|
|
3
3
|
const { read_lock } = require('../lock/reader')
|
|
4
4
|
const { read_manifest } = require('../manifest/reader')
|
|
5
|
-
const { print_help, print_hint, print_json, code } = require('../ui/output')
|
|
5
|
+
const { print_help, print_hint, print_json, print_warn, code } = require('../ui/output')
|
|
6
6
|
const { exit_with_error, UsageError } = require('../utils/errors')
|
|
7
7
|
const { find_project_root } = require('../config/paths')
|
|
8
8
|
const { EXIT_CODES } = require('../constants')
|
|
9
9
|
|
|
10
|
-
const HELP_TEXT = `Usage: happyskills install [<owner>/<skill>[@<version>]] [options]
|
|
10
|
+
const HELP_TEXT = `Usage: happyskills install [<owner>/<skill>[@<version>]] [...] [options]
|
|
11
11
|
|
|
12
|
-
Install
|
|
12
|
+
Install one or more skills and their dependencies.
|
|
13
13
|
Without arguments, installs all dependencies from skill.json (or skills-lock.json if no skill.json exists).
|
|
14
14
|
|
|
15
15
|
Arguments:
|
|
@@ -18,8 +18,8 @@ Arguments:
|
|
|
18
18
|
owner/skill@latest Force install of the latest version (bypasses lock skip check)
|
|
19
19
|
|
|
20
20
|
Options:
|
|
21
|
-
-g, --global Install globally (~/.
|
|
22
|
-
--version <ver> Pin to specific version (overrides inline @version)
|
|
21
|
+
-g, --global Install globally (~/.agents/skills/)
|
|
22
|
+
--version <ver> Pin to specific version (single skill only, overrides inline @version)
|
|
23
23
|
--force Force install on dependency conflicts
|
|
24
24
|
--fresh Ignore lock file, re-resolve from scratch
|
|
25
25
|
--agents <list> Target specific agents (comma-separated, e.g., claude,cursor)
|
|
@@ -32,8 +32,8 @@ Aliases: i, add
|
|
|
32
32
|
Examples:
|
|
33
33
|
happyskills install
|
|
34
34
|
happyskills install acme/deploy-aws
|
|
35
|
-
happyskills install acme/deploy-aws
|
|
36
|
-
happyskills install acme/deploy-aws@latest
|
|
35
|
+
happyskills install acme/deploy-aws acme/monitor acme/logging
|
|
36
|
+
happyskills install acme/deploy-aws@1.2.0 acme/monitor@latest
|
|
37
37
|
happyskills install acme/deploy-aws --version 1.2.0
|
|
38
38
|
happyskills i acme/deploy-aws -g`
|
|
39
39
|
|
|
@@ -43,20 +43,9 @@ const run = (args) => catch_errors('Install failed', async () => {
|
|
|
43
43
|
return process.exit(EXIT_CODES.SUCCESS)
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// Support owner/skill@version inline syntax
|
|
50
|
-
if (skill && skill.includes('@')) {
|
|
51
|
-
const at_idx = skill.lastIndexOf('@')
|
|
52
|
-
const inline_version = skill.slice(at_idx + 1)
|
|
53
|
-
skill = skill.slice(0, at_idx)
|
|
54
|
-
if (inline_version && !args.flags.version) version = inline_version
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const options = {
|
|
46
|
+
const skills_raw = args._
|
|
47
|
+
const base_options = {
|
|
58
48
|
global: args.flags.global || false,
|
|
59
|
-
version,
|
|
60
49
|
force: args.flags.force || false,
|
|
61
50
|
fresh: args.flags.fresh || false,
|
|
62
51
|
yes: args.flags.yes || false,
|
|
@@ -64,12 +53,12 @@ const run = (args) => catch_errors('Install failed', async () => {
|
|
|
64
53
|
project_root: find_project_root()
|
|
65
54
|
}
|
|
66
55
|
|
|
67
|
-
if (
|
|
56
|
+
if (skills_raw.length === 0) {
|
|
68
57
|
const [manifest_err, manifest] = await read_manifest()
|
|
69
58
|
if (manifest_err) {
|
|
70
|
-
const [, lock_data] = await read_lock(
|
|
59
|
+
const [, lock_data] = await read_lock(base_options.project_root)
|
|
71
60
|
if (lock_data && lock_data.skills && Object.keys(lock_data.skills).length > 0) {
|
|
72
|
-
const [errors, result] = await install_from_lock(lock_data,
|
|
61
|
+
const [errors, result] = await install_from_lock(lock_data, base_options)
|
|
73
62
|
if (errors) throw e('Install from lock failed', errors)
|
|
74
63
|
if (args.flags.json) {
|
|
75
64
|
print_json({ data: result })
|
|
@@ -80,7 +69,7 @@ const run = (args) => catch_errors('Install failed', async () => {
|
|
|
80
69
|
throw new UsageError("No skill specified and no skill.json found. Run 'happyskills init' to create a skill.json, or specify a skill to install.")
|
|
81
70
|
}
|
|
82
71
|
|
|
83
|
-
const [errors, result] = await install_from_manifest(manifest,
|
|
72
|
+
const [errors, result] = await install_from_manifest(manifest, base_options)
|
|
84
73
|
if (errors) throw e('Install from manifest failed', errors)
|
|
85
74
|
if (args.flags.json) {
|
|
86
75
|
print_json({ data: result })
|
|
@@ -89,22 +78,62 @@ const run = (args) => catch_errors('Install failed', async () => {
|
|
|
89
78
|
return
|
|
90
79
|
}
|
|
91
80
|
|
|
92
|
-
|
|
93
|
-
|
|
81
|
+
// Parse each argument: extract skill name and optional inline @version
|
|
82
|
+
const parsed = skills_raw.map(raw => {
|
|
83
|
+
let skill = raw, version = undefined
|
|
84
|
+
if (skill.includes('@')) {
|
|
85
|
+
const at_idx = skill.lastIndexOf('@')
|
|
86
|
+
version = skill.slice(at_idx + 1)
|
|
87
|
+
skill = skill.slice(0, at_idx)
|
|
88
|
+
}
|
|
89
|
+
return { skill, version }
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
for (const { skill } of parsed) {
|
|
93
|
+
if (!skill.includes('/')) {
|
|
94
|
+
throw new UsageError(`Skill must be in owner/name format (e.g., acme/deploy-aws). Got: ${skill}`)
|
|
95
|
+
}
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
// --version flag only makes sense for a single skill
|
|
99
|
+
const flag_version = args.flags.version || undefined
|
|
100
|
+
if (flag_version && parsed.length > 1) {
|
|
101
|
+
throw new UsageError('--version flag cannot be used with multiple skills. Use inline @version syntax instead (e.g., acme/foo@1.2.0 acme/bar@2.0.0).')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const results = []
|
|
105
|
+
const failures = []
|
|
106
|
+
for (const { skill, version: inline_version } of parsed) {
|
|
107
|
+
const version = flag_version || inline_version
|
|
108
|
+
const [errors, result] = await install(skill, { ...base_options, version })
|
|
109
|
+
if (errors) {
|
|
110
|
+
const msg = errors[0]?.message || errors.message || String(errors)
|
|
111
|
+
print_warn(`Failed to install ${skill}: ${msg}`)
|
|
112
|
+
failures.push({ skill, error: msg })
|
|
113
|
+
} else {
|
|
114
|
+
results.push({ skill, result })
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (results.length === 0 && failures.length > 0) {
|
|
119
|
+
throw new Error(`All ${failures.length} install(s) failed.`)
|
|
120
|
+
}
|
|
98
121
|
|
|
99
122
|
if (args.flags.json) {
|
|
100
|
-
|
|
123
|
+
const items = results.map(({ skill, result }) => ({
|
|
101
124
|
skill,
|
|
102
125
|
version: result.version,
|
|
103
126
|
installed: result.installed || [],
|
|
104
127
|
skipped: result.skipped || [],
|
|
105
128
|
warnings: result.warnings || [],
|
|
106
129
|
forced: result.forced || []
|
|
107
|
-
}
|
|
130
|
+
}))
|
|
131
|
+
const data = results.length === 1 && failures.length === 0 ? items[0] : items
|
|
132
|
+
if (failures.length > 0) {
|
|
133
|
+
print_json({ data, errors: failures })
|
|
134
|
+
} else {
|
|
135
|
+
print_json({ data })
|
|
136
|
+
}
|
|
108
137
|
return
|
|
109
138
|
}
|
|
110
139
|
|
package/src/commands/search.js
CHANGED
|
@@ -10,6 +10,9 @@ const HELP_TEXT = `Usage: happyskills search [query] [options]
|
|
|
10
10
|
|
|
11
11
|
Search the skill registry.
|
|
12
12
|
|
|
13
|
+
When a query is provided, smart search (semantic + quality ranking) is used by
|
|
14
|
+
default. Use --exact to fall back to keyword-only matching.
|
|
15
|
+
|
|
13
16
|
Arguments:
|
|
14
17
|
query Search term (optional with --mine, --personal, or --workspace)
|
|
15
18
|
|
|
@@ -19,21 +22,22 @@ Options:
|
|
|
19
22
|
--personal Search only your personal workspace
|
|
20
23
|
--tags <tags> Filter by tags (comma-separated)
|
|
21
24
|
--type <type> Filter by type (skill, kit)
|
|
22
|
-
--
|
|
23
|
-
--limit <n> Max results (default: 10, max: 50)
|
|
24
|
-
--min-quality <n> Minimum quality score 0-100
|
|
25
|
+
--exact Use keyword-only matching instead of smart search
|
|
26
|
+
--limit <n> Max results (default: 10, max: 50)
|
|
27
|
+
--min-quality <n> Minimum quality score 0-100
|
|
25
28
|
--json Output as JSON
|
|
26
29
|
|
|
27
30
|
Aliases: s
|
|
28
31
|
|
|
29
32
|
Examples:
|
|
30
33
|
happyskills search deploy
|
|
34
|
+
happyskills search "REST API with Node.js"
|
|
31
35
|
happyskills search --mine
|
|
32
|
-
happyskills search "REST API with Node.js" --smart
|
|
33
36
|
happyskills search deploy --workspace acme
|
|
34
37
|
happyskills search --type kit
|
|
35
38
|
happyskills s --personal --json
|
|
36
|
-
happyskills search "deploy to AWS"
|
|
39
|
+
happyskills search "deploy to AWS" --limit 5
|
|
40
|
+
happyskills search deploy --exact`
|
|
37
41
|
|
|
38
42
|
const QUALITY_TIERS = [
|
|
39
43
|
{ min: 80, label: 'High quality', color: cyan },
|
|
@@ -140,7 +144,7 @@ const run_smart_search = (args, query, options) => catch_errors('Smart search fa
|
|
|
140
144
|
console.log(`\n${gray(`Showing ${items.length} result${items.length === 1 ? '' : 's'}. Install with: happyskills install <owner>/<name>`)}\n`)
|
|
141
145
|
})
|
|
142
146
|
|
|
143
|
-
const run_keyword_search = (args, query, options) => catch_errors('Keyword search failed', async () => {
|
|
147
|
+
const run_keyword_search = (args, query, options, { is_exact } = {}) => catch_errors('Keyword search failed', async () => {
|
|
144
148
|
const [errors, results] = await repos_api.search(query, options)
|
|
145
149
|
if (errors) throw e('Search failed', errors)
|
|
146
150
|
|
|
@@ -149,7 +153,9 @@ const run_keyword_search = (args, query, options) => catch_errors('Keyword searc
|
|
|
149
153
|
|
|
150
154
|
if (items.length === 0) {
|
|
151
155
|
if (args.flags.json) {
|
|
152
|
-
|
|
156
|
+
const data = { query, scope: effective_scope, results: [], count: 0 }
|
|
157
|
+
if (is_exact) data.hint = 'Smart search (default) uses semantic matching for better results. Remove --exact to use it.'
|
|
158
|
+
print_json({ data })
|
|
153
159
|
return
|
|
154
160
|
}
|
|
155
161
|
const context = query ? ` for "${query}"` : ''
|
|
@@ -165,7 +171,9 @@ const run_keyword_search = (args, query, options) => catch_errors('Keyword searc
|
|
|
165
171
|
version: item.latest_version || item.version || '-',
|
|
166
172
|
visibility: item.visibility || 'public'
|
|
167
173
|
}))
|
|
168
|
-
|
|
174
|
+
const data = { query, scope: effective_scope, results: mapped, count: mapped.length }
|
|
175
|
+
if (is_exact) data.hint = 'Smart search (default) uses semantic matching for better results. Remove --exact to use it.'
|
|
176
|
+
print_json({ data })
|
|
169
177
|
return
|
|
170
178
|
}
|
|
171
179
|
|
|
@@ -191,13 +199,11 @@ const run = (args) => catch_errors('Search failed', async () => {
|
|
|
191
199
|
|
|
192
200
|
const query = args._.join(' ') || null
|
|
193
201
|
const { mine, personal, workspace, tags, type } = args.flags
|
|
194
|
-
const
|
|
202
|
+
const is_exact = !!args.flags.exact
|
|
203
|
+
// --smart / -S accepted for backward compat (now the default when a query is provided)
|
|
204
|
+
const use_smart = query && !is_exact
|
|
195
205
|
const has_scope_flag = mine || personal || workspace
|
|
196
206
|
|
|
197
|
-
if (is_smart && !query) {
|
|
198
|
-
throw new UsageError('--smart requires a search query.')
|
|
199
|
-
}
|
|
200
|
-
|
|
201
207
|
if (!query && !has_scope_flag && !tags && !type) {
|
|
202
208
|
throw new UsageError('Please provide a search query or use --mine, --personal, or --workspace.')
|
|
203
209
|
}
|
|
@@ -221,7 +227,7 @@ const run = (args) => catch_errors('Search failed', async () => {
|
|
|
221
227
|
if (tags) options.tags = tags
|
|
222
228
|
if (type) options.type = type
|
|
223
229
|
|
|
224
|
-
if (
|
|
230
|
+
if (use_smart) {
|
|
225
231
|
const [errors] = await run_smart_search(args, query, options)
|
|
226
232
|
if (errors) throw e('Smart search failed', errors)
|
|
227
233
|
} else {
|
|
@@ -231,7 +237,7 @@ const run = (args) => catch_errors('Search failed', async () => {
|
|
|
231
237
|
kw_options.workspace = options.workspace_slug
|
|
232
238
|
delete kw_options.workspace_slug
|
|
233
239
|
}
|
|
234
|
-
const [errors] = await run_keyword_search(args, query, kw_options)
|
|
240
|
+
const [errors] = await run_keyword_search(args, query, kw_options, { is_exact })
|
|
235
241
|
if (errors) throw e('Search failed', errors)
|
|
236
242
|
}
|
|
237
243
|
}).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
|
|
2
2
|
const { uninstall } = require('../engine/uninstaller')
|
|
3
|
-
const { print_help, print_json } = require('../ui/output')
|
|
3
|
+
const { print_help, print_json, print_warn } = require('../ui/output')
|
|
4
4
|
const { exit_with_error, UsageError } = require('../utils/errors')
|
|
5
5
|
const { find_project_root } = require('../config/paths')
|
|
6
6
|
const { EXIT_CODES } = require('../constants')
|
|
7
7
|
|
|
8
|
-
const HELP_TEXT = `Usage: happyskills uninstall <owner/skill> [options]
|
|
8
|
+
const HELP_TEXT = `Usage: happyskills uninstall <owner/skill> [...] [options]
|
|
9
9
|
|
|
10
|
-
Remove
|
|
10
|
+
Remove one or more skills and prune orphaned dependencies.
|
|
11
11
|
|
|
12
12
|
Arguments:
|
|
13
|
-
owner/skill Skill to remove (e.g., acme/deploy-aws)
|
|
13
|
+
owner/skill Skill(s) to remove (e.g., acme/deploy-aws teamb/logging)
|
|
14
14
|
|
|
15
15
|
Options:
|
|
16
16
|
-g, --global Remove from global scope
|
|
@@ -23,6 +23,7 @@ Aliases: rm, remove
|
|
|
23
23
|
|
|
24
24
|
Examples:
|
|
25
25
|
happyskills uninstall acme/deploy-aws
|
|
26
|
+
happyskills uninstall acme/deploy-aws acme/monitor acme/logging
|
|
26
27
|
happyskills rm acme/deploy-aws -g`
|
|
27
28
|
|
|
28
29
|
const run = (args) => catch_errors('Uninstall failed', async () => {
|
|
@@ -31,13 +32,15 @@ const run = (args) => catch_errors('Uninstall failed', async () => {
|
|
|
31
32
|
return process.exit(EXIT_CODES.SUCCESS)
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
const
|
|
35
|
-
if (
|
|
35
|
+
const skills_raw = args._
|
|
36
|
+
if (skills_raw.length === 0) {
|
|
36
37
|
throw new UsageError('Please specify a skill to uninstall (e.g., happyskills uninstall acme/deploy-aws).')
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
for (const skill of skills_raw) {
|
|
41
|
+
if (!skill.includes('/')) {
|
|
42
|
+
throw new UsageError(`Skill must be in owner/name format (e.g., acme/deploy-aws). Got: ${skill}`)
|
|
43
|
+
}
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
const options = {
|
|
@@ -46,15 +49,35 @@ const run = (args) => catch_errors('Uninstall failed', async () => {
|
|
|
46
49
|
project_root: find_project_root()
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
const
|
|
50
|
-
|
|
52
|
+
const results = []
|
|
53
|
+
const failures = []
|
|
54
|
+
for (const skill of skills_raw) {
|
|
55
|
+
const [errors, result] = await uninstall(skill, options)
|
|
56
|
+
if (errors) {
|
|
57
|
+
const msg = errors[0]?.message || errors.message || String(errors)
|
|
58
|
+
print_warn(`${skill}: ${msg}`)
|
|
59
|
+
failures.push({ skill, error: msg })
|
|
60
|
+
} else {
|
|
61
|
+
results.push({ skill, result })
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (results.length === 0 && failures.length > 0) {
|
|
66
|
+
throw new Error(`All ${failures.length} uninstall(s) failed.`)
|
|
67
|
+
}
|
|
51
68
|
|
|
52
69
|
if (args.flags.json) {
|
|
53
|
-
|
|
70
|
+
const items = results.map(({ skill, result }) => ({
|
|
54
71
|
skill,
|
|
55
72
|
removed: result.removed || [],
|
|
56
73
|
orphans_pruned: result.orphans_pruned || []
|
|
57
|
-
}
|
|
74
|
+
}))
|
|
75
|
+
const data = results.length === 1 && failures.length === 0 ? items[0] : items
|
|
76
|
+
if (failures.length > 0) {
|
|
77
|
+
print_json({ data, errors: failures })
|
|
78
|
+
} else {
|
|
79
|
+
print_json({ data })
|
|
80
|
+
}
|
|
58
81
|
return
|
|
59
82
|
}
|
|
60
83
|
}).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
|