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 +21 -0
- package/README.md +208 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +787 -0
- package/package.json +62 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|