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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { buildCollectionEnablePlan } from './enablePlan.js';
|
|
2
|
+
import { enableProjectSkill } from './projectActions.js';
|
|
3
|
+
import { getState } from './skillStore.js';
|
|
4
|
+
import { normalizeEnablementScope } from './enablementScope.js';
|
|
5
|
+
import { normalizeRoots } from './roots.js';
|
|
6
|
+
|
|
7
|
+
export async function executeCollectionEnablement(roots, input) {
|
|
8
|
+
const resolvedRoots = normalizeRoots(roots);
|
|
9
|
+
const scope = normalizeEnablementScope(input.scope);
|
|
10
|
+
const projectPath = input.projectPath || resolvedRoots.libraryRoot;
|
|
11
|
+
const skillIds = normalizeSkillIds(input.skillIds);
|
|
12
|
+
const state = await getState(resolvedRoots, projectPath, {
|
|
13
|
+
globalDir: input.globalDir,
|
|
14
|
+
hermesDir: input.hermesDir
|
|
15
|
+
});
|
|
16
|
+
const plan = buildCollectionEnablePlan(state, skillIds, { scope });
|
|
17
|
+
const skillsById = new Map(state.skills.map((skill) => [skill.id, skill]));
|
|
18
|
+
const outcomes = [
|
|
19
|
+
...plan.unchangedSkillIds.map((skillId) => buildStaticOutcome(skillsById, skillId, 'unchanged')),
|
|
20
|
+
...plan.skippedSkillIds.map((skillId) => buildStaticOutcome(skillsById, skillId, 'skipped'))
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
for (const skillId of plan.targetSkillIds) {
|
|
24
|
+
const skill = skillsById.get(skillId);
|
|
25
|
+
try {
|
|
26
|
+
const result = await enableProjectSkill(resolvedRoots, {
|
|
27
|
+
...(scope === 'project' ? { projectPath: state.projectPath } : {}),
|
|
28
|
+
scope,
|
|
29
|
+
...(input.globalDir ? { globalDir: input.globalDir } : {}),
|
|
30
|
+
...(input.hermesDir ? { hermesDir: input.hermesDir } : {}),
|
|
31
|
+
skillPath: skill.path,
|
|
32
|
+
alias: skill.name
|
|
33
|
+
});
|
|
34
|
+
outcomes.push({
|
|
35
|
+
skillId,
|
|
36
|
+
alias: result.alias,
|
|
37
|
+
status: result.unchanged ? 'unchanged' : 'enabled',
|
|
38
|
+
claudeSync: result.claudeSync
|
|
39
|
+
});
|
|
40
|
+
} catch (error) {
|
|
41
|
+
outcomes.push({
|
|
42
|
+
skillId,
|
|
43
|
+
alias: skill.name,
|
|
44
|
+
status: 'failed',
|
|
45
|
+
error: error.message || 'Collection enablement failed'
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const refreshed = scope !== 'project'
|
|
51
|
+
? { setups: [], reminders: [], refresh: { ok: true } }
|
|
52
|
+
: await refreshSetupGuidance(resolvedRoots, state.projectPath, plan.setups, input.globalDir, input.hermesDir);
|
|
53
|
+
return {
|
|
54
|
+
scope,
|
|
55
|
+
plan,
|
|
56
|
+
outcomes,
|
|
57
|
+
counts: countOutcomes(outcomes),
|
|
58
|
+
setups: refreshed.setups,
|
|
59
|
+
reminders: refreshed.reminders,
|
|
60
|
+
refresh: refreshed.refresh
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeSkillIds(value) {
|
|
65
|
+
if (!Array.isArray(value)) throw new Error('skillIds 必须是数组');
|
|
66
|
+
return [...new Set(value.map((skillId) => {
|
|
67
|
+
if (typeof skillId !== 'string' || !skillId.trim()) {
|
|
68
|
+
throw new Error('skillIds 只能包含非空字符串');
|
|
69
|
+
}
|
|
70
|
+
return skillId.trim();
|
|
71
|
+
}))];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildStaticOutcome(skillsById, skillId, status) {
|
|
75
|
+
const skill = skillsById.get(skillId);
|
|
76
|
+
return {
|
|
77
|
+
skillId,
|
|
78
|
+
alias: skill.name,
|
|
79
|
+
status
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function countOutcomes(outcomes) {
|
|
84
|
+
const counts = {
|
|
85
|
+
enabled: 0,
|
|
86
|
+
unchanged: 0,
|
|
87
|
+
skipped: 0,
|
|
88
|
+
failed: 0
|
|
89
|
+
};
|
|
90
|
+
for (const outcome of outcomes) counts[outcome.status] += 1;
|
|
91
|
+
return counts;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function refreshSetupGuidance(roots, projectPath, plannedSetups, globalDir, hermesDir) {
|
|
95
|
+
try {
|
|
96
|
+
const state = await getState(roots, projectPath, { globalDir, hermesDir });
|
|
97
|
+
const setupIds = new Set(plannedSetups.map((setup) => setup.id));
|
|
98
|
+
return {
|
|
99
|
+
setups: state.setups.filter((setup) => setupIds.has(setup.id)),
|
|
100
|
+
reminders: state.advice.filter((advice) =>
|
|
101
|
+
setupIds.has(advice.collection) && advice.type.startsWith('collection-setup-')
|
|
102
|
+
),
|
|
103
|
+
refresh: { ok: true }
|
|
104
|
+
};
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return {
|
|
107
|
+
setups: plannedSetups,
|
|
108
|
+
reminders: [],
|
|
109
|
+
refresh: {
|
|
110
|
+
ok: false,
|
|
111
|
+
error: error.message || 'Collection setup guidance refresh failed'
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { access, readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { normalizeRoots } from './roots.js';
|
|
5
|
+
|
|
6
|
+
const CONTRACT_ROOT = 'collection-metadata';
|
|
7
|
+
const SUPPORTED_SCHEMA_VERSION = 1;
|
|
8
|
+
|
|
9
|
+
export async function scanCollectionSetups(roots, projectPath, skills, enabled) {
|
|
10
|
+
const { codeRoot } = normalizeRoots(roots);
|
|
11
|
+
const collections = uniqueCollections(skills);
|
|
12
|
+
const enabledTargets = new Set(enabled.map((skill) => skill.targetPath).filter(Boolean));
|
|
13
|
+
const results = await Promise.all(collections.map(async (collection) => {
|
|
14
|
+
const contractPath = path.join(codeRoot, CONTRACT_ROOT, collection.source, `${collection.collection}.json`);
|
|
15
|
+
try {
|
|
16
|
+
const contract = await readContract(contractPath);
|
|
17
|
+
if (!contract) return null;
|
|
18
|
+
return await evaluateSetupContract({
|
|
19
|
+
collection,
|
|
20
|
+
contract: normalizeContract(contract, collection, skills),
|
|
21
|
+
contractPath,
|
|
22
|
+
projectPath,
|
|
23
|
+
skills,
|
|
24
|
+
enabledTargets
|
|
25
|
+
});
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return {
|
|
28
|
+
id: collection.id,
|
|
29
|
+
source: collection.source,
|
|
30
|
+
collection: collection.collection,
|
|
31
|
+
contractPath,
|
|
32
|
+
status: 'invalid',
|
|
33
|
+
error: error.message
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
return results.filter(Boolean).sort((left, right) => left.id.localeCompare(right.id));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildCollectionSetupAdvice(setups) {
|
|
42
|
+
const advice = [];
|
|
43
|
+
|
|
44
|
+
for (const setup of setups) {
|
|
45
|
+
if (setup.status === 'invalid') {
|
|
46
|
+
advice.push({
|
|
47
|
+
severity: 'warning',
|
|
48
|
+
type: 'collection-setup-invalid',
|
|
49
|
+
title: `库初始化配置无效:${setup.id}`,
|
|
50
|
+
detail: setup.error,
|
|
51
|
+
collection: setup.id,
|
|
52
|
+
contractPath: setup.contractPath
|
|
53
|
+
});
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (setup.affectedEnabledSkillIds.length === 0 || setup.status === 'ready') continue;
|
|
58
|
+
const required = setup.requirement === 'required';
|
|
59
|
+
const statusText = setup.status === 'partial' ? '仅完成部分配置' : '尚未配置';
|
|
60
|
+
const setupSkillText = setup.setupSkillEnabled ? '' : ` 初始化 skill ${setup.setupSkillName} 尚未启用。`;
|
|
61
|
+
advice.push({
|
|
62
|
+
severity: required ? 'warning' : 'info',
|
|
63
|
+
type: required ? 'collection-setup-required' : 'collection-setup-recommended',
|
|
64
|
+
title: `${setup.id} 已启用但${statusText}`,
|
|
65
|
+
detail: `缺少:${setup.missingArtifacts.join('、')}。${setupSkillText}请在 Agent 中运行 ${setup.setupSkillName}。`,
|
|
66
|
+
collection: setup.id,
|
|
67
|
+
status: setup.status,
|
|
68
|
+
setupSkillId: setup.setupSkillId,
|
|
69
|
+
missingArtifacts: setup.missingArtifacts,
|
|
70
|
+
affectedSkillIds: setup.affectedEnabledSkillIds,
|
|
71
|
+
actions: [{
|
|
72
|
+
type: 'run-setup-skill',
|
|
73
|
+
skillId: setup.setupSkillId,
|
|
74
|
+
instruction: setup.instruction
|
|
75
|
+
}]
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return advice;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function evaluateSetupContract({ collection, contract, contractPath, projectPath, skills, enabledTargets }) {
|
|
83
|
+
const artifacts = await Promise.all(contract.requiredArtifacts.map(async (relativePath) => ({
|
|
84
|
+
path: relativePath,
|
|
85
|
+
exists: await pathExists(path.resolve(projectPath, relativePath))
|
|
86
|
+
})));
|
|
87
|
+
const existingCount = artifacts.filter((artifact) => artifact.exists).length;
|
|
88
|
+
const status = existingCount === 0 ? 'missing' : existingCount === artifacts.length ? 'ready' : 'partial';
|
|
89
|
+
const applicableSkills = skills.filter((skill) =>
|
|
90
|
+
skill.id !== contract.skillId && contract.appliesTo.some((pattern) => matchesPattern(skill.id, pattern))
|
|
91
|
+
);
|
|
92
|
+
const setupSkill = skills.find((skill) => skill.id === contract.skillId);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
id: collection.id,
|
|
96
|
+
source: collection.source,
|
|
97
|
+
collection: collection.collection,
|
|
98
|
+
contractPath,
|
|
99
|
+
requirement: contract.requirement,
|
|
100
|
+
scope: contract.scope,
|
|
101
|
+
mode: contract.mode,
|
|
102
|
+
status,
|
|
103
|
+
setupSkillId: setupSkill.id,
|
|
104
|
+
setupSkillName: setupSkill.name,
|
|
105
|
+
setupSkillEnabled: enabledTargets.has(setupSkill.path),
|
|
106
|
+
applicableSkillIds: applicableSkills.map((skill) => skill.id),
|
|
107
|
+
affectedEnabledSkillIds: applicableSkills.filter((skill) => enabledTargets.has(skill.path)).map((skill) => skill.id),
|
|
108
|
+
requiredArtifacts: artifacts.map((artifact) => artifact.path),
|
|
109
|
+
missingArtifacts: artifacts.filter((artifact) => !artifact.exists).map((artifact) => artifact.path),
|
|
110
|
+
instruction: `请运行 ${setupSkill.name},完成 ${collection.id} 的项目初始化。`
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function normalizeContract(value, collection, skills) {
|
|
115
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('setup contract 必须是对象');
|
|
116
|
+
if (value.schemaVersion !== SUPPORTED_SCHEMA_VERSION) throw new Error(`schemaVersion 必须是 ${SUPPORTED_SCHEMA_VERSION}`);
|
|
117
|
+
const setup = value.setup;
|
|
118
|
+
if (!setup || typeof setup !== 'object' || Array.isArray(setup)) throw new Error('setup 字段必须是对象');
|
|
119
|
+
if (!['required', 'recommended'].includes(setup.requirement)) throw new Error('setup.requirement 必须是 required 或 recommended');
|
|
120
|
+
if (setup.scope !== 'project') throw new Error('setup.scope 当前只支持 project');
|
|
121
|
+
if (setup.mode !== 'interactive') throw new Error('setup.mode 当前只支持 interactive');
|
|
122
|
+
if (typeof setup.skillId !== 'string' || !setup.skillId.trim()) throw new Error('setup.skillId 不能为空');
|
|
123
|
+
|
|
124
|
+
const setupSkill = skills.find((skill) => skill.id === setup.skillId);
|
|
125
|
+
if (!setupSkill || setupSkill.source !== collection.source || setupSkill.collection !== collection.collection) {
|
|
126
|
+
throw new Error('setup.skillId 必须指向当前库中存在的 skill');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const appliesTo = normalizePatterns(setup.appliesTo);
|
|
130
|
+
const requiredArtifacts = normalizeArtifacts(setup.requiredArtifacts);
|
|
131
|
+
if (!skills.some((skill) => skill.id !== setup.skillId && appliesTo.some((pattern) => matchesPattern(skill.id, pattern)))) {
|
|
132
|
+
throw new Error('setup.appliesTo 没有匹配当前库中的 skill');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
requirement: setup.requirement,
|
|
137
|
+
scope: setup.scope,
|
|
138
|
+
mode: setup.mode,
|
|
139
|
+
skillId: setup.skillId,
|
|
140
|
+
appliesTo,
|
|
141
|
+
requiredArtifacts
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizePatterns(value) {
|
|
146
|
+
if (!Array.isArray(value) || value.length === 0) throw new Error('setup.appliesTo 必须是非空数组');
|
|
147
|
+
return value.map((pattern) => {
|
|
148
|
+
if (typeof pattern !== 'string' || !pattern.trim()) throw new Error('setup.appliesTo 只能包含字符串');
|
|
149
|
+
const text = pattern.trim();
|
|
150
|
+
const star = text.indexOf('*');
|
|
151
|
+
if (star !== -1 && (star !== text.length - 1 || text.indexOf('*', star + 1) !== -1)) {
|
|
152
|
+
throw new Error('setup.appliesTo 仅支持末尾 * 通配符');
|
|
153
|
+
}
|
|
154
|
+
return text;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function normalizeArtifacts(value) {
|
|
159
|
+
if (!Array.isArray(value) || value.length === 0) throw new Error('setup.requiredArtifacts 必须是非空数组');
|
|
160
|
+
return [...new Set(value.map((artifact) => {
|
|
161
|
+
if (typeof artifact !== 'string' || !artifact.trim()) throw new Error('setup.requiredArtifacts 只能包含字符串');
|
|
162
|
+
const normalized = path.normalize(artifact.trim());
|
|
163
|
+
if (path.isAbsolute(normalized) || normalized === '.' || normalized === '..' || normalized.startsWith(`..${path.sep}`)) {
|
|
164
|
+
throw new Error('setup.requiredArtifacts 必须是项目内相对路径');
|
|
165
|
+
}
|
|
166
|
+
return normalized;
|
|
167
|
+
}))];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function uniqueCollections(skills) {
|
|
171
|
+
const collections = new Map();
|
|
172
|
+
for (const skill of skills) {
|
|
173
|
+
const id = `${skill.source}/${skill.collection}`;
|
|
174
|
+
if (!collections.has(id)) collections.set(id, { id, source: skill.source, collection: skill.collection });
|
|
175
|
+
}
|
|
176
|
+
return [...collections.values()];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function matchesPattern(value, pattern) {
|
|
180
|
+
return pattern.endsWith('*') ? value.startsWith(pattern.slice(0, -1)) : value === pattern;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function readContract(contractPath) {
|
|
184
|
+
try {
|
|
185
|
+
return JSON.parse(await readFile(contractPath, 'utf8'));
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (error.code === 'ENOENT') return null;
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function pathExists(targetPath) {
|
|
193
|
+
try {
|
|
194
|
+
await access(targetPath, constants.F_OK);
|
|
195
|
+
return true;
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { normalizeEnablementScope } from './enablementScope.js';
|
|
2
|
+
import { isHermesEligibleSource } from './hermesStore.js';
|
|
3
|
+
|
|
4
|
+
export function buildCollectionEnablePlan(state, skillIds, options = {}) {
|
|
5
|
+
const scope = normalizeEnablementScope(
|
|
6
|
+
typeof options === 'string' ? options : options.scope
|
|
7
|
+
);
|
|
8
|
+
const requestedIds = new Set(skillIds);
|
|
9
|
+
const requestedSkills = state.skills.filter((skill) => requestedIds.has(skill.id));
|
|
10
|
+
if (requestedSkills.length !== requestedIds.size) {
|
|
11
|
+
const knownIds = new Set(requestedSkills.map((skill) => skill.id));
|
|
12
|
+
const missing = [...requestedIds].filter((skillId) => !knownIds.has(skillId));
|
|
13
|
+
throw new Error(`找不到待启用 skill:${missing.join(', ')}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const enabledTargets = new Set(
|
|
17
|
+
enabledSkillsForScope(state, scope)
|
|
18
|
+
.map((skill) => skill.targetPath)
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
);
|
|
21
|
+
const setups = scope === 'project'
|
|
22
|
+
? findSetupsForSkillIds(state, skillIds).filter((setup) => setup.status !== 'ready')
|
|
23
|
+
: [];
|
|
24
|
+
const setupSkillIds = new Set(setups.filter((setup) => !setup.setupSkillEnabled).map((setup) => setup.setupSkillId));
|
|
25
|
+
const expandedSkills = [];
|
|
26
|
+
|
|
27
|
+
for (const setupSkillId of setupSkillIds) {
|
|
28
|
+
const setupSkill = state.skills.find((skill) => skill.id === setupSkillId);
|
|
29
|
+
if (setupSkill) expandedSkills.push(setupSkill);
|
|
30
|
+
}
|
|
31
|
+
for (const skill of requestedSkills) {
|
|
32
|
+
if (!setupSkillIds.has(skill.id)) expandedSkills.push(skill);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const targetSkillIds = [];
|
|
36
|
+
const skippedSkillIds = [];
|
|
37
|
+
const unchangedSkillIds = [];
|
|
38
|
+
|
|
39
|
+
for (const skill of expandedSkills) {
|
|
40
|
+
if (enabledTargets.has(skill.path)) {
|
|
41
|
+
unchangedSkillIds.push(skill.id);
|
|
42
|
+
} else if (scope === 'hermes' && !isHermesEligibleSource(skill.source)) {
|
|
43
|
+
skippedSkillIds.push(skill.id);
|
|
44
|
+
} else if (skill.source === 'archived' || (skill.autoEnable === false && !setupSkillIds.has(skill.id))) {
|
|
45
|
+
skippedSkillIds.push(skill.id);
|
|
46
|
+
} else {
|
|
47
|
+
targetSkillIds.push(skill.id);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { scope, targetSkillIds, skippedSkillIds, unchangedSkillIds, setups };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildSkillEnablePlan(state, skillId, requestedAlias, scope = 'project') {
|
|
55
|
+
scope = normalizeEnablementScope(scope);
|
|
56
|
+
const matches = state.skills.filter((skill) => skill.id === skillId);
|
|
57
|
+
if (matches.length !== 1) {
|
|
58
|
+
throw new Error(`Expected one acquired skill for ${skillId}; found ${matches.length}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const skill = matches[0];
|
|
62
|
+
if (scope === 'hermes' && !isHermesEligibleSource(skill.source)) {
|
|
63
|
+
throw new Error(`Hermes 不允许启用 ${skill.source} 来源的 skill:${skill.id}`);
|
|
64
|
+
}
|
|
65
|
+
const alias = requestedAlias || skill.name;
|
|
66
|
+
const existing = enabledSkillsForScope(state, scope)
|
|
67
|
+
.find((enabledSkill) => enabledSkill.alias === alias);
|
|
68
|
+
if (
|
|
69
|
+
existing &&
|
|
70
|
+
(!existing.isSymlink || existing.targetPath !== skill.path)
|
|
71
|
+
) {
|
|
72
|
+
throw new Error(`${scopeLabel(scope)} alias is occupied by another entry: ${alias}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
skillId: skill.id,
|
|
77
|
+
skillPath: skill.path,
|
|
78
|
+
alias,
|
|
79
|
+
scope,
|
|
80
|
+
status: existing ? 'unchanged' : 'ready',
|
|
81
|
+
reminders: scope === 'project' ? findSetupReminders(state, skill.id) : []
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function enabledSkillsForScope(state, scope) {
|
|
86
|
+
if (scope === 'global') return state.global || [];
|
|
87
|
+
if (scope === 'hermes') return state.hermes || [];
|
|
88
|
+
return state.enabled || [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function scopeLabel(scope) {
|
|
92
|
+
return { global: 'Global', hermes: 'Hermes', project: 'Project' }[scope] || scope;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function findSetupsForSkillIds(state, skillIds) {
|
|
96
|
+
const candidates = new Set(skillIds);
|
|
97
|
+
return (state.setups || []).filter((setup) =>
|
|
98
|
+
setup.status !== 'invalid' && setup.applicableSkillIds.some((skillId) => candidates.has(skillId))
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function findSetupReminders(state, skillId) {
|
|
103
|
+
const setupIds = new Set(
|
|
104
|
+
findSetupsForSkillIds(state, [skillId])
|
|
105
|
+
.filter((setup) => setup.status !== 'ready')
|
|
106
|
+
.map((setup) => setup.setupSkillId)
|
|
107
|
+
);
|
|
108
|
+
return (state.advice || []).filter(
|
|
109
|
+
(advice) =>
|
|
110
|
+
(
|
|
111
|
+
advice.type === 'collection-setup-required' ||
|
|
112
|
+
advice.type === 'collection-setup-recommended'
|
|
113
|
+
) &&
|
|
114
|
+
setupIds.has(advice.setupSkillId)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const ENABLEMENT_SCOPES = Object.freeze(['project', 'global', 'hermes']);
|
|
2
|
+
|
|
3
|
+
export function normalizeEnablementScope(value = 'project') {
|
|
4
|
+
if (!ENABLEMENT_SCOPES.includes(value)) {
|
|
5
|
+
throw new Error(`scope 必须是 project、global 或 hermes:${value}`);
|
|
6
|
+
}
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { scanSkillLinks } from './skillLinkStore.js';
|
|
4
|
+
|
|
5
|
+
export const HERMES_ELIGIBLE_SOURCES = Object.freeze(['official', 'github', 'personal']);
|
|
6
|
+
|
|
7
|
+
export function getHermesSkillsDir(hermesDir) {
|
|
8
|
+
return path.resolve(hermesDir || path.join(homedir(), '.hermes', 'skills'));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function scanHermesSkills({ hermesDir } = {}) {
|
|
12
|
+
const directory = getHermesSkillsDir(hermesDir);
|
|
13
|
+
const skills = await scanSkillLinks(directory);
|
|
14
|
+
return skills
|
|
15
|
+
.map((skill) => ({ ...skill, agent: 'Hermes', scope: 'hermes', directory }))
|
|
16
|
+
.sort((left, right) => left.alias.localeCompare(right.alias));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isHermesEligibleSource(source) {
|
|
20
|
+
return HERMES_ELIGIBLE_SOURCES.includes(source);
|
|
21
|
+
}
|