build-skill 0.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.md +21 -0
- package/README.md +182 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +309 -0
- package/package.json +70 -0
- package/template/.claude-plugin/marketplace.json +29 -0
- package/template/.github/workflows/process-skills.yml +86 -0
- package/template/README.md +79 -0
- package/template/scripts/add-skill.js +197 -0
- package/template/scripts/sync-skills.js +243 -0
- package/template/skills/{Skill_Name}/.claude-plugin/plugin.json +14 -0
- package/template/skills/{Skill_Name}/SKILL.md +9 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Flash Brew Digital / Ben Sabic
|
|
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,182 @@
|
|
|
1
|
+
# Build Skill
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
Scaffold AI agent skills quickly with the Build Skill CLI. Creates a fully-configured skills repository following the [Agent Skills Specification](https://agentskills.io/specification).
|
|
7
|
+
|
|
8
|
+
## What are Agent Skills?
|
|
9
|
+
|
|
10
|
+
Agent Skills are folders of instructions, scripts, and resources that agents can discover and use to do things more accurately and efficiently. They work across any AI agent that supports the [open Agent Skills standard](https://agentskills.io).
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx build-skill
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This launches an interactive prompt to configure your skill.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Interactive Mode
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx build-skill
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
You'll be prompted for:
|
|
29
|
+
- **Brand/Organization name** - Your company or project name (e.g., `acme-corp`)
|
|
30
|
+
- **Skill name** - Name of your first skill (e.g., `data-processor`)
|
|
31
|
+
- **Skill description** - What the skill does and when to use it
|
|
32
|
+
|
|
33
|
+
### Quiet Mode (CI/Scripts)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx build-skill --name my-skill --description "Helps with X tasks" --quiet
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### CLI Options
|
|
40
|
+
|
|
41
|
+
| Option | Alias | Description | Default |
|
|
42
|
+
| ------ | ----- | ----------- | ------- |
|
|
43
|
+
| `--brand <brand>` | `-b` | Brand/organization name | Same as `--name` |
|
|
44
|
+
| `--name <name>` | `-n` | Skill name | *Required in quiet mode* |
|
|
45
|
+
| `--description <desc>` | `-d` | Skill description | *Required in quiet mode* |
|
|
46
|
+
| `--license <license>` | `-l` | License for the skill | `MIT` |
|
|
47
|
+
| `--website <url>` | `-w` | Website/docs URL | `https://example.com` |
|
|
48
|
+
| `--repository <repo>` | `-r` | GitHub repository (owner/repo) | `<brand>/agent-skills` |
|
|
49
|
+
| `--category <category>` | `-c` | Skill category | `general` |
|
|
50
|
+
| `--keywords <keywords>` | `-k` | Comma-separated keywords | `ai, agent, skill` |
|
|
51
|
+
| `--output <dir>` | `-o` | Output directory | `.` |
|
|
52
|
+
| `--quiet` | `-q` | Suppress prompts and visual output | `false` |
|
|
53
|
+
| `--force` | `-f` | Overwrite existing directory | `false` |
|
|
54
|
+
| `--version` | `-V` | Show version number | |
|
|
55
|
+
| `--help` | `-h` | Show help | |
|
|
56
|
+
|
|
57
|
+
### Examples
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Interactive mode
|
|
61
|
+
npx build-skill
|
|
62
|
+
|
|
63
|
+
# Specify brand and skill name
|
|
64
|
+
npx build-skill --brand acme-corp --name data-processor --description "Processes data files"
|
|
65
|
+
|
|
66
|
+
# Full configuration
|
|
67
|
+
npx build-skill \
|
|
68
|
+
--brand acme-corp \
|
|
69
|
+
--name data-processor \
|
|
70
|
+
--description "Processes CSV and JSON data files" \
|
|
71
|
+
--license Apache-2.0 \
|
|
72
|
+
--website https://acme.example.com/docs \
|
|
73
|
+
--repository acme-corp/agent-skills \
|
|
74
|
+
--category productivity \
|
|
75
|
+
--keywords "data, csv, json, processing" \
|
|
76
|
+
--quiet
|
|
77
|
+
|
|
78
|
+
# Overwrite existing directory
|
|
79
|
+
npx build-skill --name my-skill --description "My skill" --force --quiet
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Generated Structure
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
<brand>-skills/
|
|
86
|
+
├── .claude-plugin/
|
|
87
|
+
│ └── marketplace.json
|
|
88
|
+
├── .github/
|
|
89
|
+
│ └── workflows/
|
|
90
|
+
│ └── process-skills.yml
|
|
91
|
+
├── scripts/
|
|
92
|
+
│ └── sync-skills.js
|
|
93
|
+
├── skills/
|
|
94
|
+
│ └── <skill-name>/
|
|
95
|
+
│ ├── .claude-plugin/
|
|
96
|
+
│ │ └── plugin.json
|
|
97
|
+
│ └── SKILL.md
|
|
98
|
+
└── README.md
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Included Automation
|
|
102
|
+
|
|
103
|
+
### GitHub Actions Workflow
|
|
104
|
+
|
|
105
|
+
The generated repository includes a `process-skills.yml` workflow that:
|
|
106
|
+
|
|
107
|
+
- **On Pull Requests**: Validates all skills using the [Validate Agent Skill](https://github.com/marketplace/actions/validate-skill) action
|
|
108
|
+
- **On Push to Main**: Validates skills and syncs the README and marketplace.json
|
|
109
|
+
|
|
110
|
+
### Add Skill Script
|
|
111
|
+
|
|
112
|
+
The `scripts/add-skill.js` utility creates new skills:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
node scripts/add-skill.js <skill-name> "<description>"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
```bash
|
|
120
|
+
node scripts/add-skill.js data-processor "Processes CSV and JSON data files"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
This script:
|
|
124
|
+
- Creates the skill directory structure in `skills/`
|
|
125
|
+
- Generates `SKILL.md` with frontmatter
|
|
126
|
+
- Generates `.claude-plugin/plugin.json`
|
|
127
|
+
- Automatically runs sync-skills.js to update README and marketplace.json
|
|
128
|
+
|
|
129
|
+
### Sync Script
|
|
130
|
+
|
|
131
|
+
The `scripts/sync-skills.js` utility keeps your repository in sync:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
node scripts/sync-skills.js
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This script:
|
|
138
|
+
- Scans the `skills/` directory for all skills
|
|
139
|
+
- Updates the "Available Skills" table in `README.md`
|
|
140
|
+
- Updates the `plugins` array in `.claude-plugin/marketplace.json`
|
|
141
|
+
|
|
142
|
+
Run it after modifying skills to keep everything up to date.
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Install dependencies
|
|
148
|
+
pnpm install
|
|
149
|
+
|
|
150
|
+
# Build
|
|
151
|
+
pnpm build
|
|
152
|
+
|
|
153
|
+
# Run tests
|
|
154
|
+
pnpm test
|
|
155
|
+
|
|
156
|
+
# Lint
|
|
157
|
+
pnpm check
|
|
158
|
+
|
|
159
|
+
# Fix lint issues
|
|
160
|
+
pnpm fix
|
|
161
|
+
|
|
162
|
+
# Type check
|
|
163
|
+
pnpm type-check
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Resources
|
|
167
|
+
|
|
168
|
+
- [Agent Skills Specification](https://agentskills.io/specification)
|
|
169
|
+
- [Vercel Skills CLI](https://skills.sh/)
|
|
170
|
+
- [Validate Agent Skill](https://github.com/marketplace/actions/validate-skill)
|
|
171
|
+
|
|
172
|
+
## Contributing
|
|
173
|
+
|
|
174
|
+
Contributions are welcome! Please read our [Contributing Guide](.github/CONTRIBUTING.md) for more information.
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
[MIT License](LICENSE.md)
|
|
179
|
+
|
|
180
|
+
## Author
|
|
181
|
+
|
|
182
|
+
[Ben Sabic](https://bensabic.ca) at [Flash Brew Digital](https://flashbrew.digital)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { dirname, join as join2, resolve } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { log as log2 } from "@clack/prompts";
|
|
7
|
+
import { program } from "commander";
|
|
8
|
+
import pc3 from "picocolors";
|
|
9
|
+
|
|
10
|
+
// src/prompts.ts
|
|
11
|
+
import { cancel, group, log, outro, text } from "@clack/prompts";
|
|
12
|
+
import pc from "picocolors";
|
|
13
|
+
|
|
14
|
+
// src/utils.ts
|
|
15
|
+
import { execa } from "execa";
|
|
16
|
+
function normalizeName(input) {
|
|
17
|
+
return input.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
18
|
+
}
|
|
19
|
+
async function getGitConfig(key) {
|
|
20
|
+
try {
|
|
21
|
+
const { stdout } = await execa("git", ["config", "--get", key]);
|
|
22
|
+
return stdout.trim();
|
|
23
|
+
} catch {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/prompts.ts
|
|
29
|
+
var MAX_DESCRIPTION_LENGTH = 1024;
|
|
30
|
+
function validateName(value) {
|
|
31
|
+
if (!value?.trim()) {
|
|
32
|
+
return "Name is required";
|
|
33
|
+
}
|
|
34
|
+
if (!normalizeName(value)) {
|
|
35
|
+
return "Name must contain at least one letter or number";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function validateDescription(value) {
|
|
39
|
+
if (!value) {
|
|
40
|
+
return "Description is required";
|
|
41
|
+
}
|
|
42
|
+
if (value.length > MAX_DESCRIPTION_LENGTH) {
|
|
43
|
+
return `Description must be under ${MAX_DESCRIPTION_LENGTH} characters`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function promptForInput(providedBrand, providedName, providedDescription) {
|
|
47
|
+
const answers = await group(
|
|
48
|
+
{
|
|
49
|
+
brandName: () => {
|
|
50
|
+
if (providedBrand) {
|
|
51
|
+
return Promise.resolve(providedBrand);
|
|
52
|
+
}
|
|
53
|
+
return text({
|
|
54
|
+
message: "What is your brand/organization name?",
|
|
55
|
+
placeholder: "acme-corp",
|
|
56
|
+
validate: validateName
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
skillName: () => {
|
|
60
|
+
if (providedName) {
|
|
61
|
+
return Promise.resolve(providedName);
|
|
62
|
+
}
|
|
63
|
+
return text({
|
|
64
|
+
message: "What is the name of your first skill?",
|
|
65
|
+
placeholder: "my-skill",
|
|
66
|
+
validate: validateName
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
skillDescription: () => {
|
|
70
|
+
if (providedDescription) {
|
|
71
|
+
return Promise.resolve(providedDescription);
|
|
72
|
+
}
|
|
73
|
+
return text({
|
|
74
|
+
message: "Describe what this skill does and when to use it:",
|
|
75
|
+
placeholder: "Helps with X tasks. Use when working with Y or when the user mentions Z.",
|
|
76
|
+
validate: validateDescription
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
onCancel: () => {
|
|
82
|
+
cancel("Operation cancelled.");
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
return {
|
|
88
|
+
brandName: normalizeName(answers.brandName),
|
|
89
|
+
skillName: normalizeName(answers.skillName),
|
|
90
|
+
skillDescription: answers.skillDescription
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function getQuietInput(providedBrand, providedName, providedDescription) {
|
|
94
|
+
if (!providedName) {
|
|
95
|
+
throw new Error("--name is required in quiet mode");
|
|
96
|
+
}
|
|
97
|
+
if (!providedDescription) {
|
|
98
|
+
throw new Error("--description is required in quiet mode");
|
|
99
|
+
}
|
|
100
|
+
const brand = providedBrand || providedName;
|
|
101
|
+
const nameError = validateName(providedName);
|
|
102
|
+
if (nameError) {
|
|
103
|
+
throw new Error(nameError);
|
|
104
|
+
}
|
|
105
|
+
const brandError = validateName(brand);
|
|
106
|
+
if (brandError) {
|
|
107
|
+
throw new Error(`Brand ${brandError.toLowerCase()}`);
|
|
108
|
+
}
|
|
109
|
+
const descError = validateDescription(providedDescription);
|
|
110
|
+
if (descError) {
|
|
111
|
+
throw new Error(descError);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
brandName: normalizeName(brand),
|
|
115
|
+
skillName: normalizeName(providedName),
|
|
116
|
+
skillDescription: providedDescription
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function printSuccess(targetDir, brandName, skillName, quiet) {
|
|
120
|
+
if (quiet) {
|
|
121
|
+
console.log(targetDir);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
log.success(`Created ${pc.cyan(targetDir)}`);
|
|
125
|
+
log.info(`
|
|
126
|
+
Next steps:
|
|
127
|
+
${pc.cyan(`cd ${brandName}-skills`)}
|
|
128
|
+
${pc.cyan("git init")}
|
|
129
|
+
${pc.cyan("git remote add origin <YOUR_REPO_URL>")}
|
|
130
|
+
${pc.cyan("git branch -M main")}
|
|
131
|
+
${pc.cyan("git add .")}
|
|
132
|
+
${pc.cyan(`git commit -m "Initial release of ${skillName} skill"`)}
|
|
133
|
+
${pc.cyan("git push -u origin main")}
|
|
134
|
+
|
|
135
|
+
Edit your skill at:
|
|
136
|
+
${pc.cyan(`skills/${skillName}/SKILL.md`)}
|
|
137
|
+
`);
|
|
138
|
+
outro(pc.green("Happy building! \u{1F680}"));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/scaffold.ts
|
|
142
|
+
import {
|
|
143
|
+
access,
|
|
144
|
+
cp,
|
|
145
|
+
mkdir,
|
|
146
|
+
readdir,
|
|
147
|
+
readFile,
|
|
148
|
+
rename,
|
|
149
|
+
rm,
|
|
150
|
+
writeFile
|
|
151
|
+
} from "fs/promises";
|
|
152
|
+
import { join } from "path";
|
|
153
|
+
import { spinner } from "@clack/prompts";
|
|
154
|
+
import pc2 from "picocolors";
|
|
155
|
+
var SKILL_NAME_PLACEHOLDER = "{Skill_Name}";
|
|
156
|
+
async function pathExists(path) {
|
|
157
|
+
try {
|
|
158
|
+
await access(path);
|
|
159
|
+
return true;
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function copyDir(src, dest) {
|
|
165
|
+
await mkdir(dest, { recursive: true });
|
|
166
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
const srcPath = join(src, entry.name);
|
|
169
|
+
const destPath = join(dest, entry.name);
|
|
170
|
+
if (entry.isDirectory()) {
|
|
171
|
+
await copyDir(srcPath, destPath);
|
|
172
|
+
} else {
|
|
173
|
+
await cp(srcPath, destPath);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function replaceInFile(filePath, values) {
|
|
178
|
+
let content = await readFile(filePath, "utf-8");
|
|
179
|
+
for (const [key, value] of Object.entries(values)) {
|
|
180
|
+
const placeholder = `{${key}}`;
|
|
181
|
+
content = content.replaceAll(placeholder, value);
|
|
182
|
+
}
|
|
183
|
+
await writeFile(filePath, content, "utf-8");
|
|
184
|
+
}
|
|
185
|
+
async function processDirectory(dir, values) {
|
|
186
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
const fullPath = join(dir, entry.name);
|
|
189
|
+
if (entry.isDirectory()) {
|
|
190
|
+
if (entry.name === SKILL_NAME_PLACEHOLDER) {
|
|
191
|
+
const newPath = join(dir, values.Skill_Name);
|
|
192
|
+
await rename(fullPath, newPath);
|
|
193
|
+
await processDirectory(newPath, values);
|
|
194
|
+
} else {
|
|
195
|
+
await processDirectory(fullPath, values);
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
await replaceInFile(fullPath, values);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function formatError(error) {
|
|
203
|
+
if (error instanceof Error) {
|
|
204
|
+
if (error.message.includes("ENOENT")) {
|
|
205
|
+
return "Template directory not found. Please reinstall build-skill.";
|
|
206
|
+
}
|
|
207
|
+
if (error.message.includes("EACCES")) {
|
|
208
|
+
return "Permission denied. Check write permissions for the output directory.";
|
|
209
|
+
}
|
|
210
|
+
if (error.message.includes("ENOSPC")) {
|
|
211
|
+
return "No space left on device.";
|
|
212
|
+
}
|
|
213
|
+
return error.message;
|
|
214
|
+
}
|
|
215
|
+
return "An unknown error occurred";
|
|
216
|
+
}
|
|
217
|
+
async function createSkillRepository(templateDir, targetDir, values, quiet, force) {
|
|
218
|
+
const s = quiet ? null : spinner();
|
|
219
|
+
if (!await pathExists(templateDir)) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
"Template directory not found. Please reinstall build-skill."
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (await pathExists(targetDir)) {
|
|
225
|
+
if (force) {
|
|
226
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
227
|
+
} else {
|
|
228
|
+
throw new Error(`Directory already exists: ${targetDir}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
s?.start(
|
|
232
|
+
`Creating the ${pc2.cyan(values.Brand_Name)} agent skills repository...`
|
|
233
|
+
);
|
|
234
|
+
try {
|
|
235
|
+
await copyDir(templateDir, targetDir);
|
|
236
|
+
await processDirectory(targetDir, values);
|
|
237
|
+
s?.stop("Agent Skills repository created!");
|
|
238
|
+
} catch (error) {
|
|
239
|
+
s?.stop("Failed to create skill repository");
|
|
240
|
+
try {
|
|
241
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
242
|
+
} catch {
|
|
243
|
+
}
|
|
244
|
+
throw new Error(formatError(error));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/index.ts
|
|
249
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
250
|
+
var TEMPLATE_DIR = join2(__dirname, "..", "template");
|
|
251
|
+
var version = true ? "0.0.0" : "dev";
|
|
252
|
+
var BANNER = `
|
|
253
|
+
_ _ _ _ _ _ _ _
|
|
254
|
+
| |__ _ _(_) | __| | ___| | _(_) | |
|
|
255
|
+
| '_ \\| | | | | |/ _\` | / __| |/ / | | |
|
|
256
|
+
| |_) | |_| | | | (_| | \\__ \\ <| | | |
|
|
257
|
+
|_.__/ \\__,_|_|_|\\__,_| |___/_|\\_\\_|_|_|
|
|
258
|
+
`;
|
|
259
|
+
async function main() {
|
|
260
|
+
program.name("build-skill").description("Scaffold AI agent skills quickly").version(version).argument("[name]", "Skill name").argument("[description]", "Skill description").option("-b, --brand <brand>", "Brand/organization name").option("-n, --name <name>", "Skill name").option("-d, --description <description>", "Skill description").option("-l, --license <license>", "License for the skill").option("-w, --website <url>", "Website URL (e.g. docs) for the skill").option("-r, --repository <repo>", "GitHub repository (owner/repo)").option("-c, --category <category>", "Skill category").option("-k, --keywords <keywords>", "Comma-separated keywords").option("-o, --output <dir>", "Output directory", ".").option("-q, --quiet", "Suppress interactive prompts and visual output").option("-f, --force", "Overwrite existing directory").parse();
|
|
261
|
+
const options = program.opts();
|
|
262
|
+
const [argName, argDescription] = program.args;
|
|
263
|
+
const providedBrand = options.brand;
|
|
264
|
+
const providedName = options.name || argName;
|
|
265
|
+
const providedDescription = options.description || argDescription;
|
|
266
|
+
if (!options.quiet) {
|
|
267
|
+
console.log(pc3.green(BANNER));
|
|
268
|
+
}
|
|
269
|
+
const gitName = await getGitConfig("user.name");
|
|
270
|
+
const gitEmail = await getGitConfig("user.email");
|
|
271
|
+
const hasGitConfig = gitName && gitEmail;
|
|
272
|
+
if (!(hasGitConfig || options.quiet)) {
|
|
273
|
+
log2.warn(
|
|
274
|
+
"Could not detect git user.name or user.email. Using placeholders."
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const input = options.quiet ? getQuietInput(providedBrand, providedName, providedDescription) : await promptForInput(providedBrand, providedName, providedDescription);
|
|
278
|
+
const { brandName, skillName, skillDescription } = input;
|
|
279
|
+
const outputDir = resolve(process.cwd(), options.output || ".");
|
|
280
|
+
const targetDir = join2(outputDir, `${brandName}-skills`);
|
|
281
|
+
const values = {
|
|
282
|
+
Brand_Name: brandName,
|
|
283
|
+
Skill_Name: skillName,
|
|
284
|
+
Skill_Description: skillDescription,
|
|
285
|
+
Creator_Name: gitName || "Your Name",
|
|
286
|
+
Creator_Email: gitEmail || "your.email@example.com",
|
|
287
|
+
Skill_License: options.license || "MIT",
|
|
288
|
+
Skill_Homepage: options.website || "https://example.com",
|
|
289
|
+
Skill_Repository: options.repository || `${brandName}/agent-skills`,
|
|
290
|
+
Skill_Category: options.category || "general",
|
|
291
|
+
Skill_Keywords: options.keywords || "ai, agent, skill"
|
|
292
|
+
};
|
|
293
|
+
await createSkillRepository(
|
|
294
|
+
TEMPLATE_DIR,
|
|
295
|
+
targetDir,
|
|
296
|
+
values,
|
|
297
|
+
Boolean(options.quiet),
|
|
298
|
+
Boolean(options.force)
|
|
299
|
+
);
|
|
300
|
+
printSuccess(targetDir, brandName, skillName, Boolean(options.quiet));
|
|
301
|
+
}
|
|
302
|
+
main().catch((error) => {
|
|
303
|
+
console.error(
|
|
304
|
+
pc3.red(
|
|
305
|
+
error instanceof Error ? error.message : "An unexpected error occurred"
|
|
306
|
+
)
|
|
307
|
+
);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "build-skill",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Scaffold AI agent skills quickly with the Build Skill CLI.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"ai-sdk",
|
|
8
|
+
"agent",
|
|
9
|
+
"skills",
|
|
10
|
+
"tools"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/Flash-Brew-Digital/build-skill",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Flash-Brew-Digital/build-skill/issues"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": {
|
|
18
|
+
"name": "Ben Sabic",
|
|
19
|
+
"url": "https://bensabic.dev"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/Flash-Brew-Digital/build-skill.git"
|
|
24
|
+
},
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"module": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"bin": {
|
|
29
|
+
"build-skill": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"import": "./dist/index.js",
|
|
34
|
+
"types": "./dist/index.d.ts"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"template",
|
|
40
|
+
"README.md"
|
|
41
|
+
],
|
|
42
|
+
"type": "module",
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"prepublishOnly": "pnpm build",
|
|
47
|
+
"check": "ultracite check",
|
|
48
|
+
"fix": "ultracite fix",
|
|
49
|
+
"bump-deps": "npx npm-check-updates -u -p pnpm && pnpm install",
|
|
50
|
+
"type-check": "tsc --noEmit"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@clack/prompts": "^1.0.0",
|
|
54
|
+
"commander": "^14.0.3",
|
|
55
|
+
"execa": "^9.6.1",
|
|
56
|
+
"picocolors": "^1.1.1"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@biomejs/biome": "2.3.13",
|
|
60
|
+
"@types/node": "^25.2.0",
|
|
61
|
+
"tsup": "^8.5.1",
|
|
62
|
+
"typescript": "^5.9.3",
|
|
63
|
+
"ultracite": "7.1.3",
|
|
64
|
+
"vitest": "^4.0.18"
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=20"
|
|
68
|
+
},
|
|
69
|
+
"packageManager": "pnpm@10.28.2"
|
|
70
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{Brand_Name}-skills",
|
|
3
|
+
"description": "{Brand_Name} agent skills",
|
|
4
|
+
"owner": {
|
|
5
|
+
"name": "{Creator_Name}",
|
|
6
|
+
"email": "{Creator_Email}"
|
|
7
|
+
},
|
|
8
|
+
"metadata": {
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"description": "Claude Code marketplace of skills by {Brand_Name}"
|
|
11
|
+
},
|
|
12
|
+
"plugins": [
|
|
13
|
+
{
|
|
14
|
+
"name": "{Skill_Name}-skill",
|
|
15
|
+
"description": "{Skill_Description}",
|
|
16
|
+
"version": "1.0.0",
|
|
17
|
+
"author": {
|
|
18
|
+
"name": "{Creator_Name}",
|
|
19
|
+
"email": "{Creator_Email}"
|
|
20
|
+
},
|
|
21
|
+
"source": "./skills/{Skill_Name}",
|
|
22
|
+
"homepage": "{Skill_Homepage}",
|
|
23
|
+
"repository": "https://github.com/{Skill_Repository}",
|
|
24
|
+
"license": "{Skill_License}",
|
|
25
|
+
"category": "{Skill_Category}",
|
|
26
|
+
"keywords": "{Skill_Keywords}"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
name: Process Skills
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths:
|
|
7
|
+
- "**/SKILL.md"
|
|
8
|
+
pull_request:
|
|
9
|
+
branches: [main]
|
|
10
|
+
paths:
|
|
11
|
+
- "**/SKILL.md"
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: validate-skill-${{ github.ref }}
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
detect-changes:
|
|
19
|
+
runs-on: ubuntu-slim
|
|
20
|
+
if: github.event.pull_request.draft != true && github.actor != 'dependabot[bot]'
|
|
21
|
+
outputs:
|
|
22
|
+
skills: ${{ steps.changed-skills.outputs.skills }}
|
|
23
|
+
steps:
|
|
24
|
+
- name: Checkout repository
|
|
25
|
+
uses: actions/checkout@v6
|
|
26
|
+
with:
|
|
27
|
+
fetch-depth: 0
|
|
28
|
+
|
|
29
|
+
- name: Get changed skills
|
|
30
|
+
id: changed-skills
|
|
31
|
+
run: |
|
|
32
|
+
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
|
33
|
+
BASE=${{ github.event.pull_request.base.sha }}
|
|
34
|
+
HEAD=${{ github.event.pull_request.head.sha }}
|
|
35
|
+
else
|
|
36
|
+
BASE=${{ github.event.before }}
|
|
37
|
+
HEAD=${{ github.event.after }}
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Find changed SKILL.md files and extract skill directories
|
|
41
|
+
SKILLS=$(git diff --name-only $BASE $HEAD | \
|
|
42
|
+
grep 'SKILL.md$' | \
|
|
43
|
+
xargs -I {} dirname {} | \
|
|
44
|
+
sort -u | \
|
|
45
|
+
jq -R -s -c 'split("\n") | map(select(length > 0))')
|
|
46
|
+
|
|
47
|
+
echo "skills=$SKILLS" >> $GITHUB_OUTPUT
|
|
48
|
+
echo "Changed skills: $SKILLS"
|
|
49
|
+
|
|
50
|
+
validate:
|
|
51
|
+
needs: detect-changes
|
|
52
|
+
if: needs.detect-changes.outputs.skills != '[]'
|
|
53
|
+
runs-on: ubuntu-slim
|
|
54
|
+
strategy:
|
|
55
|
+
fail-fast: false
|
|
56
|
+
matrix:
|
|
57
|
+
skill: ${{ fromJson(needs.detect-changes.outputs.skills) }}
|
|
58
|
+
steps:
|
|
59
|
+
- name: Checkout repository
|
|
60
|
+
uses: actions/checkout@v6
|
|
61
|
+
|
|
62
|
+
- name: Validate ${{ matrix.skill }}
|
|
63
|
+
uses: Flash-Brew-Digital/validate-skill@v1
|
|
64
|
+
with:
|
|
65
|
+
path: ${{ matrix.skill }}
|
|
66
|
+
|
|
67
|
+
sync:
|
|
68
|
+
needs: validate
|
|
69
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
70
|
+
runs-on: ubuntu-slim
|
|
71
|
+
permissions:
|
|
72
|
+
contents: write
|
|
73
|
+
steps:
|
|
74
|
+
- name: Checkout repository
|
|
75
|
+
uses: actions/checkout@v6
|
|
76
|
+
|
|
77
|
+
- name: Sync skills
|
|
78
|
+
run: node scripts/sync-skills.js
|
|
79
|
+
|
|
80
|
+
- name: Commit changes
|
|
81
|
+
run: |
|
|
82
|
+
git config user.name "Agent Skills Bot"
|
|
83
|
+
git config user.email "agent-skills-bot@users.noreply.github.com"
|
|
84
|
+
git add README.md .claude-plugin/marketplace.json
|
|
85
|
+
git diff --staged --quiet || git commit -m "chore: sync skills documentation"
|
|
86
|
+
git push
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# {Brand_Name} Agent Skills
|
|
2
|
+
|
|
3
|
+
This repository contains a collection of agent skills for {Brand_Name}. These skills are designed to enhance the capabilities of agents by providing them with specialized functionalities.
|
|
4
|
+
|
|
5
|
+
## What are Agent Skills?
|
|
6
|
+
|
|
7
|
+
Agent Skills are folders of instructions, scripts, and resources that agents can discover and use to do things more accurately and efficiently. They work across any AI agent that supports the [open Agent Skills standard](https://agentskills.io).
|
|
8
|
+
|
|
9
|
+
## Available Skills
|
|
10
|
+
<!-- START:Available-Skills -->
|
|
11
|
+
|
|
12
|
+
| Skill | Description |
|
|
13
|
+
| ----- | ----------- |
|
|
14
|
+
| {Skill_Name} | {Skill_Description} |
|
|
15
|
+
|
|
16
|
+
<!-- END:Available-Skills -->
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
### Option 1: Skills (Recommended)
|
|
21
|
+
|
|
22
|
+
Use the [Vercel Skills CLI](https://skills.sh/) to install skills directly:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Install all skills
|
|
26
|
+
npx skills add {Brand_Name}/agent-skills
|
|
27
|
+
|
|
28
|
+
# Install specific skills
|
|
29
|
+
npx skills add {Brand_Name}/agent-skills --skill {Skill_Name}
|
|
30
|
+
|
|
31
|
+
# List available skills
|
|
32
|
+
npx skills add {Brand_Name}/agent-skills --list
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Option 2: Claude Code Plugin
|
|
36
|
+
|
|
37
|
+
Install via Claude Code's plugin system:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Add the marketplace
|
|
41
|
+
/plugin marketplace add {Brand_Name}/agent-skills
|
|
42
|
+
|
|
43
|
+
# Install specific skill
|
|
44
|
+
/plugin install {Skill_Name}-skill
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Adding New Skills
|
|
48
|
+
|
|
49
|
+
Use the included script to add new skills:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
node scripts/add-skill.js <skill-name> "<description>"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
node scripts/add-skill.js {Skill_Name} "{Skill_Description}"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This will create the skill structure and automatically update this README and the marketplace.json.
|
|
62
|
+
|
|
63
|
+
## Scripts
|
|
64
|
+
|
|
65
|
+
| Script | Description |
|
|
66
|
+
| ------ | ----------- |
|
|
67
|
+
| `node scripts/add-skill.js` | Add a new skill to the repository |
|
|
68
|
+
| `node scripts/sync-skills.js` | Sync README and marketplace.json with skills directory |
|
|
69
|
+
|
|
70
|
+
## Contributing
|
|
71
|
+
|
|
72
|
+
1. Fork this repository
|
|
73
|
+
2. Create a new skill using `node scripts/add-skill.js`
|
|
74
|
+
3. Edit the skill's `SKILL.md` with your content
|
|
75
|
+
4. Submit a pull request
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
{Skill_License}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* add-skill.js
|
|
5
|
+
*
|
|
6
|
+
* Adds a new skill to the repository.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node scripts/add-skill.js <skill-name> <description>
|
|
9
|
+
*
|
|
10
|
+
* Example: node scripts/add-skill.js my-new-skill "Helps with X tasks"
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const ROOT_DIR = join(__dirname, "..");
|
|
19
|
+
const SKILLS_DIR = join(ROOT_DIR, "skills");
|
|
20
|
+
const MARKETPLACE_PATH = join(ROOT_DIR, ".claude-plugin", "marketplace.json");
|
|
21
|
+
const SYNC_SCRIPT = join(__dirname, "sync-skills.js");
|
|
22
|
+
|
|
23
|
+
const SKILLS_SUFFIX_REGEX = /-skills$/;
|
|
24
|
+
|
|
25
|
+
function normalizeName(input) {
|
|
26
|
+
return input
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.trim()
|
|
29
|
+
.replace(/\s+/g, "-")
|
|
30
|
+
.replace(/[^a-z0-9-]/g, "")
|
|
31
|
+
.replace(/-+/g, "-")
|
|
32
|
+
.replace(/^-|-$/g, "");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function printUsage() {
|
|
36
|
+
console.log(`
|
|
37
|
+
Usage: node scripts/add-skill.js <skill-name> <description>
|
|
38
|
+
|
|
39
|
+
Arguments:
|
|
40
|
+
skill-name Name of the skill (will be normalized to lowercase with hyphens)
|
|
41
|
+
description Brief description of what the skill does
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
node scripts/add-skill.js my-new-skill "Helps with data processing tasks"
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function loadMarketplace() {
|
|
49
|
+
try {
|
|
50
|
+
const content = await readFile(MARKETPLACE_PATH, "utf-8");
|
|
51
|
+
return JSON.parse(content);
|
|
52
|
+
} catch {
|
|
53
|
+
console.error("Error: Could not read .claude-plugin/marketplace.json");
|
|
54
|
+
console.error("Make sure you're running this from the repository root.");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function skillExists(skillName) {
|
|
60
|
+
try {
|
|
61
|
+
await readFile(join(SKILLS_DIR, skillName, "SKILL.md"), "utf-8");
|
|
62
|
+
return true;
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function generateSkillMd(skillName, description, marketplace) {
|
|
69
|
+
const brandName = marketplace.name.replace(SKILLS_SUFFIX_REGEX, "");
|
|
70
|
+
const license =
|
|
71
|
+
marketplace.plugins?.[0]?.license || marketplace.license || "MIT";
|
|
72
|
+
|
|
73
|
+
return `---
|
|
74
|
+
name: ${skillName}
|
|
75
|
+
description: ${description}
|
|
76
|
+
license: ${license}
|
|
77
|
+
metadata:
|
|
78
|
+
author: ${brandName}
|
|
79
|
+
version: "1.0.0"
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function generatePluginJson(skillName, description, marketplace) {
|
|
86
|
+
const owner = marketplace.owner || {};
|
|
87
|
+
const existingPlugin = marketplace.plugins?.[0] || {};
|
|
88
|
+
|
|
89
|
+
return JSON.stringify(
|
|
90
|
+
{
|
|
91
|
+
name: skillName,
|
|
92
|
+
description,
|
|
93
|
+
version: "1.0.0",
|
|
94
|
+
author: {
|
|
95
|
+
name: owner.name || "Your Name",
|
|
96
|
+
email: owner.email || "your.email@example.com",
|
|
97
|
+
},
|
|
98
|
+
license: existingPlugin.license || "MIT",
|
|
99
|
+
homepage: existingPlugin.homepage || "https://example.com",
|
|
100
|
+
repository: existingPlugin.repository || "",
|
|
101
|
+
keywords: existingPlugin.keywords || "ai, agent, skill",
|
|
102
|
+
category: existingPlugin.category || "general",
|
|
103
|
+
},
|
|
104
|
+
null,
|
|
105
|
+
2
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function runSyncScript() {
|
|
110
|
+
const { spawn } = await import("node:child_process");
|
|
111
|
+
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
const child = spawn("node", [SYNC_SCRIPT], {
|
|
114
|
+
stdio: "inherit",
|
|
115
|
+
cwd: ROOT_DIR,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
child.on("close", (code) => {
|
|
119
|
+
if (code === 0) {
|
|
120
|
+
resolve();
|
|
121
|
+
} else {
|
|
122
|
+
reject(new Error(`sync-skills.js exited with code ${code}`));
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
child.on("error", reject);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function main() {
|
|
131
|
+
const args = process.argv.slice(2);
|
|
132
|
+
|
|
133
|
+
if (args.length < 2 || args.includes("--help") || args.includes("-h")) {
|
|
134
|
+
printUsage();
|
|
135
|
+
process.exit(args.includes("--help") || args.includes("-h") ? 0 : 1);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const [rawName, ...descParts] = args;
|
|
139
|
+
const description = descParts.join(" ");
|
|
140
|
+
const skillName = normalizeName(rawName);
|
|
141
|
+
|
|
142
|
+
if (!skillName) {
|
|
143
|
+
console.error(
|
|
144
|
+
"Error: Skill name must contain at least one letter or number"
|
|
145
|
+
);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!description) {
|
|
150
|
+
console.error("Error: Description is required");
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (await skillExists(skillName)) {
|
|
155
|
+
console.error(`Error: Skill "${skillName}" already exists`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(`\nCreating skill: ${skillName}\n`);
|
|
160
|
+
|
|
161
|
+
const marketplace = await loadMarketplace();
|
|
162
|
+
const skillDir = join(SKILLS_DIR, skillName);
|
|
163
|
+
const pluginDir = join(skillDir, ".claude-plugin");
|
|
164
|
+
|
|
165
|
+
// Create directories
|
|
166
|
+
await mkdir(pluginDir, { recursive: true });
|
|
167
|
+
|
|
168
|
+
// Generate files
|
|
169
|
+
const skillMd = generateSkillMd(skillName, description, marketplace);
|
|
170
|
+
const pluginJson = generatePluginJson(skillName, description, marketplace);
|
|
171
|
+
|
|
172
|
+
await writeFile(join(skillDir, "SKILL.md"), skillMd, "utf-8");
|
|
173
|
+
await writeFile(join(pluginDir, "plugin.json"), `${pluginJson}\n`, "utf-8");
|
|
174
|
+
|
|
175
|
+
console.log(`✓ Created skills/${skillName}/SKILL.md`);
|
|
176
|
+
console.log(`✓ Created skills/${skillName}/.claude-plugin/plugin.json`);
|
|
177
|
+
console.log();
|
|
178
|
+
|
|
179
|
+
// Run sync script
|
|
180
|
+
console.log("Running sync-skills.js...\n");
|
|
181
|
+
await runSyncScript();
|
|
182
|
+
|
|
183
|
+
console.log(`\nSkill "${skillName}" created successfully!`);
|
|
184
|
+
console.log("\nNext steps:");
|
|
185
|
+
console.log(
|
|
186
|
+
` 1. Edit skills/${skillName}/SKILL.md to add your skill content`
|
|
187
|
+
);
|
|
188
|
+
console.log(
|
|
189
|
+
` 2. Update skills/${skillName}/.claude-plugin/plugin.json if needed`
|
|
190
|
+
);
|
|
191
|
+
console.log();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
main().catch((error) => {
|
|
195
|
+
console.error("Error:", error.message);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
});
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* sync-skills.js
|
|
5
|
+
*
|
|
6
|
+
* Syncs the Available Skills section in README.md and the plugins array
|
|
7
|
+
* in .claude-plugin/marketplace.json with the actual skills in skills/
|
|
8
|
+
*
|
|
9
|
+
* Usage: node scripts/sync-skills.js
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const ROOT_DIR = join(__dirname, "..");
|
|
18
|
+
const SKILLS_DIR = join(ROOT_DIR, "skills");
|
|
19
|
+
const README_PATH = join(ROOT_DIR, "README.md");
|
|
20
|
+
const MARKETPLACE_PATH = join(ROOT_DIR, ".claude-plugin", "marketplace.json");
|
|
21
|
+
|
|
22
|
+
const FRONTMATTER_REGEX = /^---\n([\s\S]*?)\n---/;
|
|
23
|
+
const WHITESPACE_REGEX = /\S/;
|
|
24
|
+
|
|
25
|
+
function parseValue(raw) {
|
|
26
|
+
const trimmed = raw.trim();
|
|
27
|
+
if (
|
|
28
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
29
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
30
|
+
) {
|
|
31
|
+
return trimmed.slice(1, -1);
|
|
32
|
+
}
|
|
33
|
+
return trimmed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseFrontmatter(content) {
|
|
37
|
+
const match = content.match(FRONTMATTER_REGEX);
|
|
38
|
+
if (!match) {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const lines = match[1].split("\n");
|
|
43
|
+
const result = {};
|
|
44
|
+
let currentObject = null;
|
|
45
|
+
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
if (line.trim() === "") {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const colonIndex = line.indexOf(":");
|
|
52
|
+
if (colonIndex === -1) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const indent = line.search(WHITESPACE_REGEX);
|
|
57
|
+
const key = line.slice(0, colonIndex).trim();
|
|
58
|
+
const rawValue = line.slice(colonIndex + 1);
|
|
59
|
+
|
|
60
|
+
if (indent > 0 && currentObject !== null) {
|
|
61
|
+
// Nested property
|
|
62
|
+
const value = parseValue(rawValue);
|
|
63
|
+
if (value !== "") {
|
|
64
|
+
currentObject[key] = value;
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
// Top-level property
|
|
68
|
+
const value = parseValue(rawValue);
|
|
69
|
+
if (value === "") {
|
|
70
|
+
// Start of a nested object
|
|
71
|
+
currentObject = {};
|
|
72
|
+
result[key] = currentObject;
|
|
73
|
+
} else {
|
|
74
|
+
currentObject = null;
|
|
75
|
+
result[key] = value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function discoverSkills() {
|
|
84
|
+
const skills = [];
|
|
85
|
+
|
|
86
|
+
let entries;
|
|
87
|
+
try {
|
|
88
|
+
entries = await readdir(SKILLS_DIR, { withFileTypes: true });
|
|
89
|
+
} catch {
|
|
90
|
+
console.warn("No skills directory found. Skipping sync.");
|
|
91
|
+
return skills;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (!entry.isDirectory()) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const skillDir = join(SKILLS_DIR, entry.name);
|
|
100
|
+
const skillMdPath = join(skillDir, "SKILL.md");
|
|
101
|
+
const pluginJsonPath = join(skillDir, ".claude-plugin", "plugin.json");
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const skillMd = await readFile(skillMdPath, "utf-8");
|
|
105
|
+
const frontmatter = parseFrontmatter(skillMd);
|
|
106
|
+
|
|
107
|
+
let pluginJson = {};
|
|
108
|
+
try {
|
|
109
|
+
const pluginContent = await readFile(pluginJsonPath, "utf-8");
|
|
110
|
+
pluginJson = JSON.parse(pluginContent);
|
|
111
|
+
} catch {
|
|
112
|
+
// Ignore missing or invalid plugin.json
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const metadata = frontmatter.metadata || {};
|
|
116
|
+
|
|
117
|
+
skills.push({
|
|
118
|
+
dirName: entry.name,
|
|
119
|
+
name: frontmatter.name || entry.name,
|
|
120
|
+
description: frontmatter.description || pluginJson.description || "",
|
|
121
|
+
license: frontmatter.license || pluginJson.license || "MIT",
|
|
122
|
+
version: metadata.version || pluginJson.version || "1.0.0",
|
|
123
|
+
author: metadata.author || pluginJson.author || {},
|
|
124
|
+
homepage: pluginJson.homepage || "",
|
|
125
|
+
repository: pluginJson.repository || "",
|
|
126
|
+
category: pluginJson.category || "general",
|
|
127
|
+
keywords: pluginJson.keywords || "",
|
|
128
|
+
});
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.warn(
|
|
131
|
+
`Warning: Could not read skill at ${entry.name}:`,
|
|
132
|
+
error.message
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function truncate(str, maxLength = 80) {
|
|
141
|
+
if (str.length <= maxLength) {
|
|
142
|
+
return str;
|
|
143
|
+
}
|
|
144
|
+
return `${str.slice(0, maxLength - 3)}...`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function generateReadmeSkillsList(skills) {
|
|
148
|
+
if (skills.length === 0) {
|
|
149
|
+
return "\n*No skills available yet.*\n";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const lines = ["", "| Skill | Description |", "| ----- | ----------- |"];
|
|
153
|
+
|
|
154
|
+
for (const skill of skills) {
|
|
155
|
+
const escapedDesc = truncate(skill.description).replace(/\|/g, "\\|");
|
|
156
|
+
lines.push(`| ${skill.name} | ${escapedDesc} |`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
lines.push("");
|
|
160
|
+
return lines.join("\n");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function updateReadme(skills) {
|
|
164
|
+
const content = await readFile(README_PATH, "utf-8");
|
|
165
|
+
const startMarker = "<!-- START:Available-Skills -->";
|
|
166
|
+
const endMarker = "<!-- END:Available-Skills -->";
|
|
167
|
+
|
|
168
|
+
const startIndex = content.indexOf(startMarker);
|
|
169
|
+
const endIndex = content.indexOf(endMarker);
|
|
170
|
+
|
|
171
|
+
if (startIndex === -1 || endIndex === -1) {
|
|
172
|
+
console.warn("Warning: Could not find skill markers in README.md");
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const skillsList = generateReadmeSkillsList(skills);
|
|
177
|
+
const newContent =
|
|
178
|
+
content.slice(0, startIndex + startMarker.length) +
|
|
179
|
+
skillsList +
|
|
180
|
+
content.slice(endIndex);
|
|
181
|
+
|
|
182
|
+
await writeFile(README_PATH, newContent, "utf-8");
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function updateMarketplace(skills) {
|
|
187
|
+
let marketplace;
|
|
188
|
+
try {
|
|
189
|
+
const content = await readFile(MARKETPLACE_PATH, "utf-8");
|
|
190
|
+
marketplace = JSON.parse(content);
|
|
191
|
+
} catch {
|
|
192
|
+
console.warn("Warning: Could not read marketplace.json");
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
marketplace.plugins = skills.map((skill) => ({
|
|
197
|
+
name: `${skill.name}-skill`,
|
|
198
|
+
description: skill.description,
|
|
199
|
+
version: skill.version,
|
|
200
|
+
author: skill.author,
|
|
201
|
+
source: `./skills/${skill.dirName}`,
|
|
202
|
+
homepage: skill.homepage,
|
|
203
|
+
repository: skill.repository,
|
|
204
|
+
license: skill.license,
|
|
205
|
+
category: skill.category,
|
|
206
|
+
keywords: skill.keywords,
|
|
207
|
+
}));
|
|
208
|
+
|
|
209
|
+
await writeFile(
|
|
210
|
+
MARKETPLACE_PATH,
|
|
211
|
+
`${JSON.stringify(marketplace, null, 2)}\n`,
|
|
212
|
+
"utf-8"
|
|
213
|
+
);
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function main() {
|
|
218
|
+
console.log("Syncing agent skill(s)...\n");
|
|
219
|
+
|
|
220
|
+
const skills = await discoverSkills();
|
|
221
|
+
console.log(`Found ${skills.length} agent skill(s):`);
|
|
222
|
+
for (const skill of skills) {
|
|
223
|
+
console.log(` - ${skill.name}`);
|
|
224
|
+
}
|
|
225
|
+
console.log();
|
|
226
|
+
|
|
227
|
+
const readmeUpdated = await updateReadme(skills);
|
|
228
|
+
if (readmeUpdated) {
|
|
229
|
+
console.log("✓ Updated README.md");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const marketplaceUpdated = await updateMarketplace(skills);
|
|
233
|
+
if (marketplaceUpdated) {
|
|
234
|
+
console.log("✓ Updated .claude-plugin/marketplace.json");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
console.log("\nDone!");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
main().catch((error) => {
|
|
241
|
+
console.error("Error:", error.message);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{Skill_Name}",
|
|
3
|
+
"description": "{Skill_Description}",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "{Creator_Name}",
|
|
7
|
+
"email": "{Creator_Email}"
|
|
8
|
+
},
|
|
9
|
+
"license": "{Skill_License}",
|
|
10
|
+
"homepage": "{Skill_Homepage}",
|
|
11
|
+
"repository": "https://github.com/{Skill_Repository}",
|
|
12
|
+
"keywords": "{Skill_Keywords}",
|
|
13
|
+
"category": "{Skill_Category}"
|
|
14
|
+
}
|