harveyz-skill 0.6.2 → 0.8.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 +100 -0
- package/README.md +85 -39
- package/bin/cli.js +444 -51
- package/hooks/check-similar-branch/check-similar-branch.sh +75 -0
- package/lib/bundles.js +74 -7
- package/lib/installer.js +256 -30
- package/lib/targets.js +22 -13
- package/package.json +9 -6
- package/skills/analysis/git-cleanup/SKILL.md +189 -0
- package/skills/design/sync-design-html/SKILL.md +354 -0
- package/skills/design/sync-design-html/references/stack-electron.md +119 -0
- package/skills/design/sync-design-html/references/stack-nextjs.md +101 -0
- package/skills/design/sync-design-html/references/stack-react.md +117 -0
- package/skills/design/sync-design-html/references/stack-swiftui.md +115 -0
- package/skills/design/sync-design-html/references/stack-vue.md +142 -0
- package/skills/design/sync-design-html/references/stacks.md +45 -0
- package/skills/meta/contribute-skill/SKILL.md +301 -0
- package/skills/web-fetch/article-fetcher/SKILL.md +95 -509
- package/skills/web-fetch/article-fetcher/references/__pycache__/article_utils.cpython-314.pyc +0 -0
- package/skills/web-fetch/article-fetcher/references/article_utils.py +32 -5
- package/skills/web-fetch/article-fetcher/scripts/playwright_web.py +172 -0
- package/skills/web-fetch/article-fetcher/scripts/playwright_xcom.py +221 -0
- package/skills/web-fetch/article-fetcher/scripts/validate_article.py +36 -0
- package/skills/web-fetch/article-fetcher/tests/test_security.py +257 -0
- package/skills-index.json +20 -4
package/bin/cli.js
CHANGED
|
@@ -7,13 +7,13 @@ import os from 'os'
|
|
|
7
7
|
import path from 'path'
|
|
8
8
|
import { fileURLToPath } from 'url'
|
|
9
9
|
import {
|
|
10
|
-
getAllSkillItems, getAllToolItems,
|
|
10
|
+
getAllSkillItems, getAllToolItems, getAllHookItems, checkHookInstalled,
|
|
11
11
|
checkInstalled, checkToolInstalled, scopeSummary,
|
|
12
12
|
resolveSkills, resolveSkillsByName, resolveTools, resolveToolsByName,
|
|
13
13
|
TOOL_BUNDLE_CHOICES,
|
|
14
14
|
} from '../lib/bundles.js'
|
|
15
15
|
import { buildTargetChoices, resolveTargets, TARGETS } from '../lib/targets.js'
|
|
16
|
-
import { installSkills, installTools } from '../lib/installer.js'
|
|
16
|
+
import { installSkills, installTools, installHooks, uninstallHook } from '../lib/installer.js'
|
|
17
17
|
|
|
18
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
19
19
|
const require = createRequire(import.meta.url)
|
|
@@ -21,25 +21,31 @@ const { version } = require('../package.json')
|
|
|
21
21
|
|
|
22
22
|
const args = process.argv.slice(2)
|
|
23
23
|
const subcommand = args[0]
|
|
24
|
+
const jsonFlag = args.includes('--json')
|
|
24
25
|
|
|
25
26
|
// ── Help ─────────────────────────────────────────────────────────────────────
|
|
26
27
|
function printHelp() {
|
|
27
28
|
console.log(`
|
|
28
|
-
${chalk.bold('hskill')} — skill manager for Claude Code, Cursor, and
|
|
29
|
+
${chalk.bold('hskill')} — skill manager for Claude Code, Cursor, Codex, OpenClaw, and Hermes v${version}
|
|
29
30
|
|
|
30
31
|
${chalk.cyan('Usage:')}
|
|
31
|
-
hskill interactive install
|
|
32
|
+
hskill interactive install (requires TTY + fzf)
|
|
32
33
|
hskill install interactive install (explicit)
|
|
33
34
|
hskill install --bundle <b> install a skill bundle
|
|
34
35
|
hskill install --skill <s> install specific skill(s)
|
|
35
36
|
hskill install --tool <t> install shell tool(s)
|
|
36
|
-
hskill install --target <t> set target (claude/cursor/codex/all)
|
|
37
|
+
hskill install --target <t> set target (claude/cursor/codex/openclaw/hermes/all)
|
|
37
38
|
hskill install --scope <s> set scope: user (default) or project
|
|
38
39
|
hskill install --force overwrite existing installs
|
|
39
|
-
hskill list
|
|
40
|
-
hskill status
|
|
41
|
-
hskill outdated
|
|
42
|
-
hskill info <name>
|
|
40
|
+
hskill list [--json] list available skills and bundles
|
|
41
|
+
hskill status [--json] show install status for all skills and tools
|
|
42
|
+
hskill outdated [--json] list skills and tools with available updates
|
|
43
|
+
hskill info <name> [--json] show install detail for a skill or tool
|
|
44
|
+
hskill hooks list [--json] list hooks and install status
|
|
45
|
+
hskill hooks install [--name <n>] [--scope <s>] install hook (scope: user|project)
|
|
46
|
+
hskill hooks install [--project <path>] target project dir (for project scope)
|
|
47
|
+
hskill hooks install [--force] overwrite existing
|
|
48
|
+
hskill hooks uninstall <name> [--scope <s>] remove hook
|
|
43
49
|
hskill update update hskill to the latest version
|
|
44
50
|
hskill version show version
|
|
45
51
|
hskill --help show this help
|
|
@@ -49,12 +55,72 @@ function printHelp() {
|
|
|
49
55
|
hskill install --skill git-workflow-init --target claude --scope project
|
|
50
56
|
hskill install --tool p-launch
|
|
51
57
|
hskill status
|
|
58
|
+
hskill status --json
|
|
52
59
|
hskill outdated
|
|
53
60
|
hskill info git-workflow-init
|
|
61
|
+
|
|
62
|
+
${chalk.cyan('Agent / CI usage:')}
|
|
63
|
+
Use --json for machine-readable output on status, list, outdated, info.
|
|
64
|
+
Set NO_COLOR=1 to suppress ANSI color codes in all output.
|
|
65
|
+
Interactive mode (no flags) requires a TTY; use --bundle/--skill/--tool in scripts.
|
|
54
66
|
`)
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
if (args[0] === '--help' || args[0] === '-h') {
|
|
70
|
+
if (jsonFlag || args.includes('--json')) {
|
|
71
|
+
console.log(JSON.stringify({
|
|
72
|
+
name: 'hskill',
|
|
73
|
+
version,
|
|
74
|
+
description: 'Skill manager for Claude Code, Cursor, Codex, OpenClaw, and Hermes',
|
|
75
|
+
agent_notes: 'Interactive mode requires TTY. Use --json for machine-readable output. Set NO_COLOR=1 to suppress ANSI codes.',
|
|
76
|
+
commands: [
|
|
77
|
+
{
|
|
78
|
+
name: 'install',
|
|
79
|
+
description: 'Install skills or shell tools',
|
|
80
|
+
interactive_fallback: 'Requires TTY + fzf when no flags given',
|
|
81
|
+
note: '--skill and --tool are mutually exclusive; use --bundle to install both',
|
|
82
|
+
flags: [
|
|
83
|
+
{ name: '--bundle', arg: '<name>', description: 'Install a skill bundle (comma-separated)' },
|
|
84
|
+
{ name: '--skill', arg: '<name>', description: 'Install specific skill(s) (comma-separated)' },
|
|
85
|
+
{ name: '--tool', arg: '<name>', description: 'Install shell tool(s) (comma-separated)' },
|
|
86
|
+
{ name: '--target', arg: '<target>', description: 'Install target', enum: ['claude','cursor','codex','openclaw','hermes','all'] },
|
|
87
|
+
{ name: '--scope', arg: '<scope>', description: 'Install scope', enum: ['user','project'], default: 'user' },
|
|
88
|
+
{ name: '--force', description: 'Overwrite existing installs' },
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'list',
|
|
93
|
+
description: 'List available skills and bundles',
|
|
94
|
+
flags: [{ name: '--json', description: 'Machine-readable output' }],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'status',
|
|
98
|
+
description: 'Show install status for all skills and tools',
|
|
99
|
+
flags: [{ name: '--json', description: 'Machine-readable output' }],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'outdated',
|
|
103
|
+
description: 'List skills and tools with available updates',
|
|
104
|
+
flags: [{ name: '--json', description: 'Machine-readable output' }],
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'info',
|
|
108
|
+
description: 'Show install detail for a skill or tool',
|
|
109
|
+
args: ['<name>'],
|
|
110
|
+
flags: [{ name: '--json', description: 'Machine-readable output' }],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'update',
|
|
114
|
+
description: 'Update hskill to the latest version via npm',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'version',
|
|
118
|
+
description: 'Print version and exit',
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
}, null, 2))
|
|
122
|
+
process.exit(0)
|
|
123
|
+
}
|
|
58
124
|
printHelp()
|
|
59
125
|
process.exit(0)
|
|
60
126
|
}
|
|
@@ -85,6 +151,14 @@ if (subcommand === 'list') {
|
|
|
85
151
|
if (!byBundle[s.bundle]) byBundle[s.bundle] = []
|
|
86
152
|
byBundle[s.bundle].push(s.path)
|
|
87
153
|
}
|
|
154
|
+
if (jsonFlag) {
|
|
155
|
+
const bundles = {}
|
|
156
|
+
for (const [name, paths] of Object.entries(byBundle)) {
|
|
157
|
+
bundles[name] = { description: bundleMeta[name] ?? name, skills: paths }
|
|
158
|
+
}
|
|
159
|
+
console.log(JSON.stringify({ bundles, tools: tools.map(t => t.name) }, null, 2))
|
|
160
|
+
process.exit(0)
|
|
161
|
+
}
|
|
88
162
|
for (const [name, paths] of Object.entries(byBundle)) {
|
|
89
163
|
console.log(chalk.bold(name) + ' — ' + (bundleMeta[name] ?? name))
|
|
90
164
|
for (const p of paths) console.log(' ' + p)
|
|
@@ -97,11 +171,20 @@ if (subcommand === 'list') {
|
|
|
97
171
|
process.exit(0)
|
|
98
172
|
}
|
|
99
173
|
|
|
174
|
+
// ── Helper: resolve displayed version for a hook ──────────────────────────────
|
|
175
|
+
// Priority: user-installed → project-installed → source version
|
|
176
|
+
function resolveHookDisplayVersion(inst, sourceVersion) {
|
|
177
|
+
if (inst.user.version !== '—') return inst.user.version
|
|
178
|
+
if (inst.project.version !== '—') return inst.project.version
|
|
179
|
+
return sourceVersion ?? '—'
|
|
180
|
+
}
|
|
181
|
+
|
|
100
182
|
// ── Status / Outdated ─────────────────────────────────────────────────────────
|
|
101
183
|
if (subcommand === 'status' || subcommand === 'outdated') {
|
|
102
184
|
const outdatedOnly = subcommand === 'outdated'
|
|
103
185
|
const skillItems = getAllSkillItems()
|
|
104
186
|
const toolItems = getAllToolItems()
|
|
187
|
+
const hookItems = getAllHookItems()
|
|
105
188
|
|
|
106
189
|
function icon(status) {
|
|
107
190
|
if (status === 'up-to-date') return chalk.green('✓')
|
|
@@ -122,6 +205,30 @@ if (subcommand === 'status' || subcommand === 'outdated') {
|
|
|
122
205
|
return { name: t.toolName, version: t.version ?? '—', ...inst }
|
|
123
206
|
})
|
|
124
207
|
|
|
208
|
+
if (jsonFlag) {
|
|
209
|
+
const targets = ['claude', 'cursor', 'codex', 'openclaw', 'hermes']
|
|
210
|
+
const jsonSkills = skillRows.map(r => ({
|
|
211
|
+
name: r.name,
|
|
212
|
+
version: r.version,
|
|
213
|
+
user: Object.fromEntries(targets.map(t => [t, r.userDetail[t]])),
|
|
214
|
+
project: Object.fromEntries(targets.map(t => [t, r.projectDetail[t]])),
|
|
215
|
+
}))
|
|
216
|
+
const jsonTools = toolRows.map(r => ({ name: r.name, version: r.version, status: r.status }))
|
|
217
|
+
const jsonHooks = hookItems.map(h => {
|
|
218
|
+
const inst = checkHookInstalled(h.name)
|
|
219
|
+
return { name: h.name, description: h.description, user: inst.user, project: inst.project }
|
|
220
|
+
})
|
|
221
|
+
if (outdatedOnly) {
|
|
222
|
+
console.log(JSON.stringify({
|
|
223
|
+
skills: jsonSkills.filter(s => Object.values(s.user).some(v => v.status === 'update') || Object.values(s.project).some(v => v.status === 'update')),
|
|
224
|
+
tools: jsonTools.filter(t => t.status === 'update'),
|
|
225
|
+
}, null, 2))
|
|
226
|
+
} else {
|
|
227
|
+
console.log(JSON.stringify({ skills: jsonSkills, tools: jsonTools, hooks: jsonHooks }, null, 2))
|
|
228
|
+
}
|
|
229
|
+
process.exit(0)
|
|
230
|
+
}
|
|
231
|
+
|
|
125
232
|
if (outdatedOnly) {
|
|
126
233
|
const outdatedSkills = skillRows.filter(r => r.userStatus === 'update' || r.projectStatus === 'update')
|
|
127
234
|
const outdatedTools = toolRows.filter(r => r.status === 'update')
|
|
@@ -131,7 +238,7 @@ if (subcommand === 'status' || subcommand === 'outdated') {
|
|
|
131
238
|
process.exit(0)
|
|
132
239
|
}
|
|
133
240
|
|
|
134
|
-
const targets = ['claude', 'cursor', 'codex']
|
|
241
|
+
const targets = ['claude', 'cursor', 'codex', 'openclaw', 'hermes']
|
|
135
242
|
|
|
136
243
|
if (outdatedSkills.length) {
|
|
137
244
|
console.log('\n ' + chalk.bold('SKILLS WITH UPDATES'))
|
|
@@ -161,7 +268,7 @@ if (subcommand === 'status' || subcommand === 'outdated') {
|
|
|
161
268
|
}
|
|
162
269
|
|
|
163
270
|
// Full status table
|
|
164
|
-
const allNames = [...skillRows, ...toolRows
|
|
271
|
+
const allNames = [...skillRows.map(r => r.name), ...toolRows.map(r => r.name), ...hookItems.map(h => h.name)]
|
|
165
272
|
const allVers = [...skillRows, ...toolRows].map(r => r.version)
|
|
166
273
|
const nw = Math.max(...allNames.map(n => n.length), 4)
|
|
167
274
|
const vw = Math.max(...allVers.map(v => v.length), 7)
|
|
@@ -183,6 +290,23 @@ if (subcommand === 'status' || subcommand === 'outdated') {
|
|
|
183
290
|
console.log(' ' + r.name.padEnd(nw) + ' ' + chalk.dim(r.version.padEnd(vw)) + ' ' + icon(r.status))
|
|
184
291
|
}
|
|
185
292
|
|
|
293
|
+
// ── hooks ──
|
|
294
|
+
if (hookItems.length > 0) {
|
|
295
|
+
console.log('')
|
|
296
|
+
console.log(' ' + chalk.bold('HOOKS') + chalk.dim(` — ${hookItems.length} available`))
|
|
297
|
+
console.log(sep)
|
|
298
|
+
function hIcon(s) {
|
|
299
|
+
if (s === 'installed') return chalk.green('✓')
|
|
300
|
+
if (s === 'partial') return chalk.yellow('~')
|
|
301
|
+
return chalk.dim('—')
|
|
302
|
+
}
|
|
303
|
+
for (const h of hookItems) {
|
|
304
|
+
const inst = checkHookInstalled(h.name)
|
|
305
|
+
const ver = resolveHookDisplayVersion(inst, h.version)
|
|
306
|
+
console.log(' ' + h.name.padEnd(nw) + ' ' + chalk.dim(ver.padEnd(vw)) + ' ' + hIcon(inst.user.status) + ' ' + hIcon(inst.project.status) + ' ' + chalk.dim(h.description))
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
186
310
|
const installedSkills = skillRows.filter(r => r.userStatus !== 'none' || r.projectStatus !== 'none').length
|
|
187
311
|
const installedTools = toolRows.filter(r => r.status !== 'none').length
|
|
188
312
|
const outdatedCount = skillRows.filter(r => r.userStatus === 'update' || r.projectStatus === 'update').length
|
|
@@ -220,6 +344,24 @@ if (subcommand === 'info') {
|
|
|
220
344
|
return chalk.dim('— not installed')
|
|
221
345
|
}
|
|
222
346
|
|
|
347
|
+
if (jsonFlag) {
|
|
348
|
+
if (skill) {
|
|
349
|
+
const inst = checkInstalled(skill.skillName, skill.version ?? '—')
|
|
350
|
+
console.log(JSON.stringify({
|
|
351
|
+
name: skill.skillName, type: 'skill', version: skill.version ?? '—',
|
|
352
|
+
user: Object.fromEntries(targets.map(t => [t, inst.user[t]])),
|
|
353
|
+
project: Object.fromEntries(targets.map(t => [t, inst.project[t]])),
|
|
354
|
+
}, null, 2))
|
|
355
|
+
} else {
|
|
356
|
+
const inst = checkToolInstalled(tool.toolName, tool.srcPath)
|
|
357
|
+
console.log(JSON.stringify({
|
|
358
|
+
name: tool.toolName, type: 'tool', version: tool.version ?? '—',
|
|
359
|
+
installed: inst,
|
|
360
|
+
}, null, 2))
|
|
361
|
+
}
|
|
362
|
+
process.exit(0)
|
|
363
|
+
}
|
|
364
|
+
|
|
223
365
|
console.log('')
|
|
224
366
|
if (skill) {
|
|
225
367
|
const inst = checkInstalled(skill.skillName, skill.version ?? '—')
|
|
@@ -246,6 +388,115 @@ if (subcommand === 'info') {
|
|
|
246
388
|
process.exit(0)
|
|
247
389
|
}
|
|
248
390
|
|
|
391
|
+
// ── Hooks ─────────────────────────────────────────────────────────────────────
|
|
392
|
+
if (subcommand === 'hooks') {
|
|
393
|
+
const hooksSubcmd = args[1]
|
|
394
|
+
const hookArgs = args.slice(2)
|
|
395
|
+
const hookJsonFlag = hooksSubcmd === '--json' || hookArgs.includes('--json')
|
|
396
|
+
const hookNameIdx = hookArgs.indexOf('--name')
|
|
397
|
+
const hookScopeIdx = hookArgs.indexOf('--scope')
|
|
398
|
+
const hookProjectIdx = hookArgs.indexOf('--project')
|
|
399
|
+
const hookForce = hookArgs.includes('--force')
|
|
400
|
+
const hookNameArg = hookNameIdx !== -1 ? hookArgs[hookNameIdx + 1] : undefined
|
|
401
|
+
const hookScopeArg = hookScopeIdx !== -1 ? hookArgs[hookScopeIdx + 1] : 'user'
|
|
402
|
+
const hookProjectArg = hookProjectIdx !== -1 ? hookArgs[hookProjectIdx + 1] : process.cwd()
|
|
403
|
+
|
|
404
|
+
// Validate scope for install/uninstall commands
|
|
405
|
+
if (!['user', 'project'].includes(hookScopeArg) && ['install', 'uninstall'].includes(hooksSubcmd)) {
|
|
406
|
+
console.error(chalk.red(` ✗ Invalid scope: "${hookScopeArg}". Use user or project.`))
|
|
407
|
+
process.exit(1)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ── hooks list ──────────────────────────────────────────────────────────────
|
|
411
|
+
if (hooksSubcmd === 'list' || !hooksSubcmd || hooksSubcmd.startsWith('--')) {
|
|
412
|
+
const hookItems = getAllHookItems()
|
|
413
|
+
if (hookJsonFlag) {
|
|
414
|
+
const out = hookItems.map(h => {
|
|
415
|
+
const inst = checkHookInstalled(h.name)
|
|
416
|
+
const ver = resolveHookDisplayVersion(inst, h.version)
|
|
417
|
+
return { name: h.name, version: ver, description: h.description, event: h.event, user: inst.user, project: inst.project }
|
|
418
|
+
})
|
|
419
|
+
console.log(JSON.stringify({ hooks: out }, null, 2))
|
|
420
|
+
process.exit(0)
|
|
421
|
+
}
|
|
422
|
+
function hookIcon(s) {
|
|
423
|
+
if (s === 'installed') return chalk.green('✓')
|
|
424
|
+
if (s === 'partial') return chalk.yellow('~')
|
|
425
|
+
return chalk.dim('—')
|
|
426
|
+
}
|
|
427
|
+
const nameWidth = Math.max(...hookItems.map(h => h.name.length), 4)
|
|
428
|
+
const verWidth = 7
|
|
429
|
+
console.log('')
|
|
430
|
+
console.log(' ' + chalk.bold('NAME'.padEnd(nameWidth)) + ' ' + chalk.bold('VER'.padEnd(verWidth)) + ' U P ' + chalk.bold('DESCRIPTION'))
|
|
431
|
+
console.log(' ' + '─'.repeat(nameWidth) + ' ' + '─'.repeat(verWidth) + ' ─ ─ ' + '─'.repeat(20))
|
|
432
|
+
for (const h of hookItems) {
|
|
433
|
+
const inst = checkHookInstalled(h.name)
|
|
434
|
+
const ver = resolveHookDisplayVersion(inst, h.version)
|
|
435
|
+
console.log(' ' + h.name.padEnd(nameWidth) + ' ' + chalk.dim(ver.padEnd(verWidth)) + ' ' + hookIcon(inst.user.status) + ' ' + hookIcon(inst.project.status) + ' ' + h.description)
|
|
436
|
+
}
|
|
437
|
+
console.log('')
|
|
438
|
+
console.log(chalk.dim(` U=user P=project ${chalk.green('✓')}=installed ${chalk.yellow('~')}=partial ${chalk.dim('—')}=none`))
|
|
439
|
+
console.log('')
|
|
440
|
+
process.exit(0)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ── hooks install ────────────────────────────────────────────────────────────
|
|
444
|
+
if (hooksSubcmd === 'install') {
|
|
445
|
+
const hookItems = getAllHookItems()
|
|
446
|
+
let toInstall
|
|
447
|
+
|
|
448
|
+
if (hookNameArg) {
|
|
449
|
+
const found = hookItems.find(h => h.name === hookNameArg)
|
|
450
|
+
if (!found) {
|
|
451
|
+
console.error(chalk.red(` ✗ Unknown hook: "${hookNameArg}"`))
|
|
452
|
+
process.exit(1)
|
|
453
|
+
}
|
|
454
|
+
toInstall = [found]
|
|
455
|
+
} else if (!process.stdout.isTTY) {
|
|
456
|
+
toInstall = hookItems
|
|
457
|
+
} else {
|
|
458
|
+
const { checkbox } = await import('@inquirer/prompts')
|
|
459
|
+
const selected = await checkbox({
|
|
460
|
+
message: 'Select hooks to install:',
|
|
461
|
+
choices: hookItems.map(h => ({ name: `${h.name.padEnd(32)} ${h.description}`, value: h })),
|
|
462
|
+
})
|
|
463
|
+
if (!selected.length) {
|
|
464
|
+
console.log(chalk.dim(' · Nothing selected'))
|
|
465
|
+
process.exit(0)
|
|
466
|
+
}
|
|
467
|
+
toInstall = selected
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const { installed, skipped, failed } = await installHooks(toInstall, hookScopeArg, hookProjectArg, hookForce)
|
|
471
|
+
|
|
472
|
+
if (hookJsonFlag) {
|
|
473
|
+
console.log(JSON.stringify({ installed, skipped, failed }, null, 2))
|
|
474
|
+
process.exit(failed.length ? 1 : 0)
|
|
475
|
+
} else {
|
|
476
|
+
if (installed.length) console.error(chalk.green.bold(`✔ Hooks installed (${hookScopeArg}):`), installed.join(', '))
|
|
477
|
+
for (const s of skipped) console.error(chalk.dim(` · ${s.name} skipped (${s.reason})`))
|
|
478
|
+
|
|
479
|
+
for (const f of failed) console.error(chalk.red(` ✗ ${f.name} failed: ${f.reason}${f.detail ? ` — ${f.detail}` : ''}`))
|
|
480
|
+
process.exit(failed.length ? 1 : 0)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ── hooks uninstall ──────────────────────────────────────────────────────────
|
|
485
|
+
if (hooksSubcmd === 'uninstall') {
|
|
486
|
+
const nameToRemove = args[2]
|
|
487
|
+
if (!nameToRemove || nameToRemove.startsWith('--')) {
|
|
488
|
+
console.error(chalk.red(' ✗ Usage: hskill hooks uninstall <name> [--scope user|project]'))
|
|
489
|
+
process.exit(1)
|
|
490
|
+
}
|
|
491
|
+
const { removed } = await uninstallHook(nameToRemove, hookScopeArg, hookProjectArg)
|
|
492
|
+
if (!removed) console.log(chalk.dim(` · ${nameToRemove} was not installed in ${hookScopeArg} scope`))
|
|
493
|
+
process.exit(0)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
console.error(chalk.red(` ✗ Unknown hooks subcommand: "${hooksSubcmd}". Use list, install, or uninstall.`))
|
|
497
|
+
process.exit(1)
|
|
498
|
+
}
|
|
499
|
+
|
|
249
500
|
// ── Install ───────────────────────────────────────────────────────────────────
|
|
250
501
|
// subcommand is 'install' or omitted (default behavior)
|
|
251
502
|
const installArgs = subcommand === 'install' ? args.slice(1) : args
|
|
@@ -351,7 +602,161 @@ function fzfSelect() {
|
|
|
351
602
|
})
|
|
352
603
|
}
|
|
353
604
|
|
|
605
|
+
// ── Print install summary (shared between interactive loop and non-interactive) ──
|
|
606
|
+
function printSummary(skillSummary, toolSummary) {
|
|
607
|
+
if (skillSummary !== null) {
|
|
608
|
+
const anyInstalled = Object.values(skillSummary).some(r => r.installed.length > 0)
|
|
609
|
+
if (!anyInstalled) {
|
|
610
|
+
console.log(chalk.dim(' · No skills installed'))
|
|
611
|
+
} else {
|
|
612
|
+
console.log(chalk.green.bold('✔ Skills installed:'))
|
|
613
|
+
for (const [target, { installed }] of Object.entries(skillSummary)) {
|
|
614
|
+
if (installed.length > 0)
|
|
615
|
+
console.log(` ${chalk.bold(target)} ← ${installed.join(', ')}`)
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
for (const [target, { skipped, failed }] of Object.entries(skillSummary)) {
|
|
619
|
+
for (const s of skipped) {
|
|
620
|
+
const detail = s.reason === 'up-to-date'
|
|
621
|
+
? `up-to-date ${s.version}`
|
|
622
|
+
: `outdated ${s.installed} → ${s.available}, use --force`
|
|
623
|
+
console.log(chalk.dim(` · ${target}/${s.name} skipped (${detail})`))
|
|
624
|
+
}
|
|
625
|
+
for (const f of failed) {
|
|
626
|
+
console.log(chalk.red(` ✗ ${target}/${f.name} failed: ${f.reason}${f.detail ? ` — ${f.detail}` : ''}`))
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (toolSummary !== null) {
|
|
631
|
+
if (toolSummary.installed.length === 0 && !toolSummary.skipped.length && !toolSummary.failed.length) {
|
|
632
|
+
console.log(chalk.dim(' · No shell tools installed'))
|
|
633
|
+
} else {
|
|
634
|
+
if (toolSummary.installed.length > 0) {
|
|
635
|
+
console.log(chalk.green.bold('✔ Shell tools installed:'))
|
|
636
|
+
for (const name of toolSummary.installed) {
|
|
637
|
+
console.log(` ${chalk.bold('~/.local/bin')} ← ${name}`)
|
|
638
|
+
}
|
|
639
|
+
console.log('')
|
|
640
|
+
console.log(chalk.yellow.bold(' ⚡ Reload your shell to apply changes:'))
|
|
641
|
+
console.log('')
|
|
642
|
+
console.log(` ${chalk.bold.cyan('source ~/.zshrc')}`)
|
|
643
|
+
console.log('')
|
|
644
|
+
}
|
|
645
|
+
for (const s of toolSummary.skipped) {
|
|
646
|
+
console.log(chalk.dim(` · ${s.name} skipped (${s.reason === 'already_exists' ? 'already exists — use --force to overwrite' : s.reason})`))
|
|
647
|
+
}
|
|
648
|
+
for (const f of toolSummary.failed) {
|
|
649
|
+
console.log(chalk.red(` ✗ ${f.name} failed: ${f.reason}${f.detail ? ` — ${f.detail}` : ''}`))
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
354
655
|
try {
|
|
656
|
+
if (toolArg && skillArg) {
|
|
657
|
+
const msg = '--tool and --skill cannot be combined; use --bundle to install both'
|
|
658
|
+
if (jsonFlag) process.stderr.write(JSON.stringify({ error: true, message: msg }) + '\n')
|
|
659
|
+
else console.error(chalk.red(' ✗ ' + msg))
|
|
660
|
+
process.exit(1)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const isInteractive = !toolArg && !skillArg && !bundleArg
|
|
664
|
+
|
|
665
|
+
// ── Interactive mode: loop back to skill selector after each install ────────
|
|
666
|
+
if (isInteractive) {
|
|
667
|
+
if (!process.stdout.isTTY && !process.env.HSKILL_TEST_INTERACTIVE) {
|
|
668
|
+
console.error(chalk.red(' ✗ Interactive mode requires a TTY. Use --bundle, --skill, or --tool flags for non-interactive install.'))
|
|
669
|
+
console.error(chalk.dim(' Example: hskill install --bundle dev --target claude'))
|
|
670
|
+
process.exit(1)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
while (true) {
|
|
674
|
+
const selected = fzfSelect()
|
|
675
|
+
|
|
676
|
+
if (!selected.length) {
|
|
677
|
+
console.log(chalk.dim(' · Nothing selected, exiting'))
|
|
678
|
+
break
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const toolItems = selected.filter(s => s.kind === 'tool')
|
|
682
|
+
const seen = new Set()
|
|
683
|
+
const skillItems = selected.filter(s => s.kind === 'skill').filter(s => {
|
|
684
|
+
if (seen.has(s.skillName)) return false
|
|
685
|
+
seen.add(s.skillName); return true
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
if (!skillItems.length && !toolItems.length) continue
|
|
689
|
+
|
|
690
|
+
let skillSummary = null
|
|
691
|
+
if (skillItems.length > 0) {
|
|
692
|
+
// Scope selection
|
|
693
|
+
const scopeResult = spawnSync('fzf', [
|
|
694
|
+
'--prompt= › ',
|
|
695
|
+
'--header= Scope · enter 确认 · esc 取消',
|
|
696
|
+
'--layout=reverse',
|
|
697
|
+
'--border=rounded',
|
|
698
|
+
'--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
|
|
699
|
+
], {
|
|
700
|
+
input: `user — ~/.claude/skills/ (所有项目共享)\nproject — .claude/skills/ (仅当前项目)`,
|
|
701
|
+
encoding: 'utf8',
|
|
702
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
703
|
+
})
|
|
704
|
+
if (!scopeResult.stdout.trim()) {
|
|
705
|
+
console.log(chalk.dim(' · Cancelled'))
|
|
706
|
+
break
|
|
707
|
+
}
|
|
708
|
+
const scope = scopeResult.stdout.trim().startsWith('project') ? 'project' : 'user'
|
|
709
|
+
|
|
710
|
+
// Target selection
|
|
711
|
+
const targetChoices = buildTargetChoices(scope)
|
|
712
|
+
const targetInput = targetChoices.map(c => c.name).join('\n') + '\nall — all tools'
|
|
713
|
+
const targetResult = spawnSync('fzf', [
|
|
714
|
+
'--multi',
|
|
715
|
+
'--prompt= › ',
|
|
716
|
+
'--header= Install to · tab 多选 · enter 确认 · esc 取消',
|
|
717
|
+
'--layout=reverse',
|
|
718
|
+
'--border=rounded',
|
|
719
|
+
'--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
|
|
720
|
+
], {
|
|
721
|
+
input: targetInput,
|
|
722
|
+
encoding: 'utf8',
|
|
723
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
724
|
+
})
|
|
725
|
+
if (!targetResult.stdout.trim()) {
|
|
726
|
+
console.log(chalk.dim(' · Cancelled'))
|
|
727
|
+
break
|
|
728
|
+
}
|
|
729
|
+
const selectedTargets = targetResult.stdout.trim().split('\n')
|
|
730
|
+
.map(l => l.trim().split(/\s+/)[0])
|
|
731
|
+
|
|
732
|
+
if (selectedTargets.length > 0) {
|
|
733
|
+
const targets = resolveTargets(selectedTargets, scope)
|
|
734
|
+
console.log('')
|
|
735
|
+
skillSummary = await installSkills(skillItems, targets, forceFlag)
|
|
736
|
+
console.log('')
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
let toolSummary = null
|
|
741
|
+
if (toolItems.length > 0) {
|
|
742
|
+
console.log('')
|
|
743
|
+
toolSummary = await installTools(
|
|
744
|
+
toolItems.map(t => ({ toolName: t.toolName, srcPath: t.srcPath })),
|
|
745
|
+
TARGETS.shell,
|
|
746
|
+
forceFlag,
|
|
747
|
+
)
|
|
748
|
+
console.log('')
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
printSummary(skillSummary, toolSummary)
|
|
752
|
+
|
|
753
|
+
// Loop back to skill selector automatically
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
process.exit(0)
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ── Non-interactive mode: resolve items from flags, install once ────────────
|
|
355
760
|
let skillItems = []
|
|
356
761
|
let toolItems = []
|
|
357
762
|
|
|
@@ -367,21 +772,6 @@ try {
|
|
|
367
772
|
const toolBundles = bundles.filter(b => TOOL_BUNDLE_VALUES.has(b))
|
|
368
773
|
if (skillBundles.length) skillItems = resolveSkills(skillBundles).map(s => ({ kind: 'skill', ...s }))
|
|
369
774
|
if (toolBundles.length) toolItems = resolveTools(toolBundles).map(t => ({ kind: 'tool', ...t }))
|
|
370
|
-
} else {
|
|
371
|
-
const selected = fzfSelect()
|
|
372
|
-
|
|
373
|
-
if (!selected.length) {
|
|
374
|
-
console.log(chalk.dim(' · Nothing selected, exiting'))
|
|
375
|
-
process.exit(0)
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
toolItems = selected.filter(s => s.kind === 'tool')
|
|
379
|
-
|
|
380
|
-
const seen = new Set()
|
|
381
|
-
skillItems = selected.filter(s => s.kind === 'skill').filter(s => {
|
|
382
|
-
if (seen.has(s.skillName)) return false
|
|
383
|
-
seen.add(s.skillName); return true
|
|
384
|
-
})
|
|
385
775
|
}
|
|
386
776
|
|
|
387
777
|
if (!skillItems.length && !toolItems.length) {
|
|
@@ -390,10 +780,15 @@ try {
|
|
|
390
780
|
}
|
|
391
781
|
|
|
392
782
|
// ── Install skills ──────────────────────────────────────────────────────────
|
|
783
|
+
let skillSummary = null
|
|
393
784
|
if (skillItems.length > 0) {
|
|
394
785
|
// Resolve scope
|
|
395
786
|
let scope = scopeArg ?? 'user'
|
|
396
787
|
if (!scopeArg && !targetArg) {
|
|
788
|
+
if (!process.stdout.isTTY) {
|
|
789
|
+
console.error(chalk.red(' ✗ Interactive scope selection requires a TTY. Use --scope user|project.'))
|
|
790
|
+
process.exit(1)
|
|
791
|
+
}
|
|
397
792
|
const scopeResult = spawnSync('fzf', [
|
|
398
793
|
'--prompt= › ',
|
|
399
794
|
'--header= Scope · enter 确认 · esc 取消',
|
|
@@ -415,8 +810,12 @@ try {
|
|
|
415
810
|
// Resolve target
|
|
416
811
|
let selectedTargets
|
|
417
812
|
if (targetArg) {
|
|
418
|
-
selectedTargets = targetArg === 'all' ? ['claude', 'cursor', 'codex'] : [targetArg]
|
|
813
|
+
selectedTargets = targetArg === 'all' ? ['claude', 'cursor', 'codex', 'openclaw', 'hermes'] : [targetArg]
|
|
419
814
|
} else {
|
|
815
|
+
if (!process.stdout.isTTY) {
|
|
816
|
+
console.error(chalk.red(' ✗ Interactive target selection requires a TTY. Use --target claude|cursor|codex|openclaw|hermes|all.'))
|
|
817
|
+
process.exit(1)
|
|
818
|
+
}
|
|
420
819
|
const targetChoices = buildTargetChoices(scope)
|
|
421
820
|
const targetInput = targetChoices.map(c => c.name).join('\n') + '\nall — all tools'
|
|
422
821
|
const targetResult = spawnSync('fzf', [
|
|
@@ -442,43 +841,37 @@ try {
|
|
|
442
841
|
if (selectedTargets.length > 0) {
|
|
443
842
|
const targets = resolveTargets(selectedTargets, scope)
|
|
444
843
|
console.log('')
|
|
445
|
-
|
|
844
|
+
skillSummary = await installSkills(skillItems, targets, forceFlag)
|
|
446
845
|
console.log('')
|
|
447
|
-
if (Object.keys(summary).length === 0) {
|
|
448
|
-
console.log(chalk.dim(' · No skills installed'))
|
|
449
|
-
} else {
|
|
450
|
-
console.log(chalk.green.bold('✔ Skills installed:'))
|
|
451
|
-
for (const [target, names] of Object.entries(summary)) {
|
|
452
|
-
console.log(` ${chalk.bold(target)} ← ${names.join(', ')}`)
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
846
|
}
|
|
456
847
|
}
|
|
457
848
|
|
|
458
849
|
// ── Install shell tools ─────────────────────────────────────────────────────
|
|
850
|
+
let toolSummary = null
|
|
459
851
|
if (toolItems.length > 0) {
|
|
460
852
|
console.log('')
|
|
461
|
-
|
|
853
|
+
toolSummary = await installTools(
|
|
462
854
|
toolItems.map(t => ({ toolName: t.toolName, srcPath: t.srcPath })),
|
|
463
855
|
TARGETS.shell,
|
|
464
856
|
forceFlag,
|
|
465
857
|
)
|
|
466
858
|
console.log('')
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
console.log(` ${chalk.bold.cyan('source ~/.zshrc')}`)
|
|
478
|
-
console.log('')
|
|
479
|
-
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// ── Output ──────────────────────────────────────────────────────────────────
|
|
862
|
+
if (jsonFlag) {
|
|
863
|
+
const out = {}
|
|
864
|
+
if (skillSummary !== null) out.skills = skillSummary
|
|
865
|
+
if (toolSummary !== null) out.tools = toolSummary
|
|
866
|
+
console.log(JSON.stringify(out, null, 2))
|
|
867
|
+
} else {
|
|
868
|
+
printSummary(skillSummary, toolSummary)
|
|
480
869
|
}
|
|
481
870
|
} catch (err) {
|
|
482
|
-
|
|
871
|
+
if (jsonFlag) {
|
|
872
|
+
process.stderr.write(JSON.stringify({ error: true, message: err.message }) + '\n')
|
|
873
|
+
} else {
|
|
874
|
+
console.error(chalk.red(' ✗ ' + err.message))
|
|
875
|
+
}
|
|
483
876
|
process.exit(1)
|
|
484
877
|
}
|