@sokeai/cli 1.0.20 → 1.0.23

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,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
+ }
@@ -0,0 +1,178 @@
1
+ #!/bin/bash
2
+
3
+ # soke-cli 本地测试脚本
4
+ # 用于本地开发测试:编译 -> 安装到全局 -> 验证功能
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 本地测试${NC}"
15
+ echo -e "${BLUE}========================================${NC}"
16
+ echo ""
17
+
18
+ # 步骤1: 编译
19
+ echo -e "${YELLOW}[1/4] 编译 soke-cli...${NC}"
20
+ if go build -o soke-cli main.go; then
21
+ echo -e "${GREEN}✓ 编译成功${NC}"
22
+ else
23
+ echo -e "${RED}✗ 编译失败${NC}"
24
+ exit 1
25
+ fi
26
+ echo ""
27
+
28
+ # 步骤2: 检查本地版本功能
29
+ echo -e "${YELLOW}[2/4] 检查本地版本功能...${NC}"
30
+ echo -e " 检查 learning-profile 模块..."
31
+ if ./soke-cli learning-profile --help &>/dev/null; then
32
+ echo -e "${GREEN} ✓ learning-profile 模块存在${NC}"
33
+ else
34
+ echo -e "${RED} ✗ learning-profile 模块不存在${NC}"
35
+ exit 1
36
+ fi
37
+
38
+ echo -e " 检查 contact +search-dept 命令..."
39
+ if ./soke-cli contact +search-dept --help &>/dev/null; then
40
+ echo -e "${GREEN} ✓ contact +search-dept 命令存在${NC}"
41
+ else
42
+ echo -e "${RED} ✗ contact +search-dept 命令不存在${NC}"
43
+ exit 1
44
+ fi
45
+ echo ""
46
+
47
+ # 步骤3: 安装到全局
48
+ echo -e "${YELLOW}[3/4] 安装到全局...${NC}"
49
+
50
+ # 查找全局 CLI 路径
51
+ GLOBAL_CLI=$(which soke-cli 2>/dev/null)
52
+ if [ -z "$GLOBAL_CLI" ]; then
53
+ echo -e "${YELLOW} 未找到全局 soke-cli 安装${NC}"
54
+ echo -e "${YELLOW} 请先通过 npm 安装: npm install -g @sokeai/cli${NC}"
55
+ echo -e "${YELLOW} 跳过全局安装,仅测试本地版本${NC}"
56
+ SKIP_GLOBAL=true
57
+ else
58
+ echo -e " 全局 CLI 路径: ${BLUE}${GLOBAL_CLI}${NC}"
59
+
60
+ # 检查是否需要更新
61
+ NEED_UPDATE=false
62
+ if ! $GLOBAL_CLI learning-profile --help &>/dev/null; then
63
+ echo -e "${YELLOW} 全局版本不支持 learning-profile 模块${NC}"
64
+ NEED_UPDATE=true
65
+ elif ! $GLOBAL_CLI contact +search-dept --help &>/dev/null; then
66
+ echo -e "${YELLOW} 全局版本不支持 contact +search-dept 命令${NC}"
67
+ NEED_UPDATE=true
68
+ fi
69
+
70
+ if [ "$NEED_UPDATE" = true ]; then
71
+ echo -e "${YELLOW} 需要更新全局安装${NC}"
72
+ echo ""
73
+ echo -e "${YELLOW} 是否要用本地版本覆盖全局安装? (y/n)${NC}"
74
+ read -r CONFIRM
75
+
76
+ if [ "$CONFIRM" = "y" ] || [ "$CONFIRM" = "Y" ]; then
77
+ # 备份原文件
78
+ BACKUP_FILE="${GLOBAL_CLI}.backup.$(date +%Y%m%d_%H%M%S)"
79
+ echo -e " 备份原文件到: ${BACKUP_FILE}"
80
+ if sudo cp $GLOBAL_CLI $BACKUP_FILE; then
81
+ echo -e "${GREEN} ✓ 备份成功${NC}"
82
+ else
83
+ echo -e "${RED} ✗ 备份失败${NC}"
84
+ exit 1
85
+ fi
86
+
87
+ # 安装新版本
88
+ echo -e " 安装新版本..."
89
+ if sudo cp ./soke-cli $GLOBAL_CLI; then
90
+ echo -e "${GREEN} ✓ 安装成功${NC}"
91
+ echo -e "${BLUE} 备份文件: ${BACKUP_FILE}${NC}"
92
+ else
93
+ echo -e "${RED} ✗ 安装失败${NC}"
94
+ echo -e "${YELLOW} 恢复备份...${NC}"
95
+ sudo cp $BACKUP_FILE $GLOBAL_CLI
96
+ exit 1
97
+ fi
98
+ SKIP_GLOBAL=false
99
+ else
100
+ echo -e "${YELLOW} 跳过全局安装${NC}"
101
+ SKIP_GLOBAL=true
102
+ fi
103
+ else
104
+ echo -e "${GREEN} ✓ 全局版本已是最新,无需更新${NC}"
105
+ SKIP_GLOBAL=false
106
+ fi
107
+ fi
108
+ echo ""
109
+
110
+ # 步骤4: 运行测试
111
+ echo -e "${YELLOW}[4/4] 运行功能测试...${NC}"
112
+
113
+ # 测试本地版本
114
+ echo -e " ${BLUE}测试本地版本:${NC}"
115
+ if ./soke-cli learning-profile +list --help &>/dev/null; then
116
+ echo -e "${GREEN} ✓ learning-profile +list 命令可用${NC}"
117
+ else
118
+ echo -e "${RED} ✗ learning-profile +list 命令不可用${NC}"
119
+ exit 1
120
+ fi
121
+
122
+ if ./soke-cli contact +search-dept --help &>/dev/null; then
123
+ echo -e "${GREEN} ✓ contact +search-dept 命令可用${NC}"
124
+ else
125
+ echo -e "${RED} ✗ contact +search-dept 命令不可用${NC}"
126
+ exit 1
127
+ fi
128
+
129
+ if ./soke-cli contact +search-user --help &>/dev/null; then
130
+ echo -e "${GREEN} ✓ contact +search-user 命令可用${NC}"
131
+ else
132
+ echo -e "${RED} ✗ contact +search-user 命令不可用${NC}"
133
+ exit 1
134
+ fi
135
+
136
+ # 测试全局版本(如果已安装)
137
+ if [ "$SKIP_GLOBAL" = false ] && [ -n "$GLOBAL_CLI" ]; then
138
+ echo ""
139
+ echo -e " ${BLUE}测试全局版本:${NC}"
140
+ if $GLOBAL_CLI learning-profile +list --help &>/dev/null; then
141
+ echo -e "${GREEN} ✓ learning-profile +list 命令可用${NC}"
142
+ else
143
+ echo -e "${RED} ✗ learning-profile +list 命令不可用${NC}"
144
+ echo -e "${YELLOW} 提示: 全局安装可能未成功更新${NC}"
145
+ fi
146
+
147
+ if $GLOBAL_CLI contact +search-dept --help &>/dev/null; then
148
+ echo -e "${GREEN} ✓ contact +search-dept 命令可用${NC}"
149
+ else
150
+ echo -e "${RED} ✗ contact +search-dept 命令不可用${NC}"
151
+ echo -e "${YELLOW} 提示: 全局安装可能未成功更新${NC}"
152
+ fi
153
+
154
+ if $GLOBAL_CLI contact +search-user --help &>/dev/null; then
155
+ echo -e "${GREEN} ✓ contact +search-user 命令可用${NC}"
156
+ else
157
+ echo -e "${RED} ✗ contact +search-user 命令不可用${NC}"
158
+ echo -e "${YELLOW} 提示: 全局安装可能未成功更新${NC}"
159
+ fi
160
+ fi
161
+ echo ""
162
+
163
+ # 完成
164
+ echo -e "${GREEN}========================================${NC}"
165
+ echo -e "${GREEN} 本地测试完成! ✓${NC}"
166
+ echo -e "${GREEN}========================================${NC}"
167
+ echo ""
168
+ echo -e "${BLUE}下一步:${NC}"
169
+ if [ "$SKIP_GLOBAL" = true ]; then
170
+ echo -e " ${YELLOW}提示: 未更新全局安装,Skills 可能无法使用新功能${NC}"
171
+ echo -e " ${YELLOW}如需测试 Skills,请重新运行此脚本并选择更新全局安装${NC}"
172
+ echo ""
173
+ fi
174
+ echo -e " 1. 运行完整测试: ${BLUE}bash ./scripts/e2e-test.sh${NC}"
175
+ echo -e " 2. 测试 Skills: ${BLUE}npx skills add liuchenlong1111/soke-cli -y -g${NC}"
176
+ echo -e " 3. 在 AI Agent 中测试: ${BLUE}\"查询张三的学习档案\"${NC}"
177
+ echo ""
178
+