@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.
- package/README.md +67 -4
- package/package.json +1 -1
- package/scripts/e2e-test.sh +38 -0
- package/scripts/install.js +126 -36
- package/scripts/link-skills.sh +203 -0
- package/scripts/local-test.js +503 -0
- package/scripts/local-test.sh +178 -0
- package/scripts/test-auto-detect.js +125 -0
- package/skills/soke-learning-profile/SKILL.md +223 -0
|
@@ -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
|
+
|