skills-init 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 +195 -0
- package/assets/banner.png +0 -0
- package/bin/skills-init.js +7 -0
- package/package.json +47 -0
- package/profiles/core.json +31 -0
- package/scripts/lint-skills.js +75 -0
- package/skills/adversarial-review/SKILL.md +233 -0
- package/skills/simplify/SKILL.md +227 -0
- package/skills/writing-skills/SKILL.md +244 -0
- package/src/cli.js +339 -0
- package/templates/agents/README.md +12 -0
- package/templates/mcps/README.md +6 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cpSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
readlinkSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
symlinkSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const packageRoot = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
16
|
+
const defaultProfile = "core";
|
|
17
|
+
|
|
18
|
+
class UsageError extends Error {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "UsageError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readJson(path) {
|
|
26
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function printHelp() {
|
|
30
|
+
console.log(`skills-init
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
skills-init [options]
|
|
34
|
+
|
|
35
|
+
Options:
|
|
36
|
+
--target <dir> Project directory to scaffold. Defaults to cwd.
|
|
37
|
+
--profile <name> Profile from profiles/<name>.json. Defaults to core.
|
|
38
|
+
--overwrite Replace existing installed skill directories.
|
|
39
|
+
--dry-run Print planned writes without changing files.
|
|
40
|
+
--no-links Copy skills into .agents only; do not create tool links.
|
|
41
|
+
--copy-links Copy skills into tool folders instead of symlinking.
|
|
42
|
+
--all-tool-links Create every link target listed in the profile.
|
|
43
|
+
--list Print available profiles and their skills.
|
|
44
|
+
--help Show this help.
|
|
45
|
+
--version Print package version.
|
|
46
|
+
`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseArgs(argv) {
|
|
50
|
+
const options = {
|
|
51
|
+
allToolLinks: false,
|
|
52
|
+
copyLinks: false,
|
|
53
|
+
dryRun: false,
|
|
54
|
+
links: true,
|
|
55
|
+
overwrite: false,
|
|
56
|
+
profile: defaultProfile,
|
|
57
|
+
target: process.cwd(),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
61
|
+
const arg = argv[index];
|
|
62
|
+
|
|
63
|
+
if (arg === "--help" || arg === "-h") {
|
|
64
|
+
options.help = true;
|
|
65
|
+
} else if (arg === "--version" || arg === "-v") {
|
|
66
|
+
options.version = true;
|
|
67
|
+
} else if (arg === "--list") {
|
|
68
|
+
options.list = true;
|
|
69
|
+
} else if (arg === "--target") {
|
|
70
|
+
const value = argv[++index];
|
|
71
|
+
if (!value) {
|
|
72
|
+
throw new UsageError("--target requires a directory");
|
|
73
|
+
}
|
|
74
|
+
options.target = value;
|
|
75
|
+
} else if (arg === "--profile") {
|
|
76
|
+
const value = argv[++index];
|
|
77
|
+
if (!value) {
|
|
78
|
+
throw new UsageError("--profile requires a name");
|
|
79
|
+
}
|
|
80
|
+
options.profile = value;
|
|
81
|
+
} else if (arg === "--overwrite" || arg === "--force") {
|
|
82
|
+
options.overwrite = true;
|
|
83
|
+
} else if (arg === "--dry-run") {
|
|
84
|
+
options.dryRun = true;
|
|
85
|
+
} else if (arg === "--no-links") {
|
|
86
|
+
options.links = false;
|
|
87
|
+
} else if (arg === "--copy-links") {
|
|
88
|
+
options.copyLinks = true;
|
|
89
|
+
} else if (arg === "--all-tool-links") {
|
|
90
|
+
options.allToolLinks = true;
|
|
91
|
+
} else {
|
|
92
|
+
throw new UsageError(`Unknown option: ${arg}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return options;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function availableProfiles() {
|
|
100
|
+
const profilesDir = join(packageRoot, "profiles");
|
|
101
|
+
return readdirSync(profilesDir)
|
|
102
|
+
.filter((name) => name.endsWith(".json"))
|
|
103
|
+
.map((name) => name.slice(0, -".json".length))
|
|
104
|
+
.sort();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function loadProfile(profileName) {
|
|
108
|
+
const profilePath = join(packageRoot, "profiles", `${profileName}.json`);
|
|
109
|
+
if (!existsSync(profilePath)) {
|
|
110
|
+
throw new UsageError(
|
|
111
|
+
`Unknown profile "${profileName}". Available profiles: ${availableProfiles().join(", ")}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return readJson(profilePath);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function logStep({ dryRun, message }) {
|
|
118
|
+
console.log(`${dryRun ? "[dry-run] " : ""}${message}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function ensureDir(path, options) {
|
|
122
|
+
if (options.dryRun) {
|
|
123
|
+
logStep({ dryRun: true, message: `mkdir -p ${path}` });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
mkdirSync(path, { recursive: true });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function copyDirectory({ from, to, label, options }) {
|
|
130
|
+
if (existsSync(to)) {
|
|
131
|
+
if (!options.overwrite) {
|
|
132
|
+
logStep({
|
|
133
|
+
dryRun: options.dryRun,
|
|
134
|
+
message: `skip existing ${label}: ${to}`,
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!options.dryRun) {
|
|
139
|
+
rmSync(to, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
logStep({
|
|
144
|
+
dryRun: options.dryRun,
|
|
145
|
+
message: `copy ${label}: ${relative(options.targetRoot, to) || "."}`,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (!options.dryRun) {
|
|
149
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
150
|
+
cpSync(from, to, { recursive: true, dereference: false });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function copyDirectoryContents({ from, to, label, options }) {
|
|
155
|
+
ensureDir(to, options);
|
|
156
|
+
|
|
157
|
+
for (const entry of readdirSync(from, { withFileTypes: true })) {
|
|
158
|
+
const sourcePath = join(from, entry.name);
|
|
159
|
+
const targetPath = join(to, entry.name);
|
|
160
|
+
|
|
161
|
+
copyDirectory({
|
|
162
|
+
from: sourcePath,
|
|
163
|
+
to: targetPath,
|
|
164
|
+
label: `${label}/${entry.name}`,
|
|
165
|
+
options,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function shouldInstallLinkTarget({ linkTarget, targetRoot, allToolLinks }) {
|
|
171
|
+
if (allToolLinks || linkTarget.mode === "always") {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (linkTarget.mode === "if-parent-exists") {
|
|
176
|
+
const topLevel = linkTarget.dir.split("/")[0];
|
|
177
|
+
return existsSync(join(targetRoot, topLevel));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function relativeLinkTarget({ fromPath, toPath }) {
|
|
184
|
+
const target = relative(dirname(fromPath), toPath);
|
|
185
|
+
return target.startsWith(".") ? target : `.${target ? `/${target}` : ""}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function installToolSkill({ skillName, linkDir, sourceSkillDir, options }) {
|
|
189
|
+
const linkPath = join(linkDir, skillName);
|
|
190
|
+
|
|
191
|
+
if (options.copyLinks) {
|
|
192
|
+
copyDirectory({
|
|
193
|
+
from: sourceSkillDir,
|
|
194
|
+
to: linkPath,
|
|
195
|
+
label: `${skillName} tool copy`,
|
|
196
|
+
options,
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const expectedTarget = relativeLinkTarget({
|
|
202
|
+
fromPath: linkPath,
|
|
203
|
+
toPath: sourceSkillDir,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (existsSync(linkPath)) {
|
|
207
|
+
const stat = lstatSync(linkPath);
|
|
208
|
+
if (!stat.isSymbolicLink()) {
|
|
209
|
+
throw new UsageError(
|
|
210
|
+
`${linkPath} already exists and is not a symlink. Move it aside or rerun with --copy-links.`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (readlinkSync(linkPath) === expectedTarget) {
|
|
215
|
+
logStep({
|
|
216
|
+
dryRun: options.dryRun,
|
|
217
|
+
message: `keep link ${relative(options.targetRoot, linkPath)} -> ${expectedTarget}`,
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
logStep({
|
|
223
|
+
dryRun: options.dryRun,
|
|
224
|
+
message: `replace link ${relative(options.targetRoot, linkPath)} -> ${expectedTarget}`,
|
|
225
|
+
});
|
|
226
|
+
if (!options.dryRun) {
|
|
227
|
+
rmSync(linkPath, { force: true });
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
logStep({
|
|
231
|
+
dryRun: options.dryRun,
|
|
232
|
+
message: `link ${relative(options.targetRoot, linkPath)} -> ${expectedTarget}`,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!options.dryRun) {
|
|
237
|
+
mkdirSync(dirname(linkPath), { recursive: true });
|
|
238
|
+
symlinkSync(expectedTarget, linkPath, process.platform === "win32" ? "junction" : "dir");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function installProfile({ profile, options }) {
|
|
243
|
+
const targetRoot = resolve(options.target);
|
|
244
|
+
const runOptions = { ...options, targetRoot };
|
|
245
|
+
const agentsDir = join(targetRoot, ".agents");
|
|
246
|
+
const agentsSkillsDir = join(agentsDir, "skills");
|
|
247
|
+
|
|
248
|
+
ensureDir(agentsSkillsDir, runOptions);
|
|
249
|
+
ensureDir(join(agentsDir, "mcps"), runOptions);
|
|
250
|
+
|
|
251
|
+
copyDirectoryContents({
|
|
252
|
+
from: join(packageRoot, "templates", "agents"),
|
|
253
|
+
to: agentsDir,
|
|
254
|
+
label: ".agents templates",
|
|
255
|
+
options: { ...runOptions, overwrite: false },
|
|
256
|
+
});
|
|
257
|
+
copyDirectoryContents({
|
|
258
|
+
from: join(packageRoot, "templates", "mcps"),
|
|
259
|
+
to: join(agentsDir, "mcps"),
|
|
260
|
+
label: ".agents/mcps templates",
|
|
261
|
+
options: { ...runOptions, overwrite: false },
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
for (const skillName of profile.skills) {
|
|
265
|
+
const sourceSkillDir = join(packageRoot, "skills", skillName);
|
|
266
|
+
const targetSkillDir = join(agentsSkillsDir, skillName);
|
|
267
|
+
if (!existsSync(join(sourceSkillDir, "SKILL.md"))) {
|
|
268
|
+
throw new UsageError(`Profile references missing skill: ${skillName}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
copyDirectory({
|
|
272
|
+
from: sourceSkillDir,
|
|
273
|
+
to: targetSkillDir,
|
|
274
|
+
label: `${skillName} skill`,
|
|
275
|
+
options: runOptions,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!options.links) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
for (const linkTarget of profile.toolLinks ?? []) {
|
|
284
|
+
if (
|
|
285
|
+
!shouldInstallLinkTarget({
|
|
286
|
+
allToolLinks: options.allToolLinks,
|
|
287
|
+
linkTarget,
|
|
288
|
+
targetRoot,
|
|
289
|
+
})
|
|
290
|
+
) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const linkDir = join(targetRoot, linkTarget.dir);
|
|
295
|
+
ensureDir(linkDir, runOptions);
|
|
296
|
+
|
|
297
|
+
for (const skillName of profile.skills) {
|
|
298
|
+
installToolSkill({
|
|
299
|
+
linkDir,
|
|
300
|
+
options: runOptions,
|
|
301
|
+
skillName,
|
|
302
|
+
sourceSkillDir: join(agentsSkillsDir, skillName),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function listProfiles() {
|
|
309
|
+
for (const profileName of availableProfiles()) {
|
|
310
|
+
const profile = loadProfile(profileName);
|
|
311
|
+
console.log(`${profile.name}: ${profile.description}`);
|
|
312
|
+
for (const skill of profile.skills) {
|
|
313
|
+
console.log(` - ${skill}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export async function run(argv) {
|
|
319
|
+
const options = parseArgs(argv);
|
|
320
|
+
|
|
321
|
+
if (options.help) {
|
|
322
|
+
printHelp();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (options.version) {
|
|
327
|
+
const packageJson = readJson(join(packageRoot, "package.json"));
|
|
328
|
+
console.log(packageJson.version);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (options.list) {
|
|
333
|
+
listProfiles();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const profile = loadProfile(options.profile);
|
|
338
|
+
installProfile({ options, profile });
|
|
339
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Agent Skills
|
|
2
|
+
|
|
3
|
+
This directory is the source of truth for project-local agent guidance.
|
|
4
|
+
|
|
5
|
+
- Put reusable skills under `.agents/skills/<skill-name>/SKILL.md`.
|
|
6
|
+
- Keep tool-specific directories (`.claude/skills`, `.cursor/skills`,
|
|
7
|
+
`.codex/skills`, `.opencode/skills`) as generated links back to this
|
|
8
|
+
directory.
|
|
9
|
+
- Put future MCP descriptors, setup notes, or templates under `.agents/mcps`
|
|
10
|
+
unless a tool requires a different physical location.
|
|
11
|
+
|
|
12
|
+
Regenerate installed skills by rerunning the package initializer.
|