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,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Print comparable collection profiles for agent review.
|
|
4
|
+
* The script preserves the historical command name but does not decide conflicts.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node check-conflicts.cjs <collection-id>
|
|
8
|
+
* node check-conflicts.cjs <collection-id> --against <id,id,...>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const dataPath = path.join(__dirname, '..', 'references', 'featured-skills.json');
|
|
15
|
+
|
|
16
|
+
function loadJSON(filePath) {
|
|
17
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseArgs(argv) {
|
|
21
|
+
const args = [...argv];
|
|
22
|
+
const collectionId = args.shift() || '';
|
|
23
|
+
let against = [];
|
|
24
|
+
|
|
25
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
26
|
+
const arg = args[index];
|
|
27
|
+
if (arg === '--against') {
|
|
28
|
+
against = String(args[index + 1] || '').split(',').filter(Boolean);
|
|
29
|
+
index += 1;
|
|
30
|
+
} else if (arg.startsWith('--against=')) {
|
|
31
|
+
against = arg.slice('--against='.length).split(',').filter(Boolean);
|
|
32
|
+
} else {
|
|
33
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { collectionId, against };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getCollections(data) {
|
|
41
|
+
return Object.values(data.featured || {})
|
|
42
|
+
.flatMap(category => category.collections || []);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function summarizeCollection(collection) {
|
|
46
|
+
return {
|
|
47
|
+
id: collection.id,
|
|
48
|
+
name: collection.name || collection.id,
|
|
49
|
+
description: collection.description || collection.reason || collection.note || '',
|
|
50
|
+
tags: collection.tags || [],
|
|
51
|
+
capabilities: collection.skills || collection.includes || []
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildComparison(collections, candidateId, againstIds = []) {
|
|
56
|
+
const byId = new Map(collections.map(collection => [collection.id, collection]));
|
|
57
|
+
const candidate = byId.get(candidateId);
|
|
58
|
+
if (!candidate) throw new Error(`Unknown collection: ${candidateId}`);
|
|
59
|
+
|
|
60
|
+
const targetIds = againstIds.length > 0
|
|
61
|
+
? againstIds
|
|
62
|
+
: collections.map(collection => collection.id).filter(id => id !== candidateId);
|
|
63
|
+
|
|
64
|
+
const comparisons = targetIds
|
|
65
|
+
.filter(id => id !== candidateId)
|
|
66
|
+
.map(id => {
|
|
67
|
+
const collection = byId.get(id);
|
|
68
|
+
if (!collection) throw new Error(`Unknown comparison collection: ${id}`);
|
|
69
|
+
return summarizeCollection(collection);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
candidate: summarizeCollection(candidate),
|
|
74
|
+
comparisons
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function printProfile(label, collection) {
|
|
79
|
+
console.log(`## ${label}: ${collection.id}`);
|
|
80
|
+
console.log(`名称: ${collection.name}`);
|
|
81
|
+
console.log(`描述: ${collection.description || '无'}`);
|
|
82
|
+
console.log(`标签: ${collection.tags.join(', ') || '无'}`);
|
|
83
|
+
console.log(`能力: ${collection.capabilities.join(', ') || '无'}\n`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function printReport(result) {
|
|
87
|
+
console.log('# Skillcaddy 能力重叠审查素材\n');
|
|
88
|
+
printProfile('候选库', result.candidate);
|
|
89
|
+
for (const comparison of result.comparisons) {
|
|
90
|
+
printProfile('对照库', comparison);
|
|
91
|
+
}
|
|
92
|
+
console.log('请根据名称、描述、标签和能力清单判断它们是互补、重复还是信息不足。');
|
|
93
|
+
console.log('本脚本不包含固定库名规则,也不自动给出冲突结论。');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (require.main === module) {
|
|
97
|
+
try {
|
|
98
|
+
const { collectionId, against } = parseArgs(process.argv.slice(2));
|
|
99
|
+
if (!collectionId) {
|
|
100
|
+
console.error('Usage: node check-conflicts.cjs <collection-id> [--against <id,id,...>]');
|
|
101
|
+
process.exitCode = 1;
|
|
102
|
+
} else {
|
|
103
|
+
const data = loadJSON(dataPath);
|
|
104
|
+
printReport(buildComparison(getCollections(data), collectionId, against));
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error(error.message);
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
buildComparison,
|
|
114
|
+
getCollections,
|
|
115
|
+
parseArgs,
|
|
116
|
+
summarizeCollection
|
|
117
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 全局 Skills 检测脚本
|
|
4
|
+
* 用法: node check-global-skills.cjs
|
|
5
|
+
*
|
|
6
|
+
* 检测全局 skills 目录,建议用户安装 skillcaddy
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
|
|
13
|
+
// 跨平台路径检测
|
|
14
|
+
const globalPaths = {
|
|
15
|
+
darwin: {
|
|
16
|
+
codex: path.join(os.homedir(), '.agents/skills'),
|
|
17
|
+
claude: path.join(os.homedir(), '.claude/skills')
|
|
18
|
+
},
|
|
19
|
+
linux: {
|
|
20
|
+
codex: path.join(os.homedir(), '.agents/skills'),
|
|
21
|
+
claude: path.join(os.homedir(), '.claude/skills')
|
|
22
|
+
},
|
|
23
|
+
win32: {
|
|
24
|
+
codex: path.join(os.homedir(), '.agents/skills'),
|
|
25
|
+
claude: path.join(os.homedir(), '.claude/skills'),
|
|
26
|
+
// Windows 可能的其他路径
|
|
27
|
+
codexAlt: path.join(process.env.APPDATA || '', '.agents/skills'),
|
|
28
|
+
claudeAlt: path.join(process.env.APPDATA || '', '.claude/skills')
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function checkGlobalSkills() {
|
|
33
|
+
const platform = os.platform();
|
|
34
|
+
const paths = globalPaths[platform] || globalPaths.darwin;
|
|
35
|
+
|
|
36
|
+
const results = {
|
|
37
|
+
platform: platform,
|
|
38
|
+
hasGlobalSkills: false,
|
|
39
|
+
hasSkillcaddy: false,
|
|
40
|
+
globalPaths: [],
|
|
41
|
+
skillcaddyPaths: [],
|
|
42
|
+
recommendations: []
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// 检查每个可能的路径
|
|
46
|
+
for (const [type, skillPath] of Object.entries(paths)) {
|
|
47
|
+
if (!skillPath) continue;
|
|
48
|
+
|
|
49
|
+
if (fs.existsSync(skillPath)) {
|
|
50
|
+
const isDir = fs.statSync(skillPath).isDirectory();
|
|
51
|
+
if (isDir) {
|
|
52
|
+
results.hasGlobalSkills = true;
|
|
53
|
+
results.globalPaths.push({ type, path: skillPath });
|
|
54
|
+
|
|
55
|
+
// 检查是否包含 skillcaddy-manager
|
|
56
|
+
const skillcaddyPath = path.join(skillPath, 'skillcaddy-manager');
|
|
57
|
+
if (fs.existsSync(skillcaddyPath)) {
|
|
58
|
+
results.hasSkillcaddy = true;
|
|
59
|
+
results.skillcaddyPaths.push(skillcaddyPath);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 检查目录内容
|
|
63
|
+
try {
|
|
64
|
+
const items = fs.readdirSync(skillPath).filter(item => {
|
|
65
|
+
const itemPath = path.join(skillPath, item);
|
|
66
|
+
return fs.statSync(itemPath).isDirectory() && !item.startsWith('.');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (items.length > 0 && !results.skillCount) {
|
|
70
|
+
results.skillCount = items.length;
|
|
71
|
+
results.skillList = items.slice(0, 5); // 只显示前5个
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// 忽略读取错误
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return results;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function printReport(results) {
|
|
84
|
+
console.log('## 🔍 全局 Skills 检测报告\n');
|
|
85
|
+
console.log(`**操作系统**: ${results.platform}\n`);
|
|
86
|
+
|
|
87
|
+
if (!results.hasGlobalSkills) {
|
|
88
|
+
console.log('✅ 未检测到全局 skills 目录\n');
|
|
89
|
+
console.log('建议:');
|
|
90
|
+
console.log(' - 如需管理多个项目的 skills,可安装 Skillcaddy');
|
|
91
|
+
console.log(' - 访问: https://github.com/chenweil/skillcaddy\n');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log('📁 检测到全局 Skills 目录:\n');
|
|
96
|
+
results.globalPaths.forEach(({ type, path }) => {
|
|
97
|
+
console.log(` ${type}: ${path}`);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (results.skillCount) {
|
|
101
|
+
console.log(`\n📊 发现 ${results.skillCount} 个全局 skills:`);
|
|
102
|
+
results.skillList.forEach(skill => {
|
|
103
|
+
console.log(` - ${skill}`);
|
|
104
|
+
});
|
|
105
|
+
if (results.skillCount > 5) {
|
|
106
|
+
console.log(` ... 还有 ${results.skillCount - 5} 个`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log('\n---\n');
|
|
111
|
+
|
|
112
|
+
if (results.hasSkillcaddy) {
|
|
113
|
+
console.log('✅ 已安装 Skillcaddy\n');
|
|
114
|
+
console.log('全局 skillcaddy-manager 位置:');
|
|
115
|
+
results.skillcaddyPaths.forEach(p => {
|
|
116
|
+
console.log(` ${p}`);
|
|
117
|
+
});
|
|
118
|
+
console.log('\n💡 建议:');
|
|
119
|
+
console.log(' - 在项目中使用 skillcaddy-manager 启用/禁用 skills');
|
|
120
|
+
console.log(' - 使用 Web UI 管理: http://127.0.0.1:4173\n');
|
|
121
|
+
} else {
|
|
122
|
+
console.log('⚠️ 未检测到 Skillcaddy\n');
|
|
123
|
+
console.log('💡 建议安装 Skillcaddy:\n');
|
|
124
|
+
console.log('**为什么需要 Skillcaddy?**');
|
|
125
|
+
console.log(' - ✅ 集中管理所有 skills (单一事实来源)');
|
|
126
|
+
console.log(' - ✅ 按项目启用/禁用 (避免污染)');
|
|
127
|
+
console.log(' - ✅ 多 Agent 兼容 (Claude Code / Codex / OpenCode)');
|
|
128
|
+
console.log(' - ✅ 版本控制和更新管理');
|
|
129
|
+
console.log(' - ✅ 避免重复复制和版本漂移\n');
|
|
130
|
+
|
|
131
|
+
console.log('**安装步骤:**');
|
|
132
|
+
console.log('```bash');
|
|
133
|
+
console.log('# 克隆 Skillcaddy');
|
|
134
|
+
console.log('git clone https://github.com/chenweil/skillcaddy.git');
|
|
135
|
+
console.log('cd skillcaddy');
|
|
136
|
+
console.log('');
|
|
137
|
+
console.log('# 启动 Web 管理器');
|
|
138
|
+
console.log('npm start');
|
|
139
|
+
console.log('');
|
|
140
|
+
console.log('# 访问 http://127.0.0.1:4173');
|
|
141
|
+
console.log('```\n');
|
|
142
|
+
|
|
143
|
+
console.log('**迁移全局 Skills 到 Skillcaddy:**');
|
|
144
|
+
console.log('```bash');
|
|
145
|
+
console.log('# 1. 将全局 skills 移动到 Skillcaddy 库');
|
|
146
|
+
console.log('mv ~/.agents/skills/my-skill /path/to/skillcaddy/personal/my-skill');
|
|
147
|
+
console.log('');
|
|
148
|
+
console.log('# 2. 删除全局目录 (可选)');
|
|
149
|
+
console.log('rm -rf ~/.agents/skills');
|
|
150
|
+
console.log('');
|
|
151
|
+
console.log('# 3. 在项目中按需启用');
|
|
152
|
+
console.log('# 使用 Web UI 或 skillcaddy-manager skill');
|
|
153
|
+
console.log('```\n');
|
|
154
|
+
|
|
155
|
+
console.log('**了解更多:**');
|
|
156
|
+
console.log(' - GitHub: https://github.com/chenweil/skillcaddy');
|
|
157
|
+
console.log(' - 文档: /path/to/skillcaddy/README.md\n');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 主程序
|
|
162
|
+
const results = checkGlobalSkills();
|
|
163
|
+
printReport(results);
|
|
164
|
+
|
|
165
|
+
// 导出函数供其他脚本使用
|
|
166
|
+
module.exports = {
|
|
167
|
+
checkGlobalSkills,
|
|
168
|
+
globalPaths
|
|
169
|
+
};
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PORT = 4173;
|
|
6
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
7
|
+
const MAX_NOTE_LENGTH = 500;
|
|
8
|
+
const MAX_TAGS = 12;
|
|
9
|
+
const MAX_TAG_LENGTH = 40;
|
|
10
|
+
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const opts = {
|
|
13
|
+
command: '',
|
|
14
|
+
file: '',
|
|
15
|
+
port: DEFAULT_PORT,
|
|
16
|
+
project: '.',
|
|
17
|
+
yes: false,
|
|
18
|
+
forceRewrite: false
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
22
|
+
const arg = argv[index];
|
|
23
|
+
if (arg === 'list' || arg === 'apply') opts.command = arg;
|
|
24
|
+
else if (arg === '--yes') opts.yes = true;
|
|
25
|
+
else if (arg === '--force-rewrite') opts.forceRewrite = true;
|
|
26
|
+
else if (arg.startsWith('--port=')) opts.port = parsePort(arg.slice('--port='.length));
|
|
27
|
+
else if (arg === '--port') opts.port = parsePort(argv[++index]);
|
|
28
|
+
else if (arg.startsWith('--project=')) opts.project = arg.slice('--project='.length);
|
|
29
|
+
else if (arg === '--project') opts.project = argv[++index] || '.';
|
|
30
|
+
else if (!arg.startsWith('--') && !opts.file) opts.file = arg;
|
|
31
|
+
else throw new Error(`未知参数: ${arg}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return opts;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parsePort(value) {
|
|
38
|
+
const port = Number(value);
|
|
39
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) throw new Error(`无效端口: ${value}`);
|
|
40
|
+
return port;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function fetchState(port, project) {
|
|
44
|
+
const url = `http://127.0.0.1:${port}/api/state?projectPath=${encodeURIComponent(path.resolve(project))}`;
|
|
45
|
+
return requestJson(url);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function postMetadata(port, payload) {
|
|
49
|
+
return requestJson(`http://127.0.0.1:${port}/api/skill-metadata`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
body: JSON.stringify(payload)
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function requestJson(url, options = {}) {
|
|
57
|
+
const res = await fetch(url, {
|
|
58
|
+
...options,
|
|
59
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
60
|
+
});
|
|
61
|
+
const text = await res.text();
|
|
62
|
+
let body;
|
|
63
|
+
try {
|
|
64
|
+
body = text ? JSON.parse(text) : {};
|
|
65
|
+
} catch {
|
|
66
|
+
throw new Error(`${options.method || 'GET'} ${url} 返回了无效 JSON`);
|
|
67
|
+
}
|
|
68
|
+
if (!res.ok || body.ok === false) {
|
|
69
|
+
throw new Error(body.error || `${options.method || 'GET'} ${url} failed: ${res.status}`);
|
|
70
|
+
}
|
|
71
|
+
return body;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function needsTranslation(skill) {
|
|
75
|
+
if (skill.source === 'archived') return false;
|
|
76
|
+
if (String(skill.note || '').trim()) return false;
|
|
77
|
+
|
|
78
|
+
const description = String(skill.description || '').trim();
|
|
79
|
+
if (!hasMeaningfulText(description)) return false;
|
|
80
|
+
return !isMostlyChinese(description);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function hasMeaningfulText(value) {
|
|
84
|
+
return (String(value).match(/[A-Za-z\u3400-\u9fff]/gu) || []).length >= 3;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isMostlyChinese(value) {
|
|
88
|
+
const text = String(value);
|
|
89
|
+
const chinese = (text.match(/[\u3400-\u9fff]/gu) || []).length;
|
|
90
|
+
const latin = (text.match(/[A-Za-z]/g) || []).length;
|
|
91
|
+
return chinese >= 4 && chinese >= latin;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function createTranslationManifest(state) {
|
|
95
|
+
return {
|
|
96
|
+
schemaVersion: 1,
|
|
97
|
+
libraryRoot: path.resolve(state.rootDir),
|
|
98
|
+
entries: (state.skills || [])
|
|
99
|
+
.filter(needsTranslation)
|
|
100
|
+
.map((skill) => ({
|
|
101
|
+
id: skill.id,
|
|
102
|
+
source: skill.source,
|
|
103
|
+
collection: skill.collection,
|
|
104
|
+
name: skill.name,
|
|
105
|
+
description: skill.description,
|
|
106
|
+
note: ''
|
|
107
|
+
}))
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseManifest(raw) {
|
|
112
|
+
const manifest = JSON.parse(raw);
|
|
113
|
+
if (!manifest || Array.isArray(manifest) || typeof manifest !== 'object') throw new Error('manifest 必须是带 libraryRoot 和 entries 的对象');
|
|
114
|
+
if (manifest.schemaVersion !== 1) throw new Error('manifest schemaVersion 必须是 1');
|
|
115
|
+
if (!manifest.libraryRoot || typeof manifest.libraryRoot !== 'string') throw new Error('manifest 缺少 libraryRoot');
|
|
116
|
+
if (!Array.isArray(manifest.entries)) throw new Error('manifest entries 必须是数组');
|
|
117
|
+
return manifest;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function preflightManifest(manifest, state, options = {}) {
|
|
121
|
+
const errors = [];
|
|
122
|
+
const changes = [];
|
|
123
|
+
const unchanged = [];
|
|
124
|
+
const seenIds = new Set();
|
|
125
|
+
const expectedRoot = path.resolve(state.rootDir);
|
|
126
|
+
|
|
127
|
+
if (path.resolve(manifest.libraryRoot) !== expectedRoot) {
|
|
128
|
+
errors.push(`libraryRoot 不匹配: manifest=${path.resolve(manifest.libraryRoot)} server=${expectedRoot}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const byId = new Map((state.skills || []).map((skill) => [skill.id, skill]));
|
|
132
|
+
manifest.entries.forEach((entry, index) => {
|
|
133
|
+
const label = entry?.id || `entries[${index}]`;
|
|
134
|
+
try {
|
|
135
|
+
const normalized = normalizeEntry(entry, label);
|
|
136
|
+
if (seenIds.has(normalized.id)) throw new Error('id 重复');
|
|
137
|
+
seenIds.add(normalized.id);
|
|
138
|
+
|
|
139
|
+
const skill = byId.get(normalized.id);
|
|
140
|
+
if (!skill) throw new Error('找不到对应 skill');
|
|
141
|
+
const currentNote = String(skill.note || '').trim();
|
|
142
|
+
if (currentNote && currentNote !== normalized.note && !options.forceRewrite) {
|
|
143
|
+
throw new Error('已有 note,默认拒绝覆盖;确需重写时使用 --force-rewrite');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const payload = {
|
|
147
|
+
skillPath: skill.path,
|
|
148
|
+
note: normalized.note,
|
|
149
|
+
tags: normalized.tags ?? skill.tags ?? [],
|
|
150
|
+
autoEnable: normalized.autoEnable ?? skill.autoEnable ?? true
|
|
151
|
+
};
|
|
152
|
+
if (metadataMatches(skill, payload)) unchanged.push(normalized.id);
|
|
153
|
+
else changes.push({ id: normalized.id, skill, payload });
|
|
154
|
+
} catch (error) {
|
|
155
|
+
errors.push(`${label}: ${error.message}`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (errors.length > 0) {
|
|
160
|
+
const error = new Error(`预检失败:\n- ${errors.join('\n- ')}`);
|
|
161
|
+
error.errors = errors;
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { libraryRoot: expectedRoot, changes, unchanged };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function normalizeEntry(entry, label) {
|
|
169
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) throw new Error('条目必须是对象');
|
|
170
|
+
const id = typeof entry.id === 'string' ? entry.id.trim() : '';
|
|
171
|
+
if (!id) throw new Error('id 不能为空');
|
|
172
|
+
if (typeof entry.note !== 'string') throw new Error('note 必须是字符串');
|
|
173
|
+
const note = entry.note.trim();
|
|
174
|
+
if (!note) throw new Error('note 不能为空');
|
|
175
|
+
if (note.length > MAX_NOTE_LENGTH) throw new Error(`note 不能超过 ${MAX_NOTE_LENGTH} 个字符`);
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
id,
|
|
179
|
+
note,
|
|
180
|
+
tags: entry.tags === undefined ? undefined : normalizeTags(entry.tags),
|
|
181
|
+
autoEnable: entry.autoEnable === undefined ? undefined : normalizeAutoEnable(entry.autoEnable, label)
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeTags(tags) {
|
|
186
|
+
if (!Array.isArray(tags)) throw new Error('tags 必须是字符串数组');
|
|
187
|
+
const normalized = [];
|
|
188
|
+
for (const tag of tags) {
|
|
189
|
+
if (typeof tag !== 'string') throw new Error('tags 只能包含字符串');
|
|
190
|
+
const value = tag.trim();
|
|
191
|
+
if (!value) continue;
|
|
192
|
+
if (value.length > MAX_TAG_LENGTH) throw new Error(`tag 不能超过 ${MAX_TAG_LENGTH} 个字符: ${value}`);
|
|
193
|
+
if (!normalized.includes(value)) normalized.push(value);
|
|
194
|
+
}
|
|
195
|
+
if (normalized.length > MAX_TAGS) throw new Error(`tags 不能超过 ${MAX_TAGS} 个`);
|
|
196
|
+
return normalized;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function normalizeAutoEnable(value) {
|
|
200
|
+
if (typeof value !== 'boolean') throw new Error('autoEnable 必须是布尔值');
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function metadataMatches(skill, payload) {
|
|
205
|
+
return String(skill.note || '').trim() === payload.note
|
|
206
|
+
&& Boolean(skill.autoEnable ?? true) === payload.autoEnable
|
|
207
|
+
&& JSON.stringify(skill.tags || []) === JSON.stringify(payload.tags);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function executePlan(plan, options = {}) {
|
|
211
|
+
if (!options.yes) return { dryRun: true, updated: 0, failed: 0 };
|
|
212
|
+
|
|
213
|
+
let updated = 0;
|
|
214
|
+
let failed = 0;
|
|
215
|
+
for (const change of plan.changes) {
|
|
216
|
+
try {
|
|
217
|
+
await (options.postMetadata || postMetadata)(options.port, change.payload);
|
|
218
|
+
process.stderr.write(`✓ ${change.id}\n`);
|
|
219
|
+
updated += 1;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
process.stderr.write(`✗ ${change.id}: ${error.message}\n`);
|
|
222
|
+
failed += 1;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { dryRun: false, updated, failed };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function printPlan(plan) {
|
|
229
|
+
process.stderr.write(`目标库: ${plan.libraryRoot}\n`);
|
|
230
|
+
process.stderr.write(`计划更新: ${plan.changes.length},无需更新: ${plan.unchanged.length}\n`);
|
|
231
|
+
plan.changes.forEach((change) => process.stderr.write(`- ${change.id}\n`));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function listCommand(opts) {
|
|
235
|
+
const state = await fetchState(opts.port, opts.project);
|
|
236
|
+
const manifest = createTranslationManifest(state);
|
|
237
|
+
process.stdout.write(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
238
|
+
process.stderr.write(`待翻译: ${manifest.entries.length} 个 skill\n`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function applyCommand(opts) {
|
|
242
|
+
if (!opts.file) throw new Error('apply 需要 manifest 文件路径');
|
|
243
|
+
const manifest = parseManifest(fs.readFileSync(path.resolve(opts.file), 'utf8'));
|
|
244
|
+
const state = await fetchState(opts.port, opts.project);
|
|
245
|
+
const plan = preflightManifest(manifest, state, { forceRewrite: opts.forceRewrite });
|
|
246
|
+
printPlan(plan);
|
|
247
|
+
|
|
248
|
+
const result = await executePlan(plan, {
|
|
249
|
+
yes: opts.yes,
|
|
250
|
+
port: opts.port
|
|
251
|
+
});
|
|
252
|
+
if (result.dryRun) {
|
|
253
|
+
process.stderr.write('仅完成预检,未写入;确认后加 --yes 执行。\n');
|
|
254
|
+
} else {
|
|
255
|
+
process.stderr.write(`完成: 成功 ${result.updated},失败 ${result.failed}\n`);
|
|
256
|
+
}
|
|
257
|
+
return result.failed > 0 ? 1 : 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function printUsage() {
|
|
261
|
+
process.stderr.write('用法:\n');
|
|
262
|
+
process.stderr.write(' node skills/skillcaddy-manager/scripts/translate-skill-notes.cjs list [--port=4173] [--project=.]\n');
|
|
263
|
+
process.stderr.write(' node skills/skillcaddy-manager/scripts/translate-skill-notes.cjs apply <manifest.json> [--yes] [--force-rewrite]\n');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function main(argv = process.argv.slice(2)) {
|
|
267
|
+
try {
|
|
268
|
+
const opts = parseArgs(argv);
|
|
269
|
+
if (opts.command === 'list') await listCommand(opts);
|
|
270
|
+
else if (opts.command === 'apply') return applyCommand(opts);
|
|
271
|
+
else {
|
|
272
|
+
printUsage();
|
|
273
|
+
return 2;
|
|
274
|
+
}
|
|
275
|
+
return 0;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
process.stderr.write(`错误: ${error.message}\n`);
|
|
278
|
+
return 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = {
|
|
283
|
+
createTranslationManifest,
|
|
284
|
+
executePlan,
|
|
285
|
+
isMostlyChinese,
|
|
286
|
+
main,
|
|
287
|
+
needsTranslation,
|
|
288
|
+
parseArgs,
|
|
289
|
+
parseManifest,
|
|
290
|
+
preflightManifest
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
if (require.main === module) {
|
|
294
|
+
main().then((code) => {
|
|
295
|
+
process.exitCode = code;
|
|
296
|
+
});
|
|
297
|
+
}
|