@ecc-hgy/ae 0.5.0 → 0.6.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/LICENSE +21 -21
- package/README.md +168 -155
- package/bin/ae.js +2 -2
- package/package.json +43 -43
- package/skills/brainstorming/SKILL.md +133 -133
- package/skills/diagnose/SKILL.md +146 -146
- package/skills/diagnose/assets/issue-7-sections.md +35 -35
- package/skills/diagnose/scripts/hitl-loop.template.sh +41 -41
- package/skills/grill-me/SKILL.md +10 -10
- package/skills/handoff/SKILL.md +19 -19
- package/skills/improve-codebase-architecture/DEEPENING.md +37 -37
- package/skills/improve-codebase-architecture/HTML-REPORT.md +123 -123
- package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -44
- package/skills/improve-codebase-architecture/LANGUAGE.md +53 -53
- package/skills/improve-codebase-architecture/SKILL.md +101 -101
- package/skills/karpathy-guidelines/SKILL.md +63 -63
- package/skills/powerautomate-email-to-sharepoint-excel/powerautomate-email-to-sharepoint-excel-skill.md +496 -496
- package/skills/review/SKILL.md +119 -119
- package/skills/tdd/SKILL.md +157 -157
- package/skills/tdd/deep-modules.md +33 -33
- package/skills/tdd/interface-design.md +31 -31
- package/skills/tdd/mocking.md +59 -59
- package/skills/tdd/refactoring.md +10 -10
- package/skills/tdd/tests.md +61 -61
- package/skills/to-issues/SKILL.md +79 -79
- package/skills/to-issues/todo-template.md +25 -25
- package/skills/to-prd/SKILL.md +108 -108
- package/skills/using-agentic-engineering/SKILL.md +62 -62
- package/skills/verification-before-completion/SKILL.md +153 -153
- package/skills/writing-plans/SKILL.md +115 -115
- package/skills/zoom-out/SKILL.md +7 -7
- package/src/cli.js +61 -61
- package/src/commands/init.js +137 -137
- package/src/commands/setup.js +162 -162
- package/src/platforms.js +132 -132
- package/src/skeleton.js +134 -99
- package/src/utils/copy.js +100 -100
- package/src/utils/paths.js +60 -60
- package/src/utils/report.js +30 -30
- package/templates/entries/AGENTS.md +2 -0
- package/templates/entries/CLAUDE.md +5 -5
- package/templates/entries/README.md +33 -31
- package/templates/entries/handoff.md +1 -1
- package/templates/entries/spec/ADR/AGENTS.md +30 -30
- package/templates/entries/spec/ADR/CLAUDE.md +5 -5
- package/templates/entries/spec/AGENTS.md +34 -34
- package/templates/entries/spec/CLAUDE.md +5 -5
- package/templates/entries/spec/INDEX.md +28 -28
- package/templates/entries/spec/README.md +23 -23
package/src/commands/setup.js
CHANGED
|
@@ -1,162 +1,162 @@
|
|
|
1
|
-
import { mkdir, stat } from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { createInterface } from 'node:readline/promises';
|
|
4
|
-
import { copyTreeIdempotent } from '../utils/copy.js';
|
|
5
|
-
import { skillsPath } from '../utils/paths.js';
|
|
6
|
-
import { formatSetupReport } from '../utils/report.js';
|
|
7
|
-
import {
|
|
8
|
-
PLATFORMS,
|
|
9
|
-
detectPlatforms,
|
|
10
|
-
getPlatformSkillsDir,
|
|
11
|
-
listPlatformIds,
|
|
12
|
-
parsePlatformList,
|
|
13
|
-
resolvePlatforms,
|
|
14
|
-
} from '../platforms.js';
|
|
15
|
-
|
|
16
|
-
const HELP = `Usage: ae setup [options]
|
|
17
|
-
|
|
18
|
-
Options:
|
|
19
|
-
--target <path> Install AE assets into this project directory. Defaults to cwd.
|
|
20
|
-
--platform <ids> Install for comma-separated platforms, e.g. claude,codex,cursor.
|
|
21
|
-
--dry-run Show what would be installed without writing files.
|
|
22
|
-
-h, --help Show help
|
|
23
|
-
|
|
24
|
-
Platforms:
|
|
25
|
-
${listPlatformIds().join(', ')}`;
|
|
26
|
-
|
|
27
|
-
export async function run(argv = []) {
|
|
28
|
-
const options = parseArgs(argv);
|
|
29
|
-
|
|
30
|
-
if (options.help) {
|
|
31
|
-
console.log(HELP);
|
|
32
|
-
return 0;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const target = path.resolve(options.target ?? process.cwd());
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const explicitPlatforms =
|
|
39
|
-
options.platformIds.length > 0 ? resolvePlatforms(options.platformIds) : undefined;
|
|
40
|
-
await ensureTargetDirectory(target, options.dryRun);
|
|
41
|
-
const selectedPlatforms = explicitPlatforms ?? (await selectPlatforms(target));
|
|
42
|
-
|
|
43
|
-
const created = [];
|
|
44
|
-
const skipped = [];
|
|
45
|
-
const skillGroups = selectedPlatforms.map((platform) => {
|
|
46
|
-
const skillsDir = getPlatformSkillsDir(platform);
|
|
47
|
-
return {
|
|
48
|
-
dest: path.join(target, skillsDir),
|
|
49
|
-
prefix: skillsDir,
|
|
50
|
-
};
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
for (const group of skillGroups) {
|
|
54
|
-
const result = await copyTreeIdempotent(skillsPath(), group.dest, {
|
|
55
|
-
dryRun: options.dryRun,
|
|
56
|
-
});
|
|
57
|
-
created.push(...result.created.map((item) => toDisplayPath(path.join(group.prefix, item))));
|
|
58
|
-
skipped.push(...result.skipped.map((item) => toDisplayPath(path.join(group.prefix, item))));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
console.log(formatSetupReport({ created, skipped, target, dryRun: options.dryRun }));
|
|
62
|
-
return 0;
|
|
63
|
-
} catch (error) {
|
|
64
|
-
console.error(`AE setup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
65
|
-
process.exitCode = 1;
|
|
66
|
-
return 1;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function parseArgs(argv) {
|
|
71
|
-
const options = {
|
|
72
|
-
target: undefined,
|
|
73
|
-
platformIds: [],
|
|
74
|
-
dryRun: false,
|
|
75
|
-
help: false,
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
79
|
-
const arg = argv[index];
|
|
80
|
-
if (arg === '--help' || arg === '-h') {
|
|
81
|
-
options.help = true;
|
|
82
|
-
} else if (arg === '--dry-run') {
|
|
83
|
-
options.dryRun = true;
|
|
84
|
-
} else if (arg === '--platform') {
|
|
85
|
-
const value = argv[index + 1];
|
|
86
|
-
if (!value) {
|
|
87
|
-
throw new Error('--platform requires a comma-separated platform list');
|
|
88
|
-
}
|
|
89
|
-
options.platformIds.push(...parsePlatformList(value));
|
|
90
|
-
index += 1;
|
|
91
|
-
} else if (arg === '--target') {
|
|
92
|
-
const value = argv[index + 1];
|
|
93
|
-
if (!value) {
|
|
94
|
-
throw new Error('--target requires a path');
|
|
95
|
-
}
|
|
96
|
-
options.target = value;
|
|
97
|
-
index += 1;
|
|
98
|
-
} else {
|
|
99
|
-
throw new Error(`Unknown option for setup: ${arg}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return options;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function selectPlatforms(target) {
|
|
107
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
108
|
-
throw new Error(
|
|
109
|
-
`No platforms selected. In non-interactive mode, pass --platform <ids>. Available: ${listPlatformIds().join(', ')}`,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const detected = await detectPlatforms(target);
|
|
114
|
-
const detectedIds = detected.map((platform) => platform.id);
|
|
115
|
-
const defaultIds = detectedIds;
|
|
116
|
-
|
|
117
|
-
console.log('Select target platforms for AE skills:');
|
|
118
|
-
for (const [index, platform] of PLATFORMS.entries()) {
|
|
119
|
-
const marker = defaultIds.includes(platform.id) ? '*' : ' ';
|
|
120
|
-
console.log(` ${index + 1}. [${marker}] ${platform.name} (${platform.id}) -> ${getPlatformSkillsDir(platform)}/`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const defaultPrompt = defaultIds.length > 0 ? ` [${defaultIds.join(',')}]` : '';
|
|
124
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
125
|
-
try {
|
|
126
|
-
const answer = await rl.question(`Platform ids or numbers, comma-separated${defaultPrompt}: `);
|
|
127
|
-
const selected = answer.trim() ? parseInteractivePlatformAnswer(answer) : defaultIds;
|
|
128
|
-
return resolvePlatforms(selected);
|
|
129
|
-
} finally {
|
|
130
|
-
rl.close();
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function parseInteractivePlatformAnswer(answer) {
|
|
135
|
-
return parsePlatformList(answer).map((item) => {
|
|
136
|
-
const number = Number(item);
|
|
137
|
-
if (Number.isInteger(number) && number >= 1 && number <= PLATFORMS.length) {
|
|
138
|
-
return PLATFORMS[number - 1].id;
|
|
139
|
-
}
|
|
140
|
-
return item;
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async function ensureTargetDirectory(target, dryRun) {
|
|
145
|
-
try {
|
|
146
|
-
const current = await stat(target);
|
|
147
|
-
if (!current.isDirectory()) {
|
|
148
|
-
throw new Error(`target exists but is not a directory: ${target}`);
|
|
149
|
-
}
|
|
150
|
-
} catch (error) {
|
|
151
|
-
if (error?.code !== 'ENOENT') {
|
|
152
|
-
throw error;
|
|
153
|
-
}
|
|
154
|
-
if (!dryRun) {
|
|
155
|
-
await mkdir(target, { recursive: true });
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function toDisplayPath(value) {
|
|
161
|
-
return value.split(path.sep).join('/');
|
|
162
|
-
}
|
|
1
|
+
import { mkdir, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline/promises';
|
|
4
|
+
import { copyTreeIdempotent } from '../utils/copy.js';
|
|
5
|
+
import { skillsPath } from '../utils/paths.js';
|
|
6
|
+
import { formatSetupReport } from '../utils/report.js';
|
|
7
|
+
import {
|
|
8
|
+
PLATFORMS,
|
|
9
|
+
detectPlatforms,
|
|
10
|
+
getPlatformSkillsDir,
|
|
11
|
+
listPlatformIds,
|
|
12
|
+
parsePlatformList,
|
|
13
|
+
resolvePlatforms,
|
|
14
|
+
} from '../platforms.js';
|
|
15
|
+
|
|
16
|
+
const HELP = `Usage: ae setup [options]
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--target <path> Install AE assets into this project directory. Defaults to cwd.
|
|
20
|
+
--platform <ids> Install for comma-separated platforms, e.g. claude,codex,cursor.
|
|
21
|
+
--dry-run Show what would be installed without writing files.
|
|
22
|
+
-h, --help Show help
|
|
23
|
+
|
|
24
|
+
Platforms:
|
|
25
|
+
${listPlatformIds().join(', ')}`;
|
|
26
|
+
|
|
27
|
+
export async function run(argv = []) {
|
|
28
|
+
const options = parseArgs(argv);
|
|
29
|
+
|
|
30
|
+
if (options.help) {
|
|
31
|
+
console.log(HELP);
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const target = path.resolve(options.target ?? process.cwd());
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const explicitPlatforms =
|
|
39
|
+
options.platformIds.length > 0 ? resolvePlatforms(options.platformIds) : undefined;
|
|
40
|
+
await ensureTargetDirectory(target, options.dryRun);
|
|
41
|
+
const selectedPlatforms = explicitPlatforms ?? (await selectPlatforms(target));
|
|
42
|
+
|
|
43
|
+
const created = [];
|
|
44
|
+
const skipped = [];
|
|
45
|
+
const skillGroups = selectedPlatforms.map((platform) => {
|
|
46
|
+
const skillsDir = getPlatformSkillsDir(platform);
|
|
47
|
+
return {
|
|
48
|
+
dest: path.join(target, skillsDir),
|
|
49
|
+
prefix: skillsDir,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
for (const group of skillGroups) {
|
|
54
|
+
const result = await copyTreeIdempotent(skillsPath(), group.dest, {
|
|
55
|
+
dryRun: options.dryRun,
|
|
56
|
+
});
|
|
57
|
+
created.push(...result.created.map((item) => toDisplayPath(path.join(group.prefix, item))));
|
|
58
|
+
skipped.push(...result.skipped.map((item) => toDisplayPath(path.join(group.prefix, item))));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(formatSetupReport({ created, skipped, target, dryRun: options.dryRun }));
|
|
62
|
+
return 0;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(`AE setup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
65
|
+
process.exitCode = 1;
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseArgs(argv) {
|
|
71
|
+
const options = {
|
|
72
|
+
target: undefined,
|
|
73
|
+
platformIds: [],
|
|
74
|
+
dryRun: false,
|
|
75
|
+
help: false,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
79
|
+
const arg = argv[index];
|
|
80
|
+
if (arg === '--help' || arg === '-h') {
|
|
81
|
+
options.help = true;
|
|
82
|
+
} else if (arg === '--dry-run') {
|
|
83
|
+
options.dryRun = true;
|
|
84
|
+
} else if (arg === '--platform') {
|
|
85
|
+
const value = argv[index + 1];
|
|
86
|
+
if (!value) {
|
|
87
|
+
throw new Error('--platform requires a comma-separated platform list');
|
|
88
|
+
}
|
|
89
|
+
options.platformIds.push(...parsePlatformList(value));
|
|
90
|
+
index += 1;
|
|
91
|
+
} else if (arg === '--target') {
|
|
92
|
+
const value = argv[index + 1];
|
|
93
|
+
if (!value) {
|
|
94
|
+
throw new Error('--target requires a path');
|
|
95
|
+
}
|
|
96
|
+
options.target = value;
|
|
97
|
+
index += 1;
|
|
98
|
+
} else {
|
|
99
|
+
throw new Error(`Unknown option for setup: ${arg}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return options;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function selectPlatforms(target) {
|
|
107
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`No platforms selected. In non-interactive mode, pass --platform <ids>. Available: ${listPlatformIds().join(', ')}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const detected = await detectPlatforms(target);
|
|
114
|
+
const detectedIds = detected.map((platform) => platform.id);
|
|
115
|
+
const defaultIds = detectedIds;
|
|
116
|
+
|
|
117
|
+
console.log('Select target platforms for AE skills:');
|
|
118
|
+
for (const [index, platform] of PLATFORMS.entries()) {
|
|
119
|
+
const marker = defaultIds.includes(platform.id) ? '*' : ' ';
|
|
120
|
+
console.log(` ${index + 1}. [${marker}] ${platform.name} (${platform.id}) -> ${getPlatformSkillsDir(platform)}/`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const defaultPrompt = defaultIds.length > 0 ? ` [${defaultIds.join(',')}]` : '';
|
|
124
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
125
|
+
try {
|
|
126
|
+
const answer = await rl.question(`Platform ids or numbers, comma-separated${defaultPrompt}: `);
|
|
127
|
+
const selected = answer.trim() ? parseInteractivePlatformAnswer(answer) : defaultIds;
|
|
128
|
+
return resolvePlatforms(selected);
|
|
129
|
+
} finally {
|
|
130
|
+
rl.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseInteractivePlatformAnswer(answer) {
|
|
135
|
+
return parsePlatformList(answer).map((item) => {
|
|
136
|
+
const number = Number(item);
|
|
137
|
+
if (Number.isInteger(number) && number >= 1 && number <= PLATFORMS.length) {
|
|
138
|
+
return PLATFORMS[number - 1].id;
|
|
139
|
+
}
|
|
140
|
+
return item;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function ensureTargetDirectory(target, dryRun) {
|
|
145
|
+
try {
|
|
146
|
+
const current = await stat(target);
|
|
147
|
+
if (!current.isDirectory()) {
|
|
148
|
+
throw new Error(`target exists but is not a directory: ${target}`);
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error?.code !== 'ENOENT') {
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
if (!dryRun) {
|
|
155
|
+
await mkdir(target, { recursive: true });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function toDisplayPath(value) {
|
|
161
|
+
return value.split(path.sep).join('/');
|
|
162
|
+
}
|
package/src/platforms.js
CHANGED
|
@@ -1,132 +1,132 @@
|
|
|
1
|
-
import { stat } from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
export const PLATFORMS = [
|
|
5
|
-
{ id: 'claude', name: 'Claude Code', skillsDir: '.claude', aliases: ['claude-code'] },
|
|
6
|
-
{ id: 'codex', name: 'Codex', skillsDir: '.codex' },
|
|
7
|
-
{ id: 'windsurf', name: 'Windsurf', skillsDir: '.windsurf' },
|
|
8
|
-
{
|
|
9
|
-
id: 'github-copilot',
|
|
10
|
-
name: 'GitHub Copilot',
|
|
11
|
-
skillsDir: '.github',
|
|
12
|
-
aliases: ['copilot', 'github'],
|
|
13
|
-
detectionPaths: [
|
|
14
|
-
'.github/copilot-instructions.md',
|
|
15
|
-
'.github/instructions',
|
|
16
|
-
'.github/prompts',
|
|
17
|
-
'.github/skills',
|
|
18
|
-
],
|
|
19
|
-
},
|
|
20
|
-
{ id: 'kilocode', name: 'Kilo Code', skillsDir: '.kilocode', aliases: ['kilo-code'] },
|
|
21
|
-
{ id: 'kimicode', name: 'Kimi Code', skillsDir: '.kimi-code', aliases: ['kimi', 'kimi-code'] },
|
|
22
|
-
{ id: 'codebuddy', name: 'CodeBuddy', skillsDir: '.codebuddy', aliases: ['codebuddy-code'] },
|
|
23
|
-
{ id: 'qoder', name: 'Qoder', skillsDir: '.qoder' },
|
|
24
|
-
{ id: 'trae', name: 'Trae', skillsDir: '.trae' },
|
|
25
|
-
{ id: 'cursor', name: 'Cursor', skillsDir: '.cursor' },
|
|
26
|
-
{ id: 'opencode', name: 'OpenCode', skillsDir: '.opencode', aliases: ['open-code'] },
|
|
27
|
-
{ id: 'antigravity', name: 'Antigravity', skillsDir: '.agents' },
|
|
28
|
-
{ id: 'gemini', name: 'Gemini CLI', skillsDir: '.gemini', aliases: ['gemini-cli'] },
|
|
29
|
-
{ id: 'qwen', name: 'Qwen Code', skillsDir: '.qwen', aliases: ['qwen-code'] },
|
|
30
|
-
{ id: 'kiro', name: 'Kiro', skillsDir: '.kiro' },
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
const PLATFORM_BY_ID_OR_ALIAS = new Map(
|
|
34
|
-
PLATFORMS.flatMap((platform) => [
|
|
35
|
-
[platform.id, platform],
|
|
36
|
-
...(platform.aliases ?? []).map((alias) => [alias, platform]),
|
|
37
|
-
]),
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
export function listPlatformIds() {
|
|
41
|
-
return PLATFORMS.map((platform) => platform.id);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function resolvePlatforms(ids) {
|
|
45
|
-
const resolved = [];
|
|
46
|
-
const seen = new Set();
|
|
47
|
-
const unknown = [];
|
|
48
|
-
|
|
49
|
-
for (const rawId of ids) {
|
|
50
|
-
const id = rawId.trim().toLowerCase();
|
|
51
|
-
if (!id) {
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const platform = PLATFORM_BY_ID_OR_ALIAS.get(id);
|
|
56
|
-
if (!platform) {
|
|
57
|
-
unknown.push(rawId);
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!seen.has(platform.id)) {
|
|
62
|
-
resolved.push(platform);
|
|
63
|
-
seen.add(platform.id);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (unknown.length > 0) {
|
|
68
|
-
throw new Error(`Unknown platform(s): ${unknown.join(', ')}. Available: ${listPlatformIds().join(', ')}`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (resolved.length === 0) {
|
|
72
|
-
throw new Error(`No platforms selected. Available: ${listPlatformIds().join(', ')}`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return resolved;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function parsePlatformList(value) {
|
|
79
|
-
return value
|
|
80
|
-
.split(',')
|
|
81
|
-
.map((item) => item.trim())
|
|
82
|
-
.filter(Boolean);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function getPlatformSkillsDir(platform) {
|
|
86
|
-
return path.join(platform.skillsDir, 'skills');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export async function detectPlatforms(target) {
|
|
90
|
-
const detected = [];
|
|
91
|
-
|
|
92
|
-
for (const platform of PLATFORMS) {
|
|
93
|
-
if (platform.detectionPaths?.length > 0) {
|
|
94
|
-
for (const detectionPath of platform.detectionPaths) {
|
|
95
|
-
if (await pathExists(path.join(target, detectionPath))) {
|
|
96
|
-
detected.push(platform);
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (await isDirectory(path.join(target, platform.skillsDir))) {
|
|
104
|
-
detected.push(platform);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return detected;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async function pathExists(value) {
|
|
112
|
-
try {
|
|
113
|
-
await stat(value);
|
|
114
|
-
return true;
|
|
115
|
-
} catch (error) {
|
|
116
|
-
if (error?.code === 'ENOENT') {
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
throw error;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function isDirectory(value) {
|
|
124
|
-
try {
|
|
125
|
-
return (await stat(value)).isDirectory();
|
|
126
|
-
} catch (error) {
|
|
127
|
-
if (error?.code === 'ENOENT') {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
throw error;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const PLATFORMS = [
|
|
5
|
+
{ id: 'claude', name: 'Claude Code', skillsDir: '.claude', aliases: ['claude-code'] },
|
|
6
|
+
{ id: 'codex', name: 'Codex', skillsDir: '.codex' },
|
|
7
|
+
{ id: 'windsurf', name: 'Windsurf', skillsDir: '.windsurf' },
|
|
8
|
+
{
|
|
9
|
+
id: 'github-copilot',
|
|
10
|
+
name: 'GitHub Copilot',
|
|
11
|
+
skillsDir: '.github',
|
|
12
|
+
aliases: ['copilot', 'github'],
|
|
13
|
+
detectionPaths: [
|
|
14
|
+
'.github/copilot-instructions.md',
|
|
15
|
+
'.github/instructions',
|
|
16
|
+
'.github/prompts',
|
|
17
|
+
'.github/skills',
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
{ id: 'kilocode', name: 'Kilo Code', skillsDir: '.kilocode', aliases: ['kilo-code'] },
|
|
21
|
+
{ id: 'kimicode', name: 'Kimi Code', skillsDir: '.kimi-code', aliases: ['kimi', 'kimi-code'] },
|
|
22
|
+
{ id: 'codebuddy', name: 'CodeBuddy', skillsDir: '.codebuddy', aliases: ['codebuddy-code'] },
|
|
23
|
+
{ id: 'qoder', name: 'Qoder', skillsDir: '.qoder' },
|
|
24
|
+
{ id: 'trae', name: 'Trae', skillsDir: '.trae' },
|
|
25
|
+
{ id: 'cursor', name: 'Cursor', skillsDir: '.cursor' },
|
|
26
|
+
{ id: 'opencode', name: 'OpenCode', skillsDir: '.opencode', aliases: ['open-code'] },
|
|
27
|
+
{ id: 'antigravity', name: 'Antigravity', skillsDir: '.agents' },
|
|
28
|
+
{ id: 'gemini', name: 'Gemini CLI', skillsDir: '.gemini', aliases: ['gemini-cli'] },
|
|
29
|
+
{ id: 'qwen', name: 'Qwen Code', skillsDir: '.qwen', aliases: ['qwen-code'] },
|
|
30
|
+
{ id: 'kiro', name: 'Kiro', skillsDir: '.kiro' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const PLATFORM_BY_ID_OR_ALIAS = new Map(
|
|
34
|
+
PLATFORMS.flatMap((platform) => [
|
|
35
|
+
[platform.id, platform],
|
|
36
|
+
...(platform.aliases ?? []).map((alias) => [alias, platform]),
|
|
37
|
+
]),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
export function listPlatformIds() {
|
|
41
|
+
return PLATFORMS.map((platform) => platform.id);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolvePlatforms(ids) {
|
|
45
|
+
const resolved = [];
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
const unknown = [];
|
|
48
|
+
|
|
49
|
+
for (const rawId of ids) {
|
|
50
|
+
const id = rawId.trim().toLowerCase();
|
|
51
|
+
if (!id) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const platform = PLATFORM_BY_ID_OR_ALIAS.get(id);
|
|
56
|
+
if (!platform) {
|
|
57
|
+
unknown.push(rawId);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!seen.has(platform.id)) {
|
|
62
|
+
resolved.push(platform);
|
|
63
|
+
seen.add(platform.id);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (unknown.length > 0) {
|
|
68
|
+
throw new Error(`Unknown platform(s): ${unknown.join(', ')}. Available: ${listPlatformIds().join(', ')}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (resolved.length === 0) {
|
|
72
|
+
throw new Error(`No platforms selected. Available: ${listPlatformIds().join(', ')}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return resolved;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parsePlatformList(value) {
|
|
79
|
+
return value
|
|
80
|
+
.split(',')
|
|
81
|
+
.map((item) => item.trim())
|
|
82
|
+
.filter(Boolean);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getPlatformSkillsDir(platform) {
|
|
86
|
+
return path.join(platform.skillsDir, 'skills');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function detectPlatforms(target) {
|
|
90
|
+
const detected = [];
|
|
91
|
+
|
|
92
|
+
for (const platform of PLATFORMS) {
|
|
93
|
+
if (platform.detectionPaths?.length > 0) {
|
|
94
|
+
for (const detectionPath of platform.detectionPaths) {
|
|
95
|
+
if (await pathExists(path.join(target, detectionPath))) {
|
|
96
|
+
detected.push(platform);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (await isDirectory(path.join(target, platform.skillsDir))) {
|
|
104
|
+
detected.push(platform);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return detected;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function pathExists(value) {
|
|
112
|
+
try {
|
|
113
|
+
await stat(value);
|
|
114
|
+
return true;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error?.code === 'ENOENT') {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function isDirectory(value) {
|
|
124
|
+
try {
|
|
125
|
+
return (await stat(value)).isDirectory();
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error?.code === 'ENOENT') {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|