happyskills 0.34.0 → 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 +10 -0
- package/package.json +1 -1
- package/src/commands/install.js +59 -30
- package/src/commands/uninstall.js +35 -12
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ 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
|
+
|
|
10
20
|
## [0.34.0] - 2026-04-09
|
|
11
21
|
|
|
12
22
|
### Changed
|
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
|
|
|
@@ -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 } })
|