@crustjs/skills 0.0.5 → 0.0.6
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/dist/index.js +10 -843
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,844 +1,11 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
var
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const base = scope === "global" ? homedir() : process.cwd();
|
|
13
|
-
switch (agent) {
|
|
14
|
-
case "claude-code":
|
|
15
|
-
return join(base, ".claude", "skills", name);
|
|
16
|
-
case "opencode":
|
|
17
|
-
if (scope === "global") {
|
|
18
|
-
return join(base, ".config", "opencode", "skills", name);
|
|
19
|
-
}
|
|
20
|
-
return join(base, ".opencode", "skills", name);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
async function detectInstalledAgents(home) {
|
|
24
|
-
const resolvedHome = home ?? homedir();
|
|
25
|
-
const detected = [];
|
|
26
|
-
for (const agent of ALL_AGENTS) {
|
|
27
|
-
const configDir = resolveAgentConfigDir(resolvedHome, agent);
|
|
28
|
-
const exists = await access(configDir).then(() => true).catch(() => false);
|
|
29
|
-
if (exists) {
|
|
30
|
-
detected.push(agent);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return detected;
|
|
34
|
-
}
|
|
35
|
-
function resolveAgentConfigDir(home, agent) {
|
|
36
|
-
switch (agent) {
|
|
37
|
-
case "claude-code":
|
|
38
|
-
return join(home, ".claude");
|
|
39
|
-
case "opencode":
|
|
40
|
-
return join(home, ".config", "opencode");
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
// src/errors.ts
|
|
44
|
-
class SkillConflictError extends Error {
|
|
45
|
-
name = "SkillConflictError";
|
|
46
|
-
details;
|
|
47
|
-
constructor(details) {
|
|
48
|
-
const message = `Skill conflict for agent "${details.agent}": ` + `directory "${details.outputDir}" already exists but was not created by Crust ` + `(no crust.json found). Delete or rename the conflicting skill to resolve.`;
|
|
49
|
-
super(message);
|
|
50
|
-
this.details = details;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
// src/generate.ts
|
|
54
|
-
import { access as access2, mkdir, rm, writeFile } from "fs/promises";
|
|
55
|
-
import { dirname, join as join3 } from "path";
|
|
56
|
-
|
|
57
|
-
// src/manifest.ts
|
|
58
|
-
function buildManifest(command) {
|
|
59
|
-
return buildNode(command, []);
|
|
60
|
-
}
|
|
61
|
-
function buildNode(command, parentPath) {
|
|
62
|
-
const name = normalizeName(command.meta.name);
|
|
63
|
-
const path = [...parentPath, name];
|
|
64
|
-
const args = normalizeArgs(command.args);
|
|
65
|
-
const flags = normalizeFlags(command.flags);
|
|
66
|
-
const children = normalizeChildren(command.subCommands, path);
|
|
67
|
-
return {
|
|
68
|
-
name,
|
|
69
|
-
path,
|
|
70
|
-
description: command.meta.description,
|
|
71
|
-
usage: command.meta.usage,
|
|
72
|
-
runnable: typeof command.run === "function",
|
|
73
|
-
args,
|
|
74
|
-
flags,
|
|
75
|
-
children
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
function normalizeName(raw) {
|
|
79
|
-
return raw.trim().toLowerCase();
|
|
80
|
-
}
|
|
81
|
-
function normalizeArgs(argsDef) {
|
|
82
|
-
if (!argsDef || argsDef.length === 0)
|
|
83
|
-
return [];
|
|
84
|
-
return argsDef.map(normalizeArg);
|
|
85
|
-
}
|
|
86
|
-
function normalizeArg(arg) {
|
|
87
|
-
const result = {
|
|
88
|
-
name: arg.name,
|
|
89
|
-
type: arg.type,
|
|
90
|
-
required: arg.required === true,
|
|
91
|
-
variadic: arg.variadic === true
|
|
92
|
-
};
|
|
93
|
-
if (arg.description !== undefined) {
|
|
94
|
-
result.description = arg.description;
|
|
95
|
-
}
|
|
96
|
-
if (arg.default !== undefined) {
|
|
97
|
-
result.default = serializeDefault(arg.default);
|
|
98
|
-
}
|
|
99
|
-
return result;
|
|
100
|
-
}
|
|
101
|
-
function normalizeFlags(flagsDef) {
|
|
102
|
-
if (!flagsDef)
|
|
103
|
-
return [];
|
|
104
|
-
const keys = Object.keys(flagsDef).sort();
|
|
105
|
-
return keys.map((key) => {
|
|
106
|
-
return normalizeFlag(key, flagsDef[key]);
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
function normalizeFlag(name, flag) {
|
|
110
|
-
const result = {
|
|
111
|
-
name,
|
|
112
|
-
type: flag.type,
|
|
113
|
-
required: flag.required === true,
|
|
114
|
-
multiple: flag.multiple === true,
|
|
115
|
-
aliases: normalizeAliases(flag.alias)
|
|
116
|
-
};
|
|
117
|
-
if (flag.description !== undefined) {
|
|
118
|
-
result.description = flag.description;
|
|
119
|
-
}
|
|
120
|
-
if (flag.default !== undefined) {
|
|
121
|
-
result.default = serializeDefault(flag.default);
|
|
122
|
-
}
|
|
123
|
-
return result;
|
|
124
|
-
}
|
|
125
|
-
function normalizeAliases(alias) {
|
|
126
|
-
if (alias === undefined)
|
|
127
|
-
return [];
|
|
128
|
-
if (typeof alias === "string")
|
|
129
|
-
return [alias];
|
|
130
|
-
return [...alias].sort();
|
|
131
|
-
}
|
|
132
|
-
function normalizeChildren(subCommands, parentPath) {
|
|
133
|
-
const keys = Object.keys(subCommands).sort();
|
|
134
|
-
return keys.map((key) => {
|
|
135
|
-
return buildNode(subCommands[key], parentPath);
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
function serializeDefault(value) {
|
|
139
|
-
if (Array.isArray(value)) {
|
|
140
|
-
return JSON.stringify(value);
|
|
141
|
-
}
|
|
142
|
-
return String(value);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// src/render.ts
|
|
146
|
-
function escapeYaml(value) {
|
|
147
|
-
if (/[:#[\]{}&*!|>'"`,@?\\]|^\s|\s$|^---|[\n\r]/.test(value)) {
|
|
148
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`;
|
|
149
|
-
}
|
|
150
|
-
return value;
|
|
151
|
-
}
|
|
152
|
-
function escapeTableCell(value) {
|
|
153
|
-
return value.replace(/(?<!\\)\|/g, "\\|");
|
|
154
|
-
}
|
|
155
|
-
function renderSkill(manifest, meta) {
|
|
156
|
-
const files = [];
|
|
157
|
-
const allNodes = collectNodes(manifest);
|
|
158
|
-
files.push({
|
|
159
|
-
path: "SKILL.md",
|
|
160
|
-
content: renderSkillMd(manifest, meta)
|
|
161
|
-
});
|
|
162
|
-
files.push({
|
|
163
|
-
path: "command-index.md",
|
|
164
|
-
content: renderCommandIndex(manifest, allNodes)
|
|
165
|
-
});
|
|
166
|
-
for (const node of allNodes) {
|
|
167
|
-
const filePath = commandFilePath(node);
|
|
168
|
-
const content = node.children.length > 0 ? renderGroupCommand(node, manifest) : renderLeafCommand(node, manifest);
|
|
169
|
-
files.push({ path: filePath, content });
|
|
170
|
-
}
|
|
171
|
-
return files;
|
|
172
|
-
}
|
|
173
|
-
function collectNodes(root) {
|
|
174
|
-
const nodes = [root];
|
|
175
|
-
for (const child of root.children) {
|
|
176
|
-
nodes.push(...collectNodes(child));
|
|
177
|
-
}
|
|
178
|
-
return nodes;
|
|
179
|
-
}
|
|
180
|
-
function commandFilePath(node) {
|
|
181
|
-
if (node.path.length <= 1) {
|
|
182
|
-
return `commands/${node.name}.md`;
|
|
183
|
-
}
|
|
184
|
-
const segments = node.path.slice(1);
|
|
185
|
-
return `commands/${segments.join("/")}.md`;
|
|
186
|
-
}
|
|
187
|
-
function commandInvocation(node) {
|
|
188
|
-
return node.path.join(" ");
|
|
189
|
-
}
|
|
190
|
-
function relativePath(from, to) {
|
|
191
|
-
const fromParts = from.split("/").slice(0, -1);
|
|
192
|
-
const toParts = to.split("/");
|
|
193
|
-
let common = 0;
|
|
194
|
-
while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
|
|
195
|
-
common++;
|
|
196
|
-
}
|
|
197
|
-
const ups = fromParts.length - common;
|
|
198
|
-
const remaining = toParts.slice(common);
|
|
199
|
-
if (ups === 0) {
|
|
200
|
-
return remaining.join("/");
|
|
201
|
-
}
|
|
202
|
-
const upSegments = Array.from({ length: ups }, () => "..");
|
|
203
|
-
return [...upSegments, ...remaining].join("/");
|
|
204
|
-
}
|
|
205
|
-
function renderSkillMd(manifest, meta) {
|
|
206
|
-
const lines = [];
|
|
207
|
-
lines.push("---");
|
|
208
|
-
lines.push(`name: ${escapeYaml(meta.name)}`);
|
|
209
|
-
lines.push(`description: ${escapeYaml(meta.description)}`);
|
|
210
|
-
if (meta.license) {
|
|
211
|
-
lines.push(`license: ${escapeYaml(meta.license)}`);
|
|
212
|
-
}
|
|
213
|
-
if (meta.compatibility) {
|
|
214
|
-
lines.push(`compatibility: ${escapeYaml(meta.compatibility)}`);
|
|
215
|
-
}
|
|
216
|
-
if (meta.disableModelInvocation) {
|
|
217
|
-
lines.push("disable-model-invocation: true");
|
|
218
|
-
}
|
|
219
|
-
if (meta.allowedTools) {
|
|
220
|
-
lines.push(`allowed-tools: ${escapeYaml(meta.allowedTools)}`);
|
|
221
|
-
}
|
|
222
|
-
lines.push("metadata:");
|
|
223
|
-
lines.push(` version: "${meta.version}"`);
|
|
224
|
-
lines.push("---");
|
|
225
|
-
lines.push("");
|
|
226
|
-
lines.push(`# ${meta.name}`);
|
|
227
|
-
lines.push("");
|
|
228
|
-
if (manifest.description) {
|
|
229
|
-
lines.push(manifest.description);
|
|
230
|
-
lines.push("");
|
|
231
|
-
}
|
|
232
|
-
const cliName = meta.name.startsWith("use-") ? meta.name.slice(4) : meta.name;
|
|
233
|
-
lines.push(`Use this skill when working with \`${cliName}\` commands, or when you need help with \`${cliName}\` syntax, flags, or subcommands.`);
|
|
234
|
-
lines.push("");
|
|
235
|
-
lines.push("## Command Reference");
|
|
236
|
-
lines.push("");
|
|
237
|
-
lines.push(`For the full list of commands and their documentation paths, see [command-index.md](command-index.md). ` + "**Do not read all command files at once.** Instead:");
|
|
238
|
-
lines.push("");
|
|
239
|
-
lines.push("1. Check [command-index.md](command-index.md) to find the relevant command");
|
|
240
|
-
lines.push("2. Read only the specific file from the `commands/` directory that you need");
|
|
241
|
-
lines.push("");
|
|
242
|
-
if (manifest.children.length > 0) {
|
|
243
|
-
lines.push("## Available Commands");
|
|
244
|
-
lines.push("");
|
|
245
|
-
for (const child of manifest.children) {
|
|
246
|
-
const filePath = commandFilePath(child);
|
|
247
|
-
const desc = child.description ? ` - ${child.description}` : "";
|
|
248
|
-
lines.push(`- [\`${child.name}\`](${filePath})${desc}`);
|
|
249
|
-
}
|
|
250
|
-
lines.push("");
|
|
251
|
-
}
|
|
252
|
-
if (manifest.runnable) {
|
|
253
|
-
lines.push("## Usage");
|
|
254
|
-
lines.push("");
|
|
255
|
-
const rootFile = commandFilePath(manifest);
|
|
256
|
-
lines.push(`The root command is directly executable. See [${manifest.name}](${rootFile}) for usage details.`);
|
|
257
|
-
lines.push("");
|
|
258
|
-
}
|
|
259
|
-
return lines.join(`
|
|
260
|
-
`);
|
|
261
|
-
}
|
|
262
|
-
function renderCommandIndex(_manifest, allNodes) {
|
|
263
|
-
const lines = [];
|
|
264
|
-
lines.push("# Command Index");
|
|
265
|
-
lines.push("");
|
|
266
|
-
lines.push("| Command | Type | Documentation |");
|
|
267
|
-
lines.push("| ------- | ---- | ------------- |");
|
|
268
|
-
for (const node of allNodes) {
|
|
269
|
-
const invocation = commandInvocation(node);
|
|
270
|
-
const filePath = commandFilePath(node);
|
|
271
|
-
const type = commandType(node);
|
|
272
|
-
lines.push(`| \`${invocation}\` | ${type} | [${filePath}](${filePath}) |`);
|
|
273
|
-
}
|
|
274
|
-
lines.push("");
|
|
275
|
-
return lines.join(`
|
|
276
|
-
`);
|
|
277
|
-
}
|
|
278
|
-
function commandType(node) {
|
|
279
|
-
if (node.runnable && node.children.length > 0) {
|
|
280
|
-
return "runnable, group";
|
|
281
|
-
}
|
|
282
|
-
if (node.runnable) {
|
|
283
|
-
return "runnable";
|
|
284
|
-
}
|
|
285
|
-
return "group";
|
|
286
|
-
}
|
|
287
|
-
function renderLeafCommand(node, root) {
|
|
288
|
-
const lines = [];
|
|
289
|
-
const invocation = commandInvocation(node);
|
|
290
|
-
lines.push(`# \`${invocation}\``);
|
|
291
|
-
lines.push("");
|
|
292
|
-
if (node.description) {
|
|
293
|
-
lines.push(node.description);
|
|
294
|
-
lines.push("");
|
|
295
|
-
}
|
|
296
|
-
lines.push("## Usage");
|
|
297
|
-
lines.push("");
|
|
298
|
-
if (node.usage) {
|
|
299
|
-
lines.push("```");
|
|
300
|
-
lines.push(node.usage);
|
|
301
|
-
lines.push("```");
|
|
302
|
-
} else {
|
|
303
|
-
lines.push("```");
|
|
304
|
-
lines.push(buildUsageLine(node));
|
|
305
|
-
lines.push("```");
|
|
306
|
-
}
|
|
307
|
-
lines.push("");
|
|
308
|
-
if (node.args.length > 0) {
|
|
309
|
-
lines.push("## Arguments");
|
|
310
|
-
lines.push("");
|
|
311
|
-
lines.push(...renderArgsTable(node.args));
|
|
312
|
-
lines.push("");
|
|
313
|
-
}
|
|
314
|
-
if (node.flags.length > 0) {
|
|
315
|
-
lines.push("## Flags");
|
|
316
|
-
lines.push("");
|
|
317
|
-
lines.push(...renderFlagsTable(node.flags));
|
|
318
|
-
lines.push("");
|
|
319
|
-
}
|
|
320
|
-
lines.push(...renderNavigation(node, root));
|
|
321
|
-
return lines.join(`
|
|
322
|
-
`);
|
|
323
|
-
}
|
|
324
|
-
function renderGroupCommand(node, root) {
|
|
325
|
-
const lines = [];
|
|
326
|
-
const invocation = commandInvocation(node);
|
|
327
|
-
const filePath = commandFilePath(node);
|
|
328
|
-
lines.push(`# \`${invocation}\``);
|
|
329
|
-
lines.push("");
|
|
330
|
-
if (node.description) {
|
|
331
|
-
lines.push(node.description);
|
|
332
|
-
lines.push("");
|
|
333
|
-
}
|
|
334
|
-
if (node.runnable) {
|
|
335
|
-
lines.push("## Usage");
|
|
336
|
-
lines.push("");
|
|
337
|
-
if (node.usage) {
|
|
338
|
-
lines.push("```");
|
|
339
|
-
lines.push(node.usage);
|
|
340
|
-
lines.push("```");
|
|
341
|
-
} else {
|
|
342
|
-
lines.push("```");
|
|
343
|
-
lines.push(buildUsageLine(node));
|
|
344
|
-
lines.push("```");
|
|
345
|
-
}
|
|
346
|
-
lines.push("");
|
|
347
|
-
if (node.args.length > 0) {
|
|
348
|
-
lines.push("## Arguments");
|
|
349
|
-
lines.push("");
|
|
350
|
-
lines.push(...renderArgsTable(node.args));
|
|
351
|
-
lines.push("");
|
|
352
|
-
}
|
|
353
|
-
if (node.flags.length > 0) {
|
|
354
|
-
lines.push("## Flags");
|
|
355
|
-
lines.push("");
|
|
356
|
-
lines.push(...renderFlagsTable(node.flags));
|
|
357
|
-
lines.push("");
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
lines.push("## Subcommands");
|
|
361
|
-
lines.push("");
|
|
362
|
-
for (const child of node.children) {
|
|
363
|
-
const childPath = commandFilePath(child);
|
|
364
|
-
const childRelative = relativePath(filePath, childPath);
|
|
365
|
-
const desc = child.description ? ` - ${child.description}` : "";
|
|
366
|
-
lines.push(`- [\`${child.name}\`](${childRelative})${desc}`);
|
|
367
|
-
}
|
|
368
|
-
lines.push("");
|
|
369
|
-
lines.push(...renderNavigation(node, root));
|
|
370
|
-
return lines.join(`
|
|
371
|
-
`);
|
|
372
|
-
}
|
|
373
|
-
function buildUsageLine(node) {
|
|
374
|
-
const parts = [...node.path];
|
|
375
|
-
for (const arg of node.args) {
|
|
376
|
-
if (arg.variadic) {
|
|
377
|
-
parts.push(arg.required ? `<${arg.name}...>` : `[${arg.name}...]`);
|
|
378
|
-
} else {
|
|
379
|
-
parts.push(arg.required ? `<${arg.name}>` : `[${arg.name}]`);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
if (node.flags.length > 0) {
|
|
383
|
-
parts.push("[options]");
|
|
384
|
-
}
|
|
385
|
-
return parts.join(" ");
|
|
386
|
-
}
|
|
387
|
-
function renderArgsTable(args) {
|
|
388
|
-
const lines = [];
|
|
389
|
-
lines.push("| Argument | Type | Required | Description |");
|
|
390
|
-
lines.push("| -------- | ---- | -------- | ----------- |");
|
|
391
|
-
for (const arg of args) {
|
|
392
|
-
const name = arg.variadic ? `${arg.name}...` : arg.name;
|
|
393
|
-
const required = arg.required ? "Yes" : "No";
|
|
394
|
-
const desc = escapeTableCell(formatArgDescription(arg));
|
|
395
|
-
lines.push(`| \`${name}\` | ${arg.type} | ${required} | ${desc} |`);
|
|
396
|
-
}
|
|
397
|
-
return lines;
|
|
398
|
-
}
|
|
399
|
-
function formatArgDescription(arg) {
|
|
400
|
-
const parts = [];
|
|
401
|
-
if (arg.description) {
|
|
402
|
-
parts.push(arg.description);
|
|
403
|
-
}
|
|
404
|
-
if (arg.default !== undefined) {
|
|
405
|
-
parts.push(`Default: \`${arg.default}\``);
|
|
406
|
-
}
|
|
407
|
-
return parts.join(". ") || "-";
|
|
408
|
-
}
|
|
409
|
-
function renderFlagsTable(flags) {
|
|
410
|
-
const lines = [];
|
|
411
|
-
lines.push("| Flag | Type | Required | Description |");
|
|
412
|
-
lines.push("| ---- | ---- | -------- | ----------- |");
|
|
413
|
-
for (const flag of flags) {
|
|
414
|
-
const name = formatFlagName(flag);
|
|
415
|
-
const required = flag.required ? "Yes" : "No";
|
|
416
|
-
const desc = escapeTableCell(formatFlagDescription(flag));
|
|
417
|
-
lines.push(`| ${name} | ${flag.type} | ${required} | ${desc} |`);
|
|
418
|
-
}
|
|
419
|
-
return lines;
|
|
420
|
-
}
|
|
421
|
-
function formatFlagName(flag) {
|
|
422
|
-
const parts = [`\`--${flag.name}\``];
|
|
423
|
-
for (const alias of flag.aliases) {
|
|
424
|
-
parts.push(`\`-${alias}\``);
|
|
425
|
-
}
|
|
426
|
-
return parts.join(", ");
|
|
427
|
-
}
|
|
428
|
-
function formatFlagDescription(flag) {
|
|
429
|
-
const parts = [];
|
|
430
|
-
if (flag.description) {
|
|
431
|
-
parts.push(flag.description);
|
|
432
|
-
}
|
|
433
|
-
if (flag.multiple) {
|
|
434
|
-
parts.push("Can be specified multiple times");
|
|
435
|
-
}
|
|
436
|
-
if (flag.default !== undefined) {
|
|
437
|
-
parts.push(`Default: \`${flag.default}\``);
|
|
438
|
-
}
|
|
439
|
-
return parts.join(". ") || "-";
|
|
440
|
-
}
|
|
441
|
-
function renderNavigation(node, root) {
|
|
442
|
-
const lines = [];
|
|
443
|
-
const filePath = commandFilePath(node);
|
|
444
|
-
lines.push("---");
|
|
445
|
-
lines.push("");
|
|
446
|
-
if (node.path.length > 1) {
|
|
447
|
-
const parentPath = node.path.slice(0, -1);
|
|
448
|
-
const parentNode = findNode(root, parentPath);
|
|
449
|
-
if (parentNode) {
|
|
450
|
-
const parentFile = commandFilePath(parentNode);
|
|
451
|
-
const parentRelative = relativePath(filePath, parentFile);
|
|
452
|
-
const parentInvocation = commandInvocation(parentNode);
|
|
453
|
-
lines.push(`Parent: [\`${parentInvocation}\`](${parentRelative})`);
|
|
454
|
-
lines.push("");
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
const indexRelative = relativePath(filePath, "command-index.md");
|
|
458
|
-
lines.push(`[Command Index](${indexRelative})`);
|
|
459
|
-
lines.push("");
|
|
460
|
-
return lines;
|
|
461
|
-
}
|
|
462
|
-
function findNode(root, path) {
|
|
463
|
-
if (arraysEqual(root.path, path)) {
|
|
464
|
-
return root;
|
|
465
|
-
}
|
|
466
|
-
for (const child of root.children) {
|
|
467
|
-
const found = findNode(child, path);
|
|
468
|
-
if (found)
|
|
469
|
-
return found;
|
|
470
|
-
}
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
function arraysEqual(a, b) {
|
|
474
|
-
if (a.length !== b.length)
|
|
475
|
-
return false;
|
|
476
|
-
for (let i = 0;i < a.length; i++) {
|
|
477
|
-
if (a[i] !== b[i])
|
|
478
|
-
return false;
|
|
479
|
-
}
|
|
480
|
-
return true;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// src/version.ts
|
|
484
|
-
import { readFile } from "fs/promises";
|
|
485
|
-
import { join as join2 } from "path";
|
|
486
|
-
var CRUST_MANIFEST = "crust.json";
|
|
487
|
-
async function readInstalledVersion(dir) {
|
|
488
|
-
try {
|
|
489
|
-
const raw = await readFile(join2(dir, CRUST_MANIFEST), "utf-8");
|
|
490
|
-
const parsed = JSON.parse(raw);
|
|
491
|
-
if (typeof parsed === "object" && parsed !== null && "version" in parsed && typeof parsed.version === "string") {
|
|
492
|
-
return parsed.version;
|
|
493
|
-
}
|
|
494
|
-
return null;
|
|
495
|
-
} catch {
|
|
496
|
-
return null;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// src/generate.ts
|
|
501
|
-
var SKILL_NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
502
|
-
function isValidSkillName(name) {
|
|
503
|
-
return name.length >= 1 && name.length <= 64 && SKILL_NAME_PATTERN.test(name);
|
|
504
|
-
}
|
|
505
|
-
function resolveSkillName(name) {
|
|
506
|
-
return name.startsWith("use-") ? name : `use-${name}`;
|
|
507
|
-
}
|
|
508
|
-
async function generateSkill(options) {
|
|
509
|
-
const {
|
|
510
|
-
command,
|
|
511
|
-
meta,
|
|
512
|
-
agents,
|
|
513
|
-
scope = "global",
|
|
514
|
-
clean = true,
|
|
515
|
-
force = false
|
|
516
|
-
} = options;
|
|
517
|
-
const resolvedName = resolveSkillName(meta.name);
|
|
518
|
-
if (!isValidSkillName(resolvedName)) {
|
|
519
|
-
throw new Error(`Invalid skill name "${resolvedName}": must be 1\u201364 lowercase ` + `alphanumeric characters and hyphens, no leading/trailing/consecutive ` + `hyphens. Pattern: ${SKILL_NAME_PATTERN.source}`);
|
|
520
|
-
}
|
|
521
|
-
const resolvedMeta = {
|
|
522
|
-
...meta,
|
|
523
|
-
name: resolvedName
|
|
524
|
-
};
|
|
525
|
-
const manifest = buildManifest(command);
|
|
526
|
-
const renderedFiles = renderSkill(manifest, resolvedMeta);
|
|
527
|
-
const metadataFiles = renderDistributionMetadata(manifest, resolvedMeta);
|
|
528
|
-
const allFiles = [...renderedFiles, ...metadataFiles].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
529
|
-
const results = [];
|
|
530
|
-
for (const agent of agents) {
|
|
531
|
-
const outputDir = resolveAgentPath(agent, scope, resolvedMeta.name);
|
|
532
|
-
const installedVersion = await readInstalledVersion(outputDir);
|
|
533
|
-
if (installedVersion === null) {
|
|
534
|
-
const dirExists = await access2(outputDir).then(() => true).catch(() => false);
|
|
535
|
-
if (dirExists && !force) {
|
|
536
|
-
throw new SkillConflictError({ agent, outputDir });
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
const status = installedVersion === null ? "installed" : installedVersion === resolvedMeta.version ? "up-to-date" : "updated";
|
|
540
|
-
if (status === "up-to-date") {
|
|
541
|
-
results.push({
|
|
542
|
-
agent,
|
|
543
|
-
outputDir,
|
|
544
|
-
files: [],
|
|
545
|
-
status: "up-to-date"
|
|
546
|
-
});
|
|
547
|
-
continue;
|
|
548
|
-
}
|
|
549
|
-
const previousVersion = status === "updated" ? installedVersion ?? undefined : undefined;
|
|
550
|
-
if (clean) {
|
|
551
|
-
await cleanDirectory(outputDir);
|
|
552
|
-
}
|
|
553
|
-
await writeFiles(outputDir, allFiles);
|
|
554
|
-
results.push({
|
|
555
|
-
agent,
|
|
556
|
-
outputDir,
|
|
557
|
-
files: allFiles.map((f) => f.path),
|
|
558
|
-
status,
|
|
559
|
-
previousVersion
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
return { agents: results };
|
|
563
|
-
}
|
|
564
|
-
async function uninstallSkill(options) {
|
|
565
|
-
const { name, agents, scope = "global" } = options;
|
|
566
|
-
const resolvedName = resolveSkillName(name);
|
|
567
|
-
const results = [];
|
|
568
|
-
for (const agent of agents) {
|
|
569
|
-
const outputDir = resolveAgentPath(agent, scope, resolvedName);
|
|
570
|
-
const exists = await access2(outputDir).then(() => true).catch(() => false);
|
|
571
|
-
if (exists) {
|
|
572
|
-
await rm(outputDir, { recursive: true, force: true });
|
|
573
|
-
results.push({ agent, outputDir, status: "removed" });
|
|
574
|
-
} else {
|
|
575
|
-
results.push({ agent, outputDir, status: "not-found" });
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
return { agents: results };
|
|
579
|
-
}
|
|
580
|
-
async function skillStatus(options) {
|
|
581
|
-
const { name, agents, scope = "global" } = options;
|
|
582
|
-
const resolvedName = resolveSkillName(name);
|
|
583
|
-
const results = [];
|
|
584
|
-
for (const agent of agents) {
|
|
585
|
-
const outputDir = resolveAgentPath(agent, scope, resolvedName);
|
|
586
|
-
const version = await readInstalledVersion(outputDir);
|
|
587
|
-
results.push({
|
|
588
|
-
agent,
|
|
589
|
-
outputDir,
|
|
590
|
-
installed: version !== null,
|
|
591
|
-
version: version ?? undefined
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
return { agents: results };
|
|
595
|
-
}
|
|
596
|
-
function renderDistributionMetadata(manifest, meta) {
|
|
597
|
-
return [
|
|
598
|
-
{
|
|
599
|
-
path: CRUST_MANIFEST,
|
|
600
|
-
content: renderCrustJson(manifest, meta)
|
|
601
|
-
}
|
|
602
|
-
];
|
|
603
|
-
}
|
|
604
|
-
function renderCrustJson(manifest, meta) {
|
|
605
|
-
const commands = collectCommandPaths(manifest);
|
|
606
|
-
const obj = {
|
|
607
|
-
name: meta.name,
|
|
608
|
-
description: meta.description,
|
|
609
|
-
version: meta.version,
|
|
610
|
-
entrypoint: "SKILL.md",
|
|
611
|
-
commands
|
|
612
|
-
};
|
|
613
|
-
return `${JSON.stringify(obj, null, "\t")}
|
|
614
|
-
`;
|
|
615
|
-
}
|
|
616
|
-
function collectCommandPaths(node) {
|
|
617
|
-
const paths = [node.path.join(" ")];
|
|
618
|
-
for (const child of node.children) {
|
|
619
|
-
paths.push(...collectCommandPaths(child));
|
|
620
|
-
}
|
|
621
|
-
return paths;
|
|
622
|
-
}
|
|
623
|
-
async function cleanDirectory(dir) {
|
|
624
|
-
await rm(dir, { recursive: true, force: true });
|
|
625
|
-
}
|
|
626
|
-
async function writeFiles(baseDir, files) {
|
|
627
|
-
const dirs = new Set;
|
|
628
|
-
for (const file of files) {
|
|
629
|
-
const filePath = join3(baseDir, file.path);
|
|
630
|
-
const dir = dirname(filePath);
|
|
631
|
-
dirs.add(dir);
|
|
632
|
-
}
|
|
633
|
-
const sortedDirs = [...dirs].sort();
|
|
634
|
-
for (const dir of sortedDirs) {
|
|
635
|
-
await mkdir(dir, { recursive: true });
|
|
636
|
-
}
|
|
637
|
-
for (const file of files) {
|
|
638
|
-
const filePath = join3(baseDir, file.path);
|
|
639
|
-
await writeFile(filePath, file.content, "utf-8");
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
// src/plugin.ts
|
|
643
|
-
import { defineCommand } from "@crustjs/core";
|
|
644
|
-
import { confirm, multiselect, spinner } from "@crustjs/prompts";
|
|
645
|
-
function deriveSkillMeta(command, version) {
|
|
646
|
-
return {
|
|
647
|
-
name: command.meta.name,
|
|
648
|
-
description: command.meta.description ?? "",
|
|
649
|
-
version
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
function skillPlugin(options) {
|
|
653
|
-
let rootCmd;
|
|
654
|
-
let skillCmd = null;
|
|
655
|
-
return {
|
|
656
|
-
name: "skills",
|
|
657
|
-
setup(context, actions) {
|
|
658
|
-
rootCmd = context.rootCommand;
|
|
659
|
-
if (options.command !== false) {
|
|
660
|
-
const name = typeof options.command === "string" ? options.command : "skill";
|
|
661
|
-
skillCmd = buildSkillCommand(rootCmd, options);
|
|
662
|
-
actions.addSubCommand(rootCmd, name, skillCmd);
|
|
663
|
-
}
|
|
664
|
-
},
|
|
665
|
-
async middleware(_context, next) {
|
|
666
|
-
if (skillCmd && _context.route?.command === skillCmd) {
|
|
667
|
-
await next();
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
const agents = await detectInstalledAgents();
|
|
671
|
-
if (agents.length === 0) {
|
|
672
|
-
await next();
|
|
673
|
-
return;
|
|
674
|
-
}
|
|
675
|
-
const autoInstall = options.autoInstall ?? false;
|
|
676
|
-
const autoUpdate = options.autoUpdate ?? true;
|
|
677
|
-
const meta = deriveSkillMeta(rootCmd, options.version);
|
|
678
|
-
const status = await skillStatus({
|
|
679
|
-
name: meta.name,
|
|
680
|
-
agents,
|
|
681
|
-
scope: options.scope ?? "global"
|
|
682
|
-
});
|
|
683
|
-
const needsUpdate = status.agents.filter((a) => {
|
|
684
|
-
if (!a.installed)
|
|
685
|
-
return autoInstall;
|
|
686
|
-
if (a.version !== meta.version)
|
|
687
|
-
return autoUpdate;
|
|
688
|
-
return false;
|
|
689
|
-
});
|
|
690
|
-
if (needsUpdate.length > 0) {
|
|
691
|
-
try {
|
|
692
|
-
const result = await generateSkill({
|
|
693
|
-
command: rootCmd,
|
|
694
|
-
meta,
|
|
695
|
-
agents: needsUpdate.map((a) => a.agent),
|
|
696
|
-
scope: options.scope
|
|
697
|
-
});
|
|
698
|
-
const installedAgents = result.agents.filter((a) => a.status === "installed").map((a) => AGENT_LABELS[a.agent]);
|
|
699
|
-
const updatedAgents = result.agents.filter((a) => a.status === "updated").map((a) => AGENT_LABELS[a.agent]);
|
|
700
|
-
if (installedAgents.length > 0) {
|
|
701
|
-
if (options.command !== false) {
|
|
702
|
-
const manageCommand = `${rootCmd.meta.name} skill`;
|
|
703
|
-
console.log(`Auto-installed skill "${meta.name}" v${meta.version} for ${installedAgents.join(", ")}. Manage with \`${manageCommand}\`.`);
|
|
704
|
-
} else {
|
|
705
|
-
console.log(`Auto-installed skill "${meta.name}" v${meta.version} for ${installedAgents.join(", ")}.`);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
if (updatedAgents.length > 0) {
|
|
709
|
-
console.log(`Updated skill "${meta.name}" to v${meta.version} for ${updatedAgents.join(", ")}.`);
|
|
710
|
-
}
|
|
711
|
-
} catch (err) {
|
|
712
|
-
if (err instanceof SkillConflictError) {
|
|
713
|
-
console.warn(`Skill conflict: "${err.details.outputDir}" already exists ` + `but was not created by ${meta.name}. Skipping auto-update. ` + `Delete or rename the conflicting skill to resolve.`);
|
|
714
|
-
} else {
|
|
715
|
-
throw err;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
await next();
|
|
720
|
-
}
|
|
721
|
-
};
|
|
722
|
-
}
|
|
723
|
-
function buildSkillCommand(rootCmd, options) {
|
|
724
|
-
return defineCommand({
|
|
725
|
-
meta: {
|
|
726
|
-
name: "skill",
|
|
727
|
-
description: "Manage agent skill installations"
|
|
728
|
-
},
|
|
729
|
-
async run() {
|
|
730
|
-
const meta = deriveSkillMeta(rootCmd, options.version);
|
|
731
|
-
const scope = options.scope ?? "global";
|
|
732
|
-
const detectedAgents = await detectInstalledAgents();
|
|
733
|
-
if (detectedAgents.length === 0) {
|
|
734
|
-
console.log("No supported agents detected. Install Claude Code or OpenCode first.");
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
const status = await skillStatus({
|
|
738
|
-
name: meta.name,
|
|
739
|
-
agents: detectedAgents,
|
|
740
|
-
scope
|
|
741
|
-
});
|
|
742
|
-
const installedAgents = [];
|
|
743
|
-
const choices = status.agents.map((entry) => {
|
|
744
|
-
const hint = entry.installed ? `v${entry.version} installed` : "not installed";
|
|
745
|
-
if (entry.installed) {
|
|
746
|
-
installedAgents.push(entry.agent);
|
|
747
|
-
}
|
|
748
|
-
return {
|
|
749
|
-
label: AGENT_LABELS[entry.agent],
|
|
750
|
-
value: entry.agent,
|
|
751
|
-
hint
|
|
752
|
-
};
|
|
753
|
-
});
|
|
754
|
-
const selected = await multiselect({
|
|
755
|
-
message: "Select agents to install skills for",
|
|
756
|
-
choices,
|
|
757
|
-
default: installedAgents,
|
|
758
|
-
required: false
|
|
759
|
-
});
|
|
760
|
-
const toInstall = selected.filter((agent) => !installedAgents.includes(agent));
|
|
761
|
-
const toUpdate = selected.filter((agent) => {
|
|
762
|
-
const entry = status.agents.find((a) => a.agent === agent);
|
|
763
|
-
return entry?.installed === true && entry.version !== meta.version;
|
|
764
|
-
});
|
|
765
|
-
const toUninstall = installedAgents.filter((agent) => !selected.includes(agent));
|
|
766
|
-
const agentsToGenerate = [...toInstall, ...toUpdate];
|
|
767
|
-
if (agentsToGenerate.length > 0) {
|
|
768
|
-
try {
|
|
769
|
-
const result = await spinner({
|
|
770
|
-
message: "Installing skills...",
|
|
771
|
-
task: async () => generateSkill({
|
|
772
|
-
command: rootCmd,
|
|
773
|
-
meta,
|
|
774
|
-
agents: agentsToGenerate,
|
|
775
|
-
scope
|
|
776
|
-
})
|
|
777
|
-
});
|
|
778
|
-
console.log(`
|
|
779
|
-
Installed "${meta.name}" v${meta.version}`);
|
|
780
|
-
for (const r of result.agents) {
|
|
781
|
-
console.log(` ${AGENT_LABELS[r.agent]} \u2192 ${r.outputDir}`);
|
|
782
|
-
}
|
|
783
|
-
} catch (err) {
|
|
784
|
-
if (err instanceof SkillConflictError) {
|
|
785
|
-
const overwrite = await confirm({
|
|
786
|
-
message: `"${err.details.outputDir}" already exists but was not ` + `created by Crust. Overwrite?`,
|
|
787
|
-
default: false
|
|
788
|
-
});
|
|
789
|
-
if (overwrite) {
|
|
790
|
-
const result = await spinner({
|
|
791
|
-
message: "Overwriting skill...",
|
|
792
|
-
task: async () => generateSkill({
|
|
793
|
-
command: rootCmd,
|
|
794
|
-
meta,
|
|
795
|
-
agents: [err.details.agent],
|
|
796
|
-
scope,
|
|
797
|
-
force: true
|
|
798
|
-
})
|
|
799
|
-
});
|
|
800
|
-
console.log(`
|
|
801
|
-
Installed "${meta.name}" v${meta.version}`);
|
|
802
|
-
for (const r of result.agents) {
|
|
803
|
-
console.log(` ${AGENT_LABELS[r.agent]} \u2192 ${r.outputDir}`);
|
|
804
|
-
}
|
|
805
|
-
} else {
|
|
806
|
-
console.log(`
|
|
807
|
-
Skipped ${AGENT_LABELS[err.details.agent]}`);
|
|
808
|
-
}
|
|
809
|
-
} else {
|
|
810
|
-
throw err;
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
if (toUninstall.length > 0) {
|
|
815
|
-
const result = await spinner({
|
|
816
|
-
message: "Removing skills...",
|
|
817
|
-
task: async () => uninstallSkill({
|
|
818
|
-
name: meta.name,
|
|
819
|
-
agents: toUninstall,
|
|
820
|
-
scope
|
|
821
|
-
})
|
|
822
|
-
});
|
|
823
|
-
const removed = result.agents.filter((a) => a.status === "removed").map((a) => AGENT_LABELS[a.agent]);
|
|
824
|
-
if (removed.length > 0) {
|
|
825
|
-
console.log(`
|
|
826
|
-
Removed from ${removed.join(", ")}`);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
if (agentsToGenerate.length === 0 && toUninstall.length === 0) {
|
|
830
|
-
console.log("No changes.");
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
});
|
|
834
|
-
}
|
|
835
|
-
export {
|
|
836
|
-
uninstallSkill,
|
|
837
|
-
skillStatus,
|
|
838
|
-
skillPlugin,
|
|
839
|
-
resolveSkillName,
|
|
840
|
-
isValidSkillName,
|
|
841
|
-
generateSkill,
|
|
842
|
-
detectInstalledAgents,
|
|
843
|
-
SkillConflictError
|
|
844
|
-
};
|
|
2
|
+
import{access as QO}from"fs/promises";import{homedir as y}from"os";import{join as I}from"path";var XO=["claude-code","opencode"],x={"claude-code":"Claude Code",opencode:"OpenCode"};function L(O,X,Q){let Z=X==="global"?y():process.cwd();switch(O){case"claude-code":return I(Z,".claude","skills",Q);case"opencode":if(X==="global")return I(Z,".config","opencode","skills",Q);return I(Z,".opencode","skills",Q)}}async function F(O){let X=O??y(),Q=[];for(let Z of XO){let H=ZO(X,Z);if(await QO(H).then(()=>!0).catch(()=>!1))Q.push(Z)}return Q}function ZO(O,X){switch(X){case"claude-code":return I(O,".claude");case"opencode":return I(O,".config","opencode")}}class j extends Error{name="SkillConflictError";details;constructor(O){let X=`Skill conflict for agent "${O.agent}": directory "${O.outputDir}" already exists but was not created by Crust (no crust.json found). Delete or rename the conflicting skill to resolve.`;super(X);this.details=O}}import{access as o,mkdir as wO,rm as s,writeFile as LO}from"fs/promises";import{dirname as FO,join as i}from"path";function v(O){return h(O,[])}function h(O,X){let Q=HO(O.meta.name),Z=[...X,Q],H=WO(O.args),W=JO(O.flags),$=qO(O.subCommands,Z);return{name:Q,path:Z,description:O.meta.description,usage:O.meta.usage,runnable:typeof O.run==="function",args:H,flags:W,children:$}}function HO(O){return O.trim().toLowerCase()}function WO(O){if(!O||O.length===0)return[];return O.map($O)}function $O(O){let X={name:O.name,type:O.type,required:O.required===!0,variadic:O.variadic===!0};if(O.description!==void 0)X.description=O.description;if(O.default!==void 0)X.default=u(O.default);return X}function JO(O){if(!O)return[];return Object.keys(O).sort().map((Q)=>{return KO(Q,O[Q])})}function KO(O,X){let Q={name:O,type:X.type,required:X.required===!0,multiple:X.multiple===!0,aliases:RO(X.alias)};if(X.description!==void 0)Q.description=X.description;if(X.default!==void 0)Q.default=u(X.default);return Q}function RO(O){if(O===void 0)return[];if(typeof O==="string")return[O];return[...O].sort()}function qO(O,X){return Object.keys(O).sort().map((Z)=>{return h(O[Z],X)})}function u(O){if(Array.isArray(O))return JSON.stringify(O);return String(O)}function _(O){if(/[:#[\]{}&*!|>'"`,@?\\]|^\s|\s$|^---|[\n\r]/.test(O))return`"${O.replace(/\\/g,"\\\\").replace(/"/g,"\\\"").replace(/\n/g,"\\n").replace(/\r/g,"\\r")}"`;return O}function p(O){return O.replace(/(?<!\\)\|/g,"\\|")}function f(O,X){let Q=[],Z=c(O);Q.push({path:"SKILL.md",content:YO(O,X)}),Q.push({path:"command-index.md",content:BO(O,Z)});for(let H of Z){let W=G(H),$=H.children.length>0?GO(H,O):xO(H,O);Q.push({path:W,content:$})}return Q}function c(O){let X=[O];for(let Q of O.children)X.push(...c(Q));return X}function G(O){if(O.path.length<=1)return`commands/${O.name}.md`;return`commands/${O.path.slice(1).join("/")}.md`}function E(O){return O.path.join(" ")}function D(O,X){let Q=O.split("/").slice(0,-1),Z=X.split("/"),H=0;while(H<Q.length&&H<Z.length&&Q[H]===Z[H])H++;let W=Q.length-H,$=Z.slice(H);if(W===0)return $.join("/");return[...Array.from({length:W},()=>".."),...$].join("/")}function YO(O,X){let Q=[];if(Q.push("---"),Q.push(`name: ${_(X.name)}`),Q.push(`description: ${_(X.description)}`),X.license)Q.push(`license: ${_(X.license)}`);if(X.compatibility)Q.push(`compatibility: ${_(X.compatibility)}`);if(X.disableModelInvocation)Q.push("disable-model-invocation: true");if(X.allowedTools)Q.push(`allowed-tools: ${_(X.allowedTools)}`);if(Q.push("metadata:"),Q.push(` version: "${X.version}"`),Q.push("---"),Q.push(""),Q.push(`# ${X.name}`),Q.push(""),O.description)Q.push(O.description),Q.push("");let Z=X.name.startsWith("use-")?X.name.slice(4):X.name;if(Q.push(`Use this skill when working with \`${Z}\` commands, or when you need help with \`${Z}\` syntax, flags, or subcommands.`),Q.push(""),Q.push("## Command Reference"),Q.push(""),Q.push("For the full list of commands and their documentation paths, see [command-index.md](command-index.md). **Do not read all command files at once.** Instead:"),Q.push(""),Q.push("1. Check [command-index.md](command-index.md) to find the relevant command"),Q.push("2. Read only the specific file from the `commands/` directory that you need"),Q.push(""),O.children.length>0){Q.push("## Available Commands"),Q.push("");for(let H of O.children){let W=G(H),$=H.description?` - ${H.description}`:"";Q.push(`- [\`${H.name}\`](${W})${$}`)}Q.push("")}if(O.runnable){Q.push("## Usage"),Q.push("");let H=G(O);Q.push(`The root command is directly executable. See [${O.name}](${H}) for usage details.`),Q.push("")}return Q.join(`
|
|
3
|
+
`)}function BO(O,X){let Q=[];Q.push("# Command Index"),Q.push(""),Q.push("| Command | Type | Documentation |"),Q.push("| ------- | ---- | ------------- |");for(let Z of X){let H=E(Z),W=G(Z),$=UO(Z);Q.push(`| \`${H}\` | ${$} | [${W}](${W}) |`)}return Q.push(""),Q.join(`
|
|
4
|
+
`)}function UO(O){if(O.runnable&&O.children.length>0)return"runnable, group";if(O.runnable)return"runnable";return"group"}function xO(O,X){let Q=[],Z=E(O);if(Q.push(`# \`${Z}\``),Q.push(""),O.description)Q.push(O.description),Q.push("");if(Q.push("## Usage"),Q.push(""),O.usage)Q.push("```"),Q.push(O.usage),Q.push("```");else Q.push("```"),Q.push(g(O)),Q.push("```");if(Q.push(""),O.args.length>0)Q.push("## Arguments"),Q.push(""),Q.push(...m(O.args)),Q.push("");if(O.flags.length>0)Q.push("## Flags"),Q.push(""),Q.push(...d(O.flags)),Q.push("");return Q.push(...r(O,X)),Q.join(`
|
|
5
|
+
`)}function GO(O,X){let Q=[],Z=E(O),H=G(O);if(Q.push(`# \`${Z}\``),Q.push(""),O.description)Q.push(O.description),Q.push("");if(O.runnable){if(Q.push("## Usage"),Q.push(""),O.usage)Q.push("```"),Q.push(O.usage),Q.push("```");else Q.push("```"),Q.push(g(O)),Q.push("```");if(Q.push(""),O.args.length>0)Q.push("## Arguments"),Q.push(""),Q.push(...m(O.args)),Q.push("");if(O.flags.length>0)Q.push("## Flags"),Q.push(""),Q.push(...d(O.flags)),Q.push("")}Q.push("## Subcommands"),Q.push("");for(let W of O.children){let $=G(W),q=D(H,$),R=W.description?` - ${W.description}`:"";Q.push(`- [\`${W.name}\`](${q})${R}`)}return Q.push(""),Q.push(...r(O,X)),Q.join(`
|
|
6
|
+
`)}function g(O){let X=[...O.path];for(let Q of O.args)if(Q.variadic)X.push(Q.required?`<${Q.name}...>`:`[${Q.name}...]`);else X.push(Q.required?`<${Q.name}>`:`[${Q.name}]`);if(O.flags.length>0)X.push("[options]");return X.join(" ")}function m(O){let X=[];X.push("| Argument | Type | Required | Description |"),X.push("| -------- | ---- | -------- | ----------- |");for(let Q of O){let Z=Q.variadic?`${Q.name}...`:Q.name,H=Q.required?"Yes":"No",W=p(zO(Q));X.push(`| \`${Z}\` | ${Q.type} | ${H} | ${W} |`)}return X}function zO(O){let X=[];if(O.description)X.push(O.description);if(O.default!==void 0)X.push(`Default: \`${O.default}\``);return X.join(". ")||"-"}function d(O){let X=[];X.push("| Flag | Type | Required | Description |"),X.push("| ---- | ---- | -------- | ----------- |");for(let Q of O){let Z=VO(Q),H=Q.required?"Yes":"No",W=p(jO(Q));X.push(`| ${Z} | ${Q.type} | ${H} | ${W} |`)}return X}function VO(O){let X=[`\`--${O.name}\``];for(let Q of O.aliases)X.push(`\`-${Q}\``);return X.join(", ")}function jO(O){let X=[];if(O.description)X.push(O.description);if(O.multiple)X.push("Can be specified multiple times");if(O.default!==void 0)X.push(`Default: \`${O.default}\``);return X.join(". ")||"-"}function r(O,X){let Q=[],Z=G(O);if(Q.push("---"),Q.push(""),O.path.length>1){let W=O.path.slice(0,-1),$=l(X,W);if($){let q=G($),R=D(Z,q),z=E($);Q.push(`Parent: [\`${z}\`](${R})`),Q.push("")}}let H=D(Z,"command-index.md");return Q.push(`[Command Index](${H})`),Q.push(""),Q}function l(O,X){if(MO(O.path,X))return O;for(let Q of O.children){let Z=l(Q,X);if(Z)return Z}return}function MO(O,X){if(O.length!==X.length)return!1;for(let Q=0;Q<O.length;Q++)if(O[Q]!==X[Q])return!1;return!0}import{readFile as IO}from"fs/promises";import{join as _O}from"path";var P="crust.json";async function b(O){try{let X=await IO(_O(O,P),"utf-8"),Q=JSON.parse(X);if(typeof Q==="object"&&Q!==null&&"version"in Q&&typeof Q.version==="string")return Q.version;return null}catch{return null}}var t=/^[a-z0-9]+(-[a-z0-9]+)*$/;function n(O){return O.length>=1&&O.length<=64&&t.test(O)}function S(O){return O.startsWith("use-")?O:`use-${O}`}async function w(O){let{command:X,meta:Q,agents:Z,scope:H="global",clean:W=!0,force:$=!1}=O,q=S(Q.name);if(!n(q))throw Error(`Invalid skill name "${q}": must be 1\u201364 lowercase `+`alphanumeric characters and hyphens, no leading/trailing/consecutive hyphens. Pattern: ${t.source}`);let R={...Q,name:q},z=v(X),M=f(z,R),B=EO(z,R),U=[...M,...B].sort((J,Y)=>J.path<Y.path?-1:J.path>Y.path?1:0),K=[];for(let J of Z){let Y=L(J,H,R.name),V=await b(Y);if(V===null){if(await o(Y).then(()=>!0).catch(()=>!1)&&!$)throw new j({agent:J,outputDir:Y})}let T=V===null?"installed":V===R.version?"up-to-date":"updated";if(T==="up-to-date"){K.push({agent:J,outputDir:Y,files:[],status:"up-to-date"});continue}let OO=T==="updated"?V??void 0:void 0;if(W)await kO(Y);await TO(Y,U),K.push({agent:J,outputDir:Y,files:U.map((C)=>C.path),status:T,previousVersion:OO})}return{agents:K}}async function A(O){let{name:X,agents:Q,scope:Z="global"}=O,H=S(X),W=[];for(let $ of Q){let q=L($,Z,H);if(await o(q).then(()=>!0).catch(()=>!1))await s(q,{recursive:!0,force:!0}),W.push({agent:$,outputDir:q,status:"removed"});else W.push({agent:$,outputDir:q,status:"not-found"})}return{agents:W}}async function k(O){let{name:X,agents:Q,scope:Z="global"}=O,H=S(X),W=[];for(let $ of Q){let q=L($,Z,H),R=await b(q);W.push({agent:$,outputDir:q,installed:R!==null,version:R??void 0})}return{agents:W}}function EO(O,X){return[{path:P,content:SO(O,X)}]}function SO(O,X){let Q=a(O),Z={name:X.name,description:X.description,version:X.version,entrypoint:"SKILL.md",commands:Q};return`${JSON.stringify(Z,null,"\t")}
|
|
7
|
+
`}function a(O){let X=[O.path.join(" ")];for(let Q of O.children)X.push(...a(Q));return X}async function kO(O){await s(O,{recursive:!0,force:!0})}async function TO(O,X){let Q=new Set;for(let H of X){let W=i(O,H.path),$=FO(W);Q.add($)}let Z=[...Q].sort();for(let H of Z)await wO(H,{recursive:!0});for(let H of X){let W=i(O,H.path);await LO(W,H.content,"utf-8")}}import{defineCommand as DO}from"@crustjs/core";import{confirm as PO,multiselect as bO,spinner as N}from"@crustjs/prompts";function e(O,X){return{name:O.meta.name,description:O.meta.description??"",version:X}}function AO(O){let X,Q=null;return{name:"skills",setup(Z,H){if(X=Z.rootCommand,O.command!==!1){let W=typeof O.command==="string"?O.command:"skill";Q=NO(X,O),H.addSubCommand(X,W,Q)}},async middleware(Z,H){if(Q&&Z.route?.command===Q){await H();return}let W=await F();if(W.length===0){await H();return}let $=O.autoInstall??!1,q=O.autoUpdate??!0,R=e(X,O.version),M=(await k({name:R.name,agents:W,scope:O.scope??"global"})).agents.filter((B)=>{if(!B.installed)return $;if(B.version!==R.version)return q;return!1});if(M.length>0)try{let B=await w({command:X,meta:R,agents:M.map((J)=>J.agent),scope:O.scope}),U=B.agents.filter((J)=>J.status==="installed").map((J)=>x[J.agent]),K=B.agents.filter((J)=>J.status==="updated").map((J)=>x[J.agent]);if(U.length>0)if(O.command!==!1){let J=`${X.meta.name} skill`;console.log(`Auto-installed skill "${R.name}" v${R.version} for ${U.join(", ")}. Manage with \`${J}\`.`)}else console.log(`Auto-installed skill "${R.name}" v${R.version} for ${U.join(", ")}.`);if(K.length>0)console.log(`Updated skill "${R.name}" to v${R.version} for ${K.join(", ")}.`)}catch(B){if(B instanceof j)console.warn(`Skill conflict: "${B.details.outputDir}" already exists but was not created by ${R.name}. Skipping auto-update. Delete or rename the conflicting skill to resolve.`);else throw B}await H()}}}function NO(O,X){return DO({meta:{name:"skill",description:"Manage agent skill installations"},async run(){let Q=e(O,X.version),Z=X.scope??"global",H=await F();if(H.length===0){console.log("No supported agents detected. Install Claude Code or OpenCode first.");return}let W=await k({name:Q.name,agents:H,scope:Z}),$=[],q=W.agents.map((K)=>{let J=K.installed?`v${K.version} installed`:"not installed";if(K.installed)$.push(K.agent);return{label:x[K.agent],value:K.agent,hint:J}}),R=await bO({message:"Select agents to install skills for",choices:q,default:$,required:!1}),z=R.filter((K)=>!$.includes(K)),M=R.filter((K)=>{let J=W.agents.find((Y)=>Y.agent===K);return J?.installed===!0&&J.version!==Q.version}),B=$.filter((K)=>!R.includes(K)),U=[...z,...M];if(U.length>0)try{let K=await N({message:"Installing skills...",task:async()=>w({command:O,meta:Q,agents:U,scope:Z})});console.log(`
|
|
8
|
+
Installed "${Q.name}" v${Q.version}`);for(let J of K.agents)console.log(` ${x[J.agent]} \u2192 ${J.outputDir}`)}catch(K){if(K instanceof j)if(await PO({message:`"${K.details.outputDir}" already exists but was not created by Crust. Overwrite?`,default:!1})){let Y=await N({message:"Overwriting skill...",task:async()=>w({command:O,meta:Q,agents:[K.details.agent],scope:Z,force:!0})});console.log(`
|
|
9
|
+
Installed "${Q.name}" v${Q.version}`);for(let V of Y.agents)console.log(` ${x[V.agent]} \u2192 ${V.outputDir}`)}else console.log(`
|
|
10
|
+
Skipped ${x[K.details.agent]}`);else throw K}if(B.length>0){let J=(await N({message:"Removing skills...",task:async()=>A({name:Q.name,agents:B,scope:Z})})).agents.filter((Y)=>Y.status==="removed").map((Y)=>x[Y.agent]);if(J.length>0)console.log(`
|
|
11
|
+
Removed from ${J.join(", ")}`)}if(U.length===0&&B.length===0)console.log("No changes.")}})}export{A as uninstallSkill,k as skillStatus,AO as skillPlugin,S as resolveSkillName,n as isValidSkillName,w as generateSkill,F as detectInstalledAgents,j as SkillConflictError};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crustjs/skills",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Agent skill generation from Crust command definitions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@crustjs/config": "0.0.0",
|
|
46
|
-
"@crustjs/core": "0.0.
|
|
47
|
-
"@crustjs/prompts": "0.0.
|
|
46
|
+
"@crustjs/core": "0.0.9",
|
|
47
|
+
"@crustjs/prompts": "0.0.6",
|
|
48
48
|
"bunup": "^0.16.29"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"@crustjs/core": "0.0.
|
|
52
|
-
"@crustjs/prompts": "0.0.
|
|
51
|
+
"@crustjs/core": "0.0.9",
|
|
52
|
+
"@crustjs/prompts": "0.0.6",
|
|
53
53
|
"typescript": "^5"
|
|
54
54
|
}
|
|
55
55
|
}
|