@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 +1 -1
- package/dist/index.d.ts +37 -38
- package/dist/index.js +142 -114
- package/package.json +7 -7
package/README.md
CHANGED
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
|
-
/**
|
|
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 "
|
|
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
|
|
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
|
-
/**
|
|
176
|
-
|
|
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
|
|
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
|
|
235
|
-
|
|
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
|
-
/**
|
|
648
|
-
|
|
649
|
-
|
|
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
|
|
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(`
|
|
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
|
|
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 = "
|
|
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(
|
|
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(
|
|
904
|
+
function formatSkillDocumentation(commandName, appName) {
|
|
872
905
|
try {
|
|
873
|
-
return loadPackagedSkills(
|
|
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
|
|
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 {
|
|
884
|
-
|
|
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
|
-
|
|
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(
|
|
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, {
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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 (
|
|
1334
|
-
if (
|
|
1335
|
-
if (
|
|
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 (
|
|
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 (!
|
|
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
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
50
|
-
"@crustjs/style": "^0.3.
|
|
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.
|
|
55
|
-
"@crustjs/extensions": "0.
|
|
56
|
-
"@crustjs/testing": "0.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.
|
|
61
|
+
"@crustjs/core": "^0.3.1",
|
|
62
62
|
"typescript": "^7.0.0"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|