@crustjs/skills 0.2.1 → 0.3.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/README.md +1 -1
- package/dist/index.d.ts +37 -38
- package/dist/index.js +136 -109
- 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);
|
|
@@ -258,6 +258,56 @@ var SkillConflictError = class extends Error {
|
|
|
258
258
|
}
|
|
259
259
|
};
|
|
260
260
|
//#endregion
|
|
261
|
+
//#region ../utils/src/artifacts.ts
|
|
262
|
+
/**
|
|
263
|
+
* Set by `crust build` while it prepares the Command Snapshot: the absolute
|
|
264
|
+
* entry-isolated directory Extension build hooks write into. Core reads it
|
|
265
|
+
* to run the hooks; `resolveArtifactDir` reads it so sections evaluated during
|
|
266
|
+
* that run see the artifacts being built instead of the wiped `.crust/root`.
|
|
267
|
+
*/
|
|
268
|
+
const BUILD_OUT_DIR_ENV = "CRUST_INTERNAL_BUILD_OUT_DIR";
|
|
269
|
+
/** True inside a `bun build --compile` or `deno compile` executable. */
|
|
270
|
+
function isCompiledExecutable() {
|
|
271
|
+
const { Bun, Deno } = globalThis;
|
|
272
|
+
const bunMain = Bun?.main ?? "";
|
|
273
|
+
return bunMain.startsWith("/$bunfs/") || /^[A-Za-z]:[\\/]~BUN[\\/]/.test(bunMain) || Deno?.build?.standalone === true;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Absolute path of a build artifact or `crust.include` directory shipped with
|
|
277
|
+
* this CLI. `name` is a top-level directory name such as `"skills"`.
|
|
278
|
+
*
|
|
279
|
+
* The artifact path is computed from how the CLI is running — never probed:
|
|
280
|
+
* - Compiled executable (Bun or Deno): `<dir of the executable>/<name>`, which
|
|
281
|
+
* is a platform package's `bin/` or wherever the binary was placed.
|
|
282
|
+
* - Crust-built Node bundle: `<name>` next to the bundle's `bin/` directory,
|
|
283
|
+
* i.e. `.crust/root/<name>` in place and `<installed root>/<name>` after install.
|
|
284
|
+
* - Snapshot preparation inside `crust build`: `<build output dir>/<name>`, the
|
|
285
|
+
* artifacts earlier Extension build hooks wrote in this same build.
|
|
286
|
+
* - Source (`bun run`, `node`, `deno run`): `.crust/root/<name>` under the
|
|
287
|
+
* nearest package root of the real `process.argv[1]` entrypoint (following source
|
|
288
|
+
* links) — the output of the last `crust build`.
|
|
289
|
+
*
|
|
290
|
+
* @throws {Error} when `name` is not a single path segment, or in source mode
|
|
291
|
+
* when the entrypoint cannot be resolved or has no enclosing `package.json`.
|
|
292
|
+
*/
|
|
293
|
+
function resolveArtifactDir(name) {
|
|
294
|
+
if (name === "" || name === "." || name === ".." || /[\\/]/.test(name)) throw new Error(`Artifact name must be a single directory name, got ${JSON.stringify(name)}.`);
|
|
295
|
+
if (isCompiledExecutable()) return join(dirname(process.execPath), name);
|
|
296
|
+
if (process.env.CRUST_INTERNAL_BUILD === "1") return resolve(fileURLToPath(import.meta.url), "..", "..", name);
|
|
297
|
+
const buildOutDir = process.env[BUILD_OUT_DIR_ENV];
|
|
298
|
+
if (buildOutDir) return join(buildOutDir, name);
|
|
299
|
+
const entrypoint = process.argv[1];
|
|
300
|
+
let sourceEntrypoint = entrypoint;
|
|
301
|
+
if (entrypoint) try {
|
|
302
|
+
sourceEntrypoint = realpathSync(entrypoint);
|
|
303
|
+
} catch (cause) {
|
|
304
|
+
throw new Error(`Could not resolve artifact "${name}": could not resolve source entrypoint "${entrypoint}".`, { cause });
|
|
305
|
+
}
|
|
306
|
+
const packageRoot = sourceEntrypoint ? findNearestPackageRoot(sourceEntrypoint) : null;
|
|
307
|
+
if (!packageRoot) throw new Error(`Could not resolve artifact "${name}": no package.json was found above ${entrypoint ? `entrypoint "${entrypoint}"` : "process.argv[1] (unset)"}.`);
|
|
308
|
+
return join(packageRoot, ".crust", "root", name);
|
|
309
|
+
}
|
|
310
|
+
//#endregion
|
|
261
311
|
//#region ../utils/src/process.ts
|
|
262
312
|
/** Resolve a bare executable name from PATH (and PATHEXT on Windows). */
|
|
263
313
|
function which(command) {
|
|
@@ -567,6 +617,12 @@ function resolveAgentPath(agent, scope, name) {
|
|
|
567
617
|
return join(cfg.globalSkillsDir(homedir()), name);
|
|
568
618
|
}
|
|
569
619
|
//#endregion
|
|
620
|
+
//#region ../utils/src/error.ts
|
|
621
|
+
/** Narrows a caught Node.js system error to its stable errno contract. */
|
|
622
|
+
function isErrnoException(error) {
|
|
623
|
+
return error instanceof Error && "code" in error && typeof error.code === "string";
|
|
624
|
+
}
|
|
625
|
+
//#endregion
|
|
570
626
|
//#region src/link.ts
|
|
571
627
|
/** Returns whether a symlink target carries Crust's skill ownership signature. */
|
|
572
628
|
function isOwnedSkillLink(target, name) {
|
|
@@ -593,47 +649,11 @@ function isValidSkillName(name) {
|
|
|
593
649
|
return name.length >= 1 && name.length <= 64 && SKILL_NAME_PATTERN.test(name);
|
|
594
650
|
}
|
|
595
651
|
//#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
652
|
//#region src/source.ts
|
|
653
|
+
/** The packaged skills root is missing or holds no skills: the CLI has not been built yet. */
|
|
603
654
|
var SkillSourceUnavailableError = class extends Error {
|
|
604
655
|
name = "SkillSourceUnavailableError";
|
|
605
656
|
};
|
|
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
657
|
function readSkillFrontmatter(sourceDir) {
|
|
638
658
|
let content;
|
|
639
659
|
try {
|
|
@@ -644,11 +664,20 @@ function readSkillFrontmatter(sourceDir) {
|
|
|
644
664
|
}
|
|
645
665
|
return requireSkillFrontmatter(probeFrontmatter(content), `Skill source directory "${sourceDir}"`);
|
|
646
666
|
}
|
|
647
|
-
/**
|
|
648
|
-
|
|
649
|
-
|
|
667
|
+
/**
|
|
668
|
+
* Reads every self-describing skill directory under `root`, the absolute
|
|
669
|
+
* packaged skills directory (normally `resolveArtifactDir("skills")`).
|
|
670
|
+
*/
|
|
671
|
+
function loadPackagedSkills(root) {
|
|
672
|
+
let entries;
|
|
673
|
+
try {
|
|
674
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
675
|
+
} catch (error) {
|
|
676
|
+
if (!isErrnoException(error) || error.code !== "ENOENT" && error.code !== "ENOTDIR") throw error;
|
|
677
|
+
throw new SkillSourceUnavailableError(`Packaged skills not found at "${root}". Run \`crust build\` first.`, { cause: error });
|
|
678
|
+
}
|
|
650
679
|
const skills = [];
|
|
651
|
-
for (const entry of
|
|
680
|
+
for (const entry of entries) {
|
|
652
681
|
if (!entry.isDirectory()) continue;
|
|
653
682
|
const sourceDir = join(root, entry.name);
|
|
654
683
|
if (!existsSync(join(sourceDir, "SKILL.md"))) continue;
|
|
@@ -660,7 +689,7 @@ function loadPackagedSkills(source) {
|
|
|
660
689
|
description: frontmatter.description
|
|
661
690
|
});
|
|
662
691
|
}
|
|
663
|
-
if (skills.length === 0) throw new SkillSourceUnavailableError(`
|
|
692
|
+
if (skills.length === 0) throw new SkillSourceUnavailableError(`Packaged skills at "${root}" do not contain any skill directories. Run \`crust build\` first.`);
|
|
664
693
|
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
665
694
|
}
|
|
666
695
|
//#endregion
|
|
@@ -680,7 +709,8 @@ async function pathExists(path) {
|
|
|
680
709
|
try {
|
|
681
710
|
await stat(path);
|
|
682
711
|
return true;
|
|
683
|
-
} catch {
|
|
712
|
+
} catch (error) {
|
|
713
|
+
if (!isErrnoException(error) || error.code !== "ENOENT") throw error;
|
|
684
714
|
return false;
|
|
685
715
|
}
|
|
686
716
|
}
|
|
@@ -688,7 +718,8 @@ async function inspectLink(outputDir, name, expectedSourceDir) {
|
|
|
688
718
|
let entry;
|
|
689
719
|
try {
|
|
690
720
|
entry = await lstat(outputDir);
|
|
691
|
-
} catch {
|
|
721
|
+
} catch (error) {
|
|
722
|
+
if (!isErrnoException(error) || error.code !== "ENOENT") throw error;
|
|
692
723
|
return { status: "absent" };
|
|
693
724
|
}
|
|
694
725
|
if (!entry.isSymbolicLink()) return { status: "conflict" };
|
|
@@ -760,7 +791,7 @@ async function uninstallSkill(options) {
|
|
|
760
791
|
}
|
|
761
792
|
return { agents: results };
|
|
762
793
|
}
|
|
763
|
-
/** Reports
|
|
794
|
+
/** Reports ownership and health. Only ENOENT is missing; other filesystem errors reject. */
|
|
764
795
|
async function getSkillStatus(options) {
|
|
765
796
|
const agents = options.agents ?? [...ALL_AGENTS];
|
|
766
797
|
const scope = resolveEffectiveScope(options.scope ?? "global");
|
|
@@ -803,9 +834,10 @@ function planReconcile(options) {
|
|
|
803
834
|
//#endregion
|
|
804
835
|
//#region src/extension.ts
|
|
805
836
|
const SKILLS = defineExtensionId("crust:skills");
|
|
806
|
-
const DEFAULT_SKILL_COMMAND_NAME = "
|
|
837
|
+
const DEFAULT_SKILL_COMMAND_NAME = "skills";
|
|
807
838
|
const SKILLS_SECTION_TITLE = "Agent skills";
|
|
808
839
|
const DEFAULT_SKILL_SCOPE = "global";
|
|
840
|
+
const SKILLS_ARTIFACT = "skills";
|
|
809
841
|
async function resolveScope(rawScope, options) {
|
|
810
842
|
if (rawScope) return rawScope;
|
|
811
843
|
if (options.defaultScope) return options.defaultScope;
|
|
@@ -855,7 +887,7 @@ async function repairInstalledSkill(packagedSkill, scope, io, report = false) {
|
|
|
855
887
|
async function autoRepairSkills(options, io) {
|
|
856
888
|
let skills;
|
|
857
889
|
try {
|
|
858
|
-
skills = loadPackagedSkills(
|
|
890
|
+
skills = loadPackagedSkills(resolveArtifactDir(SKILLS_ARTIFACT));
|
|
859
891
|
} catch (error) {
|
|
860
892
|
if (!(error instanceof SkillSourceUnavailableError)) io.stderr(yellow(`Skipping skill link repair: ${error instanceof Error ? error.message : String(error)}`));
|
|
861
893
|
return;
|
|
@@ -868,27 +900,28 @@ async function autoRepairSkills(options, io) {
|
|
|
868
900
|
io.stderr(yellow(`Skipping skill link repair [${packagedSkill.name}]: ${error instanceof Error ? error.message : String(error)}`));
|
|
869
901
|
}
|
|
870
902
|
}
|
|
871
|
-
function formatSkillDocumentation(
|
|
903
|
+
function formatSkillDocumentation(commandName, appName) {
|
|
872
904
|
try {
|
|
873
|
-
return loadPackagedSkills(
|
|
905
|
+
return loadPackagedSkills(resolveArtifactDir(SKILLS_ARTIFACT)).map((packagedSkill) => {
|
|
874
906
|
const sourcePath = isWithin(process.cwd(), packagedSkill.sourceDir) ? relative(process.cwd(), packagedSkill.sourceDir) || "." : packagedSkill.sourceDir;
|
|
875
907
|
return `${packagedSkill.name} — ${packagedSkill.description}\n Source: ${sourcePath}`;
|
|
876
908
|
}).join("\n\n");
|
|
877
909
|
} catch (error) {
|
|
878
|
-
if (error instanceof SkillSourceUnavailableError) return
|
|
910
|
+
if (error instanceof SkillSourceUnavailableError) return `${error.message} Then run \`${appName} ${commandName}\` to link packaged skills into an agent directory.`;
|
|
879
911
|
return `Packaged skills could not be read. Run \`${appName} ${commandName}\` for details.`;
|
|
880
912
|
}
|
|
881
913
|
}
|
|
882
914
|
async function buildSkills(options, context) {
|
|
883
|
-
const {
|
|
884
|
-
|
|
885
|
-
outDir: join(context.outDir, "skills"),
|
|
915
|
+
const { renderSkills } = await Promise.resolve().then(() => build_exports);
|
|
916
|
+
return (await renderSkills(options.generated === false ? void 0 : context.snapshot, {
|
|
886
917
|
version: context.snapshot.meta.version,
|
|
887
918
|
name: options.name,
|
|
888
919
|
description: options.description,
|
|
889
920
|
extras: options.extras
|
|
890
|
-
}
|
|
891
|
-
|
|
921
|
+
})).map((file) => ({
|
|
922
|
+
path: join(SKILLS_ARTIFACT, file.path),
|
|
923
|
+
content: file.content
|
|
924
|
+
}));
|
|
892
925
|
}
|
|
893
926
|
const skill = defineExtension(SKILLS, (options) => {
|
|
894
927
|
const commandName = options.command ?? DEFAULT_SKILL_COMMAND_NAME;
|
|
@@ -897,7 +930,7 @@ const skill = defineExtension(SKILLS, (options) => {
|
|
|
897
930
|
sections: (snapshot) => [{
|
|
898
931
|
command: [],
|
|
899
932
|
title: SKILLS_SECTION_TITLE,
|
|
900
|
-
body: formatSkillDocumentation(
|
|
933
|
+
body: formatSkillDocumentation(commandName, snapshot.meta.name),
|
|
901
934
|
except: [SKILLS]
|
|
902
935
|
}],
|
|
903
936
|
build: (context) => buildSkills(options, context),
|
|
@@ -1002,7 +1035,10 @@ async function reconcileSkill(opts) {
|
|
|
1002
1035
|
if (toInstall.length === 0 && toUninstall.length === 0) io.stdout(dim(`No changes [${packagedSkill.name}].`));
|
|
1003
1036
|
}
|
|
1004
1037
|
function buildSkillCommand(commandName, options) {
|
|
1005
|
-
return defineCommand(commandName, {
|
|
1038
|
+
return defineCommand(commandName, {
|
|
1039
|
+
description: "Manage agent skill installations",
|
|
1040
|
+
aliases: commandName === DEFAULT_SKILL_COMMAND_NAME ? ["skill"] : []
|
|
1041
|
+
}, (command) => command.flags({
|
|
1006
1042
|
name: "scope",
|
|
1007
1043
|
type: "string",
|
|
1008
1044
|
choices: ["project", "global"],
|
|
@@ -1018,11 +1054,11 @@ function buildSkillCommand(commandName, options) {
|
|
|
1018
1054
|
description: "Update scope (project or global)"
|
|
1019
1055
|
}).action(async (context) => {
|
|
1020
1056
|
const scope = await resolveScope(context.flags.scope, options);
|
|
1021
|
-
for (const packagedSkill of loadPackagedSkills(
|
|
1057
|
+
for (const packagedSkill of loadPackagedSkills(resolveArtifactDir(SKILLS_ARTIFACT))) await repairInstalledSkill(packagedSkill, scope, context, true);
|
|
1022
1058
|
}))).action(async (context) => {
|
|
1023
1059
|
const installAll = context.flags.all === true;
|
|
1024
1060
|
const scope = installAll ? context.flags.scope ?? options.defaultScope ?? DEFAULT_SKILL_SCOPE : await resolveScope(context.flags.scope, options);
|
|
1025
|
-
for (const packagedSkill of loadPackagedSkills(
|
|
1061
|
+
for (const packagedSkill of loadPackagedSkills(resolveArtifactDir(SKILLS_ARTIFACT))) await reconcileSkill({
|
|
1026
1062
|
packagedSkill,
|
|
1027
1063
|
scope,
|
|
1028
1064
|
installAll,
|
|
@@ -1065,7 +1101,7 @@ function normalizeArg(arg) {
|
|
|
1065
1101
|
variadic: arg.variadic
|
|
1066
1102
|
};
|
|
1067
1103
|
if (arg.description !== void 0) result.description = arg.description;
|
|
1068
|
-
if (arg.default !== void 0) result.default =
|
|
1104
|
+
if (arg.default !== void 0) result.default = formatDefault(arg.default);
|
|
1069
1105
|
return result;
|
|
1070
1106
|
}
|
|
1071
1107
|
function normalizeFlag(flag) {
|
|
@@ -1077,15 +1113,12 @@ function normalizeFlag(flag) {
|
|
|
1077
1113
|
multiple: flag.multiple
|
|
1078
1114
|
};
|
|
1079
1115
|
if (flag.description !== void 0) result.description = flag.description;
|
|
1080
|
-
if (flag.default !== void 0) result.default =
|
|
1116
|
+
if (flag.default !== void 0) result.default = formatDefault(flag.default);
|
|
1081
1117
|
return result;
|
|
1082
1118
|
}
|
|
1083
1119
|
function manifestType(type) {
|
|
1084
1120
|
return type === "number" || type === "boolean" ? type : "string";
|
|
1085
1121
|
}
|
|
1086
|
-
function serializeDefault(value) {
|
|
1087
|
-
return formatDefault(value);
|
|
1088
|
-
}
|
|
1089
1122
|
//#endregion
|
|
1090
1123
|
//#region src/render.ts
|
|
1091
1124
|
/**
|
|
@@ -1295,21 +1328,12 @@ function renderArgsTable(args) {
|
|
|
1295
1328
|
for (const arg of args) {
|
|
1296
1329
|
const name = arg.variadic ? `${arg.name}...` : arg.name;
|
|
1297
1330
|
const required = arg.required ? "Yes" : "No";
|
|
1298
|
-
const desc = escapeTableCell(
|
|
1331
|
+
const desc = escapeTableCell(formatFieldDescription(arg));
|
|
1299
1332
|
lines.push(`| \`${name}\` | ${arg.type} | ${required} | ${desc} |`);
|
|
1300
1333
|
}
|
|
1301
1334
|
return lines;
|
|
1302
1335
|
}
|
|
1303
1336
|
/**
|
|
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
1337
|
* Renders a markdown table for named flags.
|
|
1314
1338
|
*/
|
|
1315
1339
|
function renderFlagsTable(flags) {
|
|
@@ -1319,20 +1343,16 @@ function renderFlagsTable(flags) {
|
|
|
1319
1343
|
for (const flag of flags) {
|
|
1320
1344
|
const name = flag.spellings.map((spelling) => `\`${spelling}\``).join(", ");
|
|
1321
1345
|
const required = flag.required ? "Yes" : "No";
|
|
1322
|
-
const desc = escapeTableCell(
|
|
1346
|
+
const desc = escapeTableCell(formatFieldDescription(flag));
|
|
1323
1347
|
lines.push(`| ${name} | ${flag.type} | ${required} | ${desc} |`);
|
|
1324
1348
|
}
|
|
1325
1349
|
return lines;
|
|
1326
1350
|
}
|
|
1327
|
-
|
|
1328
|
-
* Formats the description cell for a flag, including default value
|
|
1329
|
-
* and multiplicity.
|
|
1330
|
-
*/
|
|
1331
|
-
function formatFlagDescription(flag) {
|
|
1351
|
+
function formatFieldDescription(field) {
|
|
1332
1352
|
const parts = [];
|
|
1333
|
-
if (
|
|
1334
|
-
if (
|
|
1335
|
-
if (
|
|
1353
|
+
if (field.description) parts.push(field.description);
|
|
1354
|
+
if ("multiple" in field && field.multiple) parts.push("Can be specified multiple times");
|
|
1355
|
+
if (field.default !== void 0) parts.push(`Default: \`${field.default}\``);
|
|
1336
1356
|
return parts.join(". ") || "-";
|
|
1337
1357
|
}
|
|
1338
1358
|
/**
|
|
@@ -1370,6 +1390,7 @@ function findNode(root, path) {
|
|
|
1370
1390
|
//#endregion
|
|
1371
1391
|
//#region src/build.ts
|
|
1372
1392
|
var build_exports = /* @__PURE__ */ __exportAll({
|
|
1393
|
+
renderSkills: () => renderSkills,
|
|
1373
1394
|
writeSkills: () => writeSkills,
|
|
1374
1395
|
writeSkillsFromSnapshot: () => writeSkillsFromSnapshot
|
|
1375
1396
|
});
|
|
@@ -1384,18 +1405,37 @@ async function writeSkillsFromSnapshot(snapshot, options) {
|
|
|
1384
1405
|
return await writeSkillSource(snapshot, options);
|
|
1385
1406
|
}
|
|
1386
1407
|
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
1408
|
const outDir = resolve(options.outDir);
|
|
1389
1409
|
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
1410
|
for (const sourceDir of options.extras ?? []) {
|
|
1393
1411
|
const resolved = resolveSourceDir(sourceDir);
|
|
1394
1412
|
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.`);
|
|
1413
|
+
}
|
|
1414
|
+
const files = await renderSkills(snapshot, options);
|
|
1415
|
+
const cwd = resolve(".");
|
|
1416
|
+
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.`);
|
|
1417
|
+
await rm(outDir, {
|
|
1418
|
+
recursive: true,
|
|
1419
|
+
force: true
|
|
1420
|
+
});
|
|
1421
|
+
for (const file of files) {
|
|
1422
|
+
const filePath = join(outDir, file.path);
|
|
1423
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
1424
|
+
await writeFile(filePath, file.content);
|
|
1425
|
+
}
|
|
1426
|
+
return files.map((file) => file.path);
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Renders generated and authored skills as `<skill>/<file>` paths relative to a
|
|
1430
|
+
* `skills` directory, without writing them.
|
|
1431
|
+
*/
|
|
1432
|
+
async function renderSkills(snapshot, options) {
|
|
1433
|
+
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.");
|
|
1434
|
+
const skills = /* @__PURE__ */ new Map();
|
|
1435
|
+
for (const sourceDir of options.extras ?? []) {
|
|
1395
1436
|
const bundle = await loadBundleFiles(sourceDir);
|
|
1396
1437
|
validateSkillName(bundle.frontmatter.name);
|
|
1397
|
-
if (
|
|
1398
|
-
authoredNames.add(bundle.frontmatter.name);
|
|
1438
|
+
if (skills.has(bundle.frontmatter.name)) throw new SkillSourceConflictError(bundle.frontmatter.name);
|
|
1399
1439
|
skills.set(bundle.frontmatter.name, bundle.files);
|
|
1400
1440
|
}
|
|
1401
1441
|
if (snapshot) {
|
|
@@ -1404,32 +1444,19 @@ async function writeSkillSource(snapshot, options) {
|
|
|
1404
1444
|
description: options.description ?? snapshot.meta.description ?? "",
|
|
1405
1445
|
version: options.version
|
|
1406
1446
|
};
|
|
1407
|
-
if (!
|
|
1447
|
+
if (!skills.has(generatedMeta.name)) {
|
|
1408
1448
|
validateSkillName(generatedMeta.name);
|
|
1409
1449
|
requireSkillFrontmatter(generatedMeta, `Skill "${generatedMeta.name}"`);
|
|
1410
1450
|
skills.set(generatedMeta.name, renderSkill(buildManifest(snapshot), generatedMeta));
|
|
1411
1451
|
}
|
|
1412
1452
|
}
|
|
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;
|
|
1453
|
+
return [...skills].flatMap(([name, files]) => files.map((file) => ({
|
|
1454
|
+
path: join(name, file.path),
|
|
1455
|
+
content: file.content
|
|
1456
|
+
})));
|
|
1422
1457
|
}
|
|
1423
1458
|
function validateSkillName(name) {
|
|
1424
1459
|
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
1460
|
}
|
|
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
1461
|
//#endregion
|
|
1435
|
-
export { SkillConflictError, SkillSourceConflictError, SkillSourceUnavailableError, detectInstalledAgents, getAdditionalAgents, getSkillStatus, getUniversalAgents, installSkill, isUniversalAgent, isValidSkillName, loadPackagedSkills,
|
|
1462
|
+
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.0",
|
|
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.2"
|
|
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.0",
|
|
55
|
+
"@crustjs/extensions": "0.3.0",
|
|
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.0",
|
|
62
62
|
"typescript": "^7.0.0"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|