harveyz-skill 0.8.1 → 0.9.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 +15 -0
- package/bin/cli.js +311 -50
- package/lib/bundles.js +43 -11
- package/lib/installer.js +293 -5
- package/lib/targets.js +9 -0
- package/package.json +2 -2
- package/skills/harness/git-workflow-init/SKILL.md +30 -113
- package/skills/harness/git-workflow-init/references/acceptance-test.md +133 -0
- package/skills/harness/git-workflow-init/references/conflict-analysis.md +126 -0
- package/skills/harness/git-workflow-init/references/hook-templates.md +4 -4
- package/skills-index.json +1 -1
- package/tools/p-launch/__pycache__/p_launch.cpython-314.pyc +0 -0
- package/tools/p-launch/p-launch.py +6 -0
- package/tools/p-launch/p-launch.sh +25 -291
- package/tools/p-launch/p_launch.py +585 -0
- package/tools/p-launch/tests/__pycache__/test_p_launch.cpython-314-pytest-9.0.2.pyc +0 -0
- package/tools/p-launch/tests/e2e.bats +212 -7
- package/tools/p-launch/tests/test_p_launch.py +239 -0
- package/tools/p-launch/tests/unit.bats +107 -196
- package/tools/p-launch/tool.json +11 -1
- package/skills/harness/git-workflow-init/evals/evals.json +0 -104
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.0] - 2026-05-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `hskill uninstall <tool>` 命令:清理 binary、Python 模块、tool.json、venv 及 zshrc snippet
|
|
14
|
+
- `hskill uninstall <tool> --yes`:跳过所有确认(含用户配置文件)
|
|
15
|
+
- `hskill uninstall <skill> --scope <s> --target <t>`:卸载已安装的 skill 目录
|
|
16
|
+
- `tool.json` 新增 `uninstallPaths[]` 和 `configPaths[]` 字段,tool 可声明 tool-specific 清理路径
|
|
17
|
+
- fzf 交互界面在选完 item 后新增 Action 选择步骤(安装 / 卸载),支持在同一界面卸载 tool / skill / hook
|
|
18
|
+
- p-launch:迁移至 Python + Textual,新增三栏 TUI 界面
|
|
19
|
+
- p-launch:push/pull 时检测 diverged 分支(本地和远端均有新提交),跳过操作并提示
|
|
20
|
+
- p-launch:首次运行自动创建隔离 venv 并安装 textual,无需手动配置依赖
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- p-launch `tool.json` 新增 `uninstallPaths`(`p-launch-venv`)和 `configPaths`(`~/.config/p-launch`)
|
|
24
|
+
|
|
10
25
|
## [0.8.1] - 2026-05-24
|
|
11
26
|
|
|
12
27
|
### Fixed
|
package/bin/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
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, installHooks, uninstallHook } from '../lib/installer.js'
|
|
16
|
+
import { installSkills, installTools, installHooks, installHooksForTarget, uninstallHook, uninstallTool, uninstallSkill } from '../lib/installer.js'
|
|
17
17
|
|
|
18
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
19
19
|
const require = createRequire(import.meta.url)
|
|
@@ -46,6 +46,9 @@ function printHelp() {
|
|
|
46
46
|
hskill hooks install [--project <path>] target project dir (for project scope)
|
|
47
47
|
hskill hooks install [--force] overwrite existing
|
|
48
48
|
hskill hooks uninstall <name> [--scope <s>] remove hook
|
|
49
|
+
hskill uninstall <tool> uninstall a shell tool and clean up all files
|
|
50
|
+
hskill uninstall <tool> --yes skip all confirmations (incl. config files)
|
|
51
|
+
hskill uninstall <skill> --scope <s> --target <t> uninstall a skill
|
|
49
52
|
hskill update update hskill to the latest version
|
|
50
53
|
hskill version show version
|
|
51
54
|
hskill --help show this help
|
|
@@ -109,6 +112,16 @@ if (args[0] === '--help' || args[0] === '-h') {
|
|
|
109
112
|
args: ['<name>'],
|
|
110
113
|
flags: [{ name: '--json', description: 'Machine-readable output' }],
|
|
111
114
|
},
|
|
115
|
+
{
|
|
116
|
+
name: 'uninstall',
|
|
117
|
+
description: 'Uninstall a shell tool or skill',
|
|
118
|
+
args: ['<name>'],
|
|
119
|
+
flags: [
|
|
120
|
+
{ name: '--yes', description: 'Skip all confirmations including config file removal' },
|
|
121
|
+
{ name: '--scope', arg: '<scope>', description: 'Skill scope: user or project', enum: ['user','project'] },
|
|
122
|
+
{ name: '--target', arg: '<target>', description: 'Skill target: claude, cursor, codex, etc.' },
|
|
123
|
+
],
|
|
124
|
+
},
|
|
112
125
|
{
|
|
113
126
|
name: 'update',
|
|
114
127
|
description: 'Update hskill to the latest version via npm',
|
|
@@ -388,6 +401,54 @@ if (subcommand === 'info') {
|
|
|
388
401
|
process.exit(0)
|
|
389
402
|
}
|
|
390
403
|
|
|
404
|
+
// ── Uninstall ─────────────────────────────────────────────────────────────────
|
|
405
|
+
if (subcommand === 'uninstall') {
|
|
406
|
+
const nameToRemove = args[1]
|
|
407
|
+
if (!nameToRemove || nameToRemove.startsWith('--')) {
|
|
408
|
+
console.error(chalk.red(' ✗ Usage: hskill uninstall <tool-or-skill-name> [--yes] [--scope user|project] [--target claude|...]'))
|
|
409
|
+
process.exit(1)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const yesFlag = args.includes('--yes')
|
|
413
|
+
const scopeIdx2 = args.indexOf('--scope')
|
|
414
|
+
const targetIdx2 = args.indexOf('--target')
|
|
415
|
+
const scopeArg2 = scopeIdx2 !== -1 ? args[scopeIdx2 + 1] : 'user'
|
|
416
|
+
const targetArg2 = targetIdx2 !== -1 ? args[targetIdx2 + 1] : undefined
|
|
417
|
+
|
|
418
|
+
const toolItems2 = getAllToolItems()
|
|
419
|
+
const skillItems2 = getAllSkillItems()
|
|
420
|
+
const isTool = toolItems2.some(t => t.toolName === nameToRemove)
|
|
421
|
+
const isSkill = skillItems2.some(s => s.skillName === nameToRemove)
|
|
422
|
+
|
|
423
|
+
if (!isTool && !isSkill) {
|
|
424
|
+
console.error(chalk.red(` ✗ Unknown tool or skill: "${nameToRemove}"`))
|
|
425
|
+
process.exit(1)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (isTool) {
|
|
429
|
+
const { removed, failed } = await uninstallTool(nameToRemove, { yes: yesFlag })
|
|
430
|
+
if (removed.length > 0) console.error(chalk.green.bold(`✔ ${nameToRemove} uninstalled`))
|
|
431
|
+
process.exit(failed.length ? 1 : 0)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Skill uninstall
|
|
435
|
+
const scope = scopeArg2
|
|
436
|
+
const selectedTargets = targetArg2
|
|
437
|
+
? [targetArg2]
|
|
438
|
+
: ['claude', 'cursor', 'codex', 'openclaw', 'hermes']
|
|
439
|
+
const targets = resolveTargets(selectedTargets, scope)
|
|
440
|
+
|
|
441
|
+
let anyRemoved = false
|
|
442
|
+
let anyFailed = false
|
|
443
|
+
for (const { dir } of targets) {
|
|
444
|
+
const { removed, failed } = await uninstallSkill(nameToRemove, dir)
|
|
445
|
+
if (removed.length) anyRemoved = true
|
|
446
|
+
if (failed.length) anyFailed = true
|
|
447
|
+
}
|
|
448
|
+
if (anyRemoved) console.error(chalk.green.bold(`✔ ${nameToRemove} uninstalled`))
|
|
449
|
+
process.exit(anyFailed ? 1 : 0)
|
|
450
|
+
}
|
|
451
|
+
|
|
391
452
|
// ── Hooks ─────────────────────────────────────────────────────────────────────
|
|
392
453
|
if (subcommand === 'hooks') {
|
|
393
454
|
const hooksSubcmd = args[1]
|
|
@@ -396,10 +457,12 @@ if (subcommand === 'hooks') {
|
|
|
396
457
|
const hookNameIdx = hookArgs.indexOf('--name')
|
|
397
458
|
const hookScopeIdx = hookArgs.indexOf('--scope')
|
|
398
459
|
const hookProjectIdx = hookArgs.indexOf('--project')
|
|
460
|
+
const hookTargetIdx = hookArgs.indexOf('--target')
|
|
399
461
|
const hookForce = hookArgs.includes('--force')
|
|
400
462
|
const hookNameArg = hookNameIdx !== -1 ? hookArgs[hookNameIdx + 1] : undefined
|
|
401
463
|
const hookScopeArg = hookScopeIdx !== -1 ? hookArgs[hookScopeIdx + 1] : 'user'
|
|
402
464
|
const hookProjectArg = hookProjectIdx !== -1 ? hookArgs[hookProjectIdx + 1] : process.cwd()
|
|
465
|
+
const hookTargetArg = hookTargetIdx !== -1 ? hookArgs[hookTargetIdx + 1] : 'claude'
|
|
403
466
|
|
|
404
467
|
// Validate scope for install/uninstall commands
|
|
405
468
|
if (!['user', 'project'].includes(hookScopeArg) && ['install', 'uninstall'].includes(hooksSubcmd)) {
|
|
@@ -414,7 +477,16 @@ if (subcommand === 'hooks') {
|
|
|
414
477
|
const out = hookItems.map(h => {
|
|
415
478
|
const inst = checkHookInstalled(h.name)
|
|
416
479
|
const ver = resolveHookDisplayVersion(inst, h.version)
|
|
417
|
-
return {
|
|
480
|
+
return {
|
|
481
|
+
name: h.name,
|
|
482
|
+
version: ver,
|
|
483
|
+
description: h.description,
|
|
484
|
+
event: h.event,
|
|
485
|
+
user: inst.user,
|
|
486
|
+
project: inst.project,
|
|
487
|
+
claude: inst.claude,
|
|
488
|
+
codex: inst.codex,
|
|
489
|
+
}
|
|
418
490
|
})
|
|
419
491
|
console.log(JSON.stringify({ hooks: out }, null, 2))
|
|
420
492
|
process.exit(0)
|
|
@@ -427,15 +499,22 @@ if (subcommand === 'hooks') {
|
|
|
427
499
|
const nameWidth = Math.max(...hookItems.map(h => h.name.length), 4)
|
|
428
500
|
const verWidth = 7
|
|
429
501
|
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))
|
|
502
|
+
console.log(' ' + chalk.bold('NAME'.padEnd(nameWidth)) + ' ' + chalk.bold('VER'.padEnd(verWidth)) + ' U P CX ' + chalk.bold('DESCRIPTION'))
|
|
503
|
+
console.log(' ' + '─'.repeat(nameWidth) + ' ' + '─'.repeat(verWidth) + ' ─ ─ ── ' + '─'.repeat(20))
|
|
432
504
|
for (const h of hookItems) {
|
|
433
505
|
const inst = checkHookInstalled(h.name)
|
|
434
506
|
const ver = resolveHookDisplayVersion(inst, h.version)
|
|
435
|
-
console.log(
|
|
507
|
+
console.log(
|
|
508
|
+
' ' + h.name.padEnd(nameWidth) +
|
|
509
|
+
' ' + chalk.dim(ver.padEnd(verWidth)) +
|
|
510
|
+
' ' + hookIcon(inst.user.status) +
|
|
511
|
+
' ' + hookIcon(inst.project.status) +
|
|
512
|
+
' ' + hookIcon(inst.codex.user.status) +
|
|
513
|
+
' ' + h.description
|
|
514
|
+
)
|
|
436
515
|
}
|
|
437
516
|
console.log('')
|
|
438
|
-
console.log(chalk.dim(` U=user P=project ${chalk.green('✓')}=installed ${chalk.yellow('~')}=partial ${chalk.dim('—')}=none`))
|
|
517
|
+
console.log(chalk.dim(` U=claude-user P=claude-project CX=codex-user ${chalk.green('✓')}=installed ${chalk.yellow('~')}=partial ${chalk.dim('—')}=none`))
|
|
439
518
|
console.log('')
|
|
440
519
|
process.exit(0)
|
|
441
520
|
}
|
|
@@ -467,7 +546,7 @@ if (subcommand === 'hooks') {
|
|
|
467
546
|
toInstall = selected
|
|
468
547
|
}
|
|
469
548
|
|
|
470
|
-
const { installed, skipped, failed } = await
|
|
549
|
+
const { installed, skipped, failed } = await installHooksForTarget(toInstall, hookTargetArg, hookScopeArg, hookProjectArg, hookForce)
|
|
471
550
|
|
|
472
551
|
if (hookJsonFlag) {
|
|
473
552
|
console.log(JSON.stringify({ installed, skipped, failed }, null, 2))
|
|
@@ -534,6 +613,7 @@ function fzfSelect() {
|
|
|
534
613
|
requireFzf()
|
|
535
614
|
const skillItems = getAllSkillItems()
|
|
536
615
|
const toolItems = getAllToolItems()
|
|
616
|
+
const hookItems = getAllHookItems()
|
|
537
617
|
const previewPath = path.join(__dirname, 'preview.mjs')
|
|
538
618
|
|
|
539
619
|
// 构建 fzf 输入:每行 "NAME\tVERSION\tBUNDLE\tKIND\tSRCPATH"
|
|
@@ -543,6 +623,7 @@ function fzfSelect() {
|
|
|
543
623
|
return `${s.skillName}\t${s.version ?? '—'}\t${bundle}\tskill\t${s.srcPath}`
|
|
544
624
|
}),
|
|
545
625
|
...toolItems.map(t => `${t.toolName}\t${t.version ?? '—'}\tshell-tool\ttool\t${t.srcPath}`),
|
|
626
|
+
...hookItems.map(h => `${h.name}\t${h.version ?? '—'}\thook\thook\t${h.srcPath}`),
|
|
546
627
|
]
|
|
547
628
|
|
|
548
629
|
const nameWidth = Math.max(...lines.map(l => l.split('\t')[0].length))
|
|
@@ -565,6 +646,12 @@ function fzfSelect() {
|
|
|
565
646
|
pIcon = colorIcon(scopeSummary(installed.project))
|
|
566
647
|
} else if (kind === 'tool') {
|
|
567
648
|
uIcon = colorIcon(checkToolInstalled(name, srcPath).status)
|
|
649
|
+
} else if (kind === 'hook') {
|
|
650
|
+
const inst = checkHookInstalled(name)
|
|
651
|
+
uIcon = colorIcon(inst.user.status === 'installed' ? 'up-to-date'
|
|
652
|
+
: inst.user.status === 'partial' ? 'update' : '')
|
|
653
|
+
pIcon = colorIcon(inst.project.status === 'installed' ? 'up-to-date'
|
|
654
|
+
: inst.project.status === 'partial' ? 'update' : '')
|
|
568
655
|
}
|
|
569
656
|
return `${name.padEnd(nameWidth)} ${ver.padEnd(versionWidth)} U:${uIcon} P:${pIcon} ${bundle}`
|
|
570
657
|
})
|
|
@@ -598,12 +685,13 @@ function fzfSelect() {
|
|
|
598
685
|
const parts = line.split('\t')
|
|
599
686
|
const [, name, ver, bundle, kind, srcPath] = parts
|
|
600
687
|
if (kind === 'skill') return { kind: 'skill', skillName: name, srcPath, version: ver }
|
|
688
|
+
if (kind === 'hook') return { kind: 'hook', name, srcPath, version: ver }
|
|
601
689
|
return { kind: 'tool', toolName: name, srcPath, version: ver }
|
|
602
690
|
})
|
|
603
691
|
}
|
|
604
692
|
|
|
605
693
|
// ── Print install summary (shared between interactive loop and non-interactive) ──
|
|
606
|
-
function printSummary(skillSummary, toolSummary) {
|
|
694
|
+
function printSummary(skillSummary, toolSummary, hookSummary = null) {
|
|
607
695
|
if (skillSummary !== null) {
|
|
608
696
|
const anyInstalled = Object.values(skillSummary).some(r => r.installed.length > 0)
|
|
609
697
|
if (!anyInstalled) {
|
|
@@ -650,6 +738,26 @@ function printSummary(skillSummary, toolSummary) {
|
|
|
650
738
|
}
|
|
651
739
|
}
|
|
652
740
|
}
|
|
741
|
+
if (hookSummary !== null) {
|
|
742
|
+
const anyInstalled = Object.values(hookSummary).some(r => r.installed.length > 0)
|
|
743
|
+
if (!anyInstalled) {
|
|
744
|
+
console.log(chalk.dim(' · No hooks installed'))
|
|
745
|
+
} else {
|
|
746
|
+
console.log(chalk.green.bold('✔ Hooks installed:'))
|
|
747
|
+
for (const [target, { installed }] of Object.entries(hookSummary)) {
|
|
748
|
+
if (installed.length > 0)
|
|
749
|
+
console.log(` ${chalk.bold(target)} ← ${installed.join(', ')}`)
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
for (const { skipped } of Object.values(hookSummary)) {
|
|
753
|
+
for (const s of skipped) {
|
|
754
|
+
const detail = s.reason === 'outdated'
|
|
755
|
+
? `outdated ${s.installed} → ${s.available}, use --force`
|
|
756
|
+
: s.reason
|
|
757
|
+
console.log(chalk.dim(` · ${s.name} skipped (${detail})`))
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
653
761
|
}
|
|
654
762
|
|
|
655
763
|
try {
|
|
@@ -671,7 +779,14 @@ try {
|
|
|
671
779
|
}
|
|
672
780
|
|
|
673
781
|
while (true) {
|
|
674
|
-
|
|
782
|
+
// ── Test shortcut: HSKILL_TEST_ACTION bypasses fzf entirely ───────────
|
|
783
|
+
let selected
|
|
784
|
+
if (process.env.HSKILL_TEST_ACTION && process.env.HSKILL_TEST_TOOL) {
|
|
785
|
+
const testTool = getAllToolItems().find(t => t.toolName === process.env.HSKILL_TEST_TOOL)
|
|
786
|
+
selected = testTool ? [{ kind: 'tool', ...testTool }] : []
|
|
787
|
+
} else {
|
|
788
|
+
selected = fzfSelect()
|
|
789
|
+
}
|
|
675
790
|
|
|
676
791
|
if (!selected.length) {
|
|
677
792
|
console.log(chalk.dim(' · Nothing selected, exiting'))
|
|
@@ -679,76 +794,222 @@ try {
|
|
|
679
794
|
}
|
|
680
795
|
|
|
681
796
|
const toolItems = selected.filter(s => s.kind === 'tool')
|
|
797
|
+
const hookItems = selected.filter(s => s.kind === 'hook')
|
|
682
798
|
const seen = new Set()
|
|
683
799
|
const skillItems = selected.filter(s => s.kind === 'skill').filter(s => {
|
|
684
800
|
if (seen.has(s.skillName)) return false
|
|
685
801
|
seen.add(s.skillName); return true
|
|
686
802
|
})
|
|
687
803
|
|
|
688
|
-
if (!skillItems.length && !toolItems.length) continue
|
|
804
|
+
if (!skillItems.length && !toolItems.length && !hookItems.length) continue
|
|
689
805
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
806
|
+
// ── Action selection (install / uninstall) ─────────────────────────────
|
|
807
|
+
let action = 'install'
|
|
808
|
+
if (process.env.HSKILL_TEST_ACTION) {
|
|
809
|
+
action = process.env.HSKILL_TEST_ACTION
|
|
810
|
+
} else {
|
|
811
|
+
const actionResult = spawnSync('fzf', [
|
|
694
812
|
'--prompt= › ',
|
|
695
|
-
'--header=
|
|
813
|
+
'--header= Action · enter 确认 · esc 取消',
|
|
696
814
|
'--layout=reverse',
|
|
697
815
|
'--border=rounded',
|
|
698
816
|
'--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
|
|
699
817
|
], {
|
|
700
|
-
input: `
|
|
818
|
+
input: `install 安装 / 重新安装\nuninstall 卸载并清理文件`,
|
|
701
819
|
encoding: 'utf8',
|
|
702
820
|
stdio: ['pipe', 'pipe', 'inherit'],
|
|
703
821
|
})
|
|
704
|
-
if (!
|
|
822
|
+
if (!actionResult.stdout.trim()) {
|
|
705
823
|
console.log(chalk.dim(' · Cancelled'))
|
|
706
824
|
break
|
|
707
825
|
}
|
|
708
|
-
|
|
826
|
+
action = actionResult.stdout.trim().startsWith('uninstall') ? 'uninstall' : 'install'
|
|
827
|
+
}
|
|
709
828
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
'
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
829
|
+
if (action === 'install') {
|
|
830
|
+
let skillSummary = null
|
|
831
|
+
if (skillItems.length > 0) {
|
|
832
|
+
// Scope selection
|
|
833
|
+
const scopeResult = spawnSync('fzf', [
|
|
834
|
+
'--prompt= › ',
|
|
835
|
+
'--header= Scope · enter 确认 · esc 取消',
|
|
836
|
+
'--layout=reverse',
|
|
837
|
+
'--border=rounded',
|
|
838
|
+
'--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
|
|
839
|
+
], {
|
|
840
|
+
input: `user — ~/.claude/skills/ (所有项目共享)\nproject — .claude/skills/ (仅当前项目)`,
|
|
841
|
+
encoding: 'utf8',
|
|
842
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
843
|
+
})
|
|
844
|
+
if (!scopeResult.stdout.trim()) {
|
|
845
|
+
console.log(chalk.dim(' · Cancelled'))
|
|
846
|
+
break
|
|
847
|
+
}
|
|
848
|
+
const scope = scopeResult.stdout.trim().startsWith('project') ? 'project' : 'user'
|
|
849
|
+
|
|
850
|
+
// Target selection
|
|
851
|
+
const targetChoices = buildTargetChoices(scope)
|
|
852
|
+
const targetInput = targetChoices.map(c => c.name).join('\n') + '\nall — all tools'
|
|
853
|
+
const targetResult = spawnSync('fzf', [
|
|
854
|
+
'--multi',
|
|
855
|
+
'--prompt= › ',
|
|
856
|
+
'--header= Install to · tab 多选 · enter 确认 · esc 取消',
|
|
857
|
+
'--layout=reverse',
|
|
858
|
+
'--border=rounded',
|
|
859
|
+
'--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
|
|
860
|
+
], {
|
|
861
|
+
input: targetInput,
|
|
862
|
+
encoding: 'utf8',
|
|
863
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
864
|
+
})
|
|
865
|
+
if (!targetResult.stdout.trim()) {
|
|
866
|
+
console.log(chalk.dim(' · Cancelled'))
|
|
867
|
+
break
|
|
868
|
+
}
|
|
869
|
+
const selectedTargets = targetResult.stdout.trim().split('\n')
|
|
870
|
+
.map(l => l.trim().split(/\s+/)[0])
|
|
871
|
+
|
|
872
|
+
if (selectedTargets.length > 0) {
|
|
873
|
+
const targets = resolveTargets(selectedTargets, scope)
|
|
874
|
+
console.log('')
|
|
875
|
+
skillSummary = await installSkills(skillItems, targets, forceFlag)
|
|
876
|
+
console.log('')
|
|
877
|
+
}
|
|
728
878
|
}
|
|
729
|
-
const selectedTargets = targetResult.stdout.trim().split('\n')
|
|
730
|
-
.map(l => l.trim().split(/\s+/)[0])
|
|
731
879
|
|
|
732
|
-
|
|
733
|
-
|
|
880
|
+
let toolSummary = null
|
|
881
|
+
if (toolItems.length > 0) {
|
|
734
882
|
console.log('')
|
|
735
|
-
|
|
883
|
+
toolSummary = await installTools(
|
|
884
|
+
toolItems.map(t => ({ toolName: t.toolName, srcPath: t.srcPath })),
|
|
885
|
+
TARGETS.shell,
|
|
886
|
+
forceFlag,
|
|
887
|
+
)
|
|
736
888
|
console.log('')
|
|
737
889
|
}
|
|
738
|
-
}
|
|
739
890
|
|
|
740
|
-
|
|
741
|
-
|
|
891
|
+
// ── Hook install ────────────────────────────────────────────────────────
|
|
892
|
+
let hookSummary = null
|
|
893
|
+
if (hookItems.length > 0) {
|
|
894
|
+
// Scope selection
|
|
895
|
+
const hookScopeResult = spawnSync('fzf', [
|
|
896
|
+
'--prompt= › ',
|
|
897
|
+
'--header= Hook scope · enter 确认 · esc 取消',
|
|
898
|
+
'--layout=reverse',
|
|
899
|
+
'--border=rounded',
|
|
900
|
+
'--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
|
|
901
|
+
], {
|
|
902
|
+
input: `user — ~/.{claude,codex}/hooks/ (所有项目共享)\nproject — .{claude,codex}/hooks/ (仅当前项目)`,
|
|
903
|
+
encoding: 'utf8',
|
|
904
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
905
|
+
})
|
|
906
|
+
if (!hookScopeResult.stdout.trim()) {
|
|
907
|
+
console.log(chalk.dim(' · Cancelled'))
|
|
908
|
+
break
|
|
909
|
+
}
|
|
910
|
+
const hookScope = hookScopeResult.stdout.trim().startsWith('project') ? 'project' : 'user'
|
|
911
|
+
|
|
912
|
+
// Target selection (claude / codex / all)
|
|
913
|
+
const hookTargetResult = spawnSync('fzf', [
|
|
914
|
+
'--multi',
|
|
915
|
+
'--prompt= › ',
|
|
916
|
+
'--header= Install hook to · tab 多选 · enter 确认 · esc 取消',
|
|
917
|
+
'--layout=reverse',
|
|
918
|
+
'--border=rounded',
|
|
919
|
+
'--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
|
|
920
|
+
], {
|
|
921
|
+
input: `claude — ~/.claude/hooks/\ncodex — ~/.codex/hooks/\nall — claude + codex`,
|
|
922
|
+
encoding: 'utf8',
|
|
923
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
924
|
+
})
|
|
925
|
+
if (!hookTargetResult.stdout.trim()) {
|
|
926
|
+
console.log(chalk.dim(' · Cancelled'))
|
|
927
|
+
break
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const selectedHookTargets = hookTargetResult.stdout.trim().split('\n')
|
|
931
|
+
.map(l => l.trim().split(/\s+/)[0])
|
|
932
|
+
const resolvedHookTargets = selectedHookTargets.includes('all')
|
|
933
|
+
? ['claude', 'codex']
|
|
934
|
+
: selectedHookTargets.filter(t => ['claude', 'codex'].includes(t))
|
|
935
|
+
|
|
936
|
+
hookSummary = {}
|
|
937
|
+
console.log('')
|
|
938
|
+
for (const target of resolvedHookTargets) {
|
|
939
|
+
const result = await installHooksForTarget(hookItems, target, hookScope, process.cwd(), forceFlag)
|
|
940
|
+
hookSummary[target] = result
|
|
941
|
+
}
|
|
942
|
+
console.log('')
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
printSummary(skillSummary, toolSummary, hookSummary)
|
|
946
|
+
} else {
|
|
947
|
+
// ── Uninstall ───────────────────────────────────────────────────────
|
|
948
|
+
const yesFlag2 = !!process.env.HSKILL_TEST_YES
|
|
742
949
|
console.log('')
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
)
|
|
950
|
+
|
|
951
|
+
// Re-filter after possible HSKILL_TEST_TOOL injection
|
|
952
|
+
const uToolItems = selected.filter(s => s.kind === 'tool')
|
|
953
|
+
const uSkillItems = selected.filter(s => s.kind === 'skill')
|
|
954
|
+
const uHookItems = selected.filter(s => s.kind === 'hook')
|
|
955
|
+
|
|
956
|
+
for (const item of uToolItems) {
|
|
957
|
+
const { removed, failed } = await uninstallTool(item.toolName, { yes: yesFlag2 })
|
|
958
|
+
if (removed.length) console.error(chalk.green.bold(`✔ ${item.toolName} uninstalled`))
|
|
959
|
+
if (failed.length) console.error(chalk.red(` ✗ ${item.toolName}: some files could not be removed`))
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
for (const item of uSkillItems) {
|
|
963
|
+
const scopeRes = spawnSync('fzf', [
|
|
964
|
+
'--prompt= › ',
|
|
965
|
+
'--header= Uninstall from scope · enter 确认 · esc 取消',
|
|
966
|
+
'--layout=reverse', '--border=rounded',
|
|
967
|
+
'--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
|
|
968
|
+
], {
|
|
969
|
+
input: `user — ~/.claude/skills/\nproject — .claude/skills/`,
|
|
970
|
+
encoding: 'utf8', stdio: ['pipe', 'pipe', 'inherit'],
|
|
971
|
+
})
|
|
972
|
+
if (!scopeRes.stdout.trim()) { console.log(chalk.dim(' · Cancelled')); break }
|
|
973
|
+
const scope2 = scopeRes.stdout.trim().startsWith('project') ? 'project' : 'user'
|
|
974
|
+
|
|
975
|
+
const targetChoices2 = buildTargetChoices(scope2)
|
|
976
|
+
const targetRes = spawnSync('fzf', [
|
|
977
|
+
'--multi', '--prompt= › ',
|
|
978
|
+
'--header= Uninstall from · tab 多选 · enter 确认',
|
|
979
|
+
'--layout=reverse', '--border=rounded',
|
|
980
|
+
'--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
|
|
981
|
+
], {
|
|
982
|
+
input: targetChoices2.map(c => c.name).join('\n') + '\nall — all tools',
|
|
983
|
+
encoding: 'utf8', stdio: ['pipe', 'pipe', 'inherit'],
|
|
984
|
+
})
|
|
985
|
+
if (!targetRes.stdout.trim()) { console.log(chalk.dim(' · Cancelled')); break }
|
|
986
|
+
const selTargets2 = targetRes.stdout.trim().split('\n').map(l => l.trim().split(/\s+/)[0])
|
|
987
|
+
const targets2 = resolveTargets(selTargets2, scope2)
|
|
988
|
+
for (const { dir } of targets2) {
|
|
989
|
+
await uninstallSkill(item.skillName, dir)
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
for (const item of uHookItems) {
|
|
994
|
+
const hookScopeRes = spawnSync('fzf', [
|
|
995
|
+
'--prompt= › ',
|
|
996
|
+
'--header= Hook scope · enter 确认',
|
|
997
|
+
'--layout=reverse', '--border=rounded',
|
|
998
|
+
'--color=header:italic:dim,prompt:cyan,pointer:cyan,hl:cyan,hl+:cyan:bold',
|
|
999
|
+
], {
|
|
1000
|
+
input: `user — ~/.claude/hooks/\nproject — .claude/hooks/`,
|
|
1001
|
+
encoding: 'utf8', stdio: ['pipe', 'pipe', 'inherit'],
|
|
1002
|
+
})
|
|
1003
|
+
if (!hookScopeRes.stdout.trim()) { console.log(chalk.dim(' · Cancelled')); break }
|
|
1004
|
+
const hookScope2 = hookScopeRes.stdout.trim().startsWith('project') ? 'project' : 'user'
|
|
1005
|
+
await uninstallHook(item.name, hookScope2, process.cwd())
|
|
1006
|
+
}
|
|
1007
|
+
|
|
748
1008
|
console.log('')
|
|
749
1009
|
}
|
|
750
1010
|
|
|
751
|
-
|
|
1011
|
+
// In test mode with HSKILL_TEST_ACTION, run once and exit
|
|
1012
|
+
if (process.env.HSKILL_TEST_ACTION) break
|
|
752
1013
|
|
|
753
1014
|
// Loop back to skill selector automatically
|
|
754
1015
|
}
|
package/lib/bundles.js
CHANGED
|
@@ -193,9 +193,9 @@ export function checkHookInstalled(hookName) {
|
|
|
193
193
|
const home = os.homedir()
|
|
194
194
|
const cwd = process.cwd()
|
|
195
195
|
|
|
196
|
-
function
|
|
197
|
-
const scriptPath
|
|
198
|
-
const scriptExists
|
|
196
|
+
function checkClaudeScope(hooksDir, settingsPath) {
|
|
197
|
+
const scriptPath = path.join(hooksDir, `${hookName}.sh`)
|
|
198
|
+
const scriptExists = fs.existsSync(scriptPath)
|
|
199
199
|
const installedVersion = scriptExists ? readHookVersion(scriptPath) : '—'
|
|
200
200
|
let registered = false
|
|
201
201
|
try {
|
|
@@ -208,23 +208,55 @@ export function checkHookInstalled(hookName) {
|
|
|
208
208
|
)
|
|
209
209
|
)
|
|
210
210
|
} catch { /* settings.json 不存在或解析失败 */ }
|
|
211
|
+
const status = scriptExists && registered ? 'installed'
|
|
212
|
+
: scriptExists || registered ? 'partial'
|
|
213
|
+
: 'none'
|
|
214
|
+
return { status, version: installedVersion }
|
|
215
|
+
}
|
|
211
216
|
|
|
217
|
+
function checkCodexScope(hooksDir, hooksJsonPath) {
|
|
218
|
+
const scriptPath = path.join(hooksDir, `${hookName}.sh`)
|
|
219
|
+
const scriptExists = fs.existsSync(scriptPath)
|
|
220
|
+
const installedVersion = scriptExists ? readHookVersion(scriptPath) : '—'
|
|
221
|
+
let registered = false
|
|
222
|
+
try {
|
|
223
|
+
const data = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf-8'))
|
|
224
|
+
registered = Object.values(data.hooks ?? {}).some(entries =>
|
|
225
|
+
Array.isArray(entries) && entries.some(e =>
|
|
226
|
+
Array.isArray(e.hooks) && e.hooks.some(h =>
|
|
227
|
+
typeof h.command === 'string' && h.command.includes(`${hookName}.sh`)
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
} catch { /* hooks.json 不存在或解析失败 */ }
|
|
212
232
|
const status = scriptExists && registered ? 'installed'
|
|
213
233
|
: scriptExists || registered ? 'partial'
|
|
214
234
|
: 'none'
|
|
215
235
|
return { status, version: installedVersion }
|
|
216
236
|
}
|
|
217
237
|
|
|
218
|
-
const
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
const
|
|
238
|
+
const userClaudeHooks = path.join(home, '.claude', 'hooks')
|
|
239
|
+
const userClaudeSettings = path.join(home, '.claude', 'settings.json')
|
|
240
|
+
const projClaudeHooks = path.join(cwd, '.claude', 'hooks')
|
|
241
|
+
const projClaudeSettings = path.join(cwd, '.claude', 'settings.json')
|
|
242
|
+
|
|
243
|
+
const userCodexHooks = path.join(home, '.codex', 'hooks')
|
|
244
|
+
const userCodexHooksJson = path.join(home, '.codex', 'hooks.json')
|
|
245
|
+
const projCodexHooks = path.join(cwd, '.codex', 'hooks')
|
|
246
|
+
const projCodexHooksJson = path.join(cwd, '.codex', 'hooks.json')
|
|
247
|
+
|
|
248
|
+
const claudeUser = checkClaudeScope(userClaudeHooks, userClaudeSettings)
|
|
249
|
+
const claudeProject = cwd === home ? { status: 'none', version: '—' } : checkClaudeScope(projClaudeHooks, projClaudeSettings)
|
|
250
|
+
const codexUser = checkCodexScope(userCodexHooks, userCodexHooksJson)
|
|
251
|
+
const codexProject = cwd === home ? { status: 'none', version: '—' } : checkCodexScope(projCodexHooks, projCodexHooksJson)
|
|
222
252
|
|
|
223
253
|
return {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
254
|
+
// Legacy flat fields (backward compat — used by hooks list text output)
|
|
255
|
+
user: claudeUser,
|
|
256
|
+
project: claudeProject,
|
|
257
|
+
// Per-target
|
|
258
|
+
claude: { user: claudeUser, project: claudeProject },
|
|
259
|
+
codex: { user: codexUser, project: codexProject },
|
|
228
260
|
}
|
|
229
261
|
}
|
|
230
262
|
|