skillcaddy 0.27.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 +497 -0
- package/CONTEXT.md +149 -0
- package/LICENSE +21 -0
- package/README.md +499 -0
- package/README_CN.md +482 -0
- package/collection-metadata/github/mattpocock.json +16 -0
- package/lib/canonicalJson.js +11 -0
- package/lib/claudeStore.js +135 -0
- package/lib/collectionEnablement.js +115 -0
- package/lib/collectionSetup.js +199 -0
- package/lib/enablePlan.js +116 -0
- package/lib/enablementScope.js +8 -0
- package/lib/hermesStore.js +21 -0
- package/lib/libraryImage.js +1785 -0
- package/lib/managerInstall.js +109 -0
- package/lib/managerSourceWorkflow.js +234 -0
- package/lib/projectActions.js +24 -0
- package/lib/projectPath.js +51 -0
- package/lib/roots.js +99 -0
- package/lib/skillLinkStore.js +68 -0
- package/lib/skillMetadata.js +191 -0
- package/lib/skillStore.js +541 -0
- package/lib/sourceAcquisition.js +475 -0
- package/lib/sourceAcquisitionAdapter.js +47 -0
- package/lib/sourceAcquisitionError.js +18 -0
- package/lib/sourceAcquisitionPolicy.js +139 -0
- package/lib/sourceArchive.js +363 -0
- package/lib/sourceArchiveWorkspace.js +36 -0
- package/lib/sourceGit.js +95 -0
- package/lib/sourceGitAcquisitionAdapter.js +145 -0
- package/lib/sourceGitBatchUpdate.js +168 -0
- package/lib/sourceGitRepair.js +227 -0
- package/lib/sourceGitStaging.js +139 -0
- package/lib/sourceGitUpgradeAdapter.js +583 -0
- package/lib/sourceGitUrl.js +170 -0
- package/lib/sourceHttp.js +302 -0
- package/lib/sourceLocal.js +200 -0
- package/lib/sourceLocalAcquisitionAdapter.js +107 -0
- package/lib/sourceManager.js +242 -0
- package/lib/sourceMigration.js +172 -0
- package/lib/sourceMigrationIssue.js +6 -0
- package/lib/sourcePath.js +6 -0
- package/lib/sourcePolicy.js +53 -0
- package/lib/sourceRecord.js +295 -0
- package/lib/sourceRegistry.js +155 -0
- package/lib/sourceRemoteArchiveAcquisitionAdapter.js +147 -0
- package/lib/sourceRemoteFileAcquisitionAdapter.js +95 -0
- package/lib/sourceRemoteFilePolicy.js +62 -0
- package/lib/sourceRemoteFileWorkspace.js +55 -0
- package/lib/sourceSkillDiscovery.js +117 -0
- package/lib/sourceTree.js +185 -0
- package/lib/sourceUpgrade.js +596 -0
- package/lib/sourceUpgradeAdapter.js +41 -0
- package/lib/sourceUpgradePolicy.js +170 -0
- package/lib/sourceUpgradeTransaction.js +202 -0
- package/lib/sourceValidation.js +120 -0
- package/lib/sourceWorkspace.js +37 -0
- package/lib/specGate.js +235 -0
- package/lib/tuiActions.js +169 -0
- package/lib/tuiInstall.js +133 -0
- package/lib/tuiLayout.js +94 -0
- package/lib/version.js +17 -0
- package/lib/webManager.js +370 -0
- package/package.json +74 -0
- package/public/agentsUi.js +138 -0
- package/public/app.js +1431 -0
- package/public/claude.css +2 -0
- package/public/claudeUi.js +119 -0
- package/public/emptyState.js +12 -0
- package/public/enabled.css +303 -0
- package/public/favicon.svg +5 -0
- package/public/index.html +199 -0
- package/public/skillcaddy_CN.png +0 -0
- package/public/skillcaddy_EN.png +0 -0
- package/public/styles.css +1611 -0
- package/scripts/cli.js +647 -0
- package/scripts/cron-sync-update-git.sh +128 -0
- package/scripts/pull-github.sh +22 -0
- package/scripts/source.js +638 -0
- package/scripts/tui.js +1058 -0
- package/server.js +196 -0
- package/skills/skillcaddy-manager/SKILL.md +93 -0
- package/skills/skillcaddy-manager/agents/openai.yaml +7 -0
- package/skills/skillcaddy-manager/references/METADATA.md +75 -0
- package/skills/skillcaddy-manager/references/OPERATIONS.md +315 -0
- package/skills/skillcaddy-manager/references/RECOMMENDATION_GUIDE.md +51 -0
- package/skills/skillcaddy-manager/references/featured-skills.json +564 -0
- package/skills/skillcaddy-manager/references/skill-platforms.json +48 -0
- package/skills/skillcaddy-manager/scripts/check-conflicts.cjs +117 -0
- package/skills/skillcaddy-manager/scripts/check-global-skills.cjs +169 -0
- package/skills/skillcaddy-manager/scripts/translate-skill-notes.cjs +297 -0
- package/skills/skillcaddy-manager/scripts/version-manager.cjs +146 -0
- package/skills/skillcaddy-manager/scripts/view-recommendations.cjs +279 -0
- package/skills/skillcaddy-manager/skillcaddy.json +9 -0
- package/skills/spec-pipeline/spec-direction-gate/SKILL.md +119 -0
- package/skills/spec-pipeline/spec-direction-gate/check-approval.py +110 -0
- package/skills/spec-pipeline/spec-draft-intake/SKILL.md +116 -0
- package/skills/spec-pipeline/spec-role-contract/SKILL.md +113 -0
- package/skills/spec-pipeline/spec-role-contract/check-roles.py +108 -0
package/scripts/cli.js
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import { access, readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { disableSkill, getState, scanSkills } from '../lib/skillStore.js';
|
|
4
|
+
import { installCliCommand } from '../lib/tuiInstall.js';
|
|
5
|
+
import { enableProjectSkill } from '../lib/projectActions.js';
|
|
6
|
+
import { resolveLibraryRoot } from '../lib/roots.js';
|
|
7
|
+
import { readVersion } from '../lib/version.js';
|
|
8
|
+
import { restartWeb, startWeb, stopWeb } from '../lib/webManager.js';
|
|
9
|
+
import { runSourceCli } from './source.js';
|
|
10
|
+
|
|
11
|
+
const CLI_FLAGS = new Set([
|
|
12
|
+
'-h', '--help', '-v', '--version', '-u', '--update', '-a', '--analyze',
|
|
13
|
+
'--verbose'
|
|
14
|
+
]);
|
|
15
|
+
const CLI_COMMANDS = new Set(['start', 'stop', 'restart', 'update', 'analyze', 'install', 'server', 'web', 'enable', 'disable']);
|
|
16
|
+
|
|
17
|
+
export function isCliCommand(argv = []) {
|
|
18
|
+
return argv.some((arg) =>
|
|
19
|
+
CLI_FLAGS.has(arg) ||
|
|
20
|
+
CLI_COMMANDS.has(arg) ||
|
|
21
|
+
arg.startsWith('-') && arg !== '--root' && !arg.startsWith('--root=')
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parseCliArgs(argv = []) {
|
|
26
|
+
const result = {
|
|
27
|
+
command: 'tui',
|
|
28
|
+
projectPath: '',
|
|
29
|
+
rootDir: '',
|
|
30
|
+
port: undefined,
|
|
31
|
+
open: true,
|
|
32
|
+
verbose: false,
|
|
33
|
+
error: ''
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
37
|
+
const arg = argv[index];
|
|
38
|
+
|
|
39
|
+
if (arg === '-h' || arg === '--help') {
|
|
40
|
+
result.command = 'help';
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (arg === '-v' || arg === '--version') {
|
|
44
|
+
result.command = 'version';
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (arg === '-u' || arg === '--update' || arg === 'update') {
|
|
48
|
+
result.command = 'update';
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (arg === '-a' || arg === '--analyze' || arg === 'analyze') {
|
|
52
|
+
result.command = 'analyze';
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (arg === 'start' || arg === 'stop' || arg === 'restart') {
|
|
56
|
+
result.command = arg;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (arg === 'enable' || arg === 'disable') {
|
|
60
|
+
result.command = arg;
|
|
61
|
+
result.scope = '';
|
|
62
|
+
if (arg === 'enable') result.skillRef = '';
|
|
63
|
+
if (arg === 'disable') result.alias = '';
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (arg === 'install') {
|
|
67
|
+
const target = argv[index + 1];
|
|
68
|
+
if (!['cli', 'tui'].includes(target)) {
|
|
69
|
+
result.error = 'install 后需要 cli 或 tui';
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
result.command = 'install';
|
|
73
|
+
result.installTarget = target;
|
|
74
|
+
index += 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (arg === 'web') {
|
|
78
|
+
result.command = 'start';
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (arg === 'server') {
|
|
82
|
+
const next = argv[index + 1];
|
|
83
|
+
if (!['start', 'stop', 'restart'].includes(next)) {
|
|
84
|
+
result.error = 'server 后需要 start、stop 或 restart';
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
result.command = next;
|
|
88
|
+
index += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (arg === '--root') {
|
|
92
|
+
result.rootDir = argv[index + 1] || '';
|
|
93
|
+
if (!result.rootDir) result.error = '--root 需要一个目录路径';
|
|
94
|
+
index += 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (arg.startsWith('--root=')) {
|
|
98
|
+
result.rootDir = arg.slice('--root='.length);
|
|
99
|
+
if (!result.rootDir) result.error = '--root 需要一个目录路径';
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (arg === '--project') {
|
|
103
|
+
result.projectPath = argv[index + 1] || '';
|
|
104
|
+
if (!result.projectPath) result.error = '--project 需要一个目录路径';
|
|
105
|
+
if (result.scope === 'global') result.error = `${result.command} 的 --global 不能与 --project 同时使用`;
|
|
106
|
+
if (result.scope === 'hermes') result.error = `${result.command} 的 --hermes 不能与 --project 同时使用`;
|
|
107
|
+
if (result.command === 'enable' || result.command === 'disable') result.scope = 'project';
|
|
108
|
+
index += 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (arg.startsWith('--project=')) {
|
|
112
|
+
result.projectPath = arg.slice('--project='.length);
|
|
113
|
+
if (!result.projectPath) result.error = '--project 需要一个目录路径';
|
|
114
|
+
if (result.scope === 'global') result.error = `${result.command} 的 --global 不能与 --project 同时使用`;
|
|
115
|
+
if (result.scope === 'hermes') result.error = `${result.command} 的 --hermes 不能与 --project 同时使用`;
|
|
116
|
+
if (result.command === 'enable' || result.command === 'disable') result.scope = 'project';
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (arg === '--global') {
|
|
120
|
+
if (result.command !== 'enable' && result.command !== 'disable') {
|
|
121
|
+
result.error = '--global 只适用于 enable 或 disable';
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
if (result.scope === 'hermes') result.error = `${result.command} 的 --global 不能与 --hermes 同时使用`;
|
|
125
|
+
result.scope = 'global';
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (arg === '--hermes') {
|
|
129
|
+
if (result.command !== 'enable' && result.command !== 'disable') {
|
|
130
|
+
result.error = '--hermes 只适用于 enable 或 disable';
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
if (result.scope === 'global') result.error = `${result.command} 的 --hermes 不能与 --global 同时使用`;
|
|
134
|
+
if (result.scope === 'project') result.error = `${result.command} 的 --hermes 不能与 --project 同时使用`;
|
|
135
|
+
result.scope = 'hermes';
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (arg === '--alias') {
|
|
139
|
+
if (result.command !== 'enable') {
|
|
140
|
+
result.error = '--alias 只适用于 enable';
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
result.alias = argv[index + 1] || '';
|
|
144
|
+
if (!result.alias) result.error = '--alias 需要一个名称';
|
|
145
|
+
index += 1;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (arg === '-p' || arg === '--port') {
|
|
149
|
+
result.port = argv[index + 1] || '';
|
|
150
|
+
if (!result.port) result.error = `${arg} 需要一个端口号`;
|
|
151
|
+
index += 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (arg.startsWith('--port=')) {
|
|
155
|
+
result.port = arg.slice('--port='.length);
|
|
156
|
+
if (!result.port) result.error = '--port 需要一个端口号';
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (arg === '--no-open') {
|
|
160
|
+
result.open = false;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (arg === '--verbose') {
|
|
164
|
+
result.verbose = true;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (arg.startsWith('-')) {
|
|
168
|
+
result.error = `未知参数:${arg}`;
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
if (result.command === 'enable') {
|
|
172
|
+
if (result.skillRef) {
|
|
173
|
+
result.error = `只能指定一个 skill:${arg}`;
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
result.skillRef = arg;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (result.command === 'disable') {
|
|
180
|
+
if (result.alias) {
|
|
181
|
+
result.error = `只能指定一个 alias:${arg}`;
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
result.alias = arg;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (!result.projectPath) {
|
|
188
|
+
result.projectPath = arg;
|
|
189
|
+
} else {
|
|
190
|
+
result.error = `只能指定一个项目路径:${arg}`;
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (result.command === 'enable' || result.command === 'disable') {
|
|
196
|
+
const value = result.command === 'enable' ? result.skillRef : result.alias;
|
|
197
|
+
if (!value) result.error = `${result.command} 需要一个 skill 或 alias`;
|
|
198
|
+
if (!result.scope && !result.error) result.error = `${result.command} 需要 --project、--global 或 --hermes`;
|
|
199
|
+
if (result.scope === 'global' && result.projectPath && !result.error) {
|
|
200
|
+
result.error = `${result.command} 的 --global 不能与 --project 同时使用`;
|
|
201
|
+
}
|
|
202
|
+
if (result.scope === 'hermes' && result.projectPath && !result.error) {
|
|
203
|
+
result.error = `${result.command} 的 --hermes 不能与 --project 同时使用`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function buildHelpText() {
|
|
211
|
+
return `Skillcaddy:本地 skill 原件库与项目启用管理
|
|
212
|
+
|
|
213
|
+
用法:
|
|
214
|
+
skillcaddy 进入当前项目 TUI
|
|
215
|
+
skillcaddy <projectPath> 管理指定项目
|
|
216
|
+
skillcaddy start [projectPath] 启动 Web 管理器
|
|
217
|
+
skillcaddy stop 停止由 Skillcaddy 管理的 Web
|
|
218
|
+
skillcaddy restart [projectPath] 重启 Web 管理器
|
|
219
|
+
skillcaddy install cli 安装全局 CLI + TUI 命令
|
|
220
|
+
skillcaddy install tui 同上,兼容 TUI 安装说法
|
|
221
|
+
skillcaddy -v 查看版本
|
|
222
|
+
skillcaddy -h 查看帮助
|
|
223
|
+
skillcaddy -u [projectPath] 安全更新已登记的 Git skill 源
|
|
224
|
+
skillcaddy -a [projectPath] 分析项目 skill 状态并给出推荐
|
|
225
|
+
skillcaddy enable <skill-id> --project <dir> [--alias <name>]
|
|
226
|
+
skillcaddy enable <skill-id> --global [--alias <name>]
|
|
227
|
+
skillcaddy enable <skill-id> --hermes [--alias <name>]
|
|
228
|
+
skillcaddy disable <alias> --project <dir>
|
|
229
|
+
skillcaddy disable <alias> --global
|
|
230
|
+
skillcaddy disable <alias> --hermes
|
|
231
|
+
|
|
232
|
+
选项:
|
|
233
|
+
--root <dir> 指定原件库根目录(对所有命令生效)
|
|
234
|
+
-p, --port <port> Web 端口,默认 4173
|
|
235
|
+
--no-open 启动 Web 后不自动打开浏览器
|
|
236
|
+
--project <dir> 显式指定项目目录(enable/disable 必须明确选择 scope)
|
|
237
|
+
--global enable/disable 到用户共享的 ~/.agents/skills
|
|
238
|
+
--hermes enable/disable 到固定的 ~/.hermes/skills
|
|
239
|
+
--alias <name> enable 时指定链接 alias
|
|
240
|
+
--verbose 更新时显示新增、编辑、删除的 Skill 路径
|
|
241
|
+
|
|
242
|
+
说明:-u 只更新已登记的 Git 源;-a 只读,不会安装、启用或修改 skill。
|
|
243
|
+
`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function printWebResult(result, stdout) {
|
|
247
|
+
const status = {
|
|
248
|
+
started: 'Web 已启动',
|
|
249
|
+
'already-running': result.managed ? 'Web 已在运行' : '检测到外部 Web 已在运行',
|
|
250
|
+
stopped: 'Web 已停止',
|
|
251
|
+
'not-running': 'Web 当前未运行',
|
|
252
|
+
'not-managed': '端口上的 Web 不是由 Skillcaddy 管理,未停止',
|
|
253
|
+
restarted: 'Web 已重启'
|
|
254
|
+
}[result.status] || `Web 状态:${result.status}`;
|
|
255
|
+
stdout.write(`${status}\n`);
|
|
256
|
+
if (result.pid) stdout.write(`PID: ${result.pid}\n`);
|
|
257
|
+
if (result.url) stdout.write(`URL: ${result.url}\n`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function normalizeToken(value) {
|
|
261
|
+
return String(value || '').trim().toLowerCase();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function stateTokens(state) {
|
|
265
|
+
return [
|
|
266
|
+
...(state.skills || []),
|
|
267
|
+
...(state.enabled || []),
|
|
268
|
+
...(state.global || []),
|
|
269
|
+
...(state.hermes || [])
|
|
270
|
+
].flatMap((skill) => [skill.id, skill.name, skill.alias, skill.collection, ...(skill.tags || [])])
|
|
271
|
+
.map(normalizeToken)
|
|
272
|
+
.filter(Boolean);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function allFeaturedCollections(catalog) {
|
|
276
|
+
return Object.entries(catalog?.featured || {}).flatMap(([category, data]) =>
|
|
277
|
+
(data?.collections || []).map((collection) => ({ ...collection, category, categoryLabel: data.label }))
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function findFeaturedCollection(id, catalog) {
|
|
282
|
+
const wanted = normalizeToken(id);
|
|
283
|
+
return allFeaturedCollections(catalog).find((collection) => {
|
|
284
|
+
const candidates = [collection.id, collection.name, collection.repo];
|
|
285
|
+
return candidates.some((candidate) => {
|
|
286
|
+
const token = normalizeToken(candidate);
|
|
287
|
+
const wantedPrefix = wanted.split('-')[0];
|
|
288
|
+
return token === wanted ||
|
|
289
|
+
token.startsWith(`${wanted}-`) ||
|
|
290
|
+
wanted.startsWith(`${token}-`) ||
|
|
291
|
+
token.startsWith(`${wantedPrefix}-`);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function collectionAlreadyPresent(collection, tokens) {
|
|
297
|
+
const candidates = [collection.id, collection.name, ...(collection.skills || [])];
|
|
298
|
+
return candidates.some((candidate) => tokens.includes(normalizeToken(candidate)));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function projectSignals(context = {}) {
|
|
302
|
+
const technologies = new Set((context.technologies || []).map(normalizeToken));
|
|
303
|
+
const signals = [];
|
|
304
|
+
if (context.isCodeRepo) signals.push('development');
|
|
305
|
+
if (technologies.has('typescript') || technologies.has('javascript') || technologies.has('react') || technologies.has('vue') || technologies.has('svelte')) {
|
|
306
|
+
signals.push('development');
|
|
307
|
+
}
|
|
308
|
+
if (technologies.has('react') || technologies.has('vue') || technologies.has('svelte')) signals.push('frontend');
|
|
309
|
+
if (technologies.has('golang')) signals.push('golang');
|
|
310
|
+
return [...new Set(signals)];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function recommendationFromCollection(collection, reason) {
|
|
314
|
+
return {
|
|
315
|
+
id: collection.id,
|
|
316
|
+
name: collection.name,
|
|
317
|
+
category: collection.categoryLabel,
|
|
318
|
+
repo: collection.repo,
|
|
319
|
+
reason: reason || collection.reason
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function buildRecommendations(state, catalog = {}, context = {}) {
|
|
324
|
+
const maxRecommendations = catalog.recommendationStrategy?.maxRecommendations || 3;
|
|
325
|
+
const tokens = stateTokens(state);
|
|
326
|
+
const signals = projectSignals(context);
|
|
327
|
+
const hasEnabledSkills = (state.enabled || []).length > 0 || (state.global || []).length > 0 || (state.hermes || []).length > 0;
|
|
328
|
+
const mode = hasEnabledSkills || (state.skills || []).length > 0 && !context.isCodeRepo
|
|
329
|
+
? 'gap-based'
|
|
330
|
+
: signals.length > 0
|
|
331
|
+
? 'starter'
|
|
332
|
+
: 'discovery';
|
|
333
|
+
const recommendations = [];
|
|
334
|
+
|
|
335
|
+
if (mode === 'discovery') {
|
|
336
|
+
const ids = catalog.recommendationModes?.discovery?.recommend || ['skillsmp', 'skills-sh'];
|
|
337
|
+
for (const id of ids.slice(0, maxRecommendations)) {
|
|
338
|
+
const source = catalog.sources?.[id];
|
|
339
|
+
recommendations.push({
|
|
340
|
+
id,
|
|
341
|
+
name: id,
|
|
342
|
+
repo: source?.url,
|
|
343
|
+
reason: source?.note || '先浏览发现入口,再根据项目场景选择 skill。'
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
} else if (mode === 'starter') {
|
|
347
|
+
const scenario = signals.includes('golang')
|
|
348
|
+
? catalog.scenarios?.golang
|
|
349
|
+
: signals.includes('frontend')
|
|
350
|
+
? catalog.scenarios?.react
|
|
351
|
+
: signals.includes('development')
|
|
352
|
+
? catalog.scenarios?.typescript || catalog.scenarios?.['new-project']
|
|
353
|
+
: catalog.scenarios?.['new-project'];
|
|
354
|
+
const ids = [
|
|
355
|
+
...(scenario?.recommend?.primary || []),
|
|
356
|
+
...(scenario?.recommend?.optional || [])
|
|
357
|
+
];
|
|
358
|
+
for (const id of ids) {
|
|
359
|
+
const collection = findFeaturedCollection(id, catalog);
|
|
360
|
+
if (!collection || collectionAlreadyPresent(collection, tokens)) continue;
|
|
361
|
+
recommendations.push(recommendationFromCollection(collection, scenario.description));
|
|
362
|
+
if (recommendations.length >= maxRecommendations) break;
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
const categoryOrder = [...signals, ...Object.keys(catalog.featured || {})];
|
|
366
|
+
const seenCategories = new Set();
|
|
367
|
+
for (const category of categoryOrder) {
|
|
368
|
+
if (seenCategories.has(category)) continue;
|
|
369
|
+
seenCategories.add(category);
|
|
370
|
+
const data = catalog.featured?.[category];
|
|
371
|
+
const collection = (data?.collections || []).find((candidate) => !collectionAlreadyPresent(candidate, tokens));
|
|
372
|
+
if (!collection) continue;
|
|
373
|
+
recommendations.push(recommendationFromCollection({ ...collection, category, categoryLabel: data.label }, `当前项目可继续补充${data.label}能力。`));
|
|
374
|
+
if (recommendations.length >= maxRecommendations) break;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return { mode, signals, recommendations };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function formatCounts(items, getKey) {
|
|
382
|
+
const counts = new Map();
|
|
383
|
+
for (const item of items) {
|
|
384
|
+
const key = getKey(item) || 'unknown';
|
|
385
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
386
|
+
}
|
|
387
|
+
return [...counts.entries()].sort(([left], [right]) => left.localeCompare(right));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function buildAnalysisReport(state, catalog = {}, context = {}) {
|
|
391
|
+
const recommendation = buildRecommendations(state, catalog, context);
|
|
392
|
+
const sourceCounts = formatCounts(state.skills || [], (skill) => skill.source);
|
|
393
|
+
const enabledAliases = (state.enabled || []).map((skill) => skill.alias || skill.name).filter(Boolean);
|
|
394
|
+
const technologies = context.technologies?.length ? context.technologies.join('、') : '未识别';
|
|
395
|
+
const advice = state.advice || [];
|
|
396
|
+
const lines = [
|
|
397
|
+
'Skillcaddy 项目分析',
|
|
398
|
+
`项目:${state.projectPath}`,
|
|
399
|
+
`原件库:${state.rootDir}`,
|
|
400
|
+
...(state.codeRoot && state.codeRoot !== state.rootDir ? [`代码根:${state.codeRoot}`] : []),
|
|
401
|
+
'',
|
|
402
|
+
'当前状态:',
|
|
403
|
+
` 原件库 skill:${state.stats?.total ?? state.skills?.length ?? 0}(可用 ${state.stats?.available ?? state.skills?.length ?? 0})`,
|
|
404
|
+
` 项目 Agents 已启用:${state.enabled?.length || 0}`,
|
|
405
|
+
` 项目 Claude Code 已启用:${state.claude?.skills?.length || 0}`,
|
|
406
|
+
` 全局 skill:${state.global?.length || 0}`,
|
|
407
|
+
` Hermes skill:${state.hermes?.length || 0}`,
|
|
408
|
+
` Collection setup:${state.setups?.length || 0}`,
|
|
409
|
+
` 诊断项:${advice.length}`,
|
|
410
|
+
` 项目线索:${technologies}`,
|
|
411
|
+
''
|
|
412
|
+
];
|
|
413
|
+
|
|
414
|
+
if (sourceCounts.length > 0) {
|
|
415
|
+
lines.push('原件来源:');
|
|
416
|
+
for (const [source, count] of sourceCounts) lines.push(` ${source}:${count}`);
|
|
417
|
+
lines.push('');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
lines.push('项目已启用:');
|
|
421
|
+
lines.push(enabledAliases.length > 0 ? ` ${enabledAliases.join('、')}` : ' 暂无');
|
|
422
|
+
lines.push('', '诊断汇报:');
|
|
423
|
+
if (advice.length === 0) {
|
|
424
|
+
lines.push(' 未发现需要立即处理的诊断项。');
|
|
425
|
+
} else {
|
|
426
|
+
for (const item of advice.slice(0, 8)) {
|
|
427
|
+
lines.push(` [${item.severity || 'info'}] ${item.title}`);
|
|
428
|
+
if (item.detail) lines.push(` ${item.detail}`);
|
|
429
|
+
}
|
|
430
|
+
if (advice.length > 8) lines.push(` ……还有 ${advice.length - 8} 项,TUI/Web 中可查看完整诊断。`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
lines.push('', `推荐模式:${catalog.recommendationModes?.[recommendation.mode]?.label || recommendation.mode}`);
|
|
434
|
+
lines.push('推荐(最多 3 个,仅供参考,不会自动安装):');
|
|
435
|
+
if (recommendation.recommendations.length === 0) {
|
|
436
|
+
lines.push(' 暂无足够上下文生成具体推荐;可先补充项目场景或浏览推荐平台。');
|
|
437
|
+
} else {
|
|
438
|
+
recommendation.recommendations.forEach((item, index) => {
|
|
439
|
+
lines.push(` ${index + 1}. ${item.name}`);
|
|
440
|
+
if (item.category) lines.push(` 分类:${item.category}`);
|
|
441
|
+
if (item.repo) lines.push(` 入口:${item.repo}`);
|
|
442
|
+
if (item.reason) lines.push(` 理由:${item.reason}`);
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return `${lines.join('\n')}\n`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function detectProjectContext(projectPath) {
|
|
450
|
+
const context = { isCodeRepo: false, technologies: [] };
|
|
451
|
+
const add = (value) => {
|
|
452
|
+
if (value && !context.technologies.includes(value)) context.technologies.push(value);
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
const packageJson = JSON.parse(await readFile(path.join(projectPath, 'package.json'), 'utf8'));
|
|
457
|
+
context.isCodeRepo = true;
|
|
458
|
+
add('Node.js');
|
|
459
|
+
const dependencyNames = Object.keys({ ...packageJson.dependencies, ...packageJson.devDependencies });
|
|
460
|
+
if (dependencyNames.some((name) => name === 'typescript' || name === 'tsx' || name === 'ts-node')) add('TypeScript');
|
|
461
|
+
if (dependencyNames.some((name) => name === 'react' || name === 'react-dom')) add('React');
|
|
462
|
+
if (dependencyNames.some((name) => name === 'vue')) add('Vue');
|
|
463
|
+
if (dependencyNames.some((name) => name === 'svelte')) add('Svelte');
|
|
464
|
+
} catch {
|
|
465
|
+
// Other project markers below still provide useful signals.
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const markers = [
|
|
469
|
+
['go.mod', 'Golang'],
|
|
470
|
+
['Cargo.toml', 'Rust'],
|
|
471
|
+
['pyproject.toml', 'Python'],
|
|
472
|
+
['tsconfig.json', 'TypeScript']
|
|
473
|
+
];
|
|
474
|
+
for (const [file, technology] of markers) {
|
|
475
|
+
try {
|
|
476
|
+
await access(path.join(projectPath, file));
|
|
477
|
+
context.isCodeRepo = true;
|
|
478
|
+
add(technology);
|
|
479
|
+
} catch {
|
|
480
|
+
// Marker is optional.
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return context;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* 精选合集目录属于代码根:它随发行形态(clone / npm 包)一起走,
|
|
488
|
+
* 不属于原件库,因此不参与 library image 的搬运。
|
|
489
|
+
*/
|
|
490
|
+
async function loadCatalog(codeRoot) {
|
|
491
|
+
try {
|
|
492
|
+
return JSON.parse(await readFile(
|
|
493
|
+
path.join(codeRoot, 'skills', 'skillcaddy-manager', 'references', 'featured-skills.json'),
|
|
494
|
+
'utf8'
|
|
495
|
+
));
|
|
496
|
+
} catch {
|
|
497
|
+
return {};
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export async function runCli({
|
|
502
|
+
argv = [],
|
|
503
|
+
rootDir,
|
|
504
|
+
runtimeRootDir,
|
|
505
|
+
codeRoot,
|
|
506
|
+
stdout = process.stdout,
|
|
507
|
+
stderr = process.stderr,
|
|
508
|
+
handlers = {}
|
|
509
|
+
} = {}) {
|
|
510
|
+
const parsed = parseCliArgs(argv);
|
|
511
|
+
if (parsed.error) {
|
|
512
|
+
stderr.write(`错误:${parsed.error}\n\n`);
|
|
513
|
+
stderr.write(buildHelpText());
|
|
514
|
+
return 2;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// 代码根:提供可执行文件的 clone / npm 包目录。
|
|
518
|
+
const resolvedCodeRoot = path.resolve(codeRoot || runtimeRootDir || rootDir || process.cwd());
|
|
519
|
+
// 原件库根:--root 显式指定 > 调用方传入 > 由代码根推导(checkout 用代码根,
|
|
520
|
+
// 发行形态用 ~/.skillcaddy/library)。
|
|
521
|
+
const resolvedLibraryRoot = path.resolve(
|
|
522
|
+
parsed.rootDir || rootDir || resolveLibraryRoot({ codeRoot: resolvedCodeRoot })
|
|
523
|
+
);
|
|
524
|
+
const roots = { libraryRoot: resolvedLibraryRoot, codeRoot: resolvedCodeRoot };
|
|
525
|
+
const projectPath = path.resolve(parsed.projectPath || process.cwd());
|
|
526
|
+
const actions = {
|
|
527
|
+
readVersion,
|
|
528
|
+
startWeb,
|
|
529
|
+
stopWeb,
|
|
530
|
+
restartWeb,
|
|
531
|
+
runSourceCli,
|
|
532
|
+
installCliCommand,
|
|
533
|
+
getState,
|
|
534
|
+
scanSkills,
|
|
535
|
+
enableProjectSkill,
|
|
536
|
+
disableSkill,
|
|
537
|
+
loadCatalog,
|
|
538
|
+
detectProjectContext,
|
|
539
|
+
...handlers
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
if (parsed.command === 'help') {
|
|
544
|
+
stdout.write(buildHelpText());
|
|
545
|
+
return 0;
|
|
546
|
+
}
|
|
547
|
+
if (parsed.command === 'version') {
|
|
548
|
+
const version = await actions.readVersion(roots);
|
|
549
|
+
stdout.write(`${version.name} v${version.version}\n`);
|
|
550
|
+
return 0;
|
|
551
|
+
}
|
|
552
|
+
if (parsed.command === 'install') {
|
|
553
|
+
const result = await actions.installCliCommand(resolvedCodeRoot);
|
|
554
|
+
stdout.write(`${result.message}\n`);
|
|
555
|
+
stdout.write(`source: ${result.sourcePath}\n`);
|
|
556
|
+
stdout.write(`target: ${result.targetPath}\n`);
|
|
557
|
+
if (result.command) stdout.write(`command: ${result.command}\n`);
|
|
558
|
+
return 0;
|
|
559
|
+
}
|
|
560
|
+
if (parsed.command === 'update') {
|
|
561
|
+
return actions.runSourceCli({
|
|
562
|
+
argv: ['update-git', ...(parsed.verbose ? ['--verbose'] : []), '--project', projectPath],
|
|
563
|
+
rootDir: resolvedLibraryRoot,
|
|
564
|
+
projectPath,
|
|
565
|
+
stdout,
|
|
566
|
+
stderr
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
if (parsed.command === 'analyze') {
|
|
570
|
+
const [state, catalog, context] = await Promise.all([
|
|
571
|
+
actions.getState(roots, projectPath, { readOnly: true }),
|
|
572
|
+
actions.loadCatalog(resolvedCodeRoot),
|
|
573
|
+
actions.detectProjectContext(projectPath)
|
|
574
|
+
]);
|
|
575
|
+
stdout.write(buildAnalysisReport(state, catalog, context));
|
|
576
|
+
return 0;
|
|
577
|
+
}
|
|
578
|
+
if (parsed.command === 'enable' || parsed.command === 'disable') {
|
|
579
|
+
return runEnablementCommand({ actions, parsed, roots, stdout });
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const lifecycleOptions = {
|
|
583
|
+
codeRoot: resolvedCodeRoot,
|
|
584
|
+
libraryRoot: resolvedLibraryRoot,
|
|
585
|
+
projectPath,
|
|
586
|
+
port: parsed.port,
|
|
587
|
+
open: parsed.open
|
|
588
|
+
};
|
|
589
|
+
if (parsed.command === 'start') {
|
|
590
|
+
printWebResult(await actions.startWeb(lifecycleOptions), stdout);
|
|
591
|
+
return 0;
|
|
592
|
+
}
|
|
593
|
+
if (parsed.command === 'stop') {
|
|
594
|
+
printWebResult(await actions.stopWeb(lifecycleOptions), stdout);
|
|
595
|
+
return 0;
|
|
596
|
+
}
|
|
597
|
+
if (parsed.command === 'restart') {
|
|
598
|
+
printWebResult(await actions.restartWeb(lifecycleOptions), stdout);
|
|
599
|
+
return 0;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
stderr.write(buildHelpText());
|
|
603
|
+
return 2;
|
|
604
|
+
} catch (error) {
|
|
605
|
+
stderr.write(`错误:${error.message}\n`);
|
|
606
|
+
return error.exitCode || 1;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async function runEnablementCommand({ actions, parsed, roots, stdout }) {
|
|
611
|
+
if (parsed.command === 'enable') {
|
|
612
|
+
const skills = await actions.scanSkills(roots);
|
|
613
|
+
const skill = resolveSkill(skills, parsed.skillRef);
|
|
614
|
+
const input = {
|
|
615
|
+
scope: parsed.scope,
|
|
616
|
+
...(parsed.scope === 'project' ? { projectPath: path.resolve(parsed.projectPath) } : {}),
|
|
617
|
+
skillPath: skill.path,
|
|
618
|
+
alias: parsed.alias || skill.name
|
|
619
|
+
};
|
|
620
|
+
const result = await actions.enableProjectSkill(roots, input);
|
|
621
|
+
const status = result.unchanged ? '已存在' : '已启用';
|
|
622
|
+
stdout.write(`${scopeLabel(parsed.scope)}${status}:${result.alias}\n`);
|
|
623
|
+
return 0;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const result = await actions.disableSkill({
|
|
627
|
+
scope: parsed.scope,
|
|
628
|
+
roots,
|
|
629
|
+
...(parsed.scope === 'project' ? { projectPath: path.resolve(parsed.projectPath) } : {}),
|
|
630
|
+
alias: parsed.alias
|
|
631
|
+
});
|
|
632
|
+
stdout.write(`${scopeLabel(parsed.scope)}${result.removed ? '已清理' : '未找到'}:${result.alias}\n`);
|
|
633
|
+
return 0;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function scopeLabel(scope) {
|
|
637
|
+
return { project: '项目', global: '全局', hermes: 'Hermes' }[scope] || scope;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function resolveSkill(skills, value) {
|
|
641
|
+
const byId = skills.find((skill) => skill.id === value);
|
|
642
|
+
if (byId) return byId;
|
|
643
|
+
const byName = skills.filter((skill) => skill.name === value);
|
|
644
|
+
if (byName.length === 1) return byName[0];
|
|
645
|
+
if (byName.length > 1) throw new Error(`存在多个同名 skill:${value},请使用完整 id`);
|
|
646
|
+
throw new Error(`找不到 skill:${value}`);
|
|
647
|
+
}
|