@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 +24 -10
- package/bin/install.js +73 -16
- package/lib/codex-agents.js +165 -0
- package/lib/git.js +18 -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,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
|
-
|
|
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
|
-
3. **Gitignore option** -
|
|
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. **
|
|
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 =
|
|
45
|
-
const AGENT_PATHS =
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
466
|
-
|
|
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
|
-
|
|
481
|
-
|
|
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:
|
|
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 };
|