@supercorks/skills-installer 1.7.0 → 1.9.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @supercorks/skills-installer
2
2
 
3
- Interactive CLI installer for AI agent skills. Selectively install skills for GitHub Copilot, Claude, and other AI assistants using Git sparse-checkout.
3
+ Interactive CLI installer for AI agent skills and subagents. Selectively install resources for GitHub Copilot, Codex, Claude, and other AI assistants using Git sparse-checkout.
4
4
 
5
5
  ## Usage
6
6
 
@@ -16,21 +16,32 @@ npx @supercorks/skills-installer install
16
16
 
17
17
  ## What it does
18
18
 
19
- 1. **Choose installation path(s)** - Select one or more locations where skills should be installed:
19
+ 1. **Choose installation type** - Install skills, subagents, or both.
20
+
21
+ 2. **Choose installation path(s)** - Select one or more locations where resources should be installed:
20
22
  - `.github/skills/` (Copilot)
21
23
  - `~/.codex/skills/` (Codex)
22
24
  - `.claude/skills/` (Claude)
25
+ - `.github/agents/` (Copilot)
26
+ - `.agents/agents/` (Codex)
27
+ - `.claude/agents/` (Claude)
23
28
  - Custom path of your choice
24
29
 
25
- 2. **Gitignore option** - Optionally add the installation path to `.gitignore`
30
+ 3. **Gitignore option** - Optionally add the installation path to `.gitignore`
26
31
 
27
- 3. **Select skills** - Interactive checkbox to pick which skills to install:
32
+ 4. **Select skills/subagents** - Interactive checkbox to pick what to install:
28
33
  - Use `↑`/`↓` to navigate
29
34
  - Use `SPACE` to toggle selection
35
+ - Use `→` to expand and lazy-load descriptions
30
36
  - Use `A` to toggle all
31
37
  - Press `ENTER` to confirm
32
38
 
33
- 4. **Sparse clone** - Only downloads the selected skills using Git sparse-checkout, keeping the download minimal while preserving full git functionality.
39
+ 5. **Sparse clone** - Only downloads selected skills/subagents using Git sparse-checkout, keeping the download minimal while preserving full git functionality.
40
+
41
+ ## Installed repositories
42
+
43
+ - Skills repo: [https://github.com/supercorks/agent-skills](https://github.com/supercorks/agent-skills)
44
+ - Subagents repo: [https://github.com/supercorks/subagents](https://github.com/supercorks/subagents)
34
45
 
35
46
  ## Features
36
47
 
package/bin/install.js CHANGED
@@ -22,8 +22,8 @@ import {
22
22
  showSubagentSuccess,
23
23
  showError
24
24
  } from '../lib/prompts.js';
25
- import { fetchAvailableSkills } from '../lib/skills.js';
26
- import { fetchAvailableSubagents } from '../lib/subagents.js';
25
+ import { fetchAvailableSkills, fetchSkillMetadata } from '../lib/skills.js';
26
+ import { fetchAvailableSubagents, fetchSubagentMetadata } from '../lib/subagents.js';
27
27
  import {
28
28
  sparseCloneSkills,
29
29
  isGitAvailable,
@@ -279,7 +279,12 @@ async function runSkillsInstallForTarget(skills, existingInstalls, target) {
279
279
  }
280
280
 
281
281
  // Select skills (pre-select installed skills in manage mode)
282
- const selectedSkills = await promptSkillSelection(skills, installedSkills, skillsNeedingUpdate);
282
+ const selectedSkills = await promptSkillSelection(
283
+ skills,
284
+ installedSkills,
285
+ skillsNeedingUpdate,
286
+ (skillFolder) => fetchSkillMetadata(skillFolder)
287
+ );
283
288
 
284
289
  // Perform installation or update
285
290
  console.log('');
@@ -426,7 +431,12 @@ async function runSubagentsInstallForTarget(subagents, existingInstalls, target)
426
431
  }
427
432
 
428
433
  // Select subagents (pre-select installed ones in manage mode)
429
- const selectedAgents = await promptSubagentSelection(subagents, installedAgents, subagentsNeedingUpdate);
434
+ const selectedAgents = await promptSubagentSelection(
435
+ subagents,
436
+ installedAgents,
437
+ subagentsNeedingUpdate,
438
+ (filename) => fetchSubagentMetadata(filename)
439
+ );
430
440
 
431
441
  // Perform installation or update
432
442
  console.log('');
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Helpers for authenticated GitHub API requests.
3
+ * Uses env tokens first, then falls back to `gh auth token` when available.
4
+ */
5
+
6
+ import { execFileSync } from 'child_process';
7
+
8
+ let cachedToken = '';
9
+ let tokenResolved = false;
10
+
11
+ function normalizeToken(raw) {
12
+ if (!raw || typeof raw !== 'string') return '';
13
+ return raw.trim();
14
+ }
15
+
16
+ function readTokenFromGhCli() {
17
+ try {
18
+ const token = execFileSync('gh', ['auth', 'token'], {
19
+ encoding: 'utf-8',
20
+ stdio: ['ignore', 'pipe', 'ignore']
21
+ });
22
+ return normalizeToken(token);
23
+ } catch {
24
+ return '';
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Returns a GitHub token if available.
30
+ * Resolution order: GITHUB_TOKEN -> GH_TOKEN -> gh auth token.
31
+ * Value is cached for process lifetime.
32
+ * @returns {string}
33
+ */
34
+ export function getGitHubAuthToken() {
35
+ if (tokenResolved) return cachedToken;
36
+
37
+ tokenResolved = true;
38
+ cachedToken =
39
+ normalizeToken(process.env.GITHUB_TOKEN) ||
40
+ normalizeToken(process.env.GH_TOKEN) ||
41
+ readTokenFromGhCli() ||
42
+ '';
43
+
44
+ return cachedToken;
45
+ }
46
+
47
+ /**
48
+ * Build GitHub API headers with optional auth.
49
+ * @param {string} [userAgent='@supercorks/skills-installer']
50
+ * @returns {Record<string, string>}
51
+ */
52
+ export function getGitHubHeaders(userAgent = '@supercorks/skills-installer') {
53
+ const headers = {
54
+ Accept: 'application/vnd.github.v3+json',
55
+ 'User-Agent': userAgent
56
+ };
57
+
58
+ const token = getGitHubAuthToken();
59
+ if (token) {
60
+ headers.Authorization = `Bearer ${token}`;
61
+ }
62
+
63
+ return headers;
64
+ }
65
+
66
+ /**
67
+ * Test-only cache reset helper.
68
+ */
69
+ export function __resetGitHubAuthCacheForTests() {
70
+ cachedToken = '';
71
+ tokenResolved = false;
72
+ }
package/lib/prompts.js CHANGED
@@ -247,14 +247,16 @@ export async function promptGitignore(installPath) {
247
247
  * @param {Array<{name: string, description: string, folder: string}>} skills - Available skills
248
248
  * @param {string[]} installedSkills - Already installed skill folder names (will be pre-selected)
249
249
  * @param {Set<string>} skillsNeedingUpdate - Skill folder names that have updates available
250
+ * @param {(skillFolder: string) => Promise<{name?: string, description?: string}>} metadataLoader - Lazy metadata loader
250
251
  * @returns {Promise<string[]>} Selected skill folder names
251
252
  */
252
- export async function promptSkillSelection(skills, installedSkills = [], skillsNeedingUpdate = new Set()) {
253
+ export async function promptSkillSelection(skills, installedSkills = [], skillsNeedingUpdate = new Set(), metadataLoader = null) {
253
254
  return promptItemSelection(
254
255
  skills.map(s => ({ id: s.folder, name: s.name, description: s.description })),
255
256
  installedSkills,
256
257
  '📦 Available Skills',
257
- skillsNeedingUpdate
258
+ skillsNeedingUpdate,
259
+ metadataLoader
258
260
  );
259
261
  }
260
262
 
@@ -263,14 +265,16 @@ export async function promptSkillSelection(skills, installedSkills = [], skillsN
263
265
  * @param {Array<{name: string, description: string, filename: string}>} subagents - Available subagents
264
266
  * @param {string[]} installedSubagents - Already installed subagent filenames (will be pre-selected)
265
267
  * @param {Set<string>} subagentsNeedingUpdate - Subagent filenames that have updates available
268
+ * @param {(agentFilename: string) => Promise<{name?: string, description?: string}>} metadataLoader - Lazy metadata loader
266
269
  * @returns {Promise<string[]>} Selected subagent filenames
267
270
  */
268
- export async function promptSubagentSelection(subagents, installedSubagents = [], subagentsNeedingUpdate = new Set()) {
271
+ export async function promptSubagentSelection(subagents, installedSubagents = [], subagentsNeedingUpdate = new Set(), metadataLoader = null) {
269
272
  return promptItemSelection(
270
273
  subagents.map(s => ({ id: s.filename, name: s.name, description: s.description })),
271
274
  installedSubagents,
272
275
  '🤖 Available Subagents',
273
- subagentsNeedingUpdate
276
+ subagentsNeedingUpdate,
277
+ metadataLoader
274
278
  );
275
279
  }
276
280
 
@@ -280,9 +284,10 @@ export async function promptSubagentSelection(subagents, installedSubagents = []
280
284
  * @param {string[]} installedItems - Already installed item IDs (will be pre-selected)
281
285
  * @param {string} title - Title to display
282
286
  * @param {Set<string>} itemsNeedingUpdate - Item IDs that have updates available
287
+ * @param {(itemId: string) => Promise<{name?: string, description?: string}>} metadataLoader - Lazy metadata loader
283
288
  * @returns {Promise<string[]>} Selected item IDs
284
289
  */
285
- function promptItemSelection(items, installedItems = [], title = '📦 Available Items', itemsNeedingUpdate = new Set()) {
290
+ function promptItemSelection(items, installedItems = [], title = '📦 Available Items', itemsNeedingUpdate = new Set(), metadataLoader = null) {
286
291
  return new Promise((resolve, reject) => {
287
292
  const rl = readline.createInterface({
288
293
  input: process.stdin,
@@ -301,6 +306,8 @@ function promptItemSelection(items, installedItems = [], title = '📦 Available
301
306
  ? new Set(installedItems)
302
307
  : new Set(items.map(item => item.id));
303
308
  const expanded = new Set();
309
+ const metadataCache = new Set();
310
+ const metadataLoading = new Set();
304
311
 
305
312
  const render = () => {
306
313
  // Clear screen and move to top
@@ -308,7 +315,7 @@ function promptItemSelection(items, installedItems = [], title = '📦 Available
308
315
 
309
316
  console.log(`\n${title}`);
310
317
  console.log('─'.repeat(60));
311
- console.log('↑↓ navigate SPACE toggle → expand ← collapse A all ENTER confirm\n');
318
+ console.log('↑↓ navigate SPACE toggle → expand/load ← collapse A all ENTER confirm\n');
312
319
  console.log('Select items to install:\n');
313
320
 
314
321
  items.forEach((item, i) => {
@@ -331,7 +338,9 @@ function promptItemSelection(items, installedItems = [], title = '📦 Available
331
338
  if (isExpanded) {
332
339
  console.log(`${highlight}${pointer} ${checkbox} ${item.name}${reset}${updateFlag}`);
333
340
  // Show full description indented
334
- const fullDesc = item.description || 'No description available';
341
+ const fullDesc = metadataLoading.has(item.id)
342
+ ? 'Loading description...'
343
+ : (item.description || 'No description available');
335
344
  const lines = fullDesc.match(/.{1,55}/g) || [fullDesc];
336
345
  lines.forEach(line => {
337
346
  console.log(` ${highlight}${line}${reset}`);
@@ -374,7 +383,33 @@ function promptItemSelection(items, installedItems = [], title = '📦 Available
374
383
  render();
375
384
  break;
376
385
  case 'right':
377
- expanded.add(items[cursor].id);
386
+ const currentItem = items[cursor];
387
+ expanded.add(currentItem.id);
388
+ if (metadataLoader && !metadataCache.has(currentItem.id) && !metadataLoading.has(currentItem.id)) {
389
+ metadataLoading.add(currentItem.id);
390
+ render();
391
+ metadataLoader(currentItem.id)
392
+ .then((metadata) => {
393
+ if (metadata?.name) {
394
+ currentItem.name = metadata.name;
395
+ }
396
+ if (metadata?.description) {
397
+ currentItem.description = metadata.description;
398
+ } else if (!currentItem.description || currentItem.description === 'Press right arrow to load description') {
399
+ currentItem.description = 'Description unavailable';
400
+ }
401
+ })
402
+ .catch(() => {
403
+ if (!currentItem.description || currentItem.description === 'Press right arrow to load description') {
404
+ currentItem.description = 'Description unavailable (metadata fetch failed)';
405
+ }
406
+ })
407
+ .finally(() => {
408
+ metadataLoading.delete(currentItem.id);
409
+ metadataCache.add(currentItem.id);
410
+ render();
411
+ });
412
+ }
378
413
  render();
379
414
  break;
380
415
  case 'left':
package/lib/skills.js CHANGED
@@ -2,6 +2,8 @@
2
2
  * Fetch and parse available skills from the GitHub repository
3
3
  */
4
4
 
5
+ import { getGitHubHeaders } from './github-auth.js';
6
+
5
7
  const REPO_OWNER = 'supercorks';
6
8
  const REPO_NAME = 'agent-skills';
7
9
  const GITHUB_API = 'https://api.github.com';
@@ -9,6 +11,14 @@ const GITHUB_API = 'https://api.github.com';
9
11
  // Folders to exclude from skill detection (not actual skills)
10
12
  const EXCLUDED_FOLDERS = ['.github', '.claude', 'node_modules'];
11
13
 
14
+ function humanizeSkillName(folder) {
15
+ return folder
16
+ .split(/[-_]/g)
17
+ .filter(Boolean)
18
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
19
+ .join(' ');
20
+ }
21
+
12
22
  /**
13
23
  * Fetch the list of skill directories from the repository
14
24
  * Skills are at the repo root level, each folder with a SKILL.md is a skill
@@ -18,10 +28,7 @@ export async function fetchAvailableSkills() {
18
28
  const repoUrl = `${GITHUB_API}/repos/${REPO_OWNER}/${REPO_NAME}/contents`;
19
29
 
20
30
  const response = await fetch(repoUrl, {
21
- headers: {
22
- 'Accept': 'application/vnd.github.v3+json',
23
- 'User-Agent': '@supercorks/skills-installer'
24
- }
31
+ headers: getGitHubHeaders()
25
32
  });
26
33
 
27
34
  if (!response.ok) {
@@ -35,23 +42,12 @@ export async function fetchAvailableSkills() {
35
42
  item => item.type === 'dir' && !EXCLUDED_FOLDERS.includes(item.name) && !item.name.startsWith('.')
36
43
  );
37
44
 
38
- // Check each directory for SKILL.md to confirm it's a skill
39
- const skillChecks = await Promise.all(
40
- potentialSkillDirs.map(async (dir) => {
41
- const metadata = await fetchSkillMetadata(dir.name);
42
- // Only include if we found valid metadata (has a SKILL.md)
43
- if (metadata.name || metadata.description) {
44
- return {
45
- folder: dir.name,
46
- name: metadata.name || dir.name,
47
- description: metadata.description || 'No description available'
48
- };
49
- }
50
- return null;
51
- })
52
- );
53
-
54
- return skillChecks.filter(Boolean);
45
+ // Keep listing lightweight: metadata is loaded lazily on expand in the UI
46
+ return potentialSkillDirs.map(dir => ({
47
+ folder: dir.name,
48
+ name: humanizeSkillName(dir.name),
49
+ description: 'Press right arrow to load description'
50
+ }));
55
51
  }
56
52
 
57
53
  /**
@@ -59,19 +55,16 @@ export async function fetchAvailableSkills() {
59
55
  * @param {string} skillFolder - The skill folder name
60
56
  * @returns {Promise<{name: string, description: string}>}
61
57
  */
62
- async function fetchSkillMetadata(skillFolder) {
58
+ export async function fetchSkillMetadata(skillFolder) {
63
59
  const skillMdUrl = `${GITHUB_API}/repos/${REPO_OWNER}/${REPO_NAME}/contents/${skillFolder}/SKILL.md`;
64
60
 
65
61
  try {
66
62
  const response = await fetch(skillMdUrl, {
67
- headers: {
68
- 'Accept': 'application/vnd.github.v3+json',
69
- 'User-Agent': '@supercorks/skills-installer'
70
- }
63
+ headers: getGitHubHeaders()
71
64
  });
72
65
 
73
66
  if (!response.ok) {
74
- return { name: '', description: '' };
67
+ throw new Error(`Failed to fetch skill metadata: ${response.status} ${response.statusText}`);
75
68
  }
76
69
 
77
70
  const data = await response.json();
@@ -79,7 +72,7 @@ async function fetchSkillMetadata(skillFolder) {
79
72
 
80
73
  return parseSkillFrontmatter(content);
81
74
  } catch (error) {
82
- return { name: '', description: '' };
75
+ throw error;
83
76
  }
84
77
  }
85
78
 
package/lib/subagents.js CHANGED
@@ -2,10 +2,21 @@
2
2
  * Fetch and parse available subagents from the GitHub repository
3
3
  */
4
4
 
5
+ import { getGitHubHeaders } from './github-auth.js';
6
+
5
7
  const SUBAGENTS_REPO_OWNER = 'supercorks';
6
8
  const SUBAGENTS_REPO_NAME = 'subagents';
7
9
  const GITHUB_API = 'https://api.github.com';
8
10
 
11
+ function humanizeAgentName(filename) {
12
+ const base = filename.replace('.agent.md', '');
13
+ return base
14
+ .split(/[-_]/g)
15
+ .filter(Boolean)
16
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
17
+ .join(' ');
18
+ }
19
+
9
20
  /**
10
21
  * Fetch the list of subagent files from the repository
11
22
  * Subagents are .agent.md files at the repo root
@@ -15,10 +26,7 @@ export async function fetchAvailableSubagents() {
15
26
  const repoUrl = `${GITHUB_API}/repos/${SUBAGENTS_REPO_OWNER}/${SUBAGENTS_REPO_NAME}/contents`;
16
27
 
17
28
  const response = await fetch(repoUrl, {
18
- headers: {
19
- 'Accept': 'application/vnd.github.v3+json',
20
- 'User-Agent': '@supercorks/skills-installer'
21
- }
29
+ headers: getGitHubHeaders()
22
30
  });
23
31
 
24
32
  if (!response.ok) {
@@ -32,19 +40,12 @@ export async function fetchAvailableSubagents() {
32
40
  item => item.type === 'file' && item.name.endsWith('.agent.md')
33
41
  );
34
42
 
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;
43
+ // Keep listing lightweight: metadata is loaded lazily on expand in the UI
44
+ return agentFiles.map(file => ({
45
+ filename: file.name,
46
+ name: humanizeAgentName(file.name),
47
+ description: 'Press right arrow to load description'
48
+ }));
48
49
  }
49
50
 
50
51
  /**
@@ -52,19 +53,16 @@ export async function fetchAvailableSubagents() {
52
53
  * @param {string} filename - The agent filename
53
54
  * @returns {Promise<{name: string, description: string}>}
54
55
  */
55
- async function fetchSubagentMetadata(filename) {
56
+ export async function fetchSubagentMetadata(filename) {
56
57
  const fileUrl = `${GITHUB_API}/repos/${SUBAGENTS_REPO_OWNER}/${SUBAGENTS_REPO_NAME}/contents/${filename}`;
57
58
 
58
59
  try {
59
60
  const response = await fetch(fileUrl, {
60
- headers: {
61
- 'Accept': 'application/vnd.github.v3+json',
62
- 'User-Agent': '@supercorks/skills-installer'
63
- }
61
+ headers: getGitHubHeaders()
64
62
  });
65
63
 
66
64
  if (!response.ok) {
67
- return { name: '', description: '' };
65
+ throw new Error(`Failed to fetch subagent metadata: ${response.status} ${response.statusText}`);
68
66
  }
69
67
 
70
68
  const data = await response.json();
@@ -72,7 +70,7 @@ async function fetchSubagentMetadata(filename) {
72
70
 
73
71
  return parseSubagentFrontmatter(content);
74
72
  } catch (error) {
75
- return { name: '', description: '' };
73
+ throw error;
76
74
  }
77
75
  }
78
76
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supercorks/skills-installer",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "Interactive CLI installer for AI agent skills and subagents",
5
5
  "type": "module",
6
6
  "bin": {