@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 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
- 这会调用 `openskills update` 更新所有 openskills 已记录的来源,然后重新 `sync`:
233
+ 默认会优先按 profile 选择集更新(显式 `--profile` 优先,否则使用默认 profile):
225
234
 
226
235
  ```bash
227
236
  skillmanager update
228
237
  ```
229
238
 
230
- ### 如果你用了 profile 做"子集安装"(Web UI 勾选)
239
+ 如果目标 profile 不存在或没有有效选择集,会自动回退到 `openskills update`。
231
240
 
232
- 因为子集安装是按"本地目录复制安装"(为了兼容 Windows 下 openskills 的本地路径识别问题),`openskills update` 不一定能自动追踪来源;此时用 profile 方式更新最稳:
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.5",
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
- // 交互式选择已迁移到: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
 
@@ -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
- const profileName = opts?.profile;
26
- const wantsSelection = !!profileName;
36
+ if (opts?.openskills) {
37
+ await runFallbackOpenSkillsUpdate(opts);
38
+ return;
39
+ }
27
40
 
28
- if (!wantsSelection) {
29
- // Default: ask openskills to update everything it has tracked.
30
- // eslint-disable-next-line no-console
31
- console.log('正在执行 openskills update(更新所有已记录来源)…');
32
- await runOpenSkills(['update']);
33
- if (opts?.sync !== false) {
34
- await syncAgents({ output: opts?.output, cwd: process.cwd() });
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.log('\n完成。');
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
- const allSkills = Array.from(skillsById.values());
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(`将更新/重装 ${selectedIds.length} 个 skills(global=${globalInstall}, universal=${universal})…`);
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 };