@wang121ye/skillmanager 0.0.5 → 0.0.7
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/README.md +18 -3
- package/package.json +3 -2
- package/src/cli.js +2 -2
- package/src/commands/bootstrap.js +14 -40
- package/src/commands/uninstall.js +27 -9
- package/src/commands/update.js +32 -23
- package/src/lib/cli-select.js +200 -0
package/README.md
CHANGED
|
@@ -71,6 +71,15 @@ npx @wang121ye/skillmanager install
|
|
|
71
71
|
skillmanager install
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
### 命令行交互式选择(类似 openskills)
|
|
75
|
+
|
|
76
|
+
默认 `skillmanager install` 会进入交互式选择(按来源分组)。支持:
|
|
77
|
+
- 空格选择/取消
|
|
78
|
+
- a 全选,i 反选
|
|
79
|
+
- h 顶部,e 底部
|
|
80
|
+
- [ / ] 切换分组
|
|
81
|
+
- Esc 退出,Enter 确认
|
|
82
|
+
|
|
74
83
|
### 2) 安装时启用 Web UI 选择(默认全选,可批量全选/全不选/反选/搜索)
|
|
75
84
|
|
|
76
85
|
```bash
|
|
@@ -221,15 +230,15 @@ skillmanager source remove superpowers
|
|
|
221
230
|
|
|
222
231
|
### 默认更新(推荐)
|
|
223
232
|
|
|
224
|
-
|
|
233
|
+
默认会优先按 profile 选择集更新(显式 `--profile` 优先,否则使用默认 profile):
|
|
225
234
|
|
|
226
235
|
```bash
|
|
227
236
|
skillmanager update
|
|
228
237
|
```
|
|
229
238
|
|
|
230
|
-
|
|
239
|
+
如果目标 profile 不存在或没有有效选择集,会自动回退到 `openskills update`。
|
|
231
240
|
|
|
232
|
-
|
|
241
|
+
### 指定 profile 更新(子集安装最稳)
|
|
233
242
|
|
|
234
243
|
```bash
|
|
235
244
|
skillmanager update --profile laptop
|
|
@@ -242,6 +251,12 @@ skillmanager webui --profile laptop
|
|
|
242
251
|
skillmanager update --profile laptop
|
|
243
252
|
```
|
|
244
253
|
|
|
254
|
+
### 强制使用 openskills 原生更新链路
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
skillmanager update --openskills
|
|
258
|
+
```
|
|
259
|
+
|
|
245
260
|
## 卸载 skills
|
|
246
261
|
|
|
247
262
|
### 用 Web UI 勾选要卸载的已安装 skills(推荐)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wang121ye/skillmanager",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Cross-platform agent skill manager (bootstrap + optional Web UI selection) built on top of openskills.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"skillmanager",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"gray-matter": "^4.0.3",
|
|
46
46
|
"openskills": "^1.5.0",
|
|
47
47
|
"simple-git": "^3.27.0",
|
|
48
|
-
"undici": "^6.23.0"
|
|
48
|
+
"undici": "^6.23.0",
|
|
49
|
+
"yoctocolors-cjs": "^2.1.3"
|
|
49
50
|
}
|
|
50
51
|
}
|
package/src/cli.js
CHANGED
|
@@ -59,12 +59,13 @@ async function main() {
|
|
|
59
59
|
|
|
60
60
|
program
|
|
61
61
|
.command('update')
|
|
62
|
-
.description('更新 skills,并同步生成/更新 AGENTS.md
|
|
62
|
+
.description('更新 skills,并同步生成/更新 AGENTS.md(默认按 profile 更新)。')
|
|
63
63
|
.option('--global', '安装到全局(默认:当前项目)', false)
|
|
64
64
|
.option('--universal', '使用通用目录 .agent/skills(默认:.claude/skills)', false)
|
|
65
65
|
.option('--output <path>', 'sync 输出文件(默认:AGENTS.md)')
|
|
66
66
|
.option('--no-sync', '跳过 openskills sync', false)
|
|
67
67
|
.option('--profile <name>', '按 profile 选择集更新(会重新安装选中的 skills)')
|
|
68
|
+
.option('--openskills', '强制走 openskills update(忽略 profile 选择集)', false)
|
|
68
69
|
.option('--concurrency <n>', '选择模式下的并发扫描数(默认:3)', '3')
|
|
69
70
|
.option('--force-refresh', '强制刷新来源仓库缓存(重新拉取)', false)
|
|
70
71
|
.action(async (opts) => {
|
|
@@ -179,4 +180,3 @@ main().catch((err) => {
|
|
|
179
180
|
console.error(err?.stack || String(err));
|
|
180
181
|
process.exitCode = 1;
|
|
181
182
|
});
|
|
182
|
-
|
|
@@ -3,7 +3,7 @@ const { ensureDir } = require('../lib/fs');
|
|
|
3
3
|
const { loadSourcesManifest } = require('../lib/manifest');
|
|
4
4
|
const { ensureRepo } = require('../lib/git');
|
|
5
5
|
const { scanSkillsInRepo } = require('../lib/scan');
|
|
6
|
-
const { loadProfile } = require('../lib/profiles');
|
|
6
|
+
const { loadProfile, saveProfile } = require('../lib/profiles');
|
|
7
7
|
const { mapWithConcurrency } = require('../lib/concurrency');
|
|
8
8
|
const { getEffectiveDefaultProfile } = require('../lib/config');
|
|
9
9
|
const path = require('path');
|
|
@@ -11,6 +11,7 @@ const os = require('os');
|
|
|
11
11
|
const { installSourceRef, syncAgents } = require('../lib/openskills');
|
|
12
12
|
const { installFromLocalSkillDir } = require('../lib/local-install');
|
|
13
13
|
const { warnPrereqs } = require('../lib/prereqs');
|
|
14
|
+
const { promptSkillSelection } = require('../lib/cli-select');
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
function uniq(arr) {
|
|
@@ -28,47 +29,9 @@ async function bootstrap(opts) {
|
|
|
28
29
|
|
|
29
30
|
const profileName = opts?.profile || (await getEffectiveDefaultProfile());
|
|
30
31
|
const existing = await loadProfile({ profilesDir: paths.profilesDir, profileName });
|
|
31
|
-
|
|
32
|
-
const wantsSelection = existing?.selectedSkillIds && Array.isArray(existing.selectedSkillIds);
|
|
33
32
|
const globalInstall = !!opts?.global;
|
|
34
33
|
const universal = !!opts?.universal;
|
|
35
34
|
|
|
36
|
-
if (!wantsSelection) {
|
|
37
|
-
// Default path: install everything from each enabled source via openskills directly.
|
|
38
|
-
// This avoids repo scanning and matches “bootstrap 安装所有 skills”的默认体验。
|
|
39
|
-
// eslint-disable-next-line no-console
|
|
40
|
-
console.log(`将从 ${enabledSources.length} 个来源安装(global=${globalInstall}, universal=${universal})…`);
|
|
41
|
-
|
|
42
|
-
if (opts?.dryRun) {
|
|
43
|
-
// eslint-disable-next-line no-console
|
|
44
|
-
console.log('\n--dry-run 已启用:将安装以下来源(不执行安装)');
|
|
45
|
-
for (const s of enabledSources) {
|
|
46
|
-
const ref = s.openskillsRef || s.repo;
|
|
47
|
-
// eslint-disable-next-line no-console
|
|
48
|
-
console.log(`- ${s.name || s.id} (${ref || 'missing-ref'})`);
|
|
49
|
-
}
|
|
50
|
-
// eslint-disable-next-line no-console
|
|
51
|
-
console.log('\n完成(dry-run)。');
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
for (const s of enabledSources) {
|
|
56
|
-
const ref = s.openskillsRef || s.repo;
|
|
57
|
-
if (!ref) continue;
|
|
58
|
-
// eslint-disable-next-line no-console
|
|
59
|
-
console.log(`\n==> Installing source: ${s.name || s.id} (${ref})`);
|
|
60
|
-
await installSourceRef({ ref, globalInstall, universal });
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (opts?.sync !== false) {
|
|
64
|
-
await syncAgents({ output: opts?.output, cwd: process.cwd() });
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// eslint-disable-next-line no-console
|
|
68
|
-
console.log('\n完成。');
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
35
|
// Selection path: clone repos + scan SKILL.md, then install selected skill dirs.
|
|
73
36
|
const concurrency = Number(opts?.concurrency || process.env.SKILLMANAGER_CONCURRENCY || 3);
|
|
74
37
|
// eslint-disable-next-line no-console
|
|
@@ -103,7 +66,18 @@ async function bootstrap(opts) {
|
|
|
103
66
|
? existing.selectedSkillIds
|
|
104
67
|
: allSkills.map((s) => s.id);
|
|
105
68
|
|
|
106
|
-
|
|
69
|
+
const chosen = await promptSkillSelection({
|
|
70
|
+
title: `skillmanager install · profile=${profileName}`,
|
|
71
|
+
skills: allSkills,
|
|
72
|
+
initialSelectedIds: selectedIds
|
|
73
|
+
});
|
|
74
|
+
if (chosen == null) {
|
|
75
|
+
// eslint-disable-next-line no-console
|
|
76
|
+
console.log('已取消(未执行安装)。');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
selectedIds = chosen;
|
|
80
|
+
await saveProfile({ profilesDir: paths.profilesDir, profileName, selectedSkillIds: selectedIds });
|
|
107
81
|
|
|
108
82
|
selectedIds = uniq(selectedIds).filter((id) => skillsById.has(id));
|
|
109
83
|
|
|
@@ -4,6 +4,7 @@ const fsp = require('fs/promises');
|
|
|
4
4
|
|
|
5
5
|
const { listInstalledSkills } = require('../lib/installed');
|
|
6
6
|
const { syncAgents } = require('../lib/openskills');
|
|
7
|
+
const { promptSkillSelection } = require('../lib/cli-select');
|
|
7
8
|
|
|
8
9
|
function resolveTargetDir({ globalInstall, universal }) {
|
|
9
10
|
const folder = universal ? '.agent/skills' : '.claude/skills';
|
|
@@ -18,10 +19,35 @@ async function uninstall(opts, skillNames) {
|
|
|
18
19
|
const installed = await listInstalledSkills(targetDir);
|
|
19
20
|
const installedByName = new Map(installed.map((s) => [s.name, s]));
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
if (installed.length === 0) {
|
|
23
|
+
// eslint-disable-next-line no-console
|
|
24
|
+
console.log(`未检测到可卸载的 skill。目标目录:${targetDir}`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
22
27
|
|
|
28
|
+
let toRemove = [];
|
|
23
29
|
if (opts?.all) {
|
|
24
30
|
toRemove = installed.map((s) => s.name);
|
|
31
|
+
} else {
|
|
32
|
+
const initialSelected = Array.isArray(skillNames) ? skillNames.filter(Boolean) : [];
|
|
33
|
+
const chosen = await promptSkillSelection({
|
|
34
|
+
title: 'skillmanager uninstall',
|
|
35
|
+
skills: installed.map((s) => ({
|
|
36
|
+
id: s.name,
|
|
37
|
+
sourceId: 'installed',
|
|
38
|
+
sourceName: 'Installed',
|
|
39
|
+
name: s.name,
|
|
40
|
+
description: s.description,
|
|
41
|
+
skillDir: s.skillDir
|
|
42
|
+
})),
|
|
43
|
+
initialSelectedIds: initialSelected
|
|
44
|
+
});
|
|
45
|
+
if (chosen == null) {
|
|
46
|
+
// eslint-disable-next-line no-console
|
|
47
|
+
console.log('已取消(未执行卸载)。');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
toRemove = chosen;
|
|
25
51
|
}
|
|
26
52
|
|
|
27
53
|
toRemove = Array.from(new Set(toRemove)).filter((n) => installedByName.has(n));
|
|
@@ -29,14 +55,6 @@ async function uninstall(opts, skillNames) {
|
|
|
29
55
|
if (toRemove.length === 0) {
|
|
30
56
|
// eslint-disable-next-line no-console
|
|
31
57
|
console.log(`未选择任何可卸载的 skill。目标目录:${targetDir}`);
|
|
32
|
-
// eslint-disable-next-line no-console
|
|
33
|
-
console.log('用法:');
|
|
34
|
-
// eslint-disable-next-line no-console
|
|
35
|
-
console.log(' - skillmanager webui --mode uninstall');
|
|
36
|
-
// eslint-disable-next-line no-console
|
|
37
|
-
console.log(' - skillmanager uninstall <skill1> <skill2>');
|
|
38
|
-
// eslint-disable-next-line no-console
|
|
39
|
-
console.log(' - skillmanager uninstall --all');
|
|
40
58
|
return;
|
|
41
59
|
}
|
|
42
60
|
|
package/src/commands/update.js
CHANGED
|
@@ -17,31 +17,46 @@ function uniq(arr) {
|
|
|
17
17
|
return Array.from(new Set(arr));
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
async function runFallbackOpenSkillsUpdate(opts) {
|
|
21
|
+
// eslint-disable-next-line no-console
|
|
22
|
+
console.log('正在执行 openskills update(更新所有已记录来源)…');
|
|
23
|
+
await runOpenSkills(['update']);
|
|
24
|
+
if (opts?.sync !== false) {
|
|
25
|
+
await syncAgents({ output: opts?.output, cwd: process.cwd() });
|
|
26
|
+
}
|
|
27
|
+
// eslint-disable-next-line no-console
|
|
28
|
+
console.log('\n完成。');
|
|
29
|
+
}
|
|
30
|
+
|
|
20
31
|
async function update(opts) {
|
|
21
32
|
await warnPrereqs({ needGit: true, needOpenSkills: true });
|
|
22
33
|
const globalInstall = !!opts?.global;
|
|
23
34
|
const universal = !!opts?.universal;
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
if (opts?.openskills) {
|
|
37
|
+
await runFallbackOpenSkillsUpdate(opts);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
27
40
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
// Default path: profile-based update (explicit profile > default profile).
|
|
42
|
+
const paths = getAppPaths();
|
|
43
|
+
await ensureDir(paths.profilesDir);
|
|
44
|
+
const effectiveProfileName = opts?.profile || (await getEffectiveDefaultProfile());
|
|
45
|
+
const existing = await loadProfile({ profilesDir: paths.profilesDir, profileName: effectiveProfileName });
|
|
46
|
+
const hasSelection = Array.isArray(existing?.selectedSkillIds);
|
|
47
|
+
|
|
48
|
+
if (!hasSelection) {
|
|
36
49
|
// eslint-disable-next-line no-console
|
|
37
|
-
console.
|
|
50
|
+
console.warn(
|
|
51
|
+
`未找到可用 profile 选择集:${effectiveProfileName},将回退到 openskills update。` +
|
|
52
|
+
`可先运行 skillmanager webui --profile ${effectiveProfileName} 保存选择集。`
|
|
53
|
+
);
|
|
54
|
+
await runFallbackOpenSkillsUpdate(opts);
|
|
38
55
|
return;
|
|
39
56
|
}
|
|
40
57
|
|
|
41
58
|
// Profile-based update: refresh repos cache and re-install selected skill dirs.
|
|
42
|
-
const paths = getAppPaths();
|
|
43
59
|
await ensureDir(paths.reposDir);
|
|
44
|
-
await ensureDir(paths.profilesDir);
|
|
45
60
|
|
|
46
61
|
const { sources } = await loadSourcesManifest();
|
|
47
62
|
const enabledSources = sources.filter((s) => s && s.enabled !== false);
|
|
@@ -73,21 +88,16 @@ async function update(opts) {
|
|
|
73
88
|
for (const sk of skills) skillsById.set(sk.id, sk);
|
|
74
89
|
}
|
|
75
90
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const effectiveProfileName = profileName || (await getEffectiveDefaultProfile());
|
|
79
|
-
const existing = await loadProfile({ profilesDir: paths.profilesDir, profileName: effectiveProfileName });
|
|
80
|
-
let selectedIds =
|
|
81
|
-
existing?.selectedSkillIds && Array.isArray(existing.selectedSkillIds)
|
|
82
|
-
? existing.selectedSkillIds
|
|
83
|
-
: allSkills.map((s) => s.id);
|
|
91
|
+
let selectedIds = existing.selectedSkillIds;
|
|
84
92
|
|
|
85
93
|
// 交互式选择已迁移到:skillmanager webui(mode=install),先保存 profile 再 update --profile
|
|
86
94
|
|
|
87
95
|
selectedIds = uniq(selectedIds).filter((id) => skillsById.has(id));
|
|
88
96
|
|
|
89
97
|
// eslint-disable-next-line no-console
|
|
90
|
-
console.log(
|
|
98
|
+
console.log(
|
|
99
|
+
`将按 profile=${effectiveProfileName} 更新/重装 ${selectedIds.length} 个 skills(global=${globalInstall}, universal=${universal})…`
|
|
100
|
+
);
|
|
91
101
|
|
|
92
102
|
for (const id of selectedIds) {
|
|
93
103
|
const skill = skillsById.get(id);
|
|
@@ -109,4 +119,3 @@ async function update(opts) {
|
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
module.exports = { update };
|
|
112
|
-
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createPrompt,
|
|
3
|
+
useState,
|
|
4
|
+
useKeypress,
|
|
5
|
+
usePagination,
|
|
6
|
+
usePrefix,
|
|
7
|
+
isUpKey,
|
|
8
|
+
isDownKey,
|
|
9
|
+
isSpaceKey,
|
|
10
|
+
isEnterKey
|
|
11
|
+
} = require('@inquirer/core');
|
|
12
|
+
const colors = require('yoctocolors-cjs');
|
|
13
|
+
|
|
14
|
+
function groupSkills(skills) {
|
|
15
|
+
const bySource = new Map();
|
|
16
|
+
for (const skill of skills) {
|
|
17
|
+
const key = skill.sourceName || skill.sourceId || 'unknown';
|
|
18
|
+
if (!bySource.has(key)) bySource.set(key, []);
|
|
19
|
+
bySource.get(key).push(skill);
|
|
20
|
+
}
|
|
21
|
+
for (const [key, list] of bySource.entries()) {
|
|
22
|
+
list.sort((a, b) => a.name.localeCompare(b.name));
|
|
23
|
+
bySource.set(key, list);
|
|
24
|
+
}
|
|
25
|
+
return Array.from(bySource.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildItems(skills) {
|
|
29
|
+
const grouped = groupSkills(skills);
|
|
30
|
+
const items = [];
|
|
31
|
+
let groupIndex = 0;
|
|
32
|
+
for (const [sourceName, list] of grouped) {
|
|
33
|
+
items.push({ type: 'separator', label: `-- ${sourceName} --`, groupIndex });
|
|
34
|
+
for (const skill of list) {
|
|
35
|
+
items.push({
|
|
36
|
+
type: 'choice',
|
|
37
|
+
id: skill.id,
|
|
38
|
+
name: skill.name,
|
|
39
|
+
description: skill.description || '',
|
|
40
|
+
groupIndex
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
groupIndex += 1;
|
|
44
|
+
}
|
|
45
|
+
return items;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getSelectableIndexes(items) {
|
|
49
|
+
const indexes = [];
|
|
50
|
+
for (let i = 0; i < items.length; i++) {
|
|
51
|
+
if (items[i].type === 'choice') indexes.push(i);
|
|
52
|
+
}
|
|
53
|
+
return indexes;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function findNextSelectable(items, startIndex, direction) {
|
|
57
|
+
const step = direction >= 0 ? 1 : -1;
|
|
58
|
+
for (let i = startIndex + step; i >= 0 && i < items.length; i += step) {
|
|
59
|
+
if (items[i].type === 'choice') return i;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function findGroupStart(items, groupIndex) {
|
|
65
|
+
for (let i = 0; i < items.length; i++) {
|
|
66
|
+
if (items[i].type === 'choice' && items[i].groupIndex === groupIndex) return i;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const promptSkillSelection = createPrompt((config, done) => {
|
|
72
|
+
const items = buildItems(config.skills || []);
|
|
73
|
+
const selectableIndexes = getSelectableIndexes(items);
|
|
74
|
+
const firstSelectable = selectableIndexes[0] ?? 0;
|
|
75
|
+
const lastSelectable = selectableIndexes[selectableIndexes.length - 1] ?? 0;
|
|
76
|
+
const [activeIndex, setActiveIndex] = useState(firstSelectable);
|
|
77
|
+
const [status, setStatus] = useState('idle');
|
|
78
|
+
const [selectedSet, setSelectedSet] = useState(
|
|
79
|
+
new Set(Array.isArray(config.initialSelectedIds) ? config.initialSelectedIds : [])
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const moveToIndex = (idx) => {
|
|
83
|
+
if (idx == null) return;
|
|
84
|
+
if (items[idx]?.type !== 'choice') return;
|
|
85
|
+
setActiveIndex(idx);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const moveBy = (dir) => {
|
|
89
|
+
const next = findNextSelectable(items, activeIndex, dir);
|
|
90
|
+
if (next != null) {
|
|
91
|
+
setActiveIndex(next);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// loop to start/end
|
|
95
|
+
setActiveIndex(dir > 0 ? firstSelectable : lastSelectable);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const moveGroup = (dir) => {
|
|
99
|
+
const currentGroup = items[activeIndex]?.groupIndex ?? 0;
|
|
100
|
+
const maxGroup = Math.max(0, ...items.map((i) => i.groupIndex ?? 0));
|
|
101
|
+
const nextGroup = dir > 0 ? (currentGroup + 1 > maxGroup ? 0 : currentGroup + 1) : currentGroup - 1 < 0 ? maxGroup : currentGroup - 1;
|
|
102
|
+
moveToIndex(findGroupStart(items, nextGroup));
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const toggleActive = () => {
|
|
106
|
+
const item = items[activeIndex];
|
|
107
|
+
if (!item || item.type !== 'choice') return;
|
|
108
|
+
const next = new Set(selectedSet);
|
|
109
|
+
if (next.has(item.id)) next.delete(item.id);
|
|
110
|
+
else next.add(item.id);
|
|
111
|
+
setSelectedSet(next);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const selectAll = () => {
|
|
115
|
+
const next = new Set(selectedSet);
|
|
116
|
+
for (const idx of selectableIndexes) next.add(items[idx].id);
|
|
117
|
+
setSelectedSet(next);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const invertAll = () => {
|
|
121
|
+
const next = new Set();
|
|
122
|
+
for (const idx of selectableIndexes) {
|
|
123
|
+
const id = items[idx].id;
|
|
124
|
+
if (!selectedSet.has(id)) next.add(id);
|
|
125
|
+
}
|
|
126
|
+
setSelectedSet(next);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
useKeypress((key) => {
|
|
130
|
+
const keyName = key?.name || key?.sequence;
|
|
131
|
+
if (isUpKey(key)) return moveBy(-1);
|
|
132
|
+
if (isDownKey(key)) return moveBy(1);
|
|
133
|
+
if (isSpaceKey(key)) return toggleActive();
|
|
134
|
+
if (isEnterKey(key)) {
|
|
135
|
+
setStatus('done');
|
|
136
|
+
return done(Array.from(selectedSet));
|
|
137
|
+
}
|
|
138
|
+
if (keyName === 'escape') {
|
|
139
|
+
setStatus('done');
|
|
140
|
+
return done(null);
|
|
141
|
+
}
|
|
142
|
+
if (keyName === 'a') return selectAll();
|
|
143
|
+
if (keyName === 'i') return invertAll();
|
|
144
|
+
if (keyName === 'h') return moveToIndex(firstSelectable);
|
|
145
|
+
if (keyName === 'e') return moveToIndex(lastSelectable);
|
|
146
|
+
if (keyName === '[') return moveGroup(-1);
|
|
147
|
+
if (keyName === ']') return moveGroup(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const prefix = usePrefix({ status });
|
|
151
|
+
const titleText = config.title || config.message || 'Select skills to install';
|
|
152
|
+
const total = selectableIndexes.length;
|
|
153
|
+
const selected = selectedSet.size;
|
|
154
|
+
const counter = colors.dim(`${selected} / ${total} selected`);
|
|
155
|
+
const message = `${colors.bold(titleText)} ${counter}`;
|
|
156
|
+
const activeItem = items[activeIndex];
|
|
157
|
+
const desc =
|
|
158
|
+
activeItem?.type === 'choice' && activeItem.description
|
|
159
|
+
? `\n${colors.yellow(activeItem.description.trim())}`
|
|
160
|
+
: '';
|
|
161
|
+
|
|
162
|
+
const page = usePagination({
|
|
163
|
+
items,
|
|
164
|
+
active: activeIndex,
|
|
165
|
+
pageSize: config.pageSize || 18,
|
|
166
|
+
loop: false,
|
|
167
|
+
renderItem: ({ item, index, isActive }) => {
|
|
168
|
+
if (item.type === 'separator') {
|
|
169
|
+
return `\n ${colors.bold(colors.cyan(item.label))}`;
|
|
170
|
+
}
|
|
171
|
+
const cursor = isActive ? colors.cyan('>') : ' ';
|
|
172
|
+
const isChecked = selectedSet.has(item.id);
|
|
173
|
+
const dot = isChecked ? colors.green('●') : colors.dim('○');
|
|
174
|
+
const label = isChecked ? colors.green(item.name) : isActive ? colors.white(item.name) : colors.dim(item.name);
|
|
175
|
+
return `${cursor} ${dot} ${label}`;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const help =
|
|
180
|
+
'\n' +
|
|
181
|
+
colors.dim('↑↓ 选择 · 空格 勾选 · a 全选 · i 反选 · h 顶部 · e 底部 · [ ] 切组 · Esc 退出 · ⏎ 确认');
|
|
182
|
+
|
|
183
|
+
if (status === 'done') {
|
|
184
|
+
return `${prefix} ${colors.bold(titleText)} ${colors.green(`✔ ${selected} skills selected`)}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return `${prefix} ${message}\n${page}${desc}${help}`;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
async function promptSkillSelectionWrapper({ title, skills, initialSelectedIds }) {
|
|
191
|
+
const chosen = await promptSkillSelection({
|
|
192
|
+
title,
|
|
193
|
+
skills,
|
|
194
|
+
initialSelectedIds
|
|
195
|
+
});
|
|
196
|
+
if (chosen == null) return null;
|
|
197
|
+
return Array.isArray(chosen) ? chosen : [];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = { promptSkillSelection: promptSkillSelectionWrapper };
|