skillship 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/cli.js +722 -0
- package/package.json +46 -0
- package/templates/AGENTS.md +29 -0
- package/templates/README.md +27 -0
- package/templates/SKILL.md +16 -0
- package/templates/claude-md.md +5 -0
- package/templates/cursor-rule.mdc +11 -0
- package/templates/release-please-config.json +23 -0
- package/templates/release.yml +30 -0
- package/templates/validate.yml +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 skillship contributors
|
|
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,156 @@
|
|
|
1
|
+
# skillship
|
|
2
|
+
|
|
3
|
+
Make any [Agent Skill](https://agentskills.io/specification) (a `SKILL.md`
|
|
4
|
+
directory) portable across **Cursor**, **Claude Code**, **Claude Web**, and
|
|
5
|
+
**Claude Cowork**.
|
|
6
|
+
|
|
7
|
+
`skillship` is a thin orchestration layer. It does **not** reimplement the
|
|
8
|
+
multi-agent install matrix (that's [`npx skills`](https://skills.sh)) or host a
|
|
9
|
+
registry. It adds the three things the ecosystem is missing:
|
|
10
|
+
|
|
11
|
+
1. Strict, per-surface **validation profiles** (notably Claude's 200-char upload
|
|
12
|
+
cap, which the official validator doesn't enforce).
|
|
13
|
+
2. **`.skill` packaging** for Claude Web / Cowork uploads.
|
|
14
|
+
3. **`init` scaffolding** with reusable release-please CI and commit
|
|
15
|
+
conventions.
|
|
16
|
+
|
|
17
|
+
## Install / usage
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx skillship <command>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires Node.js >= 18.
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
skillship validate <dir> [--profile <p>] [--json]
|
|
29
|
+
skillship package <dir> [--out <dir>]
|
|
30
|
+
skillship install <dir> [--agent <a,b>] [--global] [--copy]
|
|
31
|
+
skillship init [name] [--ci] [--snippets]
|
|
32
|
+
skillship doctor
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`<dir>` defaults to `.` and must contain a `SKILL.md`. Validation exits non-zero
|
|
36
|
+
on failure.
|
|
37
|
+
|
|
38
|
+
### validate
|
|
39
|
+
|
|
40
|
+
Parses the `SKILL.md` YAML frontmatter (`name`, `description`, optional
|
|
41
|
+
`license`, `metadata`, `allowed-tools`) and body, then applies checks per
|
|
42
|
+
profile:
|
|
43
|
+
|
|
44
|
+
| Check | spec | cursor | claude-web | claude-cowork |
|
|
45
|
+
| --- | --- | --- | --- | --- |
|
|
46
|
+
| `name` present, lowercase/numbers/hyphens | yes | yes | yes | yes |
|
|
47
|
+
| `name` matches parent folder | yes | yes | yes | yes |
|
|
48
|
+
| `description` non-empty, no `<`/`>` | yes | yes | yes | yes |
|
|
49
|
+
| `description` length | <= 1024 | <= 1024 | **<= 200** | **<= 200** |
|
|
50
|
+
| Body recommended <= 500 lines | warn | warn | warn | warn |
|
|
51
|
+
|
|
52
|
+
`--profile` is one of `spec | cursor | claude-web | claude-cowork | all`
|
|
53
|
+
(default `all`, the strictest combination — description must be <= 200 chars).
|
|
54
|
+
`--json` emits machine-readable output for CI.
|
|
55
|
+
|
|
56
|
+
The frontmatter parser handles YAML block scalars (`>`, `>-`, `>+`, `|`, `|-`,
|
|
57
|
+
`|+`) and nested maps (e.g. `metadata:` with indented children) without
|
|
58
|
+
mis-joining keys. If `agentskills` (the Python spec validator) is on `PATH`, its
|
|
59
|
+
findings are merged in; it is never a hard dependency.
|
|
60
|
+
|
|
61
|
+
### package
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
skillship package ./my-skill # -> dist/my-skill.skill
|
|
65
|
+
skillship package ./my-skill --out out # -> out/my-skill.skill
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Runs `validate --profile all` first (aborts on failure), then produces a
|
|
69
|
+
`<name>.skill` zip whose **archive root is the skill folder** (entries are
|
|
70
|
+
`<name>/SKILL.md`, ...) — Claude rejects archives with files at the zip root.
|
|
71
|
+
Excludes `__pycache__/`, `.DS_Store`, `node_modules/`, `dist/`, `.git/`.
|
|
72
|
+
|
|
73
|
+
### install
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
skillship install ./my-skill -a cursor,claude-code
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
For filesystem agents, shells out to `npx skills add <dir> [--global] [--copy]
|
|
80
|
+
-a <agents>`. Default agents are `cursor,claude-code`. For upload-only surfaces
|
|
81
|
+
(`claude-web`, `claude-cowork`) it prints upload instructions instead.
|
|
82
|
+
|
|
83
|
+
### init
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
skillship init demo --ci --snippets
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Scaffolds a skill repo (see layout below) that auto-releases via
|
|
90
|
+
[release-please](https://github.com/googleapis/release-please-action) +
|
|
91
|
+
[Conventional Commits](https://www.conventionalcommits.org/). `--ci` adds the
|
|
92
|
+
GitHub Actions workflows; `--snippets` adds `cursor-rule.mdc` and
|
|
93
|
+
`claude-md.md`.
|
|
94
|
+
|
|
95
|
+
Scaffolded layout:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
my-skill/
|
|
99
|
+
my-skill/SKILL.md
|
|
100
|
+
snippets/ # if --snippets
|
|
101
|
+
cursor-rule.mdc
|
|
102
|
+
claude-md.md
|
|
103
|
+
release-please-config.json
|
|
104
|
+
.release-please-manifest.json
|
|
105
|
+
version.txt
|
|
106
|
+
.github/workflows/{validate,release}.yml
|
|
107
|
+
AGENTS.md
|
|
108
|
+
README.md
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
> After pushing the scaffolded repo, enable **Settings -> Actions -> Workflow
|
|
112
|
+
> permissions**: "Read and write" and "Allow GitHub Actions to create and
|
|
113
|
+
> approve pull requests" so release-please can open release PRs and upload the
|
|
114
|
+
> `.skill` asset.
|
|
115
|
+
|
|
116
|
+
The `SKILL.md` version line uses an inline marker so release-please updates it
|
|
117
|
+
in
|
|
118
|
+
place and the validator ignores it:
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
metadata:
|
|
122
|
+
version: "1.0.0" # x-release-please-version
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### doctor
|
|
126
|
+
|
|
127
|
+
Checks the local environment: Node >= 18 and `npx` (required), plus `gh` and
|
|
128
|
+
`agentskills` (optional).
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npm install
|
|
134
|
+
npm run build # tsup -> dist/cli.js
|
|
135
|
+
npm run lint # tsc --noEmit
|
|
136
|
+
npm test # vitest
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Project layout:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
src/
|
|
143
|
+
cli.ts # arg parsing, command dispatch (commander)
|
|
144
|
+
commands/{validate,package,install,init,doctor}.ts
|
|
145
|
+
lib/frontmatter.ts # YAML frontmatter parser (block scalars + maps)
|
|
146
|
+
lib/profiles.ts # profile definitions and checks
|
|
147
|
+
lib/zip.ts # .skill packaging (archiver)
|
|
148
|
+
lib/exec.ts # spawn wrappers for npx skills / gh / agentskills
|
|
149
|
+
lib/load.ts # SKILL.md loader
|
|
150
|
+
templates/ # CI + snippet + AGENTS/README/SKILL templates for init
|
|
151
|
+
test/ # vitest specs + fixtures
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/lib/load.ts
|
|
7
|
+
import { readFile } from "fs/promises";
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import { join, resolve } from "path";
|
|
10
|
+
|
|
11
|
+
// src/lib/frontmatter.ts
|
|
12
|
+
var FENCE = /^---\s*$/;
|
|
13
|
+
function splitFrontmatter(content) {
|
|
14
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
15
|
+
const lines = normalized.split("\n");
|
|
16
|
+
if (lines.length === 0 || !FENCE.test(lines[0])) {
|
|
17
|
+
return { raw: "", body: normalized };
|
|
18
|
+
}
|
|
19
|
+
let end = -1;
|
|
20
|
+
for (let i = 1; i < lines.length; i++) {
|
|
21
|
+
if (FENCE.test(lines[i])) {
|
|
22
|
+
end = i;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (end === -1) {
|
|
27
|
+
return { raw: "", body: normalized };
|
|
28
|
+
}
|
|
29
|
+
const raw = lines.slice(1, end).join("\n");
|
|
30
|
+
const body = lines.slice(end + 1).join("\n");
|
|
31
|
+
return { raw, body };
|
|
32
|
+
}
|
|
33
|
+
function tokenize(raw) {
|
|
34
|
+
return raw.split("\n").map((raw2) => {
|
|
35
|
+
const match = raw2.match(/^(\s*)(.*)$/);
|
|
36
|
+
const indent = match ? match[1].length : 0;
|
|
37
|
+
const text = match ? match[2] : raw2;
|
|
38
|
+
return { indent, text, raw: raw2 };
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function stripComment(value) {
|
|
42
|
+
let inSingle = false;
|
|
43
|
+
let inDouble = false;
|
|
44
|
+
for (let i = 0; i < value.length; i++) {
|
|
45
|
+
const ch = value[i];
|
|
46
|
+
if (ch === "'" && !inDouble) inSingle = !inSingle;
|
|
47
|
+
else if (ch === '"' && !inSingle) inDouble = !inDouble;
|
|
48
|
+
else if (ch === "#" && !inSingle && !inDouble) {
|
|
49
|
+
if (i === 0 || /\s/.test(value[i - 1])) {
|
|
50
|
+
return value.slice(0, i).trimEnd();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
function unquote(value) {
|
|
57
|
+
const v = value.trim();
|
|
58
|
+
if (v.length >= 2) {
|
|
59
|
+
if (v.startsWith('"') && v.endsWith('"')) {
|
|
60
|
+
return v.slice(1, -1).replace(/\\"/g, '"').replace(/\\n/g, "\n");
|
|
61
|
+
}
|
|
62
|
+
if (v.startsWith("'") && v.endsWith("'")) {
|
|
63
|
+
return v.slice(1, -1).replace(/''/g, "'");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return v;
|
|
67
|
+
}
|
|
68
|
+
var BLOCK_SCALAR = /^([|>])([+-]?)\s*$/;
|
|
69
|
+
function collectBlockScalar(lines, start, parentIndent, style, chomp) {
|
|
70
|
+
const collected = [];
|
|
71
|
+
let i = start;
|
|
72
|
+
let blockIndent = -1;
|
|
73
|
+
for (; i < lines.length; i++) {
|
|
74
|
+
const line = lines[i];
|
|
75
|
+
const isBlank = line.text === "";
|
|
76
|
+
if (isBlank) {
|
|
77
|
+
collected.push("");
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (line.indent <= parentIndent) break;
|
|
81
|
+
if (blockIndent === -1) blockIndent = line.indent;
|
|
82
|
+
collected.push(line.raw.slice(blockIndent));
|
|
83
|
+
}
|
|
84
|
+
let value;
|
|
85
|
+
if (style === ">") {
|
|
86
|
+
const parts = [];
|
|
87
|
+
let buffer = [];
|
|
88
|
+
const flush = () => {
|
|
89
|
+
if (buffer.length) {
|
|
90
|
+
parts.push(buffer.join(" "));
|
|
91
|
+
buffer = [];
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
for (const l of collected) {
|
|
95
|
+
if (l === "") {
|
|
96
|
+
flush();
|
|
97
|
+
parts.push("");
|
|
98
|
+
} else {
|
|
99
|
+
buffer.push(l);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
flush();
|
|
103
|
+
value = parts.join("\n").replace(/\n{2,}/g, "\n");
|
|
104
|
+
} else {
|
|
105
|
+
value = collected.join("\n");
|
|
106
|
+
}
|
|
107
|
+
if (chomp === "-") {
|
|
108
|
+
value = value.replace(/\n+$/, "");
|
|
109
|
+
} else if (chomp === "+") {
|
|
110
|
+
} else {
|
|
111
|
+
value = value.replace(/\n+$/, "\n");
|
|
112
|
+
value = value.replace(/\n$/, "");
|
|
113
|
+
}
|
|
114
|
+
value = value.trim();
|
|
115
|
+
return { value, next: i };
|
|
116
|
+
}
|
|
117
|
+
function parseFrontmatter(raw) {
|
|
118
|
+
const fm = {};
|
|
119
|
+
if (!raw.trim()) return fm;
|
|
120
|
+
const lines = tokenize(raw);
|
|
121
|
+
let i = 0;
|
|
122
|
+
while (i < lines.length) {
|
|
123
|
+
const line = lines[i];
|
|
124
|
+
if (line.text === "" || line.text.startsWith("#")) {
|
|
125
|
+
i++;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (line.indent !== 0) {
|
|
129
|
+
i++;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const colon = line.text.indexOf(":");
|
|
133
|
+
if (colon === -1) {
|
|
134
|
+
i++;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const key = line.text.slice(0, colon).trim();
|
|
138
|
+
let rest = line.text.slice(colon + 1).trim();
|
|
139
|
+
const blockMatch = rest.match(BLOCK_SCALAR);
|
|
140
|
+
if (blockMatch) {
|
|
141
|
+
const style = blockMatch[1];
|
|
142
|
+
const chomp = blockMatch[2] || "";
|
|
143
|
+
const { value, next } = collectBlockScalar(
|
|
144
|
+
lines,
|
|
145
|
+
i + 1,
|
|
146
|
+
line.indent,
|
|
147
|
+
style,
|
|
148
|
+
chomp
|
|
149
|
+
);
|
|
150
|
+
fm[key] = value;
|
|
151
|
+
i = next;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (rest === "") {
|
|
155
|
+
const childIndent = line.indent;
|
|
156
|
+
const children = {};
|
|
157
|
+
let j = i + 1;
|
|
158
|
+
let sawChild = false;
|
|
159
|
+
for (; j < lines.length; j++) {
|
|
160
|
+
const child = lines[j];
|
|
161
|
+
if (child.text === "" || child.text.startsWith("#")) continue;
|
|
162
|
+
if (child.indent <= childIndent) break;
|
|
163
|
+
const cColon = child.text.indexOf(":");
|
|
164
|
+
if (cColon === -1) continue;
|
|
165
|
+
const cKey = child.text.slice(0, cColon).trim();
|
|
166
|
+
const cVal = unquote(stripComment(child.text.slice(cColon + 1).trim()));
|
|
167
|
+
children[cKey] = cVal;
|
|
168
|
+
sawChild = true;
|
|
169
|
+
}
|
|
170
|
+
if (sawChild) {
|
|
171
|
+
fm[key] = children;
|
|
172
|
+
} else {
|
|
173
|
+
fm[key] = "";
|
|
174
|
+
}
|
|
175
|
+
i = j;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (rest.startsWith("[") && rest.endsWith("]")) {
|
|
179
|
+
const inner = rest.slice(1, -1).trim();
|
|
180
|
+
fm[key] = inner ? inner.split(",").map((s) => unquote(s.trim())) : [];
|
|
181
|
+
i++;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
rest = unquote(stripComment(rest));
|
|
185
|
+
fm[key] = rest;
|
|
186
|
+
i++;
|
|
187
|
+
}
|
|
188
|
+
return fm;
|
|
189
|
+
}
|
|
190
|
+
function parseSkill(content) {
|
|
191
|
+
const { raw, body } = splitFrontmatter(content);
|
|
192
|
+
return {
|
|
193
|
+
frontmatter: parseFrontmatter(raw),
|
|
194
|
+
body
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/lib/load.ts
|
|
199
|
+
async function loadSkill(dir) {
|
|
200
|
+
const abs = resolve(dir);
|
|
201
|
+
const skillMdPath = join(abs, "SKILL.md");
|
|
202
|
+
if (!existsSync(skillMdPath)) {
|
|
203
|
+
throw new Error(`No SKILL.md found in ${abs}`);
|
|
204
|
+
}
|
|
205
|
+
const content = await readFile(skillMdPath, "utf8");
|
|
206
|
+
return { dir: abs, skillMdPath, parsed: parseSkill(content) };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/lib/profiles.ts
|
|
210
|
+
import { basename } from "path";
|
|
211
|
+
var PROFILE_NAMES = [
|
|
212
|
+
"spec",
|
|
213
|
+
"cursor",
|
|
214
|
+
"claude-web",
|
|
215
|
+
"claude-cowork",
|
|
216
|
+
"all"
|
|
217
|
+
];
|
|
218
|
+
var DESCRIPTION_MAX = {
|
|
219
|
+
spec: 1024,
|
|
220
|
+
cursor: 1024,
|
|
221
|
+
"claude-web": 200,
|
|
222
|
+
"claude-cowork": 200
|
|
223
|
+
};
|
|
224
|
+
var BODY_RECOMMENDED_MAX_LINES = 500;
|
|
225
|
+
var NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
226
|
+
function descriptionMax(profile) {
|
|
227
|
+
if (profile === "all") {
|
|
228
|
+
return Math.min(...Object.values(DESCRIPTION_MAX));
|
|
229
|
+
}
|
|
230
|
+
return DESCRIPTION_MAX[profile];
|
|
231
|
+
}
|
|
232
|
+
function checkName(fm, folderName, findings) {
|
|
233
|
+
const name = typeof fm.name === "string" ? fm.name : void 0;
|
|
234
|
+
if (!name) {
|
|
235
|
+
findings.push({
|
|
236
|
+
severity: "error",
|
|
237
|
+
check: "name-present",
|
|
238
|
+
message: "`name` is missing from frontmatter."
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (!NAME_RE.test(name)) {
|
|
243
|
+
findings.push({
|
|
244
|
+
severity: "error",
|
|
245
|
+
check: "name-format",
|
|
246
|
+
message: `\`name\` "${name}" must be lowercase letters, numbers, and single hyphens (no leading/trailing/double hyphens).`
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (name !== folderName) {
|
|
250
|
+
findings.push({
|
|
251
|
+
severity: "error",
|
|
252
|
+
check: "name-matches-folder",
|
|
253
|
+
message: `\`name\` "${name}" must match the parent folder "${folderName}".`
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function checkDescription(fm, max, findings) {
|
|
258
|
+
const desc = typeof fm.description === "string" ? fm.description : void 0;
|
|
259
|
+
if (!desc || desc.trim() === "") {
|
|
260
|
+
findings.push({
|
|
261
|
+
severity: "error",
|
|
262
|
+
check: "description-present",
|
|
263
|
+
message: "`description` is missing or empty."
|
|
264
|
+
});
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (/[<>]/.test(desc)) {
|
|
268
|
+
findings.push({
|
|
269
|
+
severity: "error",
|
|
270
|
+
check: "description-xml",
|
|
271
|
+
message: "`description` must not contain `<` or `>` characters."
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (desc.length > max) {
|
|
275
|
+
findings.push({
|
|
276
|
+
severity: "error",
|
|
277
|
+
check: "description-length",
|
|
278
|
+
message: `\`description\` is ${desc.length} chars; limit is ${max}.`
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function checkBody(body, findings) {
|
|
283
|
+
const lineCount = body.split("\n").length;
|
|
284
|
+
if (lineCount > BODY_RECOMMENDED_MAX_LINES) {
|
|
285
|
+
findings.push({
|
|
286
|
+
severity: "warning",
|
|
287
|
+
check: "body-length",
|
|
288
|
+
message: `Body is ${lineCount} lines; recommended <= ${BODY_RECOMMENDED_MAX_LINES}.`
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function validateProfile(skill, skillDir, profile) {
|
|
293
|
+
const findings = [];
|
|
294
|
+
const folderName = basename(skillDir);
|
|
295
|
+
checkName(skill.frontmatter, folderName, findings);
|
|
296
|
+
checkDescription(skill.frontmatter, descriptionMax(profile), findings);
|
|
297
|
+
checkBody(skill.body, findings);
|
|
298
|
+
const ok = !findings.some((f) => f.severity === "error");
|
|
299
|
+
return { profile, ok, findings };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/lib/exec.ts
|
|
303
|
+
import { spawn, spawnSync } from "child_process";
|
|
304
|
+
function buildSkillsAddArgv(opts) {
|
|
305
|
+
const argv = ["skills", "add", opts.dir];
|
|
306
|
+
if (opts.global) argv.push("--global");
|
|
307
|
+
if (opts.copy) argv.push("--copy");
|
|
308
|
+
if (opts.agents.length > 0) argv.push("-a", opts.agents.join(","));
|
|
309
|
+
return argv;
|
|
310
|
+
}
|
|
311
|
+
function run(command, args, opts = {}) {
|
|
312
|
+
return new Promise((resolve3, reject) => {
|
|
313
|
+
const child = spawn(command, args, {
|
|
314
|
+
cwd: opts.cwd,
|
|
315
|
+
stdio: "inherit",
|
|
316
|
+
shell: false
|
|
317
|
+
});
|
|
318
|
+
child.on("error", reject);
|
|
319
|
+
child.on("close", (code) => resolve3(code ?? 1));
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
function isAvailable(command) {
|
|
323
|
+
const probe = process.platform === "win32" ? "where" : "which";
|
|
324
|
+
const res = spawnSync(probe, [command], { stdio: "ignore" });
|
|
325
|
+
return res.status === 0;
|
|
326
|
+
}
|
|
327
|
+
function runCapture(command, args, opts = {}) {
|
|
328
|
+
const res = spawnSync(command, args, {
|
|
329
|
+
cwd: opts.cwd,
|
|
330
|
+
encoding: "utf8"
|
|
331
|
+
});
|
|
332
|
+
return {
|
|
333
|
+
code: res.status ?? 1,
|
|
334
|
+
stdout: res.stdout ?? "",
|
|
335
|
+
stderr: res.stderr ?? ""
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/commands/validate.ts
|
|
340
|
+
function profilesToRun(profile) {
|
|
341
|
+
if (profile === "all") return ["all"];
|
|
342
|
+
return [profile];
|
|
343
|
+
}
|
|
344
|
+
function mergeAgentskills(dir, results) {
|
|
345
|
+
if (!isAvailable("agentskills")) return;
|
|
346
|
+
const res = runCapture("agentskills", ["validate", dir]);
|
|
347
|
+
if (res.code !== 0) {
|
|
348
|
+
const message = (res.stderr || res.stdout || "agentskills reported errors").trim();
|
|
349
|
+
for (const r of results) {
|
|
350
|
+
r.findings.push({
|
|
351
|
+
severity: "error",
|
|
352
|
+
check: "agentskills",
|
|
353
|
+
message
|
|
354
|
+
});
|
|
355
|
+
r.ok = false;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function validateCommand(dir, options) {
|
|
360
|
+
const profileArg = options.profile ?? "all";
|
|
361
|
+
if (!PROFILE_NAMES.includes(profileArg)) {
|
|
362
|
+
process.stderr.write(
|
|
363
|
+
`Unknown profile "${profileArg}". Valid: ${PROFILE_NAMES.join(", ")}
|
|
364
|
+
`
|
|
365
|
+
);
|
|
366
|
+
return 2;
|
|
367
|
+
}
|
|
368
|
+
let loaded;
|
|
369
|
+
try {
|
|
370
|
+
loaded = await loadSkill(dir);
|
|
371
|
+
} catch (err) {
|
|
372
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
373
|
+
if (options.json) {
|
|
374
|
+
process.stdout.write(
|
|
375
|
+
JSON.stringify({ ok: false, error: msg }, null, 2) + "\n"
|
|
376
|
+
);
|
|
377
|
+
} else {
|
|
378
|
+
process.stderr.write(`Error: ${msg}
|
|
379
|
+
`);
|
|
380
|
+
}
|
|
381
|
+
return 1;
|
|
382
|
+
}
|
|
383
|
+
const results = profilesToRun(profileArg).map(
|
|
384
|
+
(p) => validateProfile(loaded.parsed, loaded.dir, p)
|
|
385
|
+
);
|
|
386
|
+
mergeAgentskills(loaded.dir, results);
|
|
387
|
+
const ok = results.every((r) => r.ok);
|
|
388
|
+
if (options.json) {
|
|
389
|
+
process.stdout.write(JSON.stringify({ ok, results }, null, 2) + "\n");
|
|
390
|
+
} else {
|
|
391
|
+
printHuman(results, ok);
|
|
392
|
+
}
|
|
393
|
+
return ok ? 0 : 1;
|
|
394
|
+
}
|
|
395
|
+
function printHuman(results, ok) {
|
|
396
|
+
for (const result of results) {
|
|
397
|
+
const errors = result.findings.filter((f) => f.severity === "error");
|
|
398
|
+
const warnings = result.findings.filter((f) => f.severity === "warning");
|
|
399
|
+
const status = result.ok ? "PASS" : "FAIL";
|
|
400
|
+
process.stdout.write(`[${status}] profile: ${result.profile}
|
|
401
|
+
`);
|
|
402
|
+
for (const f of [...errors, ...warnings]) printFinding(f);
|
|
403
|
+
}
|
|
404
|
+
process.stdout.write(ok ? "\nAll checks passed.\n" : "\nValidation failed.\n");
|
|
405
|
+
}
|
|
406
|
+
function printFinding(f) {
|
|
407
|
+
const tag = f.severity === "error" ? " \u2717" : " \u26A0";
|
|
408
|
+
process.stdout.write(`${tag} ${f.check}: ${f.message}
|
|
409
|
+
`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/commands/package.ts
|
|
413
|
+
import { join as join3 } from "path";
|
|
414
|
+
|
|
415
|
+
// src/lib/zip.ts
|
|
416
|
+
import { createWriteStream } from "fs";
|
|
417
|
+
import { mkdir, readdir } from "fs/promises";
|
|
418
|
+
import { join as join2, relative } from "path";
|
|
419
|
+
import archiver from "archiver";
|
|
420
|
+
var EXCLUDED_DIRS = /* @__PURE__ */ new Set([
|
|
421
|
+
"__pycache__",
|
|
422
|
+
"node_modules",
|
|
423
|
+
"dist",
|
|
424
|
+
".git"
|
|
425
|
+
]);
|
|
426
|
+
var EXCLUDED_FILES = /* @__PURE__ */ new Set([".DS_Store"]);
|
|
427
|
+
async function collectFiles(root) {
|
|
428
|
+
const out = [];
|
|
429
|
+
async function walk(dir) {
|
|
430
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
431
|
+
for (const entry of entries) {
|
|
432
|
+
const full = join2(dir, entry.name);
|
|
433
|
+
if (entry.isDirectory()) {
|
|
434
|
+
if (EXCLUDED_DIRS.has(entry.name)) continue;
|
|
435
|
+
await walk(full);
|
|
436
|
+
} else if (entry.isFile()) {
|
|
437
|
+
if (EXCLUDED_FILES.has(entry.name)) continue;
|
|
438
|
+
out.push(full);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
await walk(root);
|
|
443
|
+
return out.sort();
|
|
444
|
+
}
|
|
445
|
+
async function packSkill(opts) {
|
|
446
|
+
const { skillDir, name, outDir } = opts;
|
|
447
|
+
await mkdir(outDir, { recursive: true });
|
|
448
|
+
const outPath = join2(outDir, `${name}.skill`);
|
|
449
|
+
const files = await collectFiles(skillDir);
|
|
450
|
+
await new Promise((resolve3, reject) => {
|
|
451
|
+
const output = createWriteStream(outPath);
|
|
452
|
+
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
453
|
+
output.on("close", () => resolve3());
|
|
454
|
+
output.on("error", reject);
|
|
455
|
+
archive.on("error", reject);
|
|
456
|
+
archive.pipe(output);
|
|
457
|
+
for (const file of files) {
|
|
458
|
+
const rel = relative(skillDir, file);
|
|
459
|
+
archive.file(file, { name: `${name}/${rel}` });
|
|
460
|
+
}
|
|
461
|
+
void archive.finalize();
|
|
462
|
+
});
|
|
463
|
+
return outPath;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/commands/package.ts
|
|
467
|
+
async function packageCommand(dir, options) {
|
|
468
|
+
let loaded;
|
|
469
|
+
try {
|
|
470
|
+
loaded = await loadSkill(dir);
|
|
471
|
+
} catch (err) {
|
|
472
|
+
process.stderr.write(
|
|
473
|
+
`Error: ${err instanceof Error ? err.message : String(err)}
|
|
474
|
+
`
|
|
475
|
+
);
|
|
476
|
+
return 1;
|
|
477
|
+
}
|
|
478
|
+
const result = validateProfile(loaded.parsed, loaded.dir, "all");
|
|
479
|
+
if (!result.ok) {
|
|
480
|
+
process.stderr.write("Cannot package: validation failed (--profile all).\n");
|
|
481
|
+
for (const f of result.findings.filter((x) => x.severity === "error")) {
|
|
482
|
+
process.stderr.write(` \u2717 ${f.check}: ${f.message}
|
|
483
|
+
`);
|
|
484
|
+
}
|
|
485
|
+
process.stderr.write("\nRun `skillship validate` for details.\n");
|
|
486
|
+
return 1;
|
|
487
|
+
}
|
|
488
|
+
const name = String(loaded.parsed.frontmatter.name);
|
|
489
|
+
const outDir = options.out ?? join3(process.cwd(), "dist");
|
|
490
|
+
const outPath = await packSkill({ skillDir: loaded.dir, name, outDir });
|
|
491
|
+
process.stdout.write(`Packaged ${name} -> ${outPath}
|
|
492
|
+
`);
|
|
493
|
+
return 0;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/commands/install.ts
|
|
497
|
+
var DEFAULT_AGENTS = ["cursor", "claude-code"];
|
|
498
|
+
var UPLOAD_ONLY = /* @__PURE__ */ new Set(["claude-web", "claude-cowork"]);
|
|
499
|
+
async function installCommand(dir, options) {
|
|
500
|
+
let loaded;
|
|
501
|
+
try {
|
|
502
|
+
loaded = await loadSkill(dir);
|
|
503
|
+
} catch (err) {
|
|
504
|
+
process.stderr.write(
|
|
505
|
+
`Error: ${err instanceof Error ? err.message : String(err)}
|
|
506
|
+
`
|
|
507
|
+
);
|
|
508
|
+
return 1;
|
|
509
|
+
}
|
|
510
|
+
const requested = options.agent ? options.agent.split(",").map((a) => a.trim()).filter(Boolean) : DEFAULT_AGENTS;
|
|
511
|
+
const uploadOnly = requested.filter((a) => UPLOAD_ONLY.has(a));
|
|
512
|
+
const filesystem = requested.filter((a) => !UPLOAD_ONLY.has(a));
|
|
513
|
+
if (uploadOnly.length > 0) {
|
|
514
|
+
printUploadInstructions(uploadOnly, String(loaded.parsed.frontmatter.name));
|
|
515
|
+
}
|
|
516
|
+
if (filesystem.length === 0) return 0;
|
|
517
|
+
if (!isAvailable("npx")) {
|
|
518
|
+
process.stderr.write(
|
|
519
|
+
"Error: `npx` not found. Install Node.js (>=18). Run `skillship doctor`.\n"
|
|
520
|
+
);
|
|
521
|
+
return 1;
|
|
522
|
+
}
|
|
523
|
+
const argv = buildSkillsAddArgv({
|
|
524
|
+
dir: loaded.dir,
|
|
525
|
+
agents: filesystem,
|
|
526
|
+
global: options.global,
|
|
527
|
+
copy: options.copy
|
|
528
|
+
});
|
|
529
|
+
process.stdout.write(`Running: npx ${argv.join(" ")}
|
|
530
|
+
`);
|
|
531
|
+
const code = await run("npx", argv);
|
|
532
|
+
return code;
|
|
533
|
+
}
|
|
534
|
+
function printUploadInstructions(agents, name) {
|
|
535
|
+
process.stdout.write(
|
|
536
|
+
`
|
|
537
|
+
The following surfaces are upload-only (no filesystem install): ${agents.join(", ")}
|
|
538
|
+
`
|
|
539
|
+
);
|
|
540
|
+
process.stdout.write(
|
|
541
|
+
`Run \`skillship package .\` then upload \`dist/${name}.skill\`.
|
|
542
|
+
`
|
|
543
|
+
);
|
|
544
|
+
if (agents.includes("claude-web")) {
|
|
545
|
+
process.stdout.write(
|
|
546
|
+
" Claude Web: Settings -> Capabilities -> Upload skill -> enable toggle.\n"
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
if (agents.includes("claude-cowork")) {
|
|
550
|
+
process.stdout.write(
|
|
551
|
+
" Claude Cowork: Customize -> Skills -> Upload (desktop app only).\n"
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
process.stdout.write("\n");
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// src/commands/init.ts
|
|
558
|
+
import { fileURLToPath } from "url";
|
|
559
|
+
import { dirname, join as join4, resolve as resolve2 } from "path";
|
|
560
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
|
|
561
|
+
import { existsSync as existsSync2 } from "fs";
|
|
562
|
+
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
563
|
+
function templatesDir() {
|
|
564
|
+
const candidates = [
|
|
565
|
+
join4(__dirname2, "..", "templates"),
|
|
566
|
+
// dist/cli.js -> repo/templates
|
|
567
|
+
join4(__dirname2, "..", "..", "templates"),
|
|
568
|
+
// src/commands -> repo/templates
|
|
569
|
+
join4(__dirname2, "templates")
|
|
570
|
+
];
|
|
571
|
+
for (const c of candidates) {
|
|
572
|
+
if (existsSync2(c)) return c;
|
|
573
|
+
}
|
|
574
|
+
return candidates[0];
|
|
575
|
+
}
|
|
576
|
+
async function renderTemplate(file, name) {
|
|
577
|
+
const raw = await readFile2(join4(templatesDir(), file), "utf8");
|
|
578
|
+
return raw.replaceAll("{{name}}", name);
|
|
579
|
+
}
|
|
580
|
+
async function emit(target, content) {
|
|
581
|
+
await mkdir2(dirname(target), { recursive: true });
|
|
582
|
+
await writeFile(target, content);
|
|
583
|
+
}
|
|
584
|
+
async function initCommand(name, options) {
|
|
585
|
+
const skillName = name ?? "my-skill";
|
|
586
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(skillName)) {
|
|
587
|
+
process.stderr.write(
|
|
588
|
+
`Invalid skill name "${skillName}". Use lowercase letters, numbers, and single hyphens.
|
|
589
|
+
`
|
|
590
|
+
);
|
|
591
|
+
return 1;
|
|
592
|
+
}
|
|
593
|
+
const root = resolve2(process.cwd(), skillName);
|
|
594
|
+
if (existsSync2(root)) {
|
|
595
|
+
process.stderr.write(`Error: directory "${skillName}" already exists.
|
|
596
|
+
`);
|
|
597
|
+
return 1;
|
|
598
|
+
}
|
|
599
|
+
const writes = [];
|
|
600
|
+
writes.push([
|
|
601
|
+
join4(root, skillName, "SKILL.md"),
|
|
602
|
+
await renderTemplate("SKILL.md", skillName)
|
|
603
|
+
]);
|
|
604
|
+
writes.push([join4(root, "README.md"), await renderTemplate("README.md", skillName)]);
|
|
605
|
+
writes.push([join4(root, "AGENTS.md"), await renderTemplate("AGENTS.md", skillName)]);
|
|
606
|
+
writes.push([
|
|
607
|
+
join4(root, "release-please-config.json"),
|
|
608
|
+
await renderTemplate("release-please-config.json", skillName)
|
|
609
|
+
]);
|
|
610
|
+
writes.push([
|
|
611
|
+
join4(root, ".release-please-manifest.json"),
|
|
612
|
+
JSON.stringify({ ".": "1.0.0" }, null, 2) + "\n"
|
|
613
|
+
]);
|
|
614
|
+
writes.push([join4(root, "version.txt"), "1.0.0\n"]);
|
|
615
|
+
if (options.ci) {
|
|
616
|
+
writes.push([
|
|
617
|
+
join4(root, ".github", "workflows", "validate.yml"),
|
|
618
|
+
await renderTemplate("validate.yml", skillName)
|
|
619
|
+
]);
|
|
620
|
+
writes.push([
|
|
621
|
+
join4(root, ".github", "workflows", "release.yml"),
|
|
622
|
+
await renderTemplate("release.yml", skillName)
|
|
623
|
+
]);
|
|
624
|
+
}
|
|
625
|
+
if (options.snippets) {
|
|
626
|
+
writes.push([
|
|
627
|
+
join4(root, "snippets", "cursor-rule.mdc"),
|
|
628
|
+
await renderTemplate("cursor-rule.mdc", skillName)
|
|
629
|
+
]);
|
|
630
|
+
writes.push([
|
|
631
|
+
join4(root, "snippets", "claude-md.md"),
|
|
632
|
+
await renderTemplate("claude-md.md", skillName)
|
|
633
|
+
]);
|
|
634
|
+
}
|
|
635
|
+
for (const [target, content] of writes) {
|
|
636
|
+
await emit(target, content);
|
|
637
|
+
}
|
|
638
|
+
process.stdout.write(`Scaffolded ${skillName}/ (${writes.length} files)
|
|
639
|
+
`);
|
|
640
|
+
process.stdout.write(` cd ${skillName}
|
|
641
|
+
`);
|
|
642
|
+
process.stdout.write(` npx skillship validate ${skillName} --profile all
|
|
643
|
+
`);
|
|
644
|
+
return 0;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/commands/doctor.ts
|
|
648
|
+
async function doctorCommand() {
|
|
649
|
+
const checks = [];
|
|
650
|
+
const nodeOk = nodeVersionOk();
|
|
651
|
+
checks.push({
|
|
652
|
+
name: "node >= 18",
|
|
653
|
+
required: true,
|
|
654
|
+
ok: nodeOk,
|
|
655
|
+
detail: process.version
|
|
656
|
+
});
|
|
657
|
+
const npx = isAvailable("npx");
|
|
658
|
+
checks.push({
|
|
659
|
+
name: "npx (for `skills add`)",
|
|
660
|
+
required: true,
|
|
661
|
+
ok: npx,
|
|
662
|
+
detail: npx ? "found" : "missing \u2014 install Node.js"
|
|
663
|
+
});
|
|
664
|
+
const gh = isAvailable("gh");
|
|
665
|
+
checks.push({
|
|
666
|
+
name: "gh (GitHub CLI, for releases)",
|
|
667
|
+
required: false,
|
|
668
|
+
ok: gh,
|
|
669
|
+
detail: gh ? "found" : "optional \u2014 needed for release uploads"
|
|
670
|
+
});
|
|
671
|
+
const agentskills = isAvailable("agentskills");
|
|
672
|
+
checks.push({
|
|
673
|
+
name: "agentskills (optional spec validator)",
|
|
674
|
+
required: false,
|
|
675
|
+
ok: agentskills,
|
|
676
|
+
detail: agentskills ? versionOf("agentskills") : "optional"
|
|
677
|
+
});
|
|
678
|
+
for (const c of checks) {
|
|
679
|
+
const mark = c.ok ? "\u2713" : c.required ? "\u2717" : "\u2013";
|
|
680
|
+
process.stdout.write(` ${mark} ${c.name}: ${c.detail}
|
|
681
|
+
`);
|
|
682
|
+
}
|
|
683
|
+
const failedRequired = checks.some((c) => c.required && !c.ok);
|
|
684
|
+
process.stdout.write(
|
|
685
|
+
failedRequired ? "\nMissing required dependencies.\n" : "\nEnvironment looks good.\n"
|
|
686
|
+
);
|
|
687
|
+
return failedRequired ? 1 : 0;
|
|
688
|
+
}
|
|
689
|
+
function nodeVersionOk() {
|
|
690
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
691
|
+
return major >= 18;
|
|
692
|
+
}
|
|
693
|
+
function versionOf(cmd) {
|
|
694
|
+
const res = runCapture(cmd, ["--version"]);
|
|
695
|
+
return (res.stdout || res.stderr).trim() || "found";
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/cli.ts
|
|
699
|
+
var program = new Command();
|
|
700
|
+
program.name("skillship").description(
|
|
701
|
+
"Make any Agent Skill (SKILL.md) portable across Cursor, Claude Code, Claude Web, and Claude Cowork."
|
|
702
|
+
).version("1.0.0");
|
|
703
|
+
program.command("validate").description("Validate a SKILL.md against per-surface profiles").argument("[dir]", "skill directory", ".").option("--profile <p>", "spec | cursor | claude-web | claude-cowork | all", "all").option("--json", "machine-readable output").action(async (dir, opts) => {
|
|
704
|
+
process.exit(await validateCommand(dir, opts));
|
|
705
|
+
});
|
|
706
|
+
program.command("package").description("Validate then build a .skill zip for Claude upload").argument("[dir]", "skill directory", ".").option("--out <dir>", "output directory", "dist").action(async (dir, opts) => {
|
|
707
|
+
process.exit(await packageCommand(dir, opts));
|
|
708
|
+
});
|
|
709
|
+
program.command("install").description("Install a skill via `npx skills`, or print upload instructions").argument("[dir]", "skill directory", ".").option("--agent <a,b>", "comma-separated agents").option("--global", "install globally").option("--copy", "copy instead of symlink").action(async (dir, opts) => {
|
|
710
|
+
process.exit(await installCommand(dir, opts));
|
|
711
|
+
});
|
|
712
|
+
program.command("init").description("Scaffold a new skill repo with release-please CI").argument("[name]", "skill name").option("--ci", "include GitHub Actions workflows").option("--snippets", "include cursor-rule.mdc and claude-md.md snippets").action(async (name, opts) => {
|
|
713
|
+
process.exit(await initCommand(name, opts));
|
|
714
|
+
});
|
|
715
|
+
program.command("doctor").description("Check the local environment for required/optional tools").action(async () => {
|
|
716
|
+
process.exit(await doctorCommand());
|
|
717
|
+
});
|
|
718
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
719
|
+
process.stderr.write(`${err instanceof Error ? err.stack : String(err)}
|
|
720
|
+
`);
|
|
721
|
+
process.exit(1);
|
|
722
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skillship",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Make any Agent Skill (SKILL.md) portable across Cursor, Claude Code, Claude Web, and Claude Cowork.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"skillship": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"templates",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"dev": "tsup --watch",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
22
|
+
"lint": "tsc --noEmit",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"agent-skills",
|
|
27
|
+
"skill",
|
|
28
|
+
"cursor",
|
|
29
|
+
"claude",
|
|
30
|
+
"cli"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"archiver": "^7.0.1",
|
|
35
|
+
"commander": "^12.1.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/archiver": "^6.0.3",
|
|
39
|
+
"@types/node": "^22.10.0",
|
|
40
|
+
"tsup": "^8.3.5",
|
|
41
|
+
"typescript": "^5.7.2",
|
|
42
|
+
"vitest": "^2.1.8",
|
|
43
|
+
"yauzl": "^3.2.0",
|
|
44
|
+
"@types/yauzl": "^2.10.3"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Repository guide for agents
|
|
2
|
+
|
|
3
|
+
This repo packages the `{{name}}` Agent Skill for distribution across Cursor,
|
|
4
|
+
Claude Code, Claude Web, and Claude Cowork.
|
|
5
|
+
|
|
6
|
+
## Layout
|
|
7
|
+
|
|
8
|
+
- `{{name}}/SKILL.md` — the skill itself (source of truth).
|
|
9
|
+
- `release-please-config.json`, `.release-please-manifest.json`, `version.txt` —
|
|
10
|
+
release automation via release-please + Conventional Commits.
|
|
11
|
+
- `.github/workflows/validate.yml` — validates the skill on PRs/pushes.
|
|
12
|
+
- `.github/workflows/release.yml` — cuts releases and uploads `{{name}}.skill`.
|
|
13
|
+
|
|
14
|
+
## Conventions
|
|
15
|
+
|
|
16
|
+
- Use Conventional Commits (`feat:`, `fix:`, `docs:`, ...). `feat`/`fix` bump
|
|
17
|
+
the
|
|
18
|
+
version; merging the release PR publishes `{{name}}.skill` to a GitHub
|
|
19
|
+
Release.
|
|
20
|
+
- Keep the `description` in `{{name}}/SKILL.md` <= 200 chars so it uploads to
|
|
21
|
+
Claude Web/Cowork.
|
|
22
|
+
- The version line in `SKILL.md` carries `# x-release-please-version` so
|
|
23
|
+
release-please updates it in place.
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
- `npx skillship validate {{name}} --profile all`
|
|
28
|
+
- `npx skillship package {{name}}`
|
|
29
|
+
- `npx skillship install {{name}} -a cursor,claude-code`
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# {{name}}
|
|
2
|
+
|
|
3
|
+
An [Agent Skill](https://agentskills.io/specification) packaged for Cursor,
|
|
4
|
+
Claude Code, Claude Web, and Claude Cowork.
|
|
5
|
+
|
|
6
|
+
## Develop
|
|
7
|
+
|
|
8
|
+
- Validate: `npx skillship validate {{name}} --profile all`
|
|
9
|
+
- Package: `npx skillship package {{name}}` (produces `dist/{{name}}.skill`)
|
|
10
|
+
- Install locally: `npx skillship install {{name}} -a cursor,claude-code`
|
|
11
|
+
|
|
12
|
+
## Upload to Claude
|
|
13
|
+
|
|
14
|
+
1. `npx skillship package {{name}}`
|
|
15
|
+
2. Upload `dist/{{name}}.skill`:
|
|
16
|
+
- Claude Web: Settings -> Capabilities -> Upload skill -> enable toggle.
|
|
17
|
+
- Claude Cowork: Customize -> Skills -> Upload (desktop app only).
|
|
18
|
+
|
|
19
|
+
## Releasing
|
|
20
|
+
|
|
21
|
+
This repo auto-releases with
|
|
22
|
+
[release-please](https://github.com/googleapis/release-please-action) using
|
|
23
|
+
[Conventional Commits](https://www.conventionalcommits.org/). Merging the
|
|
24
|
+
generated release PR publishes `{{name}}.skill` to a GitHub Release.
|
|
25
|
+
|
|
26
|
+
> Enable **Settings -> Actions -> Workflow permissions**: "Read and write" and
|
|
27
|
+
> "Allow GitHub Actions to create and approve pull requests".
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: {{name}}
|
|
3
|
+
description: Describe when an agent should use {{name}} and what it does. Keep this under 200 characters so it uploads cleanly to Claude Web and Cowork.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
version: "1.0.0" # x-release-please-version
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# {{name}}
|
|
10
|
+
|
|
11
|
+
Explain what this skill does and when an agent should use it.
|
|
12
|
+
|
|
13
|
+
## Instructions
|
|
14
|
+
|
|
15
|
+
1. Step one.
|
|
16
|
+
2. Step two.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
|
3
|
+
"packages": {
|
|
4
|
+
".": {
|
|
5
|
+
"release-type": "simple",
|
|
6
|
+
"changelog-sections": [
|
|
7
|
+
{ "type": "feat", "section": "Features" },
|
|
8
|
+
{ "type": "fix", "section": "Bug Fixes" },
|
|
9
|
+
{ "type": "perf", "section": "Performance" },
|
|
10
|
+
{ "type": "docs", "section": "Documentation" },
|
|
11
|
+
{ "type": "refactor", "section": "Refactors" },
|
|
12
|
+
{ "type": "ci", "section": "CI", "hidden": true },
|
|
13
|
+
{ "type": "chore", "section": "Chores", "hidden": true }
|
|
14
|
+
],
|
|
15
|
+
"extra-files": [
|
|
16
|
+
{
|
|
17
|
+
"type": "generic",
|
|
18
|
+
"path": "{{name}}/SKILL.md"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
permissions:
|
|
6
|
+
contents: write
|
|
7
|
+
pull-requests: write
|
|
8
|
+
jobs:
|
|
9
|
+
release:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: googleapis/release-please-action@v4
|
|
13
|
+
id: release
|
|
14
|
+
with:
|
|
15
|
+
config-file: release-please-config.json
|
|
16
|
+
manifest-file: .release-please-manifest.json
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
19
|
+
- uses: actions/setup-node@v4
|
|
20
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
21
|
+
with:
|
|
22
|
+
node-version: "20"
|
|
23
|
+
- name: Package skill
|
|
24
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
25
|
+
run: npx skillship package {{name}}
|
|
26
|
+
- name: Upload to release
|
|
27
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
28
|
+
env:
|
|
29
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
30
|
+
run: gh release upload "${{ steps.release.outputs.tag_name }}" dist/{{name}}.skill --clobber
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
name: validate
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
jobs:
|
|
7
|
+
validate:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
- uses: actions/setup-node@v4
|
|
12
|
+
with:
|
|
13
|
+
node-version: "20"
|
|
14
|
+
- run: npx skillship validate {{name}} --profile all
|