skanvil 1.0.0 → 1.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/ARCHITECTURE.md +3 -1
- package/CHANGELOG.md +24 -4
- package/README.md +16 -11
- package/agent-skill/skanvil/SKILL.md +24 -13
- package/agent-skill/skanvil/references/REFERENCE.md +38 -12
- package/dist/{chunk-TYO4LLN5.js → chunk-WXEWECJP.js} +658 -133
- package/dist/cli.js +22 -3
- package/dist/index.d.ts +180 -1
- package/dist/index.js +19 -1
- package/docs/commands.md +23 -11
- package/docs/getting-started.md +46 -0
- package/docs/params-and-exports.md +196 -0
- package/docs/sync.md +4 -0
- package/docs/validation.md +26 -24
- package/package.json +1 -1
- package/templates/library/README.md +13 -0
- package/templates/library/gitignore +1 -0
- package/templates/library/package.json +21 -2
|
@@ -200,18 +200,192 @@ ${this.body}`;
|
|
|
200
200
|
}
|
|
201
201
|
};
|
|
202
202
|
|
|
203
|
+
// src/core/params.ts
|
|
204
|
+
import fs from "fs/promises";
|
|
205
|
+
import path2 from "path";
|
|
206
|
+
import { z } from "zod";
|
|
207
|
+
var PARAMS_FILE = "params.json";
|
|
208
|
+
var ParamSchema = z.union([
|
|
209
|
+
z.object({
|
|
210
|
+
type: z.literal("enum"),
|
|
211
|
+
values: z.array(z.string()).min(1),
|
|
212
|
+
default: z.string(),
|
|
213
|
+
description: z.string().optional()
|
|
214
|
+
}).strict(),
|
|
215
|
+
z.object({ type: z.literal("string"), default: z.string(), description: z.string().optional() }).strict(),
|
|
216
|
+
z.object({ type: z.literal("boolean"), default: z.boolean(), description: z.string().optional() }).strict()
|
|
217
|
+
]);
|
|
218
|
+
var ParamsFileSchema = z.record(
|
|
219
|
+
z.string().regex(/^[a-zA-Z][a-zA-Z0-9]*$/, "parameter names must be camelCase letters and digits"),
|
|
220
|
+
ParamSchema
|
|
221
|
+
);
|
|
222
|
+
function parseParams(text) {
|
|
223
|
+
let json;
|
|
224
|
+
try {
|
|
225
|
+
json = JSON.parse(text);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
throw new Error(`${PARAMS_FILE} is not valid JSON: ${error.message}`);
|
|
228
|
+
}
|
|
229
|
+
if (json && typeof json === "object" && !Array.isArray(json)) {
|
|
230
|
+
const invalid = Object.keys(json).filter((key) => !/^[a-zA-Z][a-zA-Z0-9]*$/.test(key));
|
|
231
|
+
if (invalid.length > 0) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`invalid ${PARAMS_FILE}: parameter names must be camelCase letters and digits (${invalid.join(", ")})`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const parsed = ParamsFileSchema.safeParse(json);
|
|
238
|
+
if (!parsed.success) {
|
|
239
|
+
const issues = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`);
|
|
240
|
+
throw new Error(`invalid ${PARAMS_FILE}: ${issues.slice(0, 3).join("; ")}`);
|
|
241
|
+
}
|
|
242
|
+
for (const [name, definition] of Object.entries(parsed.data)) {
|
|
243
|
+
if (definition.type === "enum" && !definition.values.includes(definition.default)) {
|
|
244
|
+
throw new Error(`invalid ${PARAMS_FILE}: ${name}.default must be one of ${definition.values.join(", ")}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return parsed.data;
|
|
248
|
+
}
|
|
249
|
+
async function readParams(skillDir) {
|
|
250
|
+
let text;
|
|
251
|
+
try {
|
|
252
|
+
text = await fs.readFile(path2.join(skillDir, PARAMS_FILE), "utf8");
|
|
253
|
+
} catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
return parseParams(text);
|
|
257
|
+
}
|
|
258
|
+
function resolveParamValues(definitions, given = {}, skill = "skill", ignoreUnknown = false) {
|
|
259
|
+
const values = {};
|
|
260
|
+
for (const key of Object.keys(given)) {
|
|
261
|
+
if (!Object.prototype.hasOwnProperty.call(definitions, key) && !ignoreUnknown) {
|
|
262
|
+
const known = Object.keys(definitions);
|
|
263
|
+
throw new Error(
|
|
264
|
+
`${skill} has no parameter '${key}'` + (known.length ? ` (parameters: ${known.join(", ")})` : " (it has no parameters)")
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
for (const [name, definition] of Object.entries(definitions)) {
|
|
269
|
+
const raw = Object.prototype.hasOwnProperty.call(given, name) && given[name] !== void 0 ? given[name] : definition.default;
|
|
270
|
+
if (definition.type === "boolean") {
|
|
271
|
+
if (raw === true || raw === "true") values[name] = true;
|
|
272
|
+
else if (raw === false || raw === "false") values[name] = false;
|
|
273
|
+
else throw new Error(`${skill}: parameter '${name}' must be true or false, got ${JSON.stringify(raw)}`);
|
|
274
|
+
} else if (typeof raw !== "string") {
|
|
275
|
+
throw new Error(`${skill}: parameter '${name}' must be a string, got ${JSON.stringify(raw)}`);
|
|
276
|
+
} else if (definition.type === "enum" && !definition.values.includes(raw)) {
|
|
277
|
+
throw new Error(`${skill}: parameter '${name}' must be one of ${definition.values.join(", ")}, got "${raw}"`);
|
|
278
|
+
} else {
|
|
279
|
+
values[name] = raw;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return values;
|
|
283
|
+
}
|
|
284
|
+
function renderTemplate(text, values) {
|
|
285
|
+
const tagPattern = /\{\{\s*(#if\s+[^}]*?|else|\/if|[A-Za-z][A-Za-z0-9]*)\s*\}\}/g;
|
|
286
|
+
const tokens = [];
|
|
287
|
+
let cursor = 0;
|
|
288
|
+
let match;
|
|
289
|
+
while ((match = tagPattern.exec(text)) !== null) {
|
|
290
|
+
const body = match[1];
|
|
291
|
+
const isBlock = body.startsWith("#if") || body === "else" || body === "/if";
|
|
292
|
+
let start = match.index;
|
|
293
|
+
let end = match.index + match[0].length;
|
|
294
|
+
if (isBlock) {
|
|
295
|
+
const lineStart = text.lastIndexOf("\n", start - 1) + 1;
|
|
296
|
+
const newline = text.indexOf("\n", end);
|
|
297
|
+
const lineEnd = newline === -1 ? text.length : newline;
|
|
298
|
+
const before = text.slice(lineStart, start);
|
|
299
|
+
const after = text.slice(end, lineEnd);
|
|
300
|
+
if (before.trim() === "" && after.trim() === "" && lineStart >= cursor) {
|
|
301
|
+
start = lineStart;
|
|
302
|
+
end = newline === -1 ? text.length : newline + 1;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (start > cursor) tokens.push({ kind: "text", value: text.slice(cursor, start) });
|
|
306
|
+
if (body.startsWith("#if")) tokens.push({ kind: "if", expr: body.slice(3).trim() });
|
|
307
|
+
else if (body === "else") tokens.push({ kind: "else" });
|
|
308
|
+
else if (body === "/if") tokens.push({ kind: "end" });
|
|
309
|
+
else tokens.push({ kind: "var", name: body, raw: match[0] });
|
|
310
|
+
cursor = end;
|
|
311
|
+
tagPattern.lastIndex = end;
|
|
312
|
+
}
|
|
313
|
+
if (cursor < text.length) tokens.push({ kind: "text", value: text.slice(cursor) });
|
|
314
|
+
const has = (name) => Object.prototype.hasOwnProperty.call(values, name);
|
|
315
|
+
const evaluate = (expr) => {
|
|
316
|
+
const comparison = expr.match(/^([A-Za-z][A-Za-z0-9]*)\s*(==|!=)\s*(?:"([^"]*)"|'([^']*)')$/);
|
|
317
|
+
if (comparison) {
|
|
318
|
+
const [, name, operator, double, single] = comparison;
|
|
319
|
+
if (!has(name)) throw new Error(`{{#if ${expr}}} uses unknown parameter '${name}'`);
|
|
320
|
+
const equal = String(values[name]) === (double !== void 0 ? double : single);
|
|
321
|
+
return operator === "==" ? equal : !equal;
|
|
322
|
+
}
|
|
323
|
+
const truthy = expr.match(/^(!?)([A-Za-z][A-Za-z0-9]*)$/);
|
|
324
|
+
if (truthy) {
|
|
325
|
+
const [, negate, name] = truthy;
|
|
326
|
+
if (!has(name)) throw new Error(`{{#if ${expr}}} uses unknown parameter '${name}'`);
|
|
327
|
+
const value = values[name];
|
|
328
|
+
const result = value !== false && value !== "" && value !== "false";
|
|
329
|
+
return negate ? !result : result;
|
|
330
|
+
}
|
|
331
|
+
throw new Error(`invalid condition {{#if ${expr}}}: use name, !name, name == "value" or name != "value"`);
|
|
332
|
+
};
|
|
333
|
+
const stack = [];
|
|
334
|
+
let output = "";
|
|
335
|
+
let active = true;
|
|
336
|
+
for (const token of tokens) {
|
|
337
|
+
if (token.kind === "if") {
|
|
338
|
+
const condition = active ? evaluate(token.expr) : false;
|
|
339
|
+
stack.push({ active: condition, parentActive: active, sawElse: false });
|
|
340
|
+
active = active && condition;
|
|
341
|
+
} else if (token.kind === "else") {
|
|
342
|
+
const frame = stack[stack.length - 1];
|
|
343
|
+
if (!frame || frame.sawElse) throw new Error("{{else}} without a matching {{#if}}");
|
|
344
|
+
frame.sawElse = true;
|
|
345
|
+
active = frame.parentActive && !frame.active;
|
|
346
|
+
} else if (token.kind === "end") {
|
|
347
|
+
const frame = stack.pop();
|
|
348
|
+
if (!frame) throw new Error("{{/if}} without a matching {{#if}}");
|
|
349
|
+
active = frame.parentActive;
|
|
350
|
+
} else if (active) {
|
|
351
|
+
output += token.kind === "text" ? token.value : has(token.name) ? String(values[token.name]) : token.raw;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (stack.length > 0) throw new Error(`${stack.length} {{#if}} block(s) are not closed with {{/if}}`);
|
|
355
|
+
return output;
|
|
356
|
+
}
|
|
357
|
+
function findUndeclaredParams(text, definitions) {
|
|
358
|
+
const names = /* @__PURE__ */ new Set();
|
|
359
|
+
for (const match of text.matchAll(/\{\{\s*([A-Za-z][A-Za-z0-9]*)\s*\}\}/g)) {
|
|
360
|
+
if (match[1] !== "else" && !Object.prototype.hasOwnProperty.call(definitions, match[1])) names.add(match[1]);
|
|
361
|
+
}
|
|
362
|
+
return [...names];
|
|
363
|
+
}
|
|
364
|
+
function parseParamPairs(pairs) {
|
|
365
|
+
const values = {};
|
|
366
|
+
for (const pair of pairs) {
|
|
367
|
+
const index = pair.indexOf("=");
|
|
368
|
+
const key = index === -1 ? "" : pair.slice(0, index).trim();
|
|
369
|
+
if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(key)) {
|
|
370
|
+
throw new Error(`Invalid --param '${pair}'. Use name=value, e.g. --param ticketTracker=jira`);
|
|
371
|
+
}
|
|
372
|
+
values[key] = pair.slice(index + 1);
|
|
373
|
+
}
|
|
374
|
+
return values;
|
|
375
|
+
}
|
|
376
|
+
|
|
203
377
|
// src/core/templates.ts
|
|
204
|
-
import
|
|
205
|
-
import
|
|
378
|
+
import fs3 from "fs/promises";
|
|
379
|
+
import path4 from "path";
|
|
206
380
|
import { stringify } from "yaml";
|
|
207
381
|
|
|
208
382
|
// src/util/fs.ts
|
|
209
|
-
import
|
|
383
|
+
import fs2 from "fs/promises";
|
|
210
384
|
import { createHash } from "crypto";
|
|
211
|
-
import
|
|
385
|
+
import path3 from "path";
|
|
212
386
|
async function exists(target) {
|
|
213
387
|
try {
|
|
214
|
-
await
|
|
388
|
+
await fs2.access(target);
|
|
215
389
|
return true;
|
|
216
390
|
} catch {
|
|
217
391
|
return false;
|
|
@@ -219,14 +393,14 @@ async function exists(target) {
|
|
|
219
393
|
}
|
|
220
394
|
async function isEmptyOrMissing(dir) {
|
|
221
395
|
try {
|
|
222
|
-
return (await
|
|
396
|
+
return (await fs2.readdir(dir)).length === 0;
|
|
223
397
|
} catch {
|
|
224
398
|
return true;
|
|
225
399
|
}
|
|
226
400
|
}
|
|
227
401
|
async function listFiles(dir, relative = "") {
|
|
228
402
|
const out = [];
|
|
229
|
-
for (const entry of await
|
|
403
|
+
for (const entry of await fs2.readdir(path3.join(dir, relative), { withFileTypes: true })) {
|
|
230
404
|
const rel = relative ? `${relative}/${entry.name}` : entry.name;
|
|
231
405
|
if (entry.isDirectory()) {
|
|
232
406
|
out.push(...await listFiles(dir, rel));
|
|
@@ -241,7 +415,7 @@ async function hashDirectory(dir) {
|
|
|
241
415
|
for (const file of await listFiles(dir)) {
|
|
242
416
|
hash.update(file);
|
|
243
417
|
hash.update("\0");
|
|
244
|
-
hash.update(await
|
|
418
|
+
hash.update(await fs2.readFile(path3.join(dir, file)));
|
|
245
419
|
hash.update("\0");
|
|
246
420
|
}
|
|
247
421
|
return hash.digest("hex").slice(0, 16);
|
|
@@ -286,8 +460,8 @@ function renderSkillMarkdown(text, variables) {
|
|
|
286
460
|
}
|
|
287
461
|
async function copyTemplateDir(source, destination, options = {}, relative = "") {
|
|
288
462
|
const { variables = {}, skip = () => false, render: shouldRender = true } = options;
|
|
289
|
-
await
|
|
290
|
-
for (const entry of await
|
|
463
|
+
await fs3.mkdir(path4.join(destination, relative), { recursive: true });
|
|
464
|
+
for (const entry of await fs3.readdir(path4.join(source, relative), { withFileTypes: true })) {
|
|
291
465
|
const relativePath = relative ? `${relative}/${entry.name}` : entry.name;
|
|
292
466
|
if (skip(relativePath)) continue;
|
|
293
467
|
if (entry.isDirectory()) {
|
|
@@ -295,45 +469,45 @@ async function copyTemplateDir(source, destination, options = {}, relative = "")
|
|
|
295
469
|
continue;
|
|
296
470
|
}
|
|
297
471
|
const targetName = shouldRender ? FILE_ALIASES[entry.name] || entry.name : entry.name;
|
|
298
|
-
const from =
|
|
299
|
-
const target =
|
|
300
|
-
if (shouldRender && TEXT_EXTENSIONS.has(
|
|
301
|
-
const text = await
|
|
472
|
+
const from = path4.join(source, relative, entry.name);
|
|
473
|
+
const target = path4.join(destination, relative, targetName);
|
|
474
|
+
if (shouldRender && TEXT_EXTENSIONS.has(path4.extname(entry.name))) {
|
|
475
|
+
const text = await fs3.readFile(from, "utf8");
|
|
302
476
|
const rendered = entry.name === "SKILL.md" ? renderSkillMarkdown(text, variables) : render(text, variables);
|
|
303
|
-
await
|
|
477
|
+
await fs3.writeFile(target, rendered, "utf8");
|
|
304
478
|
} else {
|
|
305
|
-
await
|
|
479
|
+
await fs3.copyFile(from, target);
|
|
306
480
|
}
|
|
307
481
|
}
|
|
308
482
|
}
|
|
309
483
|
function builtinTemplate(name) {
|
|
310
|
-
return
|
|
484
|
+
return path4.join(TEMPLATES_ROOT, name);
|
|
311
485
|
}
|
|
312
486
|
async function resolveSkillTemplate(library, categoryId, explicit) {
|
|
313
|
-
const localRoot =
|
|
487
|
+
const localRoot = path4.join(library.root, library.config.templates.dir);
|
|
314
488
|
if (explicit) {
|
|
315
|
-
const candidate =
|
|
489
|
+
const candidate = path4.join(localRoot, explicit);
|
|
316
490
|
if (await exists(candidate)) return candidate;
|
|
317
491
|
throw new Error(`Template '${explicit}' not found in ${localRoot}.`);
|
|
318
492
|
}
|
|
319
|
-
for (const candidate of [
|
|
493
|
+
for (const candidate of [path4.join(localRoot, `skill-${categoryId}`), path4.join(localRoot, "skill")]) {
|
|
320
494
|
if (await exists(candidate)) return candidate;
|
|
321
495
|
}
|
|
322
496
|
return builtinTemplate("skill");
|
|
323
497
|
}
|
|
324
498
|
async function ejectSkillTemplate(library, categoryId, force = false) {
|
|
325
499
|
const folder = categoryId ? `skill-${library.categories.resolve(categoryId).id}` : "skill";
|
|
326
|
-
const target =
|
|
500
|
+
const target = path4.join(library.root, library.config.templates.dir, folder);
|
|
327
501
|
if (await exists(target) && !force) {
|
|
328
502
|
throw new Error(`Template folder already exists: ${target} (use --force to overwrite).`);
|
|
329
503
|
}
|
|
330
|
-
await
|
|
504
|
+
await fs3.rm(target, { recursive: true, force: true });
|
|
331
505
|
await copyTemplateDir(builtinTemplate("skill"), target, { render: false });
|
|
332
506
|
return target;
|
|
333
507
|
}
|
|
334
508
|
|
|
335
509
|
// src/core/validate.ts
|
|
336
|
-
import
|
|
510
|
+
import path5 from "path";
|
|
337
511
|
import { isMap as isMap2, isScalar as isScalar2 } from "yaml";
|
|
338
512
|
|
|
339
513
|
// src/core/naming.ts
|
|
@@ -400,6 +574,14 @@ var RULES = {
|
|
|
400
574
|
severity: "warn",
|
|
401
575
|
description: "The body no longer contains the generic text of the built-in skill template."
|
|
402
576
|
},
|
|
577
|
+
"params-file": {
|
|
578
|
+
severity: "error",
|
|
579
|
+
description: "params.json, when present, is valid: camelCase names, a type, and a default (in its values for enums)."
|
|
580
|
+
},
|
|
581
|
+
"params-template": {
|
|
582
|
+
severity: "error",
|
|
583
|
+
description: "{{placeholders}} and {{#if}} blocks use declared parameters and are balanced."
|
|
584
|
+
},
|
|
403
585
|
"readme-missing": { severity: "error", description: "Every indexed skill is listed in the README.", fixable: true },
|
|
404
586
|
"readme-orphan": { severity: "error", description: "README does not reference missing skills.", fixable: true },
|
|
405
587
|
"readme-section": {
|
|
@@ -557,7 +739,7 @@ function findBrokenReferences(body, files) {
|
|
|
557
739
|
continue;
|
|
558
740
|
const clean = safeDecode(raw.split(/[?#]/)[0]);
|
|
559
741
|
if (!clean) continue;
|
|
560
|
-
const resolved =
|
|
742
|
+
const resolved = path5.posix.normalize(clean).replace(/\/+$/, "");
|
|
561
743
|
if (resolved === "." || resolved === "") continue;
|
|
562
744
|
if (resolved === ".." || resolved.startsWith("../")) {
|
|
563
745
|
problems.push(`reference '${raw}' points outside the skill folder`);
|
|
@@ -567,6 +749,31 @@ function findBrokenReferences(body, files) {
|
|
|
567
749
|
}
|
|
568
750
|
return problems;
|
|
569
751
|
}
|
|
752
|
+
async function renderForValidation(library, folder, raw, collector) {
|
|
753
|
+
let params;
|
|
754
|
+
try {
|
|
755
|
+
params = await library.readSkillParams(folder);
|
|
756
|
+
} catch (error) {
|
|
757
|
+
collector.add("params-file", error.message, folder);
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
if (!params) return null;
|
|
761
|
+
const source = raw.toLfString();
|
|
762
|
+
const undeclared = findUndeclaredParams(source, params);
|
|
763
|
+
if (undeclared.length > 0) {
|
|
764
|
+
collector.add(
|
|
765
|
+
"params-template",
|
|
766
|
+
`uses undeclared parameter(s) ${undeclared.map((name) => `{{${name}}}`).join(", ")}; declare them in ${PARAMS_FILE}`,
|
|
767
|
+
folder
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
return SkillFile.parse(renderTemplate(source, resolveParamValues(params, {}, folder)));
|
|
772
|
+
} catch (error) {
|
|
773
|
+
collector.add("params-template", error.message, folder);
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
570
777
|
async function validateLibrary(library, options = {}) {
|
|
571
778
|
const collector = new Collector(library.config.validation.rules);
|
|
572
779
|
const folders = await library.listSkillFolders();
|
|
@@ -581,6 +788,8 @@ async function validateLibrary(library, options = {}) {
|
|
|
581
788
|
collector.add("skill-file", "missing SKILL.md", folder);
|
|
582
789
|
continue;
|
|
583
790
|
}
|
|
791
|
+
const rendered = await renderForValidation(library, folder, skill, collector);
|
|
792
|
+
if (rendered) skill = rendered;
|
|
584
793
|
const files = new Set(await listFiles(library.skillPath(folder)));
|
|
585
794
|
const allowUnderscore = library.categories.fromFolder(folder).prefix === "_";
|
|
586
795
|
const name = validateSkillFile(collector, folder, skill, ruleOptions, allowUnderscore, files);
|
|
@@ -636,9 +845,10 @@ function formatDiagnostic(diagnostic) {
|
|
|
636
845
|
}
|
|
637
846
|
|
|
638
847
|
// src/core/sync.ts
|
|
639
|
-
import
|
|
848
|
+
import fs4 from "fs/promises";
|
|
640
849
|
import os from "os";
|
|
641
|
-
import
|
|
850
|
+
import path6 from "path";
|
|
851
|
+
import { createHash as createHash2 } from "crypto";
|
|
642
852
|
var TARGET_PRESETS = {
|
|
643
853
|
claude: ".claude/skills",
|
|
644
854
|
// Claude Code
|
|
@@ -663,51 +873,72 @@ function resolveTarget(dest, target, options = {}) {
|
|
|
663
873
|
`Global installs only accept presets (${Object.keys(USER_TARGET_PRESETS).join(", ")}), got '${target}'.`
|
|
664
874
|
);
|
|
665
875
|
}
|
|
666
|
-
return
|
|
876
|
+
return path6.join(options.homeDir ?? os.homedir(), preset);
|
|
667
877
|
}
|
|
668
|
-
const root =
|
|
669
|
-
if (TARGET_PRESETS[target]) return
|
|
670
|
-
const resolved =
|
|
671
|
-
const relative =
|
|
672
|
-
if (!relative || relative.startsWith("..") ||
|
|
878
|
+
const root = path6.resolve(dest);
|
|
879
|
+
if (TARGET_PRESETS[target]) return path6.resolve(root, TARGET_PRESETS[target]);
|
|
880
|
+
const resolved = path6.resolve(root, target);
|
|
881
|
+
const relative = path6.relative(root, resolved);
|
|
882
|
+
if (!relative || relative.startsWith("..") || path6.isAbsolute(relative)) {
|
|
673
883
|
throw new Error(`Target '${target}' must be a folder inside ${root}.`);
|
|
674
884
|
}
|
|
675
885
|
return resolved;
|
|
676
886
|
}
|
|
677
887
|
async function readManifest(dir) {
|
|
678
888
|
try {
|
|
679
|
-
const parsed = JSON.parse(await
|
|
889
|
+
const parsed = JSON.parse(await fs4.readFile(path6.join(dir, MANIFEST_FILE), "utf8"));
|
|
680
890
|
return parsed && parsed.tool === "skanvil" && parsed.skills ? parsed : null;
|
|
681
891
|
} catch {
|
|
682
892
|
return null;
|
|
683
893
|
}
|
|
684
894
|
}
|
|
685
895
|
async function resolveSource(from, dest) {
|
|
686
|
-
const direct =
|
|
896
|
+
const direct = path6.resolve(dest, from);
|
|
687
897
|
if (await exists(direct)) return direct;
|
|
688
|
-
let dir =
|
|
898
|
+
let dir = path6.resolve(dest);
|
|
689
899
|
for (; ; ) {
|
|
690
|
-
const candidate =
|
|
900
|
+
const candidate = path6.join(dir, "node_modules", from);
|
|
691
901
|
if (await exists(candidate)) return candidate;
|
|
692
|
-
const parent =
|
|
902
|
+
const parent = path6.dirname(dir);
|
|
693
903
|
if (parent === dir) break;
|
|
694
904
|
dir = parent;
|
|
695
905
|
}
|
|
696
906
|
throw new Error(`Cannot find skills source '${from}' (not a folder, nor an installed npm package).`);
|
|
697
907
|
}
|
|
698
|
-
async function place(source, target, mode, warnings) {
|
|
699
|
-
await
|
|
700
|
-
if (mode === "link") {
|
|
908
|
+
async function place(source, target, mode, warnings, rendered) {
|
|
909
|
+
await fs4.rm(target, { recursive: true, force: true });
|
|
910
|
+
if (mode === "link" && rendered !== null) {
|
|
911
|
+
warnings.push(`${path6.basename(target)} has parameters, so it was copied rendered instead of linked`);
|
|
912
|
+
} else if (mode === "link") {
|
|
701
913
|
try {
|
|
702
|
-
await
|
|
914
|
+
await fs4.symlink(source, target, process.platform === "win32" ? "junction" : "dir");
|
|
703
915
|
return "link";
|
|
704
916
|
} catch (error) {
|
|
705
|
-
warnings.push(`could not link ${
|
|
917
|
+
warnings.push(`could not link ${path6.basename(target)} (${error.message}); copied instead`);
|
|
706
918
|
}
|
|
707
919
|
}
|
|
708
|
-
await
|
|
920
|
+
await fs4.cp(source, target, { recursive: true });
|
|
921
|
+
if (rendered !== null) {
|
|
922
|
+
await fs4.writeFile(path6.join(target, "SKILL.md"), rendered, "utf8");
|
|
923
|
+
await fs4.rm(path6.join(target, PARAMS_FILE), { force: true });
|
|
924
|
+
}
|
|
709
925
|
return "copy";
|
|
710
926
|
}
|
|
927
|
+
async function materialize(library, name, definitions, values) {
|
|
928
|
+
const dir = library.skillPath(name);
|
|
929
|
+
if (!definitions) return { hash: await hashDirectory(dir), rendered: null };
|
|
930
|
+
const raw = await fs4.readFile(path6.join(dir, "SKILL.md"), "utf8");
|
|
931
|
+
const rendered = renderTemplate(raw, resolveParamValues(definitions, values, name, true));
|
|
932
|
+
const hash = createHash2("sha256");
|
|
933
|
+
for (const file of await listFiles(dir)) {
|
|
934
|
+
if (file === PARAMS_FILE) continue;
|
|
935
|
+
hash.update(file);
|
|
936
|
+
hash.update("\0");
|
|
937
|
+
hash.update(file === "SKILL.md" ? Buffer.from(rendered, "utf8") : await fs4.readFile(path6.join(dir, file)));
|
|
938
|
+
hash.update("\0");
|
|
939
|
+
}
|
|
940
|
+
return { hash: hash.digest("hex").slice(0, 16), rendered };
|
|
941
|
+
}
|
|
711
942
|
async function syncSkills(library, options) {
|
|
712
943
|
const mode = options.mode ?? library.config.sync.mode;
|
|
713
944
|
const filtered = Boolean(options.categories?.length || options.skills?.length);
|
|
@@ -739,15 +970,34 @@ async function syncSkills(library, options) {
|
|
|
739
970
|
}
|
|
740
971
|
}
|
|
741
972
|
}
|
|
742
|
-
const
|
|
743
|
-
for (const name of candidates)
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
973
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
974
|
+
for (const name of candidates) definitions.set(name, await library.readSkillParams(name));
|
|
975
|
+
const declared = new Set([...definitions.values()].flatMap((defs) => Object.keys(defs ?? {})));
|
|
976
|
+
if (options.params) {
|
|
977
|
+
const unknown = Object.keys(options.params).filter((key) => !declared.has(key));
|
|
978
|
+
if (unknown.length > 0) {
|
|
979
|
+
throw new Error(
|
|
980
|
+
`Unknown parameter(s) ${unknown.join(", ")}: no synced skill declares them` + (declared.size ? ` (declared: ${[...declared].join(", ")})` : " (no synced skill has parameters)")
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
const cache = /* @__PURE__ */ new Map();
|
|
985
|
+
const plans = [];
|
|
986
|
+
for (const target of options.targets) {
|
|
987
|
+
const dir = resolveTarget(options.dest, target, { scope: options.scope, homeDir: options.homeDir });
|
|
750
988
|
const manifest = await readManifest(dir) ?? { tool: "skanvil", version: "", source: "", skills: {} };
|
|
989
|
+
const remembered = Object.fromEntries(Object.entries(manifest.params ?? {}).filter(([key]) => declared.has(key)));
|
|
990
|
+
const values = options.params ?? remembered;
|
|
991
|
+
const items = /* @__PURE__ */ new Map();
|
|
992
|
+
for (const name of candidates) {
|
|
993
|
+
const key = `${name}\0${JSON.stringify(values)}`;
|
|
994
|
+
if (!cache.has(key)) cache.set(key, await materialize(library, name, definitions.get(name) ?? null, values));
|
|
995
|
+
items.set(name, cache.get(key));
|
|
996
|
+
}
|
|
997
|
+
plans.push({ target, dir, manifest, values, items });
|
|
998
|
+
}
|
|
999
|
+
const results = [];
|
|
1000
|
+
for (const { target, dir, manifest, values, items } of plans) {
|
|
751
1001
|
const result = {
|
|
752
1002
|
target,
|
|
753
1003
|
path: dir,
|
|
@@ -756,21 +1006,23 @@ async function syncSkills(library, options) {
|
|
|
756
1006
|
unchanged: [],
|
|
757
1007
|
removed: [],
|
|
758
1008
|
skipped: [...skipped],
|
|
759
|
-
warnings: []
|
|
1009
|
+
warnings: [],
|
|
1010
|
+
params: declared.size ? values : {}
|
|
760
1011
|
};
|
|
761
1012
|
const next = {
|
|
762
1013
|
tool: "skanvil",
|
|
763
1014
|
version: packageVersion(),
|
|
764
1015
|
// Relative, so a committed manifest does not change from machine to machine.
|
|
765
|
-
source:
|
|
766
|
-
skills: filtered ? { ...manifest.skills } : {}
|
|
1016
|
+
source: path6.relative(dir, library.root).split(path6.sep).join("/"),
|
|
1017
|
+
skills: filtered ? { ...manifest.skills } : {},
|
|
1018
|
+
...declared.size && Object.keys(values).length ? { params: values } : {}
|
|
767
1019
|
};
|
|
768
|
-
if (!options.dryRun) await
|
|
1020
|
+
if (!options.dryRun) await fs4.mkdir(dir, { recursive: true });
|
|
769
1021
|
for (const name of candidates) {
|
|
770
|
-
const destination =
|
|
1022
|
+
const destination = path6.join(dir, name);
|
|
771
1023
|
const previous = manifest.skills[name];
|
|
772
1024
|
const present = await exists(destination);
|
|
773
|
-
const hash =
|
|
1025
|
+
const { hash, rendered } = items.get(name);
|
|
774
1026
|
if (present && !previous) {
|
|
775
1027
|
result.skipped.push({ skill: name, reason: `${destination} exists and is not managed by skanvil` });
|
|
776
1028
|
continue;
|
|
@@ -781,7 +1033,7 @@ async function syncSkills(library, options) {
|
|
|
781
1033
|
continue;
|
|
782
1034
|
}
|
|
783
1035
|
(present ? result.updated : result.added).push(name);
|
|
784
|
-
const actualMode = options.dryRun ? mode : await place(library.skillPath(name), destination, mode, result.warnings);
|
|
1036
|
+
const actualMode = options.dryRun ? mode : await place(library.skillPath(name), destination, mode, result.warnings, rendered);
|
|
785
1037
|
next.skills[name] = { hash, mode: actualMode };
|
|
786
1038
|
}
|
|
787
1039
|
if (!filtered) {
|
|
@@ -792,11 +1044,11 @@ async function syncSkills(library, options) {
|
|
|
792
1044
|
continue;
|
|
793
1045
|
}
|
|
794
1046
|
result.removed.push(name);
|
|
795
|
-
if (!options.dryRun) await
|
|
1047
|
+
if (!options.dryRun) await fs4.rm(path6.join(dir, name), { recursive: true, force: true });
|
|
796
1048
|
}
|
|
797
1049
|
}
|
|
798
1050
|
if (!options.dryRun) {
|
|
799
|
-
await
|
|
1051
|
+
await fs4.writeFile(path6.join(dir, MANIFEST_FILE), `${JSON.stringify(next, null, 2)}
|
|
800
1052
|
`, "utf8");
|
|
801
1053
|
}
|
|
802
1054
|
results.push(result);
|
|
@@ -805,16 +1057,16 @@ async function syncSkills(library, options) {
|
|
|
805
1057
|
}
|
|
806
1058
|
|
|
807
1059
|
// src/core/agent-skill.ts
|
|
808
|
-
import
|
|
809
|
-
import
|
|
1060
|
+
import fs5 from "fs/promises";
|
|
1061
|
+
import path7 from "path";
|
|
810
1062
|
var AGENT_SKILL_NAME = "skanvil";
|
|
811
1063
|
var AGENT_SKILL_VERSION_KEY = "skanvil-version";
|
|
812
1064
|
function agentSkillSource() {
|
|
813
|
-
return
|
|
1065
|
+
return path7.join(PACKAGE_ROOT, "agent-skill", AGENT_SKILL_NAME);
|
|
814
1066
|
}
|
|
815
1067
|
async function installedVersion(dir) {
|
|
816
1068
|
try {
|
|
817
|
-
const skill = SkillFile.parse(await
|
|
1069
|
+
const skill = SkillFile.parse(await fs5.readFile(path7.join(dir, "SKILL.md"), "utf8"));
|
|
818
1070
|
return skill.getString("metadata", AGENT_SKILL_VERSION_KEY) || null;
|
|
819
1071
|
} catch {
|
|
820
1072
|
return null;
|
|
@@ -824,7 +1076,7 @@ async function installAgentSkill(options) {
|
|
|
824
1076
|
const version = packageVersion();
|
|
825
1077
|
const results = [];
|
|
826
1078
|
const destinations = options.targets.map(
|
|
827
|
-
(target) =>
|
|
1079
|
+
(target) => path7.join(resolveTarget(options.dest, target, { scope: options.scope, homeDir: options.homeDir }), AGENT_SKILL_NAME)
|
|
828
1080
|
);
|
|
829
1081
|
for (const [index, target] of options.targets.entries()) {
|
|
830
1082
|
const destination = destinations[index];
|
|
@@ -843,18 +1095,255 @@ async function installAgentSkill(options) {
|
|
|
843
1095
|
results.push({ target, path: destination, status: "unchanged" });
|
|
844
1096
|
continue;
|
|
845
1097
|
}
|
|
846
|
-
await
|
|
847
|
-
await
|
|
848
|
-
await
|
|
849
|
-
const skillPath =
|
|
850
|
-
const skill = SkillFile.parse(await
|
|
1098
|
+
await fs5.rm(destination, { recursive: true, force: true });
|
|
1099
|
+
await fs5.mkdir(path7.dirname(destination), { recursive: true });
|
|
1100
|
+
await fs5.cp(agentSkillSource(), destination, { recursive: true });
|
|
1101
|
+
const skillPath = path7.join(destination, "SKILL.md");
|
|
1102
|
+
const skill = SkillFile.parse(await fs5.readFile(skillPath, "utf8"));
|
|
851
1103
|
skill.set(["metadata", AGENT_SKILL_VERSION_KEY], version);
|
|
852
|
-
await
|
|
1104
|
+
await fs5.writeFile(skillPath, skill.toString(), "utf8");
|
|
853
1105
|
results.push({ target, path: destination, status: present ? "updated" : "installed" });
|
|
854
1106
|
}
|
|
855
1107
|
return results;
|
|
856
1108
|
}
|
|
857
1109
|
|
|
1110
|
+
// src/core/build.ts
|
|
1111
|
+
import fs6 from "fs/promises";
|
|
1112
|
+
import path8 from "path";
|
|
1113
|
+
var RESERVED = new Set(
|
|
1114
|
+
"break case catch class const continue debugger default delete do else enum export extends false finally for function if import in instanceof new null return super switch this throw true try typeof var void while with yield let static implements interface package private protected public await arguments eval skills getSkill renderTemplate resolveParamValues".split(" ")
|
|
1115
|
+
);
|
|
1116
|
+
function camelCase(value) {
|
|
1117
|
+
return value.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part, index) => index === 0 ? part.toLowerCase() : part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1118
|
+
}
|
|
1119
|
+
function capitalize(identifier) {
|
|
1120
|
+
return identifier.charAt(0).toUpperCase() + identifier.slice(1);
|
|
1121
|
+
}
|
|
1122
|
+
function exportNames(library, names) {
|
|
1123
|
+
const fix = (name) => /^[a-z]/.test(name) && !RESERVED.has(name) ? name : `skill${capitalize(name)}`;
|
|
1124
|
+
const short = new Map(names.map((name) => [name, fix(camelCase(library.categories.stripPrefix(name)))]));
|
|
1125
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1126
|
+
for (const value of short.values()) counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
1127
|
+
const result = /* @__PURE__ */ new Map();
|
|
1128
|
+
for (const name of names) {
|
|
1129
|
+
const candidate = short.get(name);
|
|
1130
|
+
result.set(name, counts.get(candidate) > 1 ? fix(camelCase(name)) : candidate);
|
|
1131
|
+
}
|
|
1132
|
+
return result;
|
|
1133
|
+
}
|
|
1134
|
+
function isText(buffer) {
|
|
1135
|
+
return !buffer.includes(0) && Buffer.from(buffer.toString("utf8"), "utf8").equals(buffer);
|
|
1136
|
+
}
|
|
1137
|
+
function docComment(lines, indent = "") {
|
|
1138
|
+
const body = lines.flatMap((line) => line.split("\n")).map((line) => `${indent} * ${line.replace(/\*\//g, "*\\/")}`.trimEnd());
|
|
1139
|
+
return `${indent}/**
|
|
1140
|
+
${body.join("\n")}
|
|
1141
|
+
${indent} */
|
|
1142
|
+
`;
|
|
1143
|
+
}
|
|
1144
|
+
function paramsType(definitions) {
|
|
1145
|
+
const entries = Object.entries(definitions).map(([name, definition]) => {
|
|
1146
|
+
const type = definition.type === "enum" ? definition.values.map((value) => JSON.stringify(value)).join(" | ") : definition.type === "boolean" ? "boolean" : "string";
|
|
1147
|
+
const doc = docComment(
|
|
1148
|
+
[
|
|
1149
|
+
...definition.description ? [definition.description] : [],
|
|
1150
|
+
`@defaultValue ${JSON.stringify(definition.default)}`
|
|
1151
|
+
],
|
|
1152
|
+
" "
|
|
1153
|
+
);
|
|
1154
|
+
return `${doc} ${name}?: ${type};`;
|
|
1155
|
+
});
|
|
1156
|
+
return entries.length ? `{
|
|
1157
|
+
${entries.join("\n")}
|
|
1158
|
+
}` : "Record<string, never>";
|
|
1159
|
+
}
|
|
1160
|
+
function runtimeSource(skills) {
|
|
1161
|
+
const data = skills.map((skill) => ({
|
|
1162
|
+
name: skill.name,
|
|
1163
|
+
category: skill.category,
|
|
1164
|
+
title: skill.title,
|
|
1165
|
+
description: skill.description,
|
|
1166
|
+
params: skill.params,
|
|
1167
|
+
template: skill.template,
|
|
1168
|
+
files: skill.files
|
|
1169
|
+
}));
|
|
1170
|
+
return `const renderTemplate = ${renderTemplate.toString()};
|
|
1171
|
+
|
|
1172
|
+
const resolveParamValues = ${resolveParamValues.toString()};
|
|
1173
|
+
|
|
1174
|
+
const definitions = ${JSON.stringify(data, null, 2)};
|
|
1175
|
+
|
|
1176
|
+
function createSkill(definition) {
|
|
1177
|
+
const render = (params) =>
|
|
1178
|
+
renderTemplate(definition.template, resolveParamValues(definition.params, params || {}, definition.name));
|
|
1179
|
+
return Object.freeze({
|
|
1180
|
+
name: definition.name,
|
|
1181
|
+
category: definition.category,
|
|
1182
|
+
title: definition.title,
|
|
1183
|
+
description: definition.description,
|
|
1184
|
+
params: definition.params,
|
|
1185
|
+
render,
|
|
1186
|
+
files: (params) => Object.assign({ "SKILL.md": { encoding: "utf8", content: render(params) } }, definition.files)
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const skills = Object.freeze(definitions.map(createSkill));
|
|
1191
|
+
|
|
1192
|
+
function getSkill(name) {
|
|
1193
|
+
const skill = skills.find((candidate) => candidate.name === name);
|
|
1194
|
+
if (!skill) throw new Error("Unknown skill '" + name + "'. Available: " + skills.map((s) => s.name).join(", "));
|
|
1195
|
+
return skill;
|
|
1196
|
+
}
|
|
1197
|
+
`;
|
|
1198
|
+
}
|
|
1199
|
+
function declarationSource(skills, packageName) {
|
|
1200
|
+
const header = `// Generated by skanvil build. Do not edit.
|
|
1201
|
+
|
|
1202
|
+
/** Definition of a skill parameter (from its params.json). */
|
|
1203
|
+
export interface SkillParamDefinition {
|
|
1204
|
+
type: "enum" | "string" | "boolean";
|
|
1205
|
+
/** Allowed values of an enum parameter. */
|
|
1206
|
+
values?: string[];
|
|
1207
|
+
default: string | boolean;
|
|
1208
|
+
description?: string;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/** A file of a skill. Text files are UTF-8, binary files base64. */
|
|
1212
|
+
export interface SkillFileContent {
|
|
1213
|
+
encoding: "utf8" | "base64";
|
|
1214
|
+
content: string;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/** A skill of ${packageName}. */
|
|
1218
|
+
export interface Skill<P = Record<string, unknown>> {
|
|
1219
|
+
/** Folder and frontmatter name, e.g. "front-api-review". */
|
|
1220
|
+
readonly name: string;
|
|
1221
|
+
/** Category id. */
|
|
1222
|
+
readonly category: string;
|
|
1223
|
+
/** Display title. */
|
|
1224
|
+
readonly title: string;
|
|
1225
|
+
/** Frontmatter description (rendered with default parameters). */
|
|
1226
|
+
readonly description: string;
|
|
1227
|
+
/** Declared parameters. */
|
|
1228
|
+
readonly params: Readonly<Record<string, SkillParamDefinition>>;
|
|
1229
|
+
/** Returns SKILL.md rendered with the given parameters (defaults for the rest). Throws on invalid values. */
|
|
1230
|
+
render(params?: P): string;
|
|
1231
|
+
/** Every file of the skill folder, keyed by relative path, with SKILL.md rendered. Write them to e.g. .claude/skills/<name>/. */
|
|
1232
|
+
files(params?: P): Record<string, SkillFileContent>;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/** Every skill of the package, in folder order. */
|
|
1236
|
+
export declare const skills: readonly Skill[];
|
|
1237
|
+
|
|
1238
|
+
/** Finds a skill by folder name. Throws when it does not exist. */
|
|
1239
|
+
export declare function getSkill(name: SkillName): Skill;
|
|
1240
|
+
|
|
1241
|
+
/** Folder names of the skills in this package. */
|
|
1242
|
+
export type SkillName = ${skills.map((skill) => JSON.stringify(skill.name)).join(" | ") || "never"};
|
|
1243
|
+
`;
|
|
1244
|
+
const perSkill = skills.map((skill) => {
|
|
1245
|
+
const typeName = `${capitalize(skill.exportName)}Params`;
|
|
1246
|
+
return `
|
|
1247
|
+
${docComment([`Parameters of the \`${skill.name}\` skill.`])}export type ${typeName} = ${paramsType(skill.params)};
|
|
1248
|
+
|
|
1249
|
+
` + docComment([
|
|
1250
|
+
`${skill.title}: ${skill.description}`,
|
|
1251
|
+
"",
|
|
1252
|
+
`@returns The \`${skill.name}\` SKILL.md${Object.keys(skill.params).length ? ", rendered with the given parameters" : ""}.`
|
|
1253
|
+
]) + `export declare function ${skill.exportName}(params?: ${typeName}): string;
|
|
1254
|
+
`;
|
|
1255
|
+
});
|
|
1256
|
+
return header + perSkill.join("");
|
|
1257
|
+
}
|
|
1258
|
+
async function buildLibrary(library, options = {}) {
|
|
1259
|
+
if (!options.force) {
|
|
1260
|
+
const validation = await validateLibrary(library);
|
|
1261
|
+
if (validation.errors > 0) {
|
|
1262
|
+
throw new Error(
|
|
1263
|
+
`Validation found ${validation.errors} error(s); run 'skanvil validate' (or build with --force).`
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
const folders = [];
|
|
1268
|
+
for (const folder of await library.listSkillFolders()) {
|
|
1269
|
+
if (library.categories.fromFolder(folder).validated && await library.hasSkill(folder)) folders.push(folder);
|
|
1270
|
+
}
|
|
1271
|
+
const names = exportNames(library, folders);
|
|
1272
|
+
const skills = [];
|
|
1273
|
+
for (const folder of folders) {
|
|
1274
|
+
const dir = library.skillPath(folder);
|
|
1275
|
+
const params = await library.readSkillParams(folder) ?? {};
|
|
1276
|
+
const template = (await fs6.readFile(path8.join(dir, "SKILL.md"), "utf8")).replace(/\r\n/g, "\n");
|
|
1277
|
+
const rendered = await library.readSkillRendered(folder);
|
|
1278
|
+
const files = {};
|
|
1279
|
+
for (const file of await listFiles(dir)) {
|
|
1280
|
+
if (file === "SKILL.md" || file === PARAMS_FILE) continue;
|
|
1281
|
+
const buffer = await fs6.readFile(path8.join(dir, file));
|
|
1282
|
+
files[file] = isText(buffer) ? { encoding: "utf8", content: buffer.toString("utf8") } : { encoding: "base64", content: buffer.toString("base64") };
|
|
1283
|
+
}
|
|
1284
|
+
skills.push({
|
|
1285
|
+
name: folder,
|
|
1286
|
+
exportName: names.get(folder),
|
|
1287
|
+
category: library.categories.fromFolder(folder).id,
|
|
1288
|
+
title: rendered.title || folder,
|
|
1289
|
+
description: rendered.getString("description"),
|
|
1290
|
+
params,
|
|
1291
|
+
template,
|
|
1292
|
+
files
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
let packageName = library.config.name ?? path8.basename(library.root);
|
|
1296
|
+
try {
|
|
1297
|
+
packageName = JSON.parse(await fs6.readFile(path8.join(library.root, "package.json"), "utf8")).name ?? packageName;
|
|
1298
|
+
} catch {
|
|
1299
|
+
}
|
|
1300
|
+
const outDir = path8.resolve(library.root, options.outDir ?? "dist");
|
|
1301
|
+
await fs6.mkdir(outDir, { recursive: true });
|
|
1302
|
+
const runtime = runtimeSource(skills);
|
|
1303
|
+
const exportsList = skills.map((skill) => ` ${skill.exportName}`);
|
|
1304
|
+
const functions = skills.map((skill, index) => `const ${skill.exportName} = (params) => skills[${index}].render(params);`).join("\n");
|
|
1305
|
+
const banner = "// Generated by skanvil build. Do not edit.\n";
|
|
1306
|
+
const esm = `${banner}${runtime}
|
|
1307
|
+
${functions}
|
|
1308
|
+
|
|
1309
|
+
export {
|
|
1310
|
+
skills,
|
|
1311
|
+
getSkill${exportsList.length ? ",\n" + exportsList.join(",\n") : ""}
|
|
1312
|
+
};
|
|
1313
|
+
`;
|
|
1314
|
+
const cjs = `${banner}"use strict";
|
|
1315
|
+
${runtime}
|
|
1316
|
+
${functions}
|
|
1317
|
+
|
|
1318
|
+
module.exports = {
|
|
1319
|
+
skills,
|
|
1320
|
+
getSkill${exportsList.length ? ",\n" + exportsList.join(",\n") : ""}
|
|
1321
|
+
};
|
|
1322
|
+
`;
|
|
1323
|
+
const dts = declarationSource(skills, packageName);
|
|
1324
|
+
const written = {
|
|
1325
|
+
"index.mjs": esm,
|
|
1326
|
+
"index.cjs": cjs,
|
|
1327
|
+
"index.d.mts": dts,
|
|
1328
|
+
"index.d.cts": dts
|
|
1329
|
+
};
|
|
1330
|
+
for (const [file, content] of Object.entries(written)) {
|
|
1331
|
+
await fs6.writeFile(path8.join(outDir, file), content, "utf8");
|
|
1332
|
+
}
|
|
1333
|
+
return {
|
|
1334
|
+
outDir,
|
|
1335
|
+
files: Object.keys(written).map(
|
|
1336
|
+
(file) => path8.relative(library.root, path8.join(outDir, file)).split(path8.sep).join("/")
|
|
1337
|
+
),
|
|
1338
|
+
skills: skills.map((skill) => ({
|
|
1339
|
+
name: skill.name,
|
|
1340
|
+
exportName: skill.exportName,
|
|
1341
|
+
category: skill.category,
|
|
1342
|
+
params: Object.keys(skill.params)
|
|
1343
|
+
}))
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
|
|
858
1347
|
// src/core/readme-index.ts
|
|
859
1348
|
var SUMMARY_PATTERN = /<summary><strong>(.+?)<\/strong><\/summary>/;
|
|
860
1349
|
function folderPattern(skillsDir) {
|
|
@@ -1260,46 +1749,46 @@ var Categories = class {
|
|
|
1260
1749
|
};
|
|
1261
1750
|
|
|
1262
1751
|
// src/core/config.ts
|
|
1263
|
-
import
|
|
1264
|
-
import
|
|
1265
|
-
import { z } from "zod";
|
|
1752
|
+
import fs7 from "fs/promises";
|
|
1753
|
+
import path9 from "path";
|
|
1754
|
+
import { z as z2 } from "zod";
|
|
1266
1755
|
var CONFIG_FILE = ".skanvilrc.json";
|
|
1267
1756
|
var SEVERITIES = ["error", "warn", "off"];
|
|
1268
1757
|
var PREFIX_PATTERN = /^(|_|[a-z0-9]+(?:-[a-z0-9]+)*-)$/;
|
|
1269
|
-
var CategorySchema =
|
|
1270
|
-
id:
|
|
1271
|
-
prefix:
|
|
1758
|
+
var CategorySchema = z2.object({
|
|
1759
|
+
id: z2.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "category id must be kebab-case").describe("Category identifier used by `--type` (kebab-case)."),
|
|
1760
|
+
prefix: z2.string().regex(PREFIX_PATTERN, "prefix must be empty, '_' or kebab-case ending in '-'").optional().describe(
|
|
1272
1761
|
"Folder-name prefix of the category's skills, e.g. `front-` or `_`. Only the default category may be empty."
|
|
1273
1762
|
),
|
|
1274
|
-
title:
|
|
1275
|
-
aliases:
|
|
1276
|
-
default:
|
|
1763
|
+
title: z2.string().min(1).optional().describe("README section title. Defaults to the title-cased id."),
|
|
1764
|
+
aliases: z2.array(z2.string()).optional().describe("Alternative names accepted by `--type` (case-insensitive)."),
|
|
1765
|
+
default: z2.boolean().optional().describe(
|
|
1277
1766
|
"Marks the fallback category for folders without a known prefix. Exactly one category must be the default."
|
|
1278
1767
|
),
|
|
1279
|
-
indexed:
|
|
1280
|
-
validated:
|
|
1281
|
-
metadata:
|
|
1282
|
-
compatibility:
|
|
1768
|
+
indexed: z2.boolean().optional().describe("List the category's skills in the README index. Default: true."),
|
|
1769
|
+
validated: z2.boolean().optional().describe("Validate the category's skills against the spec and allow syncing them. Default: true."),
|
|
1770
|
+
metadata: z2.record(z2.string(), z2.string()).optional().describe("Key/value pairs written to the `metadata` frontmatter map of new or retyped skills."),
|
|
1771
|
+
compatibility: z2.string().max(500).optional().describe("Value written to the `compatibility` frontmatter field of new or retyped skills (max 500 chars).")
|
|
1283
1772
|
}).strict().describe("A skill category: folder prefix, README section and frontmatter defaults.");
|
|
1284
|
-
var ConfigSchema =
|
|
1285
|
-
$schema:
|
|
1286
|
-
name:
|
|
1287
|
-
scope:
|
|
1288
|
-
skillsDir:
|
|
1289
|
-
readme:
|
|
1290
|
-
categories:
|
|
1291
|
-
validation:
|
|
1292
|
-
requiredSections:
|
|
1293
|
-
placeholders:
|
|
1294
|
-
maxLines:
|
|
1295
|
-
rules:
|
|
1773
|
+
var ConfigSchema = z2.object({
|
|
1774
|
+
$schema: z2.string().optional().describe("JSON Schema reference for editor support."),
|
|
1775
|
+
name: z2.string().optional().describe("Library name (informational)."),
|
|
1776
|
+
scope: z2.string().optional().describe("npm scope without '@' (informational)."),
|
|
1777
|
+
skillsDir: z2.string().min(1).optional().describe("Folder holding the skills, relative to the library root. Default: skills."),
|
|
1778
|
+
readme: z2.string().min(1).optional().describe("README file holding the skills index. Default: README.md."),
|
|
1779
|
+
categories: z2.array(CategorySchema).min(1).optional().describe("Skill categories. Replaces the default preset (front, back, transversal, internal) when set."),
|
|
1780
|
+
validation: z2.object({
|
|
1781
|
+
requiredSections: z2.array(z2.string()).optional().describe("Level-2 headings (`## Name`) every SKILL.md must contain, compared case-insensitively."),
|
|
1782
|
+
placeholders: z2.array(z2.string()).optional().describe("Forbidden placeholder strings. ALL-CAPS entries (TODO) match case-sensitively as words."),
|
|
1783
|
+
maxLines: z2.number().int().positive().optional().describe("Line count reported by the `max-lines` rule. Default: 500."),
|
|
1784
|
+
rules: z2.record(z2.string(), z2.enum(SEVERITIES)).optional().describe('Per-rule severity overrides, e.g. { "max-lines": "off" }. See `skanvil validate --help`.')
|
|
1296
1785
|
}).strict().optional().describe("Validation settings."),
|
|
1297
|
-
templates:
|
|
1298
|
-
dir:
|
|
1786
|
+
templates: z2.object({
|
|
1787
|
+
dir: z2.string().min(1).optional().describe("Folder with custom templates, relative to the library root. Default: .skanvil/templates.")
|
|
1299
1788
|
}).strict().optional().describe("Custom template settings."),
|
|
1300
|
-
sync:
|
|
1301
|
-
targets:
|
|
1302
|
-
mode:
|
|
1789
|
+
sync: z2.object({
|
|
1790
|
+
targets: z2.array(z2.string()).optional().describe("Default `skanvil sync` targets: presets (claude, copilot, agents, codex) or paths."),
|
|
1791
|
+
mode: z2.enum(["copy", "link"]).optional().describe("Default sync mode. Default: copy.")
|
|
1303
1792
|
}).strict().optional().describe("Defaults for `skanvil sync`.")
|
|
1304
1793
|
}).strict().describe("skanvil library configuration.");
|
|
1305
1794
|
var DEFAULT_CATEGORIES = [
|
|
@@ -1434,7 +1923,7 @@ function resolveConfig(raw = {}) {
|
|
|
1434
1923
|
async function readRawConfig(root) {
|
|
1435
1924
|
let text;
|
|
1436
1925
|
try {
|
|
1437
|
-
text = await
|
|
1926
|
+
text = await fs7.readFile(path9.join(root, CONFIG_FILE), "utf8");
|
|
1438
1927
|
} catch {
|
|
1439
1928
|
return {};
|
|
1440
1929
|
}
|
|
@@ -1448,12 +1937,12 @@ async function loadConfig(root) {
|
|
|
1448
1937
|
return resolveConfig(await readRawConfig(root));
|
|
1449
1938
|
}
|
|
1450
1939
|
function configJsonSchema() {
|
|
1451
|
-
return
|
|
1940
|
+
return z2.toJSONSchema(ConfigSchema, { target: "draft-7" });
|
|
1452
1941
|
}
|
|
1453
1942
|
|
|
1454
1943
|
// src/core/library.ts
|
|
1455
|
-
import
|
|
1456
|
-
import
|
|
1944
|
+
import fs8 from "fs/promises";
|
|
1945
|
+
import path10 from "path";
|
|
1457
1946
|
var Library = class _Library {
|
|
1458
1947
|
/** Absolute library root. */
|
|
1459
1948
|
root;
|
|
@@ -1468,7 +1957,7 @@ var Library = class _Library {
|
|
|
1468
1957
|
* @param config - Resolved configuration. @defaultValue the default preset
|
|
1469
1958
|
*/
|
|
1470
1959
|
constructor(root, config = resolveConfig()) {
|
|
1471
|
-
this.root =
|
|
1960
|
+
this.root = path10.resolve(root);
|
|
1472
1961
|
this.config = config;
|
|
1473
1962
|
this.categories = new Categories(config.categories);
|
|
1474
1963
|
}
|
|
@@ -1480,7 +1969,7 @@ var Library = class _Library {
|
|
|
1480
1969
|
* @throws Error when the config is invalid or the skills folder is missing.
|
|
1481
1970
|
*/
|
|
1482
1971
|
static async open(root) {
|
|
1483
|
-
const library = new _Library(root, await loadConfig(
|
|
1972
|
+
const library = new _Library(root, await loadConfig(path10.resolve(root)));
|
|
1484
1973
|
if (!await exists(library.skillsDir)) {
|
|
1485
1974
|
throw new Error(
|
|
1486
1975
|
`No ${library.config.skillsDir}/ folder found in ${library.root}. Run 'skanvil init' first or use --cwd.`
|
|
@@ -1490,29 +1979,29 @@ var Library = class _Library {
|
|
|
1490
1979
|
}
|
|
1491
1980
|
/** Absolute path of the skills folder. */
|
|
1492
1981
|
get skillsDir() {
|
|
1493
|
-
return
|
|
1982
|
+
return path10.join(this.root, this.config.skillsDir);
|
|
1494
1983
|
}
|
|
1495
1984
|
/** Skills folder as written in README entries (posix separators), e.g. `skills`. */
|
|
1496
1985
|
get skillsDirRef() {
|
|
1497
|
-
return this.config.skillsDir.split(
|
|
1986
|
+
return this.config.skillsDir.split(path10.sep).join("/");
|
|
1498
1987
|
}
|
|
1499
1988
|
/** Absolute path of the README holding the index. */
|
|
1500
1989
|
get readmePath() {
|
|
1501
|
-
return
|
|
1990
|
+
return path10.join(this.root, this.config.readme);
|
|
1502
1991
|
}
|
|
1503
1992
|
/**
|
|
1504
1993
|
* @param name - Skill folder name.
|
|
1505
1994
|
* @returns Absolute path of the skill folder (it may not exist).
|
|
1506
1995
|
*/
|
|
1507
1996
|
skillPath(name) {
|
|
1508
|
-
return
|
|
1997
|
+
return path10.join(this.skillsDir, name);
|
|
1509
1998
|
}
|
|
1510
1999
|
/**
|
|
1511
2000
|
* @param name - Skill folder name.
|
|
1512
2001
|
* @returns Absolute path of the skill's `SKILL.md` (it may not exist).
|
|
1513
2002
|
*/
|
|
1514
2003
|
skillFilePath(name) {
|
|
1515
|
-
return
|
|
2004
|
+
return path10.join(this.skillPath(name), "SKILL.md");
|
|
1516
2005
|
}
|
|
1517
2006
|
/**
|
|
1518
2007
|
* Lists the skill folders. Hidden folders (starting with `.`) are ignored.
|
|
@@ -1520,7 +2009,7 @@ var Library = class _Library {
|
|
|
1520
2009
|
* @returns Folder names sorted with `localeCompare`.
|
|
1521
2010
|
*/
|
|
1522
2011
|
async listSkillFolders() {
|
|
1523
|
-
const entries = await
|
|
2012
|
+
const entries = await fs8.readdir(this.skillsDir, { withFileTypes: true });
|
|
1524
2013
|
return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
|
|
1525
2014
|
}
|
|
1526
2015
|
/**
|
|
@@ -1549,7 +2038,33 @@ var Library = class _Library {
|
|
|
1549
2038
|
* @throws The file system error when the file cannot be read.
|
|
1550
2039
|
*/
|
|
1551
2040
|
async readSkill(name) {
|
|
1552
|
-
return SkillFile.parse(await
|
|
2041
|
+
return SkillFile.parse(await fs8.readFile(this.skillFilePath(name), "utf8"));
|
|
2042
|
+
}
|
|
2043
|
+
/**
|
|
2044
|
+
* Reads a skill's parameter definitions (`params.json`).
|
|
2045
|
+
*
|
|
2046
|
+
* @param name - Skill folder name.
|
|
2047
|
+
* @returns The definitions, or `null` when the skill has none.
|
|
2048
|
+
* @throws Error when `params.json` exists but is invalid.
|
|
2049
|
+
*/
|
|
2050
|
+
async readSkillParams(name) {
|
|
2051
|
+
return readParams(this.skillPath(name));
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Reads a skill's `SKILL.md` rendered with parameter values. Skills without `params.json` are
|
|
2055
|
+
* returned as they are.
|
|
2056
|
+
*
|
|
2057
|
+
* @param name - Skill folder name.
|
|
2058
|
+
* @param values - Parameter values; missing ones take their defaults. @defaultValue `{}`
|
|
2059
|
+
* @returns The rendered file.
|
|
2060
|
+
* @throws Error when the file cannot be read, `params.json` is invalid, a value is invalid, or
|
|
2061
|
+
* the template is malformed.
|
|
2062
|
+
*/
|
|
2063
|
+
async readSkillRendered(name, values = {}) {
|
|
2064
|
+
const text = await fs8.readFile(this.skillFilePath(name), "utf8");
|
|
2065
|
+
const params = await this.readSkillParams(name);
|
|
2066
|
+
if (!params) return SkillFile.parse(text);
|
|
2067
|
+
return SkillFile.parse(renderTemplate(text, resolveParamValues(params, values, name)));
|
|
1553
2068
|
}
|
|
1554
2069
|
/**
|
|
1555
2070
|
* Writes a skill's `SKILL.md`, keeping the line ending it was read with.
|
|
@@ -1558,7 +2073,7 @@ var Library = class _Library {
|
|
|
1558
2073
|
* @param skill - File content to write.
|
|
1559
2074
|
*/
|
|
1560
2075
|
async writeSkill(name, skill) {
|
|
1561
|
-
await
|
|
2076
|
+
await fs8.writeFile(this.skillFilePath(name), skill.toString(), "utf8");
|
|
1562
2077
|
}
|
|
1563
2078
|
/**
|
|
1564
2079
|
* Reads the README.
|
|
@@ -1568,7 +2083,7 @@ var Library = class _Library {
|
|
|
1568
2083
|
*/
|
|
1569
2084
|
async readReadme() {
|
|
1570
2085
|
try {
|
|
1571
|
-
return splitEol(await
|
|
2086
|
+
return splitEol(await fs8.readFile(this.readmePath, "utf8"));
|
|
1572
2087
|
} catch {
|
|
1573
2088
|
throw new Error(`Cannot read ${this.config.readme} in ${this.root}.`);
|
|
1574
2089
|
}
|
|
@@ -1591,7 +2106,7 @@ var Library = class _Library {
|
|
|
1591
2106
|
* @param readme - Content and line ending, usually from {@link Library.readReadme}.
|
|
1592
2107
|
*/
|
|
1593
2108
|
async writeReadme(readme) {
|
|
1594
|
-
await
|
|
2109
|
+
await fs8.writeFile(this.readmePath, fromLf(readme.text, readme.eol), "utf8");
|
|
1595
2110
|
}
|
|
1596
2111
|
/**
|
|
1597
2112
|
* Lists the skills documented in each README section of the indexed categories.
|
|
@@ -1629,7 +2144,7 @@ var Library = class _Library {
|
|
|
1629
2144
|
let title = "";
|
|
1630
2145
|
let description = "";
|
|
1631
2146
|
try {
|
|
1632
|
-
const skill = await this.readSkill(name);
|
|
2147
|
+
const skill = await this.readSkillRendered(name).catch(() => this.readSkill(name));
|
|
1633
2148
|
title = skill.title;
|
|
1634
2149
|
description = skill.getString("description");
|
|
1635
2150
|
} catch {
|
|
@@ -1646,8 +2161,8 @@ var Library = class _Library {
|
|
|
1646
2161
|
|
|
1647
2162
|
// src/core/init.ts
|
|
1648
2163
|
import { spawnSync } from "child_process";
|
|
1649
|
-
import
|
|
1650
|
-
import
|
|
2164
|
+
import fs9 from "fs/promises";
|
|
2165
|
+
import path11 from "path";
|
|
1651
2166
|
var defaultCategories = new Categories(resolveConfig().categories);
|
|
1652
2167
|
function parseProfile(value) {
|
|
1653
2168
|
const indexed = defaultCategories.indexed.map((category) => category.id);
|
|
@@ -1668,11 +2183,11 @@ function parseProfile(value) {
|
|
|
1668
2183
|
return [...new Set(ids)];
|
|
1669
2184
|
}
|
|
1670
2185
|
async function createLibrary(target, options = {}) {
|
|
1671
|
-
target =
|
|
2186
|
+
target = path11.resolve(target);
|
|
1672
2187
|
if (!await isEmptyOrMissing(target)) {
|
|
1673
2188
|
throw new Error(`Target folder is not empty: ${target}`);
|
|
1674
2189
|
}
|
|
1675
|
-
const name = toKebabCase(options.name ||
|
|
2190
|
+
const name = toKebabCase(options.name || path11.basename(target));
|
|
1676
2191
|
if (!name) {
|
|
1677
2192
|
throw new Error("Library name is required.");
|
|
1678
2193
|
}
|
|
@@ -1700,7 +2215,8 @@ async function createLibrary(target, options = {}) {
|
|
|
1700
2215
|
packageName: scope ? `@${scope}/${name}` : name,
|
|
1701
2216
|
title,
|
|
1702
2217
|
description,
|
|
1703
|
-
sections: buildSections(sectionTitles)
|
|
2218
|
+
sections: buildSections(sectionTitles),
|
|
2219
|
+
skanvilRange: `^${packageVersion()}`
|
|
1704
2220
|
}
|
|
1705
2221
|
});
|
|
1706
2222
|
const rc = {
|
|
@@ -1718,7 +2234,7 @@ async function createLibrary(target, options = {}) {
|
|
|
1718
2234
|
sync: { targets: syncTargets, mode: "copy" }
|
|
1719
2235
|
};
|
|
1720
2236
|
resolveConfig(rc);
|
|
1721
|
-
await
|
|
2237
|
+
await fs9.writeFile(path11.join(target, CONFIG_FILE), `${JSON.stringify(rc, null, 2)}
|
|
1722
2238
|
`, "utf8");
|
|
1723
2239
|
if (options.ejectTemplates) {
|
|
1724
2240
|
await ejectSkillTemplate(new Library(target, resolveConfig(rc)));
|
|
@@ -1733,7 +2249,7 @@ async function createLibrary(target, options = {}) {
|
|
|
1733
2249
|
}
|
|
1734
2250
|
|
|
1735
2251
|
// src/commands/shared.ts
|
|
1736
|
-
import
|
|
2252
|
+
import path12 from "path";
|
|
1737
2253
|
|
|
1738
2254
|
// src/util/output.ts
|
|
1739
2255
|
import pc from "picocolors";
|
|
@@ -1776,7 +2292,7 @@ var Output = class {
|
|
|
1776
2292
|
|
|
1777
2293
|
// src/commands/shared.ts
|
|
1778
2294
|
function resolveRoot(options) {
|
|
1779
|
-
return
|
|
2295
|
+
return path12.resolve(options.cwd || process.cwd());
|
|
1780
2296
|
}
|
|
1781
2297
|
function openLibrary(options) {
|
|
1782
2298
|
return Library.open(resolveRoot(options));
|
|
@@ -1805,7 +2321,7 @@ async function validateAfter(library, label, skip) {
|
|
|
1805
2321
|
}
|
|
1806
2322
|
|
|
1807
2323
|
// src/core/skills.ts
|
|
1808
|
-
import
|
|
2324
|
+
import fs10 from "fs/promises";
|
|
1809
2325
|
async function toLibrary(ref) {
|
|
1810
2326
|
return typeof ref === "string" ? Library.open(ref) : ref;
|
|
1811
2327
|
}
|
|
@@ -1875,7 +2391,7 @@ async function scaffoldSkill(ref, options) {
|
|
|
1875
2391
|
await library.writeReadme(readme);
|
|
1876
2392
|
}
|
|
1877
2393
|
} catch (error) {
|
|
1878
|
-
await
|
|
2394
|
+
await fs10.rm(target, { recursive: true, force: true });
|
|
1879
2395
|
throw error;
|
|
1880
2396
|
}
|
|
1881
2397
|
return { skillName, title, type: category.id, target, template };
|
|
@@ -1895,7 +2411,7 @@ async function editSkill(ref, skillName, options) {
|
|
|
1895
2411
|
throw new Error(`Skill folder already exists: ${library.skillPath(newName)}`);
|
|
1896
2412
|
}
|
|
1897
2413
|
const skillFile = library.skillFilePath(skillName);
|
|
1898
|
-
const originalSkill = await
|
|
2414
|
+
const originalSkill = await fs10.readFile(skillFile, "utf8");
|
|
1899
2415
|
const skill = await library.readSkill(skillName);
|
|
1900
2416
|
skill.set(["name"], newName);
|
|
1901
2417
|
if (description) skill.set(["description"], description);
|
|
@@ -1921,13 +2437,13 @@ async function editSkill(ref, skillName, options) {
|
|
|
1921
2437
|
readme.text = insertEntry(removeEntry(readme.text, skillName, dir), newCategory.title, entry);
|
|
1922
2438
|
}
|
|
1923
2439
|
try {
|
|
1924
|
-
await
|
|
2440
|
+
await fs10.writeFile(skillFile, skill.toString(), "utf8");
|
|
1925
2441
|
await library.writeReadme(readme);
|
|
1926
2442
|
if (newName !== skillName) {
|
|
1927
|
-
await
|
|
2443
|
+
await fs10.rename(library.skillPath(skillName), library.skillPath(newName));
|
|
1928
2444
|
}
|
|
1929
2445
|
} catch (error) {
|
|
1930
|
-
await
|
|
2446
|
+
await fs10.writeFile(skillFile, originalSkill, "utf8").catch(() => void 0);
|
|
1931
2447
|
await library.writeReadme(originalReadme).catch(() => void 0);
|
|
1932
2448
|
throw new Error(`Edit failed and was rolled back: ${error.message}`);
|
|
1933
2449
|
}
|
|
@@ -1937,7 +2453,7 @@ async function removeSkill(ref, skillName) {
|
|
|
1937
2453
|
const library = await toLibrary(ref);
|
|
1938
2454
|
await library.requireSkill(skillName);
|
|
1939
2455
|
const readme = await library.readReadmeOrEmpty();
|
|
1940
|
-
await
|
|
2456
|
+
await fs10.rm(library.skillPath(skillName), { recursive: true, force: true });
|
|
1941
2457
|
if (readme.text) {
|
|
1942
2458
|
readme.text = removeEntry(readme.text, skillName, library.skillsDirRef);
|
|
1943
2459
|
await library.writeReadme(readme);
|
|
@@ -2068,6 +2584,13 @@ export {
|
|
|
2068
2584
|
packageVersion,
|
|
2069
2585
|
displayPath,
|
|
2070
2586
|
SkillFile,
|
|
2587
|
+
PARAMS_FILE,
|
|
2588
|
+
parseParams,
|
|
2589
|
+
readParams,
|
|
2590
|
+
resolveParamValues,
|
|
2591
|
+
renderTemplate,
|
|
2592
|
+
findUndeclaredParams,
|
|
2593
|
+
parseParamPairs,
|
|
2071
2594
|
toKebabCase,
|
|
2072
2595
|
toTitleCase,
|
|
2073
2596
|
resolveSkillTemplate,
|
|
@@ -2084,6 +2607,8 @@ export {
|
|
|
2084
2607
|
AGENT_SKILL_NAME,
|
|
2085
2608
|
agentSkillSource,
|
|
2086
2609
|
installAgentSkill,
|
|
2610
|
+
exportNames,
|
|
2611
|
+
buildLibrary,
|
|
2087
2612
|
rebuildIndex,
|
|
2088
2613
|
reconcileIndex,
|
|
2089
2614
|
doctor,
|