@crustjs/skills 0.2.1 → 0.3.1

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/README.md CHANGED
@@ -5,7 +5,7 @@ Package and install agent skills for AI coding assistants.
5
5
  ## Install
6
6
 
7
7
  ```sh
8
- bun add @crustjs/skills
8
+ npm install @crustjs/skills
9
9
  ```
10
10
 
11
11
  ## Documentation
package/dist/index.d.ts CHANGED
@@ -1,29 +1,4 @@
1
1
  import { CommandDefinition, CommandSnapshot, ExtensionFactory } from "@crustjs/core";
2
- //#region src/build.d.ts
3
- /** Options for rendering a skill source. */
4
- interface WriteSkillsOptions {
5
- /** Application whose command tree is rendered into a generated skill. Omit to write only `extras`. */
6
- readonly app?: {
7
- snapshot(): Promise<CommandSnapshot>;
8
- };
9
- /** `skills` directory that receives one subdirectory per skill. */
10
- readonly outDir: string;
11
- /** Version recorded in the generated skill's SKILL.md metadata. Omitted when absent. */
12
- readonly version?: string;
13
- /** Generated skill name. Defaults to the root command name. */
14
- readonly name?: string;
15
- /** Generated skill description. Defaults to the root command description. */
16
- readonly description?: string;
17
- /** Hand-authored skill directories included alongside the generated skill. */
18
- readonly extras?: readonly (string | URL)[];
19
- }
20
- /**
21
- * Renders generated and authored skills into a package-ready skill source.
22
- */
23
- export declare function writeSkills({ app, ...options }: WriteSkillsOptions): Promise<readonly string[]>;
24
- /** Renders skills from a Command Snapshot prepared in this or another process. */
25
- export declare function writeSkillsFromSnapshot(snapshot: CommandSnapshot, options: Omit<WriteSkillsOptions, "app">): Promise<readonly string[]>;
26
- //#endregion
27
2
  //#region src/agents.d.ts
28
3
  type AgentClass = "universal" | "additional";
29
4
  type Scope = "global" | "project";
@@ -95,10 +70,11 @@ interface SkillStatusResult {
95
70
  status: SkillLinkStatus;
96
71
  }>;
97
72
  }
98
- /** Options for the skills extension. */
73
+ /**
74
+ * Options for the skills extension. Packaged skills are read at runtime from
75
+ * `resolveArtifactDir("skills")` (`@crustjs/core`), the directory `crust build` stages.
76
+ */
99
77
  interface SkillOptions {
100
- /** Packaged skills directory read at runtime for discovery and installation. */
101
- distDir: string | URL;
102
78
  /** Hand-authored skill directories (URL, absolute, or package-root-relative path) built alongside the generated skill. */
103
79
  extras?: readonly (string | URL)[];
104
80
  /** Generated command skill name. Defaults to the root command name. */
@@ -116,10 +92,35 @@ interface SkillOptions {
116
92
  defaultScope?: Scope;
117
93
  /** Repair stale or dangling owned links before commands run. @default true */
118
94
  autoUpdate?: boolean;
119
- /** Name of the interactive management command. @default "skill" */
95
+ /** Name of the interactive management command. The default includes a `skill` alias. @default "skills" */
120
96
  command?: string;
121
97
  }
122
98
  //#endregion
99
+ //#region src/build.d.ts
100
+ /** Options for rendering a skill source. */
101
+ interface WriteSkillsOptions {
102
+ /** Application whose command tree is rendered into a generated skill. Omit to write only `extras`. */
103
+ readonly app?: {
104
+ snapshot(): Promise<CommandSnapshot>;
105
+ };
106
+ /** `skills` directory that receives one subdirectory per skill. */
107
+ readonly outDir: string;
108
+ /** Version recorded in the generated skill's SKILL.md metadata. Omitted when absent. */
109
+ readonly version?: string;
110
+ /** Generated skill name. Defaults to the root command name. */
111
+ readonly name?: string;
112
+ /** Generated skill description. Defaults to the root command description. */
113
+ readonly description?: string;
114
+ /** Hand-authored skill directories included alongside the generated skill. */
115
+ readonly extras?: readonly (string | URL)[];
116
+ }
117
+ /**
118
+ * Renders generated and authored skills into a package-ready skill source.
119
+ */
120
+ export declare function writeSkills({ app, ...options }: WriteSkillsOptions): Promise<readonly string[]>;
121
+ /** Renders skills from a Command Snapshot prepared in this or another process. */
122
+ export declare function writeSkillsFromSnapshot(snapshot: CommandSnapshot, options: Omit<WriteSkillsOptions, "app">): Promise<readonly string[]>;
123
+ //#endregion
123
124
  //#region src/errors.d.ts
124
125
  export declare class SkillSourceConflictError extends Error {
125
126
  override readonly name = "SkillSourceConflictError";
@@ -145,7 +146,7 @@ export declare const skill: ExtensionFactory<[options: SkillOptions], {}, [], []
145
146
  export declare function installSkill(options: InstallSkillOptions): Promise<InstallSkillResult>;
146
147
  /** Unlinks only agent-directory entries carrying the requested skill's ownership signature. */
147
148
  export declare function uninstallSkill(options: UninstallSkillOptions): Promise<UninstallSkillResult>;
148
- /** Reports the ownership and health of each requested agent-directory entry. */
149
+ /** Reports ownership and health. Only ENOENT is missing; other filesystem errors reject. */
149
150
  export declare function getSkillStatus(options: SkillStatusOptions): Promise<SkillStatusResult>;
150
151
  //#endregion
151
152
  //#region src/skill-name.d.ts
@@ -158,21 +159,19 @@ export declare function getSkillStatus(options: SkillStatusOptions): Promise<Ski
158
159
  export declare function isValidSkillName(name: string): boolean;
159
160
  //#endregion
160
161
  //#region src/source.d.ts
162
+ /** The packaged skills root is missing or holds no skills: the CLI has not been built yet. */
161
163
  export declare class SkillSourceUnavailableError extends Error {
162
164
  override readonly name = "SkillSourceUnavailableError";
163
165
  }
164
- /**
165
- * Resolves a logical packaged skill-source root. When the package path is
166
- * unavailable, falls back to the executable directory: absolute and URL
167
- * sources by their basename, relative sources by the same relative path.
168
- */
169
- export declare function resolveSkillSource(source: string | URL): string;
170
166
  interface PackagedSkill {
171
167
  readonly sourceDir: string;
172
168
  readonly name: string;
173
169
  readonly description: string;
174
170
  }
175
- /** Reads every self-describing skill directory in a packaged skill source. */
176
- export declare function loadPackagedSkills(source: string | URL): readonly PackagedSkill[];
171
+ /**
172
+ * Reads every self-describing skill directory under `root`, the absolute
173
+ * packaged skills directory (normally `resolveArtifactDir("skills")`).
174
+ */
175
+ export declare function loadPackagedSkills(root: string): readonly PackagedSkill[];
177
176
  //#endregion
178
177
  export type { AgentClass, AgentResult, AgentTarget, InstallSkillOptions, InstallSkillResult, InstallStatus, PackagedSkill, Scope, SkillLinkStatus, SkillOptions, SkillStatusOptions, SkillStatusResult, UninstallSkillOptions, UninstallSkillResult, UninstallStatus, WriteSkillsOptions };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { lstat, mkdir, readFile, readdir, readlink, realpath, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
2
2
  import { basename, delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
3
- import { accessSync, constants, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { accessSync, constants, existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { buildCommandDocumentation, formatDefault, sectionsFor } from "@crustjs/core/tooling";
6
6
  import { defineCommand, defineExtension, defineExtensionId } from "@crustjs/core";
@@ -41,7 +41,7 @@ function isWithin(parent, child) {
41
41
  * @returns The absolute path of the nearest enclosing directory containing
42
42
  * `package.json`, or `null` if the filesystem root is reached first.
43
43
  *
44
- * @internal Used only by {@link resolveSourceDir}.
44
+ * @internal Shared by {@link resolveSourceDir} and `resolveArtifactDir`.
45
45
  */
46
46
  function findNearestPackageRoot(startPath) {
47
47
  let current = resolve(startPath);
@@ -230,12 +230,13 @@ async function loadBundleFiles(sourceDir) {
230
230
  const collected = await collectBundleEntries(canonicalRoot, canonicalRoot, "", /* @__PURE__ */ new Set([canonicalRoot]));
231
231
  const skillMd = collected.find((f) => f.relPath === SKILL_MD);
232
232
  if (!skillMd) throw new Error(`Extra skill directory is missing SKILL.md at its root "${canonicalRoot}". Every extra skill directory must contain a top-level SKILL.md file.`);
233
+ const files = await Promise.all(collected.map(async (entry) => ({
234
+ path: entry.relPath,
235
+ content: await readFile(entry.absPath)
236
+ })));
233
237
  return {
234
- files: await Promise.all(collected.map(async (entry) => ({
235
- path: entry.relPath,
236
- content: await readFile(entry.absPath)
237
- }))),
238
- frontmatter: requireSkillFrontmatter(probeFrontmatter(await readFile(skillMd.absPath, "utf-8")), `Extra skill SKILL.md at "${join(canonicalRoot, SKILL_MD)}"`)
238
+ files,
239
+ frontmatter: requireSkillFrontmatter(probeFrontmatter(files[collected.indexOf(skillMd)].content.toString("utf-8")), `Extra skill SKILL.md at "${join(canonicalRoot, SKILL_MD)}"`)
239
240
  };
240
241
  }
241
242
  //#endregion
@@ -258,6 +259,56 @@ var SkillConflictError = class extends Error {
258
259
  }
259
260
  };
260
261
  //#endregion
262
+ //#region ../utils/src/artifacts.ts
263
+ /**
264
+ * Set by `crust build` while it prepares the Command Snapshot: the absolute
265
+ * entry-isolated directory Extension build hooks write into. Core reads it
266
+ * to run the hooks; `resolveArtifactDir` reads it so sections evaluated during
267
+ * that run see the artifacts being built instead of the wiped `.crust/root`.
268
+ */
269
+ const BUILD_OUT_DIR_ENV = "CRUST_INTERNAL_BUILD_OUT_DIR";
270
+ /** True inside a `bun build --compile` or `deno compile` executable. */
271
+ function isCompiledExecutable() {
272
+ const { Bun, Deno } = globalThis;
273
+ const bunMain = Bun?.main ?? "";
274
+ return bunMain.startsWith("/$bunfs/") || /^[A-Za-z]:[\\/]~BUN[\\/]/.test(bunMain) || Deno?.build?.standalone === true;
275
+ }
276
+ /**
277
+ * Absolute path of a build artifact or `crust.include` directory shipped with
278
+ * this CLI. `name` is a top-level directory name such as `"skills"`.
279
+ *
280
+ * The artifact path is computed from how the CLI is running — never probed:
281
+ * - Compiled executable (Bun or Deno): `<dir of the executable>/<name>`, which
282
+ * is a platform package's `bin/` or wherever the binary was placed.
283
+ * - Crust-built Node bundle: `<name>` next to the bundle's `bin/` directory,
284
+ * i.e. `.crust/root/<name>` in place and `<installed root>/<name>` after install.
285
+ * - Snapshot preparation inside `crust build`: `<build output dir>/<name>`, the
286
+ * artifacts earlier Extension build hooks wrote in this same build.
287
+ * - Source (`bun run`, `node`, `deno run`): `.crust/root/<name>` under the
288
+ * nearest package root of the real `process.argv[1]` entrypoint (following source
289
+ * links) — the output of the last `crust build`.
290
+ *
291
+ * @throws {Error} when `name` is not a single path segment, or in source mode
292
+ * when the entrypoint cannot be resolved or has no enclosing `package.json`.
293
+ */
294
+ function resolveArtifactDir(name) {
295
+ if (name === "" || name === "." || name === ".." || /[\\/]/.test(name)) throw new Error(`Artifact name must be a single directory name, got ${JSON.stringify(name)}.`);
296
+ if (isCompiledExecutable()) return join(dirname(process.execPath), name);
297
+ if (process.env.CRUST_INTERNAL_BUILD === "1") return resolve(fileURLToPath(import.meta.url), "..", "..", name);
298
+ const buildOutDir = process.env[BUILD_OUT_DIR_ENV];
299
+ if (buildOutDir) return join(buildOutDir, name);
300
+ const entrypoint = process.argv[1];
301
+ let sourceEntrypoint = entrypoint;
302
+ if (entrypoint) try {
303
+ sourceEntrypoint = realpathSync(entrypoint);
304
+ } catch (cause) {
305
+ throw new Error(`Could not resolve artifact "${name}": could not resolve source entrypoint "${entrypoint}".`, { cause });
306
+ }
307
+ const packageRoot = sourceEntrypoint ? findNearestPackageRoot(sourceEntrypoint) : null;
308
+ if (!packageRoot) throw new Error(`Could not resolve artifact "${name}": no package.json was found above ${entrypoint ? `entrypoint "${entrypoint}"` : "process.argv[1] (unset)"}.`);
309
+ return join(packageRoot, ".crust", "root", name);
310
+ }
311
+ //#endregion
261
312
  //#region ../utils/src/process.ts
262
313
  /** Resolve a bare executable name from PATH (and PATHEXT on Windows). */
263
314
  function which(command) {
@@ -567,6 +618,12 @@ function resolveAgentPath(agent, scope, name) {
567
618
  return join(cfg.globalSkillsDir(homedir()), name);
568
619
  }
569
620
  //#endregion
621
+ //#region ../utils/src/error.ts
622
+ /** Narrows a caught Node.js system error to its stable errno contract. */
623
+ function isErrnoException(error) {
624
+ return error instanceof Error && "code" in error && typeof error.code === "string";
625
+ }
626
+ //#endregion
570
627
  //#region src/link.ts
571
628
  /** Returns whether a symlink target carries Crust's skill ownership signature. */
572
629
  function isOwnedSkillLink(target, name) {
@@ -593,47 +650,11 @@ function isValidSkillName(name) {
593
650
  return name.length >= 1 && name.length <= 64 && SKILL_NAME_PATTERN.test(name);
594
651
  }
595
652
  //#endregion
596
- //#region ../utils/src/error.ts
597
- /** Narrows a caught Node.js system error to its stable errno contract. */
598
- function isErrnoException(error) {
599
- return error instanceof Error && "code" in error && typeof error.code === "string";
600
- }
601
- //#endregion
602
653
  //#region src/source.ts
654
+ /** The packaged skills root is missing or holds no skills: the CLI has not been built yet. */
603
655
  var SkillSourceUnavailableError = class extends Error {
604
656
  name = "SkillSourceUnavailableError";
605
657
  };
606
- function directoryPath(path) {
607
- try {
608
- return statSync(path).isDirectory() ? path : null;
609
- } catch {
610
- return null;
611
- }
612
- }
613
- function fallbackName(source) {
614
- if (source instanceof URL) return basename(fileURLToPath(source));
615
- return isAbsolute(source) ? basename(source) : source;
616
- }
617
- /**
618
- * Resolves a logical packaged skill-source root. When the package path is
619
- * unavailable, falls back to the executable directory: absolute and URL
620
- * sources by their basename, relative sources by the same relative path.
621
- */
622
- function resolveSkillSource(source) {
623
- if (source instanceof URL && source.protocol !== "file:") throw new Error(`Skill source URL must use file: protocol, got "${source.protocol}".`);
624
- let primary;
625
- try {
626
- primary = resolveSourceDir(source);
627
- } catch {}
628
- if (primary) {
629
- const resolved = directoryPath(primary);
630
- if (resolved) return resolved;
631
- }
632
- const fallback = join(dirname(process.execPath), fallbackName(source));
633
- const resolvedFallback = directoryPath(fallback);
634
- if (resolvedFallback) return resolvedFallback;
635
- throw new SkillSourceUnavailableError(`Could not resolve skill source${primary ? ` at "${primary}"` : ""} or executable-relative fallback "${fallback}".`);
636
- }
637
658
  function readSkillFrontmatter(sourceDir) {
638
659
  let content;
639
660
  try {
@@ -644,11 +665,20 @@ function readSkillFrontmatter(sourceDir) {
644
665
  }
645
666
  return requireSkillFrontmatter(probeFrontmatter(content), `Skill source directory "${sourceDir}"`);
646
667
  }
647
- /** Reads every self-describing skill directory in a packaged skill source. */
648
- function loadPackagedSkills(source) {
649
- const root = resolveSkillSource(source);
668
+ /**
669
+ * Reads every self-describing skill directory under `root`, the absolute
670
+ * packaged skills directory (normally `resolveArtifactDir("skills")`).
671
+ */
672
+ function loadPackagedSkills(root) {
673
+ let entries;
674
+ try {
675
+ entries = readdirSync(root, { withFileTypes: true });
676
+ } catch (error) {
677
+ if (!isErrnoException(error) || error.code !== "ENOENT" && error.code !== "ENOTDIR") throw error;
678
+ throw new SkillSourceUnavailableError(`Packaged skills not found at "${root}". Run \`crust build\` first.`, { cause: error });
679
+ }
650
680
  const skills = [];
651
- for (const entry of readdirSync(root, { withFileTypes: true })) {
681
+ for (const entry of entries) {
652
682
  if (!entry.isDirectory()) continue;
653
683
  const sourceDir = join(root, entry.name);
654
684
  if (!existsSync(join(sourceDir, "SKILL.md"))) continue;
@@ -660,7 +690,7 @@ function loadPackagedSkills(source) {
660
690
  description: frontmatter.description
661
691
  });
662
692
  }
663
- if (skills.length === 0) throw new SkillSourceUnavailableError(`Skill source "${root}" does not contain any skill directories.`);
693
+ if (skills.length === 0) throw new SkillSourceUnavailableError(`Packaged skills at "${root}" do not contain any skill directories. Run \`crust build\` first.`);
664
694
  return skills.sort((a, b) => a.name.localeCompare(b.name));
665
695
  }
666
696
  //#endregion
@@ -680,7 +710,8 @@ async function pathExists(path) {
680
710
  try {
681
711
  await stat(path);
682
712
  return true;
683
- } catch {
713
+ } catch (error) {
714
+ if (!isErrnoException(error) || error.code !== "ENOENT") throw error;
684
715
  return false;
685
716
  }
686
717
  }
@@ -688,7 +719,8 @@ async function inspectLink(outputDir, name, expectedSourceDir) {
688
719
  let entry;
689
720
  try {
690
721
  entry = await lstat(outputDir);
691
- } catch {
722
+ } catch (error) {
723
+ if (!isErrnoException(error) || error.code !== "ENOENT") throw error;
692
724
  return { status: "absent" };
693
725
  }
694
726
  if (!entry.isSymbolicLink()) return { status: "conflict" };
@@ -760,7 +792,7 @@ async function uninstallSkill(options) {
760
792
  }
761
793
  return { agents: results };
762
794
  }
763
- /** Reports the ownership and health of each requested agent-directory entry. */
795
+ /** Reports ownership and health. Only ENOENT is missing; other filesystem errors reject. */
764
796
  async function getSkillStatus(options) {
765
797
  const agents = options.agents ?? [...ALL_AGENTS];
766
798
  const scope = resolveEffectiveScope(options.scope ?? "global");
@@ -803,9 +835,10 @@ function planReconcile(options) {
803
835
  //#endregion
804
836
  //#region src/extension.ts
805
837
  const SKILLS = defineExtensionId("crust:skills");
806
- const DEFAULT_SKILL_COMMAND_NAME = "skill";
838
+ const DEFAULT_SKILL_COMMAND_NAME = "skills";
807
839
  const SKILLS_SECTION_TITLE = "Agent skills";
808
840
  const DEFAULT_SKILL_SCOPE = "global";
841
+ const SKILLS_ARTIFACT = "skills";
809
842
  async function resolveScope(rawScope, options) {
810
843
  if (rawScope) return rawScope;
811
844
  if (options.defaultScope) return options.defaultScope;
@@ -855,7 +888,7 @@ async function repairInstalledSkill(packagedSkill, scope, io, report = false) {
855
888
  async function autoRepairSkills(options, io) {
856
889
  let skills;
857
890
  try {
858
- skills = loadPackagedSkills(options.distDir);
891
+ skills = loadPackagedSkills(resolveArtifactDir(SKILLS_ARTIFACT));
859
892
  } catch (error) {
860
893
  if (!(error instanceof SkillSourceUnavailableError)) io.stderr(yellow(`Skipping skill link repair: ${error instanceof Error ? error.message : String(error)}`));
861
894
  return;
@@ -868,27 +901,28 @@ async function autoRepairSkills(options, io) {
868
901
  io.stderr(yellow(`Skipping skill link repair [${packagedSkill.name}]: ${error instanceof Error ? error.message : String(error)}`));
869
902
  }
870
903
  }
871
- function formatSkillDocumentation(source, commandName, appName) {
904
+ function formatSkillDocumentation(commandName, appName) {
872
905
  try {
873
- return loadPackagedSkills(source).map((packagedSkill) => {
906
+ return loadPackagedSkills(resolveArtifactDir(SKILLS_ARTIFACT)).map((packagedSkill) => {
874
907
  const sourcePath = isWithin(process.cwd(), packagedSkill.sourceDir) ? relative(process.cwd(), packagedSkill.sourceDir) || "." : packagedSkill.sourceDir;
875
908
  return `${packagedSkill.name} — ${packagedSkill.description}\n Source: ${sourcePath}`;
876
909
  }).join("\n\n");
877
910
  } catch (error) {
878
- if (error instanceof SkillSourceUnavailableError) return `The packaged skills directory is unavailable. Run \`${appName} ${commandName}\` to link packaged skills into an agent directory.`;
911
+ if (error instanceof SkillSourceUnavailableError) return `${error.message} Then run \`${appName} ${commandName}\` to link packaged skills into an agent directory.`;
879
912
  return `Packaged skills could not be read. Run \`${appName} ${commandName}\` for details.`;
880
913
  }
881
914
  }
882
915
  async function buildSkills(options, context) {
883
- const { writeSkills, writeSkillsFromSnapshot } = await Promise.resolve().then(() => build_exports);
884
- const writeOptions = {
885
- outDir: join(context.outDir, "skills"),
916
+ const { renderSkills } = await Promise.resolve().then(() => build_exports);
917
+ return (await renderSkills(options.generated === false ? void 0 : context.snapshot, {
886
918
  version: context.snapshot.meta.version,
887
919
  name: options.name,
888
920
  description: options.description,
889
921
  extras: options.extras
890
- };
891
- return (options.generated === false ? await writeSkills(writeOptions) : await writeSkillsFromSnapshot(context.snapshot, writeOptions)).map((file) => join("skills", file));
922
+ })).map((file) => ({
923
+ path: join(SKILLS_ARTIFACT, file.path),
924
+ content: file.content
925
+ }));
892
926
  }
893
927
  const skill = defineExtension(SKILLS, (options) => {
894
928
  const commandName = options.command ?? DEFAULT_SKILL_COMMAND_NAME;
@@ -897,7 +931,7 @@ const skill = defineExtension(SKILLS, (options) => {
897
931
  sections: (snapshot) => [{
898
932
  command: [],
899
933
  title: SKILLS_SECTION_TITLE,
900
- body: formatSkillDocumentation(options.distDir, commandName, snapshot.meta.name),
934
+ body: formatSkillDocumentation(commandName, snapshot.meta.name),
901
935
  except: [SKILLS]
902
936
  }],
903
937
  build: (context) => buildSkills(options, context),
@@ -1002,7 +1036,10 @@ async function reconcileSkill(opts) {
1002
1036
  if (toInstall.length === 0 && toUninstall.length === 0) io.stdout(dim(`No changes [${packagedSkill.name}].`));
1003
1037
  }
1004
1038
  function buildSkillCommand(commandName, options) {
1005
- return defineCommand(commandName, { description: "Manage agent skill installations" }, (command) => command.flags({
1039
+ return defineCommand(commandName, {
1040
+ description: "Manage agent skill installations",
1041
+ aliases: commandName === DEFAULT_SKILL_COMMAND_NAME ? ["skill"] : []
1042
+ }, (command) => command.flags({
1006
1043
  name: "scope",
1007
1044
  type: "string",
1008
1045
  choices: ["project", "global"],
@@ -1018,11 +1055,11 @@ function buildSkillCommand(commandName, options) {
1018
1055
  description: "Update scope (project or global)"
1019
1056
  }).action(async (context) => {
1020
1057
  const scope = await resolveScope(context.flags.scope, options);
1021
- for (const packagedSkill of loadPackagedSkills(options.distDir)) await repairInstalledSkill(packagedSkill, scope, context, true);
1058
+ for (const packagedSkill of loadPackagedSkills(resolveArtifactDir(SKILLS_ARTIFACT))) await repairInstalledSkill(packagedSkill, scope, context, true);
1022
1059
  }))).action(async (context) => {
1023
1060
  const installAll = context.flags.all === true;
1024
1061
  const scope = installAll ? context.flags.scope ?? options.defaultScope ?? DEFAULT_SKILL_SCOPE : await resolveScope(context.flags.scope, options);
1025
- for (const packagedSkill of loadPackagedSkills(options.distDir)) await reconcileSkill({
1062
+ for (const packagedSkill of loadPackagedSkills(resolveArtifactDir(SKILLS_ARTIFACT))) await reconcileSkill({
1026
1063
  packagedSkill,
1027
1064
  scope,
1028
1065
  installAll,
@@ -1065,7 +1102,7 @@ function normalizeArg(arg) {
1065
1102
  variadic: arg.variadic
1066
1103
  };
1067
1104
  if (arg.description !== void 0) result.description = arg.description;
1068
- if (arg.default !== void 0) result.default = serializeDefault(arg.default);
1105
+ if (arg.default !== void 0) result.default = formatDefault(arg.default);
1069
1106
  return result;
1070
1107
  }
1071
1108
  function normalizeFlag(flag) {
@@ -1077,15 +1114,12 @@ function normalizeFlag(flag) {
1077
1114
  multiple: flag.multiple
1078
1115
  };
1079
1116
  if (flag.description !== void 0) result.description = flag.description;
1080
- if (flag.default !== void 0) result.default = serializeDefault(flag.default);
1117
+ if (flag.default !== void 0) result.default = formatDefault(flag.default);
1081
1118
  return result;
1082
1119
  }
1083
1120
  function manifestType(type) {
1084
1121
  return type === "number" || type === "boolean" ? type : "string";
1085
1122
  }
1086
- function serializeDefault(value) {
1087
- return formatDefault(value);
1088
- }
1089
1123
  //#endregion
1090
1124
  //#region src/render.ts
1091
1125
  /**
@@ -1295,21 +1329,12 @@ function renderArgsTable(args) {
1295
1329
  for (const arg of args) {
1296
1330
  const name = arg.variadic ? `${arg.name}...` : arg.name;
1297
1331
  const required = arg.required ? "Yes" : "No";
1298
- const desc = escapeTableCell(formatArgDescription(arg));
1332
+ const desc = escapeTableCell(formatFieldDescription(arg));
1299
1333
  lines.push(`| \`${name}\` | ${arg.type} | ${required} | ${desc} |`);
1300
1334
  }
1301
1335
  return lines;
1302
1336
  }
1303
1337
  /**
1304
- * Formats the description cell for an argument, including default value.
1305
- */
1306
- function formatArgDescription(arg) {
1307
- const parts = [];
1308
- if (arg.description) parts.push(arg.description);
1309
- if (arg.default !== void 0) parts.push(`Default: \`${arg.default}\``);
1310
- return parts.join(". ") || "-";
1311
- }
1312
- /**
1313
1338
  * Renders a markdown table for named flags.
1314
1339
  */
1315
1340
  function renderFlagsTable(flags) {
@@ -1319,20 +1344,16 @@ function renderFlagsTable(flags) {
1319
1344
  for (const flag of flags) {
1320
1345
  const name = flag.spellings.map((spelling) => `\`${spelling}\``).join(", ");
1321
1346
  const required = flag.required ? "Yes" : "No";
1322
- const desc = escapeTableCell(formatFlagDescription(flag));
1347
+ const desc = escapeTableCell(formatFieldDescription(flag));
1323
1348
  lines.push(`| ${name} | ${flag.type} | ${required} | ${desc} |`);
1324
1349
  }
1325
1350
  return lines;
1326
1351
  }
1327
- /**
1328
- * Formats the description cell for a flag, including default value
1329
- * and multiplicity.
1330
- */
1331
- function formatFlagDescription(flag) {
1352
+ function formatFieldDescription(field) {
1332
1353
  const parts = [];
1333
- if (flag.description) parts.push(flag.description);
1334
- if (flag.multiple) parts.push("Can be specified multiple times");
1335
- if (flag.default !== void 0) parts.push(`Default: \`${flag.default}\``);
1354
+ if (field.description) parts.push(field.description);
1355
+ if ("multiple" in field && field.multiple) parts.push("Can be specified multiple times");
1356
+ if (field.default !== void 0) parts.push(`Default: \`${field.default}\``);
1336
1357
  return parts.join(". ") || "-";
1337
1358
  }
1338
1359
  /**
@@ -1370,6 +1391,7 @@ function findNode(root, path) {
1370
1391
  //#endregion
1371
1392
  //#region src/build.ts
1372
1393
  var build_exports = /* @__PURE__ */ __exportAll({
1394
+ renderSkills: () => renderSkills,
1373
1395
  writeSkills: () => writeSkills,
1374
1396
  writeSkillsFromSnapshot: () => writeSkillsFromSnapshot
1375
1397
  });
@@ -1384,18 +1406,37 @@ async function writeSkillsFromSnapshot(snapshot, options) {
1384
1406
  return await writeSkillSource(snapshot, options);
1385
1407
  }
1386
1408
  async function writeSkillSource(snapshot, options) {
1387
- if (snapshot === void 0 && (options.extras?.length ?? 0) === 0) throw new Error("Nothing to write: provide an app or at least one extra skill directory.");
1388
1409
  const outDir = resolve(options.outDir);
1389
1410
  if (basename(outDir) !== "skills") throw new Error(`Skill source outDir "${outDir}" must be named "skills".`);
1390
- const skills = /* @__PURE__ */ new Map();
1391
- const authoredNames = /* @__PURE__ */ new Set();
1392
1411
  for (const sourceDir of options.extras ?? []) {
1393
1412
  const resolved = resolveSourceDir(sourceDir);
1394
1413
  if (isWithin(outDir, resolved)) throw new Error(`Extra skill directory "${resolved}" is inside outDir "${outDir}", which is replaced on every build. Move authored skills outside the build output.`);
1414
+ }
1415
+ const files = await renderSkills(snapshot, options);
1416
+ const cwd = resolve(".");
1417
+ if (outDir === dirname(outDir) || isWithin(outDir, cwd)) throw new Error(`Refusing to replace "${outDir}": outDir must be a dedicated directory, not the filesystem root, the working directory, or an ancestor of it.`);
1418
+ await rm(outDir, {
1419
+ recursive: true,
1420
+ force: true
1421
+ });
1422
+ for (const file of files) {
1423
+ const filePath = join(outDir, file.path);
1424
+ await mkdir(dirname(filePath), { recursive: true });
1425
+ await writeFile(filePath, file.content);
1426
+ }
1427
+ return files.map((file) => file.path);
1428
+ }
1429
+ /**
1430
+ * Renders generated and authored skills as `<skill>/<file>` paths relative to a
1431
+ * `skills` directory, without writing them.
1432
+ */
1433
+ async function renderSkills(snapshot, options) {
1434
+ if (snapshot === void 0 && (options.extras?.length ?? 0) === 0) throw new Error("Nothing to write: provide an app or at least one extra skill directory.");
1435
+ const skills = /* @__PURE__ */ new Map();
1436
+ for (const sourceDir of options.extras ?? []) {
1395
1437
  const bundle = await loadBundleFiles(sourceDir);
1396
1438
  validateSkillName(bundle.frontmatter.name);
1397
- if (authoredNames.has(bundle.frontmatter.name)) throw new SkillSourceConflictError(bundle.frontmatter.name);
1398
- authoredNames.add(bundle.frontmatter.name);
1439
+ if (skills.has(bundle.frontmatter.name)) throw new SkillSourceConflictError(bundle.frontmatter.name);
1399
1440
  skills.set(bundle.frontmatter.name, bundle.files);
1400
1441
  }
1401
1442
  if (snapshot) {
@@ -1404,32 +1445,19 @@ async function writeSkillSource(snapshot, options) {
1404
1445
  description: options.description ?? snapshot.meta.description ?? "",
1405
1446
  version: options.version
1406
1447
  };
1407
- if (!authoredNames.has(generatedMeta.name)) {
1448
+ if (!skills.has(generatedMeta.name)) {
1408
1449
  validateSkillName(generatedMeta.name);
1409
1450
  requireSkillFrontmatter(generatedMeta, `Skill "${generatedMeta.name}"`);
1410
1451
  skills.set(generatedMeta.name, renderSkill(buildManifest(snapshot), generatedMeta));
1411
1452
  }
1412
1453
  }
1413
- const cwd = resolve(".");
1414
- if (outDir === dirname(outDir) || isWithin(outDir, cwd)) throw new Error(`Refusing to replace "${outDir}": outDir must be a dedicated directory, not the filesystem root, the working directory, or an ancestor of it.`);
1415
- await rm(outDir, {
1416
- recursive: true,
1417
- force: true
1418
- });
1419
- const written = [];
1420
- for (const [name, files] of skills) written.push(...(await writeFiles(join(outDir, name), files)).map((path) => join(name, path)));
1421
- return written;
1454
+ return [...skills].flatMap(([name, files]) => files.map((file) => ({
1455
+ path: join(name, file.path),
1456
+ content: file.content
1457
+ })));
1422
1458
  }
1423
1459
  function validateSkillName(name) {
1424
1460
  if (!isValidSkillName(name)) throw new Error(`Invalid skill name "${name}": must be 1–64 lowercase alphanumeric characters and hyphens, no leading/trailing/consecutive hyphens.`);
1425
1461
  }
1426
- async function writeFiles(baseDir, files) {
1427
- for (const file of files) {
1428
- const filePath = join(baseDir, file.path);
1429
- await mkdir(dirname(filePath), { recursive: true });
1430
- await writeFile(filePath, file.content);
1431
- }
1432
- return files.map((file) => file.path);
1433
- }
1434
1462
  //#endregion
1435
- export { SkillConflictError, SkillSourceConflictError, SkillSourceUnavailableError, detectInstalledAgents, getAdditionalAgents, getSkillStatus, getUniversalAgents, installSkill, isUniversalAgent, isValidSkillName, loadPackagedSkills, resolveSkillSource, skill, uninstallSkill, writeSkills, writeSkillsFromSnapshot };
1463
+ export { SkillConflictError, SkillSourceConflictError, SkillSourceUnavailableError, detectInstalledAgents, getAdditionalAgents, getSkillStatus, getUniversalAgents, installSkill, isUniversalAgent, isValidSkillName, loadPackagedSkills, skill, uninstallSkill, writeSkills, writeSkillsFromSnapshot };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crustjs/skills",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Package and install agent skills for AI coding assistants.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -46,19 +46,19 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@crustjs/progress": "^0.1.1",
49
- "@crustjs/prompts": "^0.2.1",
50
- "@crustjs/style": "^0.3.1"
49
+ "@crustjs/prompts": "^0.2.2",
50
+ "@crustjs/style": "^0.3.3"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@crustjs/config": "0.0.0",
54
- "@crustjs/core": "0.2.1",
55
- "@crustjs/extensions": "0.2.1",
56
- "@crustjs/testing": "0.1.1",
54
+ "@crustjs/core": "0.3.1",
55
+ "@crustjs/extensions": "0.3.1",
56
+ "@crustjs/testing": "0.1.2",
57
57
  "@crustjs/utils": "0.0.0",
58
58
  "tsdown": "^0.23.0"
59
59
  },
60
60
  "peerDependencies": {
61
- "@crustjs/core": "^0.2.1",
61
+ "@crustjs/core": "^0.3.1",
62
62
  "typescript": "^7.0.0"
63
63
  },
64
64
  "peerDependenciesMeta": {