@supercorks/skills-installer 1.11.0 → 1.12.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 and subagents. Selectively install resources for GitHub Copilot, Codex, 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 and Codex agent conversion where needed.
4
4
 
5
5
  ## Usage
6
6
 
@@ -18,16 +18,26 @@ npx @supercorks/skills-installer install
18
18
 
19
19
  1. **Choose installation type** - Install skills, subagents, or both.
20
20
 
21
- 2. **Choose installation path(s)** - Select one or more locations where resources should be installed:
22
- - `.github/skills/` (Copilot)
23
- - `~/.codex/skills/` (Codex)
24
- - `.claude/skills/` (Claude)
25
- - `.github/agents/` (Copilot)
26
- - `.agents/agents/` (Codex)
27
- - `.claude/agents/` (Claude)
21
+ 2. **Choose installation path(s)** - Select one or more locations where resources should be installed. The installer labels each option with harness, scope, and available count, for example `.github/skills/ (copilot | local | 24 skills)`.
22
+
23
+ Skills:
24
+ - `.github/skills/` (copilot | local)
25
+ - `~/.copilot/skills/` (copilot | global)
26
+ - `.agents/skills/` (codex | local)
27
+ - `~/.agents/skills/` (codex | global)
28
+ - `.claude/skills/` (claude | local)
29
+ - `~/.claude/skills/` (claude | global)
30
+
31
+ Agents:
32
+ - `.github/agents/` (copilot | local)
33
+ - `~/.copilot/agents/` (copilot | global)
34
+ - `.claude/agents/` (claude | local)
35
+ - `~/.claude/agents/` (claude | global)
36
+ - `.codex/agents/` (codex | local, installed as converted TOML agents)
37
+ - `~/.codex/agents/` (codex | global, installed as converted TOML agents)
28
38
  - Custom path of your choice
29
39
 
30
- 3. **Gitignore option** - Optionally add the installation path to `.gitignore`
40
+ 3. **Gitignore option** - If launched from inside a git repository, optionally add the installation path to `.gitignore`
31
41
 
32
42
  4. **Select skills/subagents** - Interactive checkbox to pick what to install:
33
43
  - Use `↑`/`↓` to navigate
@@ -36,7 +46,9 @@ npx @supercorks/skills-installer install
36
46
  - Use `A` to toggle all
37
47
  - Press `ENTER` to confirm
38
48
 
39
- 5. **Sparse clone** - Only downloads selected skills/subagents using Git sparse-checkout, keeping the download minimal while preserving full git functionality.
49
+ 5. **Install backend**
50
+ - Skills and Markdown-based agents use Git sparse-checkout for minimal download while preserving full git functionality.
51
+ - Codex agents are generated as TOML files from the source Markdown agent definitions.
40
52
 
41
53
  ## Installed repositories
42
54
 
@@ -48,6 +60,8 @@ npx @supercorks/skills-installer install
48
60
  - **Minimal download** - Uses `git clone --filter=blob:none` for efficient cloning
49
61
  - **Push capable** - The sparse clone preserves the full git history, allowing you to commit and push changes
50
62
  - **Auto-discovery** - Fetches the latest skill list from the repository
63
+ - **Global and local targets** - Offers documented project/user locations for Copilot, Codex, and Claude where the resource format is compatible
64
+ - **Codex agent conversion** - Converts Markdown subagents into Codex TOML custom agents for `.codex/agents/` targets
51
65
  - **Recursive directory creation** - Custom paths are created automatically
52
66
 
53
67
  ## Requirements
package/bin/install.js CHANGED
@@ -27,6 +27,7 @@ import { fetchAvailableSubagents, fetchSubagentMetadata } from '../lib/subagents
27
27
  import {
28
28
  sparseCloneSkills,
29
29
  isGitAvailable,
30
+ isInsideGitWorkTree,
30
31
  listCheckedOutSkills,
31
32
  updateSparseCheckout,
32
33
  sparseCloneSubagents,
@@ -36,13 +37,15 @@ import {
36
37
  checkSubagentsForUpdates
37
38
  } from '../lib/git.js';
38
39
  import { createRequire } from 'module';
40
+ import { allAgentDetectionTargets, allSkillDetectionTargets, getAgentInstallMode } from '../lib/install-targets.js';
41
+ import { checkCodexAgentUpdates, listInstalledCodexAgents, syncCodexAgents } from '../lib/codex-agents.js';
39
42
 
40
43
  const require = createRequire(import.meta.url);
41
44
  const { version: VERSION } = require('../package.json');
42
45
 
43
46
  // Common installation paths to check for existing installations
44
- const SKILL_PATHS = ['.github/skills/', '~/.codex/skills/', '.claude/skills/'];
45
- const AGENT_PATHS = ['.github/agents/', '.agents/agents/', '.claude/agents/'];
47
+ const SKILL_PATHS = allSkillDetectionTargets().map(target => target.path);
48
+ const AGENT_PATHS = allAgentDetectionTargets().map(target => target.path);
46
49
 
47
50
  function resolveInstallPath(path) {
48
51
  if (path === '~') return homedir();
@@ -91,6 +94,24 @@ async function detectExistingAgentInstallations() {
91
94
 
92
95
  for (const path of AGENT_PATHS) {
93
96
  const absolutePath = resolveInstallPath(path);
97
+ const installMode = getAgentInstallMode(path);
98
+
99
+ if (installMode === 'codex-toml') {
100
+ try {
101
+ const agents = await listInstalledCodexAgents(absolutePath);
102
+ if (agents.length > 0) {
103
+ installations.push({
104
+ path,
105
+ agentCount: agents.length,
106
+ agents
107
+ });
108
+ }
109
+ } catch {
110
+ // Ignore errors reading existing installations
111
+ }
112
+ continue;
113
+ }
114
+
94
115
  const gitDir = join(absolutePath, '.git');
95
116
 
96
117
  if (existsSync(gitDir)) {
@@ -219,7 +240,7 @@ async function runSkillsInstall() {
219
240
  const existingInstalls = await detectExistingSkillInstallations();
220
241
 
221
242
  // Ask where to install (showing existing installations if any)
222
- const installTargets = await promptInstallPath(existingInstalls);
243
+ const installTargets = await promptInstallPath(existingInstalls, skills.length);
223
244
 
224
245
  for (let i = 0; i < installTargets.length; i++) {
225
246
  const target = installTargets[i];
@@ -279,8 +300,14 @@ async function runSkillsInstallForTarget(skills, existingInstalls, target) {
279
300
 
280
301
  // Ask about .gitignore (only for fresh installs and if not already in .gitignore)
281
302
  let shouldGitignore = false;
303
+ const isInGitWorkTree = isInsideGitWorkTree();
282
304
  const gitignorePath = resolveInstallPath('.gitignore');
283
- if (!isManageMode && !isHomePath(installPath) && !isInGitignore(gitignorePath, installPath)) {
305
+ if (
306
+ isInGitWorkTree &&
307
+ !isManageMode &&
308
+ !isHomePath(installPath) &&
309
+ !isInGitignore(gitignorePath, installPath)
310
+ ) {
284
311
  shouldGitignore = await promptGitignore(installPath);
285
312
  }
286
313
 
@@ -373,7 +400,7 @@ async function runSubagentsInstall() {
373
400
  const existingInstalls = await detectExistingAgentInstallations();
374
401
 
375
402
  // Ask where to install (showing existing installations if any)
376
- const installTargets = await promptAgentInstallPath(existingInstalls);
403
+ const installTargets = await promptAgentInstallPath(existingInstalls, subagents.length);
377
404
 
378
405
  for (let i = 0; i < installTargets.length; i++) {
379
406
  const target = installTargets[i];
@@ -393,15 +420,16 @@ async function runSubagentsInstall() {
393
420
  async function runSubagentsInstallForTarget(subagents, existingInstalls, target) {
394
421
  const { path: installPath, isExisting } = target;
395
422
  const absoluteInstallPath = resolveInstallPath(installPath);
423
+ const installMode = getAgentInstallMode(installPath);
396
424
  const gitDir = join(absoluteInstallPath, '.git');
397
- const hasExistingRepo = existsSync(gitDir);
425
+ const hasExistingRepo = installMode === 'sparse-git' && existsSync(gitDir);
398
426
 
399
427
  // Get currently installed subagents if managing existing installation
400
428
  let installedAgents = [];
401
429
  if (isExisting) {
402
430
  const existingInstall = existingInstalls.find(i => i.path === installPath);
403
431
  installedAgents = existingInstall?.agents || [];
404
- } else {
432
+ } else if (installMode === 'sparse-git') {
405
433
  // Check if manually entered path has an existing installation
406
434
  if (hasExistingRepo) {
407
435
  try {
@@ -411,16 +439,27 @@ async function runSubagentsInstallForTarget(subagents, existingInstalls, target)
411
439
  // Prompt will default to selecting all subagents.
412
440
  }
413
441
  }
442
+ } else {
443
+ try {
444
+ installedAgents = await listInstalledCodexAgents(absoluteInstallPath);
445
+ } catch {
446
+ // Ignore detection failures for custom Codex agent paths.
447
+ }
414
448
  }
415
449
 
416
- const isManageMode = isExisting || hasExistingRepo || installedAgents.length > 0;
450
+ const isManageMode = installMode === 'sparse-git'
451
+ ? (isExisting || hasExistingRepo || installedAgents.length > 0)
452
+ : installedAgents.length > 0;
417
453
 
418
454
  // Check for updates if in manage mode
419
455
  let subagentsNeedingUpdate = new Set();
420
456
  if (isManageMode) {
421
457
  const updateSpinner = showSpinner('Checking for available updates...');
422
458
  try {
423
- subagentsNeedingUpdate = await checkSubagentsForUpdates(absoluteInstallPath, installedAgents);
459
+ subagentsNeedingUpdate = installMode === 'sparse-git'
460
+ ? await checkSubagentsForUpdates(absoluteInstallPath, installedAgents)
461
+ : await checkCodexAgentUpdates(absoluteInstallPath, installedAgents);
462
+
424
463
  if (subagentsNeedingUpdate.size > 0) {
425
464
  updateSpinner.stop(`✅ Found ${subagentsNeedingUpdate.size} subagent${subagentsNeedingUpdate.size !== 1 ? 's' : ''} with updates available`);
426
465
  } else {
@@ -433,8 +472,14 @@ async function runSubagentsInstallForTarget(subagents, existingInstalls, target)
433
472
 
434
473
  // Ask about .gitignore (only for fresh installs and if not already in .gitignore)
435
474
  let shouldGitignore = false;
475
+ const isInGitWorkTree = isInsideGitWorkTree();
436
476
  const gitignorePath = resolveInstallPath('.gitignore');
437
- if (!isManageMode && !isHomePath(installPath) && !isInGitignore(gitignorePath, installPath)) {
477
+ if (
478
+ isInGitWorkTree &&
479
+ !isManageMode &&
480
+ !isHomePath(installPath) &&
481
+ !isInGitignore(gitignorePath, installPath)
482
+ ) {
438
483
  shouldGitignore = await promptGitignore(installPath);
439
484
  }
440
485
 
@@ -462,9 +507,15 @@ async function runSubagentsInstallForTarget(subagents, existingInstalls, target)
462
507
  const updateSpinner = showSpinner('Updating subagents installation...');
463
508
 
464
509
  try {
465
- await updateSubagentsSparseCheckout(absoluteInstallPath, selectedAgents, (message) => {
466
- updateSpinner.stop(` ${message}`);
467
- });
510
+ if (installMode === 'sparse-git') {
511
+ await updateSubagentsSparseCheckout(absoluteInstallPath, selectedAgents, (message) => {
512
+ updateSpinner.stop(` ${message}`);
513
+ });
514
+ } else {
515
+ await syncCodexAgents(absoluteInstallPath, selectedAgents, (message) => {
516
+ updateSpinner.stop(` ${message}`);
517
+ });
518
+ }
468
519
  } catch (error) {
469
520
  updateSpinner.stop('❌ Update failed');
470
521
  showError(error.message);
@@ -477,9 +528,15 @@ async function runSubagentsInstallForTarget(subagents, existingInstalls, target)
477
528
  const installSpinner = showSpinner('Installing selected subagents...');
478
529
 
479
530
  try {
480
- await sparseCloneSubagents(installPath, selectedAgents, (message) => {
481
- installSpinner.stop(` ${message}`);
482
- });
531
+ if (installMode === 'sparse-git') {
532
+ await sparseCloneSubagents(installPath, selectedAgents, (message) => {
533
+ installSpinner.stop(` ${message}`);
534
+ });
535
+ } else {
536
+ await syncCodexAgents(absoluteInstallPath, selectedAgents, (message) => {
537
+ installSpinner.stop(` ${message}`);
538
+ });
539
+ }
483
540
  } catch (error) {
484
541
  installSpinner.stop('❌ Installation failed');
485
542
  showError(error.message);
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Convert Markdown subagent definitions into Codex TOML custom agents.
3
+ */
4
+
5
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
6
+ import { join, resolve } from 'path';
7
+ import { homedir } from 'os';
8
+ import { fetchSubagentContent, humanizeAgentName, parseSubagentDefinition } from './subagents.js';
9
+
10
+ const GENERATED_COMMENT_PREFIX = '# Generated by @supercorks/skills-installer from ';
11
+
12
+ function resolvePath(path) {
13
+ if (path === '~') return homedir();
14
+ if (path.startsWith('~/')) return resolve(homedir(), path.slice(2));
15
+ return resolve(path);
16
+ }
17
+
18
+ export function codexTomlFilenameForAgent(agentFilename) {
19
+ return `${agentFilename.replace(/\.agent\.md$/i, '')}.toml`;
20
+ }
21
+
22
+ export function codexAgentNameForAgent(agentFilename, displayName = '') {
23
+ const preferred = displayName || agentFilename.replace(/\.agent\.md$/i, '');
24
+ return preferred
25
+ .trim()
26
+ .toLowerCase()
27
+ .replace(/[^a-z0-9]+/g, '_')
28
+ .replace(/^_+|_+$/g, '') || 'agent';
29
+ }
30
+
31
+ function escapeTomlBasicString(value) {
32
+ return String(value)
33
+ .replace(/\\/g, '\\\\')
34
+ .replace(/"/g, '\\"')
35
+ .replace(/\n/g, ' ')
36
+ .trim();
37
+ }
38
+
39
+ function formatDeveloperInstructions(body) {
40
+ const normalizedBody = (body || '').replace(/\r\n/g, '\n').trim();
41
+ if (!normalizedBody) {
42
+ return "developer_instructions = '''\nAct as the requested specialist and complete the assigned task.\n'''";
43
+ }
44
+
45
+ if (!normalizedBody.includes("'''")) {
46
+ return `developer_instructions = '''\n${normalizedBody}\n'''`;
47
+ }
48
+
49
+ const escaped = normalizedBody
50
+ .replace(/\\/g, '\\\\')
51
+ .replace(/"""/g, '\\"\\"\\"');
52
+
53
+ return `developer_instructions = """\n${escaped}\n"""`;
54
+ }
55
+
56
+ export function convertSubagentMarkdownToCodexToml(content, agentFilename) {
57
+ const definition = parseSubagentDefinition(content, agentFilename);
58
+ const outputFilename = codexTomlFilenameForAgent(agentFilename);
59
+ const codexName = codexAgentNameForAgent(agentFilename, definition.name);
60
+ const description = definition.description || `${humanizeAgentName(agentFilename)} custom agent`;
61
+
62
+ const toml = [
63
+ `${GENERATED_COMMENT_PREFIX}${agentFilename}`,
64
+ `name = "${escapeTomlBasicString(codexName)}"`,
65
+ `description = "${escapeTomlBasicString(description)}"`,
66
+ formatDeveloperInstructions(definition.body),
67
+ '',
68
+ ].join('\n');
69
+
70
+ return {
71
+ sourceFilename: agentFilename,
72
+ outputFilename,
73
+ name: definition.name,
74
+ description,
75
+ toml,
76
+ };
77
+ }
78
+
79
+ function parseGeneratedSourceFilename(tomlContent, outputFilename) {
80
+ const commentMatch = tomlContent.match(/^# Generated by @supercorks\/skills-installer from ([^\n]+)$/m);
81
+ if (commentMatch) {
82
+ return commentMatch[1].trim();
83
+ }
84
+
85
+ return null;
86
+ }
87
+
88
+ function listGeneratedCodexAgentEntries(targetPath) {
89
+ const absolutePath = resolvePath(targetPath);
90
+ if (!existsSync(absolutePath)) {
91
+ return [];
92
+ }
93
+
94
+ return readdirSync(absolutePath)
95
+ .filter(name => name.endsWith('.toml'))
96
+ .map(outputFilename => {
97
+ const absoluteFilePath = join(absolutePath, outputFilename);
98
+ const content = readFileSync(absoluteFilePath, 'utf8');
99
+ const sourceFilename = parseGeneratedSourceFilename(content, outputFilename);
100
+ if (!sourceFilename) {
101
+ return null;
102
+ }
103
+
104
+ return {
105
+ sourceFilename,
106
+ outputFilename,
107
+ absoluteFilePath,
108
+ content,
109
+ };
110
+ })
111
+ .filter(Boolean);
112
+ }
113
+
114
+ export async function listInstalledCodexAgents(targetPath) {
115
+ return listGeneratedCodexAgentEntries(targetPath).map(entry => entry.sourceFilename);
116
+ }
117
+
118
+ export async function checkCodexAgentUpdates(targetPath, agentFilenames) {
119
+ const installedEntries = listGeneratedCodexAgentEntries(targetPath);
120
+ const bySourceFilename = new Map(installedEntries.map(entry => [entry.sourceFilename, entry]));
121
+ const needsUpdate = new Set();
122
+
123
+ for (const agentFilename of agentFilenames) {
124
+ const entry = bySourceFilename.get(agentFilename);
125
+ if (!entry) {
126
+ continue;
127
+ }
128
+
129
+ try {
130
+ const content = await fetchSubagentContent(agentFilename);
131
+ const converted = convertSubagentMarkdownToCodexToml(content, agentFilename);
132
+ if (converted.toml !== entry.content) {
133
+ needsUpdate.add(agentFilename);
134
+ }
135
+ } catch {
136
+ // Skip update markers when the remote file cannot be fetched.
137
+ }
138
+ }
139
+
140
+ return needsUpdate;
141
+ }
142
+
143
+ export async function syncCodexAgents(targetPath, agentFilenames, onProgress = () => {}) {
144
+ const absolutePath = resolvePath(targetPath);
145
+ mkdirSync(absolutePath, { recursive: true });
146
+
147
+ const existingEntries = listGeneratedCodexAgentEntries(absolutePath);
148
+ const selectedSet = new Set(agentFilenames);
149
+
150
+ for (const entry of existingEntries) {
151
+ if (!selectedSet.has(entry.sourceFilename) && existsSync(entry.absoluteFilePath)) {
152
+ rmSync(entry.absoluteFilePath, { force: true });
153
+ }
154
+ }
155
+
156
+ for (let index = 0; index < agentFilenames.length; index += 1) {
157
+ const agentFilename = agentFilenames[index];
158
+ onProgress(`Converting ${index + 1}/${agentFilenames.length}: ${agentFilename}`);
159
+ const content = await fetchSubagentContent(agentFilename);
160
+ const converted = convertSubagentMarkdownToCodexToml(content, agentFilename);
161
+ writeFileSync(join(absolutePath, converted.outputFilename), converted.toml, 'utf8');
162
+ }
163
+
164
+ onProgress('Done!');
165
+ }
package/lib/git.js CHANGED
@@ -28,6 +28,24 @@ export function isGitAvailable() {
28
28
  }
29
29
  }
30
30
 
31
+ /**
32
+ * Check whether a directory is inside a git work tree
33
+ * @param {string} path - Directory to check
34
+ * @returns {boolean}
35
+ */
36
+ export function isInsideGitWorkTree(path = process.cwd()) {
37
+ const absolutePath = resolvePath(path);
38
+ try {
39
+ const output = execSync('git rev-parse --is-inside-work-tree', {
40
+ cwd: absolutePath,
41
+ stdio: ['ignore', 'pipe', 'ignore']
42
+ }).toString().trim();
43
+ return output === 'true';
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
31
49
  /**
32
50
  * Execute a git command and return the result
33
51
  * @param {string[]} args - Git command arguments
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Supported install targets for skills and agents.
3
+ */
4
+
5
+ export const SKILL_INSTALL_TARGETS = [
6
+ { path: '.github/skills/', harness: 'copilot', scope: 'local' },
7
+ { path: '~/.copilot/skills/', harness: 'copilot', scope: 'global' },
8
+ { path: '.agents/skills/', harness: 'codex', scope: 'local' },
9
+ { path: '~/.agents/skills/', harness: 'codex', scope: 'global' },
10
+ { path: '.claude/skills/', harness: 'claude', scope: 'local' },
11
+ { path: '~/.claude/skills/', harness: 'claude', scope: 'global' }
12
+ ];
13
+
14
+ export const LEGACY_SKILL_INSTALL_TARGETS = [
15
+ {
16
+ path: '~/.codex/skills/',
17
+ harness: 'codex',
18
+ scope: 'legacy global'
19
+ }
20
+ ];
21
+
22
+ export const AGENT_INSTALL_TARGETS = [
23
+ { path: '.github/agents/', harness: 'copilot', scope: 'local', installMode: 'sparse-git' },
24
+ { path: '~/.copilot/agents/', harness: 'copilot', scope: 'global', installMode: 'sparse-git' },
25
+ { path: '.claude/agents/', harness: 'claude', scope: 'local', installMode: 'sparse-git' },
26
+ { path: '~/.claude/agents/', harness: 'claude', scope: 'global', installMode: 'sparse-git' },
27
+ { path: '.codex/agents/', harness: 'codex', scope: 'local', installMode: 'codex-toml' },
28
+ { path: '~/.codex/agents/', harness: 'codex', scope: 'global', installMode: 'codex-toml' }
29
+ ];
30
+
31
+ export const LEGACY_AGENT_INSTALL_TARGETS = [
32
+ {
33
+ path: '.agents/agents/',
34
+ harness: 'codex',
35
+ scope: 'legacy local'
36
+ }
37
+ ];
38
+
39
+ export function allSkillDetectionTargets() {
40
+ return [...SKILL_INSTALL_TARGETS, ...LEGACY_SKILL_INSTALL_TARGETS];
41
+ }
42
+
43
+ export function allAgentDetectionTargets() {
44
+ return [...AGENT_INSTALL_TARGETS, ...LEGACY_AGENT_INSTALL_TARGETS];
45
+ }
46
+
47
+ export function getAgentInstallMode(path) {
48
+ const exactTarget = getTargetByPath(AGENT_INSTALL_TARGETS, path);
49
+ if (exactTarget?.installMode) {
50
+ return exactTarget.installMode;
51
+ }
52
+
53
+ const normalizedPath = path.replace(/\\/g, '/').replace(/\/+$/, '');
54
+ if (normalizedPath.endsWith('/.codex/agents') || normalizedPath === '.codex/agents' || normalizedPath === '~/.codex/agents') {
55
+ return 'codex-toml';
56
+ }
57
+
58
+ return 'sparse-git';
59
+ }
60
+
61
+ export function getTargetByPath(targets, path) {
62
+ return targets.find(target => target.path === path);
63
+ }
64
+
65
+ export function formatTargetLabel(target, count, noun, { installed = false } = {}) {
66
+ const plural = count === 1 ? noun : `${noun}s`;
67
+ const countText = installed ? `${count} ${plural} installed` : `${count} ${plural}`;
68
+ const detail = target
69
+ ? `${target.harness} | ${target.scope} | ${countText}`
70
+ : countText;
71
+
72
+ return `${target?.path || ''} (${detail})`;
73
+ }
package/lib/prompts.js CHANGED
@@ -4,18 +4,20 @@
4
4
 
5
5
  import inquirer from 'inquirer';
6
6
  import * as readline from 'readline';
7
+ import {
8
+ AGENT_INSTALL_TARGETS,
9
+ SKILL_INSTALL_TARGETS,
10
+ allAgentDetectionTargets,
11
+ allSkillDetectionTargets,
12
+ formatTargetLabel,
13
+ getTargetByPath
14
+ } from './install-targets.js';
7
15
 
8
16
  const SKILL_PATH_CHOICES = {
9
- GITHUB: '.github/skills/',
10
- CODEX_HOME: '~/.codex/skills/',
11
- CLAUDE: '.claude/skills/',
12
17
  CUSTOM: '__custom__'
13
18
  };
14
19
 
15
20
  const AGENT_PATH_CHOICES = {
16
- GITHUB: '.github/agents/',
17
- CODEX: '.agents/agents/',
18
- CLAUDE: '.claude/agents/',
19
21
  CUSTOM: '__custom__'
20
22
  };
21
23
 
@@ -61,17 +63,22 @@ export async function promptInstallType() {
61
63
  /**
62
64
  * Prompt user to select one or more installation paths, showing existing installations
63
65
  * @param {Array<{path: string, skillCount: number}>} existingInstalls - Detected existing installations
66
+ * @param {number} availableSkillCount - Number of skills available for new installations
64
67
  * @returns {Promise<Array<{path: string, isExisting: boolean}>>} Selected paths and whether each is existing
65
68
  */
66
- export async function promptInstallPath(existingInstalls = []) {
69
+ export async function promptInstallPath(existingInstalls = [], availableSkillCount = 0) {
67
70
  const choices = [];
68
71
  const existingPaths = existingInstalls.map(i => i.path);
72
+ const detectionTargets = allSkillDetectionTargets();
69
73
 
70
74
  // Add existing installations at the top
71
75
  if (existingInstalls.length > 0) {
72
76
  existingInstalls.forEach(install => {
77
+ const target = getTargetByPath(detectionTargets, install.path);
73
78
  choices.push({
74
- name: `${install.path} (${install.skillCount} skill${install.skillCount !== 1 ? 's' : ''} installed)`,
79
+ name: target
80
+ ? formatTargetLabel(target, install.skillCount, 'skill', { installed: true })
81
+ : `${install.path} (${install.skillCount} skill${install.skillCount !== 1 ? 's' : ''} installed)`,
75
82
  value: install.path
76
83
  });
77
84
  });
@@ -79,15 +86,12 @@ export async function promptInstallPath(existingInstalls = []) {
79
86
  }
80
87
 
81
88
  // Standard path options
82
- const standardPaths = [
83
- { path: SKILL_PATH_CHOICES.GITHUB, label: `${SKILL_PATH_CHOICES.GITHUB} (Copilot)` },
84
- { path: SKILL_PATH_CHOICES.CODEX_HOME, label: `${SKILL_PATH_CHOICES.CODEX_HOME} (Codex)` },
85
- { path: SKILL_PATH_CHOICES.CLAUDE, label: `${SKILL_PATH_CHOICES.CLAUDE} (Claude)` }
86
- ];
87
-
88
- standardPaths.forEach(({ path, label }) => {
89
- if (!existingPaths.includes(path)) {
90
- choices.push({ name: label, value: path });
89
+ SKILL_INSTALL_TARGETS.forEach(target => {
90
+ if (!existingPaths.includes(target.path)) {
91
+ choices.push({
92
+ name: formatTargetLabel(target, availableSkillCount, 'skill'),
93
+ value: target.path
94
+ });
91
95
  }
92
96
  });
93
97
 
@@ -144,17 +148,22 @@ export async function promptInstallPath(existingInstalls = []) {
144
148
  /**
145
149
  * Prompt user to select one or more subagent installation paths
146
150
  * @param {Array<{path: string, agentCount: number}>} existingInstalls - Detected existing installations
151
+ * @param {number} availableAgentCount - Number of agents available for new installations
147
152
  * @returns {Promise<Array<{path: string, isExisting: boolean}>>} Selected paths and whether each is existing
148
153
  */
149
- export async function promptAgentInstallPath(existingInstalls = []) {
154
+ export async function promptAgentInstallPath(existingInstalls = [], availableAgentCount = 0) {
150
155
  const choices = [];
151
156
  const existingPaths = existingInstalls.map(i => i.path);
157
+ const detectionTargets = allAgentDetectionTargets();
152
158
 
153
159
  // Add existing installations at the top
154
160
  if (existingInstalls.length > 0) {
155
161
  existingInstalls.forEach(install => {
162
+ const target = getTargetByPath(detectionTargets, install.path);
156
163
  choices.push({
157
- name: `${install.path} (${install.agentCount} agent${install.agentCount !== 1 ? 's' : ''} installed)`,
164
+ name: target
165
+ ? formatTargetLabel(target, install.agentCount, 'agent', { installed: true })
166
+ : `${install.path} (${install.agentCount} agent${install.agentCount !== 1 ? 's' : ''} installed)`,
158
167
  value: install.path
159
168
  });
160
169
  });
@@ -162,15 +171,13 @@ export async function promptAgentInstallPath(existingInstalls = []) {
162
171
  }
163
172
 
164
173
  // Standard path options
165
- const standardPaths = [
166
- { path: AGENT_PATH_CHOICES.GITHUB, label: `${AGENT_PATH_CHOICES.GITHUB} (Copilot)` },
167
- { path: AGENT_PATH_CHOICES.CODEX, label: `${AGENT_PATH_CHOICES.CODEX} (Codex)` },
168
- { path: AGENT_PATH_CHOICES.CLAUDE, label: `${AGENT_PATH_CHOICES.CLAUDE} (Claude)` }
169
- ];
170
-
171
- standardPaths.forEach(({ path, label }) => {
172
- if (!existingPaths.includes(path)) {
173
- choices.push({ name: label, value: path });
174
+ AGENT_INSTALL_TARGETS.forEach(target => {
175
+ if (!existingPaths.includes(target.path)) {
176
+ choices.push({
177
+ name: formatTargetLabel(target, availableAgentCount, 'agent'),
178
+ value: target.path,
179
+ disabled: target.disabledReason
180
+ });
174
181
  }
175
182
  });
176
183
 
package/lib/subagents.js CHANGED
@@ -17,6 +17,29 @@ function humanizeAgentName(filename) {
17
17
  .join(' ');
18
18
  }
19
19
 
20
+ function extractSubagentSections(content) {
21
+ const standardMatch = content.match(/^(---\s*\n[\s\S]*?\n---)(?:\s*\n)?/);
22
+ if (standardMatch) {
23
+ return {
24
+ frontmatter: standardMatch[1].replace(/^---\s*\n|\n---$/g, ''),
25
+ body: content.slice(standardMatch[0].length).trim(),
26
+ };
27
+ }
28
+
29
+ const chatAgentMatch = content.match(/^```chatagent\s*\n---\s*\n([\s\S]*?)\n---\s*\n```(?:\s*\n)?/);
30
+ if (chatAgentMatch) {
31
+ return {
32
+ frontmatter: chatAgentMatch[1],
33
+ body: content.slice(chatAgentMatch[0].length).trim(),
34
+ };
35
+ }
36
+
37
+ return {
38
+ frontmatter: '',
39
+ body: content.trim(),
40
+ };
41
+ }
42
+
20
43
  /**
21
44
  * Fetch the list of subagent files from the repository
22
45
  * Subagents are .agent.md files at the repo root
@@ -54,6 +77,20 @@ export async function fetchAvailableSubagents() {
54
77
  * @returns {Promise<{name: string, description: string}>}
55
78
  */
56
79
  export async function fetchSubagentMetadata(filename) {
80
+ try {
81
+ const content = await fetchSubagentContent(filename);
82
+ return parseSubagentFrontmatter(content);
83
+ } catch (error) {
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Fetch the raw source for a subagent file
90
+ * @param {string} filename - The agent filename
91
+ * @returns {Promise<string>}
92
+ */
93
+ export async function fetchSubagentContent(filename) {
57
94
  const fileUrl = `${GITHUB_API}/repos/${SUBAGENTS_REPO_OWNER}/${SUBAGENTS_REPO_NAME}/contents/${filename}`;
58
95
 
59
96
  try {
@@ -66,9 +103,7 @@ export async function fetchSubagentMetadata(filename) {
66
103
  }
67
104
 
68
105
  const data = await response.json();
69
- const content = Buffer.from(data.content, 'base64').toString('utf-8');
70
-
71
- return parseSubagentFrontmatter(content);
106
+ return Buffer.from(data.content, 'base64').toString('utf-8');
72
107
  } catch (error) {
73
108
  throw error;
74
109
  }
@@ -81,13 +116,7 @@ export async function fetchSubagentMetadata(filename) {
81
116
  * @returns {{name: string, description: string}}
82
117
  */
83
118
  function parseSubagentFrontmatter(content) {
84
- // Try to match standard --- frontmatter
85
- const standardMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
86
-
87
- // Try to match ```chatagent fenced frontmatter
88
- const chatAgentMatch = content.match(/```chatagent\s*\n---\s*\n([\s\S]*?)\n---/);
89
-
90
- const frontmatterContent = standardMatch?.[1] || chatAgentMatch?.[1];
119
+ const { frontmatter: frontmatterContent } = extractSubagentSections(content);
91
120
 
92
121
  if (!frontmatterContent) {
93
122
  return { name: '', description: '' };
@@ -102,6 +131,24 @@ function parseSubagentFrontmatter(content) {
102
131
  };
103
132
  }
104
133
 
134
+ /**
135
+ * Parse a subagent file into metadata and body content.
136
+ * @param {string} content - The .agent.md file content
137
+ * @param {string} filename - The source filename for fallback naming
138
+ * @returns {{name: string, description: string, body: string, filename: string}}
139
+ */
140
+ export function parseSubagentDefinition(content, filename = '') {
141
+ const metadata = parseSubagentFrontmatter(content);
142
+ const { body } = extractSubagentSections(content);
143
+
144
+ return {
145
+ filename,
146
+ name: metadata.name || humanizeAgentName(filename),
147
+ description: metadata.description || '',
148
+ body,
149
+ };
150
+ }
151
+
105
152
  /**
106
153
  * Get the subagents repository clone URL
107
154
  * @returns {string}
@@ -110,4 +157,4 @@ export function getSubagentsRepoUrl() {
110
157
  return `https://github.com/${SUBAGENTS_REPO_OWNER}/${SUBAGENTS_REPO_NAME}.git`;
111
158
  }
112
159
 
113
- export { SUBAGENTS_REPO_OWNER, SUBAGENTS_REPO_NAME };
160
+ export { SUBAGENTS_REPO_OWNER, SUBAGENTS_REPO_NAME, humanizeAgentName };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supercorks/skills-installer",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "Interactive CLI installer for AI agent skills and subagents",
5
5
  "type": "module",
6
6
  "bin": {