@supercorks/skills-installer 1.11.1 → 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 +23 -9
- package/bin/install.js +58 -14
- package/lib/codex-agents.js +165 -0
- package/lib/install-targets.js +73 -0
- package/lib/prompts.js +35 -28
- package/lib/subagents.js +58 -11
- package/package.json +1 -1
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,13 +18,23 @@ 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
|
-
|
|
23
|
-
|
|
24
|
-
- `.
|
|
25
|
-
-
|
|
26
|
-
- `.agents/
|
|
27
|
-
-
|
|
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
40
|
3. **Gitignore option** - If launched from inside a git repository, optionally add the installation path to `.gitignore`
|
|
@@ -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. **
|
|
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
|
@@ -37,13 +37,15 @@ import {
|
|
|
37
37
|
checkSubagentsForUpdates
|
|
38
38
|
} from '../lib/git.js';
|
|
39
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';
|
|
40
42
|
|
|
41
43
|
const require = createRequire(import.meta.url);
|
|
42
44
|
const { version: VERSION } = require('../package.json');
|
|
43
45
|
|
|
44
46
|
// Common installation paths to check for existing installations
|
|
45
|
-
const SKILL_PATHS =
|
|
46
|
-
const AGENT_PATHS =
|
|
47
|
+
const SKILL_PATHS = allSkillDetectionTargets().map(target => target.path);
|
|
48
|
+
const AGENT_PATHS = allAgentDetectionTargets().map(target => target.path);
|
|
47
49
|
|
|
48
50
|
function resolveInstallPath(path) {
|
|
49
51
|
if (path === '~') return homedir();
|
|
@@ -92,6 +94,24 @@ async function detectExistingAgentInstallations() {
|
|
|
92
94
|
|
|
93
95
|
for (const path of AGENT_PATHS) {
|
|
94
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
|
+
|
|
95
115
|
const gitDir = join(absolutePath, '.git');
|
|
96
116
|
|
|
97
117
|
if (existsSync(gitDir)) {
|
|
@@ -220,7 +240,7 @@ async function runSkillsInstall() {
|
|
|
220
240
|
const existingInstalls = await detectExistingSkillInstallations();
|
|
221
241
|
|
|
222
242
|
// Ask where to install (showing existing installations if any)
|
|
223
|
-
const installTargets = await promptInstallPath(existingInstalls);
|
|
243
|
+
const installTargets = await promptInstallPath(existingInstalls, skills.length);
|
|
224
244
|
|
|
225
245
|
for (let i = 0; i < installTargets.length; i++) {
|
|
226
246
|
const target = installTargets[i];
|
|
@@ -380,7 +400,7 @@ async function runSubagentsInstall() {
|
|
|
380
400
|
const existingInstalls = await detectExistingAgentInstallations();
|
|
381
401
|
|
|
382
402
|
// Ask where to install (showing existing installations if any)
|
|
383
|
-
const installTargets = await promptAgentInstallPath(existingInstalls);
|
|
403
|
+
const installTargets = await promptAgentInstallPath(existingInstalls, subagents.length);
|
|
384
404
|
|
|
385
405
|
for (let i = 0; i < installTargets.length; i++) {
|
|
386
406
|
const target = installTargets[i];
|
|
@@ -400,15 +420,16 @@ async function runSubagentsInstall() {
|
|
|
400
420
|
async function runSubagentsInstallForTarget(subagents, existingInstalls, target) {
|
|
401
421
|
const { path: installPath, isExisting } = target;
|
|
402
422
|
const absoluteInstallPath = resolveInstallPath(installPath);
|
|
423
|
+
const installMode = getAgentInstallMode(installPath);
|
|
403
424
|
const gitDir = join(absoluteInstallPath, '.git');
|
|
404
|
-
const hasExistingRepo = existsSync(gitDir);
|
|
425
|
+
const hasExistingRepo = installMode === 'sparse-git' && existsSync(gitDir);
|
|
405
426
|
|
|
406
427
|
// Get currently installed subagents if managing existing installation
|
|
407
428
|
let installedAgents = [];
|
|
408
429
|
if (isExisting) {
|
|
409
430
|
const existingInstall = existingInstalls.find(i => i.path === installPath);
|
|
410
431
|
installedAgents = existingInstall?.agents || [];
|
|
411
|
-
} else {
|
|
432
|
+
} else if (installMode === 'sparse-git') {
|
|
412
433
|
// Check if manually entered path has an existing installation
|
|
413
434
|
if (hasExistingRepo) {
|
|
414
435
|
try {
|
|
@@ -418,16 +439,27 @@ async function runSubagentsInstallForTarget(subagents, existingInstalls, target)
|
|
|
418
439
|
// Prompt will default to selecting all subagents.
|
|
419
440
|
}
|
|
420
441
|
}
|
|
442
|
+
} else {
|
|
443
|
+
try {
|
|
444
|
+
installedAgents = await listInstalledCodexAgents(absoluteInstallPath);
|
|
445
|
+
} catch {
|
|
446
|
+
// Ignore detection failures for custom Codex agent paths.
|
|
447
|
+
}
|
|
421
448
|
}
|
|
422
449
|
|
|
423
|
-
const isManageMode =
|
|
450
|
+
const isManageMode = installMode === 'sparse-git'
|
|
451
|
+
? (isExisting || hasExistingRepo || installedAgents.length > 0)
|
|
452
|
+
: installedAgents.length > 0;
|
|
424
453
|
|
|
425
454
|
// Check for updates if in manage mode
|
|
426
455
|
let subagentsNeedingUpdate = new Set();
|
|
427
456
|
if (isManageMode) {
|
|
428
457
|
const updateSpinner = showSpinner('Checking for available updates...');
|
|
429
458
|
try {
|
|
430
|
-
subagentsNeedingUpdate =
|
|
459
|
+
subagentsNeedingUpdate = installMode === 'sparse-git'
|
|
460
|
+
? await checkSubagentsForUpdates(absoluteInstallPath, installedAgents)
|
|
461
|
+
: await checkCodexAgentUpdates(absoluteInstallPath, installedAgents);
|
|
462
|
+
|
|
431
463
|
if (subagentsNeedingUpdate.size > 0) {
|
|
432
464
|
updateSpinner.stop(`✅ Found ${subagentsNeedingUpdate.size} subagent${subagentsNeedingUpdate.size !== 1 ? 's' : ''} with updates available`);
|
|
433
465
|
} else {
|
|
@@ -475,9 +507,15 @@ async function runSubagentsInstallForTarget(subagents, existingInstalls, target)
|
|
|
475
507
|
const updateSpinner = showSpinner('Updating subagents installation...');
|
|
476
508
|
|
|
477
509
|
try {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
+
}
|
|
481
519
|
} catch (error) {
|
|
482
520
|
updateSpinner.stop('❌ Update failed');
|
|
483
521
|
showError(error.message);
|
|
@@ -490,9 +528,15 @@ async function runSubagentsInstallForTarget(subagents, existingInstalls, target)
|
|
|
490
528
|
const installSpinner = showSpinner('Installing selected subagents...');
|
|
491
529
|
|
|
492
530
|
try {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
+
}
|
|
496
540
|
} catch (error) {
|
|
497
541
|
installSpinner.stop('❌ Installation failed');
|
|
498
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
|
+
}
|
|
@@ -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:
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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:
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|