add-skill 0.0.1 → 1.0.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/.claude/skills/vercel-deploy/SKILL.md +109 -0
- package/.claude/skills/vercel-deploy/scripts/deploy.sh +249 -0
- package/.codex/skills/vercel-deploy/SKILL.md +109 -0
- package/.codex/skills/vercel-deploy/scripts/deploy.sh +249 -0
- package/.opencode/skill/vercel-deploy/SKILL.md +109 -0
- package/.opencode/skill/vercel-deploy/scripts/deploy.sh +249 -0
- package/README.md +192 -0
- package/bun.lock +25 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +509 -0
- package/package.json +33 -8
- package/src/agents.ts +52 -0
- package/src/git.ts +83 -0
- package/src/index.ts +309 -0
- package/src/installer.ts +90 -0
- package/src/skills.ts +128 -0
- package/src/types.ts +22 -0
- package/tsconfig.json +22 -0
- package/index.js +0 -1
package/src/index.ts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import * as p from '@clack/prompts';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { parseSource, cloneRepo, cleanupTempDir } from './git.js';
|
|
7
|
+
import { discoverSkills, getSkillDisplayName } from './skills.js';
|
|
8
|
+
import { installSkillForAgent, isSkillInstalled, getInstallPath } from './installer.js';
|
|
9
|
+
import { detectInstalledAgents, agents } from './agents.js';
|
|
10
|
+
import type { Skill, AgentType } from './types.js';
|
|
11
|
+
|
|
12
|
+
const version = '1.0.0';
|
|
13
|
+
|
|
14
|
+
interface Options {
|
|
15
|
+
global?: boolean;
|
|
16
|
+
agent?: string[];
|
|
17
|
+
yes?: boolean;
|
|
18
|
+
skill?: string[];
|
|
19
|
+
list?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.name('add-skill')
|
|
24
|
+
.description('Install skills onto coding agents (OpenCode, Claude Code, Codex)')
|
|
25
|
+
.version(version)
|
|
26
|
+
.argument('<source>', 'Git repo URL, GitHub shorthand (owner/repo), or direct path to skill')
|
|
27
|
+
.option('-g, --global', 'Install skill globally (user-level) instead of project-level')
|
|
28
|
+
.option('-a, --agent <agents...>', 'Specify agents to install to (opencode, claude-code, codex)')
|
|
29
|
+
.option('-s, --skill <skills...>', 'Specify skill names to install (skip selection prompt)')
|
|
30
|
+
.option('-l, --list', 'List available skills in the repository without installing')
|
|
31
|
+
.option('-y, --yes', 'Skip confirmation prompts')
|
|
32
|
+
.action(async (source: string, options: Options) => {
|
|
33
|
+
await main(source, options);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
program.parse();
|
|
37
|
+
|
|
38
|
+
async function main(source: string, options: Options) {
|
|
39
|
+
console.log();
|
|
40
|
+
p.intro(chalk.bgCyan.black(' add-skill '));
|
|
41
|
+
|
|
42
|
+
let tempDir: string | null = null;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const spinner = p.spinner();
|
|
46
|
+
|
|
47
|
+
spinner.start('Parsing source...');
|
|
48
|
+
const parsed = parseSource(source);
|
|
49
|
+
spinner.stop(`Source: ${chalk.cyan(parsed.url)}${parsed.subpath ? ` (${parsed.subpath})` : ''}`);
|
|
50
|
+
|
|
51
|
+
spinner.start('Cloning repository...');
|
|
52
|
+
tempDir = await cloneRepo(parsed.url);
|
|
53
|
+
spinner.stop('Repository cloned');
|
|
54
|
+
|
|
55
|
+
spinner.start('Discovering skills...');
|
|
56
|
+
const skills = await discoverSkills(tempDir, parsed.subpath);
|
|
57
|
+
|
|
58
|
+
if (skills.length === 0) {
|
|
59
|
+
spinner.stop(chalk.red('No skills found'));
|
|
60
|
+
p.outro(chalk.red('No valid skills found. Skills require a SKILL.md with name and description.'));
|
|
61
|
+
await cleanup(tempDir);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
spinner.stop(`Found ${chalk.green(skills.length)} skill${skills.length > 1 ? 's' : ''}`);
|
|
66
|
+
|
|
67
|
+
if (options.list) {
|
|
68
|
+
console.log();
|
|
69
|
+
p.log.step(chalk.bold('Available Skills'));
|
|
70
|
+
for (const skill of skills) {
|
|
71
|
+
p.log.message(` ${chalk.cyan(getSkillDisplayName(skill))}`);
|
|
72
|
+
p.log.message(` ${chalk.dim(skill.description)}`);
|
|
73
|
+
}
|
|
74
|
+
console.log();
|
|
75
|
+
p.outro('Use --skill <name> to install specific skills');
|
|
76
|
+
await cleanup(tempDir);
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let selectedSkills: Skill[];
|
|
81
|
+
|
|
82
|
+
if (options.skill && options.skill.length > 0) {
|
|
83
|
+
selectedSkills = skills.filter(s =>
|
|
84
|
+
options.skill!.some(name =>
|
|
85
|
+
s.name.toLowerCase() === name.toLowerCase() ||
|
|
86
|
+
getSkillDisplayName(s).toLowerCase() === name.toLowerCase()
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (selectedSkills.length === 0) {
|
|
91
|
+
p.log.error(`No matching skills found for: ${options.skill.join(', ')}`);
|
|
92
|
+
p.log.info('Available skills:');
|
|
93
|
+
for (const s of skills) {
|
|
94
|
+
p.log.message(` - ${getSkillDisplayName(s)}`);
|
|
95
|
+
}
|
|
96
|
+
await cleanup(tempDir);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
p.log.info(`Selected ${selectedSkills.length} skill${selectedSkills.length !== 1 ? 's' : ''}: ${selectedSkills.map(s => chalk.cyan(getSkillDisplayName(s))).join(', ')}`);
|
|
101
|
+
} else if (skills.length === 1) {
|
|
102
|
+
selectedSkills = skills;
|
|
103
|
+
const firstSkill = skills[0]!;
|
|
104
|
+
p.log.info(`Skill: ${chalk.cyan(getSkillDisplayName(firstSkill))}`);
|
|
105
|
+
p.log.message(chalk.dim(firstSkill.description));
|
|
106
|
+
} else if (options.yes) {
|
|
107
|
+
selectedSkills = skills;
|
|
108
|
+
p.log.info(`Installing all ${skills.length} skills`);
|
|
109
|
+
} else {
|
|
110
|
+
const skillChoices = skills.map(s => ({
|
|
111
|
+
value: s,
|
|
112
|
+
label: getSkillDisplayName(s),
|
|
113
|
+
hint: s.description.length > 60 ? s.description.slice(0, 57) + '...' : s.description,
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
const selected = await p.multiselect({
|
|
117
|
+
message: 'Select skills to install',
|
|
118
|
+
options: skillChoices,
|
|
119
|
+
required: true,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (p.isCancel(selected)) {
|
|
123
|
+
p.cancel('Installation cancelled');
|
|
124
|
+
await cleanup(tempDir);
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
selectedSkills = selected as Skill[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let targetAgents: AgentType[];
|
|
132
|
+
|
|
133
|
+
if (options.agent && options.agent.length > 0) {
|
|
134
|
+
const validAgents = ['opencode', 'claude-code', 'codex'];
|
|
135
|
+
const invalidAgents = options.agent.filter(a => !validAgents.includes(a));
|
|
136
|
+
|
|
137
|
+
if (invalidAgents.length > 0) {
|
|
138
|
+
p.log.error(`Invalid agents: ${invalidAgents.join(', ')}`);
|
|
139
|
+
p.log.info(`Valid agents: ${validAgents.join(', ')}`);
|
|
140
|
+
await cleanup(tempDir);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
targetAgents = options.agent as AgentType[];
|
|
145
|
+
} else {
|
|
146
|
+
spinner.start('Detecting installed agents...');
|
|
147
|
+
const installedAgents = await detectInstalledAgents();
|
|
148
|
+
spinner.stop(`Detected ${installedAgents.length} agent${installedAgents.length !== 1 ? 's' : ''}`);
|
|
149
|
+
|
|
150
|
+
if (installedAgents.length === 0) {
|
|
151
|
+
if (options.yes) {
|
|
152
|
+
targetAgents = ['opencode', 'claude-code', 'codex'];
|
|
153
|
+
p.log.info('Installing to all agents (none detected)');
|
|
154
|
+
} else {
|
|
155
|
+
p.log.warn('No coding agents detected. You can still install skills.');
|
|
156
|
+
|
|
157
|
+
const allAgentChoices = Object.entries(agents).map(([key, config]) => ({
|
|
158
|
+
value: key as AgentType,
|
|
159
|
+
label: config.displayName,
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
const selected = await p.multiselect({
|
|
163
|
+
message: 'Select agents to install skills to',
|
|
164
|
+
options: allAgentChoices,
|
|
165
|
+
required: true,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (p.isCancel(selected)) {
|
|
169
|
+
p.cancel('Installation cancelled');
|
|
170
|
+
await cleanup(tempDir);
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
targetAgents = selected as AgentType[];
|
|
175
|
+
}
|
|
176
|
+
} else if (installedAgents.length === 1 || options.yes) {
|
|
177
|
+
targetAgents = installedAgents;
|
|
178
|
+
if (installedAgents.length === 1) {
|
|
179
|
+
const firstAgent = installedAgents[0]!;
|
|
180
|
+
p.log.info(`Installing to: ${chalk.cyan(agents[firstAgent].displayName)}`);
|
|
181
|
+
} else {
|
|
182
|
+
p.log.info(`Installing to: ${installedAgents.map(a => chalk.cyan(agents[a].displayName)).join(', ')}`);
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
const agentChoices = installedAgents.map(a => ({
|
|
186
|
+
value: a,
|
|
187
|
+
label: agents[a].displayName,
|
|
188
|
+
hint: `${options.global ? agents[a].globalSkillsDir : agents[a].skillsDir}`,
|
|
189
|
+
}));
|
|
190
|
+
|
|
191
|
+
const selected = await p.multiselect({
|
|
192
|
+
message: 'Select agents to install skills to',
|
|
193
|
+
options: agentChoices,
|
|
194
|
+
required: true,
|
|
195
|
+
initialValues: installedAgents,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (p.isCancel(selected)) {
|
|
199
|
+
p.cancel('Installation cancelled');
|
|
200
|
+
await cleanup(tempDir);
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
targetAgents = selected as AgentType[];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let installGlobally = options.global ?? false;
|
|
209
|
+
|
|
210
|
+
if (options.global === undefined && !options.yes) {
|
|
211
|
+
const scope = await p.select({
|
|
212
|
+
message: 'Installation scope',
|
|
213
|
+
options: [
|
|
214
|
+
{ value: false, label: 'Project', hint: 'Install in current directory (committed with your project)' },
|
|
215
|
+
{ value: true, label: 'Global', hint: 'Install in home directory (available across all projects)' },
|
|
216
|
+
],
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (p.isCancel(scope)) {
|
|
220
|
+
p.cancel('Installation cancelled');
|
|
221
|
+
await cleanup(tempDir);
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
installGlobally = scope as boolean;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
console.log();
|
|
229
|
+
p.log.step(chalk.bold('Installation Summary'));
|
|
230
|
+
|
|
231
|
+
for (const skill of selectedSkills) {
|
|
232
|
+
p.log.message(` ${chalk.cyan(getSkillDisplayName(skill))}`);
|
|
233
|
+
for (const agent of targetAgents) {
|
|
234
|
+
const path = getInstallPath(skill.name, agent, { global: installGlobally });
|
|
235
|
+
const installed = await isSkillInstalled(skill.name, agent, { global: installGlobally });
|
|
236
|
+
const status = installed ? chalk.yellow(' (will overwrite)') : '';
|
|
237
|
+
p.log.message(` ${chalk.dim('→')} ${agents[agent].displayName}: ${chalk.dim(path)}${status}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
console.log();
|
|
241
|
+
|
|
242
|
+
if (!options.yes) {
|
|
243
|
+
const confirmed = await p.confirm({ message: 'Proceed with installation?' });
|
|
244
|
+
|
|
245
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
246
|
+
p.cancel('Installation cancelled');
|
|
247
|
+
await cleanup(tempDir);
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
spinner.start('Installing skills...');
|
|
253
|
+
|
|
254
|
+
const results: { skill: string; agent: string; success: boolean; path: string; error?: string }[] = [];
|
|
255
|
+
|
|
256
|
+
for (const skill of selectedSkills) {
|
|
257
|
+
for (const agent of targetAgents) {
|
|
258
|
+
const result = await installSkillForAgent(skill, agent, { global: installGlobally });
|
|
259
|
+
results.push({
|
|
260
|
+
skill: getSkillDisplayName(skill),
|
|
261
|
+
agent: agents[agent].displayName,
|
|
262
|
+
...result,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
spinner.stop('Installation complete');
|
|
268
|
+
|
|
269
|
+
console.log();
|
|
270
|
+
const successful = results.filter(r => r.success);
|
|
271
|
+
const failed = results.filter(r => !r.success);
|
|
272
|
+
|
|
273
|
+
if (successful.length > 0) {
|
|
274
|
+
p.log.success(chalk.green(`Successfully installed ${successful.length} skill${successful.length !== 1 ? 's' : ''}`));
|
|
275
|
+
for (const r of successful) {
|
|
276
|
+
p.log.message(` ${chalk.green('✓')} ${r.skill} → ${r.agent}`);
|
|
277
|
+
p.log.message(` ${chalk.dim(r.path)}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (failed.length > 0) {
|
|
282
|
+
console.log();
|
|
283
|
+
p.log.error(chalk.red(`Failed to install ${failed.length} skill${failed.length !== 1 ? 's' : ''}`));
|
|
284
|
+
for (const r of failed) {
|
|
285
|
+
p.log.message(` ${chalk.red('✗')} ${r.skill} → ${r.agent}`);
|
|
286
|
+
p.log.message(` ${chalk.dim(r.error)}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
console.log();
|
|
291
|
+
p.outro(chalk.green('Done!'));
|
|
292
|
+
} catch (error) {
|
|
293
|
+
p.log.error(error instanceof Error ? error.message : 'Unknown error occurred');
|
|
294
|
+
p.outro(chalk.red('Installation failed'));
|
|
295
|
+
process.exit(1);
|
|
296
|
+
} finally {
|
|
297
|
+
await cleanup(tempDir);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function cleanup(tempDir: string | null) {
|
|
302
|
+
if (tempDir) {
|
|
303
|
+
try {
|
|
304
|
+
await cleanupTempDir(tempDir);
|
|
305
|
+
} catch {
|
|
306
|
+
// Ignore cleanup errors
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
package/src/installer.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { mkdir, cp, access, readdir } from 'fs/promises';
|
|
2
|
+
import { join, basename } from 'path';
|
|
3
|
+
import type { Skill, AgentType } from './types.js';
|
|
4
|
+
import { agents } from './agents.js';
|
|
5
|
+
|
|
6
|
+
interface InstallResult {
|
|
7
|
+
success: boolean;
|
|
8
|
+
path: string;
|
|
9
|
+
error?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function installSkillForAgent(
|
|
13
|
+
skill: Skill,
|
|
14
|
+
agentType: AgentType,
|
|
15
|
+
options: { global?: boolean; cwd?: string } = {}
|
|
16
|
+
): Promise<InstallResult> {
|
|
17
|
+
const agent = agents[agentType];
|
|
18
|
+
const skillName = skill.name || basename(skill.path);
|
|
19
|
+
|
|
20
|
+
const targetBase = options.global
|
|
21
|
+
? agent.globalSkillsDir
|
|
22
|
+
: join(options.cwd || process.cwd(), agent.skillsDir);
|
|
23
|
+
|
|
24
|
+
const targetDir = join(targetBase, skillName);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
await mkdir(targetDir, { recursive: true });
|
|
28
|
+
await copyDirectory(skill.path, targetDir);
|
|
29
|
+
|
|
30
|
+
return { success: true, path: targetDir };
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return {
|
|
33
|
+
success: false,
|
|
34
|
+
path: targetDir,
|
|
35
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function copyDirectory(src: string, dest: string): Promise<void> {
|
|
41
|
+
await mkdir(dest, { recursive: true });
|
|
42
|
+
|
|
43
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
44
|
+
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
const srcPath = join(src, entry.name);
|
|
47
|
+
const destPath = join(dest, entry.name);
|
|
48
|
+
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
await copyDirectory(srcPath, destPath);
|
|
51
|
+
} else {
|
|
52
|
+
await cp(srcPath, destPath);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function isSkillInstalled(
|
|
58
|
+
skillName: string,
|
|
59
|
+
agentType: AgentType,
|
|
60
|
+
options: { global?: boolean; cwd?: string } = {}
|
|
61
|
+
): Promise<boolean> {
|
|
62
|
+
const agent = agents[agentType];
|
|
63
|
+
|
|
64
|
+
const targetBase = options.global
|
|
65
|
+
? agent.globalSkillsDir
|
|
66
|
+
: join(options.cwd || process.cwd(), agent.skillsDir);
|
|
67
|
+
|
|
68
|
+
const skillDir = join(targetBase, skillName);
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await access(skillDir);
|
|
72
|
+
return true;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getInstallPath(
|
|
79
|
+
skillName: string,
|
|
80
|
+
agentType: AgentType,
|
|
81
|
+
options: { global?: boolean; cwd?: string } = {}
|
|
82
|
+
): string {
|
|
83
|
+
const agent = agents[agentType];
|
|
84
|
+
|
|
85
|
+
const targetBase = options.global
|
|
86
|
+
? agent.globalSkillsDir
|
|
87
|
+
: join(options.cwd || process.cwd(), agent.skillsDir);
|
|
88
|
+
|
|
89
|
+
return join(targetBase, skillName);
|
|
90
|
+
}
|
package/src/skills.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from 'fs/promises';
|
|
2
|
+
import { join, basename, dirname } from 'path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import type { Skill } from './types.js';
|
|
5
|
+
|
|
6
|
+
const SKIP_DIRS = ['node_modules', '.git', 'dist', 'build', '__pycache__'];
|
|
7
|
+
|
|
8
|
+
async function hasSkillMd(dir: string): Promise<boolean> {
|
|
9
|
+
try {
|
|
10
|
+
const skillPath = join(dir, 'SKILL.md');
|
|
11
|
+
const stats = await stat(skillPath);
|
|
12
|
+
return stats.isFile();
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function parseSkillMd(skillMdPath: string): Promise<Skill | null> {
|
|
19
|
+
try {
|
|
20
|
+
const content = await readFile(skillMdPath, 'utf-8');
|
|
21
|
+
const { data } = matter(content);
|
|
22
|
+
|
|
23
|
+
if (!data.name || !data.description) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
name: data.name,
|
|
29
|
+
description: data.description,
|
|
30
|
+
path: dirname(skillMdPath),
|
|
31
|
+
metadata: data.metadata,
|
|
32
|
+
};
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function findSkillDirs(dir: string, depth = 0, maxDepth = 5): Promise<string[]> {
|
|
39
|
+
const skillDirs: string[] = [];
|
|
40
|
+
|
|
41
|
+
if (depth > maxDepth) return skillDirs;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
if (await hasSkillMd(dir)) {
|
|
45
|
+
skillDirs.push(dir);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
49
|
+
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (entry.isDirectory() && !SKIP_DIRS.includes(entry.name)) {
|
|
52
|
+
const subDirs = await findSkillDirs(join(dir, entry.name), depth + 1, maxDepth);
|
|
53
|
+
skillDirs.push(...subDirs);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Ignore errors
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return skillDirs;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function discoverSkills(basePath: string, subpath?: string): Promise<Skill[]> {
|
|
64
|
+
const skills: Skill[] = [];
|
|
65
|
+
const seenNames = new Set<string>();
|
|
66
|
+
const searchPath = subpath ? join(basePath, subpath) : basePath;
|
|
67
|
+
|
|
68
|
+
// If pointing directly at a skill, return just that
|
|
69
|
+
if (await hasSkillMd(searchPath)) {
|
|
70
|
+
const skill = await parseSkillMd(join(searchPath, 'SKILL.md'));
|
|
71
|
+
if (skill) {
|
|
72
|
+
skills.push(skill);
|
|
73
|
+
return skills;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Search common skill locations first
|
|
78
|
+
const prioritySearchDirs = [
|
|
79
|
+
searchPath,
|
|
80
|
+
join(searchPath, 'skills'),
|
|
81
|
+
join(searchPath, 'skills/.curated'),
|
|
82
|
+
join(searchPath, 'skills/.experimental'),
|
|
83
|
+
join(searchPath, 'skills/.system'),
|
|
84
|
+
join(searchPath, '.codex/skills'),
|
|
85
|
+
join(searchPath, '.claude/skills'),
|
|
86
|
+
join(searchPath, '.opencode/skill'),
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
for (const dir of prioritySearchDirs) {
|
|
90
|
+
try {
|
|
91
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
92
|
+
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
if (entry.isDirectory()) {
|
|
95
|
+
const skillDir = join(dir, entry.name);
|
|
96
|
+
if (await hasSkillMd(skillDir)) {
|
|
97
|
+
const skill = await parseSkillMd(join(skillDir, 'SKILL.md'));
|
|
98
|
+
if (skill && !seenNames.has(skill.name)) {
|
|
99
|
+
skills.push(skill);
|
|
100
|
+
seenNames.add(skill.name);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Directory doesn't exist
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Fall back to recursive search if nothing found
|
|
111
|
+
if (skills.length === 0) {
|
|
112
|
+
const allSkillDirs = await findSkillDirs(searchPath);
|
|
113
|
+
|
|
114
|
+
for (const skillDir of allSkillDirs) {
|
|
115
|
+
const skill = await parseSkillMd(join(skillDir, 'SKILL.md'));
|
|
116
|
+
if (skill && !seenNames.has(skill.name)) {
|
|
117
|
+
skills.push(skill);
|
|
118
|
+
seenNames.add(skill.name);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return skills;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getSkillDisplayName(skill: Skill): string {
|
|
127
|
+
return skill.name || basename(skill.path);
|
|
128
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type AgentType = 'opencode' | 'claude-code' | 'codex';
|
|
2
|
+
|
|
3
|
+
export interface Skill {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
path: string;
|
|
7
|
+
metadata?: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface AgentConfig {
|
|
11
|
+
name: string;
|
|
12
|
+
displayName: string;
|
|
13
|
+
skillsDir: string;
|
|
14
|
+
globalSkillsDir: string;
|
|
15
|
+
detectInstalled: () => Promise<boolean>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ParsedSource {
|
|
19
|
+
type: 'github' | 'gitlab' | 'git';
|
|
20
|
+
url: string;
|
|
21
|
+
subpath?: string;
|
|
22
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext"],
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleDetection": "force",
|
|
7
|
+
"allowJs": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"noFallthroughCasesInSwitch": true,
|
|
14
|
+
"noUncheckedIndexedAccess": true,
|
|
15
|
+
"noUnusedLocals": false,
|
|
16
|
+
"noUnusedParameters": false,
|
|
17
|
+
"noPropertyAccessFromIndexSignature": false,
|
|
18
|
+
"esModuleInterop": true
|
|
19
|
+
},
|
|
20
|
+
"include": ["src/**/*"],
|
|
21
|
+
"exclude": ["node_modules", "dist"]
|
|
22
|
+
}
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
console.log("Hello World");
|