@rohilaharsh/skills-pm 0.1.1

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -0
  3. package/dist/cli.js +418 -0
  4. package/package.json +39 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Harsh Rohila
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,148 @@
1
+ # skills-pm
2
+
3
+ A package manager for [Cursor](https://cursor.com) agent skills. Install skills from public or private GitHub repositories with a single command.
4
+
5
+ ## Prerequisites
6
+
7
+ - [Node.js](https://nodejs.org) >= 18 or [Bun](https://bun.sh)
8
+ - Git (for cloning repositories)
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ # Run without installing
14
+ npx @rohilaharsh/skills-pm <command>
15
+ bunx @rohilaharsh/skills-pm <command>
16
+
17
+ # Or install globally
18
+ npm install -g @rohilaharsh/skills-pm
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ### Install a skill
24
+
25
+ ```bash
26
+ # From a public repo (owner/repo shorthand)
27
+ skills-pm add vercel-labs/agent-skills -s frontend-design
28
+
29
+ # From a full GitHub URL
30
+ skills-pm add https://github.com/vercel-labs/agent-skills -s frontend-design
31
+
32
+ # From a specific branch
33
+ skills-pm add owner/repo -s my-skill --ref develop
34
+
35
+ # From a specific tag
36
+ skills-pm add owner/repo -s my-skill --ref v1.2.0
37
+
38
+ # From a specific commit SHA
39
+ skills-pm add owner/repo -s my-skill --ref abc123f
40
+
41
+ # Install globally (available across all projects)
42
+ skills-pm add owner/repo -s my-skill -g
43
+ ```
44
+
45
+ The `-s` (or `--skill`) flag is required. You must specify the skill name to install.
46
+
47
+ ### Private repositories
48
+
49
+ Private repos work automatically if you have git credentials configured (SSH keys, HTTPS credentials, or a git credential helper). No extra setup needed.
50
+
51
+ ```bash
52
+ skills-pm add my-org/private-skills-repo -s internal-skill
53
+ ```
54
+
55
+ ### List installed skills
56
+
57
+ ```bash
58
+ # Show all installed skills (project + global)
59
+ skills-pm list
60
+
61
+ # Show only global skills
62
+ skills-pm list -g
63
+
64
+ # Alias
65
+ skills-pm ls
66
+ ```
67
+
68
+ ### Remove a skill
69
+
70
+ ```bash
71
+ # Remove from project
72
+ skills-pm remove my-skill
73
+
74
+ # Remove from global
75
+ skills-pm remove my-skill -g
76
+
77
+ # Alias
78
+ skills-pm rm my-skill
79
+ ```
80
+
81
+ ## How it works
82
+
83
+ 1. **Clone** — The repository is shallow-cloned into `~/.cache/skills-pm/<owner>/<repo>/<ref>/`
84
+ 2. **Discover** — SKILL.md files are found by scanning [standard search paths](https://github.com/vercel-labs/skills#skill-discovery) used by the agent skills ecosystem
85
+ 3. **Filter** — The skill matching the `-s` name is selected
86
+ 4. **Symlink** — The skill directory is symlinked into the target location
87
+ 5. **Record** — Metadata is written to track installed skills
88
+
89
+ ### Installation paths
90
+
91
+ | Scope | Skills directory | Metadata file |
92
+ |-------|-----------------|---------------|
93
+ | Project (default) | `.agents/skills/<name>/` | `.skills-pm.json` |
94
+ | Global (`-g`) | `~/.cursor/skills/<name>/` | `~/.cache/skills-pm/global.json` |
95
+
96
+ ### Skill discovery
97
+
98
+ The tool searches repositories using the same paths as [vercel-labs/skills](https://github.com/vercel-labs/skills):
99
+
100
+ - Root `SKILL.md`
101
+ - `skills/`, `skills/.curated/`, `skills/.experimental/`, `skills/.system/`
102
+ - Agent-specific directories (`.agents/skills/`, `.claude/skills/`, `.cursor/skills/`, etc.)
103
+ - Recursive fallback if no skills found in standard locations
104
+
105
+ A valid `SKILL.md` file must have YAML frontmatter with `name` and `description`:
106
+
107
+ ```markdown
108
+ ---
109
+ name: my-skill
110
+ description: What this skill does
111
+ ---
112
+
113
+ # My Skill
114
+
115
+ Instructions for the agent...
116
+ ```
117
+
118
+ ## Options reference
119
+
120
+ | Option | Description |
121
+ |--------|-------------|
122
+ | `-s, --skill <name>` | Skill name to install (required for `add`) |
123
+ | `--ref <ref>` | Git branch, tag, or commit SHA (default: default branch) |
124
+ | `-g, --global` | Use global scope instead of project |
125
+ | `-h, --help` | Show help message |
126
+
127
+ ## Development
128
+
129
+ ```bash
130
+ # Install dependencies
131
+ bun install
132
+
133
+ # Run tests
134
+ bun test
135
+
136
+ # Build for distribution
137
+ bun run build
138
+
139
+ # Run from source
140
+ bun run src/cli.ts add owner/repo -s skill-name
141
+
142
+ # Run the built version
143
+ node dist/cli.js add owner/repo -s skill-name
144
+ ```
145
+
146
+ ## License
147
+
148
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,418 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { parseArgs } from "util";
5
+
6
+ // src/source-parser.ts
7
+ function parseSource(input) {
8
+ if (!input) {
9
+ throw new Error("Source cannot be empty");
10
+ }
11
+ if (input.startsWith("https://github.com/")) {
12
+ const path = input.replace("https://github.com/", "").replace(/\.git$/, "").replace(/\/$/, "");
13
+ return parseShorthand(path);
14
+ }
15
+ return parseShorthand(input);
16
+ }
17
+ function parseShorthand(path) {
18
+ const parts = path.split("/");
19
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
20
+ throw new Error(`Invalid source "${path}". Expected format: owner/repo or https://github.com/owner/repo`);
21
+ }
22
+ return { owner: parts[0], repo: parts[1] };
23
+ }
24
+
25
+ // src/git.ts
26
+ import { join } from "path";
27
+ import { stat } from "fs/promises";
28
+ import { execFile } from "child_process";
29
+ function execGit(args) {
30
+ return new Promise((resolve, reject) => {
31
+ execFile("git", args, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
32
+ if (error) {
33
+ reject(new Error(`git ${args[0]} failed (exit ${error.code}): ${stderr.trim()}`));
34
+ } else {
35
+ resolve({ stdout, stderr });
36
+ }
37
+ });
38
+ });
39
+ }
40
+ async function cloneRepo(source, options) {
41
+ const refLabel = options.ref ?? "HEAD";
42
+ const destDir = join(options.cacheBase, source.owner, source.repo, refLabel);
43
+ if (await dirExists(destDir)) {
44
+ return destDir;
45
+ }
46
+ const url = `https://github.com/${source.owner}/${source.repo}.git`;
47
+ const cloneArgs = ["clone", "--depth", "1"];
48
+ if (options.ref) {
49
+ cloneArgs.push("--branch", options.ref);
50
+ }
51
+ cloneArgs.push(url, destDir);
52
+ await execGit(cloneArgs);
53
+ return destDir;
54
+ }
55
+ async function dirExists(path) {
56
+ try {
57
+ const s = await stat(path);
58
+ return s.isDirectory();
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ // src/discover.ts
65
+ import { join as join2 } from "path";
66
+ import { readdir, stat as stat2 } from "fs/promises";
67
+ var SEARCH_DIRS = [
68
+ "skills",
69
+ "skills/.curated",
70
+ "skills/.experimental",
71
+ "skills/.system",
72
+ ".agents/skills",
73
+ ".augment/skills",
74
+ ".claude/skills",
75
+ ".codebuddy/skills",
76
+ ".commandcode/skills",
77
+ ".continue/skills",
78
+ ".cortex/skills",
79
+ ".crush/skills",
80
+ ".factory/skills",
81
+ ".goose/skills",
82
+ ".junie/skills",
83
+ ".iflow/skills",
84
+ ".kilocode/skills",
85
+ ".kiro/skills",
86
+ ".kode/skills",
87
+ ".mcpjam/skills",
88
+ ".vibe/skills",
89
+ ".mux/skills",
90
+ ".openhands/skills",
91
+ ".pi/skills",
92
+ ".qoder/skills",
93
+ ".qwen/skills",
94
+ ".roo/skills",
95
+ ".trae/skills",
96
+ ".windsurf/skills",
97
+ ".zencoder/skills",
98
+ ".neovate/skills",
99
+ ".pochi/skills",
100
+ ".adal/skills"
101
+ ];
102
+ async function discoverSkillPaths(repoDir) {
103
+ const found = [];
104
+ const rootSkill = join2(repoDir, "SKILL.md");
105
+ if (await fileExists(rootSkill)) {
106
+ found.push(rootSkill);
107
+ }
108
+ for (const dir of SEARCH_DIRS) {
109
+ const fullDir = join2(repoDir, dir);
110
+ if (!await dirExists2(fullDir))
111
+ continue;
112
+ const entries = await readdir(fullDir, { withFileTypes: true });
113
+ for (const entry of entries) {
114
+ if (!entry.isDirectory())
115
+ continue;
116
+ const skillMd = join2(fullDir, entry.name, "SKILL.md");
117
+ if (await fileExists(skillMd)) {
118
+ found.push(skillMd);
119
+ }
120
+ }
121
+ }
122
+ if (found.length > 0)
123
+ return found;
124
+ return recursiveFind(repoDir);
125
+ }
126
+ async function recursiveFind(dir) {
127
+ const results = [];
128
+ const entries = await readdir(dir, { withFileTypes: true });
129
+ for (const entry of entries) {
130
+ const fullPath = join2(dir, entry.name);
131
+ if (entry.isDirectory()) {
132
+ if (entry.name === "node_modules" || entry.name === ".git")
133
+ continue;
134
+ results.push(...await recursiveFind(fullPath));
135
+ } else if (entry.name === "SKILL.md") {
136
+ results.push(fullPath);
137
+ }
138
+ }
139
+ return results;
140
+ }
141
+ async function fileExists(path) {
142
+ try {
143
+ const s = await stat2(path);
144
+ return s.isFile();
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+ async function dirExists2(path) {
150
+ try {
151
+ const s = await stat2(path);
152
+ return s.isDirectory();
153
+ } catch {
154
+ return false;
155
+ }
156
+ }
157
+
158
+ // src/parse-skill.ts
159
+ import matter from "gray-matter";
160
+ import { readFile } from "fs/promises";
161
+ import { dirname } from "path";
162
+ async function parseSkill(skillMdPath) {
163
+ const content = await readFile(skillMdPath, "utf-8");
164
+ const { data } = matter(content);
165
+ if (!data.name) {
166
+ throw new Error(`SKILL.md at ${skillMdPath} missing required field: name`);
167
+ }
168
+ if (!data.description) {
169
+ throw new Error(`SKILL.md at ${skillMdPath} missing required field: description`);
170
+ }
171
+ return {
172
+ name: data.name,
173
+ description: data.description,
174
+ dir: dirname(skillMdPath)
175
+ };
176
+ }
177
+
178
+ // src/install.ts
179
+ import { symlink, mkdir as mkdir2 } from "fs/promises";
180
+ import { join as join3 } from "path";
181
+
182
+ // src/metadata.ts
183
+ import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
184
+ import { dirname as dirname2 } from "path";
185
+ async function readMetadata(metaPath) {
186
+ try {
187
+ const content = await readFile2(metaPath, "utf-8");
188
+ return JSON.parse(content);
189
+ } catch {
190
+ return { skills: {} };
191
+ }
192
+ }
193
+ async function writeSkillEntry(metaPath, name, entry) {
194
+ const meta = await readMetadata(metaPath);
195
+ meta.skills[name] = entry;
196
+ await mkdir(dirname2(metaPath), { recursive: true });
197
+ await writeFile(metaPath, JSON.stringify(meta, null, 2) + `
198
+ `);
199
+ }
200
+ async function removeSkillEntry(metaPath, name) {
201
+ const meta = await readMetadata(metaPath);
202
+ delete meta.skills[name];
203
+ await writeFile(metaPath, JSON.stringify(meta, null, 2) + `
204
+ `);
205
+ }
206
+
207
+ // src/install.ts
208
+ async function installSkill(options) {
209
+ const linkPath = join3(options.targetBase, options.name);
210
+ await mkdir2(options.targetBase, { recursive: true });
211
+ await symlink(options.sourceDir, linkPath);
212
+ await writeSkillEntry(options.metaPath, options.name, {
213
+ source: options.source,
214
+ ref: options.ref,
215
+ skillDir: options.sourceDir,
216
+ installedAt: new Date().toISOString()
217
+ });
218
+ }
219
+
220
+ // src/commands/add.ts
221
+ async function addSkill(options) {
222
+ const skillPaths = await discoverSkillPaths(options.repoDir);
223
+ if (skillPaths.length === 0) {
224
+ throw new Error(`No skills found in ${options.repoDir}`);
225
+ }
226
+ const skills = [];
227
+ for (const path of skillPaths) {
228
+ try {
229
+ skills.push(await parseSkill(path));
230
+ } catch {}
231
+ }
232
+ const match = skills.find((s) => s.name.toLowerCase() === options.skillName.toLowerCase());
233
+ if (!match) {
234
+ const available = skills.map((s) => s.name).join(", ");
235
+ throw new Error(`Skill "${options.skillName}" not found. Available: ${available}`);
236
+ }
237
+ await installSkill({
238
+ name: match.name,
239
+ sourceDir: match.dir,
240
+ targetBase: options.targetBase,
241
+ metaPath: options.metaPath,
242
+ source: options.source,
243
+ ref: options.ref
244
+ });
245
+ return {
246
+ name: match.name,
247
+ description: match.description,
248
+ installedTo: options.targetBase
249
+ };
250
+ }
251
+
252
+ // src/commands/list.ts
253
+ async function listSkills(options) {
254
+ const result = { project: [], global: [] };
255
+ if (!options.globalOnly) {
256
+ const projectMeta = await readMetadata(options.projectMetaPath);
257
+ result.project = Object.entries(projectMeta.skills).map(([name, entry]) => ({
258
+ name,
259
+ source: entry.source,
260
+ ref: entry.ref,
261
+ installedAt: entry.installedAt
262
+ }));
263
+ }
264
+ const globalMeta = await readMetadata(options.globalMetaPath);
265
+ result.global = Object.entries(globalMeta.skills).map(([name, entry]) => ({
266
+ name,
267
+ source: entry.source,
268
+ ref: entry.ref,
269
+ installedAt: entry.installedAt
270
+ }));
271
+ return result;
272
+ }
273
+
274
+ // src/commands/remove.ts
275
+ import { rm } from "fs/promises";
276
+ import { join as join4 } from "path";
277
+ async function removeSkill(options) {
278
+ const meta = await readMetadata(options.metaPath);
279
+ if (!meta.skills[options.name]) {
280
+ throw new Error(`Skill "${options.name}" is not installed`);
281
+ }
282
+ const linkPath = join4(options.targetBase, options.name);
283
+ await rm(linkPath, { force: true });
284
+ await removeSkillEntry(options.metaPath, options.name);
285
+ }
286
+
287
+ // src/paths.ts
288
+ import { join as join5 } from "path";
289
+ import { homedir } from "os";
290
+ function getProjectPaths(cwd) {
291
+ return {
292
+ targetBase: join5(cwd, ".agents", "skills"),
293
+ metaPath: join5(cwd, ".skills-pm.json")
294
+ };
295
+ }
296
+ function getGlobalPaths() {
297
+ return {
298
+ targetBase: join5(homedir(), ".cursor", "skills"),
299
+ metaPath: join5(homedir(), ".cache", "skills-pm", "global.json")
300
+ };
301
+ }
302
+ function getCacheBase() {
303
+ return join5(homedir(), ".cache", "skills-pm");
304
+ }
305
+
306
+ // src/cli.ts
307
+ var HELP_TEXT = `skills-pm - Cursor Skills Package Manager
308
+
309
+ Usage:
310
+ skills-pm add <repo> -s <skill-name> [--ref <branch|SHA>] [-g]
311
+ skills-pm list [-g]
312
+ skills-pm remove <skill-name> [-g]
313
+
314
+ Options:
315
+ -s, --skill <name> Skill name to install (required for add)
316
+ --ref <ref> Git branch, tag, or commit SHA (default: HEAD)
317
+ -g, --global Install/list/remove globally (~/.cursor/skills/)
318
+ -h, --help Show this help message`;
319
+ var { values, positionals } = parseArgs({
320
+ args: process.argv.slice(2),
321
+ options: {
322
+ skill: { type: "string", short: "s" },
323
+ ref: { type: "string" },
324
+ global: { type: "boolean", short: "g", default: false },
325
+ help: { type: "boolean", short: "h", default: false }
326
+ },
327
+ allowPositionals: true,
328
+ strict: true
329
+ });
330
+ var command = positionals[0];
331
+ if (!command || values.help) {
332
+ console.log(HELP_TEXT);
333
+ process.exit(0);
334
+ }
335
+ function formatSkillLine(s) {
336
+ const refSuffix = s.ref !== "HEAD" ? ` @ ${s.ref}` : "";
337
+ return ` ${s.name} (${s.source}${refSuffix})`;
338
+ }
339
+ async function handleAdd() {
340
+ const repoArg = positionals[1];
341
+ if (!repoArg) {
342
+ console.error("Error: repository is required. Usage: skills-pm add <owner/repo> -s <skill-name>");
343
+ process.exit(1);
344
+ }
345
+ const skillName = values.skill;
346
+ if (!skillName) {
347
+ console.error("Error: --skill (-s) is required. Usage: skills-pm add <owner/repo> -s <skill-name>");
348
+ process.exit(1);
349
+ }
350
+ const source = parseSource(repoArg);
351
+ const sourceStr = `${source.owner}/${source.repo}`;
352
+ const ref = values.ref;
353
+ const refLabel = ref ? ` (ref: ${ref})` : "";
354
+ console.log(`Cloning ${sourceStr}${refLabel}...`);
355
+ const repoDir = await cloneRepo(source, {
356
+ cacheBase: getCacheBase(),
357
+ ref
358
+ });
359
+ const paths = values.global ? getGlobalPaths() : getProjectPaths(process.cwd());
360
+ const result = await addSkill({
361
+ repoDir,
362
+ skillName,
363
+ targetBase: paths.targetBase,
364
+ metaPath: paths.metaPath,
365
+ source: sourceStr,
366
+ ref: ref ?? "HEAD"
367
+ });
368
+ console.log(`Installed "${result.name}" to ${result.installedTo}`);
369
+ }
370
+ async function handleList() {
371
+ const result = await listSkills({
372
+ projectMetaPath: getProjectPaths(process.cwd()).metaPath,
373
+ globalMetaPath: getGlobalPaths().metaPath,
374
+ globalOnly: values.global
375
+ });
376
+ if (result.project.length === 0 && result.global.length === 0) {
377
+ console.log("No skills installed.");
378
+ return;
379
+ }
380
+ if (result.project.length > 0) {
381
+ console.log("Project skills:");
382
+ result.project.forEach((s) => console.log(formatSkillLine(s)));
383
+ }
384
+ if (result.global.length > 0) {
385
+ console.log("Global skills:");
386
+ result.global.forEach((s) => console.log(formatSkillLine(s)));
387
+ }
388
+ }
389
+ async function handleRemove() {
390
+ const skillName = positionals[1];
391
+ if (!skillName) {
392
+ console.error("Error: skill name is required. Usage: skills-pm remove <skill-name>");
393
+ process.exit(1);
394
+ }
395
+ const paths = values.global ? getGlobalPaths() : getProjectPaths(process.cwd());
396
+ await removeSkill({
397
+ name: skillName,
398
+ targetBase: paths.targetBase,
399
+ metaPath: paths.metaPath
400
+ });
401
+ console.log(`Removed "${skillName}"`);
402
+ }
403
+ var commands = {
404
+ add: handleAdd,
405
+ list: handleList,
406
+ ls: handleList,
407
+ remove: handleRemove,
408
+ rm: handleRemove
409
+ };
410
+ var handler = commands[command];
411
+ if (!handler) {
412
+ console.error(`Unknown command: ${command}`);
413
+ process.exit(1);
414
+ }
415
+ handler().catch((err) => {
416
+ console.error(`Error: ${err.message}`);
417
+ process.exit(1);
418
+ });
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@rohilaharsh/skills-pm",
3
+ "version": "0.1.1",
4
+ "description": "A package manager for Cursor agent skills. Install skills from public or private GitHub repositories.",
5
+ "type": "module",
6
+ "main": "dist/cli.js",
7
+ "bin": {
8
+ "skills-pm": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "bun build src/cli.ts --outdir dist --target node --packages external",
15
+ "test": "bun test",
16
+ "prepublishOnly": "bun test && bun run build",
17
+ "start": "bun run src/cli.ts",
18
+ "release": "bash scripts/publish.sh"
19
+ },
20
+ "keywords": [
21
+ "cursor",
22
+ "skills",
23
+ "agent",
24
+ "ai",
25
+ "cli",
26
+ "package-manager"
27
+ ],
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/user/skills-pm"
32
+ },
33
+ "devDependencies": {
34
+ "@types/bun": "latest"
35
+ },
36
+ "dependencies": {
37
+ "gray-matter": "^4.0.3"
38
+ }
39
+ }