@sokeai/cli 1.0.21 → 1.0.25

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.
@@ -0,0 +1,203 @@
1
+ #!/bin/bash
2
+
3
+ # soke-cli Skills 本地测试脚本
4
+ # 将本地 skills 链接到 Claude 的 skills 目录进行测试
5
+
6
+ # 颜色输出
7
+ RED='\033[0;31m'
8
+ GREEN='\033[0;32m'
9
+ YELLOW='\033[1;33m'
10
+ BLUE='\033[0;34m'
11
+ NC='\033[0m'
12
+
13
+ echo -e "${BLUE}========================================${NC}"
14
+ echo -e "${BLUE} soke-cli Skills 本地测试${NC}"
15
+ echo -e "${BLUE}========================================${NC}"
16
+ echo ""
17
+
18
+ # 检查 skills 目录是否存在
19
+ if [ ! -d "./skills" ]; then
20
+ echo -e "${RED}错误: 未找到 ./skills 目录${NC}"
21
+ exit 1
22
+ fi
23
+
24
+ # 查找所有可能的 skills 目录
25
+ echo -e "${YELLOW}查找 Claude skills 目录...${NC}"
26
+ SKILLS_DIRS=(
27
+ "$HOME/.codex/skills"
28
+ "$HOME/.openclaw/skills"
29
+ "$HOME/.agents/skills"
30
+ "$HOME/.workclaw/skills"
31
+ "$HOME/.skills"
32
+ )
33
+
34
+ FOUND_DIRS=()
35
+ for dir in "${SKILLS_DIRS[@]}"; do
36
+ if [ -d "$dir" ]; then
37
+ FOUND_DIRS+=("$dir")
38
+ echo -e " ${GREEN}✓${NC} 找到: $dir"
39
+ fi
40
+ done
41
+
42
+ if [ ${#FOUND_DIRS[@]} -eq 0 ]; then
43
+ echo -e "${RED}错误: 未找到任何 Claude skills 目录${NC}"
44
+ echo -e "${YELLOW}提示: 请先运行 'npx skills add' 安装任意一个 skill 来初始化目录${NC}"
45
+ exit 1
46
+ fi
47
+ echo ""
48
+
49
+ # 选择要链接的目录
50
+ if [ ${#FOUND_DIRS[@]} -eq 1 ]; then
51
+ TARGET_DIR="${FOUND_DIRS[0]}"
52
+ echo -e "${BLUE}将链接到: ${TARGET_DIR}${NC}"
53
+ else
54
+ echo -e "${YELLOW}找到多个 skills 目录,请选择:${NC}"
55
+ for i in "${!FOUND_DIRS[@]}"; do
56
+ echo -e " $((i+1)). ${FOUND_DIRS[$i]}"
57
+ done
58
+ echo ""
59
+
60
+ # 如果有参数,使用参数作为选择
61
+ if [ -n "$1" ]; then
62
+ choice=$1
63
+ else
64
+ echo -n "请输入序号 (1-${#FOUND_DIRS[@]}) 或 'all' 链接到所有目录: "
65
+ read -r choice
66
+ fi
67
+
68
+ if [ "$choice" = "all" ]; then
69
+ echo -e "${BLUE}将链接到所有目录${NC}"
70
+ LINK_ALL=true
71
+ elif [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#FOUND_DIRS[@]} ]; then
72
+ TARGET_DIR="${FOUND_DIRS[$((choice-1))]}"
73
+ echo -e "${BLUE}已选择: ${TARGET_DIR}${NC}"
74
+ LINK_ALL=false
75
+ else
76
+ echo -e "${RED}无效的选择${NC}"
77
+ exit 1
78
+ fi
79
+ fi
80
+ echo ""
81
+
82
+ # 获取当前项目的绝对路径
83
+ PROJECT_DIR=$(pwd)
84
+ SOURCE_SKILLS_DIR="${PROJECT_DIR}/skills"
85
+
86
+ # 列出要链接的 skills
87
+ echo -e "${YELLOW}准备链接以下 skills:${NC}"
88
+ for skill_dir in "$SOURCE_SKILLS_DIR"/*; do
89
+ if [ -d "$skill_dir" ]; then
90
+ skill_name=$(basename "$skill_dir")
91
+ echo -e " - ${BLUE}${skill_name}${NC}"
92
+ fi
93
+ done
94
+ echo ""
95
+
96
+ # 确认操作
97
+ echo -e "${YELLOW}是否继续? (y/n)${NC}"
98
+ read -r CONFIRM
99
+ if [ "$CONFIRM" != "y" ] && [ "$CONFIRM" != "Y" ]; then
100
+ echo -e "${YELLOW}取消操作${NC}"
101
+ exit 0
102
+ fi
103
+ echo ""
104
+
105
+ # 创建符号链接
106
+ echo -e "${YELLOW}创建符号链接...${NC}"
107
+ SUCCESS_COUNT=0
108
+ SKIP_COUNT=0
109
+
110
+ # 如果选择链接到所有目录
111
+ if [ "$LINK_ALL" = true ]; then
112
+ for target_dir in "${FOUND_DIRS[@]}"; do
113
+ echo -e "${BLUE}链接到: ${target_dir}${NC}"
114
+ for skill_dir in "$SOURCE_SKILLS_DIR"/*; do
115
+ if [ -d "$skill_dir" ]; then
116
+ skill_name=$(basename "$skill_dir")
117
+ target_link="${target_dir}/${skill_name}"
118
+
119
+ if [ -L "$target_link" ]; then
120
+ current_target=$(readlink "$target_link")
121
+ if [ "$current_target" = "$skill_dir" ]; then
122
+ echo -e " ${GREEN}✓${NC} ${skill_name} (已存在)"
123
+ SKIP_COUNT=$((SKIP_COUNT + 1))
124
+ else
125
+ rm "$target_link"
126
+ ln -s "$skill_dir" "$target_link"
127
+ echo -e " ${GREEN}✓${NC} ${skill_name} (已更新)"
128
+ SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
129
+ fi
130
+ elif [ -e "$target_link" ]; then
131
+ echo -e " ${RED}✗${NC} ${skill_name} (存在同名文件)"
132
+ else
133
+ ln -s "$skill_dir" "$target_link"
134
+ echo -e " ${GREEN}✓${NC} ${skill_name} (已链接)"
135
+ SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
136
+ fi
137
+ fi
138
+ done
139
+ echo ""
140
+ done
141
+ else
142
+ # 链接到单个目录
143
+ for skill_dir in "$SOURCE_SKILLS_DIR"/*; do
144
+ if [ -d "$skill_dir" ]; then
145
+ skill_name=$(basename "$skill_dir")
146
+ target_link="${TARGET_DIR}/${skill_name}"
147
+
148
+ # 检查是否已存在
149
+ if [ -L "$target_link" ]; then
150
+ # 已存在符号链接,检查是否指向正确位置
151
+ current_target=$(readlink "$target_link")
152
+ if [ "$current_target" = "$skill_dir" ]; then
153
+ echo -e " ${GREEN}✓${NC} ${skill_name} (已存在,指向正确)"
154
+ SKIP_COUNT=$((SKIP_COUNT + 1))
155
+ else
156
+ echo -e " ${YELLOW}!${NC} ${skill_name} (已存在,但指向: ${current_target})"
157
+ echo -n " 是否覆盖? (y/n): "
158
+ read -r overwrite
159
+ if [ "$overwrite" = "y" ] || [ "$overwrite" = "Y" ]; then
160
+ rm "$target_link"
161
+ ln -s "$skill_dir" "$target_link"
162
+ echo -e " ${GREEN}✓${NC} 已覆盖"
163
+ SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
164
+ else
165
+ echo -e " ${YELLOW}跳过${NC}"
166
+ SKIP_COUNT=$((SKIP_COUNT + 1))
167
+ fi
168
+ fi
169
+ elif [ -e "$target_link" ]; then
170
+ # 存在同名文件/目录
171
+ echo -e " ${RED}✗${NC} ${skill_name} (存在同名文件/目录)"
172
+ echo -e " 请手动删除: rm -rf ${target_link}"
173
+ else
174
+ # 创建新链接
175
+ ln -s "$skill_dir" "$target_link"
176
+ echo -e " ${GREEN}✓${NC} ${skill_name} (已链接)"
177
+ SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
178
+ fi
179
+ fi
180
+ done
181
+ fi
182
+ echo ""
183
+
184
+ # 完成
185
+ echo -e "${GREEN}========================================${NC}"
186
+ echo -e "${GREEN} Skills 链接完成! ✓${NC}"
187
+ echo -e "${GREEN}========================================${NC}"
188
+ echo ""
189
+ echo -e "${BLUE}统计:${NC}"
190
+ echo -e " 新建链接: ${GREEN}${SUCCESS_COUNT}${NC}"
191
+ echo -e " 已存在: ${YELLOW}${SKIP_COUNT}${NC}"
192
+ echo ""
193
+ echo -e "${BLUE}下一步:${NC}"
194
+ echo -e " 1. 确保全局 CLI 已更新: ${BLUE}bash ./scripts/local-test.sh${NC}"
195
+ echo -e " 2. 在 Claude Code 中测试:"
196
+ echo -e " ${BLUE}\"查询张三的学习档案\"${NC}"
197
+ echo -e " ${BLUE}\"查询技术部的学员学习情况\"${NC}"
198
+ echo ""
199
+ echo -e "${YELLOW}提示:${NC}"
200
+ echo -e " - Skills 已链接到: ${TARGET_DIR}"
201
+ echo -e " - 修改本地 skills 文件会立即生效"
202
+ echo -e " - 删除链接: ${BLUE}rm ${TARGET_DIR}/soke-*${NC}"
203
+ echo ""
@@ -0,0 +1,503 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * 本地测试脚本 - 将 skills 分发到本地 AI Agent 环境
5
+ * 用于开发完 skill 后在本地测试,无需发布到 npm
6
+ *
7
+ * 使用方法:
8
+ * node scripts/local-test.js
9
+ * node scripts/local-test.js --skill soke-course
10
+ * node scripts/local-test.js --clean
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const os = require('os');
16
+
17
+ // 颜色输出
18
+ const colors = {
19
+ reset: '\x1b[0m',
20
+ bright: '\x1b[1m',
21
+ red: '\x1b[31m',
22
+ green: '\x1b[32m',
23
+ yellow: '\x1b[33m',
24
+ blue: '\x1b[34m',
25
+ cyan: '\x1b[36m'
26
+ };
27
+
28
+ function log(message, color = 'reset') {
29
+ console.log(`${colors[color]}${message}${colors.reset}`);
30
+ }
31
+
32
+ function logSection(title) {
33
+ console.log('');
34
+ log(`${'='.repeat(60)}`, 'blue');
35
+ log(` ${title}`, 'bright');
36
+ log(`${'='.repeat(60)}`, 'blue');
37
+ console.log('');
38
+ }
39
+
40
+ function logSuccess(message) {
41
+ log(`✅ ${message}`, 'green');
42
+ }
43
+
44
+ function logError(message) {
45
+ log(`❌ ${message}`, 'red');
46
+ }
47
+
48
+ function logWarning(message) {
49
+ log(`⚠️ ${message}`, 'yellow');
50
+ }
51
+
52
+ function logInfo(message) {
53
+ log(`ℹ️ ${message}`, 'cyan');
54
+ }
55
+
56
+ /**
57
+ * 递归复制目录
58
+ */
59
+ function copyDirRecursive(srcDir, destDir) {
60
+ if (!fs.existsSync(srcDir)) return false;
61
+
62
+ if (!fs.existsSync(destDir)) {
63
+ fs.mkdirSync(destDir, { recursive: true });
64
+ }
65
+
66
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
67
+
68
+ for (const entry of entries) {
69
+ const srcPath = path.join(srcDir, entry.name);
70
+ const destPath = path.join(destDir, entry.name);
71
+
72
+ if (entry.isDirectory()) {
73
+ copyDirRecursive(srcPath, destPath);
74
+ } else if (entry.isSymbolicLink()) {
75
+ try {
76
+ const linkTarget = fs.readlinkSync(srcPath);
77
+ try {
78
+ fs.unlinkSync(destPath);
79
+ } catch (_) {}
80
+ fs.symlinkSync(linkTarget, destPath);
81
+ } catch (_) {}
82
+ } else {
83
+ fs.copyFileSync(srcPath, destPath);
84
+ }
85
+ }
86
+
87
+ return true;
88
+ }
89
+
90
+ /**
91
+ * 检测所有 soke-* skills
92
+ */
93
+ function detectSkillNames(packagedSkillsDir) {
94
+ if (!fs.existsSync(packagedSkillsDir)) return [];
95
+
96
+ try {
97
+ const entries = fs.readdirSync(packagedSkillsDir, { withFileTypes: true });
98
+ return entries
99
+ .filter(entry => entry.isDirectory() && entry.name.startsWith('soke-'))
100
+ .map(entry => entry.name)
101
+ .sort();
102
+ } catch (_) {
103
+ return [];
104
+ }
105
+ }
106
+
107
+ /**
108
+ * 从 SKILL.md 解析元数据
109
+ */
110
+ function parseSkillMetadata(skillDir) {
111
+ const skillMdPath = path.join(skillDir, 'SKILL.md');
112
+ if (!fs.existsSync(skillMdPath)) {
113
+ return null;
114
+ }
115
+
116
+ try {
117
+ const content = fs.readFileSync(skillMdPath, 'utf8');
118
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
119
+ if (!frontmatterMatch) return null;
120
+
121
+ const frontmatter = frontmatterMatch[1];
122
+ const metadata = {};
123
+
124
+ const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
125
+ if (nameMatch) metadata.name = nameMatch[1].trim();
126
+
127
+ const summaryMatch = frontmatter.match(/^summary:\s*(.+)$/m);
128
+ if (summaryMatch) metadata.summary = summaryMatch[1].trim();
129
+
130
+ const descMatch = frontmatter.match(/^description:\s*["'](.+)["']$/m);
131
+ if (descMatch) {
132
+ metadata.description = descMatch[1].trim();
133
+ } else {
134
+ const descMatch2 = frontmatter.match(/^description:\s*(.+)$/m);
135
+ if (descMatch2) metadata.description = descMatch2[1].trim();
136
+ }
137
+
138
+ const versionMatch = frontmatter.match(/^version:\s*(.+)$/m);
139
+ if (versionMatch) metadata.version = versionMatch[1].trim();
140
+
141
+ const binsMatch = frontmatter.match(/bins:\s*\[(.+?)\]/);
142
+ if (binsMatch) {
143
+ metadata.bins = binsMatch[1].split(',').map(b => b.trim().replace(/['"]/g, ''));
144
+ }
145
+
146
+ return metadata;
147
+ } catch (_) {
148
+ return null;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * 推断 skill emoji
154
+ */
155
+ function inferSkillEmoji(skillName) {
156
+ const emojiMap = {
157
+ 'soke-exam': '📝',
158
+ 'soke-course': '📚',
159
+ 'soke-shared': '🔧',
160
+ 'soke-user': '👤',
161
+ 'soke-contact': '📇',
162
+ 'soke-department': '🏢',
163
+ 'soke-approval': '✅',
164
+ 'soke-attendance': '📅',
165
+ 'soke-report': '📊'
166
+ };
167
+ return emojiMap[skillName] || '📦';
168
+ }
169
+
170
+ /**
171
+ * 检测本地 AI Agent 环境
172
+ * 只检测 agent 相关的目录,不包括全局安装目录
173
+ */
174
+ function detectLocalAgentDirs() {
175
+ const homeDir = os.homedir();
176
+ const dirs = [];
177
+
178
+ // 1. workclaw (Claude Code) - Agent skills 目录
179
+ const workclawDir = path.join(homeDir, '.workclaw', 'skills');
180
+ if (fs.existsSync(path.join(homeDir, '.workclaw'))) {
181
+ dirs.push({
182
+ name: 'workclaw (Claude Code)',
183
+ path: workclawDir,
184
+ registryPath: path.join(workclawDir, 'registry.json'),
185
+ type: 'workclaw'
186
+ });
187
+ }
188
+
189
+ // 2. claude (Claude Desktop) - Agent skills 目录
190
+ const claudeDir = path.join(homeDir, '.claude', 'skills');
191
+ if (fs.existsSync(path.join(homeDir, '.claude'))) {
192
+ dirs.push({
193
+ name: 'claude (Claude Desktop)',
194
+ path: claudeDir,
195
+ registryPath: null,
196
+ type: 'claude'
197
+ });
198
+ }
199
+
200
+ // 3. sokeclaw - Agent skills 目录
201
+ const sokeclawDir = path.join(homeDir, '.sokeclaw', 'openai-agents', 'workspaces', 'main', 'skills');
202
+ if (fs.existsSync(path.join(homeDir, '.sokeclaw'))) {
203
+ dirs.push({
204
+ name: 'sokeclaw',
205
+ path: sokeclawDir,
206
+ registryPath: null,
207
+ type: 'sokeclaw'
208
+ });
209
+ }
210
+
211
+ // 4. zev - Agent skills 目录
212
+ const zevDir = path.join(homeDir, '.zev', 'openai-agents', 'workspaces', 'main', 'skills');
213
+ if (fs.existsSync(path.join(homeDir, '.zev'))) {
214
+ dirs.push({
215
+ name: 'zev',
216
+ path: zevDir,
217
+ registryPath: null,
218
+ type: 'zev'
219
+ });
220
+ }
221
+
222
+ return dirs;
223
+ }
224
+
225
+ /**
226
+ * 更新 workclaw registry.json
227
+ */
228
+ function updateWorkclawRegistry(registryPath, skillName, metadata, skillInstallPath) {
229
+ let registry;
230
+
231
+ try {
232
+ registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
233
+ } catch (_) {
234
+ registry = { version: 1, migrations: {}, skills: [] };
235
+ }
236
+
237
+ if (registry.version == null) registry.version = 1;
238
+ if (!registry.migrations) registry.migrations = {};
239
+ if (!Array.isArray(registry.skills)) registry.skills = [];
240
+
241
+ const displayName = metadata.summary || metadata.name || skillName;
242
+ const description = metadata.description || `${displayName} - 授客AI CLI工具`;
243
+ const version = metadata.version || '1.0.0';
244
+ const emoji = inferSkillEmoji(skillName);
245
+ const requires = metadata.bins ? { bins: metadata.bins } : {};
246
+
247
+ const skillEntry = {
248
+ id: `skill:${skillName}`,
249
+ name: skillName,
250
+ displayName: displayName,
251
+ description: description,
252
+ source: { type: 'local', slug: '', url: '' },
253
+ install: {
254
+ path: skillInstallPath,
255
+ installedAt: new Date().toISOString(),
256
+ updatedAt: new Date().toISOString(),
257
+ version: version
258
+ },
259
+ state: { enabled: true, health: 'ok', lastError: '' },
260
+ runtime: { supported: ['openclaw'], enabled: ['openclaw'], primary: 'openclaw' },
261
+ security: { riskLevel: 'normal', requiresApproval: false },
262
+ metadata: { emoji: emoji, homepage: '', requires: requires }
263
+ };
264
+
265
+ const idx = registry.skills.findIndex(s => s && s.id === skillEntry.id);
266
+ if (idx >= 0) {
267
+ registry.skills[idx] = { ...registry.skills[idx], ...skillEntry };
268
+ } else {
269
+ registry.skills.push(skillEntry);
270
+ }
271
+
272
+ fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
273
+ }
274
+
275
+ /**
276
+ * 分发单个 skill 到所有本地环境
277
+ */
278
+ function distributeSkill(skillName, packagedSkillsDir, targetDirs) {
279
+ const srcDir = path.join(packagedSkillsDir, skillName);
280
+
281
+ if (!fs.existsSync(srcDir)) {
282
+ logError(`Skill 目录不存在: ${srcDir}`);
283
+ return false;
284
+ }
285
+
286
+ const metadata = parseSkillMetadata(srcDir);
287
+ if (!metadata || !metadata.name) {
288
+ logWarning(`无法解析 ${skillName} 的元数据,将使用默认值`);
289
+ }
290
+
291
+ logInfo(`分发 ${skillName}...`);
292
+ console.log('');
293
+
294
+ let successCount = 0;
295
+
296
+ for (const target of targetDirs) {
297
+ try {
298
+ fs.mkdirSync(target.path, { recursive: true });
299
+
300
+ const destDir = path.join(target.path, skillName);
301
+ const success = copyDirRecursive(srcDir, destDir);
302
+
303
+ if (success) {
304
+ logSuccess(` → ${target.name}`);
305
+ logInfo(` ${destDir}`);
306
+
307
+ // 更新 workclaw registry
308
+ if (target.type === 'workclaw' && target.registryPath) {
309
+ updateWorkclawRegistry(target.registryPath, skillName, metadata, destDir);
310
+ logInfo(` 已更新 registry.json`);
311
+ }
312
+
313
+ successCount++;
314
+ } else {
315
+ logError(` → ${target.name} (复制失败)`);
316
+ }
317
+ } catch (err) {
318
+ logError(` → ${target.name} (错误: ${err.message})`);
319
+ }
320
+ console.log('');
321
+ }
322
+
323
+ return successCount > 0;
324
+ }
325
+
326
+ /**
327
+ * 清理所有本地环境中的 skills
328
+ */
329
+ function cleanSkills(targetDirs, skillNames) {
330
+ logSection('清理本地 Skills');
331
+
332
+ for (const target of targetDirs) {
333
+ logInfo(`清理 ${target.name}...`);
334
+
335
+ for (const skillName of skillNames) {
336
+ const skillDir = path.join(target.path, skillName);
337
+
338
+ if (fs.existsSync(skillDir)) {
339
+ try {
340
+ fs.rmSync(skillDir, { recursive: true, force: true });
341
+ logSuccess(` ✓ 删除 ${skillName}`);
342
+ } catch (err) {
343
+ logError(` ✗ 删除 ${skillName} 失败: ${err.message}`);
344
+ }
345
+ }
346
+ }
347
+
348
+ // 清理 workclaw registry
349
+ if (target.type === 'workclaw' && target.registryPath && fs.existsSync(target.registryPath)) {
350
+ try {
351
+ const registry = JSON.parse(fs.readFileSync(target.registryPath, 'utf8'));
352
+ if (Array.isArray(registry.skills)) {
353
+ const before = registry.skills.length;
354
+ registry.skills = registry.skills.filter(s => {
355
+ return !s || !s.name || !skillNames.includes(s.name);
356
+ });
357
+ const after = registry.skills.length;
358
+
359
+ if (before !== after) {
360
+ fs.writeFileSync(target.registryPath, JSON.stringify(registry, null, 2));
361
+ logSuccess(` ✓ 清理 registry.json (删除 ${before - after} 个条目)`);
362
+ }
363
+ }
364
+ } catch (err) {
365
+ logError(` ✗ 清理 registry.json 失败: ${err.message}`);
366
+ }
367
+ }
368
+
369
+ console.log('');
370
+ }
371
+ }
372
+
373
+ /**
374
+ * 主函数
375
+ */
376
+ function main() {
377
+ const args = process.argv.slice(2);
378
+ const isClean = args.includes('--clean');
379
+ const skillArg = args.find(arg => arg.startsWith('--skill='));
380
+ const specificSkill = skillArg ? skillArg.split('=')[1] : null;
381
+
382
+ logSection('本地 Skill 测试工具');
383
+
384
+ // 检测项目目录
385
+ const packageRoot = path.join(__dirname, '..');
386
+ const packagedSkillsDir = path.join(packageRoot, 'skills');
387
+
388
+ if (!fs.existsSync(packagedSkillsDir)) {
389
+ logError(`Skills 目录不存在: ${packagedSkillsDir}`);
390
+ process.exit(1);
391
+ }
392
+
393
+ // 检测所有 skills
394
+ const allSkills = detectSkillNames(packagedSkillsDir);
395
+
396
+ if (allSkills.length === 0) {
397
+ logError('未检测到任何 soke-* skills');
398
+ process.exit(1);
399
+ }
400
+
401
+ logInfo(`检测到 ${allSkills.length} 个 skills: ${allSkills.join(', ')}`);
402
+ console.log('');
403
+
404
+ // 检测本地 AI Agent 环境
405
+ const targetDirs = detectLocalAgentDirs();
406
+
407
+ if (targetDirs.length === 0) {
408
+ logError('未检测到任何本地 AI Agent 环境');
409
+ logInfo('支持的环境:');
410
+ logInfo(' • workclaw (~/.workclaw/skills/)');
411
+ logInfo(' • claude (~/.claude/skills/)');
412
+ logInfo(' • sokeclaw (~/.sokeclaw/openai-agents/workspaces/main/skills/)');
413
+ logInfo(' • zev (~/.zev/openai-agents/workspaces/main/skills/)');
414
+ console.log('');
415
+ logWarning('注意: 此脚本只分发到 Agent skills 目录,不包括全局安装目录');
416
+ process.exit(1);
417
+ }
418
+
419
+ logInfo(`检测到 ${targetDirs.length} 个本地环境:`);
420
+ for (const target of targetDirs) {
421
+ logInfo(` • ${target.name}`);
422
+ logInfo(` ${target.path}`);
423
+ }
424
+ console.log('');
425
+
426
+ // 清理模式
427
+ if (isClean) {
428
+ cleanSkills(targetDirs, allSkills);
429
+ logSuccess('清理完成!');
430
+ return;
431
+ }
432
+
433
+ // 确定要分发的 skills
434
+ const skillsToDistribute = specificSkill
435
+ ? (allSkills.includes(specificSkill) ? [specificSkill] : [])
436
+ : allSkills;
437
+
438
+ if (skillsToDistribute.length === 0) {
439
+ logError(`Skill 不存在: ${specificSkill}`);
440
+ process.exit(1);
441
+ }
442
+
443
+ // 分发 skills
444
+ logSection('分发 Skills 到本地环境');
445
+
446
+ let totalSuccess = 0;
447
+ let totalFailed = 0;
448
+
449
+ for (const skillName of skillsToDistribute) {
450
+ const success = distributeSkill(skillName, packagedSkillsDir, targetDirs);
451
+ if (success) {
452
+ totalSuccess++;
453
+ } else {
454
+ totalFailed++;
455
+ }
456
+ }
457
+
458
+ // 总结
459
+ logSection('分发完成');
460
+
461
+ logInfo(`总计: ${skillsToDistribute.length} 个 skills`);
462
+ logSuccess(`成功: ${totalSuccess} 个`);
463
+ if (totalFailed > 0) {
464
+ logError(`失败: ${totalFailed} 个`);
465
+ }
466
+ console.log('');
467
+
468
+ // 下一步提示
469
+ logSection('下一步');
470
+ console.log('');
471
+ log('1. 重启你的 AI Agent', 'cyan');
472
+ log(' • Claude Code: 重启 VS Code 或重新打开 Claude Code 窗口', 'cyan');
473
+ log(' • Claude Desktop: 重启 Claude Desktop 应用', 'cyan');
474
+ log(' • Sokeclaw: 重启 sokeclaw 进程', 'cyan');
475
+ log(' • Zev: 重启 zev 进程', 'cyan');
476
+ console.log('');
477
+ log('2. 在对话中测试 skill 功能', 'cyan');
478
+ log(' 例如: "查询课程列表" 或 "查询考试成绩"', 'cyan');
479
+ console.log('');
480
+ log('3. 验证命令是否可用:', 'cyan');
481
+ console.log('');
482
+ for (const skillName of skillsToDistribute) {
483
+ log(` soke-cli ${skillName.replace('soke-', '')} --help`, 'yellow');
484
+ }
485
+ console.log('');
486
+ log('4. 测试完成后,可以清理:', 'cyan');
487
+ log(' node scripts/local-test.js --clean', 'yellow');
488
+ console.log('');
489
+ log('💡 提示:', 'cyan');
490
+ log(' • 此脚本只分发到 Agent skills 目录', 'cyan');
491
+ log(' • 全局安装 (npm install -g) 需要单独处理', 'cyan');
492
+ log(' • 修改后重新运行此脚本即可更新', 'cyan');
493
+ console.log('');
494
+ }
495
+
496
+ // 运行
497
+ try {
498
+ main();
499
+ } catch (err) {
500
+ logError(`发生错误: ${err.message}`);
501
+ console.error(err);
502
+ process.exit(1);
503
+ }