@supercorks/skills-installer 1.0.0 → 1.2.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 +4 -4
- package/bin/install.js +357 -33
- package/lib/git.js +166 -1
- package/lib/prompts.js +200 -39
- package/lib/subagents.js +115 -0
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -5,13 +5,13 @@ Interactive CLI installer for AI agent skills. Selectively install skills for Gi
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx @supercorks/skills-installer
|
|
8
|
+
npx @supercorks/skills-installer
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Or with the
|
|
11
|
+
Or explicitly with the `install` command:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npx
|
|
14
|
+
npx @supercorks/skills-installer install
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## What it does
|
|
@@ -74,7 +74,7 @@ npm install
|
|
|
74
74
|
# Run locally
|
|
75
75
|
npm start
|
|
76
76
|
# or
|
|
77
|
-
node bin/install.js
|
|
77
|
+
node bin/install.js
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
## License
|
package/bin/install.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @supercorks/skills-installer
|
|
5
|
-
* Interactive CLI installer for AI agent skills
|
|
5
|
+
* Interactive CLI installer for AI agent skills and subagents
|
|
6
6
|
*
|
|
7
7
|
* Usage: npx @supercorks/skills-installer install
|
|
8
8
|
*/
|
|
@@ -10,17 +10,90 @@
|
|
|
10
10
|
import { existsSync, appendFileSync, readFileSync, writeFileSync } from 'fs';
|
|
11
11
|
import { resolve, join } from 'path';
|
|
12
12
|
import {
|
|
13
|
-
|
|
13
|
+
promptInstallType,
|
|
14
|
+
promptInstallPath,
|
|
15
|
+
promptAgentInstallPath,
|
|
14
16
|
promptGitignore,
|
|
15
17
|
promptSkillSelection,
|
|
18
|
+
promptSubagentSelection,
|
|
16
19
|
showSpinner,
|
|
17
20
|
showSuccess,
|
|
21
|
+
showSubagentSuccess,
|
|
18
22
|
showError
|
|
19
23
|
} from '../lib/prompts.js';
|
|
20
24
|
import { fetchAvailableSkills } from '../lib/skills.js';
|
|
21
|
-
import {
|
|
25
|
+
import { fetchAvailableSubagents } from '../lib/subagents.js';
|
|
26
|
+
import {
|
|
27
|
+
sparseCloneSkills,
|
|
28
|
+
isGitAvailable,
|
|
29
|
+
listCheckedOutSkills,
|
|
30
|
+
updateSparseCheckout,
|
|
31
|
+
sparseCloneSubagents,
|
|
32
|
+
listCheckedOutSubagents,
|
|
33
|
+
updateSubagentsSparseCheckout
|
|
34
|
+
} from '../lib/git.js';
|
|
35
|
+
|
|
36
|
+
const VERSION = '1.2.0';
|
|
22
37
|
|
|
23
|
-
|
|
38
|
+
// Common installation paths to check for existing installations
|
|
39
|
+
const SKILL_PATHS = ['.github/skills/', '.claude/skills/'];
|
|
40
|
+
const AGENT_PATHS = ['.github/agents/', '.claude/agents/'];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Detect existing skill installations in common paths
|
|
44
|
+
* @returns {Promise<Array<{path: string, skillCount: number, skills: string[]}>>}
|
|
45
|
+
*/
|
|
46
|
+
async function detectExistingSkillInstallations() {
|
|
47
|
+
const installations = [];
|
|
48
|
+
|
|
49
|
+
for (const path of SKILL_PATHS) {
|
|
50
|
+
const absolutePath = resolve(process.cwd(), path);
|
|
51
|
+
const gitDir = join(absolutePath, '.git');
|
|
52
|
+
|
|
53
|
+
if (existsSync(gitDir)) {
|
|
54
|
+
try {
|
|
55
|
+
const skills = await listCheckedOutSkills(absolutePath);
|
|
56
|
+
installations.push({
|
|
57
|
+
path,
|
|
58
|
+
skillCount: skills.length,
|
|
59
|
+
skills
|
|
60
|
+
});
|
|
61
|
+
} catch {
|
|
62
|
+
// Ignore errors reading existing installations
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return installations;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Detect existing subagent installations in common paths
|
|
72
|
+
* @returns {Promise<Array<{path: string, agentCount: number, agents: string[]}>>}
|
|
73
|
+
*/
|
|
74
|
+
async function detectExistingAgentInstallations() {
|
|
75
|
+
const installations = [];
|
|
76
|
+
|
|
77
|
+
for (const path of AGENT_PATHS) {
|
|
78
|
+
const absolutePath = resolve(process.cwd(), path);
|
|
79
|
+
const gitDir = join(absolutePath, '.git');
|
|
80
|
+
|
|
81
|
+
if (existsSync(gitDir)) {
|
|
82
|
+
try {
|
|
83
|
+
const agents = await listCheckedOutSubagents(absolutePath);
|
|
84
|
+
installations.push({
|
|
85
|
+
path,
|
|
86
|
+
agentCount: agents.length,
|
|
87
|
+
agents
|
|
88
|
+
});
|
|
89
|
+
} catch {
|
|
90
|
+
// Ignore errors reading existing installations
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return installations;
|
|
96
|
+
}
|
|
24
97
|
|
|
25
98
|
/**
|
|
26
99
|
* Print usage information
|
|
@@ -30,7 +103,7 @@ function printUsage() {
|
|
|
30
103
|
@supercorks/skills-installer v${VERSION}
|
|
31
104
|
|
|
32
105
|
Usage:
|
|
33
|
-
npx @supercorks/skills-installer Install skills interactively (default)
|
|
106
|
+
npx @supercorks/skills-installer Install skills/subagents interactively (default)
|
|
34
107
|
npx @supercorks/skills-installer --help Show this help message
|
|
35
108
|
npx @supercorks/skills-installer --version Show version
|
|
36
109
|
|
|
@@ -67,7 +140,7 @@ function addToGitignore(gitignorePath, pathToIgnore) {
|
|
|
67
140
|
* Main installation flow
|
|
68
141
|
*/
|
|
69
142
|
async function runInstall() {
|
|
70
|
-
console.log('\n🔧 AI Agent Skills Installer\n');
|
|
143
|
+
console.log('\n🔧 AI Agent Skills & Subagents Installer\n');
|
|
71
144
|
|
|
72
145
|
// Check git availability
|
|
73
146
|
if (!isGitAvailable()) {
|
|
@@ -75,7 +148,27 @@ async function runInstall() {
|
|
|
75
148
|
process.exit(1);
|
|
76
149
|
}
|
|
77
150
|
|
|
78
|
-
// Step 1:
|
|
151
|
+
// Step 1: Ask what to install
|
|
152
|
+
const { skills: installSkills, subagents: installSubagents } = await promptInstallType();
|
|
153
|
+
|
|
154
|
+
// Install skills if selected
|
|
155
|
+
if (installSkills) {
|
|
156
|
+
await runSkillsInstall();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Install subagents if selected
|
|
160
|
+
if (installSubagents) {
|
|
161
|
+
await runSubagentsInstall();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Skills installation flow
|
|
167
|
+
*/
|
|
168
|
+
async function runSkillsInstall() {
|
|
169
|
+
console.log('\n📦 Skills Installation\n');
|
|
170
|
+
|
|
171
|
+
// Fetch available skills
|
|
79
172
|
let skills;
|
|
80
173
|
const fetchSpinner = showSpinner('Fetching available skills from repository...');
|
|
81
174
|
try {
|
|
@@ -92,48 +185,279 @@ async function runInstall() {
|
|
|
92
185
|
process.exit(1);
|
|
93
186
|
}
|
|
94
187
|
|
|
95
|
-
//
|
|
96
|
-
const
|
|
188
|
+
// Detect existing installations
|
|
189
|
+
const existingInstalls = await detectExistingSkillInstallations();
|
|
190
|
+
|
|
191
|
+
// Ask where to install (showing existing installations if any)
|
|
192
|
+
const { path: installPath, isExisting } = await promptInstallPath(existingInstalls);
|
|
97
193
|
const absoluteInstallPath = resolve(process.cwd(), installPath);
|
|
98
194
|
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
195
|
+
// Get currently installed skills if managing existing installation
|
|
196
|
+
let installedSkills = [];
|
|
197
|
+
if (isExisting) {
|
|
198
|
+
const existingInstall = existingInstalls.find(i => i.path === installPath);
|
|
199
|
+
installedSkills = existingInstall?.skills || [];
|
|
200
|
+
} else {
|
|
201
|
+
// Check if manually entered path has an existing installation
|
|
202
|
+
const gitDir = join(absoluteInstallPath, '.git');
|
|
203
|
+
if (existsSync(gitDir)) {
|
|
204
|
+
try {
|
|
205
|
+
installedSkills = await listCheckedOutSkills(absoluteInstallPath);
|
|
206
|
+
} catch {
|
|
207
|
+
// If we can't read it, treat as fresh install
|
|
208
|
+
}
|
|
209
|
+
}
|
|
103
210
|
}
|
|
104
211
|
|
|
105
|
-
|
|
106
|
-
const shouldGitignore = await promptGitignore(installPath);
|
|
212
|
+
const isManageMode = installedSkills.length > 0;
|
|
107
213
|
|
|
108
|
-
//
|
|
109
|
-
|
|
214
|
+
// Ask about .gitignore (only for fresh installs)
|
|
215
|
+
let shouldGitignore = false;
|
|
216
|
+
if (!isManageMode) {
|
|
217
|
+
shouldGitignore = await promptGitignore(installPath);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Select skills (pre-select installed skills in manage mode)
|
|
221
|
+
const selectedSkills = await promptSkillSelection(skills, installedSkills);
|
|
110
222
|
|
|
111
|
-
//
|
|
223
|
+
// Perform installation or update
|
|
112
224
|
console.log('');
|
|
113
|
-
const installSpinner = showSpinner('Installing selected skills...');
|
|
114
225
|
|
|
226
|
+
if (isManageMode) {
|
|
227
|
+
// Calculate changes
|
|
228
|
+
const toAdd = selectedSkills.filter(s => !installedSkills.includes(s));
|
|
229
|
+
const toRemove = installedSkills.filter(s => !selectedSkills.includes(s));
|
|
230
|
+
const unchanged = selectedSkills.filter(s => installedSkills.includes(s));
|
|
231
|
+
|
|
232
|
+
if (toAdd.length === 0 && toRemove.length === 0) {
|
|
233
|
+
console.log('ℹ️ No changes to apply. Pulling latest updates...');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const updateSpinner = showSpinner('Updating skills installation...');
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
await updateSparseCheckout(absoluteInstallPath, selectedSkills, (message) => {
|
|
240
|
+
updateSpinner.stop(` ${message}`);
|
|
241
|
+
});
|
|
242
|
+
} catch (error) {
|
|
243
|
+
updateSpinner.stop('❌ Update failed');
|
|
244
|
+
showError(error.message);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Show summary of changes
|
|
249
|
+
showSkillManageSuccess(installPath, skills, toAdd, toRemove, unchanged);
|
|
250
|
+
} else {
|
|
251
|
+
const installSpinner = showSpinner('Installing selected skills...');
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
await sparseCloneSkills(installPath, selectedSkills, (message) => {
|
|
255
|
+
installSpinner.stop(` ${message}`);
|
|
256
|
+
});
|
|
257
|
+
} catch (error) {
|
|
258
|
+
installSpinner.stop('❌ Installation failed');
|
|
259
|
+
showError(error.message);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Update .gitignore if requested
|
|
264
|
+
if (shouldGitignore) {
|
|
265
|
+
const gitignorePath = resolve(process.cwd(), '.gitignore');
|
|
266
|
+
addToGitignore(gitignorePath, installPath);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Show success
|
|
270
|
+
const installedSkillNames = skills
|
|
271
|
+
.filter(s => selectedSkills.includes(s.folder))
|
|
272
|
+
.map(s => s.name);
|
|
273
|
+
|
|
274
|
+
showSuccess(installPath, installedSkillNames);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Subagents installation flow
|
|
280
|
+
*/
|
|
281
|
+
async function runSubagentsInstall() {
|
|
282
|
+
console.log('\n🤖 Subagents Installation\n');
|
|
283
|
+
|
|
284
|
+
// Fetch available subagents
|
|
285
|
+
let subagents;
|
|
286
|
+
const fetchSpinner = showSpinner('Fetching available subagents from repository...');
|
|
115
287
|
try {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
});
|
|
288
|
+
subagents = await fetchAvailableSubagents();
|
|
289
|
+
fetchSpinner.stop(`✅ Found ${subagents.length} available subagents`);
|
|
119
290
|
} catch (error) {
|
|
120
|
-
|
|
121
|
-
showError(error.message);
|
|
291
|
+
fetchSpinner.stop('❌ Failed to fetch subagents');
|
|
292
|
+
showError(`Could not fetch subagents list: ${error.message}`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (subagents.length === 0) {
|
|
297
|
+
showError('No subagents found in the repository');
|
|
122
298
|
process.exit(1);
|
|
123
299
|
}
|
|
124
300
|
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
301
|
+
// Detect existing installations
|
|
302
|
+
const existingInstalls = await detectExistingAgentInstallations();
|
|
303
|
+
|
|
304
|
+
// Ask where to install (showing existing installations if any)
|
|
305
|
+
const { path: installPath, isExisting } = await promptAgentInstallPath(existingInstalls);
|
|
306
|
+
const absoluteInstallPath = resolve(process.cwd(), installPath);
|
|
307
|
+
|
|
308
|
+
// Get currently installed subagents if managing existing installation
|
|
309
|
+
let installedAgents = [];
|
|
310
|
+
if (isExisting) {
|
|
311
|
+
const existingInstall = existingInstalls.find(i => i.path === installPath);
|
|
312
|
+
installedAgents = existingInstall?.agents || [];
|
|
313
|
+
} else {
|
|
314
|
+
// Check if manually entered path has an existing installation
|
|
315
|
+
const gitDir = join(absoluteInstallPath, '.git');
|
|
316
|
+
if (existsSync(gitDir)) {
|
|
317
|
+
try {
|
|
318
|
+
installedAgents = await listCheckedOutSubagents(absoluteInstallPath);
|
|
319
|
+
} catch {
|
|
320
|
+
// If we can't read it, treat as fresh install
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const isManageMode = installedAgents.length > 0;
|
|
326
|
+
|
|
327
|
+
// Ask about .gitignore (only for fresh installs)
|
|
328
|
+
let shouldGitignore = false;
|
|
329
|
+
if (!isManageMode) {
|
|
330
|
+
shouldGitignore = await promptGitignore(installPath);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Select subagents (pre-select installed ones in manage mode)
|
|
334
|
+
const selectedAgents = await promptSubagentSelection(subagents, installedAgents);
|
|
335
|
+
|
|
336
|
+
// Perform installation or update
|
|
337
|
+
console.log('');
|
|
338
|
+
|
|
339
|
+
if (isManageMode) {
|
|
340
|
+
// Calculate changes
|
|
341
|
+
const toAdd = selectedAgents.filter(s => !installedAgents.includes(s));
|
|
342
|
+
const toRemove = installedAgents.filter(s => !selectedAgents.includes(s));
|
|
343
|
+
const unchanged = selectedAgents.filter(s => installedAgents.includes(s));
|
|
344
|
+
|
|
345
|
+
if (toAdd.length === 0 && toRemove.length === 0) {
|
|
346
|
+
console.log('ℹ️ No changes to apply. Pulling latest updates...');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const updateSpinner = showSpinner('Updating subagents installation...');
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
await updateSubagentsSparseCheckout(absoluteInstallPath, selectedAgents, (message) => {
|
|
353
|
+
updateSpinner.stop(` ${message}`);
|
|
354
|
+
});
|
|
355
|
+
} catch (error) {
|
|
356
|
+
updateSpinner.stop('❌ Update failed');
|
|
357
|
+
showError(error.message);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Show summary of changes
|
|
362
|
+
showAgentManageSuccess(installPath, subagents, toAdd, toRemove, unchanged);
|
|
363
|
+
} else {
|
|
364
|
+
const installSpinner = showSpinner('Installing selected subagents...');
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
await sparseCloneSubagents(installPath, selectedAgents, (message) => {
|
|
368
|
+
installSpinner.stop(` ${message}`);
|
|
369
|
+
});
|
|
370
|
+
} catch (error) {
|
|
371
|
+
installSpinner.stop('❌ Installation failed');
|
|
372
|
+
showError(error.message);
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Update .gitignore if requested
|
|
377
|
+
if (shouldGitignore) {
|
|
378
|
+
const gitignorePath = resolve(process.cwd(), '.gitignore');
|
|
379
|
+
addToGitignore(gitignorePath, installPath);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Show success
|
|
383
|
+
const installedAgentNames = subagents
|
|
384
|
+
.filter(s => selectedAgents.includes(s.filename))
|
|
385
|
+
.map(s => s.name);
|
|
386
|
+
|
|
387
|
+
showSubagentSuccess(installPath, installedAgentNames);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Display success message for manage mode with skill change summary
|
|
393
|
+
* @param {string} installPath - Where skills are installed
|
|
394
|
+
* @param {Array<{name: string, folder: string}>} allSkills - All available skills
|
|
395
|
+
* @param {string[]} added - Skill folders that were added
|
|
396
|
+
* @param {string[]} removed - Skill folders that were removed
|
|
397
|
+
* @param {string[]} unchanged - Skill folders that were unchanged
|
|
398
|
+
*/
|
|
399
|
+
function showSkillManageSuccess(installPath, allSkills, added, removed, unchanged) {
|
|
400
|
+
const getSkillName = (folder) => allSkills.find(s => s.folder === folder)?.name || folder;
|
|
401
|
+
|
|
402
|
+
console.log('\n' + '═'.repeat(50));
|
|
403
|
+
console.log('✅ Skills updated successfully!');
|
|
404
|
+
console.log('═'.repeat(50));
|
|
405
|
+
console.log(`\n📁 Location: ${installPath}`);
|
|
406
|
+
|
|
407
|
+
if (added.length > 0) {
|
|
408
|
+
console.log(`\n➕ Added (${added.length}):`);
|
|
409
|
+
added.forEach(folder => console.log(` • ${getSkillName(folder)}`));
|
|
129
410
|
}
|
|
411
|
+
|
|
412
|
+
if (removed.length > 0) {
|
|
413
|
+
console.log(`\n➖ Removed (${removed.length}):`);
|
|
414
|
+
removed.forEach(folder => console.log(` • ${getSkillName(folder)}`));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (unchanged.length > 0) {
|
|
418
|
+
console.log(`\n📦 Unchanged (${unchanged.length}):`);
|
|
419
|
+
unchanged.forEach(folder => console.log(` • ${getSkillName(folder)}`));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const totalInstalled = added.length + unchanged.length;
|
|
423
|
+
console.log(`\n🚀 ${totalInstalled} skill${totalInstalled !== 1 ? 's' : ''} now installed.`);
|
|
424
|
+
console.log('═'.repeat(50) + '\n');
|
|
425
|
+
}
|
|
130
426
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
427
|
+
/**
|
|
428
|
+
* Display success message for manage mode with subagent change summary
|
|
429
|
+
* @param {string} installPath - Where subagents are installed
|
|
430
|
+
* @param {Array<{name: string, filename: string}>} allAgents - All available subagents
|
|
431
|
+
* @param {string[]} added - Agent filenames that were added
|
|
432
|
+
* @param {string[]} removed - Agent filenames that were removed
|
|
433
|
+
* @param {string[]} unchanged - Agent filenames that were unchanged
|
|
434
|
+
*/
|
|
435
|
+
function showAgentManageSuccess(installPath, allAgents, added, removed, unchanged) {
|
|
436
|
+
const getAgentName = (filename) => allAgents.find(s => s.filename === filename)?.name || filename;
|
|
437
|
+
|
|
438
|
+
console.log('\n' + '═'.repeat(50));
|
|
439
|
+
console.log('✅ Subagents updated successfully!');
|
|
440
|
+
console.log('═'.repeat(50));
|
|
441
|
+
console.log(`\n📁 Location: ${installPath}`);
|
|
442
|
+
|
|
443
|
+
if (added.length > 0) {
|
|
444
|
+
console.log(`\n➕ Added (${added.length}):`);
|
|
445
|
+
added.forEach(filename => console.log(` • ${getAgentName(filename)}`));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (removed.length > 0) {
|
|
449
|
+
console.log(`\n➖ Removed (${removed.length}):`);
|
|
450
|
+
removed.forEach(filename => console.log(` • ${getAgentName(filename)}`));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (unchanged.length > 0) {
|
|
454
|
+
console.log(`\n🤖 Unchanged (${unchanged.length}):`);
|
|
455
|
+
unchanged.forEach(filename => console.log(` • ${getAgentName(filename)}`));
|
|
456
|
+
}
|
|
135
457
|
|
|
136
|
-
|
|
458
|
+
const totalInstalled = added.length + unchanged.length;
|
|
459
|
+
console.log(`\n🚀 ${totalInstalled} subagent${totalInstalled !== 1 ? 's' : ''} now installed.`);
|
|
460
|
+
console.log('═'.repeat(50) + '\n');
|
|
137
461
|
}
|
|
138
462
|
|
|
139
463
|
/**
|
package/lib/git.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Git sparse-checkout utilities for cloning selected skills
|
|
2
|
+
* Git sparse-checkout utilities for cloning selected skills and subagents
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { execSync, spawn } from 'child_process';
|
|
6
6
|
import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync, appendFileSync } from 'fs';
|
|
7
7
|
import { join, resolve } from 'path';
|
|
8
8
|
import { getRepoUrl } from './skills.js';
|
|
9
|
+
import { getSubagentsRepoUrl } from './subagents.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Check if git is available
|
|
@@ -118,6 +119,42 @@ export async function sparseCloneSkills(targetPath, skillFolders, onProgress = (
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Update the sparse-checkout to include exactly the specified skills
|
|
124
|
+
* This replaces all existing skills with the new selection
|
|
125
|
+
* @param {string} repoPath - Path to the existing sparse-checkout repo
|
|
126
|
+
* @param {string[]} skillFolders - Skill folders to include (replaces existing)
|
|
127
|
+
* @param {(message: string) => void} onProgress - Progress callback
|
|
128
|
+
* @returns {Promise<void>}
|
|
129
|
+
*/
|
|
130
|
+
export async function updateSparseCheckout(repoPath, skillFolders, onProgress = () => {}) {
|
|
131
|
+
const absolutePath = resolve(repoPath);
|
|
132
|
+
|
|
133
|
+
if (!existsSync(join(absolutePath, '.git'))) {
|
|
134
|
+
throw new Error(`"${repoPath}" is not a git repository`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Pull latest changes first
|
|
138
|
+
onProgress('Pulling latest changes...');
|
|
139
|
+
try {
|
|
140
|
+
await runGitCommand(['pull'], absolutePath);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
// Ignore pull errors (e.g., no upstream configured)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Write new patterns to sparse-checkout file (replaces existing)
|
|
146
|
+
onProgress('Updating sparse-checkout configuration...');
|
|
147
|
+
const sparseCheckoutPath = join(absolutePath, '.git', 'info', 'sparse-checkout');
|
|
148
|
+
const patterns = skillFolders.map(folder => `/${folder}/`).join('\n');
|
|
149
|
+
writeFileSync(sparseCheckoutPath, patterns + '\n');
|
|
150
|
+
|
|
151
|
+
// Re-apply sparse-checkout
|
|
152
|
+
onProgress('Applying changes...');
|
|
153
|
+
await runGitCommand(['read-tree', '-mu', 'HEAD'], absolutePath);
|
|
154
|
+
|
|
155
|
+
onProgress('Done!');
|
|
156
|
+
}
|
|
157
|
+
|
|
121
158
|
/**
|
|
122
159
|
* Add more skills to an existing sparse-checkout
|
|
123
160
|
* @param {string} repoPath - Path to the existing sparse-checkout repo
|
|
@@ -176,3 +213,131 @@ export async function pullUpdates(repoPath) {
|
|
|
176
213
|
const absolutePath = resolve(repoPath);
|
|
177
214
|
return runGitCommand(['pull'], absolutePath);
|
|
178
215
|
}
|
|
216
|
+
|
|
217
|
+
// ==================== SUBAGENTS FUNCTIONS ====================
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Perform a sparse clone of the subagents repository with only selected agent files
|
|
221
|
+
*
|
|
222
|
+
* @param {string} targetPath - Where to clone the repository
|
|
223
|
+
* @param {string[]} agentFilenames - Array of agent filenames to include (e.g., 'Developer.agent.md')
|
|
224
|
+
* @param {(message: string) => void} onProgress - Progress callback
|
|
225
|
+
* @returns {Promise<void>}
|
|
226
|
+
*/
|
|
227
|
+
export async function sparseCloneSubagents(targetPath, agentFilenames, onProgress = () => {}) {
|
|
228
|
+
const repoUrl = getSubagentsRepoUrl();
|
|
229
|
+
const absolutePath = resolve(targetPath);
|
|
230
|
+
|
|
231
|
+
// Check if target already exists and has content
|
|
232
|
+
if (existsSync(absolutePath)) {
|
|
233
|
+
const gitDir = join(absolutePath, '.git');
|
|
234
|
+
if (existsSync(gitDir)) {
|
|
235
|
+
throw new Error(`Directory "${targetPath}" already contains a git repository. Please remove it first or choose a different path.`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Create target directory
|
|
240
|
+
mkdirSync(absolutePath, { recursive: true });
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
// Clone with blob filter for minimal download, no checkout yet
|
|
244
|
+
onProgress('Initializing sparse clone...');
|
|
245
|
+
await runGitCommand([
|
|
246
|
+
'clone',
|
|
247
|
+
'--filter=blob:none',
|
|
248
|
+
'--no-checkout',
|
|
249
|
+
'--sparse',
|
|
250
|
+
repoUrl,
|
|
251
|
+
'.'
|
|
252
|
+
], absolutePath);
|
|
253
|
+
|
|
254
|
+
// Use non-cone mode for precise control over what's checked out
|
|
255
|
+
onProgress('Configuring sparse-checkout...');
|
|
256
|
+
await runGitCommand([
|
|
257
|
+
'sparse-checkout',
|
|
258
|
+
'init',
|
|
259
|
+
'--no-cone'
|
|
260
|
+
], absolutePath);
|
|
261
|
+
|
|
262
|
+
// Write explicit patterns - only selected agent files
|
|
263
|
+
const sparseCheckoutPath = join(absolutePath, '.git', 'info', 'sparse-checkout');
|
|
264
|
+
const patterns = agentFilenames.map(filename => `/${filename}`).join('\n');
|
|
265
|
+
writeFileSync(sparseCheckoutPath, patterns + '\n');
|
|
266
|
+
|
|
267
|
+
// Checkout the files
|
|
268
|
+
onProgress('Checking out files...');
|
|
269
|
+
await runGitCommand(['checkout'], absolutePath);
|
|
270
|
+
|
|
271
|
+
onProgress('Done!');
|
|
272
|
+
} catch (error) {
|
|
273
|
+
// Clean up on failure
|
|
274
|
+
try {
|
|
275
|
+
rmSync(absolutePath, { recursive: true, force: true });
|
|
276
|
+
} catch {
|
|
277
|
+
// Ignore cleanup errors
|
|
278
|
+
}
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Update the sparse-checkout for subagents to include exactly the specified agents
|
|
285
|
+
* @param {string} repoPath - Path to the existing sparse-checkout repo
|
|
286
|
+
* @param {string[]} agentFilenames - Agent filenames to include (replaces existing)
|
|
287
|
+
* @param {(message: string) => void} onProgress - Progress callback
|
|
288
|
+
* @returns {Promise<void>}
|
|
289
|
+
*/
|
|
290
|
+
export async function updateSubagentsSparseCheckout(repoPath, agentFilenames, onProgress = () => {}) {
|
|
291
|
+
const absolutePath = resolve(repoPath);
|
|
292
|
+
|
|
293
|
+
if (!existsSync(join(absolutePath, '.git'))) {
|
|
294
|
+
throw new Error(`"${repoPath}" is not a git repository`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Pull latest changes first
|
|
298
|
+
onProgress('Pulling latest changes...');
|
|
299
|
+
try {
|
|
300
|
+
await runGitCommand(['pull'], absolutePath);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
// Ignore pull errors (e.g., no upstream configured)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Write new patterns to sparse-checkout file (replaces existing)
|
|
306
|
+
onProgress('Updating sparse-checkout configuration...');
|
|
307
|
+
const sparseCheckoutPath = join(absolutePath, '.git', 'info', 'sparse-checkout');
|
|
308
|
+
const patterns = agentFilenames.map(filename => `/${filename}`).join('\n');
|
|
309
|
+
writeFileSync(sparseCheckoutPath, patterns + '\n');
|
|
310
|
+
|
|
311
|
+
// Re-apply sparse-checkout
|
|
312
|
+
onProgress('Applying changes...');
|
|
313
|
+
await runGitCommand(['read-tree', '-mu', 'HEAD'], absolutePath);
|
|
314
|
+
|
|
315
|
+
onProgress('Done!');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* List currently checked out subagents in a sparse-checkout repo
|
|
320
|
+
* @param {string} repoPath - Path to the sparse-checkout repo
|
|
321
|
+
* @returns {Promise<string[]>} List of agent filenames
|
|
322
|
+
*/
|
|
323
|
+
export async function listCheckedOutSubagents(repoPath) {
|
|
324
|
+
const absolutePath = resolve(repoPath);
|
|
325
|
+
|
|
326
|
+
if (!existsSync(join(absolutePath, '.git'))) {
|
|
327
|
+
throw new Error(`"${repoPath}" is not a git repository`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Read patterns from sparse-checkout file (non-cone mode format: /filename)
|
|
331
|
+
const sparseCheckoutPath = join(absolutePath, '.git', 'info', 'sparse-checkout');
|
|
332
|
+
if (!existsSync(sparseCheckoutPath)) {
|
|
333
|
+
return [];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const content = readFileSync(sparseCheckoutPath, 'utf-8');
|
|
337
|
+
const patterns = content.split('\n').filter(Boolean);
|
|
338
|
+
|
|
339
|
+
// Extract filenames from patterns like /Developer.agent.md
|
|
340
|
+
return patterns
|
|
341
|
+
.map(p => p.replace(/^\//, '')) // Remove leading slash
|
|
342
|
+
.filter(p => p && p.endsWith('.agent.md'));
|
|
343
|
+
}
|
package/lib/prompts.js
CHANGED
|
@@ -5,12 +5,18 @@
|
|
|
5
5
|
import inquirer from 'inquirer';
|
|
6
6
|
import * as readline from 'readline';
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const SKILL_PATH_CHOICES = {
|
|
9
9
|
GITHUB: '.github/skills/',
|
|
10
10
|
CLAUDE: '.claude/skills/',
|
|
11
11
|
CUSTOM: '__custom__'
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
const AGENT_PATH_CHOICES = {
|
|
15
|
+
GITHUB: '.github/agents/',
|
|
16
|
+
CLAUDE: '.claude/agents/',
|
|
17
|
+
CUSTOM: '__custom__'
|
|
18
|
+
};
|
|
19
|
+
|
|
14
20
|
/**
|
|
15
21
|
* Extract the first sentence from a description
|
|
16
22
|
* @param {string} text - Full description text
|
|
@@ -27,25 +33,72 @@ function getFirstSentence(text, maxLength = 60) {
|
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
/**
|
|
30
|
-
* Prompt user to select
|
|
31
|
-
* @returns {Promise<
|
|
36
|
+
* Prompt user to select what to install
|
|
37
|
+
* @returns {Promise<{skills: boolean, subagents: boolean}>}
|
|
38
|
+
*/
|
|
39
|
+
export async function promptInstallType() {
|
|
40
|
+
const { installType } = await inquirer.prompt([
|
|
41
|
+
{
|
|
42
|
+
type: 'list',
|
|
43
|
+
name: 'installType',
|
|
44
|
+
message: 'What would you like to install?',
|
|
45
|
+
choices: [
|
|
46
|
+
{ name: 'Skills only', value: 'skills' },
|
|
47
|
+
{ name: 'Subagents only', value: 'subagents' },
|
|
48
|
+
{ name: 'Both skills and subagents', value: 'both' }
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
skills: installType === 'skills' || installType === 'both',
|
|
55
|
+
subagents: installType === 'subagents' || installType === 'both'
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Prompt user to select installation path, showing existing installations
|
|
61
|
+
* @param {Array<{path: string, skillCount: number}>} existingInstalls - Detected existing installations
|
|
62
|
+
* @returns {Promise<{path: string, isExisting: boolean}>} The selected path and whether it's existing
|
|
32
63
|
*/
|
|
33
|
-
export async function promptInstallPath() {
|
|
64
|
+
export async function promptInstallPath(existingInstalls = []) {
|
|
65
|
+
const choices = [];
|
|
66
|
+
|
|
67
|
+
// Add existing installations at the top
|
|
68
|
+
if (existingInstalls.length > 0) {
|
|
69
|
+
existingInstalls.forEach(install => {
|
|
70
|
+
choices.push({
|
|
71
|
+
name: `${install.path} (${install.skillCount} skill${install.skillCount !== 1 ? 's' : ''} installed)`,
|
|
72
|
+
value: { path: install.path, isExisting: true }
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
choices.push(new inquirer.Separator('── New installation ──'));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Standard path options
|
|
79
|
+
const standardPaths = [SKILL_PATH_CHOICES.GITHUB, SKILL_PATH_CHOICES.CLAUDE];
|
|
80
|
+
const existingPaths = existingInstalls.map(i => i.path);
|
|
81
|
+
|
|
82
|
+
standardPaths.forEach(path => {
|
|
83
|
+
if (!existingPaths.includes(path)) {
|
|
84
|
+
choices.push({ name: path, value: { path, isExisting: false } });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
choices.push({ name: 'Custom path...', value: { path: SKILL_PATH_CHOICES.CUSTOM, isExisting: false } });
|
|
89
|
+
|
|
34
90
|
const { pathChoice } = await inquirer.prompt([
|
|
35
91
|
{
|
|
36
92
|
type: 'list',
|
|
37
93
|
name: 'pathChoice',
|
|
38
|
-
message:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
{ name: 'Custom path...', value: PATH_CHOICES.CUSTOM }
|
|
43
|
-
],
|
|
44
|
-
default: PATH_CHOICES.GITHUB
|
|
94
|
+
message: existingInstalls.length > 0
|
|
95
|
+
? 'Select an existing installation to manage, or choose a new location:'
|
|
96
|
+
: 'Where would you like to install the skills?',
|
|
97
|
+
choices
|
|
45
98
|
}
|
|
46
99
|
]);
|
|
47
100
|
|
|
48
|
-
if (pathChoice ===
|
|
101
|
+
if (pathChoice.path === SKILL_PATH_CHOICES.CUSTOM) {
|
|
49
102
|
const { customPath } = await inquirer.prompt([
|
|
50
103
|
{
|
|
51
104
|
type: 'input',
|
|
@@ -59,7 +112,69 @@ export async function promptInstallPath() {
|
|
|
59
112
|
}
|
|
60
113
|
}
|
|
61
114
|
]);
|
|
62
|
-
return customPath.trim();
|
|
115
|
+
return { path: customPath.trim(), isExisting: false };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return pathChoice;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Prompt user to select subagent installation path
|
|
123
|
+
* @param {Array<{path: string, agentCount: number}>} existingInstalls - Detected existing installations
|
|
124
|
+
* @returns {Promise<{path: string, isExisting: boolean}>} The selected path and whether it's existing
|
|
125
|
+
*/
|
|
126
|
+
export async function promptAgentInstallPath(existingInstalls = []) {
|
|
127
|
+
const choices = [];
|
|
128
|
+
|
|
129
|
+
// Add existing installations at the top
|
|
130
|
+
if (existingInstalls.length > 0) {
|
|
131
|
+
existingInstalls.forEach(install => {
|
|
132
|
+
choices.push({
|
|
133
|
+
name: `${install.path} (${install.agentCount} agent${install.agentCount !== 1 ? 's' : ''} installed)`,
|
|
134
|
+
value: { path: install.path, isExisting: true }
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
choices.push(new inquirer.Separator('── New installation ──'));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Standard path options
|
|
141
|
+
const standardPaths = [AGENT_PATH_CHOICES.GITHUB, AGENT_PATH_CHOICES.CLAUDE];
|
|
142
|
+
const existingPaths = existingInstalls.map(i => i.path);
|
|
143
|
+
|
|
144
|
+
standardPaths.forEach(path => {
|
|
145
|
+
if (!existingPaths.includes(path)) {
|
|
146
|
+
choices.push({ name: path, value: { path, isExisting: false } });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
choices.push({ name: 'Custom path...', value: { path: AGENT_PATH_CHOICES.CUSTOM, isExisting: false } });
|
|
151
|
+
|
|
152
|
+
const { pathChoice } = await inquirer.prompt([
|
|
153
|
+
{
|
|
154
|
+
type: 'list',
|
|
155
|
+
name: 'pathChoice',
|
|
156
|
+
message: existingInstalls.length > 0
|
|
157
|
+
? 'Select an existing installation to manage, or choose a new location:'
|
|
158
|
+
: 'Where would you like to install the subagents?',
|
|
159
|
+
choices
|
|
160
|
+
}
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
if (pathChoice.path === AGENT_PATH_CHOICES.CUSTOM) {
|
|
164
|
+
const { customPath } = await inquirer.prompt([
|
|
165
|
+
{
|
|
166
|
+
type: 'input',
|
|
167
|
+
name: 'customPath',
|
|
168
|
+
message: 'Enter custom installation path:',
|
|
169
|
+
validate: (input) => {
|
|
170
|
+
if (!input.trim()) {
|
|
171
|
+
return 'Please enter a valid path';
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
]);
|
|
177
|
+
return { path: customPath.trim(), isExisting: false };
|
|
63
178
|
}
|
|
64
179
|
|
|
65
180
|
return pathChoice;
|
|
@@ -86,9 +201,39 @@ export async function promptGitignore(installPath) {
|
|
|
86
201
|
/**
|
|
87
202
|
* Prompt user to select skills to install with expand/collapse support
|
|
88
203
|
* @param {Array<{name: string, description: string, folder: string}>} skills - Available skills
|
|
204
|
+
* @param {string[]} installedSkills - Already installed skill folder names (will be pre-selected)
|
|
89
205
|
* @returns {Promise<string[]>} Selected skill folder names
|
|
90
206
|
*/
|
|
91
|
-
export async function promptSkillSelection(skills) {
|
|
207
|
+
export async function promptSkillSelection(skills, installedSkills = []) {
|
|
208
|
+
return promptItemSelection(
|
|
209
|
+
skills.map(s => ({ id: s.folder, name: s.name, description: s.description })),
|
|
210
|
+
installedSkills,
|
|
211
|
+
'📦 Available Skills'
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Prompt user to select subagents to install with expand/collapse support
|
|
217
|
+
* @param {Array<{name: string, description: string, filename: string}>} subagents - Available subagents
|
|
218
|
+
* @param {string[]} installedSubagents - Already installed subagent filenames (will be pre-selected)
|
|
219
|
+
* @returns {Promise<string[]>} Selected subagent filenames
|
|
220
|
+
*/
|
|
221
|
+
export async function promptSubagentSelection(subagents, installedSubagents = []) {
|
|
222
|
+
return promptItemSelection(
|
|
223
|
+
subagents.map(s => ({ id: s.filename, name: s.name, description: s.description })),
|
|
224
|
+
installedSubagents,
|
|
225
|
+
'🤖 Available Subagents'
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Generic item selection prompt with expand/collapse support
|
|
231
|
+
* @param {Array<{id: string, name: string, description: string}>} items - Available items
|
|
232
|
+
* @param {string[]} installedItems - Already installed item IDs (will be pre-selected)
|
|
233
|
+
* @param {string} title - Title to display
|
|
234
|
+
* @returns {Promise<string[]>} Selected item IDs
|
|
235
|
+
*/
|
|
236
|
+
function promptItemSelection(items, installedItems = [], title = '📦 Available Items') {
|
|
92
237
|
return new Promise((resolve, reject) => {
|
|
93
238
|
const rl = readline.createInterface({
|
|
94
239
|
input: process.stdin,
|
|
@@ -102,22 +247,22 @@ export async function promptSkillSelection(skills) {
|
|
|
102
247
|
readline.emitKeypressEvents(process.stdin, rl);
|
|
103
248
|
|
|
104
249
|
let cursor = 0;
|
|
105
|
-
const selected = new Set();
|
|
250
|
+
const selected = new Set(installedItems);
|
|
106
251
|
const expanded = new Set();
|
|
107
252
|
|
|
108
253
|
const render = () => {
|
|
109
254
|
// Clear screen and move to top
|
|
110
255
|
process.stdout.write('\x1B[2J\x1B[H');
|
|
111
256
|
|
|
112
|
-
console.log(
|
|
257
|
+
console.log(`\n${title}`);
|
|
113
258
|
console.log('─'.repeat(60));
|
|
114
259
|
console.log('↑↓ navigate SPACE toggle → expand ← collapse A all ENTER confirm\n');
|
|
115
|
-
console.log('Select
|
|
260
|
+
console.log('Select items to install:\n');
|
|
116
261
|
|
|
117
|
-
|
|
118
|
-
const isSelected = selected.has(
|
|
262
|
+
items.forEach((item, i) => {
|
|
263
|
+
const isSelected = selected.has(item.id);
|
|
119
264
|
const isCursor = i === cursor;
|
|
120
|
-
const isExpanded = expanded.has(
|
|
265
|
+
const isExpanded = expanded.has(item.id);
|
|
121
266
|
|
|
122
267
|
const checkbox = isSelected ? '◉' : '○';
|
|
123
268
|
const pointer = isCursor ? '❯' : ' ';
|
|
@@ -127,23 +272,23 @@ export async function promptSkillSelection(skills) {
|
|
|
127
272
|
const highlight = isCursor ? '\x1B[36m' : ''; // Cyan for selected
|
|
128
273
|
const reset = '\x1B[0m';
|
|
129
274
|
|
|
130
|
-
const shortDesc = getFirstSentence(
|
|
275
|
+
const shortDesc = getFirstSentence(item.description);
|
|
131
276
|
|
|
132
277
|
if (isExpanded) {
|
|
133
|
-
console.log(`${highlight}${pointer} ${checkbox} ${
|
|
278
|
+
console.log(`${highlight}${pointer} ${checkbox} ${item.name}${reset}`);
|
|
134
279
|
// Show full description indented
|
|
135
|
-
const fullDesc =
|
|
280
|
+
const fullDesc = item.description || 'No description available';
|
|
136
281
|
const lines = fullDesc.match(/.{1,55}/g) || [fullDesc];
|
|
137
282
|
lines.forEach(line => {
|
|
138
283
|
console.log(` ${highlight}${line}${reset}`);
|
|
139
284
|
});
|
|
140
285
|
} else {
|
|
141
|
-
console.log(`${highlight}${pointer} ${checkbox} ${
|
|
286
|
+
console.log(`${highlight}${pointer} ${checkbox} ${item.name} ${expandIcon} ${shortDesc}${reset}`);
|
|
142
287
|
}
|
|
143
288
|
});
|
|
144
289
|
|
|
145
290
|
const selectedCount = selected.size;
|
|
146
|
-
console.log(`\n${selectedCount}
|
|
291
|
+
console.log(`\n${selectedCount} item${selectedCount !== 1 ? 's' : ''} selected`);
|
|
147
292
|
};
|
|
148
293
|
|
|
149
294
|
const cleanup = () => {
|
|
@@ -165,51 +310,51 @@ export async function promptSkillSelection(skills) {
|
|
|
165
310
|
|
|
166
311
|
switch (key.name) {
|
|
167
312
|
case 'up':
|
|
168
|
-
cursor = cursor > 0 ? cursor - 1 :
|
|
313
|
+
cursor = cursor > 0 ? cursor - 1 : items.length - 1;
|
|
169
314
|
render();
|
|
170
315
|
break;
|
|
171
316
|
case 'down':
|
|
172
|
-
cursor = cursor <
|
|
317
|
+
cursor = cursor < items.length - 1 ? cursor + 1 : 0;
|
|
173
318
|
render();
|
|
174
319
|
break;
|
|
175
320
|
case 'right':
|
|
176
|
-
expanded.add(
|
|
321
|
+
expanded.add(items[cursor].id);
|
|
177
322
|
render();
|
|
178
323
|
break;
|
|
179
324
|
case 'left':
|
|
180
|
-
expanded.delete(
|
|
325
|
+
expanded.delete(items[cursor].id);
|
|
181
326
|
render();
|
|
182
327
|
break;
|
|
183
328
|
case 'space':
|
|
184
|
-
const
|
|
185
|
-
if (selected.has(
|
|
186
|
-
selected.delete(
|
|
329
|
+
const itemId = items[cursor].id;
|
|
330
|
+
if (selected.has(itemId)) {
|
|
331
|
+
selected.delete(itemId);
|
|
187
332
|
} else {
|
|
188
|
-
selected.add(
|
|
333
|
+
selected.add(itemId);
|
|
189
334
|
}
|
|
190
335
|
render();
|
|
191
336
|
break;
|
|
192
337
|
case 'a':
|
|
193
338
|
// Toggle all
|
|
194
|
-
if (selected.size ===
|
|
339
|
+
if (selected.size === items.length) {
|
|
195
340
|
selected.clear();
|
|
196
341
|
} else {
|
|
197
|
-
|
|
342
|
+
items.forEach(item => selected.add(item.id));
|
|
198
343
|
}
|
|
199
344
|
render();
|
|
200
345
|
break;
|
|
201
346
|
case 'return':
|
|
202
347
|
if (selected.size === 0) {
|
|
203
348
|
// Show error inline
|
|
204
|
-
process.stdout.write('\x1B[31mPlease select at least one
|
|
349
|
+
process.stdout.write('\x1B[31mPlease select at least one item\x1B[0m');
|
|
205
350
|
setTimeout(render, 1000);
|
|
206
351
|
} else {
|
|
207
352
|
cleanup();
|
|
208
353
|
// Clear and show final selection
|
|
209
354
|
process.stdout.write('\x1B[2J\x1B[H');
|
|
210
|
-
console.log('
|
|
211
|
-
|
|
212
|
-
console.log(` ✓ ${
|
|
355
|
+
console.log(`\n${title.split(' ')[0]} Selected:`);
|
|
356
|
+
items.filter(item => selected.has(item.id)).forEach(item => {
|
|
357
|
+
console.log(` ✓ ${item.name}`);
|
|
213
358
|
});
|
|
214
359
|
console.log('');
|
|
215
360
|
resolve(Array.from(selected));
|
|
@@ -267,6 +412,22 @@ export function showSuccess(installPath, installedSkills) {
|
|
|
267
412
|
console.log('═'.repeat(50) + '\n');
|
|
268
413
|
}
|
|
269
414
|
|
|
415
|
+
/**
|
|
416
|
+
* Display success message for subagent installation
|
|
417
|
+
* @param {string} installPath - Where subagents were installed
|
|
418
|
+
* @param {string[]} installedAgents - List of installed subagent names
|
|
419
|
+
*/
|
|
420
|
+
export function showSubagentSuccess(installPath, installedAgents) {
|
|
421
|
+
console.log('\n' + '═'.repeat(50));
|
|
422
|
+
console.log('✅ Subagents installed successfully!');
|
|
423
|
+
console.log('═'.repeat(50));
|
|
424
|
+
console.log(`\n📁 Location: ${installPath}`);
|
|
425
|
+
console.log(`\n🤖 Installed subagents (${installedAgents.length}):`);
|
|
426
|
+
installedAgents.forEach(agent => console.log(` • ${agent}`));
|
|
427
|
+
console.log('\n🚀 Your AI agent will automatically discover these subagents.');
|
|
428
|
+
console.log('═'.repeat(50) + '\n');
|
|
429
|
+
}
|
|
430
|
+
|
|
270
431
|
/**
|
|
271
432
|
* Display error message
|
|
272
433
|
* @param {string} message - Error message
|
package/lib/subagents.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch and parse available subagents from the GitHub repository
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const SUBAGENTS_REPO_OWNER = 'supercorks';
|
|
6
|
+
const SUBAGENTS_REPO_NAME = 'subagents';
|
|
7
|
+
const GITHUB_API = 'https://api.github.com';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fetch the list of subagent files from the repository
|
|
11
|
+
* Subagents are .agent.md files at the repo root
|
|
12
|
+
* @returns {Promise<Array<{name: string, description: string, filename: string}>>}
|
|
13
|
+
*/
|
|
14
|
+
export async function fetchAvailableSubagents() {
|
|
15
|
+
const repoUrl = `${GITHUB_API}/repos/${SUBAGENTS_REPO_OWNER}/${SUBAGENTS_REPO_NAME}/contents`;
|
|
16
|
+
|
|
17
|
+
const response = await fetch(repoUrl, {
|
|
18
|
+
headers: {
|
|
19
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
20
|
+
'User-Agent': '@supercorks/skills-installer'
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
throw new Error(`Failed to fetch subagents list: ${response.status} ${response.statusText}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const contents = await response.json();
|
|
29
|
+
|
|
30
|
+
// Filter to .agent.md files only
|
|
31
|
+
const agentFiles = contents.filter(
|
|
32
|
+
item => item.type === 'file' && item.name.endsWith('.agent.md')
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Fetch metadata for each agent file
|
|
36
|
+
const agentChecks = await Promise.all(
|
|
37
|
+
agentFiles.map(async (file) => {
|
|
38
|
+
const metadata = await fetchSubagentMetadata(file.name);
|
|
39
|
+
return {
|
|
40
|
+
filename: file.name,
|
|
41
|
+
name: metadata.name || file.name.replace('.agent.md', ''),
|
|
42
|
+
description: metadata.description || 'No description available'
|
|
43
|
+
};
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return agentChecks;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Fetch and parse frontmatter from a subagent file
|
|
52
|
+
* @param {string} filename - The agent filename
|
|
53
|
+
* @returns {Promise<{name: string, description: string}>}
|
|
54
|
+
*/
|
|
55
|
+
async function fetchSubagentMetadata(filename) {
|
|
56
|
+
const fileUrl = `${GITHUB_API}/repos/${SUBAGENTS_REPO_OWNER}/${SUBAGENTS_REPO_NAME}/contents/${filename}`;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(fileUrl, {
|
|
60
|
+
headers: {
|
|
61
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
62
|
+
'User-Agent': '@supercorks/skills-installer'
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
return { name: '', description: '' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const data = await response.json();
|
|
71
|
+
const content = Buffer.from(data.content, 'base64').toString('utf-8');
|
|
72
|
+
|
|
73
|
+
return parseSubagentFrontmatter(content);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return { name: '', description: '' };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Parse the subagent frontmatter to extract name and description
|
|
81
|
+
* Supports both standard YAML frontmatter and chatagent format
|
|
82
|
+
* @param {string} content - The .agent.md file content
|
|
83
|
+
* @returns {{name: string, description: string}}
|
|
84
|
+
*/
|
|
85
|
+
function parseSubagentFrontmatter(content) {
|
|
86
|
+
// Try to match standard --- frontmatter
|
|
87
|
+
const standardMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
88
|
+
|
|
89
|
+
// Try to match ```chatagent fenced frontmatter
|
|
90
|
+
const chatAgentMatch = content.match(/```chatagent\s*\n---\s*\n([\s\S]*?)\n---/);
|
|
91
|
+
|
|
92
|
+
const frontmatterContent = standardMatch?.[1] || chatAgentMatch?.[1];
|
|
93
|
+
|
|
94
|
+
if (!frontmatterContent) {
|
|
95
|
+
return { name: '', description: '' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const nameMatch = frontmatterContent.match(/name:\s*['"]?([^'"\n]+)['"]?/);
|
|
99
|
+
const descMatch = frontmatterContent.match(/description:\s*['"]?([^'"\n]+)['"]?/);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
name: nameMatch?.[1]?.trim() || '',
|
|
103
|
+
description: descMatch?.[1]?.trim() || ''
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get the subagents repository clone URL
|
|
109
|
+
* @returns {string}
|
|
110
|
+
*/
|
|
111
|
+
export function getSubagentsRepoUrl() {
|
|
112
|
+
return `https://github.com/${SUBAGENTS_REPO_OWNER}/${SUBAGENTS_REPO_NAME}.git`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export { SUBAGENTS_REPO_OWNER, SUBAGENTS_REPO_NAME };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supercorks/skills-installer",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Interactive CLI installer for AI agent skills",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Interactive CLI installer for AI agent skills and subagents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"skills-installer": "./bin/install.js"
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"lib/"
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
|
-
"start": "node bin/install.js install"
|
|
14
|
+
"start": "node bin/install.js install",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest"
|
|
15
17
|
},
|
|
16
18
|
"repository": {
|
|
17
19
|
"type": "git",
|
|
@@ -28,6 +30,7 @@
|
|
|
28
30
|
"ai",
|
|
29
31
|
"agent",
|
|
30
32
|
"skills",
|
|
33
|
+
"subagents",
|
|
31
34
|
"copilot",
|
|
32
35
|
"claude",
|
|
33
36
|
"installer",
|
|
@@ -38,6 +41,9 @@
|
|
|
38
41
|
"dependencies": {
|
|
39
42
|
"inquirer": "^9.2.12"
|
|
40
43
|
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"vitest": "^2.1.0"
|
|
46
|
+
},
|
|
41
47
|
"engines": {
|
|
42
48
|
"node": ">=18.0.0"
|
|
43
49
|
}
|