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.
Files changed (99) hide show
  1. package/CHANGELOG.md +497 -0
  2. package/CONTEXT.md +149 -0
  3. package/LICENSE +21 -0
  4. package/README.md +499 -0
  5. package/README_CN.md +482 -0
  6. package/collection-metadata/github/mattpocock.json +16 -0
  7. package/lib/canonicalJson.js +11 -0
  8. package/lib/claudeStore.js +135 -0
  9. package/lib/collectionEnablement.js +115 -0
  10. package/lib/collectionSetup.js +199 -0
  11. package/lib/enablePlan.js +116 -0
  12. package/lib/enablementScope.js +8 -0
  13. package/lib/hermesStore.js +21 -0
  14. package/lib/libraryImage.js +1785 -0
  15. package/lib/managerInstall.js +109 -0
  16. package/lib/managerSourceWorkflow.js +234 -0
  17. package/lib/projectActions.js +24 -0
  18. package/lib/projectPath.js +51 -0
  19. package/lib/roots.js +99 -0
  20. package/lib/skillLinkStore.js +68 -0
  21. package/lib/skillMetadata.js +191 -0
  22. package/lib/skillStore.js +541 -0
  23. package/lib/sourceAcquisition.js +475 -0
  24. package/lib/sourceAcquisitionAdapter.js +47 -0
  25. package/lib/sourceAcquisitionError.js +18 -0
  26. package/lib/sourceAcquisitionPolicy.js +139 -0
  27. package/lib/sourceArchive.js +363 -0
  28. package/lib/sourceArchiveWorkspace.js +36 -0
  29. package/lib/sourceGit.js +95 -0
  30. package/lib/sourceGitAcquisitionAdapter.js +145 -0
  31. package/lib/sourceGitBatchUpdate.js +168 -0
  32. package/lib/sourceGitRepair.js +227 -0
  33. package/lib/sourceGitStaging.js +139 -0
  34. package/lib/sourceGitUpgradeAdapter.js +583 -0
  35. package/lib/sourceGitUrl.js +170 -0
  36. package/lib/sourceHttp.js +302 -0
  37. package/lib/sourceLocal.js +200 -0
  38. package/lib/sourceLocalAcquisitionAdapter.js +107 -0
  39. package/lib/sourceManager.js +242 -0
  40. package/lib/sourceMigration.js +172 -0
  41. package/lib/sourceMigrationIssue.js +6 -0
  42. package/lib/sourcePath.js +6 -0
  43. package/lib/sourcePolicy.js +53 -0
  44. package/lib/sourceRecord.js +295 -0
  45. package/lib/sourceRegistry.js +155 -0
  46. package/lib/sourceRemoteArchiveAcquisitionAdapter.js +147 -0
  47. package/lib/sourceRemoteFileAcquisitionAdapter.js +95 -0
  48. package/lib/sourceRemoteFilePolicy.js +62 -0
  49. package/lib/sourceRemoteFileWorkspace.js +55 -0
  50. package/lib/sourceSkillDiscovery.js +117 -0
  51. package/lib/sourceTree.js +185 -0
  52. package/lib/sourceUpgrade.js +596 -0
  53. package/lib/sourceUpgradeAdapter.js +41 -0
  54. package/lib/sourceUpgradePolicy.js +170 -0
  55. package/lib/sourceUpgradeTransaction.js +202 -0
  56. package/lib/sourceValidation.js +120 -0
  57. package/lib/sourceWorkspace.js +37 -0
  58. package/lib/specGate.js +235 -0
  59. package/lib/tuiActions.js +169 -0
  60. package/lib/tuiInstall.js +133 -0
  61. package/lib/tuiLayout.js +94 -0
  62. package/lib/version.js +17 -0
  63. package/lib/webManager.js +370 -0
  64. package/package.json +74 -0
  65. package/public/agentsUi.js +138 -0
  66. package/public/app.js +1431 -0
  67. package/public/claude.css +2 -0
  68. package/public/claudeUi.js +119 -0
  69. package/public/emptyState.js +12 -0
  70. package/public/enabled.css +303 -0
  71. package/public/favicon.svg +5 -0
  72. package/public/index.html +199 -0
  73. package/public/skillcaddy_CN.png +0 -0
  74. package/public/skillcaddy_EN.png +0 -0
  75. package/public/styles.css +1611 -0
  76. package/scripts/cli.js +647 -0
  77. package/scripts/cron-sync-update-git.sh +128 -0
  78. package/scripts/pull-github.sh +22 -0
  79. package/scripts/source.js +638 -0
  80. package/scripts/tui.js +1058 -0
  81. package/server.js +196 -0
  82. package/skills/skillcaddy-manager/SKILL.md +93 -0
  83. package/skills/skillcaddy-manager/agents/openai.yaml +7 -0
  84. package/skills/skillcaddy-manager/references/METADATA.md +75 -0
  85. package/skills/skillcaddy-manager/references/OPERATIONS.md +315 -0
  86. package/skills/skillcaddy-manager/references/RECOMMENDATION_GUIDE.md +51 -0
  87. package/skills/skillcaddy-manager/references/featured-skills.json +564 -0
  88. package/skills/skillcaddy-manager/references/skill-platforms.json +48 -0
  89. package/skills/skillcaddy-manager/scripts/check-conflicts.cjs +117 -0
  90. package/skills/skillcaddy-manager/scripts/check-global-skills.cjs +169 -0
  91. package/skills/skillcaddy-manager/scripts/translate-skill-notes.cjs +297 -0
  92. package/skills/skillcaddy-manager/scripts/version-manager.cjs +146 -0
  93. package/skills/skillcaddy-manager/scripts/view-recommendations.cjs +279 -0
  94. package/skills/skillcaddy-manager/skillcaddy.json +9 -0
  95. package/skills/spec-pipeline/spec-direction-gate/SKILL.md +119 -0
  96. package/skills/spec-pipeline/spec-direction-gate/check-approval.py +110 -0
  97. package/skills/spec-pipeline/spec-draft-intake/SKILL.md +116 -0
  98. package/skills/spec-pipeline/spec-role-contract/SKILL.md +113 -0
  99. package/skills/spec-pipeline/spec-role-contract/check-roles.py +108 -0
@@ -0,0 +1,541 @@
1
+ import {
2
+ lstat,
3
+ mkdir,
4
+ readdir,
5
+ readFile,
6
+ rm,
7
+ symlink
8
+ } from 'node:fs/promises';
9
+ import { homedir } from 'node:os';
10
+ import path from 'node:path';
11
+ import { getClaudeStatus } from './claudeStore.js';
12
+ import { buildCollectionSetupAdvice, scanCollectionSetups } from './collectionSetup.js';
13
+ import { getHermesSkillsDir, isHermesEligibleSource, scanHermesSkills } from './hermesStore.js';
14
+ import { normalizeProjectPath, requirePath } from './projectPath.js';
15
+ import { normalizeRoots } from './roots.js';
16
+ import { readSkillMetadata } from './skillMetadata.js';
17
+ import {
18
+ assertInsideAllowedSkillSources,
19
+ isInsideAllowedSkillSources,
20
+ REPOSITORY_SKILLS_FOLDER,
21
+ REPOSITORY_SKILLS_SOURCE,
22
+ resolveAllowedSkillSource,
23
+ SOURCE_FOLDERS,
24
+ SOURCE_INSTALLING_MARKER,
25
+ SOURCE_INSTALLING_MARKER_CONTENT
26
+ } from './sourcePolicy.js';
27
+ import { discoverSourceSkillDirectories } from './sourceSkillDiscovery.js';
28
+ import { readSourceRecords } from './sourceRegistry.js';
29
+ import { normalizeEnablementScope } from './enablementScope.js';
30
+ import {
31
+ linkPathExists,
32
+ resolveLinkTarget,
33
+ scanSkillLinks
34
+ } from './skillLinkStore.js';
35
+
36
+ export async function ensureSourceFolders(roots) {
37
+ const { libraryRoot, codeRoot } = normalizeRoots(roots);
38
+
39
+ await Promise.all(
40
+ SOURCE_FOLDERS.map((source) => mkdir(path.join(libraryRoot, source), { recursive: true }))
41
+ );
42
+
43
+ // 仓库自带 skills 桶属于代码根。发行形态(npm 包)里它已经存在,
44
+ // 只读安装目录下创建失败也不应该阻断启动,所以这里吞掉创建错误。
45
+ await mkdir(path.join(codeRoot, REPOSITORY_SKILLS_FOLDER), { recursive: true }).catch(() => {});
46
+ }
47
+
48
+ export async function getState(roots, projectPath, { readOnly = false, globalDir, hermesDir } = {}) {
49
+ const resolvedRoots = normalizeRoots(roots);
50
+ const { libraryRoot } = resolvedRoots;
51
+ const safeProjectPath = normalizeProjectPath(projectPath);
52
+ if (!readOnly) await ensureSourceFolders(resolvedRoots);
53
+ const skills = await scanSkills(resolvedRoots);
54
+ const enabled = await scanEnabledSkills(safeProjectPath);
55
+ const global = (await scanGlobalSkills({ globalDir })).map((skill) => ({
56
+ ...skill,
57
+ canDisable: skill.isSymlink && isInsideAllowedSkillSources(resolvedRoots, skill.targetPath)
58
+ }));
59
+ const hermes = (await scanHermesSkills({ hermesDir })).map((skill) => ({
60
+ ...skill,
61
+ canDisable: skill.isSymlink && isInsideAllowedSkillSources(resolvedRoots, skill.targetPath)
62
+ }));
63
+ const setups = await scanCollectionSetups(resolvedRoots, safeProjectPath, skills, enabled);
64
+ const advice = buildSkillAdvice(resolvedRoots, { skills, enabled, global, hermes, setups });
65
+ const sources = await readSourceRecords(libraryRoot);
66
+
67
+ return {
68
+ rootDir: libraryRoot,
69
+ libraryRoot,
70
+ codeRoot: resolvedRoots.codeRoot,
71
+ projectPath: safeProjectPath,
72
+ skills,
73
+ enabled,
74
+ global,
75
+ hermes,
76
+ setups,
77
+ advice,
78
+ sources: buildSourcesInfo(sources),
79
+ claude: await getClaudeStatus(safeProjectPath),
80
+ stats: { total: skills.length, enabled: enabled.length, available: skills.filter((skill) => skill.source !== 'archived').length }
81
+ };
82
+ }
83
+
84
+ export async function scanSkills(roots) {
85
+ const resolvedRoots = normalizeRoots(roots);
86
+ const groups = await Promise.all([
87
+ scanRepositorySkills(resolvedRoots),
88
+ ...SOURCE_FOLDERS.map((source) => scanSourceFolder(resolvedRoots, path.join(resolvedRoots.libraryRoot, source), source))
89
+ ]);
90
+
91
+ return groups.flat().sort(compareSkills);
92
+ }
93
+
94
+ function compareSkills(left, right) {
95
+ return left.collection.localeCompare(right.collection) || left.name.localeCompare(right.name) || left.id.localeCompare(right.id);
96
+ }
97
+
98
+ async function scanRepositorySkills(roots) {
99
+ const sourceDir = path.join(roots.codeRoot, REPOSITORY_SKILLS_FOLDER);
100
+ return scanSourceFolder(roots, sourceDir, REPOSITORY_SKILLS_SOURCE);
101
+ }
102
+
103
+ async function scanSourceFolder(roots, sourceDir, source) {
104
+ const entries = await safeReaddir(sourceDir);
105
+ const skillGroups = await Promise.all(
106
+ entries
107
+ .filter((entry) => entry.isDirectory())
108
+ .map((entry) => scanSourceEntry(roots, source, sourceDir, entry.name))
109
+ );
110
+ return skillGroups.flat();
111
+ }
112
+
113
+ export async function scanEnabledSkills(projectPath) {
114
+ return scanSkillLinks(getAgentsSkillsDir(normalizeProjectPath(projectPath)));
115
+ }
116
+
117
+ export async function scanGlobalSkills({ globalDir } = {}) {
118
+ const directory = getGlobalSkillsDir(globalDir);
119
+ const skills = await scanSkillLinks(directory);
120
+ return skills
121
+ .map((skill) => ({ ...skill, agent: 'Agents', scope: 'global', directory }))
122
+ .sort((left, right) => left.alias.localeCompare(right.alias));
123
+ }
124
+
125
+ function buildSkillAdvice(roots, { skills, enabled, global, hermes, setups }) {
126
+ const advice = buildCollectionSetupAdvice(setups);
127
+ const availableByName = groupBy(skills.filter((skill) => skill.source !== 'archived'), (skill) => skill.name);
128
+ const projectByAlias = new Map(enabled.map((skill) => [skill.alias, skill]));
129
+ const globalByAlias = groupBy(global, (skill) => skill.alias);
130
+ const hermesByAlias = groupBy(hermes, (skill) => skill.alias);
131
+
132
+ for (const skill of enabled) {
133
+ if (!skill.isSymlink) {
134
+ advice.push({
135
+ severity: 'warning',
136
+ type: 'project-unmanaged-entry',
137
+ title: `项目存在非软链接 skill:${skill.alias}`,
138
+ detail: `${skill.linkPath} 不是 Skillcaddy 可安全清理的软链接。启用同名 skill 前请先确认该目录用途。`,
139
+ alias: skill.alias,
140
+ path: skill.linkPath
141
+ });
142
+ continue;
143
+ }
144
+
145
+ if (!skill.exists) {
146
+ advice.push({
147
+ severity: 'warning',
148
+ type: 'project-broken-link',
149
+ title: `项目 skill 断链:${skill.alias}`,
150
+ detail: `${skill.linkPath} 指向不存在的目标:${skill.targetPath}`,
151
+ alias: skill.alias,
152
+ path: skill.linkPath,
153
+ targetPath: skill.targetPath
154
+ });
155
+ continue;
156
+ }
157
+
158
+ if (!isInsideAllowedSkillSources(roots, skill.targetPath)) {
159
+ advice.push({
160
+ severity: 'info',
161
+ type: 'project-unmanaged-symlink',
162
+ title: `项目存在不纳管 skill:${skill.alias}`,
163
+ detail: `${skill.alias} 指向 Skillcaddy 来源目录之外:${skill.targetPath}`,
164
+ alias: skill.alias,
165
+ path: skill.linkPath,
166
+ targetPath: skill.targetPath
167
+ });
168
+ }
169
+ }
170
+
171
+ for (const [alias, globalMatches] of globalByAlias) {
172
+ const projectSkill = projectByAlias.get(alias);
173
+ if (projectSkill) {
174
+ advice.push({
175
+ severity: 'info',
176
+ type: 'global-shadowed-by-project',
177
+ title: `项目 skill 会遮蔽全局 skill:${alias}`,
178
+ detail: `${alias} 同时存在于项目和全局目录。通常项目级优先,若行为异常请确认 agent 的加载顺序。`,
179
+ alias,
180
+ global: globalMatches.map(formatSkillLocation),
181
+ project: formatSkillLocation(projectSkill)
182
+ });
183
+ }
184
+
185
+ const availableMatches = availableByName.get(alias) || [];
186
+ for (const availableSkill of availableMatches) {
187
+ if (projectSkill?.targetPath === availableSkill.path) continue;
188
+ if (globalMatches.some((skill) => skill.targetPath === availableSkill.path)) continue;
189
+ advice.push({
190
+ severity: 'info',
191
+ type: 'global-alias-conflict',
192
+ title: `全局已存在同名 skill:${alias}`,
193
+ detail: `启用 ${availableSkill.source}/${availableSkill.collection}/${availableSkill.name} 到项目后,会与全局 ${alias} 同名。`,
194
+ alias,
195
+ skillId: availableSkill.id,
196
+ global: globalMatches.map(formatSkillLocation)
197
+ });
198
+ }
199
+ }
200
+
201
+ for (const skill of hermes) {
202
+ const matchingSource = skills.find((candidate) => candidate.path === skill.targetPath);
203
+ const managedTarget = skill.isSymlink && isInsideAllowedSkillSources(roots, skill.targetPath);
204
+ if (!matchingSource && !managedTarget) continue;
205
+
206
+ if (!skill.isSymlink) {
207
+ advice.push({
208
+ severity: 'warning',
209
+ type: 'hermes-unmanaged-entry',
210
+ title: `Hermes 存在非软链接 skill:${skill.alias}`,
211
+ detail: `${skill.linkPath} 不是 Skillcaddy 可安全清理的软链接。启用同名 Hermes skill 前请先确认该条目用途。`,
212
+ alias: skill.alias,
213
+ path: skill.linkPath,
214
+ scope: 'hermes'
215
+ });
216
+ continue;
217
+ }
218
+
219
+ if (!skill.exists) {
220
+ advice.push({
221
+ severity: 'warning',
222
+ type: 'hermes-broken-link',
223
+ title: `Hermes skill 断链:${skill.alias}`,
224
+ detail: `${skill.linkPath} 指向不存在的目标:${skill.targetPath}`,
225
+ alias: skill.alias,
226
+ path: skill.linkPath,
227
+ targetPath: skill.targetPath,
228
+ scope: 'hermes'
229
+ });
230
+ continue;
231
+ }
232
+
233
+ if (!isInsideAllowedSkillSources(roots, skill.targetPath)) {
234
+ advice.push({
235
+ severity: 'info',
236
+ type: 'hermes-unmanaged-symlink',
237
+ title: `Hermes 存在不纳管 skill:${skill.alias}`,
238
+ detail: `${skill.alias} 指向当前 Skillcaddy 来源目录之外:${skill.targetPath}`,
239
+ alias: skill.alias,
240
+ path: skill.linkPath,
241
+ targetPath: skill.targetPath,
242
+ scope: 'hermes'
243
+ });
244
+ }
245
+ }
246
+
247
+ for (const [name, matches] of availableByName) {
248
+ if (matches.length <= 1) continue;
249
+ const reservedAliases = new Set([
250
+ ...projectByAlias.keys(),
251
+ ...globalByAlias.keys(),
252
+ ...hermesByAlias.keys()
253
+ ]);
254
+ const skillsWithAliases = matches.map((skill) => ({
255
+ id: skill.id,
256
+ source: skill.source,
257
+ collection: skill.collection,
258
+ path: skill.path,
259
+ suggestedAlias: suggestUniqueAlias(skill, reservedAliases)
260
+ }));
261
+ advice.push({
262
+ severity: 'info',
263
+ type: 'library-duplicate-name',
264
+ title: `原件库存在同名 skill:${name}`,
265
+ detail: `启用时需要按来源确认;可在 skill 详情中使用建议名称,避免默认 alias ${name} 指向非预期 skill。`,
266
+ alias: name,
267
+ skills: skillsWithAliases,
268
+ actions: skillsWithAliases.map((skill) => ({
269
+ type: 'enable-with-alias',
270
+ skillId: skill.id,
271
+ alias: skill.suggestedAlias
272
+ }))
273
+ });
274
+ }
275
+
276
+ return advice.sort((left, right) => severityRank(right.severity) - severityRank(left.severity) || left.title.localeCompare(right.title));
277
+ }
278
+
279
+ function suggestUniqueAlias(skill, reservedAliases) {
280
+ const prefix = skill.collection === skill.name ? skill.source : skill.collection;
281
+ const base = normalizeSuggestedAlias(`${prefix}-${skill.name}`) || normalizeSuggestedAlias(`${skill.source}-${skill.name}`) || 'skill';
282
+ let alias = base;
283
+ let suffix = 2;
284
+ while (reservedAliases.has(alias)) {
285
+ alias = `${base}-${suffix}`;
286
+ suffix += 1;
287
+ }
288
+ reservedAliases.add(alias);
289
+ return alias;
290
+ }
291
+
292
+ function normalizeSuggestedAlias(value) {
293
+ return String(value || '')
294
+ .trim()
295
+ .replace(/[^a-zA-Z0-9._-]+/g, '-')
296
+ .replace(/^-+|-+$/g, '');
297
+ }
298
+
299
+ function groupBy(items, getKey) {
300
+ const groups = new Map();
301
+ for (const item of items) {
302
+ const key = getKey(item);
303
+ if (!groups.has(key)) groups.set(key, []);
304
+ groups.get(key).push(item);
305
+ }
306
+ return groups;
307
+ }
308
+
309
+ function formatSkillLocation(skill) {
310
+ return {
311
+ alias: skill.alias,
312
+ path: skill.linkPath,
313
+ targetPath: skill.targetPath,
314
+ isSymlink: skill.isSymlink,
315
+ exists: skill.exists,
316
+ agent: skill.agent
317
+ };
318
+ }
319
+
320
+ function severityRank(severity) {
321
+ return { warning: 2, info: 1 }[severity] || 0;
322
+ }
323
+
324
+ export async function enableSkill(roots, input) {
325
+ const resolvedRoots = normalizeRoots(roots);
326
+ const scope = normalizeEnablementScope(input.scope);
327
+ const skillPath = requirePath(input.skillPath, 'skillPath');
328
+ const alias = normalizeAlias(input.alias || path.basename(skillPath));
329
+ const resolvedSkillPath = path.resolve(skillPath);
330
+
331
+ assertInsideSources(resolvedRoots, resolvedSkillPath);
332
+ await assertDirectory(resolvedSkillPath, 'skillPath');
333
+ assertHermesEligible(resolvedRoots, resolvedSkillPath, scope);
334
+
335
+ const skillsDir = getScopeSkillsDir(scope, input);
336
+ await mkdir(skillsDir, { recursive: true });
337
+
338
+ const linkPath = path.join(skillsDir, alias);
339
+ if (await linkPathExists(linkPath)) {
340
+ const stat = await lstat(linkPath);
341
+ if (!stat.isSymbolicLink()) {
342
+ throw new Error(`目标已存在且不是软链接:${linkPath}`);
343
+ }
344
+
345
+ const existingTarget = await resolveLinkTarget(linkPath);
346
+ if (existingTarget !== resolvedSkillPath) {
347
+ throw new Error(`别名已指向其他 skill:${existingTarget}`);
348
+ }
349
+
350
+ return { ok: true, scope, alias, linkPath, targetPath: resolvedSkillPath, unchanged: true };
351
+ }
352
+
353
+ await symlink(resolvedSkillPath, linkPath, 'dir');
354
+ return { ok: true, scope, alias, linkPath, targetPath: resolvedSkillPath, unchanged: false };
355
+ }
356
+
357
+ export async function disableSkill(input) {
358
+ const scope = normalizeEnablementScope(input.scope);
359
+ const alias = normalizeAlias(input.alias);
360
+ const linkPath = path.join(getScopeSkillsDir(scope, input), alias);
361
+
362
+ if (!(await linkPathExists(linkPath))) {
363
+ return { ok: true, scope, alias, linkPath, removed: false };
364
+ }
365
+
366
+ const stat = await lstat(linkPath);
367
+ if (!stat.isSymbolicLink()) {
368
+ throw new Error(`拒绝删除非软链接:${linkPath}`);
369
+ }
370
+
371
+ if (scope === 'global' || scope === 'hermes') {
372
+ const roots = resolveDisableRoots(input);
373
+ if (!roots) {
374
+ throw new Error(`${scope} disable requires rootDir`);
375
+ }
376
+ const targetPath = await resolveLinkTarget(linkPath);
377
+ if (!isInsideAllowedSkillSources(roots, targetPath)) {
378
+ throw new Error(`拒绝清理不属于当前 Skillcaddy 原件库的 ${scope} 软链接:${targetPath}`);
379
+ }
380
+ }
381
+
382
+ await rm(linkPath);
383
+ return { ok: true, scope, alias, linkPath, removed: true };
384
+ }
385
+
386
+ export function getGlobalSkillsDir(globalDir) {
387
+ return path.resolve(globalDir || path.join(homedir(), '.agents', 'skills'));
388
+ }
389
+
390
+ function getScopeSkillsDir(scope, input) {
391
+ if (scope === 'global') return getGlobalSkillsDir(input.globalDir);
392
+ if (scope === 'hermes') return getHermesSkillsDir(input.hermesDir);
393
+ return getAgentsSkillsDir(normalizeProjectPath(input.projectPath));
394
+ }
395
+
396
+ function assertHermesEligible(roots, skillPath, scope) {
397
+ if (scope !== 'hermes') return;
398
+ const source = resolveAllowedSkillSource(roots, skillPath);
399
+ if (!source || !isHermesEligibleSource(source.sourceFolder)) {
400
+ throw new Error('Hermes 只允许启用 official、github 或 personal 来源的 skill');
401
+ }
402
+ }
403
+
404
+ function getAgentsSkillsDir(projectPath) {
405
+ return path.join(normalizeProjectPath(projectPath), '.agents', 'skills');
406
+ }
407
+
408
+ async function scanSourceEntry(roots, source, sourceDir, entryName) {
409
+ const entryPath = path.join(sourceDir, entryName);
410
+ if (await sourcePublicationInProgress(entryPath)) return [];
411
+ const skillDirs = await discoverSourceSkillDirectories(entryPath);
412
+ if (await sourcePublicationInProgress(entryPath)) return [];
413
+ return Promise.all(
414
+ skillDirs.map((skillPath) => {
415
+ const relative = path.relative(sourceDir, skillPath);
416
+ const name = relative === `${entryName}/skills` ? entryName : path.basename(skillPath);
417
+ return buildSkillRecord(roots, source, sourceDir, skillPath, name, entryName);
418
+ })
419
+ );
420
+ }
421
+
422
+ async function sourcePublicationInProgress(sourcePath) {
423
+ try {
424
+ return await readFile(
425
+ path.join(sourcePath, SOURCE_INSTALLING_MARKER),
426
+ 'utf8'
427
+ ) === SOURCE_INSTALLING_MARKER_CONTENT;
428
+ } catch {
429
+ return false;
430
+ }
431
+ }
432
+
433
+ async function buildSkillRecord(roots, source, sourceDir, skillPath, name, collection) {
434
+ const skillFile = path.join(skillPath, 'SKILL.md');
435
+ const relativePath = path.relative(sourceDir, skillPath);
436
+ const [description, metadata] = await Promise.all([
437
+ readSkillDescription(skillFile),
438
+ readSkillMetadata(roots, skillPath)
439
+ ]);
440
+ const collectionPath = path.join(sourceDir, collection);
441
+
442
+ return {
443
+ id: `${source}/${relativePath}`,
444
+ source,
445
+ collection,
446
+ collectionPath,
447
+ name,
448
+ path: skillPath,
449
+ relativePath,
450
+ hasSkillFile: true,
451
+ description,
452
+ note: metadata.note,
453
+ tags: metadata.tags,
454
+ autoEnable: metadata.autoEnable,
455
+ metadataPath: metadata.metadataPath,
456
+ metadataStorage: metadata.metadataStorage,
457
+ hasMetadata: metadata.hasMetadata,
458
+ metadataError: metadata.metadataError
459
+ };
460
+ }
461
+
462
+ async function readSkillDescription(skillFile) {
463
+ const content = await readFile(skillFile, 'utf8');
464
+ const match = content.match(/^description:\s*(.+)$/m);
465
+ return match ? match[1].replace(/^["']|["']$/g, '') : firstMarkdownLine(content);
466
+ }
467
+
468
+ function firstMarkdownLine(content) {
469
+ return content
470
+ .split('\n')
471
+ .map((line) => line.trim())
472
+ .find((line) => line && !line.startsWith('---') && !line.startsWith('#')) || '';
473
+ }
474
+
475
+ async function safeReaddir(dir) {
476
+ try {
477
+ return await readdir(dir, { withFileTypes: true });
478
+ } catch {
479
+ return [];
480
+ }
481
+ }
482
+
483
+ async function assertDirectory(targetPath, label) {
484
+ const stat = await lstat(targetPath);
485
+ if (!stat.isDirectory()) {
486
+ throw new Error(`${label} 必须是目录:${targetPath}`);
487
+ }
488
+ }
489
+
490
+ function assertInsideSources(roots, skillPath) {
491
+ assertInsideAllowedSkillSources(roots, skillPath);
492
+ }
493
+
494
+ /**
495
+ * disable 的 roots 解析。优先接受完整的 roots 对象;历史调用点只传 rootDir
496
+ * (原件库根),此时代码根退化为同一个目录。
497
+ */
498
+ function resolveDisableRoots(input) {
499
+ if (input.roots && typeof input.roots === 'object') {
500
+ return normalizeRoots(input.roots);
501
+ }
502
+ if (typeof input.rootDir === 'string' && input.rootDir) {
503
+ return normalizeRoots(input.rootDir, { codeRoot: input.codeRoot });
504
+ }
505
+ if (typeof input.libraryRoot === 'string' && input.libraryRoot) {
506
+ return normalizeRoots(input.libraryRoot, { codeRoot: input.codeRoot });
507
+ }
508
+ return null;
509
+ }
510
+
511
+ function normalizeAlias(value) {
512
+ if (!value || typeof value !== 'string') {
513
+ throw new Error('alias 不能为空');
514
+ }
515
+
516
+ const alias = value.trim();
517
+ if (!/^[a-zA-Z0-9._-]+$/.test(alias)) {
518
+ throw new Error('alias 只能包含字母、数字、点、下划线和短横线');
519
+ }
520
+
521
+ return alias;
522
+ }
523
+
524
+ function buildSourcesInfo(records) {
525
+ return records.map((record) => {
526
+ const info = {
527
+ sourceId: record.sourceId,
528
+ type: record.type,
529
+ installPath: record.installPath
530
+ };
531
+
532
+ if (record.type === 'git' && record.origin?.commit) {
533
+ info.version = record.origin.commit.slice(0, 7);
534
+ } else if (record.integrity?.value) {
535
+ info.version = record.integrity.value.slice(0, 12);
536
+ info.algorithm = record.integrity.algorithm;
537
+ }
538
+
539
+ return info;
540
+ });
541
+ }