skill-forger 0.1.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 +143 -0
- package/dist/cli.d.mts +8 -0
- package/dist/cli.mjs +403 -0
- package/package.json +48 -0
- package/skills_schema.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present Marc Mansour
|
|
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,143 @@
|
|
|
1
|
+
# 🤹 skill-forger
|
|
2
|
+
|
|
3
|
+
Manage project Agent [skills](https://skills.sh/) from `skills.json`. Uses [`skills`](https://github.com/vercel-labs/skills) CLI under the hood.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
**Install all skills from `skills.json`:**
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx skill-forger
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<img src="./assets/install.svg" alt="Install preview">
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
**Add new skills to project:**
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx skill-forger add skills.sh/vercel-labs/skills/find-skills
|
|
21
|
+
|
|
22
|
+
npx skill-forger add anthropics/skills:skill-creator
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img src="./assets/add.svg" alt="Install preview">
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
This creates a `skills.json` file:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"$schema": "https://unpkg.com/skill-forger/skills_schema.json",
|
|
34
|
+
"skills": [
|
|
35
|
+
{ "source": "vercel-labs/skills", "skills": ["find-skills"] },
|
|
36
|
+
{ "source": "anthropics/skills", "skills": ["skill-creator"] }
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## CLI Usage
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npx skill-forger # Install skills from skills.json (default)
|
|
45
|
+
npx skill-forger install, i # Same as above
|
|
46
|
+
npx skill-forger add <source>... # Add skill source(s) to skills.json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Commands
|
|
50
|
+
|
|
51
|
+
#### `install` (default)
|
|
52
|
+
|
|
53
|
+
Installs all skills defined in `skills.json`.
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
npx skill-forger install [options]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| Option | Description |
|
|
60
|
+
| ---------------- | ------------------------------------------------- |
|
|
61
|
+
| `--agent <name>` | Target agent (default: `claude-code`, repeatable) |
|
|
62
|
+
| `-g, --global` | Install skills globally |
|
|
63
|
+
| `-h, --help` | Show help |
|
|
64
|
+
|
|
65
|
+
#### `add`
|
|
66
|
+
|
|
67
|
+
Adds skill source(s) to `skills.json` and installs them.
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
npx skill-forger add <source>... [options]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
| Option | Description |
|
|
74
|
+
| ---------------- | ------------------------------------------------- |
|
|
75
|
+
| `--agent <name>` | Target agent (default: `claude-code`, repeatable) |
|
|
76
|
+
| `-h, --help` | Show help |
|
|
77
|
+
|
|
78
|
+
### Source Formats
|
|
79
|
+
|
|
80
|
+
Sources can be specified in multiple formats:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
# GitHub owner/repo format
|
|
84
|
+
npx skill-forger add vercel-labs/skills
|
|
85
|
+
|
|
86
|
+
# skills.sh URL
|
|
87
|
+
npx skill-forger add https://skills.sh/vercel-labs/skills/find-skills
|
|
88
|
+
npx skill-forger add skills.sh/vercel-labs/skills/find-skills
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Multiple sources
|
|
92
|
+
npx skill-forger add org/repo-a:skill1 org/repo-b:skill2
|
|
93
|
+
|
|
94
|
+
# Specify skills (comma separated)
|
|
95
|
+
npx skill-forger add vercel-labs/agent-skills:vercel-deploy,vercel-react-native-skills
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Examples
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
# Install all skills from skills.json
|
|
102
|
+
npx skill-forger
|
|
103
|
+
|
|
104
|
+
# Add a skill source (all skills)
|
|
105
|
+
npx skill-forger add vercel-labs/skills
|
|
106
|
+
|
|
107
|
+
# Add specific skills from a source
|
|
108
|
+
npx skill-forger add vercel-labs/agent-skills:vercel-deploy,vercel-react-native-skills
|
|
109
|
+
|
|
110
|
+
# Add from skills.sh URL
|
|
111
|
+
npx skill-forger add https://skills.sh/vercel-labs/skills/find-skills
|
|
112
|
+
|
|
113
|
+
# Install skills globally
|
|
114
|
+
npx skill-forger install --global
|
|
115
|
+
|
|
116
|
+
# Install for multiple agents
|
|
117
|
+
npx skill-forger install --agent claude-code --agent cursor
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Development
|
|
121
|
+
|
|
122
|
+
🤖 Are you a robot? Read [AGENTS.md](./AGENTS.md).
|
|
123
|
+
|
|
124
|
+
<details>
|
|
125
|
+
|
|
126
|
+
<summary>local development</summary>
|
|
127
|
+
|
|
128
|
+
- Clone this repository
|
|
129
|
+
- Install [Bun](https://bun.sh)
|
|
130
|
+
- Install dependencies using `bun install`
|
|
131
|
+
- Run interactive tests using `bun run dev`
|
|
132
|
+
|
|
133
|
+
</details>
|
|
134
|
+
|
|
135
|
+
## Alternatives
|
|
136
|
+
|
|
137
|
+
- Proposal PR for adding `skill-lock.json` ([vercel-labs/skills#234](https://github.com/vercel-labs/skills/pull/234))
|
|
138
|
+
- Proposal PR for adding `.skills` ([vercel-labs/skills#134](https://github.com/vercel-labs/skills/pull/134))
|
|
139
|
+
- [hairyf/skills-manifest](https://github.com/hairyf/skills-manifest)
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
Published under the [MIT](https://github.com/unjs/skill-forger/blob/main/LICENSE) license 💛.
|
package/dist/cli.d.mts
ADDED
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
//#region src/config.ts
|
|
10
|
+
function findSkillsConfig(cwd = process.cwd()) {
|
|
11
|
+
let dir = resolve(cwd);
|
|
12
|
+
const root = dirname(dir);
|
|
13
|
+
while (dir !== root) {
|
|
14
|
+
const candidate = join(dir, "skills.json");
|
|
15
|
+
if (existsSync(candidate)) return candidate;
|
|
16
|
+
const parent = dirname(dir);
|
|
17
|
+
if (parent === dir) break;
|
|
18
|
+
dir = parent;
|
|
19
|
+
}
|
|
20
|
+
const rootCandidate = join(dir, "skills.json");
|
|
21
|
+
if (existsSync(rootCandidate)) return rootCandidate;
|
|
22
|
+
}
|
|
23
|
+
function defaultConfig() {
|
|
24
|
+
return { skills: [] };
|
|
25
|
+
}
|
|
26
|
+
async function readSkillsConfig(options = {}) {
|
|
27
|
+
const { cwd, createIfNotExists = false } = options;
|
|
28
|
+
const skillsPath = findSkillsConfig(cwd);
|
|
29
|
+
if (!skillsPath) {
|
|
30
|
+
if (createIfNotExists) {
|
|
31
|
+
const newPath = join(resolve(cwd ?? process.cwd()), "skills.json");
|
|
32
|
+
const config = defaultConfig();
|
|
33
|
+
await writeFile(newPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
34
|
+
return {
|
|
35
|
+
config,
|
|
36
|
+
path: newPath
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
throw new Error("skills.json not found in current directory or any parent directory.");
|
|
40
|
+
}
|
|
41
|
+
const raw = await readFile(skillsPath, "utf8");
|
|
42
|
+
return {
|
|
43
|
+
config: assertSkillsConfig(JSON.parse(raw)),
|
|
44
|
+
path: skillsPath
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function updateSkillsConfig(updater, options = {}) {
|
|
48
|
+
const { cwd, createIfNotExists = true } = options;
|
|
49
|
+
const { config, path } = await readSkillsConfig({
|
|
50
|
+
cwd,
|
|
51
|
+
createIfNotExists
|
|
52
|
+
});
|
|
53
|
+
const validated = assertSkillsConfig(await updater(config) ?? config);
|
|
54
|
+
await writeFile(path, `${JSON.stringify(validated, null, 2)}\n`, "utf8");
|
|
55
|
+
return {
|
|
56
|
+
config: validated,
|
|
57
|
+
path
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function addSkill(source, skills = [], options = {}) {
|
|
61
|
+
return updateSkillsConfig((config) => {
|
|
62
|
+
const entry = config.skills.find((item) => item.source === source);
|
|
63
|
+
if (!entry) {
|
|
64
|
+
config.skills.push({
|
|
65
|
+
source,
|
|
66
|
+
skills: [...skills]
|
|
67
|
+
});
|
|
68
|
+
return config;
|
|
69
|
+
}
|
|
70
|
+
if (skills.length === 0 || !entry.skills?.length) {
|
|
71
|
+
entry.skills = [];
|
|
72
|
+
return config;
|
|
73
|
+
}
|
|
74
|
+
const merged = new Set(entry.skills);
|
|
75
|
+
for (const skill of skills) merged.add(skill);
|
|
76
|
+
entry.skills = [...merged];
|
|
77
|
+
return config;
|
|
78
|
+
}, options);
|
|
79
|
+
}
|
|
80
|
+
function assertSkillsConfig(value) {
|
|
81
|
+
if (!value || typeof value !== "object" || !("skills" in value)) throw new Error("Invalid skills.json: missing 'skills' key.");
|
|
82
|
+
if (!("$schema" in value)) return {
|
|
83
|
+
$schema: "https://unpkg.com/skill-forger/skills_schema.json",
|
|
84
|
+
...value
|
|
85
|
+
};
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/utils/colors.ts
|
|
90
|
+
const enabled = process.stdout.isTTY !== false;
|
|
91
|
+
function code(n) {
|
|
92
|
+
return enabled ? `\x1B[${n}m` : "";
|
|
93
|
+
}
|
|
94
|
+
const c = {
|
|
95
|
+
reset: code(0),
|
|
96
|
+
bold: code(1),
|
|
97
|
+
dim: code(2),
|
|
98
|
+
red: code(31),
|
|
99
|
+
green: code(32),
|
|
100
|
+
yellow: code(33),
|
|
101
|
+
blue: code(34),
|
|
102
|
+
magenta: code(35),
|
|
103
|
+
cyan: code(36)
|
|
104
|
+
};
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/utils/gitignore.ts
|
|
107
|
+
function findGitignore(cwd = process.cwd()) {
|
|
108
|
+
let dir = resolve(cwd);
|
|
109
|
+
const root = dirname(dir);
|
|
110
|
+
while (dir !== root) {
|
|
111
|
+
const candidate = join(dir, ".gitignore");
|
|
112
|
+
if (existsSync(candidate)) return candidate;
|
|
113
|
+
const parent = dirname(dir);
|
|
114
|
+
if (parent === dir) break;
|
|
115
|
+
dir = parent;
|
|
116
|
+
}
|
|
117
|
+
const rootCandidate = join(dir, ".gitignore");
|
|
118
|
+
if (existsSync(rootCandidate)) return rootCandidate;
|
|
119
|
+
}
|
|
120
|
+
async function addGitignoreEntry(entries, options = {}) {
|
|
121
|
+
const { cwd, createIfNotExists = true } = options;
|
|
122
|
+
const resolvedCwd = resolve(cwd ?? process.cwd());
|
|
123
|
+
const uniqueEntries = [...new Set((Array.isArray(entries) ? entries : [entries]).map((e) => e.trim()).filter(Boolean))];
|
|
124
|
+
if (uniqueEntries.length === 0) return;
|
|
125
|
+
let gitignorePath = findGitignore(resolvedCwd);
|
|
126
|
+
if (!gitignorePath) {
|
|
127
|
+
if (!createIfNotExists) return;
|
|
128
|
+
gitignorePath = join(resolvedCwd, ".gitignore");
|
|
129
|
+
await writeFile(gitignorePath, `${uniqueEntries.join("\n")}\n`, "utf8");
|
|
130
|
+
return gitignorePath;
|
|
131
|
+
}
|
|
132
|
+
const content = await readFile(gitignorePath, "utf8");
|
|
133
|
+
const existingLines = new Set(content.split("\n").map((line) => line.trim()));
|
|
134
|
+
const newEntries = uniqueEntries.filter((entry) => !existingLines.has(entry));
|
|
135
|
+
if (newEntries.length === 0) return gitignorePath;
|
|
136
|
+
const newContent = `${content}${content.endsWith("\n") ? "" : "\n"}${newEntries.join("\n")}\n`;
|
|
137
|
+
await writeFile(gitignorePath, newContent, "utf8");
|
|
138
|
+
return gitignorePath;
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region src/skills.ts
|
|
142
|
+
let _skillsBinaryCache = null;
|
|
143
|
+
function findSkillsBinary(options) {
|
|
144
|
+
if (options?.cache !== false && _skillsBinaryCache !== null) return _skillsBinaryCache;
|
|
145
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
146
|
+
while (true) {
|
|
147
|
+
const candidate = join(dir, "node_modules", ".bin", "skills");
|
|
148
|
+
if (existsSync(candidate)) {
|
|
149
|
+
_skillsBinaryCache = candidate;
|
|
150
|
+
return candidate;
|
|
151
|
+
}
|
|
152
|
+
const parent = dirname(dir);
|
|
153
|
+
if (parent === dir) break;
|
|
154
|
+
dir = parent;
|
|
155
|
+
}
|
|
156
|
+
_skillsBinaryCache = void 0;
|
|
157
|
+
}
|
|
158
|
+
async function installSkills(options = {}) {
|
|
159
|
+
const { config, path: configPath } = await readSkillsConfig({ cwd: options.cwd });
|
|
160
|
+
await addGitignoreEntry(".agents", { cwd: dirname(configPath) });
|
|
161
|
+
const total = config.skills.length;
|
|
162
|
+
const totalStart = performance.now();
|
|
163
|
+
const globalPrefix = options.global ? `${c.magenta}[ global ]${c.reset} ` : "";
|
|
164
|
+
console.log(`${globalPrefix}🤹 Installing ${total} skill${total === 1 ? "" : "s"}...\n`);
|
|
165
|
+
let i = 0;
|
|
166
|
+
for (const entry of config.skills) {
|
|
167
|
+
i++;
|
|
168
|
+
await installSkillSource(entry, {
|
|
169
|
+
...options,
|
|
170
|
+
prefix: `[${i}/${total}] `
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const totalDuration = formatDuration(performance.now() - totalStart);
|
|
174
|
+
console.log(`${globalPrefix}🎉 Done! ${total} skill${total === 1 ? "" : "s"} installed in ${c.green}${totalDuration}${c.reset}.`);
|
|
175
|
+
}
|
|
176
|
+
async function installSkillSource(entry, options) {
|
|
177
|
+
const skillsBinary = findSkillsBinary();
|
|
178
|
+
const globalPrefix = options.global ? `${c.magenta}[ global ]${c.reset} ` : "";
|
|
179
|
+
const skillList = (entry.skills?.length || 0) > 0 ? entry.skills?.join(", ") : "all skills";
|
|
180
|
+
console.log(`${globalPrefix}${c.cyan}◐${c.reset} ${options.prefix || ""}Installing ${c.cyan}${entry.source}${c.reset} ${c.dim}(${skillList})${c.reset}`);
|
|
181
|
+
const [command, args] = skillsBinary ? [skillsBinary, ["add", entry.source]] : ["npx", [
|
|
182
|
+
"skills",
|
|
183
|
+
"add",
|
|
184
|
+
entry.source
|
|
185
|
+
]];
|
|
186
|
+
if (entry.skills && entry.skills.length > 0) args.push("--skill", ...entry.skills);
|
|
187
|
+
else args.push("--skill", "*");
|
|
188
|
+
if (options.agents && options.agents.length > 0) args.push("--agent", ...options.agents);
|
|
189
|
+
if (options.global) args.push("--global");
|
|
190
|
+
if (options.yes) args.push("--yes");
|
|
191
|
+
if (process.env.DEBUG) console.log(`${c.dim}$ ${[command.replace(process.cwd(), "."), ...args].join(" ")}${c.reset}\n`);
|
|
192
|
+
const skillStart = performance.now();
|
|
193
|
+
await runCommand(command, args);
|
|
194
|
+
const skillDuration = formatDuration(performance.now() - skillStart);
|
|
195
|
+
console.log(`${globalPrefix}${c.green}✔${c.reset} Installed ${entry.source} ${c.dim}(${skillDuration})${c.reset}\n`);
|
|
196
|
+
}
|
|
197
|
+
function formatDuration(ms) {
|
|
198
|
+
const rounded = Math.round(ms);
|
|
199
|
+
if (rounded < 1e3) return `${rounded}ms`;
|
|
200
|
+
const seconds = Math.round(rounded / 1e3);
|
|
201
|
+
return seconds < 60 ? `${seconds}s` : `${Math.round(seconds / 60)}m`;
|
|
202
|
+
}
|
|
203
|
+
function runCommand(command, args) {
|
|
204
|
+
return new Promise((resolve, reject) => {
|
|
205
|
+
const stdout = [];
|
|
206
|
+
const stderr = [];
|
|
207
|
+
const child = spawn(command, args, { stdio: "pipe" });
|
|
208
|
+
child.stdout.on("data", (data) => stdout.push(data));
|
|
209
|
+
child.stderr.on("data", (data) => stderr.push(data));
|
|
210
|
+
child.on("error", reject);
|
|
211
|
+
child.on("close", (code) => {
|
|
212
|
+
if (code === 0) {
|
|
213
|
+
resolve();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const output = [Buffer.concat(stdout).toString(), Buffer.concat(stderr).toString()].filter(Boolean).join("\n");
|
|
217
|
+
if (output) console.error(output);
|
|
218
|
+
reject(/* @__PURE__ */ new Error(`${command} exited with code ${code ?? "unknown"}.`));
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/cli.ts
|
|
224
|
+
const { version } = createRequire(import.meta.url)("../package.json");
|
|
225
|
+
const name = "skill-forger";
|
|
226
|
+
const SKILLS_SH_RE = /^(?:https?:\/\/)?skills\.sh\/(.+)/;
|
|
227
|
+
function parseSource(input) {
|
|
228
|
+
const skillsShMatch = input.match(SKILLS_SH_RE)?.[1];
|
|
229
|
+
if (skillsShMatch) {
|
|
230
|
+
const [namespace, repo, ...skills] = skillsShMatch.split("/");
|
|
231
|
+
if (!(namespace && repo)) return {
|
|
232
|
+
source: input,
|
|
233
|
+
skills: []
|
|
234
|
+
};
|
|
235
|
+
const filtered = skills.flatMap((s) => s.split(",")).map((s) => s.trim()).filter(Boolean);
|
|
236
|
+
return {
|
|
237
|
+
source: `${namespace}/${repo}`,
|
|
238
|
+
skills: filtered.includes("*") ? [] : filtered
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const [source = "", ...parts] = input.split(":");
|
|
242
|
+
const skills = parts.flatMap((p) => p.split(",")).map((s) => s.trim()).filter(Boolean);
|
|
243
|
+
return {
|
|
244
|
+
source,
|
|
245
|
+
skills: skills.includes("*") ? [] : skills
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
async function handleInstall(values) {
|
|
249
|
+
if (!findSkillsConfig()) {
|
|
250
|
+
console.log(`${c.yellow}No skills.json found.${c.reset}
|
|
251
|
+
|
|
252
|
+
Get started by adding a skill source:
|
|
253
|
+
|
|
254
|
+
${c.dim}$${c.reset} npx ${name} add ${c.cyan}vercel-labs/skills${c.reset}
|
|
255
|
+
`);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
await installSkills({
|
|
259
|
+
yes: true,
|
|
260
|
+
agents: values.agent || ["claude-code"],
|
|
261
|
+
global: values.global
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
async function handleAdd(positionals, values) {
|
|
265
|
+
const sources = positionals.slice(1);
|
|
266
|
+
if (sources.length === 0) {
|
|
267
|
+
showUsage("add");
|
|
268
|
+
throw new Error("Missing skill source.");
|
|
269
|
+
}
|
|
270
|
+
const parsedSources = [];
|
|
271
|
+
for (const rawSource of sources) {
|
|
272
|
+
const { source, skills } = parseSource(rawSource);
|
|
273
|
+
const existing = parsedSources.find((p) => p.source === source);
|
|
274
|
+
if (existing) if (skills.length === 0 || existing.skills.length === 0) existing.skills = [];
|
|
275
|
+
else existing.skills = [...new Set([...existing.skills, ...skills])];
|
|
276
|
+
else parsedSources.push({
|
|
277
|
+
source,
|
|
278
|
+
skills: [...skills]
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
const agents = values.agent || ["claude-code"];
|
|
282
|
+
const globalPrefix = values.global ? `${c.magenta}[ global ]${c.reset} ` : "";
|
|
283
|
+
for (const { source, skills } of parsedSources) {
|
|
284
|
+
await installSkillSource({
|
|
285
|
+
source,
|
|
286
|
+
skills
|
|
287
|
+
}, {
|
|
288
|
+
agents,
|
|
289
|
+
yes: true,
|
|
290
|
+
global: values.global
|
|
291
|
+
});
|
|
292
|
+
await addSkill(source, skills);
|
|
293
|
+
console.log(`${globalPrefix}${c.green}✔${c.reset} Added ${c.cyan}${source}${c.reset} to skills.json`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function main(argv = process.argv.slice(2)) {
|
|
297
|
+
const { values, positionals } = parseArgs({
|
|
298
|
+
args: argv,
|
|
299
|
+
allowPositionals: true,
|
|
300
|
+
options: {
|
|
301
|
+
agent: {
|
|
302
|
+
type: "string",
|
|
303
|
+
multiple: true
|
|
304
|
+
},
|
|
305
|
+
global: {
|
|
306
|
+
type: "boolean",
|
|
307
|
+
short: "g"
|
|
308
|
+
},
|
|
309
|
+
help: {
|
|
310
|
+
type: "boolean",
|
|
311
|
+
short: "h"
|
|
312
|
+
},
|
|
313
|
+
version: {
|
|
314
|
+
type: "boolean",
|
|
315
|
+
short: "v"
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
if (values.version) {
|
|
320
|
+
console.log(`${name} ${version}`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const command = positionals[0];
|
|
324
|
+
if (values.help) {
|
|
325
|
+
showUsage(command);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (!command || command === "install" || command === "i") {
|
|
329
|
+
await handleInstall(values);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (command === "add") {
|
|
333
|
+
await handleAdd(positionals, values);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
throw new Error(`Unknown command: ${command}`);
|
|
337
|
+
}
|
|
338
|
+
function showUsage(command) {
|
|
339
|
+
if (command === "add") {
|
|
340
|
+
console.log(`
|
|
341
|
+
${c.bold}Usage:${c.reset} ${c.cyan}${name} add${c.reset} <source>... [options]
|
|
342
|
+
|
|
343
|
+
${c.bold}Arguments:${c.reset}
|
|
344
|
+
${c.cyan}<source>${c.reset} Skill source ${c.dim}(e.g., vercel-labs/skills:pdf,commit)${c.reset}
|
|
345
|
+
|
|
346
|
+
${c.bold}Options:${c.reset}
|
|
347
|
+
${c.cyan}--agent${c.reset} <name> Target agent ${c.dim}(default: claude-code, can be repeated)${c.reset}
|
|
348
|
+
${c.cyan}-h, --help${c.reset} Show this help message
|
|
349
|
+
|
|
350
|
+
${c.bold}Examples:${c.reset}
|
|
351
|
+
${c.dim}$${c.reset} ${name} add vercel-labs/skills
|
|
352
|
+
${c.dim}$${c.reset} ${name} add vercel-labs/skills:pdf,commit
|
|
353
|
+
${c.dim}$${c.reset} ${name} add vercel-labs/skills:find-skills anthropics/skills:skill-creator
|
|
354
|
+
${c.dim}$${c.reset} ${name} add https://skills.sh/vercel-labs/skills/pdf
|
|
355
|
+
`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (command === "install" || command === "i") {
|
|
359
|
+
console.log(`
|
|
360
|
+
${c.bold}Usage:${c.reset} ${c.cyan}${name} install${c.reset} [options]
|
|
361
|
+
|
|
362
|
+
${c.bold}Options:${c.reset}
|
|
363
|
+
${c.cyan}--agent${c.reset} <name> Target agent ${c.dim}(default: claude-code, can be repeated)${c.reset}
|
|
364
|
+
${c.cyan}-g, --global${c.reset} Install skills globally
|
|
365
|
+
${c.cyan}-h, --help${c.reset} Show this help message
|
|
366
|
+
|
|
367
|
+
${c.bold}Examples:${c.reset}
|
|
368
|
+
${c.dim}$${c.reset} npx ${name} install
|
|
369
|
+
${c.dim}$${c.reset} npx ${name} install --global
|
|
370
|
+
${c.dim}$${c.reset} npx ${name} install --agent claude-code --agent cursor
|
|
371
|
+
`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
console.log(`
|
|
375
|
+
${c.bold}${name}${c.reset} ${c.dim}v${version}${c.reset}
|
|
376
|
+
|
|
377
|
+
Manage project skills declaratively with ${c.cyan}skills.json${c.reset}
|
|
378
|
+
|
|
379
|
+
${c.bold}Usage:${c.reset} ${c.cyan}${name}${c.reset} <command> [options]
|
|
380
|
+
|
|
381
|
+
${c.bold}Commands:${c.reset}
|
|
382
|
+
${c.cyan}install, i${c.reset} Install skills from skills.json ${c.dim}(default)${c.reset}
|
|
383
|
+
${c.cyan}add${c.reset} Add a skill source to skills.json
|
|
384
|
+
|
|
385
|
+
${c.bold}Options:${c.reset}
|
|
386
|
+
${c.cyan}-h, --help${c.reset} Show help for a command
|
|
387
|
+
${c.cyan}-v, --version${c.reset} Show version number
|
|
388
|
+
|
|
389
|
+
${c.bold}Examples:${c.reset}
|
|
390
|
+
${c.dim}$${c.reset} ${name} ${c.dim}# Install all skills${c.reset}
|
|
391
|
+
${c.dim}$${c.reset} ${name} add vercel-labs/skills ${c.dim}# Add a skill source${c.reset}
|
|
392
|
+
${c.dim}$${c.reset} ${name} add owner/repo:pdf,commit ${c.dim}# Add specific skills${c.reset}
|
|
393
|
+
${c.dim}$${c.reset} ${name} add org/a:skill1 org/b:skill2 ${c.dim}# Add multiple sources${c.reset}
|
|
394
|
+
|
|
395
|
+
Run ${c.cyan}${name} <command> --help${c.reset} for command-specific help.
|
|
396
|
+
`);
|
|
397
|
+
}
|
|
398
|
+
main().catch((error) => {
|
|
399
|
+
console.error(`${c.red}error:${c.reset} ${error instanceof Error ? error.message : error}`);
|
|
400
|
+
process.exitCode = 1;
|
|
401
|
+
});
|
|
402
|
+
//#endregion
|
|
403
|
+
export { main, parseSource };
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skill-forger",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Manage project Agent skills declaratively with skills.json",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Marc Mansour",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/marcm8793/skillforge.git"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"bin": {
|
|
13
|
+
"skill-forger": "./dist/cli.mjs",
|
|
14
|
+
"sf": "./dist/cli.mjs"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"skills_schema.json"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"skills",
|
|
22
|
+
"cli",
|
|
23
|
+
"agent-skills",
|
|
24
|
+
"ai-agents",
|
|
25
|
+
"claude-code"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "obuild",
|
|
29
|
+
"dev": "vitest --watch",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"check": "ultracite check",
|
|
32
|
+
"fix": "ultracite fix",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"prepack": "bun run build"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"skills": "^1.4.6"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@biomejs/biome": "2.4.7",
|
|
41
|
+
"@types/bun": "latest",
|
|
42
|
+
"jiti": "^2.6.1",
|
|
43
|
+
"obuild": "^0.4.32",
|
|
44
|
+
"typescript": "^5",
|
|
45
|
+
"ultracite": "7.3.2",
|
|
46
|
+
"vitest": "^4.1.2"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://unpkg.com/skill-forger/skills_schema.json",
|
|
4
|
+
"title": "Skills Config",
|
|
5
|
+
"description": "Configuration file for skill-forger - manages project skills",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["skills"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "JSON Schema reference"
|
|
12
|
+
},
|
|
13
|
+
"skills": {
|
|
14
|
+
"type": "array",
|
|
15
|
+
"description": "List of skill sources to install",
|
|
16
|
+
"items": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"required": ["source"],
|
|
19
|
+
"properties": {
|
|
20
|
+
"source": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Skill source identifier (e.g., 'vercel-labs/skills')"
|
|
23
|
+
},
|
|
24
|
+
"skills": {
|
|
25
|
+
"type": "array",
|
|
26
|
+
"description": "Specific skills to install from this source (empty array or omitted = all)",
|
|
27
|
+
"items": {
|
|
28
|
+
"type": "string"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|