@synsci/cli 1.0.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/README.md +39 -0
- package/bin/cli.js +8 -0
- package/package.json +44 -0
- package/src/agents.js +81 -0
- package/src/ascii.js +126 -0
- package/src/index.js +358 -0
- package/src/installer.js +611 -0
- package/src/prompts.js +529 -0
package/src/installer.js
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, symlinkSync, readdirSync, readFileSync, writeFileSync, rmSync, lstatSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join, basename, dirname } from 'path';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
|
|
8
|
+
const REPO_URL = 'https://github.com/Orchestra-Research/AI-research-SKILLs';
|
|
9
|
+
const CANONICAL_DIR = join(homedir(), '.syntheticsciences', 'skills');
|
|
10
|
+
const LOCK_FILE = join(homedir(), '.syntheticsciences', '.lock.json');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Ensure the canonical skills directory exists
|
|
14
|
+
*/
|
|
15
|
+
function ensureCanonicalDir() {
|
|
16
|
+
const orchestraDir = join(homedir(), '.syntheticsciences');
|
|
17
|
+
if (!existsSync(orchestraDir)) {
|
|
18
|
+
mkdirSync(orchestraDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
if (!existsSync(CANONICAL_DIR)) {
|
|
21
|
+
mkdirSync(CANONICAL_DIR, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read lock file
|
|
27
|
+
*/
|
|
28
|
+
function readLock() {
|
|
29
|
+
if (existsSync(LOCK_FILE)) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(LOCK_FILE, 'utf8'));
|
|
32
|
+
} catch {
|
|
33
|
+
return { version: null, installedAt: null, skills: [] };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { version: null, installedAt: null, skills: [] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Write lock file
|
|
41
|
+
*/
|
|
42
|
+
function writeLock(data) {
|
|
43
|
+
writeFileSync(LOCK_FILE, JSON.stringify(data, null, 2));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Download skills from GitHub
|
|
48
|
+
*/
|
|
49
|
+
async function downloadSkills(categories, spinner) {
|
|
50
|
+
ensureCanonicalDir();
|
|
51
|
+
|
|
52
|
+
// Clone or update the repository to a temp location
|
|
53
|
+
const tempDir = join(homedir(), '.syntheticsciences', '.temp-clone');
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
if (existsSync(tempDir)) {
|
|
57
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
spinner.text = 'Cloning repository...';
|
|
61
|
+
execSync(`git clone --depth 1 ${REPO_URL}.git ${tempDir}`, {
|
|
62
|
+
stdio: 'pipe',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const skills = [];
|
|
66
|
+
|
|
67
|
+
// Copy selected categories
|
|
68
|
+
for (const categoryId of categories) {
|
|
69
|
+
const categoryPath = join(tempDir, categoryId);
|
|
70
|
+
if (!existsSync(categoryPath)) continue;
|
|
71
|
+
|
|
72
|
+
const targetCategoryPath = join(CANONICAL_DIR, categoryId);
|
|
73
|
+
if (!existsSync(targetCategoryPath)) {
|
|
74
|
+
mkdirSync(targetCategoryPath, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check if it's a standalone skill (SKILL.md directly in category)
|
|
78
|
+
const standaloneSkillPath = join(categoryPath, 'SKILL.md');
|
|
79
|
+
if (existsSync(standaloneSkillPath)) {
|
|
80
|
+
// Copy the entire category as a standalone skill
|
|
81
|
+
spinner.text = `Downloading ${categoryId}...`;
|
|
82
|
+
execSync(`cp -r "${categoryPath}"/* "${targetCategoryPath}/"`, { stdio: 'pipe' });
|
|
83
|
+
skills.push({ category: categoryId, skill: categoryId, standalone: true });
|
|
84
|
+
} else {
|
|
85
|
+
// It's a nested category with multiple skills
|
|
86
|
+
const entries = readdirSync(categoryPath, { withFileTypes: true });
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (entry.isDirectory()) {
|
|
89
|
+
const skillPath = join(categoryPath, entry.name, 'SKILL.md');
|
|
90
|
+
if (existsSync(skillPath)) {
|
|
91
|
+
spinner.text = `Downloading ${entry.name}...`;
|
|
92
|
+
const targetSkillPath = join(targetCategoryPath, entry.name);
|
|
93
|
+
if (!existsSync(targetSkillPath)) {
|
|
94
|
+
mkdirSync(targetSkillPath, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
execSync(`cp -r "${join(categoryPath, entry.name)}"/* "${targetSkillPath}/"`, { stdio: 'pipe' });
|
|
97
|
+
skills.push({ category: categoryId, skill: entry.name, standalone: false });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Cleanup
|
|
105
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
106
|
+
|
|
107
|
+
return skills;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (existsSync(tempDir)) {
|
|
110
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create symlinks for an agent
|
|
118
|
+
*/
|
|
119
|
+
function createSymlinks(agent, skills, spinner) {
|
|
120
|
+
const agentSkillsPath = agent.skillsPath;
|
|
121
|
+
|
|
122
|
+
// Ensure agent skills directory exists
|
|
123
|
+
if (!existsSync(agentSkillsPath)) {
|
|
124
|
+
mkdirSync(agentSkillsPath, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let linkedCount = 0;
|
|
128
|
+
|
|
129
|
+
for (const skill of skills) {
|
|
130
|
+
const sourcePath = skill.standalone
|
|
131
|
+
? join(CANONICAL_DIR, skill.category)
|
|
132
|
+
: join(CANONICAL_DIR, skill.category, skill.skill);
|
|
133
|
+
|
|
134
|
+
const linkName = skill.standalone ? skill.category : skill.skill;
|
|
135
|
+
const linkPath = join(agentSkillsPath, linkName);
|
|
136
|
+
|
|
137
|
+
// Remove existing symlink if present
|
|
138
|
+
if (existsSync(linkPath)) {
|
|
139
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
symlinkSync(sourcePath, linkPath);
|
|
144
|
+
linkedCount++;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
// Ignore symlink errors (e.g., already exists)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return linkedCount;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Download specific skills from GitHub
|
|
155
|
+
*/
|
|
156
|
+
async function downloadSpecificSkills(skillPaths, spinner) {
|
|
157
|
+
ensureCanonicalDir();
|
|
158
|
+
|
|
159
|
+
// Clone or update the repository to a temp location
|
|
160
|
+
const tempDir = join(homedir(), '.syntheticsciences', '.temp-clone');
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
if (existsSync(tempDir)) {
|
|
164
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
spinner.text = 'Cloning repository...';
|
|
168
|
+
execSync(`git clone --depth 1 ${REPO_URL}.git ${tempDir}`, {
|
|
169
|
+
stdio: 'pipe',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const skills = [];
|
|
173
|
+
|
|
174
|
+
// Copy selected skills
|
|
175
|
+
for (const skillPath of skillPaths) {
|
|
176
|
+
// skillPath can be like '06-post-training/verl' or '20-ml-paper-writing' (standalone)
|
|
177
|
+
const parts = skillPath.split('/');
|
|
178
|
+
const categoryId = parts[0];
|
|
179
|
+
const skillName = parts[1] || null;
|
|
180
|
+
|
|
181
|
+
const targetCategoryPath = join(CANONICAL_DIR, categoryId);
|
|
182
|
+
if (!existsSync(targetCategoryPath)) {
|
|
183
|
+
mkdirSync(targetCategoryPath, { recursive: true });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (skillName) {
|
|
187
|
+
// Nested skill like '06-post-training/verl'
|
|
188
|
+
const sourcePath = join(tempDir, categoryId, skillName);
|
|
189
|
+
if (existsSync(sourcePath)) {
|
|
190
|
+
spinner.text = `Downloading ${skillName}...`;
|
|
191
|
+
const targetSkillPath = join(targetCategoryPath, skillName);
|
|
192
|
+
if (!existsSync(targetSkillPath)) {
|
|
193
|
+
mkdirSync(targetSkillPath, { recursive: true });
|
|
194
|
+
}
|
|
195
|
+
execSync(`cp -r "${sourcePath}"/* "${targetSkillPath}/"`, { stdio: 'pipe' });
|
|
196
|
+
skills.push({ category: categoryId, skill: skillName, standalone: false });
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
// Standalone skill like '20-ml-paper-writing'
|
|
200
|
+
const sourcePath = join(tempDir, categoryId);
|
|
201
|
+
if (existsSync(sourcePath)) {
|
|
202
|
+
spinner.text = `Downloading ${categoryId}...`;
|
|
203
|
+
execSync(`cp -r "${sourcePath}"/* "${targetCategoryPath}/"`, { stdio: 'pipe' });
|
|
204
|
+
skills.push({ category: categoryId, skill: categoryId, standalone: true });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Cleanup
|
|
210
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
211
|
+
|
|
212
|
+
return skills;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (existsSync(tempDir)) {
|
|
215
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
216
|
+
}
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Install specific skills to agents
|
|
223
|
+
*/
|
|
224
|
+
export async function installSpecificSkills(skillPaths, agents) {
|
|
225
|
+
const spinner = ora('Downloading from GitHub...').start();
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
// Download skills
|
|
229
|
+
const skills = await downloadSpecificSkills(skillPaths, spinner);
|
|
230
|
+
spinner.succeed(`Downloaded ${skills.length} skills`);
|
|
231
|
+
|
|
232
|
+
// Create symlinks for each agent
|
|
233
|
+
spinner.start('Creating symlinks...');
|
|
234
|
+
|
|
235
|
+
for (const agent of agents) {
|
|
236
|
+
const count = createSymlinks(agent, skills, spinner);
|
|
237
|
+
console.log(` ${chalk.green('✓')} ${agent.name.padEnd(14)} ${chalk.dim('→')} ${agent.skillsPath.replace(homedir(), '~').padEnd(25)} ${chalk.green(count + ' skills')}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
spinner.stop();
|
|
241
|
+
|
|
242
|
+
// Update lock file
|
|
243
|
+
const lock = readLock();
|
|
244
|
+
lock.version = '1.0.0';
|
|
245
|
+
lock.installedAt = new Date().toISOString();
|
|
246
|
+
lock.skills = [...(lock.skills || []), ...skills];
|
|
247
|
+
lock.agents = agents.map(a => a.id);
|
|
248
|
+
writeLock(lock);
|
|
249
|
+
|
|
250
|
+
return skills.length;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
spinner.fail('Installation failed');
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Install skills to agents
|
|
259
|
+
*/
|
|
260
|
+
export async function installSkills(categories, agents) {
|
|
261
|
+
const spinner = ora('Downloading from GitHub...').start();
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
// Download skills
|
|
265
|
+
const skills = await downloadSkills(categories, spinner);
|
|
266
|
+
spinner.succeed(`Downloaded ${skills.length} skills`);
|
|
267
|
+
|
|
268
|
+
// Create symlinks for each agent
|
|
269
|
+
spinner.start('Creating symlinks...');
|
|
270
|
+
const results = [];
|
|
271
|
+
|
|
272
|
+
for (const agent of agents) {
|
|
273
|
+
const count = createSymlinks(agent, skills, spinner);
|
|
274
|
+
results.push({ agent, count });
|
|
275
|
+
console.log(` ${chalk.green('✓')} ${agent.name.padEnd(14)} ${chalk.dim('→')} ${agent.skillsPath.replace(homedir(), '~').padEnd(25)} ${chalk.green(count + ' skills')}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
spinner.stop();
|
|
279
|
+
|
|
280
|
+
// Update lock file
|
|
281
|
+
const lock = readLock();
|
|
282
|
+
lock.version = '1.0.0';
|
|
283
|
+
lock.installedAt = new Date().toISOString();
|
|
284
|
+
lock.skills = skills;
|
|
285
|
+
lock.agents = agents.map(a => a.id);
|
|
286
|
+
writeLock(lock);
|
|
287
|
+
|
|
288
|
+
return skills.length;
|
|
289
|
+
} catch (error) {
|
|
290
|
+
spinner.fail('Installation failed');
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* List installed skills by scanning actual folders
|
|
297
|
+
*/
|
|
298
|
+
export function listInstalledSkills() {
|
|
299
|
+
// Check if canonical skills directory exists
|
|
300
|
+
if (!existsSync(CANONICAL_DIR)) {
|
|
301
|
+
console.log(chalk.yellow(' No skills installed yet.'));
|
|
302
|
+
console.log();
|
|
303
|
+
console.log(` Run ${chalk.cyan('npx @syntheticsciences/ai-research-skills')} to install skills.`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Scan the actual skills directory
|
|
308
|
+
const categories = readdirSync(CANONICAL_DIR, { withFileTypes: true })
|
|
309
|
+
.filter(d => d.isDirectory())
|
|
310
|
+
.map(d => d.name)
|
|
311
|
+
.sort();
|
|
312
|
+
|
|
313
|
+
if (categories.length === 0) {
|
|
314
|
+
console.log(chalk.yellow(' No skills installed yet.'));
|
|
315
|
+
console.log();
|
|
316
|
+
console.log(` Run ${chalk.cyan('npx @syntheticsciences/ai-research-skills')} to install skills.`);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const byCategory = {};
|
|
321
|
+
let totalSkills = 0;
|
|
322
|
+
|
|
323
|
+
for (const category of categories) {
|
|
324
|
+
const categoryPath = join(CANONICAL_DIR, category);
|
|
325
|
+
|
|
326
|
+
// Check if it's a standalone skill (has SKILL.md directly)
|
|
327
|
+
const standaloneSkill = join(categoryPath, 'SKILL.md');
|
|
328
|
+
if (existsSync(standaloneSkill)) {
|
|
329
|
+
byCategory[category] = [category];
|
|
330
|
+
totalSkills++;
|
|
331
|
+
} else {
|
|
332
|
+
// It's a category with nested skills
|
|
333
|
+
const skills = readdirSync(categoryPath, { withFileTypes: true })
|
|
334
|
+
.filter(d => d.isDirectory() && existsSync(join(categoryPath, d.name, 'SKILL.md')))
|
|
335
|
+
.map(d => d.name)
|
|
336
|
+
.sort();
|
|
337
|
+
|
|
338
|
+
if (skills.length > 0) {
|
|
339
|
+
byCategory[category] = skills;
|
|
340
|
+
totalSkills += skills.length;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
console.log(chalk.white.bold(` Installed Skills (${totalSkills})`));
|
|
346
|
+
console.log();
|
|
347
|
+
|
|
348
|
+
for (const [category, skills] of Object.entries(byCategory)) {
|
|
349
|
+
console.log(chalk.cyan(` ${category}`));
|
|
350
|
+
for (const skill of skills) {
|
|
351
|
+
if (skill === category) {
|
|
352
|
+
// Standalone skill
|
|
353
|
+
console.log(` ${chalk.dim('●')} ${chalk.white('(standalone)')}`);
|
|
354
|
+
} else {
|
|
355
|
+
console.log(` ${chalk.dim('●')} ${skill}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
console.log();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Show storage location
|
|
362
|
+
console.log(chalk.dim(` Location: ${CANONICAL_DIR.replace(homedir(), '~')}`));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get all category IDs
|
|
367
|
+
*/
|
|
368
|
+
export function getAllCategoryIds() {
|
|
369
|
+
return [
|
|
370
|
+
'01-model-architecture',
|
|
371
|
+
'02-tokenization',
|
|
372
|
+
'03-fine-tuning',
|
|
373
|
+
'04-mechanistic-interpretability',
|
|
374
|
+
'05-data-processing',
|
|
375
|
+
'06-post-training',
|
|
376
|
+
'07-safety-alignment',
|
|
377
|
+
'08-distributed-training',
|
|
378
|
+
'09-infrastructure',
|
|
379
|
+
'10-optimization',
|
|
380
|
+
'11-evaluation',
|
|
381
|
+
'12-inference-serving',
|
|
382
|
+
'13-mlops',
|
|
383
|
+
'14-agents',
|
|
384
|
+
'15-rag',
|
|
385
|
+
'16-prompt-engineering',
|
|
386
|
+
'17-observability',
|
|
387
|
+
'18-multimodal',
|
|
388
|
+
'19-emerging-techniques',
|
|
389
|
+
'20-ml-paper-writing',
|
|
390
|
+
];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Get installed skill paths for updating
|
|
395
|
+
* Returns array like ['06-post-training/verl', '20-ml-paper-writing']
|
|
396
|
+
*/
|
|
397
|
+
export function getInstalledSkillPaths() {
|
|
398
|
+
if (!existsSync(CANONICAL_DIR)) {
|
|
399
|
+
return [];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const skillPaths = [];
|
|
403
|
+
const categories = readdirSync(CANONICAL_DIR, { withFileTypes: true })
|
|
404
|
+
.filter(d => d.isDirectory())
|
|
405
|
+
.map(d => d.name);
|
|
406
|
+
|
|
407
|
+
for (const category of categories) {
|
|
408
|
+
const categoryPath = join(CANONICAL_DIR, category);
|
|
409
|
+
|
|
410
|
+
// Check if it's a standalone skill (has SKILL.md directly)
|
|
411
|
+
const standaloneSkill = join(categoryPath, 'SKILL.md');
|
|
412
|
+
if (existsSync(standaloneSkill)) {
|
|
413
|
+
skillPaths.push(category);
|
|
414
|
+
} else {
|
|
415
|
+
// It's a category with nested skills
|
|
416
|
+
const skills = readdirSync(categoryPath, { withFileTypes: true })
|
|
417
|
+
.filter(d => d.isDirectory() && existsSync(join(categoryPath, d.name, 'SKILL.md')))
|
|
418
|
+
.map(d => d.name);
|
|
419
|
+
|
|
420
|
+
for (const skill of skills) {
|
|
421
|
+
skillPaths.push(`${category}/${skill}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return skillPaths;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Update only installed skills (re-download from GitHub)
|
|
431
|
+
*/
|
|
432
|
+
export async function updateInstalledSkills(agents) {
|
|
433
|
+
const installedPaths = getInstalledSkillPaths();
|
|
434
|
+
|
|
435
|
+
if (installedPaths.length === 0) {
|
|
436
|
+
console.log(chalk.yellow(' No skills installed to update.'));
|
|
437
|
+
return 0;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const spinner = ora('Updating from GitHub...').start();
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
// Download only the installed skills
|
|
444
|
+
const skills = await downloadSpecificSkills(installedPaths, spinner);
|
|
445
|
+
spinner.succeed(`Updated ${skills.length} skills`);
|
|
446
|
+
|
|
447
|
+
// Re-create symlinks for each agent
|
|
448
|
+
spinner.start('Refreshing symlinks...');
|
|
449
|
+
|
|
450
|
+
for (const agent of agents) {
|
|
451
|
+
const count = createSymlinks(agent, skills, spinner);
|
|
452
|
+
console.log(` ${chalk.green('✓')} ${agent.name.padEnd(14)} ${chalk.dim('→')} ${agent.skillsPath.replace(homedir(), '~').padEnd(25)} ${chalk.green(count + ' skills')}`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
spinner.stop();
|
|
456
|
+
|
|
457
|
+
// Update lock file
|
|
458
|
+
const lock = readLock();
|
|
459
|
+
lock.version = '1.0.0';
|
|
460
|
+
lock.installedAt = new Date().toISOString();
|
|
461
|
+
lock.skills = skills;
|
|
462
|
+
lock.agents = agents.map(a => a.id);
|
|
463
|
+
writeLock(lock);
|
|
464
|
+
|
|
465
|
+
return skills.length;
|
|
466
|
+
} catch (error) {
|
|
467
|
+
spinner.fail('Update failed');
|
|
468
|
+
throw error;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Uninstall all skills
|
|
474
|
+
*/
|
|
475
|
+
export async function uninstallAllSkills(agents) {
|
|
476
|
+
const spinner = ora('Removing skills...').start();
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
// Remove symlinks from each agent
|
|
480
|
+
for (const agent of agents) {
|
|
481
|
+
if (existsSync(agent.skillsPath)) {
|
|
482
|
+
const entries = readdirSync(agent.skillsPath, { withFileTypes: true });
|
|
483
|
+
for (const entry of entries) {
|
|
484
|
+
const linkPath = join(agent.skillsPath, entry.name);
|
|
485
|
+
// Only remove if it's a symlink pointing to our canonical dir
|
|
486
|
+
try {
|
|
487
|
+
const stats = lstatSync(linkPath);
|
|
488
|
+
if (stats.isSymbolicLink()) {
|
|
489
|
+
rmSync(linkPath, { force: true });
|
|
490
|
+
}
|
|
491
|
+
} catch {
|
|
492
|
+
// Ignore errors
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
console.log(` ${chalk.green('✓')} Removed symlinks from ${agent.name}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Remove canonical skills directory
|
|
500
|
+
if (existsSync(CANONICAL_DIR)) {
|
|
501
|
+
rmSync(CANONICAL_DIR, { recursive: true, force: true });
|
|
502
|
+
console.log(` ${chalk.green('✓')} Removed ${CANONICAL_DIR.replace(homedir(), '~')}`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Remove lock file
|
|
506
|
+
if (existsSync(LOCK_FILE)) {
|
|
507
|
+
rmSync(LOCK_FILE, { force: true });
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
spinner.stop();
|
|
511
|
+
return true;
|
|
512
|
+
} catch (error) {
|
|
513
|
+
spinner.fail('Uninstall failed');
|
|
514
|
+
throw error;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Uninstall specific skills
|
|
520
|
+
* @param {Array<string>} skillPaths - Paths like ['06-post-training/verl', '20-ml-paper-writing']
|
|
521
|
+
* @param {Array} agents - List of agents to remove symlinks from
|
|
522
|
+
*/
|
|
523
|
+
export async function uninstallSpecificSkills(skillPaths, agents) {
|
|
524
|
+
const spinner = ora('Removing selected skills...').start();
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
for (const skillPath of skillPaths) {
|
|
528
|
+
const parts = skillPath.split('/');
|
|
529
|
+
const categoryId = parts[0];
|
|
530
|
+
const skillName = parts[1] || null;
|
|
531
|
+
|
|
532
|
+
// Determine the link name (what appears in agent's skills folder)
|
|
533
|
+
const linkName = skillName || categoryId;
|
|
534
|
+
|
|
535
|
+
// Remove symlinks from each agent
|
|
536
|
+
for (const agent of agents) {
|
|
537
|
+
const linkPath = join(agent.skillsPath, linkName);
|
|
538
|
+
try {
|
|
539
|
+
if (existsSync(linkPath)) {
|
|
540
|
+
const stats = lstatSync(linkPath);
|
|
541
|
+
if (stats.isSymbolicLink()) {
|
|
542
|
+
rmSync(linkPath, { force: true });
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
546
|
+
// Ignore errors
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Remove from canonical directory
|
|
551
|
+
if (skillName) {
|
|
552
|
+
// Nested skill like '06-post-training/verl'
|
|
553
|
+
const skillDir = join(CANONICAL_DIR, categoryId, skillName);
|
|
554
|
+
if (existsSync(skillDir)) {
|
|
555
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
556
|
+
}
|
|
557
|
+
// Clean up empty category folder
|
|
558
|
+
const categoryDir = join(CANONICAL_DIR, categoryId);
|
|
559
|
+
if (existsSync(categoryDir)) {
|
|
560
|
+
const remaining = readdirSync(categoryDir);
|
|
561
|
+
if (remaining.length === 0) {
|
|
562
|
+
rmSync(categoryDir, { recursive: true, force: true });
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
} else {
|
|
566
|
+
// Standalone skill like '20-ml-paper-writing'
|
|
567
|
+
const skillDir = join(CANONICAL_DIR, categoryId);
|
|
568
|
+
if (existsSync(skillDir)) {
|
|
569
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
spinner.text = `Removed ${linkName}`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
spinner.succeed(`Removed ${skillPaths.length} skill${skillPaths.length !== 1 ? 's' : ''}`);
|
|
577
|
+
|
|
578
|
+
// Update lock file
|
|
579
|
+
const lock = readLock();
|
|
580
|
+
if (lock.skills) {
|
|
581
|
+
lock.skills = lock.skills.filter(s => {
|
|
582
|
+
const path = s.standalone ? s.category : `${s.category}/${s.skill}`;
|
|
583
|
+
return !skillPaths.includes(path);
|
|
584
|
+
});
|
|
585
|
+
writeLock(lock);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return skillPaths.length;
|
|
589
|
+
} catch (error) {
|
|
590
|
+
spinner.fail('Uninstall failed');
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Get installed skills with display info for selection
|
|
597
|
+
* Returns array of { path, name, category } for UI
|
|
598
|
+
*/
|
|
599
|
+
export function getInstalledSkillsForSelection() {
|
|
600
|
+
const paths = getInstalledSkillPaths();
|
|
601
|
+
return paths.map(path => {
|
|
602
|
+
const parts = path.split('/');
|
|
603
|
+
if (parts.length === 1) {
|
|
604
|
+
// Standalone skill
|
|
605
|
+
return { path, name: parts[0], category: 'Standalone', standalone: true };
|
|
606
|
+
} else {
|
|
607
|
+
// Nested skill
|
|
608
|
+
return { path, name: parts[1], category: parts[0], standalone: false };
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
}
|