@wang121ye/skillmanager 0.0.5 → 0.0.6

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 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wang121ye/skillmanager",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
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
  }
@@ -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
- // 交互式选择已迁移到:skillmanager webui(mode=install)
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
- let toRemove = Array.isArray(skillNames) ? skillNames.filter(Boolean) : [];
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
 
@@ -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 };