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/dist/cli.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  RULES,
6
6
  TARGET_PRESETS,
7
7
  addCommand,
8
+ buildLibrary,
8
9
  collectSkills,
9
10
  createLibrary,
10
11
  displayPath,
@@ -20,6 +21,7 @@ import {
20
21
  listCommand,
21
22
  openLibrary,
22
23
  packageVersion,
24
+ parseParamPairs,
23
25
  printValidation,
24
26
  rebuildIndex,
25
27
  removeSkill,
@@ -31,7 +33,7 @@ import {
31
33
  toKebabCase,
32
34
  toTitleCase,
33
35
  validateLibrary
34
- } from "./chunk-TYO4LLN5.js";
36
+ } from "./chunk-WXEWECJP.js";
35
37
 
36
38
  // src/cli.ts
37
39
  import { Command, Option } from "commander";
@@ -170,7 +172,8 @@ async function syncCommand(dest, options) {
170
172
  force: options.force,
171
173
  categories: splitList(options.category),
172
174
  skills: splitList(options.skill),
173
- scope: options.global ? "user" : "project"
175
+ scope: options.global ? "user" : "project",
176
+ params: options.param?.length ? parseParamPairs(options.param) : void 0
174
177
  });
175
178
  if (options.json) {
176
179
  console.log(JSON.stringify({ dryRun: Boolean(options.dryRun), mode, results }, null, 2));
@@ -188,6 +191,21 @@ async function syncCommand(dest, options) {
188
191
  }
189
192
  return 0;
190
193
  }
194
+ async function buildCommand(options) {
195
+ const library = await openLibrary(options);
196
+ const out = new Output("build", options.json);
197
+ const result = await buildLibrary(library, { outDir: options.out, force: options.force });
198
+ if (options.json) {
199
+ console.log(JSON.stringify(result, null, 2));
200
+ return 0;
201
+ }
202
+ out.success(`Built ${result.skills.length} skill(s) into ${displayPath(result.outDir)}`);
203
+ for (const skill of result.skills) {
204
+ const params = skill.params.length ? ` (params: ${skill.params.join(", ")})` : "";
205
+ out.print(` ${skill.exportName} ${skill.name}${params}`);
206
+ }
207
+ return 0;
208
+ }
191
209
 
192
210
  // src/commands/wizard.ts
193
211
  import fs from "fs/promises";
@@ -747,6 +765,7 @@ ${Object.entries(RULES).map(([id, rule]) => ` ${id.padEnd(22)} ${rule.severity.
747
765
  program2.command("doctor").description("diagnose the library and optionally repair fixable problems").option("--fix", "apply automatic fixes (README entries, name mismatch, tabs, tools -> allowed-tools)").addOption(cwdOption()).addOption(jsonOption()).action(run((options) => doctorCommand(options)));
748
766
  const templates = program2.command("templates").description("manage skill templates");
749
767
  templates.command("eject").description("copy the built-in skill template into the library for customisation").option("--category <id>", "eject as a category-specific template (skill-<id>)").option("--force", "overwrite an existing template folder").addOption(cwdOption()).addOption(jsonOption()).action(run((options) => ejectCommand(options)));
768
+ program2.command("build").description("generate the library's npm exports: one typed function per skill (dist/index.mjs, index.cjs, .d.ts)").option("--out <dir>", "output folder relative to the library root (default: dist)").option("--force", "build even when validation reports errors").addOption(cwdOption()).addOption(jsonOption()).action(run((options) => buildCommand(options)));
750
769
  program2.command("install-skill [dest]").description("install or update the skanvil agent skill so coding agents in a project know how to use skanvil").option(
751
770
  "--target <name>",
752
771
  `preset (${Object.keys(TARGET_PRESETS).join(", ")}) or path relative to dest; repeatable or comma-separated (default: claude)`,
@@ -756,7 +775,7 @@ ${Object.entries(RULES).map(([id, rule]) => ` ${id.padEnd(22)} ${rule.severity.
756
775
  "--target <name>",
757
776
  `preset (${Object.keys(TARGET_PRESETS).join(", ")}) or path relative to dest; repeatable or comma-separated`,
758
777
  collect
759
- ).option("--from <pkg|path>", "skills library to install (npm package in node_modules or folder); default: --cwd").option("--link", "symlink skills instead of copying (junctions on Windows)").option("--copy", "copy skills (overrides sync.mode = link)").option("--category <id>", "only skills of this category (repeatable)", collect).option("--skill <name>", "only this skill (repeatable)", collect).option("--force", "also sync skills with validation errors").option("--dry-run", "show what would change without writing").option("--global", "install into your user folders (~/.claude/skills, ~/.copilot/skills, ~/.agents/skills)").addOption(cwdOption()).addOption(jsonOption()).action(run((dest, options) => syncCommand(dest, options)));
778
+ ).option("--from <pkg|path>", "skills library to install (npm package in node_modules or folder); default: --cwd").option("--link", "symlink skills instead of copying (junctions on Windows)").option("--copy", "copy skills (overrides sync.mode = link)").option("--category <id>", "only skills of this category (repeatable)", collect).option("--skill <name>", "only this skill (repeatable)", collect).option("--force", "also sync skills with validation errors").option("--dry-run", "show what would change without writing").option("--param <name=value>", "value for parametrized skills (repeatable); remembered per target", collect).option("--global", "install into your user folders (~/.claude/skills, ~/.copilot/skills, ~/.agents/skills)").addOption(cwdOption()).addOption(jsonOption()).action(run((dest, options) => syncCommand(dest, options)));
760
779
  return program2;
761
780
  }
762
781
  var argv = process.argv.slice(2);
package/dist/index.d.ts CHANGED
@@ -270,6 +270,104 @@ declare class Categories {
270
270
  buildTitle(type: unknown, rawName: unknown, explicitTitle?: unknown): string;
271
271
  }
272
272
 
273
+ /** File that declares a skill's parameters, next to its `SKILL.md`. Not part of the Agent Skills spec. */
274
+ declare const PARAMS_FILE = "params.json";
275
+ declare const ParamSchema: z.ZodUnion<readonly [z.ZodObject<{
276
+ type: z.ZodLiteral<"enum">;
277
+ values: z.ZodArray<z.ZodString>;
278
+ default: z.ZodString;
279
+ description: z.ZodOptional<z.ZodString>;
280
+ }, z.core.$strict>, z.ZodObject<{
281
+ type: z.ZodLiteral<"string">;
282
+ default: z.ZodString;
283
+ description: z.ZodOptional<z.ZodString>;
284
+ }, z.core.$strict>, z.ZodObject<{
285
+ type: z.ZodLiteral<"boolean">;
286
+ default: z.ZodBoolean;
287
+ description: z.ZodOptional<z.ZodString>;
288
+ }, z.core.$strict>]>;
289
+ /** One parameter definition. Every parameter has a default, so skills always render. */
290
+ type ParamDefinition = z.infer<typeof ParamSchema>;
291
+ /** Parameter definitions of a skill, by name. */
292
+ type ParamDefinitions = Record<string, ParamDefinition>;
293
+ /** Parameter values, as passed by users (`--param key=value` gives strings). */
294
+ type ParamValues = Record<string, string | boolean>;
295
+ /**
296
+ * Parses and checks the content of a `params.json`.
297
+ *
298
+ * @param text - File content.
299
+ * @returns The definitions.
300
+ * @throws Error describing the first problems: invalid JSON, unknown fields, missing defaults,
301
+ * or an enum default that is not one of its values.
302
+ */
303
+ declare function parseParams(text: string): ParamDefinitions;
304
+ /**
305
+ * Reads a skill folder's `params.json`.
306
+ *
307
+ * @param skillDir - Absolute skill folder.
308
+ * @returns The definitions, or `null` when the skill has no `params.json`.
309
+ * @throws Error when the file exists but is invalid (see {@link parseParams}).
310
+ */
311
+ declare function readParams(skillDir: string): Promise<ParamDefinitions | null>;
312
+ /**
313
+ * Checks values against definitions and fills in defaults.
314
+ *
315
+ * Strings `"true"` / `"false"` are accepted for boolean parameters, so CLI values work.
316
+ *
317
+ * @param definitions - The skill's parameters.
318
+ * @param given - Values to apply. @defaultValue `{}`
319
+ * @param skill - Skill name used in error messages.
320
+ * @param ignoreUnknown - Ignore values for parameters this skill does not declare (used when one
321
+ * set of values is applied to many skills). @defaultValue `false`
322
+ * @returns A value for every declared parameter.
323
+ * @throws Error for an unknown parameter (unless ignored), an enum value outside its list, or a
324
+ * value of the wrong type.
325
+ *
326
+ * @remarks Self-contained on purpose: `skanvil build` embeds this function's source.
327
+ */
328
+ declare function resolveParamValues(definitions: ParamDefinitions, given?: Record<string, unknown>, skill?: string, ignoreUnknown?: boolean): ParamValues;
329
+ /**
330
+ * Renders a skill template.
331
+ *
332
+ * Syntax:
333
+ * - `{{name}}` is replaced by the value of a declared parameter. Names that are not parameters are
334
+ * left untouched, so literal `{{…}}` text in a skill without parameters is safe.
335
+ * - `{{#if name}}`, `{{#if !name}}`, `{{#if name == "value"}}` and `{{#if name != "value"}}`
336
+ * (single or double quotes) start a block, with an optional `{{else}}`, closed by `{{/if}}`.
337
+ * Blocks nest. A tag alone on its line removes the whole line, so blocks leave no blank lines.
338
+ *
339
+ * @param text - Template (LF or CRLF).
340
+ * @param values - Values for every parameter, normally from {@link resolveParamValues}.
341
+ * @returns The rendered text.
342
+ * @throws Error for an `{{#if}}` on an unknown parameter, a malformed condition, or unbalanced
343
+ * `{{#if}}` / `{{else}}` / `{{/if}}` tags.
344
+ *
345
+ * @example
346
+ * ```ts
347
+ * renderTemplate('Use {{tracker}}.\n{{#if tracker == "jira"}}\nAdd the key.\n{{/if}}\n', { tracker: "jira" });
348
+ * // "Use jira.\nAdd the key.\n"
349
+ * ```
350
+ *
351
+ * @remarks Self-contained on purpose: `skanvil build` embeds this function's source.
352
+ */
353
+ declare function renderTemplate(text: string, values: Record<string, string | boolean>): string;
354
+ /**
355
+ * Lists the `{{name}}` placeholders of a template that are not declared parameters.
356
+ *
357
+ * @param text - Template.
358
+ * @param definitions - Declared parameters.
359
+ * @returns Unique undeclared names, in order of appearance.
360
+ */
361
+ declare function findUndeclaredParams(text: string, definitions: ParamDefinitions): string[];
362
+ /**
363
+ * Parses `key=value` pairs (from repeated `--param` options).
364
+ *
365
+ * @param pairs - Entries such as `ticketTracker=jira`; values may contain `=`.
366
+ * @returns The values (strings; booleans are converted by {@link resolveParamValues}).
367
+ * @throws Error for an entry without `=` or with an invalid name.
368
+ */
369
+ declare function parseParamPairs(pairs: string[]): Record<string, string>;
370
+
273
371
  /**
274
372
  * Reading and editing `SKILL.md` files.
275
373
  *
@@ -471,6 +569,25 @@ declare class Library {
471
569
  * @throws The file system error when the file cannot be read.
472
570
  */
473
571
  readSkill(name: string): Promise<SkillFile>;
572
+ /**
573
+ * Reads a skill's parameter definitions (`params.json`).
574
+ *
575
+ * @param name - Skill folder name.
576
+ * @returns The definitions, or `null` when the skill has none.
577
+ * @throws Error when `params.json` exists but is invalid.
578
+ */
579
+ readSkillParams(name: string): Promise<ParamDefinitions | null>;
580
+ /**
581
+ * Reads a skill's `SKILL.md` rendered with parameter values. Skills without `params.json` are
582
+ * returned as they are.
583
+ *
584
+ * @param name - Skill folder name.
585
+ * @param values - Parameter values; missing ones take their defaults. @defaultValue `{}`
586
+ * @returns The rendered file.
587
+ * @throws Error when the file cannot be read, `params.json` is invalid, a value is invalid, or
588
+ * the template is malformed.
589
+ */
590
+ readSkillRendered(name: string, values?: Record<string, unknown>): Promise<SkillFile>;
474
591
  /**
475
592
  * Writes a skill's `SKILL.md`, keeping the line ending it was read with.
476
593
  *
@@ -567,6 +684,12 @@ interface SyncOptions {
567
684
  scope?: TargetScope;
568
685
  /** Home folder for the `user` scope. @defaultValue `os.homedir()` */
569
686
  homeDir?: string;
687
+ /**
688
+ * Values for parametrized skills (`params.json`), applied to every skill that declares them.
689
+ * When omitted, each target reuses the values of its previous sync, then the defaults.
690
+ * Every key must be declared by at least one synced skill.
691
+ */
692
+ params?: Record<string, string | boolean>;
570
693
  }
571
694
  /** Outcome of {@link syncSkills} for one target. */
572
695
  interface TargetResult {
@@ -589,6 +712,8 @@ interface TargetResult {
589
712
  }[];
590
713
  /** Non-fatal problems, such as a link that fell back to a copy. */
591
714
  warnings: string[];
715
+ /** Parameter values used for this target (empty when no synced skill has parameters). */
716
+ params: Record<string, string | boolean>;
592
717
  }
593
718
  /**
594
719
  * Resolves a sync target to an absolute folder.
@@ -689,6 +814,60 @@ interface AgentSkillResult {
689
814
  */
690
815
  declare function installAgentSkill(options: InstallAgentSkillOptions): Promise<AgentSkillResult[]>;
691
816
 
817
+ /** Options of {@link buildLibrary}. */
818
+ interface BuildOptions {
819
+ /** Output folder, relative to the library root. @defaultValue `"dist"` */
820
+ outDir?: string;
821
+ /** Build even when validation reports errors. @defaultValue `false` */
822
+ force?: boolean;
823
+ }
824
+ /** One exported skill. */
825
+ interface BuiltSkill {
826
+ /** Skill folder name. */
827
+ name: string;
828
+ /** Name of the exported function. */
829
+ exportName: string;
830
+ /** Category id. */
831
+ category: string;
832
+ /** Declared parameter names. */
833
+ params: string[];
834
+ }
835
+ /** Result of {@link buildLibrary}. */
836
+ interface BuildResult {
837
+ /** Absolute output folder. */
838
+ outDir: string;
839
+ /** Generated files, relative to the library root (posix). */
840
+ files: string[];
841
+ /** Exported skills, in folder order. */
842
+ skills: BuiltSkill[];
843
+ }
844
+ /**
845
+ * Chooses export names: the folder name without its category prefix, in camelCase
846
+ * (`front-api-review` becomes `apiReview`). Names that would collide fall back to the full folder
847
+ * name (`frontReview`, `backReview`), and invalid or reserved identifiers get a `skill` prefix.
848
+ *
849
+ * @param library - Library whose categories define the prefixes.
850
+ * @param names - Skill folder names.
851
+ * @returns Export name by folder name.
852
+ */
853
+ declare function exportNames(library: Library, names: string[]): Map<string, string>;
854
+ /**
855
+ * Generates `index.mjs`, `index.cjs`, `index.d.mts` and `index.d.cts` for the library's
856
+ * publishable skills (categories with `validated: true`).
857
+ *
858
+ * @param library - Library to build.
859
+ * @param options - Output folder and error handling.
860
+ * @returns What was generated.
861
+ * @throws Error when validation reports errors (unless `force`), or a skill's parameters are invalid.
862
+ *
863
+ * @example
864
+ * ```ts
865
+ * const { skills } = await buildLibrary(await Library.open("."));
866
+ * // then, in a consumer: import { apiReview } from "@acme/my-skills";
867
+ * ```
868
+ */
869
+ declare function buildLibrary(library: Library, options?: BuildOptions): Promise<BuildResult>;
870
+
692
871
  /** Severity of a reported diagnostic (rules set to `off` report nothing). */
693
872
  type DiagnosticSeverity = Exclude<Severity, "off">;
694
873
  /** A single validation finding. */
@@ -1079,4 +1258,4 @@ interface ListedSkill {
1079
1258
  */
1080
1259
  declare function collectSkills(library: Library, category?: string): Promise<ListedSkill[]>;
1081
1260
 
1082
- export { AGENT_SKILL_NAME, type AddSkillOptions, type AgentSkillResult, CONFIG_FILE, Categories, type Category, ConfigSchema, type CreateLibraryOptions, DEFAULT_CATEGORIES, type Diagnostic, type DoctorResult, type EditSkillOptions, type InstallAgentSkillOptions, Library, type ListedSkill, MANIFEST_FILE, RULES, type RawConfig, type ResolveTargetOptions, type ResolvedConfig, type Severity, SkillFile, type SkillInfo, type SyncOptions, TARGET_PRESETS, type TargetResult, type TargetScope, USER_TARGET_PRESETS, type ValidationResult, agentSkillSource, collectSkills, configJsonSchema, createLibrary, doctor, editSkill, ejectSkillTemplate, formatDiagnostic, installAgentSkill, loadConfig, parseProfile, rebuildIndex, reconcileIndex, removeSkill, resolveConfig, resolveSkillTemplate, resolveTarget, scaffoldSkill, syncSkills, validateLibrary };
1261
+ export { AGENT_SKILL_NAME, type AddSkillOptions, type AgentSkillResult, type BuildOptions, type BuildResult, type BuiltSkill, CONFIG_FILE, Categories, type Category, ConfigSchema, type CreateLibraryOptions, DEFAULT_CATEGORIES, type Diagnostic, type DoctorResult, type EditSkillOptions, type InstallAgentSkillOptions, Library, type ListedSkill, MANIFEST_FILE, PARAMS_FILE, type ParamDefinition, type ParamDefinitions, type ParamValues, RULES, type RawConfig, type ResolveTargetOptions, type ResolvedConfig, type Severity, SkillFile, type SkillInfo, type SyncOptions, TARGET_PRESETS, type TargetResult, type TargetScope, USER_TARGET_PRESETS, type ValidationResult, agentSkillSource, buildLibrary, collectSkills, configJsonSchema, createLibrary, doctor, editSkill, ejectSkillTemplate, exportNames, findUndeclaredParams, formatDiagnostic, installAgentSkill, loadConfig, parseParamPairs, parseParams, parseProfile, readParams, rebuildIndex, reconcileIndex, removeSkill, renderTemplate, resolveConfig, resolveParamValues, resolveSkillTemplate, resolveTarget, scaffoldSkill, syncSkills, validateLibrary };
package/dist/index.js CHANGED
@@ -6,31 +6,40 @@ import {
6
6
  DEFAULT_CATEGORIES,
7
7
  Library,
8
8
  MANIFEST_FILE,
9
+ PARAMS_FILE,
9
10
  RULES,
10
11
  SkillFile,
11
12
  TARGET_PRESETS,
12
13
  USER_TARGET_PRESETS,
13
14
  agentSkillSource,
15
+ buildLibrary,
14
16
  collectSkills,
15
17
  configJsonSchema,
16
18
  createLibrary,
17
19
  doctor,
18
20
  editSkill,
19
21
  ejectSkillTemplate,
22
+ exportNames,
23
+ findUndeclaredParams,
20
24
  formatDiagnostic,
21
25
  installAgentSkill,
22
26
  loadConfig,
27
+ parseParamPairs,
28
+ parseParams,
23
29
  parseProfile,
30
+ readParams,
24
31
  rebuildIndex,
25
32
  reconcileIndex,
26
33
  removeSkill,
34
+ renderTemplate,
27
35
  resolveConfig,
36
+ resolveParamValues,
28
37
  resolveSkillTemplate,
29
38
  resolveTarget,
30
39
  scaffoldSkill,
31
40
  syncSkills,
32
41
  validateLibrary
33
- } from "./chunk-TYO4LLN5.js";
42
+ } from "./chunk-WXEWECJP.js";
34
43
  export {
35
44
  AGENT_SKILL_NAME,
36
45
  CONFIG_FILE,
@@ -39,25 +48,34 @@ export {
39
48
  DEFAULT_CATEGORIES,
40
49
  Library,
41
50
  MANIFEST_FILE,
51
+ PARAMS_FILE,
42
52
  RULES,
43
53
  SkillFile,
44
54
  TARGET_PRESETS,
45
55
  USER_TARGET_PRESETS,
46
56
  agentSkillSource,
57
+ buildLibrary,
47
58
  collectSkills,
48
59
  configJsonSchema,
49
60
  createLibrary,
50
61
  doctor,
51
62
  editSkill,
52
63
  ejectSkillTemplate,
64
+ exportNames,
65
+ findUndeclaredParams,
53
66
  formatDiagnostic,
54
67
  installAgentSkill,
55
68
  loadConfig,
69
+ parseParamPairs,
70
+ parseParams,
56
71
  parseProfile,
72
+ readParams,
57
73
  rebuildIndex,
58
74
  reconcileIndex,
59
75
  removeSkill,
76
+ renderTemplate,
60
77
  resolveConfig,
78
+ resolveParamValues,
61
79
  resolveSkillTemplate,
62
80
  resolveTarget,
63
81
  scaffoldSkill,
package/docs/commands.md CHANGED
@@ -176,17 +176,18 @@ Copy the built-in skill template into the library so you can edit it.
176
176
 
177
177
  Install the library's skills into a project. `dest` defaults to the current directory. See [Sync](sync.md).
178
178
 
179
- | Option | Description |
180
- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
181
- | `--target <name>` | `claude`, `copilot`, `agents`, `codex`, or a path relative to `dest`. Repeatable and comma-separated. Default: `sync.targets` from the config, or `claude`. |
182
- | `--from <pkg\|path>` | Source library: a folder, or an npm package installed in a `node_modules` above `dest`. Default: `--cwd`. |
183
- | `--link` | Symlink instead of copying (junctions on Windows). |
184
- | `--copy` | Copy, even if the config sets `sync.mode: "link"`. |
185
- | `--category <id>` | Only sync this category. Repeatable. Disables pruning. |
186
- | `--skill <name>` | Only sync this skill. Repeatable. Disables pruning. |
187
- | `--force` | Also sync skills that have validation errors. |
188
- | `--dry-run` | Show what would change without writing. |
189
- | `--global` | Install into your user folders instead of a project (see below). Do not pass `dest` with it. |
179
+ | Option | Description |
180
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
181
+ | `--target <name>` | `claude`, `copilot`, `agents`, `codex`, or a path relative to `dest`. Repeatable and comma-separated. Default: `sync.targets` from the config, or `claude`. |
182
+ | `--from <pkg\|path>` | Source library: a folder, or an npm package installed in a `node_modules` above `dest`. Default: `--cwd`. |
183
+ | `--link` | Symlink instead of copying (junctions on Windows). |
184
+ | `--copy` | Copy, even if the config sets `sync.mode: "link"`. |
185
+ | `--category <id>` | Only sync this category. Repeatable. Disables pruning. |
186
+ | `--skill <name>` | Only sync this skill. Repeatable. Disables pruning. |
187
+ | `--force` | Also sync skills that have validation errors. |
188
+ | `--dry-run` | Show what would change without writing. |
189
+ | `--param <name=value>` | Value for [parametrized skills](params-and-exports.md). Repeatable. It applies to every synced skill that declares it, and an unknown name is an error. Remembered per target. |
190
+ | `--global` | Install into your user folders instead of a project (see below). Do not pass `dest` with it. |
190
191
 
191
192
  Custom `--target` paths must resolve to a folder inside `dest`: `../shared` or `.` are rejected before anything is written. With `--global`, only presets are accepted:
192
193
 
@@ -200,6 +201,17 @@ skanvil sync --from @acme/my-skills --target agents --link
200
201
  skanvil sync -C ./my-skills --global --target claude
201
202
  ```
202
203
 
204
+ ## `build`
205
+
206
+ Generate the library's npm exports in `dist/`: `index.mjs`, `index.cjs` and their type declarations. They contain one function per publishable skill that returns its `SKILL.md` (rendered with parameters), a `skills` list and `getSkill(name)`. See [Parameters and exports](params-and-exports.md).
207
+
208
+ | Option | Description |
209
+ | ------------- | ------------------------------------------------------------ |
210
+ | `--out <dir>` | Output folder relative to the library root. Default: `dist`. |
211
+ | `--force` | Build even when validation reports errors. |
212
+
213
+ `--json` prints `{ outDir, files, skills: [{ name, exportName, category, params }] }`.
214
+
203
215
  ## `install-skill [dest]`
204
216
 
205
217
  Install or update the `skanvil` agent skill in a project, so coding agents there know how to use skanvil: which commands to run, the golden rules, and how to fix each diagnostic. `dest` defaults to the current directory.
@@ -4,6 +4,52 @@ This guide creates a skills library, adds and edits skills, checks them in CI, p
4
4
 
5
5
  > **Prefer menus?** Run `npx skanvil` without arguments in a terminal. The [wizard](commands.md#wizard) walks through every step below. After each action it prints the equivalent command, which is the one this guide uses.
6
6
 
7
+ ## 0. Let your AI agent learn skanvil (optional)
8
+
9
+ skanvil ships an agent skill that teaches coding agents (Claude Code, GitHub Copilot, Codex…) how to use it:
10
+
11
+ - the commands and the rules (never rename folders or edit the README index by hand);
12
+ - how to fix every validation error;
13
+ - how to write good skill content.
14
+
15
+ Paste this prompt into your agent, inside the repository where you want to work. That can be an existing skills library, an application that consumes one, or an empty folder for a new library.
16
+
17
+ ```text
18
+ Install and learn the skanvil agent skill in this repository.
19
+
20
+ 1. Work out which skills folder you read: Claude Code uses .claude/skills, GitHub Copilot
21
+ .github/skills, Codex .agents/skills. Then run the matching command, for example:
22
+ npx skanvil@latest install-skill --target claude
23
+ Use --target copilot or --target agents (comma-separated for several) if that fits you
24
+ better. Do not use --global unless I ask for it.
25
+ 2. Read the installed <skills folder>/skanvil/SKILL.md completely (for example
26
+ .claude/skills/skanvil/SKILL.md), and then its references/REFERENCE.md.
27
+ 3. Detect the context:
28
+ - if there is a .skanvilrc.json, run `npx skanvil list --json` and `npx skanvil validate --json`;
29
+ - otherwise, tell me this is not a skills library yet.
30
+ 4. Reply with:
31
+ - a short summary of how you will use skanvil here;
32
+ - the golden rules you will follow;
33
+ - what the library contains and any validation problems you found.
34
+ Do not change any skill until I ask.
35
+
36
+ From now on, whenever I ask you to create, rename, move, delete, validate, parametrize, build or
37
+ install skills, use the skanvil skill and its commands. Do not edit skill folders or the README
38
+ index by hand, always pass every option (the commands are interactive otherwise), and never run
39
+ `skanvil` without arguments, because that opens a menu meant for humans.
40
+ ```
41
+
42
+ After that, you can ask in plain language, and the agent will turn each request into the right commands. For example:
43
+
44
+ - _"Create a skills library called acme-skills with categories mobile and data."_
45
+ - _"Add a backend skill for zero-downtime database migrations and write its content."_
46
+ - _"Make the code-review skill configurable for GitHub or Jira."_
47
+ - _"Install the library's skills into ../my-app for Claude Code and Copilot."_
48
+
49
+ The agent will then run `init`, `add`, `edit`, `validate`, `sync --param`, `build` and so on, and check the result with `skanvil validate`.
50
+
51
+ `skanvil init` already installs this skill in new libraries, in `.claude/skills/skanvil` by default. Run the prompt again after upgrading skanvil, and `install-skill` will refresh the skill.
52
+
7
53
  ## 1. Create a library
8
54
 
9
55
  ```bash
@@ -0,0 +1,196 @@
1
+ # Parameters and package exports
2
+
3
+ Two optional features turn a skills library into something other code can use.
4
+
5
+ - **Parameters** make one skill adapt to each team or project: the issue tracker, the package
6
+ manager, a dashboard URL. You keep one skill instead of near-identical copies.
7
+ - **Exports** (`skanvil build`) make the library's npm package importable:
8
+ `import { codeReview } from "@acme/skills"` returns the skill's `SKILL.md`, rendered with the
9
+ parameters you pass. The result is fully typed.
10
+
11
+ Skills without parameters work exactly as before. You can use exports without ever adding a
12
+ parameter.
13
+
14
+ ## When this is worth it
15
+
16
+ - **One skill, many variants.** Teams use different trackers, CI systems or package managers, and
17
+ you are about to duplicate a skill to change a few lines.
18
+ - **Facts an agent cannot infer.** For example the Grafana URL, the incident Slack channel or the
19
+ real deploy command. A parameter keeps them out of the shared text.
20
+ - **Skills consumed by code**, such as a project generator that installs tailored skills, a custom
21
+ agent built with an SDK that puts a skill in its prompt, or a monorepo script that installs a
22
+ skill per package.
23
+
24
+ If every project uses the same tools, plain `SKILL.md` files plus `skanvil sync` are simpler.
25
+
26
+ ## Declaring parameters
27
+
28
+ Add a `params.json` next to the skill's `SKILL.md`:
29
+
30
+ ```text
31
+ skills/front-code-review/
32
+ ├── SKILL.md
33
+ ├── params.json
34
+ └── references/
35
+ ```
36
+
37
+ ```json
38
+ {
39
+ "ticketTracker": {
40
+ "type": "enum",
41
+ "values": ["github", "jira", "linear"],
42
+ "default": "github",
43
+ "description": "Where findings are filed"
44
+ },
45
+ "packageManager": { "type": "string", "default": "npm" },
46
+ "strict": { "type": "boolean", "default": false, "description": "Block merges on any finding" }
47
+ }
48
+ ```
49
+
50
+ Rules for `params.json`:
51
+
52
+ - **Names** are camelCase letters and digits.
53
+ - **Types** are `enum` (with `values`), `string` or `boolean`.
54
+ - **Defaults** are required, so a skill always renders. An enum default must be one of its values.
55
+ - **The file stays out of agents' folders.** `params.json` is not part of the Agent Skills spec, so
56
+ `sync` and `build` never copy it to them.
57
+
58
+ ## Template syntax
59
+
60
+ Use the parameters anywhere in `SKILL.md`, including the frontmatter:
61
+
62
+ ```md
63
+ Run `{{packageManager}} test` before reviewing, and file each finding in {{ticketTracker}}.
64
+
65
+ {{#if ticketTracker == "jira"}}
66
+ Use the `PROJ-123` key format in the summary.
67
+ {{else}}
68
+ Reference issues as `#123`.
69
+ {{/if}}
70
+
71
+ {{#if strict}}
72
+ Block the merge until every finding is resolved.
73
+ {{/if}}
74
+ ```
75
+
76
+ | Syntax | Meaning |
77
+ | ----------------------------------------------------- | --------------------------------------------------------------------- |
78
+ | `{{name}}` | The parameter's value. |
79
+ | `{{#if name}}` / `{{#if !name}}` | The parameter is true, or false. For strings, "true" means not empty. |
80
+ | `{{#if name == "value"}}` / `{{#if name != "value"}}` | Equality test (single or double quotes). |
81
+ | `{{else}}`, `{{/if}}` | Else branch and end of block. Blocks can nest. |
82
+
83
+ How rendering behaves:
84
+
85
+ - **Tag lines vanish.** A block tag alone on its line removes the whole line, so blocks leave no
86
+ blank lines behind.
87
+ - **Literal braces are safe.** `{{text}}` that is not a declared parameter is left as it is. In a
88
+ skill that has a `params.json`, validation reports it, because it is usually a typo.
89
+
90
+ ## Validation
91
+
92
+ `skanvil validate` checks parametrized skills as rendered with their defaults, so every spec and
93
+ library rule still applies. Two extra rules apply:
94
+
95
+ - **`params-file`**: `params.json` is valid.
96
+ - **`params-template`**: `{{…}}` references and `{{#if}}` blocks use declared parameters and are
97
+ balanced.
98
+
99
+ Fix the defaults first: the README index and `skanvil list` show the description rendered with
100
+ them.
101
+
102
+ ## Installing with values (`sync --param`)
103
+
104
+ ```bash
105
+ npx skanvil sync --from @acme/skills --target claude \
106
+ --param ticketTracker=jira --param packageManager=pnpm --param strict=true
107
+ ```
108
+
109
+ What happens:
110
+
111
+ - **Rendering.** Every synced skill that declares a parameter gets that value. The others take
112
+ their defaults. The installed `SKILL.md` is rendered and `params.json` is left out.
113
+ - **Typos fail early.** A name that no synced skill declares is an error, raised before anything
114
+ is written.
115
+ - **Values are remembered per target**, in `.skanvil-sync.json`. A later `skanvil sync` without
116
+ `--param` reuses them, and passing `--param` again replaces them.
117
+ - **No links.** A rendered skill cannot be linked, so `--link` copies parametrized skills (with a
118
+ warning) and still links the others.
119
+
120
+ ## Package exports (`skanvil build`)
121
+
122
+ ```bash
123
+ npx skanvil build # writes dist/index.mjs, index.cjs, index.d.mts, index.d.cts
124
+ ```
125
+
126
+ Generated libraries already wire this up:
127
+
128
+ - `npm run build` runs `skanvil validate` and then `skanvil build`.
129
+ - `prepublishOnly` builds before every publish.
130
+ - `package.json` has `exports`, `main` and `types` pointing at `dist/`.
131
+ - `dist/` is gitignored.
132
+
133
+ The build refuses to run while validation reports errors, unless you pass `--force`.
134
+
135
+ For a library with `front-code-review` (with the parameters above) and `back-migrations`,
136
+ consumers get:
137
+
138
+ ```ts
139
+ import { codeReview, migrations, skills, getSkill, type CodeReviewParams } from "@acme/skills";
140
+
141
+ codeReview(); // SKILL.md with the defaults
142
+ codeReview({ ticketTracker: "jira", strict: true }); // rendered for one team
143
+ codeReview({ ticketTracker: "trello" }); // type error, and a runtime error in plain JS
144
+
145
+ migrations(); // a skill without parameters: its SKILL.md as is
146
+
147
+ for (const skill of skills) console.log(skill.name, skill.category, skill.description);
148
+
149
+ // Every file of a skill (references, scripts, assets), with SKILL.md rendered:
150
+ const files = getSkill("front-code-review").files({ ticketTracker: "linear" });
151
+ // { "SKILL.md": { encoding: "utf8", content: "…" }, "references/REFERENCE.md": { … }, … }
152
+ ```
153
+
154
+ How exports are named:
155
+
156
+ - **The default** is the folder name without its category prefix, in camelCase:
157
+ `front-code-review` becomes `codeReview`.
158
+ - **Clashes** use the full folder name, for example `frontReview` and `backReview`.
159
+ - **Names that cannot be identifiers** get a `skill` prefix, for example `skill2faSetup`.
160
+ - **What is exported:** only publishable skills, meaning categories with `validated: true`.
161
+ `_internal` skills are never exported.
162
+
163
+ The generated code has no dependencies, not even skanvil. It embeds the same renderer that
164
+ skanvil uses, so `codeReview({ … })` and `skanvil sync --param …` always produce the same text.
165
+
166
+ ### Examples
167
+
168
+ **A project generator** writes tailored skills into a new service:
169
+
170
+ ```ts
171
+ import fs from "node:fs";
172
+ import path from "node:path";
173
+ import { getSkill } from "@acme/skills";
174
+
175
+ function installSkill(name: string, params: Record<string, string | boolean>, root = ".claude/skills") {
176
+ for (const [file, { encoding, content }] of Object.entries(getSkill(name as never).files(params as never))) {
177
+ const target = path.join(root, name, file);
178
+ fs.mkdirSync(path.dirname(target), { recursive: true });
179
+ fs.writeFileSync(target, Buffer.from(content, encoding));
180
+ }
181
+ }
182
+
183
+ installSkill("front-code-review", { ticketTracker: "jira", packageManager: "pnpm" });
184
+ ```
185
+
186
+ **A custom agent** puts a skill in its system prompt:
187
+
188
+ ```ts
189
+ import { codeReview } from "@acme/skills";
190
+
191
+ const system = codeReview({ ticketTracker: "github", strict: true });
192
+ // pass `system` to your Claude API / Agent SDK call
193
+ ```
194
+
195
+ The skill is then versioned like any dependency, so Renovate or Dependabot tell you when it
196
+ changes.