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/tui.js
ADDED
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
import { execFile, spawn } from 'node:child_process';
|
|
4
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
5
|
+
import { writeFile } from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import {
|
|
9
|
+
disableAlias,
|
|
10
|
+
enableCollectionChoice,
|
|
11
|
+
enableSkillChoice,
|
|
12
|
+
formatAdvice,
|
|
13
|
+
listEnabledAliases,
|
|
14
|
+
listSkillChoices,
|
|
15
|
+
loadTuiState,
|
|
16
|
+
saveSkillMetadata,
|
|
17
|
+
summarizeState,
|
|
18
|
+
syncClaude
|
|
19
|
+
} from '../lib/tuiActions.js';
|
|
20
|
+
import { buildCollectionEnablePlan } from '../lib/enablePlan.js';
|
|
21
|
+
import { isRemoteSkillFileInput } from '../lib/sourceHttp.js';
|
|
22
|
+
import { isDefiniteGitSourceInput } from '../lib/sourceGitUrl.js';
|
|
23
|
+
import { applyAddSource, planAddSource } from '../lib/sourceManager.js';
|
|
24
|
+
import { checkCliInstall, installCliCommand } from '../lib/tuiInstall.js';
|
|
25
|
+
import {
|
|
26
|
+
formatCompactSkillChoice,
|
|
27
|
+
formatSkillTableDivider,
|
|
28
|
+
formatSkillTableHeader,
|
|
29
|
+
getSkillIntroduction,
|
|
30
|
+
getSkillPage,
|
|
31
|
+
getSkillTableLayout
|
|
32
|
+
} from '../lib/tuiLayout.js';
|
|
33
|
+
import { resolveLibraryRoot } from '../lib/roots.js';
|
|
34
|
+
import { isCliCommand, runCli } from './cli.js';
|
|
35
|
+
import { formatOrigin } from './source.js';
|
|
36
|
+
|
|
37
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
const codeRootDir = path.resolve(__dirname, '..');
|
|
39
|
+
|
|
40
|
+
if (isCliCommand(process.argv.slice(2))) {
|
|
41
|
+
const exitCode = await runCli({
|
|
42
|
+
argv: process.argv.slice(2),
|
|
43
|
+
rootDir: process.env.SKILLCADDY_ROOT,
|
|
44
|
+
runtimeRootDir: codeRootDir
|
|
45
|
+
});
|
|
46
|
+
process.exit(exitCode);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const options = parseArgs(process.argv.slice(2));
|
|
50
|
+
// 原件库根与代码根解耦:clone 形态下两者同目录,发行形态(npm/npx)下
|
|
51
|
+
// 原件库落到 ~/.skillcaddy/library,不会跟着包缓存一起消失。
|
|
52
|
+
const libraryRoot = path.resolve(
|
|
53
|
+
options.root || process.env.SKILLCADDY_ROOT || resolveLibraryRoot({ codeRoot: codeRootDir })
|
|
54
|
+
);
|
|
55
|
+
const roots = { libraryRoot, codeRoot: codeRootDir };
|
|
56
|
+
|
|
57
|
+
const rl = createInterface({ input, output });
|
|
58
|
+
let state = await loadTuiState(roots, options.projectPath || process.cwd());
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await mainLoop();
|
|
62
|
+
} finally {
|
|
63
|
+
rl.close();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function mainLoop() {
|
|
67
|
+
while (true) {
|
|
68
|
+
printSummary();
|
|
69
|
+
console.log([
|
|
70
|
+
'1. 查看已启用 skill',
|
|
71
|
+
'2. 浏览/搜索 skill',
|
|
72
|
+
'3. 启用 skill',
|
|
73
|
+
'4. 清理已启用 skill',
|
|
74
|
+
'5. 同步 Claude Code',
|
|
75
|
+
'6. 编辑 skill metadata',
|
|
76
|
+
'7. 查看项目诊断',
|
|
77
|
+
'8. 刷新项目状态',
|
|
78
|
+
'9. 更新 GitHub skill 库',
|
|
79
|
+
'10. 批量生成中文 note',
|
|
80
|
+
'11. 安装/检查全局 CLI + TUI 命令',
|
|
81
|
+
'12. 按地址添加远程 Source 到原件库',
|
|
82
|
+
'13. 管理全局 skill(~/.agents/skills)',
|
|
83
|
+
'14. 管理 Hermes skill(~/.hermes/skills)',
|
|
84
|
+
'q. 退出'
|
|
85
|
+
].join('\n'));
|
|
86
|
+
|
|
87
|
+
const choice = await ask('选择操作');
|
|
88
|
+
if (choice === 'q') {
|
|
89
|
+
console.log('\nbyebye 👋');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
if (choice === '1') printEnabledSkills();
|
|
95
|
+
else if (choice === '2') await browseSkills();
|
|
96
|
+
else if (choice === '3') await enableSkillFlow();
|
|
97
|
+
else if (choice === '4') await disableAliasFlow();
|
|
98
|
+
else if (choice === '5') await syncClaudeFlow();
|
|
99
|
+
else if (choice === '6') await editMetadataFlow();
|
|
100
|
+
else if (choice === '7') printAdvice();
|
|
101
|
+
else if (choice === '8') await refreshState();
|
|
102
|
+
else if (choice === '9') await updateGithubSkillsFlow();
|
|
103
|
+
else if (choice === '10') await batchTranslateNotesFlow();
|
|
104
|
+
else if (choice === '11') await installGlobalCommandFlow();
|
|
105
|
+
else if (choice === '12') await addRemoteSourceFlow();
|
|
106
|
+
else if (choice === '13') await globalSkillsFlow();
|
|
107
|
+
else if (choice === '14') await hermesSkillsFlow();
|
|
108
|
+
else console.log('未知操作');
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const category = error?.category ? ` [${error.category}]` : '';
|
|
111
|
+
console.log(`错误${category}:${error.message}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await ask('回车继续');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function installGlobalCommandFlow() {
|
|
119
|
+
const current = await checkCliInstall(codeRootDir);
|
|
120
|
+
console.log(current.message);
|
|
121
|
+
if (current.ok) return;
|
|
122
|
+
if (current.status !== 'missing') throw new Error(current.message);
|
|
123
|
+
|
|
124
|
+
const installed = await installCliCommand(codeRootDir);
|
|
125
|
+
console.log(installed.message);
|
|
126
|
+
console.log(`命令: ${installed.command}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function addRemoteSourceFlow() {
|
|
130
|
+
const input = await ask('远程 Source 地址(留空或 b 返回)');
|
|
131
|
+
if (!input || isBack(input)) return;
|
|
132
|
+
|
|
133
|
+
if (isRemoteSkillFileInput(input)) {
|
|
134
|
+
const name = await ask('Remote file name(必填,b 返回)');
|
|
135
|
+
if (isBack(name)) return;
|
|
136
|
+
await applyRemoteSourcePlan(
|
|
137
|
+
await planAddSource({ rootDir: libraryRoot }, { input, name })
|
|
138
|
+
);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!isSupportedRemoteAddress(input)) {
|
|
143
|
+
printUnsupportedRemoteSourceInput();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let plan;
|
|
148
|
+
let archiveIdentityResolved = false;
|
|
149
|
+
try {
|
|
150
|
+
plan = await planAddSource({ rootDir: libraryRoot }, { input });
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error?.category !== 'source-collision' || isDefiniteGitSourceInput(input)) {
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
plan = await customizeRemoteArchivePlan(input);
|
|
156
|
+
if (!plan) return;
|
|
157
|
+
archiveIdentityResolved = true;
|
|
158
|
+
}
|
|
159
|
+
if (plan.input.type === 'remote-zip' && !archiveIdentityResolved) {
|
|
160
|
+
plan = await customizeRemoteArchivePlan(input, plan);
|
|
161
|
+
if (!plan) return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!['git', 'remote-zip'].includes(plan.input.type)) {
|
|
165
|
+
printUnsupportedRemoteSourceInput();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
await applyRemoteSourcePlan(plan);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function customizeRemoteArchivePlan(input, plan) {
|
|
172
|
+
const name = await ask('Archive 名称(留空自动推断,b 返回)');
|
|
173
|
+
if (isBack(name)) return null;
|
|
174
|
+
const namespace = await ask('Archive namespace(留空自动推断,b 返回)');
|
|
175
|
+
if (isBack(namespace)) return null;
|
|
176
|
+
if (!name && !namespace) {
|
|
177
|
+
if (plan) return plan;
|
|
178
|
+
return planAddSource({ rootDir: libraryRoot }, { input });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const request = { input };
|
|
182
|
+
if (name) request.name = name;
|
|
183
|
+
if (namespace) request.namespace = namespace;
|
|
184
|
+
return planAddSource({ rootDir: libraryRoot }, request);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isSupportedRemoteAddress(input) {
|
|
188
|
+
if (isDefiniteGitSourceInput(input)) return true;
|
|
189
|
+
try {
|
|
190
|
+
const url = new URL(input.trim());
|
|
191
|
+
return ['http:', 'https:'].includes(url.protocol);
|
|
192
|
+
} catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function printUnsupportedRemoteSourceInput() {
|
|
198
|
+
console.log(
|
|
199
|
+
'当前 TUI 仅支持 HTTPS/SSH/SCP Git、公共 HTTP(S) Archive 和直接 /SKILL.md Remote file;Web source acquisition 暂不支持'
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function applyRemoteSourcePlan(plan) {
|
|
204
|
+
printRemoteSourcePlan(plan);
|
|
205
|
+
if (plan.status !== 'ready') {
|
|
206
|
+
if (plan.status === 'already-installed') {
|
|
207
|
+
const result = await applyAddSource({ rootDir: libraryRoot }, plan);
|
|
208
|
+
state = await loadTuiState(roots, state.projectPath);
|
|
209
|
+
printRemoteSourceResult(result);
|
|
210
|
+
} else {
|
|
211
|
+
console.log(`未执行添加:${plan.status}`);
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const confirm = await ask('确认添加? y/N');
|
|
217
|
+
if (!['y', 'yes'].includes(confirm.toLowerCase())) {
|
|
218
|
+
console.log('已取消添加');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const result = await applyAddSource({ rootDir: libraryRoot }, plan);
|
|
223
|
+
state = await loadTuiState(roots, state.projectPath);
|
|
224
|
+
printRemoteSourceResult(result);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function printRemoteSourcePlan(plan) {
|
|
228
|
+
console.log(`Add plan: ${plan.status}`);
|
|
229
|
+
console.log(`source: ${plan.sourceId}`);
|
|
230
|
+
console.log(`install: ${plan.installPath}`);
|
|
231
|
+
console.log(`input: ${plan.input.type} ${plan.input.remote || plan.input.display}`);
|
|
232
|
+
console.log(`origin: ${formatOrigin(plan.origin)}`);
|
|
233
|
+
if (plan.focus) {
|
|
234
|
+
console.log(`focus: ${plan.focus.ref}${plan.focus.path ? ` ${plan.focus.path}` : ''}`);
|
|
235
|
+
}
|
|
236
|
+
if (plan.integrity) {
|
|
237
|
+
console.log(`integrity: ${plan.integrity.algorithm} ${plan.integrity.value}`);
|
|
238
|
+
}
|
|
239
|
+
console.log(`skills (${plan.skills.length}):`);
|
|
240
|
+
for (const skillPath of plan.skills) console.log(` - ${skillPath}`);
|
|
241
|
+
console.log(`warnings (${plan.warnings.length}):`);
|
|
242
|
+
for (const warning of plan.warnings) {
|
|
243
|
+
console.log(` - [${warning.category}] ${warning.skillPath}: ${warning.message}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function printRemoteSourceResult(result) {
|
|
248
|
+
console.log(`Outcome: ${result.status}`);
|
|
249
|
+
console.log(`source: ${result.sourceId}`);
|
|
250
|
+
console.log(`install: ${result.installPath}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function printSummary() {
|
|
254
|
+
const summary = summarizeState(state);
|
|
255
|
+
console.log('\nSkillcaddy TUI');
|
|
256
|
+
console.log(`原件库: ${libraryRoot}`);
|
|
257
|
+
if (codeRootDir !== libraryRoot) console.log(`代码根: ${codeRootDir}`);
|
|
258
|
+
console.log(`项目: ${summary.projectPath}`);
|
|
259
|
+
console.log(`Skills: ${summary.totalSkills} 可用: ${summary.availableSkills} 项目 Agents: ${summary.enabledAgents} 全局 Agents: ${state.global.length} Hermes: ${state.hermes.length} Claude: ${summary.enabledClaude} 诊断: ${summary.advice}`);
|
|
260
|
+
console.log('');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function refreshState() {
|
|
264
|
+
console.log(`当前原件库: ${libraryRoot}`);
|
|
265
|
+
const nextProject = await ask(`项目路径 [${state.projectPath}]`);
|
|
266
|
+
state = await loadTuiState(roots, nextProject || state.projectPath);
|
|
267
|
+
console.log('已刷新');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function updateGithubSkillsFlow() {
|
|
271
|
+
const githubDir = path.join(libraryRoot, 'github');
|
|
272
|
+
console.log(`将更新 GitHub skill 库: ${githubDir}`);
|
|
273
|
+
console.log('dirty 的子库会被跳过;不会 stash、reset 或覆盖本地改动。');
|
|
274
|
+
const confirm = await ask('确认运行更新? y/N');
|
|
275
|
+
if (!['y', 'yes'].includes(confirm.toLowerCase())) return;
|
|
276
|
+
|
|
277
|
+
await runCommand('bash', [
|
|
278
|
+
path.join(codeRootDir, 'scripts', 'pull-github.sh'),
|
|
279
|
+
githubDir,
|
|
280
|
+
state.projectPath
|
|
281
|
+
]);
|
|
282
|
+
state = await loadTuiState(roots, state.projectPath);
|
|
283
|
+
console.log('已重新扫描本地状态');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function printEnabledSkills() {
|
|
287
|
+
console.log('\n已启用 skill / Agents');
|
|
288
|
+
if (state.enabled.length === 0) {
|
|
289
|
+
console.log('当前项目没有启用 Agents skill');
|
|
290
|
+
} else {
|
|
291
|
+
printSkillChoices(state.enabled.map(toEnabledSkillChoice));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
console.log('\n已启用 skill / Claude Code');
|
|
295
|
+
const claudeSkills = state.claude?.skills || [];
|
|
296
|
+
if (claudeSkills.length === 0) {
|
|
297
|
+
console.log('当前项目没有启用 Claude Code skill');
|
|
298
|
+
} else {
|
|
299
|
+
printSkillChoices(claudeSkills.map((skill, index) => {
|
|
300
|
+
const agentsSkill = state.enabled.find((item) => item.alias === skill.alias);
|
|
301
|
+
return toEnabledSkillChoice(agentsSkill || skill, index);
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
console.log('\n已启用 skill / 全局 Agents');
|
|
306
|
+
if (state.global.length === 0) {
|
|
307
|
+
console.log('当前没有全局 Agents skill');
|
|
308
|
+
} else {
|
|
309
|
+
printSkillChoices(state.global.map(toEnabledSkillChoice));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
console.log('\n已启用 skill / Hermes');
|
|
313
|
+
if (state.hermes.length === 0) {
|
|
314
|
+
console.log('当前没有 Hermes skill');
|
|
315
|
+
} else {
|
|
316
|
+
printSkillChoices(state.hermes.map(toEnabledSkillChoice));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function toEnabledSkillChoice(enabledSkill, index) {
|
|
321
|
+
const sourceSkill = findSourceSkillForEnabled(enabledSkill);
|
|
322
|
+
return {
|
|
323
|
+
index: index + 1,
|
|
324
|
+
enabled: true,
|
|
325
|
+
skill: sourceSkill || {
|
|
326
|
+
name: enabledSkill.alias,
|
|
327
|
+
note: enabledSkill.isSymlink ? '' : '非软链接',
|
|
328
|
+
path: enabledSkill.targetPath || enabledSkill.linkPath,
|
|
329
|
+
autoEnable: true
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function findSourceSkillForEnabled(enabledSkill) {
|
|
335
|
+
if (!enabledSkill) return null;
|
|
336
|
+
if (enabledSkill.targetPath) {
|
|
337
|
+
const byTarget = state.skills.find((skill) => skill.path === enabledSkill.targetPath);
|
|
338
|
+
if (byTarget) return byTarget;
|
|
339
|
+
}
|
|
340
|
+
return state.skills.find((skill) => skill.name === enabledSkill.alias) || null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function browseSkills() {
|
|
344
|
+
let query = '';
|
|
345
|
+
let source = '';
|
|
346
|
+
|
|
347
|
+
while (true) {
|
|
348
|
+
const skills = listSkillChoices(state, { query, source, limit: state.skills.length });
|
|
349
|
+
const libraries = groupSkillChoices(skills);
|
|
350
|
+
console.log(`\nSkill 库${query ? ` / 搜索: ${query}` : ''}${source ? ` / 来源: ${source}` : ''}`);
|
|
351
|
+
if (libraries.length === 0) {
|
|
352
|
+
console.log(`没有匹配的 skill。当前原件库是 ${libraryRoot};如果你在 worktree 里开发,请用 --root 指向真实原件库。`);
|
|
353
|
+
} else {
|
|
354
|
+
printLibraryChoices(libraries);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
console.log('输入库序号进入库;/关键词 搜索;s 设置来源;r 重置;b 返回');
|
|
358
|
+
const selected = await ask('选择');
|
|
359
|
+
if (isBack(selected)) return;
|
|
360
|
+
if (selected === 'r') {
|
|
361
|
+
query = '';
|
|
362
|
+
source = '';
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (selected === 's') {
|
|
366
|
+
source = await ask('来源 official/github/personal/archived,可空');
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (selected.startsWith('/')) {
|
|
370
|
+
query = selected.slice(1).trim();
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const index = Number(selected);
|
|
375
|
+
if (!Number.isInteger(index) || index < 1 || index > libraries.length) {
|
|
376
|
+
console.log('请输入列表序号,或使用 /关键词 搜索');
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
await libraryFlow(libraries[index - 1]);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function libraryFlow(library) {
|
|
385
|
+
let page = 1;
|
|
386
|
+
|
|
387
|
+
while (true) {
|
|
388
|
+
const skills = reindexChoices(listSkillChoices(state, {
|
|
389
|
+
source: library.source,
|
|
390
|
+
limit: state.skills.length
|
|
391
|
+
}).filter((choice) => choice.skill.collection === library.collection));
|
|
392
|
+
const currentLibrary = groupSkillChoices(skills)[0] || library;
|
|
393
|
+
|
|
394
|
+
console.log(`\n库详情: ${library.source}/${library.collection}`);
|
|
395
|
+
console.log(`路径: ${library.collectionPath}`);
|
|
396
|
+
console.log(`Skills: ${currentLibrary.total} 可一键添加: ${currentLibrary.bulkable} 已启用: ${currentLibrary.enabled} 跳过一键: ${currentLibrary.skipped}`);
|
|
397
|
+
printCollectionSetupStatus(library.source, library.collection);
|
|
398
|
+
const skillPage = getSkillPage(skills, page);
|
|
399
|
+
page = skillPage.currentPage;
|
|
400
|
+
printCompactSkillChoices(skillPage);
|
|
401
|
+
console.log('──────────────────');
|
|
402
|
+
console.log(`输入 skill 序号进入详情;a 一键添加;${skillPage.totalPages > 1 ? 'n/p 翻页;' : ''}b 返回`);
|
|
403
|
+
|
|
404
|
+
const selected = await ask('选择');
|
|
405
|
+
if (isBack(selected)) return;
|
|
406
|
+
if (selected === 'n' && page < skillPage.totalPages) {
|
|
407
|
+
page += 1;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (selected === 'p' && page > 1) {
|
|
411
|
+
page -= 1;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (selected === 'a') {
|
|
415
|
+
const scopeChoice = await ask('1. 启用到当前项目 2. 启用到全局 3. 启用到 Hermes b. 取消');
|
|
416
|
+
if (isBack(scopeChoice)) continue;
|
|
417
|
+
if (!['1', '2', '3'].includes(scopeChoice)) {
|
|
418
|
+
console.log('请输入 1、2、3 或 b');
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
await enableLibraryFlow(currentLibrary, skills, scopeChoice === '2' ? 'global' : scopeChoice === '3' ? 'hermes' : 'project');
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const index = Number(selected);
|
|
426
|
+
if (!Number.isInteger(index) || index < 1 || index > skills.length) {
|
|
427
|
+
console.log('请输入 skill 序号,或 a 一键添加该库');
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
await skillDetailFlow(skills[index - 1].skill.id);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function enableLibraryFlow(library, choices, scope = 'project') {
|
|
436
|
+
const plan = buildCollectionEnablePlan(state, choices.map((choice) => choice.skill.id), { scope });
|
|
437
|
+
const targetIds = new Set(plan.targetSkillIds);
|
|
438
|
+
const targets = choices.filter((choice) => targetIds.has(choice.skill.id));
|
|
439
|
+
const skipped = choices.filter((choice) => plan.skippedSkillIds.includes(choice.skill.id));
|
|
440
|
+
const pendingSetups = scope === 'project' ? plan.setups : [];
|
|
441
|
+
if (targets.length === 0) {
|
|
442
|
+
console.log(skipped.length ? `没有需要启用的 skill,已跳过 ${skipped.length} 个不参与一键加入` : '没有需要启用的 skill');
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
console.log(`将启用 ${targets.length} 个 skill;跳过 ${skipped.length} 个不参与一键加入。`);
|
|
447
|
+
targets.forEach((choice) => console.log(`- ${choice.skill.name}`));
|
|
448
|
+
let showSetupGuidance = false;
|
|
449
|
+
if (pendingSetups.length > 0) {
|
|
450
|
+
printSetupPreflight(pendingSetups);
|
|
451
|
+
console.log('1. 启用并显示配置指引\n2. 仅启用,稍后配置\nb. 取消');
|
|
452
|
+
const setupChoice = await ask('选择');
|
|
453
|
+
if (isBack(setupChoice)) return;
|
|
454
|
+
if (!['1', '2'].includes(setupChoice)) {
|
|
455
|
+
console.log('请输入 1、2 或 b');
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
showSetupGuidance = setupChoice === '1';
|
|
459
|
+
} else {
|
|
460
|
+
const confirm = await ask(`确认一键添加 ${library.source}/${library.collection}? y/N`);
|
|
461
|
+
if (!['y', 'yes'].includes(confirm.toLowerCase())) return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const result = await enableCollectionChoice(
|
|
465
|
+
roots,
|
|
466
|
+
state,
|
|
467
|
+
choices.map((choice) => choice.skill.id),
|
|
468
|
+
{ scope }
|
|
469
|
+
);
|
|
470
|
+
state = await loadTuiState(roots, state.projectPath);
|
|
471
|
+
result.outcomes
|
|
472
|
+
.filter((outcome) => outcome.status === 'failed')
|
|
473
|
+
.forEach((outcome) => console.log(`失败 ${outcome.alias}: ${outcome.error}`));
|
|
474
|
+
|
|
475
|
+
console.log(`已处理 ${library.collection}: 启用 ${result.counts.enabled},已存在 ${result.counts.unchanged},跳过 ${result.counts.skipped},失败 ${result.counts.failed}`);
|
|
476
|
+
if (!result.refresh.ok) {
|
|
477
|
+
console.log(`配置提醒刷新失败:${result.refresh.error}`);
|
|
478
|
+
}
|
|
479
|
+
const refreshedSetups = result.refresh.ok ? result.setups : pendingSetups;
|
|
480
|
+
const remainingSetups = refreshedSetups.filter((setup) => setup.status !== 'ready');
|
|
481
|
+
if (scope === 'project' && remainingSetups.length > 0) {
|
|
482
|
+
showSetupGuidance ? printSetupGuidance(remainingSetups) : console.log(`库已启用但仍待配置;稍后请运行 ${remainingSetups.map((setup) => setup.setupSkillName).join('、')}。`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function printCollectionSetupStatus(source, collection) {
|
|
487
|
+
const setup = (state.setups || []).find((item) => item.id === `${source}/${collection}`);
|
|
488
|
+
if (!setup) return;
|
|
489
|
+
const labels = { missing: setup.affectedEnabledSkillIds.length > 0 ? '待配置' : '启用后需配置', partial: '配置不完整', ready: '已就绪', invalid: '配置定义无效' };
|
|
490
|
+
console.log(`初始化: ${labels[setup.status] || setup.status}${setup.setupSkillName ? `(${setup.setupSkillName})` : ''}`);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function printSetupPreflight(setups) {
|
|
494
|
+
setups.forEach((setup) => {
|
|
495
|
+
const status = setup.status === 'partial' ? '配置不完整' : '尚未配置';
|
|
496
|
+
console.log(`初始化提醒: ${setup.id} ${status},缺少 ${setup.missingArtifacts.join('、')}`);
|
|
497
|
+
console.log(`初始化 skill: ${setup.setupSkillName}`);
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function printSetupGuidance(setups) {
|
|
502
|
+
setups.forEach((setup) => console.log(`下一步: ${setup.instruction}`));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function enableSkillFlow() {
|
|
506
|
+
const value = await chooseSkill('要启用的 skill 关键词或 id');
|
|
507
|
+
if (!value) return;
|
|
508
|
+
const result = await enableSkillChoice(roots, state, value);
|
|
509
|
+
state = await loadTuiState(roots, state.projectPath);
|
|
510
|
+
console.log(result.unchanged ? `已存在:${result.alias}` : `已启用:${result.alias}`);
|
|
511
|
+
if (result.claudeSync?.ok === false) {
|
|
512
|
+
console.log(`Claude Code 同步失败:${result.claudeSync.error}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function globalSkillsFlow() {
|
|
517
|
+
while (true) {
|
|
518
|
+
console.log('\n全局 Agents skill');
|
|
519
|
+
const globalAliases = listEnabledAliases(state, 'global');
|
|
520
|
+
if (globalAliases.length === 0) console.log('当前没有全局 skill');
|
|
521
|
+
else globalAliases.forEach((item) => console.log(`${item.index}. ${item.label}${item.canDisable ? '' : ' (只读)'}`));
|
|
522
|
+
console.log('a. 启用 skill 到全局\nd. 清理全局 skill\nb. 返回');
|
|
523
|
+
const choice = await ask('选择');
|
|
524
|
+
if (isBack(choice)) return;
|
|
525
|
+
if (choice === 'a') {
|
|
526
|
+
const value = await chooseSkill('要全局启用的 skill 关键词或 id', { scope: 'global' });
|
|
527
|
+
if (!value) continue;
|
|
528
|
+
const skill = findSkillById(value);
|
|
529
|
+
if (!skill) {
|
|
530
|
+
console.log(`找不到 skill:${value}`);
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const suggestedAlias = findSuggestedAlias(skill.id) || skill.name;
|
|
534
|
+
const alias = await ask(`全局 skill 名称 [${suggestedAlias}]`) || suggestedAlias;
|
|
535
|
+
const result = await enableSkillChoice(roots, state, skill.id, { scope: 'global', alias });
|
|
536
|
+
state = await loadTuiState(roots, state.projectPath);
|
|
537
|
+
console.log(result.unchanged ? `全局已存在:${result.alias}` : `全局已启用:${result.alias}`);
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (choice === 'd') {
|
|
541
|
+
if (globalAliases.length === 0) continue;
|
|
542
|
+
const selected = await ask('输入序号或 skill 名称');
|
|
543
|
+
const alias = resolveIndexedValue(globalAliases, selected, 'alias');
|
|
544
|
+
const item = globalAliases.find((candidate) => candidate.alias === alias);
|
|
545
|
+
if (!item?.canDisable) {
|
|
546
|
+
console.log('这个全局链接不属于当前原件库,只能查看,不能由当前库清理');
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const confirm = await ask(`清理全局 skill ${alias}? y/N`);
|
|
550
|
+
if (!['y', 'yes'].includes(confirm.toLowerCase())) continue;
|
|
551
|
+
const result = await disableAlias(state, alias, { scope: 'global', roots });
|
|
552
|
+
state = await loadTuiState(roots, state.projectPath);
|
|
553
|
+
console.log(result.removed ? `已清理全局:${alias}` : `未找到:${alias}`);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
console.log('未知操作');
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function hermesSkillsFlow() {
|
|
561
|
+
while (true) {
|
|
562
|
+
console.log('\nHermes skill');
|
|
563
|
+
const hermesAliases = listEnabledAliases(state, 'hermes');
|
|
564
|
+
if (hermesAliases.length === 0) console.log('当前没有 Hermes skill');
|
|
565
|
+
else hermesAliases.forEach((item) => console.log(`${item.index}. ${item.label}${item.canDisable ? '' : ' (只读)'}`));
|
|
566
|
+
console.log('a. 启用 skill 到 Hermes\nd. 清理 Hermes skill\nb. 返回');
|
|
567
|
+
const choice = await ask('选择');
|
|
568
|
+
if (isBack(choice)) return;
|
|
569
|
+
if (choice === 'a') {
|
|
570
|
+
const value = await chooseSkill('要启用到 Hermes 的 skill 关键词或 id', { scope: 'hermes' });
|
|
571
|
+
if (!value) continue;
|
|
572
|
+
const skill = findSkillById(value);
|
|
573
|
+
if (!skill) {
|
|
574
|
+
console.log(`找不到 skill:${value}`);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const suggestedAlias = findSuggestedAlias(skill.id) || skill.name;
|
|
578
|
+
const alias = await ask(`Hermes skill 名称 [${suggestedAlias}]`) || suggestedAlias;
|
|
579
|
+
const result = await enableSkillChoice(roots, state, skill.id, { scope: 'hermes', alias });
|
|
580
|
+
state = await loadTuiState(roots, state.projectPath);
|
|
581
|
+
console.log(result.unchanged ? `Hermes 已存在:${result.alias}` : `Hermes 已启用:${result.alias}`);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (choice === 'd') {
|
|
585
|
+
if (hermesAliases.length === 0) continue;
|
|
586
|
+
const selected = await ask('输入序号或 skill 名称');
|
|
587
|
+
const alias = resolveIndexedValue(hermesAliases, selected, 'alias');
|
|
588
|
+
const item = hermesAliases.find((candidate) => candidate.alias === alias);
|
|
589
|
+
if (!item?.canDisable) {
|
|
590
|
+
console.log('这个 Hermes 链接不属于当前原件库,只能查看,不能由当前库清理');
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const confirm = await ask(`清理 Hermes skill ${alias}? y/N`);
|
|
594
|
+
if (!['y', 'yes'].includes(confirm.toLowerCase())) continue;
|
|
595
|
+
const result = await disableAlias(state, alias, { scope: 'hermes', roots });
|
|
596
|
+
state = await loadTuiState(roots, state.projectPath);
|
|
597
|
+
console.log(result.removed ? `已清理 Hermes:${alias}` : `未找到:${alias}`);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
console.log('未知操作');
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function disableAliasFlow() {
|
|
605
|
+
const aliases = listEnabledAliases(state);
|
|
606
|
+
if (aliases.length === 0) {
|
|
607
|
+
console.log('当前项目没有 Agents skill');
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
aliases.forEach((item) => console.log(`${item.index}. ${item.label}${item.canDisable ? '' : ' (不可清理)'}`));
|
|
611
|
+
const selected = await ask('输入序号或 skill 名称');
|
|
612
|
+
const alias = resolveIndexedValue(aliases, selected, 'alias');
|
|
613
|
+
const result = await disableAlias(state, alias);
|
|
614
|
+
state = await loadTuiState(roots, state.projectPath);
|
|
615
|
+
console.log(result.removed ? `已清理:${alias}` : `未找到:${alias}`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async function syncClaudeFlow() {
|
|
619
|
+
const result = await syncClaude(state);
|
|
620
|
+
state = await loadTuiState(roots, state.projectPath);
|
|
621
|
+
console.log(result.unchanged ? 'Claude Code 已是最新' : `已同步 Claude Code:${result.linkPath}`);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async function editMetadataFlow() {
|
|
625
|
+
const value = await chooseSkill('要编辑 metadata 的 skill 关键词或 id');
|
|
626
|
+
if (!value) return;
|
|
627
|
+
await editAllMetadata(value);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async function skillDetailFlow(skillId) {
|
|
631
|
+
while (true) {
|
|
632
|
+
const skill = state.skills.find((item) => item.id === skillId);
|
|
633
|
+
if (!skill) {
|
|
634
|
+
console.log(`找不到 skill:${skillId}`);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
printSkillDetail(skill);
|
|
639
|
+
console.log([
|
|
640
|
+
'1. 启用到当前项目 Agents,并同步 Claude Code',
|
|
641
|
+
'2. 使用其他名称启用到当前项目',
|
|
642
|
+
'3. 启用到全局 Agents',
|
|
643
|
+
'4. 启用到 Hermes',
|
|
644
|
+
'5. 清理当前项目 skill',
|
|
645
|
+
'6. 编辑备注',
|
|
646
|
+
'7. 编辑 tags',
|
|
647
|
+
`8. ${skill.autoEnable === false ? '开启' : '关闭'}参与一键加入`,
|
|
648
|
+
'9. 编辑全部 metadata',
|
|
649
|
+
'b. 返回列表'
|
|
650
|
+
].join('\n'));
|
|
651
|
+
|
|
652
|
+
const choice = await ask('选择操作');
|
|
653
|
+
if (isBack(choice)) return;
|
|
654
|
+
|
|
655
|
+
if (choice === '1') await enableSkillById(skill.id);
|
|
656
|
+
else if (choice === '2') await enableSkillWithAlias(skill);
|
|
657
|
+
else if (choice === '3') await enableGlobalSkillById(skill.id);
|
|
658
|
+
else if (choice === '4') await enableHermesSkillById(skill.id);
|
|
659
|
+
else if (choice === '5') await disableSkillAlias(skill);
|
|
660
|
+
else if (choice === '6') await editNote(skill);
|
|
661
|
+
else if (choice === '7') await editTags(skill);
|
|
662
|
+
else if (choice === '8') await toggleAutoEnable(skill);
|
|
663
|
+
else if (choice === '9') await editAllMetadata(skill.id);
|
|
664
|
+
else console.log('未知操作');
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function printSkillDetail(skill) {
|
|
669
|
+
const enabled = findEnabledSkill(skill);
|
|
670
|
+
const globalEnabled = findGlobalSkill(skill);
|
|
671
|
+
const hermesEnabled = findHermesSkill(skill);
|
|
672
|
+
console.log('\nSkill 详情');
|
|
673
|
+
console.log(`名称: ${skill.name}`);
|
|
674
|
+
console.log(`ID: ${skill.id}`);
|
|
675
|
+
console.log(`来源: ${skill.source}/${skill.collection}`);
|
|
676
|
+
const statuses = [];
|
|
677
|
+
if (enabled) statuses.push(`项目=${enabled.alias}`);
|
|
678
|
+
if (globalEnabled) statuses.push(`全局=${globalEnabled.alias}`);
|
|
679
|
+
if (hermesEnabled) statuses.push(`Hermes=${hermesEnabled.alias}`);
|
|
680
|
+
console.log(`状态: ${statuses.length > 0 ? statuses.join(',') : '未启用'}`);
|
|
681
|
+
console.log(`一键加入: ${skill.autoEnable === false ? '否' : '是'}`);
|
|
682
|
+
console.log(`Tags: ${(skill.tags || []).join(', ') || '-'}`);
|
|
683
|
+
console.log(`介绍: ${getSkillIntroduction(skill)}`);
|
|
684
|
+
console.log(`路径: ${skill.path}`);
|
|
685
|
+
console.log('');
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async function enableSkillById(skillId) {
|
|
689
|
+
const result = await enableSkillChoice(roots, state, skillId);
|
|
690
|
+
await reportEnableResult(result);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async function enableSkillWithAlias(skill) {
|
|
694
|
+
const suggestedAlias = findSuggestedAlias(skill.id) || skill.name;
|
|
695
|
+
const alias = await ask(`项目中的 skill 名称 [${suggestedAlias}]`) || suggestedAlias;
|
|
696
|
+
const result = await enableSkillChoice(roots, state, skill.id, { alias });
|
|
697
|
+
await reportEnableResult(result);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async function enableGlobalSkillById(skillId) {
|
|
701
|
+
const skill = state.skills.find((item) => item.id === skillId);
|
|
702
|
+
if (!skill) return;
|
|
703
|
+
const suggestedAlias = findSuggestedAlias(skill.id) || skill.name;
|
|
704
|
+
const alias = await ask(`全局 skill 名称 [${suggestedAlias}]`) || suggestedAlias;
|
|
705
|
+
const result = await enableSkillChoice(roots, state, skill.id, { scope: 'global', alias });
|
|
706
|
+
await reportEnableResult(result);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async function enableHermesSkillById(skillId) {
|
|
710
|
+
const skill = state.skills.find((item) => item.id === skillId);
|
|
711
|
+
if (!skill) return;
|
|
712
|
+
if (!['official', 'github', 'personal'].includes(skill.source)) {
|
|
713
|
+
console.log('Hermes 只接受 official、github 或 personal 来源的 skill');
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const suggestedAlias = findSuggestedAlias(skill.id) || skill.name;
|
|
717
|
+
const alias = await ask(`Hermes skill 名称 [${suggestedAlias}]`) || suggestedAlias;
|
|
718
|
+
const result = await enableSkillChoice(roots, state, skill.id, { scope: 'hermes', alias });
|
|
719
|
+
await reportEnableResult(result);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async function reportEnableResult(result) {
|
|
723
|
+
state = await loadTuiState(roots, state.projectPath);
|
|
724
|
+
const prefix = result.scope === 'global' ? '全局' : result.scope === 'hermes' ? 'Hermes' : '项目';
|
|
725
|
+
console.log(result.unchanged ? `${prefix}已存在:${result.alias}` : `${prefix}已启用:${result.alias}`);
|
|
726
|
+
if (result.claudeSync?.ok === false) {
|
|
727
|
+
console.log(`Claude Code 同步失败:${result.claudeSync.error}`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function findSuggestedAlias(skillId) {
|
|
732
|
+
for (const advice of state.advice) {
|
|
733
|
+
const action = (advice.actions || []).find((item) => item.type === 'enable-with-alias' && item.skillId === skillId);
|
|
734
|
+
if (action) return action.alias;
|
|
735
|
+
}
|
|
736
|
+
return '';
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function findSkillById(skillId) {
|
|
740
|
+
return state.skills.find((skill) => skill.id === skillId);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async function disableSkillAlias(skill) {
|
|
744
|
+
const enabled = findEnabledSkill(skill);
|
|
745
|
+
if (!enabled) {
|
|
746
|
+
console.log('当前项目未启用这个 skill');
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
const confirm = await ask(`清理已启用 skill ${enabled.alias}? y/N`);
|
|
750
|
+
if (!['y', 'yes'].includes(confirm.toLowerCase())) return;
|
|
751
|
+
const result = await disableAlias(state, enabled.alias);
|
|
752
|
+
state = await loadTuiState(roots, state.projectPath);
|
|
753
|
+
console.log(result.removed ? `已清理:${enabled.alias}` : `未找到:${enabled.alias}`);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
async function editNote(skill) {
|
|
757
|
+
const note = await ask(`备注 [${skill.note || ''}]`);
|
|
758
|
+
if (!note) return;
|
|
759
|
+
await saveSkillMetadata(roots, state, skill.id, { note });
|
|
760
|
+
state = await loadTuiState(roots, state.projectPath);
|
|
761
|
+
console.log('已保存备注');
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
async function editTags(skill) {
|
|
765
|
+
const tags = await ask(`Tags 逗号分隔 [${(skill.tags || []).join(', ')}]`);
|
|
766
|
+
if (!tags) return;
|
|
767
|
+
await saveSkillMetadata(roots, state, skill.id, {
|
|
768
|
+
tags: tags.split(',').map((tag) => tag.trim()).filter(Boolean)
|
|
769
|
+
});
|
|
770
|
+
state = await loadTuiState(roots, state.projectPath);
|
|
771
|
+
console.log('已保存 tags');
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
async function toggleAutoEnable(skill) {
|
|
775
|
+
await saveSkillMetadata(roots, state, skill.id, {
|
|
776
|
+
autoEnable: skill.autoEnable === false
|
|
777
|
+
});
|
|
778
|
+
state = await loadTuiState(roots, state.projectPath);
|
|
779
|
+
console.log('已更新一键加入设置');
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async function editAllMetadata(skillId) {
|
|
783
|
+
const skill = state.skills.find((item) => item.id === skillId);
|
|
784
|
+
const note = await ask(`备注 [${skill?.note || ''}]`);
|
|
785
|
+
const tags = await ask(`Tags 逗号分隔 [${(skill?.tags || []).join(', ')}]`);
|
|
786
|
+
const autoEnableInput = await ask(`参与一键加入 true/false [${skill?.autoEnable !== false}]`);
|
|
787
|
+
|
|
788
|
+
const metadata = {};
|
|
789
|
+
if (note) metadata.note = note;
|
|
790
|
+
if (tags) metadata.tags = tags.split(',').map((tag) => tag.trim()).filter(Boolean);
|
|
791
|
+
if (autoEnableInput) metadata.autoEnable = parseBoolean(autoEnableInput);
|
|
792
|
+
|
|
793
|
+
await saveSkillMetadata(roots, state, skillId, metadata);
|
|
794
|
+
state = await loadTuiState(roots, state.projectPath);
|
|
795
|
+
console.log('已保存 metadata');
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function printAdvice() {
|
|
799
|
+
const advice = formatAdvice(state.advice);
|
|
800
|
+
if (advice.length === 0) {
|
|
801
|
+
console.log('当前没有 advice');
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
advice.forEach((item) => {
|
|
805
|
+
console.log(`${item.index}. [${item.severity}] ${item.title}`);
|
|
806
|
+
console.log(` ${item.detail}`);
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
async function chooseSkill(prompt, options = {}) {
|
|
811
|
+
const query = await ask(prompt);
|
|
812
|
+
const choices = listSkillChoices(state, {
|
|
813
|
+
query,
|
|
814
|
+
limit: options.scope === 'hermes' ? state.skills.length : 20,
|
|
815
|
+
scope: options.scope
|
|
816
|
+
})
|
|
817
|
+
.filter((choice) => options.scope !== 'hermes' || ['official', 'github', 'personal'].includes(choice.skill.source))
|
|
818
|
+
.slice(0, 20);
|
|
819
|
+
if (choices.length === 0) {
|
|
820
|
+
console.log(`没有匹配的 skill。当前原件库是 ${libraryRoot};如果你在 worktree 里开发,请用 --root 指向真实原件库。`);
|
|
821
|
+
return '';
|
|
822
|
+
}
|
|
823
|
+
if (choices.length === 1) {
|
|
824
|
+
console.log(`匹配:${choices[0].label}${choices[0].enabled ? ' (已启用)' : ''}`);
|
|
825
|
+
const confirm = await ask('直接使用这个 skill? Y/n');
|
|
826
|
+
if (!confirm || ['y', 'yes'].includes(confirm.toLowerCase())) return choices[0].id;
|
|
827
|
+
}
|
|
828
|
+
printSkillChoices(choices);
|
|
829
|
+
const selected = await ask('输入序号或完整 id');
|
|
830
|
+
return resolveIndexedValue(choices, selected, 'id');
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function printSkillChoices(choices) {
|
|
834
|
+
printCompactSkillChoices(getSkillPage(choices, 1, Math.max(choices.length, 1)));
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function printCompactSkillChoices(skillPage) {
|
|
838
|
+
const indexWidth = String(skillPage.end).length;
|
|
839
|
+
const tableLayout = getSkillTableLayout(output.columns);
|
|
840
|
+
console.log(`\nSkills ${skillPage.start}-${skillPage.end} / ${skillPage.total}`);
|
|
841
|
+
console.log(formatSkillTableHeader(tableLayout));
|
|
842
|
+
console.log(formatSkillTableDivider(tableLayout));
|
|
843
|
+
skillPage.choices.forEach((choice) => console.log(formatCompactSkillChoice(choice, indexWidth, tableLayout)));
|
|
844
|
+
if (skillPage.totalPages > 1) {
|
|
845
|
+
console.log(`第 ${skillPage.currentPage}/${skillPage.totalPages} 页`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function findEnabledSkill(skill) {
|
|
850
|
+
return state.enabled.find((enabled) => enabled.targetPath === skill.path);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function findGlobalSkill(skill) {
|
|
854
|
+
return state.global.find((enabled) => enabled.targetPath === skill.path);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function findHermesSkill(skill) {
|
|
858
|
+
return state.hermes.find((enabled) => enabled.targetPath === skill.path);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function groupSkillChoices(choices) {
|
|
862
|
+
const groups = new Map();
|
|
863
|
+
for (const choice of choices) {
|
|
864
|
+
const key = `${choice.skill.source}/${choice.skill.collection}`;
|
|
865
|
+
if (!groups.has(key)) {
|
|
866
|
+
groups.set(key, {
|
|
867
|
+
index: groups.size + 1,
|
|
868
|
+
key,
|
|
869
|
+
source: choice.skill.source,
|
|
870
|
+
collection: choice.skill.collection,
|
|
871
|
+
collectionPath: choice.skill.collectionPath,
|
|
872
|
+
total: 0,
|
|
873
|
+
enabled: 0,
|
|
874
|
+
skipped: 0,
|
|
875
|
+
bulkable: 0
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
const group = groups.get(key);
|
|
879
|
+
group.total += 1;
|
|
880
|
+
if (choice.enabled) group.enabled += 1;
|
|
881
|
+
if (choice.skill.autoEnable === false) group.skipped += 1;
|
|
882
|
+
if (!choice.enabled && choice.skill.autoEnable !== false) group.bulkable += 1;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
return [...groups.values()];
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function printLibraryChoices(libraries) {
|
|
889
|
+
libraries.forEach((library) => {
|
|
890
|
+
const parts = [`${library.total} 个 skill`];
|
|
891
|
+
if (library.enabled) parts.push(`已启用 ${library.enabled}`);
|
|
892
|
+
if (library.skipped) parts.push(`一键跳过 ${library.skipped}`);
|
|
893
|
+
const setup = (state.setups || []).find((item) => item.id === `${library.source}/${library.collection}`);
|
|
894
|
+
if (setup?.status === 'missing') parts.push(setup.affectedEnabledSkillIds.length > 0 ? '待配置' : '需初始化');
|
|
895
|
+
else if (setup?.status === 'partial') parts.push('配置不完整');
|
|
896
|
+
else if (setup?.status === 'ready') parts.push('已就绪');
|
|
897
|
+
console.log(`${library.index}. ${library.source}/${library.collection} (${parts.join(',')})`);
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function reindexChoices(choices) {
|
|
902
|
+
return choices.map((choice, index) => ({ ...choice, index: index + 1 }));
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function resolveIndexedValue(items, selected, field) {
|
|
906
|
+
const text = String(selected || '').trim();
|
|
907
|
+
const index = Number(text);
|
|
908
|
+
if (Number.isInteger(index) && index >= 1 && index <= items.length) {
|
|
909
|
+
return items[index - 1][field];
|
|
910
|
+
}
|
|
911
|
+
return text;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function parseBoolean(value) {
|
|
915
|
+
const text = String(value).trim().toLowerCase();
|
|
916
|
+
if (['true', '1', 'yes', 'y', 'on'].includes(text)) return true;
|
|
917
|
+
if (['false', '0', 'no', 'n', 'off'].includes(text)) return false;
|
|
918
|
+
throw new Error('autoEnable 必须是 true 或 false');
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
async function ask(prompt) {
|
|
922
|
+
try {
|
|
923
|
+
return (await rl.question(`${prompt}: `)).trim();
|
|
924
|
+
} catch (error) {
|
|
925
|
+
if (error.code === 'ABORT_ERR' || error.code === 'ERR_USE_AFTER_CLOSE' || error.code === 'ERR_INVALID_STATE' || error.name === 'AbortError') {
|
|
926
|
+
rl.close();
|
|
927
|
+
console.log('\nbyebye 👋');
|
|
928
|
+
process.exit(0);
|
|
929
|
+
}
|
|
930
|
+
throw error;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
async function runCommand(command, args) {
|
|
935
|
+
await new Promise((resolve, reject) => {
|
|
936
|
+
const child = spawn(command, args, {
|
|
937
|
+
cwd: codeRootDir,
|
|
938
|
+
stdio: 'inherit'
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
child.on('error', reject);
|
|
942
|
+
child.on('exit', (code) => {
|
|
943
|
+
code === 0 ? resolve() : reject(new Error(`${command} exited with code ${code}`));
|
|
944
|
+
});
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function isBack(value) {
|
|
949
|
+
return String(value || '').trim().toLowerCase() === 'b';
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
async function batchTranslateNotesFlow() {
|
|
953
|
+
// 1. 筛选待翻译的 skills
|
|
954
|
+
const pending = state.skills.filter((skill) => {
|
|
955
|
+
if (skill.source === 'archived') return false;
|
|
956
|
+
const note = String(skill.note || '').trim();
|
|
957
|
+
const description = String(skill.description || '').trim();
|
|
958
|
+
return description && !note;
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
if (pending.length === 0) {
|
|
962
|
+
console.log('所有 skill 都已有中文介绍');
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
console.log(`\n待翻译 skill: ${pending.length} 个`);
|
|
967
|
+
|
|
968
|
+
// 2. 让用户选择批次大小
|
|
969
|
+
console.log('处理方式:');
|
|
970
|
+
console.log(' 1. 处理 5 个');
|
|
971
|
+
console.log(' 2. 处理 10 个');
|
|
972
|
+
console.log(' 3. 处理全部');
|
|
973
|
+
console.log(' b. 返回');
|
|
974
|
+
|
|
975
|
+
const batchChoice = await ask('选择');
|
|
976
|
+
if (isBack(batchChoice)) return;
|
|
977
|
+
|
|
978
|
+
let batchSize;
|
|
979
|
+
if (batchChoice === '1') batchSize = 5;
|
|
980
|
+
else if (batchChoice === '2') batchSize = 10;
|
|
981
|
+
else if (batchChoice === '3') batchSize = pending.length;
|
|
982
|
+
else {
|
|
983
|
+
console.log('未知选项');
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const toProcess = pending.slice(0, batchSize);
|
|
988
|
+
console.log(`\n将处理 ${toProcess.length} 个 skill:\n`);
|
|
989
|
+
|
|
990
|
+
// 3. 显示待处理列表
|
|
991
|
+
toProcess.forEach((skill, index) => {
|
|
992
|
+
console.log(`${index + 1}. ${skill.name} [${skill.source}/${skill.collection}]`);
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
const confirmStart = await ask('\n开始处理? y/N');
|
|
996
|
+
if (!['y', 'yes'].includes(confirmStart.toLowerCase())) return;
|
|
997
|
+
|
|
998
|
+
// 4. 逐个处理
|
|
999
|
+
const results = [];
|
|
1000
|
+
for (let i = 0; i < toProcess.length; i += 1) {
|
|
1001
|
+
const skill = toProcess[i];
|
|
1002
|
+
console.log(`\n${'─'.repeat(50)}`);
|
|
1003
|
+
console.log(`[${i + 1}/${toProcess.length}] ${skill.name}`);
|
|
1004
|
+
console.log(`来源: ${skill.source}/${skill.collection}`);
|
|
1005
|
+
console.log(`\n英文描述:`);
|
|
1006
|
+
console.log(skill.description);
|
|
1007
|
+
console.log('');
|
|
1008
|
+
|
|
1009
|
+
const note = await ask('输入中文介绍 (留空跳过, q 退出)');
|
|
1010
|
+
if (note.toLowerCase() === 'q') {
|
|
1011
|
+
console.log('已中断');
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
1014
|
+
if (!note) {
|
|
1015
|
+
console.log('已跳过');
|
|
1016
|
+
results.push({ skill, status: 'skipped' });
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
try {
|
|
1021
|
+
await saveSkillMetadata(roots, state, skill.id, { note });
|
|
1022
|
+
state = await loadTuiState(roots, state.projectPath);
|
|
1023
|
+
console.log('✓ 已保存');
|
|
1024
|
+
results.push({ skill, status: 'saved', note });
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
console.log(`✗ 保存失败: ${error.message}`);
|
|
1027
|
+
results.push({ skill, status: 'failed', error: error.message });
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// 5. 汇总结果
|
|
1032
|
+
console.log(`\n${'─'.repeat(50)}`);
|
|
1033
|
+
console.log('处理完成');
|
|
1034
|
+
const saved = results.filter((r) => r.status === 'saved').length;
|
|
1035
|
+
const skipped = results.filter((r) => r.status === 'skipped').length;
|
|
1036
|
+
const failed = results.filter((r) => r.status === 'failed').length;
|
|
1037
|
+
console.log(`已保存: ${saved},已跳过: ${skipped},失败: ${failed}`);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function parseArgs(args) {
|
|
1041
|
+
const options = { root: '', projectPath: '' };
|
|
1042
|
+
|
|
1043
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1044
|
+
const arg = args[index];
|
|
1045
|
+
if (arg === '--root') {
|
|
1046
|
+
options.root = args[index + 1] || '';
|
|
1047
|
+
index += 1;
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
if (arg.startsWith('--root=')) {
|
|
1051
|
+
options.root = arg.slice('--root='.length);
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
if (!options.projectPath) options.projectPath = arg;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
return options;
|
|
1058
|
+
}
|