fetch-skill 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Shane Holloman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # fetch-skill
2
+
3
+ Install agent skills onto your coding agents from any git repository.
4
+
5
+ <!-- agent-list:start -->
6
+ Supports **Opencode**, **Claude Code**, **Codex**, **Cursor**, and [11 more](#available-agents).
7
+ <!-- agent-list:end -->
8
+
9
+ ## Quick Start
10
+
11
+ ```bash
12
+ npx fetch-skill shaneholloman/agent-skills
13
+ ```
14
+
15
+ ## What are Agent Skills?
16
+
17
+ Agent skills are reusable instruction sets that extend your coding agent's capabilities. They're defined in `SKILL.md` files with YAML frontmatter containing a `name` and `description`.
18
+
19
+ Skills let agents perform specialized tasks like:
20
+
21
+ - Generating release notes from git history
22
+ - Creating PRs following your team's conventions
23
+ - Integrating with external tools (Linear, Notion, etc.)
24
+
25
+ ## Usage
26
+
27
+ ### Source Formats
28
+
29
+ The `<source>` argument accepts multiple formats:
30
+
31
+ ```bash
32
+ # GitHub shorthand
33
+ npx fetch-skill shaneholloman/agent-skills
34
+
35
+ # Full GitHub URL
36
+ npx fetch-skill https://github.com/shaneholloman/agent-skills
37
+
38
+ # Direct path to a skill in a repo
39
+ npx fetch-skill https://github.com/shaneholloman/agent-skills/tree/main/skills/frontend-design
40
+
41
+ # GitLab URL
42
+ npx fetch-skill https://gitlab.com/org/repo
43
+
44
+ # Any git URL
45
+ npx fetch-skill git@github.com:shaneholloman/agent-skills.git
46
+ ```
47
+
48
+ ### Options
49
+
50
+ | Option | Description |
51
+ | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
52
+ | `-g, --global` | Install to user directory instead of project |
53
+ | `-a, --agent <agents...>` | <!-- agent-names:start -->Target specific agents (e.g., `claude-code`, `codex`). See [Available Agents](#available-agents)<!-- agent-names:end --> |
54
+ | `-s, --skill <skills...>` | Install specific skills by name |
55
+ | `-l, --list` | List available skills without installing |
56
+ | `-y, --yes` | Skip all confirmation prompts |
57
+ | `-V, --version` | Show version number |
58
+ | `-h, --help` | Show help |
59
+
60
+ ### Examples
61
+
62
+ ```bash
63
+ # List skills in a repository
64
+ npx fetch-skill shaneholloman/agent-skills --list
65
+
66
+ # Install multiple specific skills
67
+ npx fetch-skill shaneholloman/agent-skills --skill frontend-design --skill skill-creator
68
+
69
+ # Install to specific agents
70
+ npx fetch-skill shaneholloman/agent-skills -a claude-code -a opencode
71
+
72
+ # Non-interactive installation (CI/CD friendly)
73
+ npx fetch-skill shaneholloman/agent-skills --skill frontend-design -g -a claude-code -y
74
+
75
+ # Install all skills from a repo
76
+ npx fetch-skill shaneholloman/agent-skills -y -g
77
+ ```
78
+
79
+ ## Available Agents
80
+
81
+ Skills can be installed to any of these supported agents. Use `-g, --global` to install to the global path instead of project-level.
82
+
83
+ <!-- available-agents:start -->
84
+ | Agent | Project Path | Global Path |
85
+ | -------------- | ------------------- | ------------------------------- |
86
+ | OpenCode | `.opencode/skill/` | `~/.config/opencode/skill/` |
87
+ | Claude Code | `.claude/skills/` | `~/.claude/skills/` |
88
+ | Codex | `.codex/skills/` | `~/.codex/skills/` |
89
+ | Cursor | `.cursor/skills/` | `~/.cursor/skills/` |
90
+ | Amp | `.agents/skills/` | `~/.config/agents/skills/` |
91
+ | Kilo Code | `.kilocode/skills/` | `~/.kilocode/skills/` |
92
+ | Roo Code | `.roo/skills/` | `~/.roo/skills/` |
93
+ | Goose | `.goose/skills/` | `~/.config/goose/skills/` |
94
+ | Gemini CLI | `.gemini/skills/` | `~/.gemini/skills/` |
95
+ | Antigravity | `.agent/skills/` | `~/.gemini/antigravity/skills/` |
96
+ | GitHub Copilot | `.github/skills/` | `~/.copilot/skills/` |
97
+ | Clawdbot | `skills/` | `~/.clawdbot/skills/` |
98
+ | Droid | `.factory/skills/` | `~/.factory/skills/` |
99
+ | Gemini CLI | `.gemini/skills/` | `~/.gemini/skills/` |
100
+ | Windsurf | `.windsurf/skills/` | `~/.codeium/windsurf/skills/` |
101
+ <!-- available-agents:end -->
102
+
103
+ ## Agent Detection
104
+
105
+ The CLI automatically detects which coding agents you have installed by checking for their configuration directories. If none are detected, you'll be prompted to select which agents to install to.
106
+
107
+ ## Creating Skills
108
+
109
+ Skills are directories containing a `SKILL.md` file with YAML frontmatter:
110
+
111
+ ```markdown
112
+ ---
113
+ name: my-skill
114
+ description: What this skill does and when to use it
115
+ ---
116
+
117
+ # My Skill
118
+
119
+ Instructions for the agent to follow when this skill is activated.
120
+
121
+ ## When to Use
122
+
123
+ Describe the scenarios where this skill should be used.
124
+
125
+ ## Steps
126
+
127
+ 1. First, do this
128
+ 2. Then, do that
129
+ ```
130
+
131
+ ### Required Fields
132
+
133
+ - `name`: Unique identifier (lowercase, hyphens allowed)
134
+ - `description`: Brief explanation of what the skill does
135
+
136
+ ### Skill Discovery
137
+
138
+ The CLI searches for skills in these locations within a repository:
139
+
140
+ <!-- skill-discovery:start -->
141
+ - Root directory (if it contains `SKILL.md`)
142
+ - `skills/`
143
+ - `skills/.curated/`
144
+ - `skills/.experimental/`
145
+ - `skills/.system/`
146
+ - `.opencode/skill/`
147
+ - `.claude/skills/`
148
+ - `.codex/skills/`
149
+ - `.cursor/skills/`
150
+ - `.agents/skills/`
151
+ - `.kilocode/skills/`
152
+ - `.roo/skills/`
153
+ - `.goose/skills/`
154
+ - `.gemini/skills/`
155
+ - `.agent/skills/`
156
+ - `.github/skills/`
157
+ - `./skills/`
158
+ - `.factory/skills/`
159
+ - `.windsurf/skills/`
160
+ <!-- skill-discovery:end -->
161
+
162
+ If no skills are found in standard locations, a recursive search is performed.
163
+
164
+ ## Compatibility
165
+
166
+ Skills are generally compatible across agents since they follow a shared [Agent Skills specification](https://agentskills.io). However, some features may be agent-specific:
167
+
168
+ | Feature | OpenCode | Claude Code | Codex | Cursor | Antigravity | Roo Code | Github Copilot | Amp | Clawdbot |
169
+ | --------------- | -------- | ----------- | ----- | ------ | ----------- | -------- | -------------- | --- | -------- |
170
+ | Basic skills | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
171
+ | `allowed-tools` | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
172
+ | `context: fork` | No | Yes | No | No | No | No | No | No | No |
173
+ | Hooks | No | Yes | No | No | No | No | No | No | No |
174
+
175
+ ## Troubleshooting
176
+
177
+ ### "No skills found"
178
+
179
+ Ensure the repository contains valid `SKILL.md` files with both `name` and `description` in the frontmatter.
180
+
181
+ ### Skill not loading in agent
182
+
183
+ - Verify the skill was installed to the correct path
184
+ - Check the agent's documentation for skill loading requirements
185
+ - Ensure the `SKILL.md` frontmatter is valid YAML
186
+
187
+ ### Permission errors
188
+
189
+ Ensure you have write access to the target directory.
190
+
191
+ ## Related Links
192
+
193
+ - [Vercel Agent Skills Repository](https://github.com/shaneholloman/agent-skills)
194
+ - [Agent Skills Specification](https://agentskills.io)
195
+ - [OpenCode Skills Documentation](https://opencode.ai/docs/skills)
196
+ - [Claude Code Skills Documentation](https://code.claude.com/docs/en/skills)
197
+ - [Codex Skills Documentation](https://developers.openai.com/codex/skills)
198
+ - [Cursor Skills Documentation](https://cursor.com/docs/context/skills)
199
+ - [Gemini CLI Skills Documentation](https://geminicli.com/docs/cli/skills/)
200
+ - [Amp Skills Documentation](https://ampcode.com/manual#agent-skills)
201
+ - [Antigravity Skills Documentation](https://antigravity.google/docs/skills)
202
+ - [GitHub Copilot Agent Skills](https://docs.github.com/en/copilot/concepts/agents/about-agent-skills)
203
+ - [Roo Code Skills Documentation](https://docs.roocode.com/features/skills)
204
+ - [Clawdbot Skills Documentation](https://docs.clawd.bot/tools/skills)
205
+
206
+ ## License
207
+
208
+ MIT
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,787 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import * as p from "@clack/prompts";
5
+ import chalk from "chalk";
6
+ import { program } from "commander";
7
+
8
+ // package.json
9
+ var package_default = {
10
+ name: "fetch-skill",
11
+ version: "1.0.0",
12
+ description: "Install agent skills onto coding agents (OpenCode, Claude Code, Codex, Cursor)",
13
+ type: "module",
14
+ bin: {
15
+ "fetch-skill": "dist/index.js"
16
+ },
17
+ files: [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ scripts: {
23
+ build: "tsup src/index.ts --format esm --dts --clean",
24
+ dev: "tsx src/index.ts",
25
+ lint: "biome lint --write .",
26
+ format: "biome format --write .",
27
+ check: "biome check --write .",
28
+ prepublishOnly: "npm run build",
29
+ "update-readme": "tsx scripts/update-readme.ts"
30
+ },
31
+ keywords: [
32
+ "cli",
33
+ "skills",
34
+ "opencode",
35
+ "claude-code",
36
+ "codex",
37
+ "cursor",
38
+ "amp",
39
+ "antigravity",
40
+ "roo-code",
41
+ "ai-agents"
42
+ ],
43
+ repository: {
44
+ type: "git",
45
+ url: "git+https://github.com/shaneholloman/fetch-skill.git"
46
+ },
47
+ homepage: "https://github.com/shaneholloman/fetch-skill#readme",
48
+ bugs: {
49
+ url: "https://github.com/shaneholloman/fetch-skill/issues"
50
+ },
51
+ author: "Shane Holloman",
52
+ license: "MIT",
53
+ dependencies: {
54
+ "@clack/prompts": "^0.11.0",
55
+ chalk: "^5.6.2",
56
+ commander: "^14.0.2",
57
+ "gray-matter": "^4.0.3",
58
+ "simple-git": "^3.30.0"
59
+ },
60
+ devDependencies: {
61
+ "@biomejs/biome": "^2.3.11",
62
+ "@types/node": "^25.0.9",
63
+ tsup: "^8.5.1",
64
+ tsx: "^4.21.0",
65
+ typescript: "^5.9.3"
66
+ },
67
+ engines: {
68
+ node: ">=22"
69
+ }
70
+ };
71
+
72
+ // src/agents.ts
73
+ import { existsSync } from "fs";
74
+ import { homedir } from "os";
75
+ import { join } from "path";
76
+ var home = homedir();
77
+ var agents = {
78
+ opencode: {
79
+ name: "opencode",
80
+ displayName: "OpenCode",
81
+ skillsDir: ".opencode/skill",
82
+ globalSkillsDir: join(home, ".config/opencode/skill"),
83
+ detectInstalled: async () => {
84
+ return existsSync(join(home, ".config/opencode")) || existsSync(join(home, ".claude/skills"));
85
+ }
86
+ },
87
+ "claude-code": {
88
+ name: "claude-code",
89
+ displayName: "Claude Code",
90
+ skillsDir: ".claude/skills",
91
+ globalSkillsDir: join(home, ".claude/skills"),
92
+ detectInstalled: async () => {
93
+ return existsSync(join(home, ".claude"));
94
+ }
95
+ },
96
+ codex: {
97
+ name: "codex",
98
+ displayName: "Codex",
99
+ skillsDir: ".codex/skills",
100
+ globalSkillsDir: join(home, ".codex/skills"),
101
+ detectInstalled: async () => {
102
+ return existsSync(join(home, ".codex"));
103
+ }
104
+ },
105
+ cursor: {
106
+ name: "cursor",
107
+ displayName: "Cursor",
108
+ skillsDir: ".cursor/skills",
109
+ globalSkillsDir: join(home, ".cursor/skills"),
110
+ detectInstalled: async () => {
111
+ return existsSync(join(home, ".cursor"));
112
+ }
113
+ },
114
+ amp: {
115
+ name: "amp",
116
+ displayName: "Amp",
117
+ skillsDir: ".agents/skills",
118
+ globalSkillsDir: join(home, ".config/agents/skills"),
119
+ detectInstalled: async () => {
120
+ return existsSync(join(home, ".config/amp"));
121
+ }
122
+ },
123
+ kilo: {
124
+ name: "kilo",
125
+ displayName: "Kilo Code",
126
+ skillsDir: ".kilocode/skills",
127
+ globalSkillsDir: join(home, ".kilocode/skills"),
128
+ detectInstalled: async () => {
129
+ return existsSync(join(home, ".kilocode"));
130
+ }
131
+ },
132
+ roo: {
133
+ name: "roo",
134
+ displayName: "Roo Code",
135
+ skillsDir: ".roo/skills",
136
+ globalSkillsDir: join(home, ".roo/skills"),
137
+ detectInstalled: async () => {
138
+ return existsSync(join(home, ".roo"));
139
+ }
140
+ },
141
+ goose: {
142
+ name: "goose",
143
+ displayName: "Goose",
144
+ skillsDir: ".goose/skills",
145
+ globalSkillsDir: join(home, ".config/goose/skills"),
146
+ detectInstalled: async () => {
147
+ return existsSync(join(home, ".config/goose"));
148
+ }
149
+ },
150
+ "gemini-cli": {
151
+ name: "gemini-cli",
152
+ displayName: "Gemini CLI",
153
+ skillsDir: ".gemini/skills",
154
+ globalSkillsDir: join(home, ".gemini/skills"),
155
+ detectInstalled: async () => {
156
+ return existsSync(join(home, ".gemini"));
157
+ }
158
+ },
159
+ antigravity: {
160
+ name: "antigravity",
161
+ displayName: "Antigravity",
162
+ skillsDir: ".agent/skills",
163
+ globalSkillsDir: join(home, ".gemini/antigravity/skills"),
164
+ detectInstalled: async () => {
165
+ return existsSync(join(process.cwd(), ".agent")) || existsSync(join(home, ".gemini/antigravity"));
166
+ }
167
+ },
168
+ "github-copilot": {
169
+ name: "github-copilot",
170
+ displayName: "GitHub Copilot",
171
+ skillsDir: ".github/skills",
172
+ globalSkillsDir: join(home, ".copilot/skills"),
173
+ detectInstalled: async () => {
174
+ return existsSync(join(process.cwd(), ".github")) || existsSync(join(home, ".copilot"));
175
+ }
176
+ },
177
+ clawdbot: {
178
+ name: "clawdbot",
179
+ displayName: "Clawdbot",
180
+ skillsDir: "skills",
181
+ globalSkillsDir: join(home, ".clawdbot/skills"),
182
+ detectInstalled: async () => {
183
+ return existsSync(join(home, ".clawdbot"));
184
+ }
185
+ },
186
+ droid: {
187
+ name: "droid",
188
+ displayName: "Droid",
189
+ skillsDir: ".factory/skills",
190
+ globalSkillsDir: join(home, ".factory/skills"),
191
+ detectInstalled: async () => {
192
+ return existsSync(join(home, ".factory/skills"));
193
+ }
194
+ },
195
+ gemini: {
196
+ name: "gemini",
197
+ displayName: "Gemini CLI",
198
+ skillsDir: ".gemini/skills",
199
+ globalSkillsDir: join(home, ".gemini/skills"),
200
+ detectInstalled: async () => {
201
+ return existsSync(join(home, ".gemini"));
202
+ }
203
+ },
204
+ windsurf: {
205
+ name: "windsurf",
206
+ displayName: "Windsurf",
207
+ skillsDir: ".windsurf/skills",
208
+ globalSkillsDir: join(home, ".codeium/windsurf/skills"),
209
+ detectInstalled: async () => {
210
+ return existsSync(join(home, ".codeium/windsurf"));
211
+ }
212
+ }
213
+ };
214
+ async function detectInstalledAgents() {
215
+ const installed = [];
216
+ for (const [type, config] of Object.entries(agents)) {
217
+ if (await config.detectInstalled()) {
218
+ installed.push(type);
219
+ }
220
+ }
221
+ return installed;
222
+ }
223
+
224
+ // src/git.ts
225
+ import { mkdtemp, rm } from "fs/promises";
226
+ import { tmpdir } from "os";
227
+ import { join as join2, normalize, resolve, sep } from "path";
228
+ import simpleGit from "simple-git";
229
+ function parseSource(input) {
230
+ const githubTreeMatch = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/);
231
+ if (githubTreeMatch) {
232
+ const [, owner, repo, , subpath] = githubTreeMatch;
233
+ return {
234
+ type: "github",
235
+ url: `https://github.com/${owner}/${repo}.git`,
236
+ subpath
237
+ };
238
+ }
239
+ const githubRepoMatch = input.match(/github\.com\/([^/]+)\/([^/]+)/);
240
+ if (githubRepoMatch) {
241
+ const [, owner, repo] = githubRepoMatch;
242
+ const cleanRepo = repo.replace(/\.git$/, "");
243
+ return {
244
+ type: "github",
245
+ url: `https://github.com/${owner}/${cleanRepo}.git`
246
+ };
247
+ }
248
+ const gitlabTreeMatch = input.match(/gitlab\.com\/([^/]+)\/([^/]+)\/-\/tree\/([^/]+)\/(.+)/);
249
+ if (gitlabTreeMatch) {
250
+ const [, owner, repo, , subpath] = gitlabTreeMatch;
251
+ return {
252
+ type: "gitlab",
253
+ url: `https://gitlab.com/${owner}/${repo}.git`,
254
+ subpath
255
+ };
256
+ }
257
+ const gitlabRepoMatch = input.match(/gitlab\.com\/([^/]+)\/([^/]+)/);
258
+ if (gitlabRepoMatch) {
259
+ const [, owner, repo] = gitlabRepoMatch;
260
+ const cleanRepo = repo.replace(/\.git$/, "");
261
+ return {
262
+ type: "gitlab",
263
+ url: `https://gitlab.com/${owner}/${cleanRepo}.git`
264
+ };
265
+ }
266
+ const shorthandMatch = input.match(/^([^/]+)\/([^/]+)(?:\/(.+))?$/);
267
+ if (shorthandMatch && !input.includes(":")) {
268
+ const [, owner, repo, subpath] = shorthandMatch;
269
+ return {
270
+ type: "github",
271
+ url: `https://github.com/${owner}/${repo}.git`,
272
+ subpath
273
+ };
274
+ }
275
+ return {
276
+ type: "git",
277
+ url: input
278
+ };
279
+ }
280
+ async function cloneRepo(url) {
281
+ const tempDir = await mkdtemp(join2(tmpdir(), "fetch-skill-"));
282
+ const git = simpleGit();
283
+ await git.clone(url, tempDir, ["--depth", "1"]);
284
+ return tempDir;
285
+ }
286
+ async function cleanupTempDir(dir) {
287
+ const normalizedDir = normalize(resolve(dir));
288
+ const normalizedTmpDir = normalize(resolve(tmpdir()));
289
+ if (!normalizedDir.startsWith(normalizedTmpDir + sep) && normalizedDir !== normalizedTmpDir) {
290
+ throw new Error("Attempted to clean up directory outside of temp directory");
291
+ }
292
+ await rm(dir, { recursive: true, force: true });
293
+ }
294
+
295
+ // src/installer.ts
296
+ import { access, cp, mkdir, readdir } from "fs/promises";
297
+ import { basename, join as join3, normalize as normalize2, resolve as resolve2, sep as sep2 } from "path";
298
+ function sanitizeName(name) {
299
+ let sanitized = name.replace(/[/\\:\0]/g, "");
300
+ sanitized = sanitized.replace(/^[.\s]+|[.\s]+$/g, "");
301
+ sanitized = sanitized.replace(/^\.+/, "");
302
+ if (!sanitized || sanitized.length === 0) {
303
+ sanitized = "unnamed-skill";
304
+ }
305
+ if (sanitized.length > 255) {
306
+ sanitized = sanitized.substring(0, 255);
307
+ }
308
+ return sanitized;
309
+ }
310
+ function isPathSafe(basePath, targetPath) {
311
+ const normalizedBase = normalize2(resolve2(basePath));
312
+ const normalizedTarget = normalize2(resolve2(targetPath));
313
+ return normalizedTarget.startsWith(normalizedBase + sep2) || normalizedTarget === normalizedBase;
314
+ }
315
+ async function installSkillForAgent(skill, agentType, options = {}) {
316
+ const agent = agents[agentType];
317
+ const rawSkillName = skill.name || basename(skill.path);
318
+ const skillName = sanitizeName(rawSkillName);
319
+ const targetBase = options.global ? agent.globalSkillsDir : join3(options.cwd || process.cwd(), agent.skillsDir);
320
+ const targetDir = join3(targetBase, skillName);
321
+ if (!isPathSafe(targetBase, targetDir)) {
322
+ return {
323
+ success: false,
324
+ path: targetDir,
325
+ error: "Invalid skill name: potential path traversal detected"
326
+ };
327
+ }
328
+ try {
329
+ await mkdir(targetDir, { recursive: true });
330
+ await copyDirectory(skill.path, targetDir);
331
+ return { success: true, path: targetDir };
332
+ } catch (error) {
333
+ return {
334
+ success: false,
335
+ path: targetDir,
336
+ error: error instanceof Error ? error.message : "Unknown error"
337
+ };
338
+ }
339
+ }
340
+ var EXCLUDE_FILES = /* @__PURE__ */ new Set(["README.md", "metadata.json"]);
341
+ var isExcluded = (name) => {
342
+ if (EXCLUDE_FILES.has(name)) return true;
343
+ if (name.startsWith("_")) return true;
344
+ return false;
345
+ };
346
+ async function copyDirectory(src, dest) {
347
+ await mkdir(dest, { recursive: true });
348
+ const entries = await readdir(src, { withFileTypes: true });
349
+ for (const entry of entries) {
350
+ if (isExcluded(entry.name)) {
351
+ continue;
352
+ }
353
+ const srcPath = join3(src, entry.name);
354
+ const destPath = join3(dest, entry.name);
355
+ if (entry.isDirectory()) {
356
+ await copyDirectory(srcPath, destPath);
357
+ } else {
358
+ await cp(srcPath, destPath);
359
+ }
360
+ }
361
+ }
362
+ async function isSkillInstalled(skillName, agentType, options = {}) {
363
+ const agent = agents[agentType];
364
+ const sanitized = sanitizeName(skillName);
365
+ const targetBase = options.global ? agent.globalSkillsDir : join3(options.cwd || process.cwd(), agent.skillsDir);
366
+ const skillDir = join3(targetBase, sanitized);
367
+ if (!isPathSafe(targetBase, skillDir)) {
368
+ return false;
369
+ }
370
+ try {
371
+ await access(skillDir);
372
+ return true;
373
+ } catch {
374
+ return false;
375
+ }
376
+ }
377
+ function getInstallPath(skillName, agentType, options = {}) {
378
+ const agent = agents[agentType];
379
+ const sanitized = sanitizeName(skillName);
380
+ const targetBase = options.global ? agent.globalSkillsDir : join3(options.cwd || process.cwd(), agent.skillsDir);
381
+ const installPath = join3(targetBase, sanitized);
382
+ if (!isPathSafe(targetBase, installPath)) {
383
+ throw new Error("Invalid skill name: potential path traversal detected");
384
+ }
385
+ return installPath;
386
+ }
387
+
388
+ // src/skills.ts
389
+ import { readdir as readdir2, readFile, stat } from "fs/promises";
390
+ import { basename as basename2, dirname, join as join4 } from "path";
391
+ import matter from "gray-matter";
392
+ var SKIP_DIRS = ["node_modules", ".git", "dist", "build", "__pycache__"];
393
+ async function hasSkillMd(dir) {
394
+ try {
395
+ const skillPath = join4(dir, "SKILL.md");
396
+ const stats = await stat(skillPath);
397
+ return stats.isFile();
398
+ } catch {
399
+ return false;
400
+ }
401
+ }
402
+ async function parseSkillMd(skillMdPath) {
403
+ try {
404
+ const content = await readFile(skillMdPath, "utf-8");
405
+ const { data } = matter(content);
406
+ if (!data.name || !data.description) {
407
+ return null;
408
+ }
409
+ return {
410
+ name: data.name,
411
+ description: data.description,
412
+ path: dirname(skillMdPath),
413
+ metadata: data.metadata
414
+ };
415
+ } catch {
416
+ return null;
417
+ }
418
+ }
419
+ async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
420
+ const skillDirs = [];
421
+ if (depth > maxDepth) return skillDirs;
422
+ try {
423
+ if (await hasSkillMd(dir)) {
424
+ skillDirs.push(dir);
425
+ }
426
+ const entries = await readdir2(dir, { withFileTypes: true });
427
+ for (const entry of entries) {
428
+ if (entry.isDirectory() && !SKIP_DIRS.includes(entry.name)) {
429
+ const subDirs = await findSkillDirs(join4(dir, entry.name), depth + 1, maxDepth);
430
+ skillDirs.push(...subDirs);
431
+ }
432
+ }
433
+ } catch {
434
+ }
435
+ return skillDirs;
436
+ }
437
+ async function discoverSkills(basePath, subpath) {
438
+ const skills = [];
439
+ const seenNames = /* @__PURE__ */ new Set();
440
+ const searchPath = subpath ? join4(basePath, subpath) : basePath;
441
+ if (await hasSkillMd(searchPath)) {
442
+ const skill = await parseSkillMd(join4(searchPath, "SKILL.md"));
443
+ if (skill) {
444
+ skills.push(skill);
445
+ return skills;
446
+ }
447
+ }
448
+ const prioritySearchDirs = [
449
+ searchPath,
450
+ join4(searchPath, "skills"),
451
+ join4(searchPath, "skills/.curated"),
452
+ join4(searchPath, "skills/.experimental"),
453
+ join4(searchPath, "skills/.system"),
454
+ join4(searchPath, ".codex/skills"),
455
+ join4(searchPath, ".claude/skills"),
456
+ join4(searchPath, ".opencode/skill"),
457
+ join4(searchPath, ".cursor/skills"),
458
+ join4(searchPath, ".agents/skills"),
459
+ join4(searchPath, ".kilocode/skills"),
460
+ join4(searchPath, ".roo/skills"),
461
+ join4(searchPath, ".goose/skills"),
462
+ join4(searchPath, ".agent/skills"),
463
+ join4(searchPath, ".github/skills")
464
+ ];
465
+ for (const dir of prioritySearchDirs) {
466
+ try {
467
+ const entries = await readdir2(dir, { withFileTypes: true });
468
+ for (const entry of entries) {
469
+ if (entry.isDirectory()) {
470
+ const skillDir = join4(dir, entry.name);
471
+ if (await hasSkillMd(skillDir)) {
472
+ const skill = await parseSkillMd(join4(skillDir, "SKILL.md"));
473
+ if (skill && !seenNames.has(skill.name)) {
474
+ skills.push(skill);
475
+ seenNames.add(skill.name);
476
+ }
477
+ }
478
+ }
479
+ }
480
+ } catch {
481
+ }
482
+ }
483
+ if (skills.length === 0) {
484
+ const allSkillDirs = await findSkillDirs(searchPath);
485
+ for (const skillDir of allSkillDirs) {
486
+ const skill = await parseSkillMd(join4(skillDir, "SKILL.md"));
487
+ if (skill && !seenNames.has(skill.name)) {
488
+ skills.push(skill);
489
+ seenNames.add(skill.name);
490
+ }
491
+ }
492
+ }
493
+ return skills;
494
+ }
495
+ function getSkillDisplayName(skill) {
496
+ return skill.name || basename2(skill.path);
497
+ }
498
+
499
+ // src/index.ts
500
+ var version = package_default.version;
501
+ program.name("fetch-skill").description(
502
+ "Install skills onto coding agents (OpenCode, Claude Code, Codex, Cursor, Antigravity, Github Copilot, Roo Code)"
503
+ ).version(version).argument("<source>", "Git repo URL, GitHub shorthand (owner/repo), or direct path to skill").option("-g, --global", "Install skill globally (user-level) instead of project-level").option(
504
+ "-a, --agent <agents...>",
505
+ "Specify agents to install to (opencode, claude-code, codex, cursor, antigravity, gitub-copilot, roo)"
506
+ ).option("-s, --skill <skills...>", "Specify skill names to install (skip selection prompt)").option("-l, --list", "List available skills in the repository without installing").option("-y, --yes", "Skip confirmation prompts").configureOutput({
507
+ outputError: (str, write) => {
508
+ if (str.includes("missing required argument")) {
509
+ console.log();
510
+ console.log(
511
+ `${chalk.bgRed.white.bold(" ERROR ")} ${chalk.red("Missing required argument: source")}`
512
+ );
513
+ console.log();
514
+ console.log(chalk.dim(" Usage:"));
515
+ console.log(
516
+ ` ${chalk.cyan("npx fetch-skill")} ${chalk.yellow("<source>")} ${chalk.dim("[options]")}`
517
+ );
518
+ console.log();
519
+ console.log(chalk.dim(" Example:"));
520
+ console.log(
521
+ ` ${chalk.cyan("npx fetch-skill")} ${chalk.yellow("shaneholloman/agent-skills")}`
522
+ );
523
+ console.log();
524
+ console.log(
525
+ chalk.dim(" Run") + ` ${chalk.cyan("npx fetch-skill --help")} ` + chalk.dim("for more information.")
526
+ );
527
+ console.log();
528
+ } else {
529
+ write(str);
530
+ }
531
+ }
532
+ }).action(async (source, options) => {
533
+ await main(source, options);
534
+ });
535
+ program.parse();
536
+ async function main(source, options) {
537
+ console.log();
538
+ p.intro(chalk.bgCyan.black(" fetch-skill "));
539
+ let tempDir = null;
540
+ try {
541
+ const spinner2 = p.spinner();
542
+ spinner2.start("Parsing source...");
543
+ const parsed = parseSource(source);
544
+ spinner2.stop(
545
+ `Source: ${chalk.cyan(parsed.url)}${parsed.subpath ? ` (${parsed.subpath})` : ""}`
546
+ );
547
+ spinner2.start("Cloning repository...");
548
+ tempDir = await cloneRepo(parsed.url);
549
+ spinner2.stop("Repository cloned");
550
+ spinner2.start("Discovering skills...");
551
+ const skills = await discoverSkills(tempDir, parsed.subpath);
552
+ if (skills.length === 0) {
553
+ spinner2.stop(chalk.red("No skills found"));
554
+ p.outro(
555
+ chalk.red("No valid skills found. Skills require a SKILL.md with name and description.")
556
+ );
557
+ await cleanup(tempDir);
558
+ process.exit(1);
559
+ }
560
+ spinner2.stop(`Found ${chalk.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
561
+ if (options.list) {
562
+ console.log();
563
+ p.log.step(chalk.bold("Available Skills"));
564
+ for (const skill of skills) {
565
+ p.log.message(` ${chalk.cyan(getSkillDisplayName(skill))}`);
566
+ p.log.message(` ${chalk.dim(skill.description)}`);
567
+ }
568
+ console.log();
569
+ p.outro("Use --skill <name> to install specific skills");
570
+ await cleanup(tempDir);
571
+ process.exit(0);
572
+ }
573
+ let selectedSkills;
574
+ if (options.skill && options.skill.length > 0) {
575
+ selectedSkills = skills.filter(
576
+ (s) => options.skill.some(
577
+ (name) => s.name.toLowerCase() === name.toLowerCase() || getSkillDisplayName(s).toLowerCase() === name.toLowerCase()
578
+ )
579
+ );
580
+ if (selectedSkills.length === 0) {
581
+ p.log.error(`No matching skills found for: ${options.skill.join(", ")}`);
582
+ p.log.info("Available skills:");
583
+ for (const s of skills) {
584
+ p.log.message(` - ${getSkillDisplayName(s)}`);
585
+ }
586
+ await cleanup(tempDir);
587
+ process.exit(1);
588
+ }
589
+ p.log.info(
590
+ `Selected ${selectedSkills.length} skill${selectedSkills.length !== 1 ? "s" : ""}: ${selectedSkills.map((s) => chalk.cyan(getSkillDisplayName(s))).join(", ")}`
591
+ );
592
+ } else if (skills.length === 1) {
593
+ selectedSkills = skills;
594
+ const firstSkill = skills[0];
595
+ p.log.info(`Skill: ${chalk.cyan(getSkillDisplayName(firstSkill))}`);
596
+ p.log.message(chalk.dim(firstSkill.description));
597
+ } else if (options.yes) {
598
+ selectedSkills = skills;
599
+ p.log.info(`Installing all ${skills.length} skills`);
600
+ } else {
601
+ const skillChoices = skills.map((s) => ({
602
+ value: s,
603
+ label: getSkillDisplayName(s),
604
+ hint: s.description.length > 60 ? `${s.description.slice(0, 57)}...` : s.description
605
+ }));
606
+ const selected = await p.multiselect({
607
+ message: "Select skills to install",
608
+ options: skillChoices,
609
+ required: true
610
+ });
611
+ if (p.isCancel(selected)) {
612
+ p.cancel("Installation cancelled");
613
+ await cleanup(tempDir);
614
+ process.exit(0);
615
+ }
616
+ selectedSkills = selected;
617
+ }
618
+ let targetAgents;
619
+ const validAgents = Object.keys(agents);
620
+ if (options.agent && options.agent.length > 0) {
621
+ const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
622
+ if (invalidAgents.length > 0) {
623
+ p.log.error(`Invalid agents: ${invalidAgents.join(", ")}`);
624
+ p.log.info(`Valid agents: ${validAgents.join(", ")}`);
625
+ await cleanup(tempDir);
626
+ process.exit(1);
627
+ }
628
+ targetAgents = options.agent;
629
+ } else {
630
+ spinner2.start("Detecting installed agents...");
631
+ const installedAgents = await detectInstalledAgents();
632
+ spinner2.stop(
633
+ `Detected ${installedAgents.length} agent${installedAgents.length !== 1 ? "s" : ""}`
634
+ );
635
+ if (installedAgents.length === 0) {
636
+ if (options.yes) {
637
+ targetAgents = validAgents;
638
+ p.log.info("Installing to all agents (none detected)");
639
+ } else {
640
+ p.log.warn("No coding agents detected. You can still install skills.");
641
+ const allAgentChoices = Object.entries(agents).map(([key, config]) => ({
642
+ value: key,
643
+ label: config.displayName
644
+ }));
645
+ const selected = await p.multiselect({
646
+ message: "Select agents to install skills to",
647
+ options: allAgentChoices,
648
+ required: true
649
+ });
650
+ if (p.isCancel(selected)) {
651
+ p.cancel("Installation cancelled");
652
+ await cleanup(tempDir);
653
+ process.exit(0);
654
+ }
655
+ targetAgents = selected;
656
+ }
657
+ } else if (installedAgents.length === 1 || options.yes) {
658
+ targetAgents = installedAgents;
659
+ if (installedAgents.length === 1) {
660
+ const firstAgent = installedAgents[0];
661
+ p.log.info(`Installing to: ${chalk.cyan(agents[firstAgent].displayName)}`);
662
+ } else {
663
+ p.log.info(
664
+ `Installing to: ${installedAgents.map((a) => chalk.cyan(agents[a].displayName)).join(", ")}`
665
+ );
666
+ }
667
+ } else {
668
+ const agentChoices = installedAgents.map((a) => ({
669
+ value: a,
670
+ label: agents[a].displayName,
671
+ hint: `${options.global ? agents[a].globalSkillsDir : agents[a].skillsDir}`
672
+ }));
673
+ const selected = await p.multiselect({
674
+ message: "Select agents to install skills to",
675
+ options: agentChoices,
676
+ required: true,
677
+ initialValues: installedAgents
678
+ });
679
+ if (p.isCancel(selected)) {
680
+ p.cancel("Installation cancelled");
681
+ await cleanup(tempDir);
682
+ process.exit(0);
683
+ }
684
+ targetAgents = selected;
685
+ }
686
+ }
687
+ let installGlobally = options.global ?? false;
688
+ if (options.global === void 0 && !options.yes) {
689
+ const scope = await p.select({
690
+ message: "Installation scope",
691
+ options: [
692
+ {
693
+ value: false,
694
+ label: "Project",
695
+ hint: "Install in current directory (committed with your project)"
696
+ },
697
+ {
698
+ value: true,
699
+ label: "Global",
700
+ hint: "Install in home directory (available across all projects)"
701
+ }
702
+ ]
703
+ });
704
+ if (p.isCancel(scope)) {
705
+ p.cancel("Installation cancelled");
706
+ await cleanup(tempDir);
707
+ process.exit(0);
708
+ }
709
+ installGlobally = scope;
710
+ }
711
+ console.log();
712
+ p.log.step(chalk.bold("Installation Summary"));
713
+ for (const skill of selectedSkills) {
714
+ p.log.message(` ${chalk.cyan(getSkillDisplayName(skill))}`);
715
+ for (const agent of targetAgents) {
716
+ const path = getInstallPath(skill.name, agent, { global: installGlobally });
717
+ const installed = await isSkillInstalled(skill.name, agent, { global: installGlobally });
718
+ const status = installed ? chalk.yellow(" (will overwrite)") : "";
719
+ p.log.message(
720
+ ` ${chalk.dim("\u2192")} ${agents[agent].displayName}: ${chalk.dim(path)}${status}`
721
+ );
722
+ }
723
+ }
724
+ console.log();
725
+ if (!options.yes) {
726
+ const confirmed = await p.confirm({ message: "Proceed with installation?" });
727
+ if (p.isCancel(confirmed) || !confirmed) {
728
+ p.cancel("Installation cancelled");
729
+ await cleanup(tempDir);
730
+ process.exit(0);
731
+ }
732
+ }
733
+ spinner2.start("Installing skills...");
734
+ const results = [];
735
+ for (const skill of selectedSkills) {
736
+ for (const agent of targetAgents) {
737
+ const result = await installSkillForAgent(skill, agent, { global: installGlobally });
738
+ results.push({
739
+ skill: getSkillDisplayName(skill),
740
+ agent: agents[agent].displayName,
741
+ ...result
742
+ });
743
+ }
744
+ }
745
+ spinner2.stop("Installation complete");
746
+ console.log();
747
+ const successful = results.filter((r) => r.success);
748
+ const failed = results.filter((r) => !r.success);
749
+ if (successful.length > 0) {
750
+ p.log.success(
751
+ chalk.green(
752
+ `Successfully installed ${successful.length} skill${successful.length !== 1 ? "s" : ""}`
753
+ )
754
+ );
755
+ for (const r of successful) {
756
+ p.log.message(` ${chalk.green("\u2713")} ${r.skill} \u2192 ${r.agent}`);
757
+ p.log.message(` ${chalk.dim(r.path)}`);
758
+ }
759
+ }
760
+ if (failed.length > 0) {
761
+ console.log();
762
+ p.log.error(
763
+ chalk.red(`Failed to install ${failed.length} skill${failed.length !== 1 ? "s" : ""}`)
764
+ );
765
+ for (const r of failed) {
766
+ p.log.message(` ${chalk.red("\u2717")} ${r.skill} \u2192 ${r.agent}`);
767
+ p.log.message(` ${chalk.dim(r.error)}`);
768
+ }
769
+ }
770
+ console.log();
771
+ p.outro(chalk.green("Done!"));
772
+ } catch (error) {
773
+ p.log.error(error instanceof Error ? error.message : "Unknown error occurred");
774
+ p.outro(chalk.red("Installation failed"));
775
+ process.exit(1);
776
+ } finally {
777
+ await cleanup(tempDir);
778
+ }
779
+ }
780
+ async function cleanup(tempDir) {
781
+ if (tempDir) {
782
+ try {
783
+ await cleanupTempDir(tempDir);
784
+ } catch {
785
+ }
786
+ }
787
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "fetch-skill",
3
+ "version": "1.0.0",
4
+ "description": "Install agent skills onto coding agents (OpenCode, Claude Code, Codex, Cursor)",
5
+ "type": "module",
6
+ "bin": {
7
+ "fetch-skill": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsup src/index.ts --format esm --dts --clean",
16
+ "dev": "tsx src/index.ts",
17
+ "lint": "biome lint --write .",
18
+ "format": "biome format --write .",
19
+ "check": "biome check --write .",
20
+ "prepublishOnly": "npm run build",
21
+ "update-readme": "tsx scripts/update-readme.ts"
22
+ },
23
+ "keywords": [
24
+ "cli",
25
+ "skills",
26
+ "opencode",
27
+ "claude-code",
28
+ "codex",
29
+ "cursor",
30
+ "amp",
31
+ "antigravity",
32
+ "roo-code",
33
+ "ai-agents"
34
+ ],
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/shaneholloman/fetch-skill.git"
38
+ },
39
+ "homepage": "https://github.com/shaneholloman/fetch-skill#readme",
40
+ "bugs": {
41
+ "url": "https://github.com/shaneholloman/fetch-skill/issues"
42
+ },
43
+ "author": "Shane Holloman",
44
+ "license": "MIT",
45
+ "dependencies": {
46
+ "@clack/prompts": "^0.11.0",
47
+ "chalk": "^5.6.2",
48
+ "commander": "^14.0.2",
49
+ "gray-matter": "^4.0.3",
50
+ "simple-git": "^3.30.0"
51
+ },
52
+ "devDependencies": {
53
+ "@biomejs/biome": "^2.3.11",
54
+ "@types/node": "^25.0.9",
55
+ "tsup": "^8.5.1",
56
+ "tsx": "^4.21.0",
57
+ "typescript": "^5.9.3"
58
+ },
59
+ "engines": {
60
+ "node": ">=22"
61
+ }
62
+ }