@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.
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/dist/cli.js +418 -0
- 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
|
+
}
|