@wang121ye/skillmanager 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ye.wang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # skillmanager(`@wang121ye/skillmanager`)
2
+
3
+ 跨平台(Windows / Linux / macOS)的 **Agent Skills 管理器**:把「官方 skills + 第三方 skills 仓库 + 你自己的 skills 仓库」统一做 **安装(install)**,并可选用 **Web UI 交互式勾选**来安装子集。
4
+
5
+ 本项目 **基于 `openskills`** 实现安装与 `AGENTS.md` 同步。
6
+
7
+ ## 安装与使用
8
+
9
+ ### 全局安装(推荐)
10
+
11
+ ```bash
12
+ npm i -g @wang121ye/skillmanager
13
+ skillmanager install
14
+ ```
15
+
16
+ ### 直接 npx(无需安装)
17
+
18
+ ```bash
19
+ npx @wang121ye/skillmanager install
20
+ ```
21
+
22
+ > 说明:你最初写的 `npx skillmanager bootstrap` 只有在发布“非 scope 包名”时才成立。
23
+ > 现在按 scoped 包发布为 `@wang121ye/skillmanager`,对应 npx 用法是 `npx @wang121ye/skillmanager ...`,但执行的命令名仍然叫 `skillmanager`(由 `bin` 决定)。
24
+
25
+ ## 关键能力
26
+
27
+ ### 1) 一键安装(默认装“全部来源的全部 skills”)
28
+
29
+ ```bash
30
+ skillmanager install
31
+ ```
32
+
33
+ ### 2) 安装时启用 Web UI 选择(默认全选,可批量全选/全不选/反选/搜索)
34
+
35
+ ```bash
36
+ skillmanager webui
37
+ ```
38
+
39
+ 你也可以用某个 profile 名称(会保存选择集到该 profile)。**多数情况下不需要显式传 `--profile`**,直接用默认 profile(通常是 `default` 或你在 `skillmanager config set-default-profile` 里设置的值)即可:
40
+
41
+ ```bash
42
+ skillmanager webui --profile laptop
43
+ skillmanager install --profile laptop
44
+ ```
45
+
46
+ ## 把 `--profile laptop` 设为默认(推荐)
47
+
48
+ 设置一次默认 profile 后,绝大多数命令都可以不写 `--profile`:
49
+
50
+ ```bash
51
+ skillmanager config set-default-profile laptop
52
+ skillmanager webui
53
+ skillmanager install
54
+ ```
55
+
56
+ 你也可以用环境变量临时覆盖默认 profile:
57
+
58
+ ```bash
59
+ SKILLMANAGER_PROFILE=laptop skillmanager webui
60
+ ```
61
+
62
+ ## profile 配置的“上传/下载”(换电脑同步)
63
+
64
+ 你可以把某个 profile 上传到一个远端 URL(例如 OSS 对象地址),另一台电脑再 pull 回来。
65
+
66
+ > 安全提示:**开放公共写权限非常危险**,任何人都可以篡改你的 profile。更安全的做法是使用签名 URL、私有桶 + 凭证、或 Git 私有仓库。
67
+
68
+ ### 1) 设置远端 URL(只需一次)
69
+
70
+ ```bash
71
+ skillmanager config set-remote-profile-url https://<bucket>.<region>.aliyuncs.com/skillmanager_profile.json
72
+ ```
73
+
74
+ 你也可以不写入本地配置,改用环境变量(更适合 CI/临时机器):
75
+
76
+ ```bash
77
+ SKILLMANAGER_PROFILE_URL=https://<bucket>.<region>.aliyuncs.com/skillmanager_profile.json
78
+ ```
79
+
80
+ ### 2) 上传当前 profile
81
+
82
+ ```bash
83
+ skillmanager config push --profile laptop
84
+ ```
85
+
86
+ ### 3) 新电脑下载并写入本地 profile
87
+
88
+ ```bash
89
+ skillmanager config pull --profile laptop
90
+ ```
91
+
92
+ ### 3) 安装位置(交给 openskills)
93
+
94
+ - 默认:项目级(`./.claude/skills`)
95
+ - `--global`:全局(`~/.claude/skills`)
96
+ - `--universal`:使用通用目录(`.agent/skills` / `~/.agent/skills`)
97
+
98
+ 示例:
99
+
100
+ ```bash
101
+ skillmanager install --global --universal
102
+ ```
103
+
104
+ ### 4) 同步 `AGENTS.md`
105
+
106
+ 默认会执行 `openskills sync`(输出到当前目录的 `AGENTS.md`)。
107
+
108
+ - 指定输出文件:
109
+
110
+ ```bash
111
+ skillmanager install --output AGENTS.md
112
+ ```
113
+
114
+ - 跳过同步:
115
+
116
+ ```bash
117
+ skillmanager install --no-sync
118
+ ```
119
+
120
+ ### 5) dry-run(只打印要装什么,不实际安装)
121
+
122
+ ```bash
123
+ skillmanager install --dry-run
124
+ ```
125
+
126
+ ## 更方便地添加/管理第三方仓库
127
+
128
+ 以后不需要手动去编辑 `sources.json`,可以直接用命令写入用户配置:
129
+
130
+ ```bash
131
+ skillmanager source add https://github.com/obra/superpowers
132
+ skillmanager source add ComposioHQ/awesome-claude-skills
133
+ skillmanager source list
134
+ ```
135
+
136
+ 禁用/启用/删除:
137
+
138
+ ```bash
139
+ skillmanager source disable superpowers
140
+ skillmanager source enable superpowers
141
+ skillmanager source remove superpowers
142
+ ```
143
+
144
+ > `source add` 支持输入 `owner/repo` 或 GitHub URL(也支持 `git@github.com:owner/repo.git`)。
145
+
146
+ ## 更新已安装 skills(无论哪种来源)
147
+
148
+ ### 默认更新(推荐)
149
+
150
+ 这会调用 `openskills update` 更新所有 openskills 已记录的来源,然后重新 `sync`:
151
+
152
+ ```bash
153
+ skillmanager update
154
+ ```
155
+
156
+ ### 如果你用了 profile 做“子集安装”(Web UI 勾选)
157
+
158
+ 因为子集安装是按“本地目录复制安装”(为了兼容 Windows 下 openskills 的本地路径识别问题),`openskills update` 不一定能自动追踪来源;此时用 profile 方式更新最稳:
159
+
160
+ ```bash
161
+ skillmanager update --profile laptop
162
+ ```
163
+
164
+ 需要临时调整选择集:先用 Web UI 更新 profile,再执行 update:
165
+
166
+ ```bash
167
+ skillmanager webui --profile laptop
168
+ skillmanager update --profile laptop
169
+ ```
170
+
171
+ ## 卸载 skills
172
+
173
+ ### 用 Web UI 勾选要卸载的已安装 skills(推荐)
174
+
175
+ ```bash
176
+ skillmanager webui --mode uninstall
177
+ ```
178
+
179
+ ### 直接按名称卸载
180
+
181
+ ```bash
182
+ skillmanager uninstall algorithmic-art xlsx
183
+ ```
184
+
185
+ ### 清空目标目录(危险操作,需要显式 --all)
186
+
187
+ ```bash
188
+ skillmanager uninstall --all
189
+ ```
190
+
191
+ ## Skills 来源配置(官方 / 第三方 / 你自己的)
192
+
193
+ skillmanager 会在第一次运行时,把内置的 `manifests/sources.json` 复制到你的用户配置目录,之后你只需要编辑 **用户配置文件**即可:
194
+
195
+ ```bash
196
+ skillmanager paths
197
+ ```
198
+
199
+ 会打印出类似:
200
+
201
+ - `manifest: C:\Users\<you>\AppData\Roaming\skillmanager\sources.json`(Windows)
202
+ - macOS/Linux 则在 `~/.config/skillmanager/sources.json` 附近
203
+
204
+ `sources.json` 里维护三类来源(你可以继续追加第三方仓库):
205
+
206
+ - 官方:`anthropics/skills`
207
+ - 你的:`wangyendt/wayne-skills`
208
+ - 第三方:自行添加(`enabled: true`)
209
+
210
+ ## 发布到 npm(给你未来用)
211
+
212
+ 你要发布 scoped 包到 npm,一般是:
213
+
214
+ ```bash
215
+ npm login
216
+ npm publish --access public
217
+ ```
218
+
219
+ (你提到的 npm 账号是 `wang121ye`:确保你拥有 `@wang121ye` scope 的发布权限;发布 scoped 包通常需要 `npm publish --access public`,本项目已在 `publishConfig` 里默认设置为 public。)
@@ -0,0 +1,38 @@
1
+ {
2
+ "version": 2,
3
+ "sources": [
4
+ {
5
+ "id": "anthropic",
6
+ "name": "Anthropic Official Skills",
7
+ "kind": "git",
8
+ "enabled": true,
9
+ "repo": "https://github.com/anthropics/skills.git",
10
+ "openskillsRef": "anthropics/skills"
11
+ },
12
+ {
13
+ "id": "wayne",
14
+ "name": "Wayne Skills",
15
+ "kind": "git",
16
+ "enabled": true,
17
+ "repo": "https://github.com/wangyendt/wayne-skills.git",
18
+ "openskillsRef": "wangyendt/wayne-skills"
19
+ },
20
+ {
21
+ "id": "superpowers",
22
+ "name": "Obra Superpowers",
23
+ "kind": "git",
24
+ "enabled": true,
25
+ "repo": "https://github.com/obra/superpowers.git",
26
+ "openskillsRef": "obra/superpowers"
27
+ },
28
+ {
29
+ "id": "awesome-claude-skills",
30
+ "name": "Awesome Claude Skills (ComposioHQ)",
31
+ "kind": "git",
32
+ "enabled": true,
33
+ "repo": "https://github.com/ComposioHQ/awesome-claude-skills.git",
34
+ "openskillsRef": "ComposioHQ/awesome-claude-skills"
35
+ }
36
+ ]
37
+ }
38
+
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@wang121ye/skillmanager",
3
+ "version": "0.0.1",
4
+ "description": "Cross-platform agent skill manager (bootstrap + optional Web UI selection) built on top of openskills.",
5
+ "keywords": [
6
+ "skillmanager",
7
+ "openskills",
8
+ "claude",
9
+ "cursor",
10
+ "skills",
11
+ "agent"
12
+ ],
13
+ "homepage": "https://github.com/wangyendt/skillmanager#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/wangyendt/skillmanager/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/wangyendt/skillmanager.git"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "license": "MIT",
25
+ "author": "Wayne",
26
+ "type": "commonjs",
27
+ "bin": {
28
+ "skillmanager": "src/cli.js"
29
+ },
30
+ "main": "src/index.js",
31
+ "files": [
32
+ "src/",
33
+ "manifests/"
34
+ ],
35
+ "engines": {
36
+ "node": ">=20.6.0"
37
+ },
38
+ "scripts": {
39
+ "test": "node -e \"console.log('no tests yet')\""
40
+ },
41
+ "dependencies": {
42
+ "commander": "^13.1.0",
43
+ "express": "^4.21.2",
44
+ "fast-glob": "^3.3.2",
45
+ "gray-matter": "^4.0.3",
46
+ "openskills": "^1.5.0",
47
+ "simple-git": "^3.27.0",
48
+ "undici": "^7.4.0"
49
+ }
50
+ }
package/src/cli.js ADDED
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+
4
+ const { Command } = require('commander');
5
+
6
+ const { bootstrap } = require('./commands/bootstrap');
7
+ const { selectUi } = require('./commands/select');
8
+ const { where } = require('./commands/where');
9
+ const { update } = require('./commands/update');
10
+ const { uninstall } = require('./commands/uninstall');
11
+ const { webui } = require('./commands/webui');
12
+ const { listSources, addSource, removeSource, setEnabled } = require('./commands/source');
13
+ const { showConfig, setDefaultProfileCmd, setRemoteProfileUrlCmd, pushProfileCmd, pullProfileCmd } = require('./commands/config');
14
+
15
+ async function main() {
16
+ const program = new Command();
17
+
18
+ program
19
+ .name('skillmanager')
20
+ .description('跨平台 Agent Skills 管理器(基于 openskills)')
21
+ .version(require('../package.json').version);
22
+
23
+ program
24
+ .command('install')
25
+ .description('安装 skills:按 sources 配置安装全部,或用 Web UI 选择子集安装。')
26
+ .option('--global', '安装到全局(默认:当前项目)', false)
27
+ .option('--universal', '使用通用目录 .agent/skills(默认:.claude/skills)', false)
28
+ .option('--output <path>', 'sync 输出文件(默认:AGENTS.md)')
29
+ .option('--no-sync', '跳过 openskills sync', false)
30
+ .option('--dry-run', '仅打印将执行的内容,不实际安装', false)
31
+ .option('--concurrency <n>', '选择模式下的并发扫描数(默认:3)', '3')
32
+ .option('--profile <name>', '选择配置名(默认:来自 config 或 SKILLMANAGER_PROFILE 环境变量)')
33
+ .action(async (opts) => {
34
+ await bootstrap(opts);
35
+ });
36
+
37
+ program
38
+ .command('webui')
39
+ .description('打开 Web UI:用于选择并安装(install)或选择并卸载(uninstall)。')
40
+ .option('--mode <install|uninstall>', '模式:install(选择并安装)或 uninstall(选择并卸载)', 'install')
41
+ .option('--profile <name>', '作用:install 模式下使用/保存到的 profile(默认:使用默认 profile)')
42
+ .option('--global', '作用:把目标切到全局目录(~/.claude/skills 或 ~/.agent/skills)', false)
43
+ .option('--universal', '作用:使用 .agent/skills(通用 AGENTS.md 场景;默认是 .claude/skills)', false)
44
+ .option('--output <path>', '作用:sync 输出文件路径(默认:AGENTS.md)')
45
+ .option('--no-sync', '作用:不执行 openskills sync(仅安装/卸载,不更新 AGENTS.md)', false)
46
+ .option('--concurrency <n>', '作用:install 模式下并发拉取/扫描来源仓库,提高速度(默认:3)', '3')
47
+ .action(async (opts) => {
48
+ await webui(opts);
49
+ });
50
+
51
+ program
52
+ .command('paths')
53
+ .description('打印 skillmanager 的配置/缓存目录,以及 sources.json 位置。')
54
+ .action(async () => {
55
+ await where();
56
+ });
57
+
58
+ program
59
+ .command('update')
60
+ .description('更新 skills,并同步生成/更新 AGENTS.md。')
61
+ .option('--global', '安装到全局(默认:当前项目)', false)
62
+ .option('--universal', '使用通用目录 .agent/skills(默认:.claude/skills)', false)
63
+ .option('--output <path>', 'sync 输出文件(默认:AGENTS.md)')
64
+ .option('--no-sync', '跳过 openskills sync', false)
65
+ .option('--profile <name>', '按 profile 选择集更新(会重新安装选中的 skills)')
66
+ .option('--concurrency <n>', '选择模式下的并发扫描数(默认:3)', '3')
67
+ .action(async (opts) => {
68
+ await update(opts);
69
+ });
70
+
71
+ program
72
+ .command('uninstall')
73
+ .description('卸载 skills(从 .claude/skills 或 .agent/skills 删除,并可 sync 更新 AGENTS.md)。')
74
+ .argument('[skillNames...]', '要卸载的 skill 名称(目录名),可多个')
75
+ .option('--global', '卸载全局目录(默认:当前项目)', false)
76
+ .option('--universal', '使用通用目录 .agent/skills(默认:.claude/skills)', false)
77
+ .option('--all', '卸载目标目录下所有已安装 skills', false)
78
+ .option('--output <path>', 'sync 输出文件(默认:AGENTS.md)')
79
+ .option('--no-sync', '跳过 openskills sync', false)
80
+ .action(async (skillNames, opts) => {
81
+ await uninstall(opts, skillNames);
82
+ });
83
+
84
+ const config = program.command('config').description('配置管理:默认 profile、Web UI 选择等。');
85
+
86
+ config
87
+ .command('set-default-profile')
88
+ .description('设置默认 profile(当你不传 --profile 时会用它)。')
89
+ .argument('<name>', 'profile 名称,例如 laptop')
90
+ .action(async (name) => {
91
+ await setDefaultProfileCmd(name);
92
+ });
93
+
94
+ config
95
+ .command('set-remote-profile-url')
96
+ .description('设置远端 profile URL(用于 config push/pull)。')
97
+ .argument('<url>', '例如 https://<bucket>.<region>.aliyuncs.com/skillmanager_profile.json')
98
+ .action(async (url) => {
99
+ await setRemoteProfileUrlCmd(url);
100
+ });
101
+
102
+ config
103
+ .command('push')
104
+ .description('上传某个 profile 到远端 URL(HTTP PUT)。')
105
+ .option('--profile <name>', 'profile 名称(默认:default)', 'default')
106
+ .option('--url <url>', '远端 URL(可省略,使用 config.remoteProfileUrl 或 SKILLMANAGER_PROFILE_URL)')
107
+ .action(async (opts) => {
108
+ await pushProfileCmd(opts);
109
+ });
110
+
111
+ config
112
+ .command('pull')
113
+ .description('从远端 URL 下载 profile 并保存到本地(HTTP GET)。')
114
+ .option('--profile <name>', 'profile 名称(默认:default)', 'default')
115
+ .option('--url <url>', '远端 URL(可省略,使用 config.remoteProfileUrl 或 SKILLMANAGER_PROFILE_URL)')
116
+ .action(async (opts) => {
117
+ await pullProfileCmd(opts);
118
+ });
119
+
120
+ config
121
+ .command('show')
122
+ .description('显示配置(含默认 profile)。')
123
+ .action(async () => {
124
+ await showConfig();
125
+ });
126
+
127
+ const source = program.command('source').description('管理 skills 来源仓库(官方/第三方/自研)。');
128
+
129
+ source
130
+ .command('list')
131
+ .description('列出当前 sources.json 中的来源。')
132
+ .action(async () => {
133
+ await listSources();
134
+ });
135
+
136
+ source
137
+ .command('add')
138
+ .description('添加来源:支持 owner/repo、GitHub URL、或 git@... SSH URL。')
139
+ .argument('<repoOrRef>', '例如 ComposioHQ/awesome-claude-skills 或 https://github.com/obra/superpowers')
140
+ .option('--id <id>', '指定来源 id(可选)')
141
+ .option('--name <name>', '显示名称(可选)')
142
+ .option('--ref <openskillsRef>', '指定 openskillsRef(可选,形如 owner/repo)')
143
+ .option('--disabled', '添加为禁用(bootstrap 默认不安装)', false)
144
+ .action(async (repoOrRef, opts) => {
145
+ await addSource(repoOrRef, opts);
146
+ });
147
+
148
+ source
149
+ .command('remove')
150
+ .description('删除来源(按 id)。')
151
+ .argument('<id>', '来源 id')
152
+ .action(async (id) => {
153
+ await removeSource(id);
154
+ });
155
+
156
+ source
157
+ .command('enable')
158
+ .description('启用来源(按 id)。')
159
+ .argument('<id>', '来源 id')
160
+ .action(async (id) => {
161
+ await setEnabled(id, true);
162
+ });
163
+
164
+ source
165
+ .command('disable')
166
+ .description('禁用来源(按 id)。')
167
+ .argument('<id>', '来源 id')
168
+ .action(async (id) => {
169
+ await setEnabled(id, false);
170
+ });
171
+
172
+ await program.parseAsync(process.argv);
173
+ }
174
+
175
+ main().catch((err) => {
176
+ console.error(err?.stack || String(err));
177
+ process.exitCode = 1;
178
+ });
179
+
@@ -0,0 +1,153 @@
1
+ const { getAppPaths } = require('../lib/paths');
2
+ const { ensureDir } = require('../lib/fs');
3
+ const { loadSourcesManifest } = require('../lib/manifest');
4
+ const { ensureRepo } = require('../lib/git');
5
+ const { scanSkillsInRepo } = require('../lib/scan');
6
+ const { loadProfile } = require('../lib/profiles');
7
+ const { mapWithConcurrency } = require('../lib/concurrency');
8
+ const { getEffectiveDefaultProfile } = require('../lib/config');
9
+ const path = require('path');
10
+ const os = require('os');
11
+ const { installSourceRef, syncAgents } = require('../lib/openskills');
12
+ const { installFromLocalSkillDir } = require('../lib/local-install');
13
+
14
+
15
+ function uniq(arr) {
16
+ return Array.from(new Set(arr));
17
+ }
18
+
19
+ async function bootstrap(opts) {
20
+ const paths = getAppPaths();
21
+ await ensureDir(paths.reposDir);
22
+ await ensureDir(paths.profilesDir);
23
+
24
+ const { sources } = await loadSourcesManifest();
25
+ const enabledSources = sources.filter((s) => s && s.enabled !== false);
26
+
27
+ const profileName = opts?.profile || (await getEffectiveDefaultProfile());
28
+ const existing = await loadProfile({ profilesDir: paths.profilesDir, profileName });
29
+
30
+ const wantsSelection = existing?.selectedSkillIds && Array.isArray(existing.selectedSkillIds);
31
+ const globalInstall = !!opts?.global;
32
+ const universal = !!opts?.universal;
33
+
34
+ if (!wantsSelection) {
35
+ // Default path: install everything from each enabled source via openskills directly.
36
+ // This avoids repo scanning and matches “bootstrap 安装所有 skills”的默认体验。
37
+ // eslint-disable-next-line no-console
38
+ console.log(`将从 ${enabledSources.length} 个来源安装(global=${globalInstall}, universal=${universal})…`);
39
+
40
+ if (opts?.dryRun) {
41
+ // eslint-disable-next-line no-console
42
+ console.log('\n--dry-run 已启用:将安装以下来源(不执行安装)');
43
+ for (const s of enabledSources) {
44
+ const ref = s.openskillsRef || s.repo;
45
+ // eslint-disable-next-line no-console
46
+ console.log(`- ${s.name || s.id} (${ref || 'missing-ref'})`);
47
+ }
48
+ // eslint-disable-next-line no-console
49
+ console.log('\n完成(dry-run)。');
50
+ return;
51
+ }
52
+
53
+ for (const s of enabledSources) {
54
+ const ref = s.openskillsRef || s.repo;
55
+ if (!ref) continue;
56
+ // eslint-disable-next-line no-console
57
+ console.log(`\n==> Installing source: ${s.name || s.id} (${ref})`);
58
+ await installSourceRef({ ref, globalInstall, universal });
59
+ }
60
+
61
+ if (opts?.sync !== false) {
62
+ await syncAgents({ output: opts?.output, cwd: process.cwd() });
63
+ }
64
+
65
+ // eslint-disable-next-line no-console
66
+ console.log('\n完成。');
67
+ return;
68
+ }
69
+
70
+ // Selection path: clone repos + scan SKILL.md, then install selected skill dirs.
71
+ const concurrency = Number(opts?.concurrency || process.env.SKILLMANAGER_CONCURRENCY || 3);
72
+ // eslint-disable-next-line no-console
73
+ console.log(`并发扫描:${Math.max(1, concurrency)}(可用 --concurrency 或环境变量 SKILLMANAGER_CONCURRENCY 调整)`);
74
+
75
+ const skillsById = new Map();
76
+ const perSource = await mapWithConcurrency(enabledSources, concurrency, async (s) => {
77
+ try {
78
+ const repoDir = await ensureRepo({ reposDir: paths.reposDir, source: s });
79
+ const skills = await scanSkillsInRepo({
80
+ sourceId: s.id,
81
+ sourceName: s.name || s.id,
82
+ repoDir
83
+ });
84
+ return { source: s, skills };
85
+ } catch (err) {
86
+ // eslint-disable-next-line no-console
87
+ console.warn(`警告:拉取/扫描来源失败,将跳过:${s.name || s.id}`);
88
+ // eslint-disable-next-line no-console
89
+ console.warn(err?.message || String(err));
90
+ return { source: s, skills: [] };
91
+ }
92
+ });
93
+
94
+ for (const { skills } of perSource) {
95
+ for (const sk of skills) skillsById.set(sk.id, sk);
96
+ }
97
+
98
+ const allSkills = Array.from(skillsById.values());
99
+ let selectedIds =
100
+ existing?.selectedSkillIds && Array.isArray(existing.selectedSkillIds)
101
+ ? existing.selectedSkillIds
102
+ : allSkills.map((s) => s.id);
103
+
104
+ // 交互式选择已迁移到:skillmanager webui(mode=install)
105
+
106
+ selectedIds = uniq(selectedIds).filter((id) => skillsById.has(id));
107
+
108
+ // eslint-disable-next-line no-console
109
+ console.log(`将安装 ${selectedIds.length} 个 skills(global=${globalInstall}, universal=${universal})…`);
110
+
111
+ if (opts?.dryRun) {
112
+ // eslint-disable-next-line no-console
113
+ console.log('\n--dry-run 已启用:仅展示前 30 个将安装的 skills(按解析顺序)');
114
+ for (const id of selectedIds.slice(0, 30)) {
115
+ const s = skillsById.get(id);
116
+ // eslint-disable-next-line no-console
117
+ console.log(`- ${s.name} [${s.sourceId}] (${s.id})`);
118
+ }
119
+ if (selectedIds.length > 30) {
120
+ // eslint-disable-next-line no-console
121
+ console.log(`… 还有 ${selectedIds.length - 30} 个`);
122
+ }
123
+ // eslint-disable-next-line no-console
124
+ console.log('\n完成(dry-run)。');
125
+ return;
126
+ }
127
+
128
+ // 2) install selected
129
+ for (const id of selectedIds) {
130
+ const skill = skillsById.get(id);
131
+ // eslint-disable-next-line no-console
132
+ console.log(`\n==> Installing: ${skill.name} (${skill.sourceId})`);
133
+
134
+ // NOTE: On Windows, openskills does not recognize absolute paths like C:\...
135
+ // So for selection-mode we perform a direct local install (copy) ourselves, then rely on openskills sync.
136
+ const folder = universal ? '.agent/skills' : '.claude/skills';
137
+ const targetDir = globalInstall ? path.join(os.homedir(), folder) : path.join(process.cwd(), folder);
138
+ const { targetPath } = await installFromLocalSkillDir({ skillDir: skill.skillDir, targetDir });
139
+ // eslint-disable-next-line no-console
140
+ console.log(`✅ Installed (local copy): ${targetPath}`);
141
+ }
142
+
143
+ // 3) sync AGENTS.md (optional)
144
+ if (opts?.sync !== false) {
145
+ await syncAgents({ output: opts?.output, cwd: process.cwd() });
146
+ }
147
+
148
+ // eslint-disable-next-line no-console
149
+ console.log('\n完成。');
150
+ }
151
+
152
+ module.exports = { bootstrap };
153
+