@crustjs/skills 0.0.2 → 0.0.4
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 +77 -8
- package/dist/index.d.ts +143 -16
- package/dist/index.js +214 -168
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,6 +106,8 @@ crust skills generate ./src/cli.ts -n my-cli -d "My CLI" --no-clean
|
|
|
106
106
|
|
|
107
107
|
High-level API that runs the full pipeline: introspection, rendering, and writing to disk.
|
|
108
108
|
|
|
109
|
+
The `meta.name` must be a valid skill name — lowercase alphanumeric with hyphens, 1–64 characters (validated against the [Agent Skills spec](https://agentskills.io/specification) pattern). Use `isValidSkillName()` to check before calling.
|
|
110
|
+
|
|
109
111
|
```ts
|
|
110
112
|
import { generateSkill } from "@crustjs/skills";
|
|
111
113
|
|
|
@@ -114,6 +116,7 @@ const result = await generateSkill({
|
|
|
114
116
|
meta: { name: "my-cli", description: "My CLI tool", version: "1.0.0" },
|
|
115
117
|
outDir: "./dist", // default: "."
|
|
116
118
|
clean: true, // default: true — removes existing skill dir first
|
|
119
|
+
force: false, // default: false — throws SkillConflictError if dir exists without crust.json
|
|
117
120
|
});
|
|
118
121
|
|
|
119
122
|
// result.outputDir — absolute path to the generated skill directory
|
|
@@ -147,12 +150,61 @@ for (const file of files) {
|
|
|
147
150
|
}
|
|
148
151
|
```
|
|
149
152
|
|
|
153
|
+
### `isValidSkillName(name)`
|
|
154
|
+
|
|
155
|
+
Validates a skill name against the [Agent Skills spec](https://agentskills.io/specification) pattern: 1–64 lowercase alphanumeric characters and hyphens, no leading/trailing/consecutive hyphens.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { isValidSkillName } from "@crustjs/skills";
|
|
159
|
+
|
|
160
|
+
isValidSkillName("my-cli"); // true
|
|
161
|
+
isValidSkillName("My_CLI"); // false — uppercase and underscores not allowed
|
|
162
|
+
isValidSkillName("-leading"); // false — leading hyphen
|
|
163
|
+
isValidSkillName("a".repeat(65)); // false — exceeds 64 characters
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
> **Note:** `generateSkill()` automatically validates `meta.name` (after prepending `use-`) and throws a descriptive error if the name is invalid.
|
|
167
|
+
|
|
168
|
+
## Skill Metadata
|
|
169
|
+
|
|
170
|
+
The `SkillMeta` object controls the generated `SKILL.md` frontmatter. Beyond the required `name`, `description`, and `version` fields, several optional fields are supported:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const meta: SkillMeta = {
|
|
174
|
+
name: "my-cli",
|
|
175
|
+
description: "CLI tool for managing widgets",
|
|
176
|
+
version: "1.0.0",
|
|
177
|
+
|
|
178
|
+
// Optional fields — emitted in SKILL.md YAML frontmatter when set
|
|
179
|
+
allowedTools: "Bash(my-cli *) Read Grep", // Pre-approved tools (avoids per-use prompts)
|
|
180
|
+
license: "MIT", // License name or reference
|
|
181
|
+
compatibility: "Requires my-cli on PATH", // Environment requirements (max 500 chars)
|
|
182
|
+
disableModelInvocation: false, // true = agent won't auto-load; user must invoke manually
|
|
183
|
+
};
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
| Field | Frontmatter Key | Description |
|
|
187
|
+
| ----- | --------------- | ----------- |
|
|
188
|
+
| `allowedTools` | `allowed-tools` | Space-delimited list of pre-approved tools (e.g. `Bash(my-cli *) Read Grep`) |
|
|
189
|
+
| `license` | `license` | License name or file reference |
|
|
190
|
+
| `compatibility` | `compatibility` | Environment requirements or compatibility notes |
|
|
191
|
+
| `disableModelInvocation` | `disable-model-invocation` | When `true`, prevents agents from auto-loading the skill |
|
|
192
|
+
|
|
193
|
+
## Escaping
|
|
194
|
+
|
|
195
|
+
The renderer automatically handles special characters in generated output:
|
|
196
|
+
|
|
197
|
+
- **YAML frontmatter**: Values containing YAML-special characters (`:`, `#`, `*`, `!`, `[`, `{`, `'`, `"`, etc.) are wrapped in double quotes with internal quotes escaped.
|
|
198
|
+
- **Markdown tables**: Literal `|` characters in argument/flag descriptions are escaped as `\|` to prevent broken table rendering.
|
|
199
|
+
|
|
200
|
+
No manual escaping is needed — pass raw values and the renderer handles the rest.
|
|
201
|
+
|
|
150
202
|
## Output Structure
|
|
151
203
|
|
|
152
|
-
Generated output goes to `<outDir>/skills
|
|
204
|
+
Generated output goes to `<outDir>/skills/use-<name>/` (the `use-` prefix is applied automatically):
|
|
153
205
|
|
|
154
206
|
```
|
|
155
|
-
skills/my-cli/
|
|
207
|
+
skills/use-my-cli/
|
|
156
208
|
SKILL.md # Entrypoint — loaded by the agent
|
|
157
209
|
command-index.md # Maps all commands to documentation file paths
|
|
158
210
|
commands/ # Per-command documentation mirroring the CLI hierarchy
|
|
@@ -161,8 +213,7 @@ skills/my-cli/
|
|
|
161
213
|
db/
|
|
162
214
|
migrate.md # Nested subcommand
|
|
163
215
|
seed.md
|
|
164
|
-
|
|
165
|
-
README.md # Install instructions for consumers
|
|
216
|
+
crust.json # Machine-readable bundle metadata (Crust ownership marker)
|
|
166
217
|
```
|
|
167
218
|
|
|
168
219
|
### File Details
|
|
@@ -172,8 +223,26 @@ skills/my-cli/
|
|
|
172
223
|
| `SKILL.md` | Agent entrypoint with YAML frontmatter. Directs agents to load specific command files on demand (lazy loading). |
|
|
173
224
|
| `command-index.md` | Markdown table listing every command, its type (runnable/group), and documentation path. |
|
|
174
225
|
| `commands/*.md` | Per-command reference files. Leaf commands include usage, arguments, flags, defaults, and aliases. Group commands list subcommands with links. |
|
|
175
|
-
| `
|
|
176
|
-
|
|
226
|
+
| `crust.json` | Crust-specific JSON metadata: name, description, version, entrypoint, and list of all command paths. Also serves as an ownership marker — its presence indicates the skill was generated by Crust. |
|
|
227
|
+
|
|
228
|
+
## Conflict Detection
|
|
229
|
+
|
|
230
|
+
Each skill directory contains a `crust.json` file that acts as an ownership marker. If `generateSkill()` encounters an existing directory without `crust.json`, it throws a `SkillConflictError` to prevent overwriting skills created manually or by other tools.
|
|
231
|
+
|
|
232
|
+
Pass `force: true` to overwrite, or handle the error:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
import { generateSkill, SkillConflictError } from "@crustjs/skills";
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
await generateSkill({ command, meta, agents });
|
|
239
|
+
} catch (err) {
|
|
240
|
+
if (err instanceof SkillConflictError) {
|
|
241
|
+
console.error(`Conflict: ${err.details.outputDir}`);
|
|
242
|
+
// err.details.agent — the agent where the conflict occurred
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
177
246
|
|
|
178
247
|
## Installing Generated Skills
|
|
179
248
|
|
|
@@ -182,13 +251,13 @@ After generating a skill bundle, consumers can install it by copying the skill d
|
|
|
182
251
|
### OpenCode
|
|
183
252
|
|
|
184
253
|
```sh
|
|
185
|
-
cp -r skills/my-cli/ .opencode/skills/my-cli/
|
|
254
|
+
cp -r skills/use-my-cli/ .opencode/skills/use-my-cli/
|
|
186
255
|
```
|
|
187
256
|
|
|
188
257
|
### Claude Code
|
|
189
258
|
|
|
190
259
|
```sh
|
|
191
|
-
cp -r skills/my-cli/ .claude/skills/my-cli/
|
|
260
|
+
cp -r skills/use-my-cli/ .claude/skills/use-my-cli/
|
|
192
261
|
```
|
|
193
262
|
|
|
194
263
|
The agent will discover the skill from `SKILL.md` and load command documentation on demand from the `commands/` directory.
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { AnyCommand } from "@crustjs/core";
|
|
|
3
3
|
* Metadata for the generated skill bundle.
|
|
4
4
|
*
|
|
5
5
|
* This information populates the `SKILL.md` frontmatter and distribution
|
|
6
|
-
* metadata files (`
|
|
6
|
+
* metadata files (`crust.json`).
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
9
9
|
* ```ts
|
|
@@ -12,15 +12,61 @@ import { AnyCommand } from "@crustjs/core";
|
|
|
12
12
|
* description: "CLI tool for managing widgets",
|
|
13
13
|
* version: "1.0.0",
|
|
14
14
|
* };
|
|
15
|
+
* // generateSkill() will output to `use-my-cli/` with name "use-my-cli"
|
|
15
16
|
* ```
|
|
16
17
|
*/
|
|
17
18
|
interface SkillMeta {
|
|
18
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Skill name — the user-facing CLI name (e.g. `"my-cli"`).
|
|
21
|
+
*
|
|
22
|
+
* `generateSkill()`, `uninstallSkill()`, and `skillStatus()` automatically
|
|
23
|
+
* prefix `use-` to this name for output directory paths, SKILL.md frontmatter,
|
|
24
|
+
* and crust.json metadata. For example, `name: "my-cli"` produces output
|
|
25
|
+
* under `use-my-cli/`.
|
|
26
|
+
*
|
|
27
|
+
* The resolved name (with `use-` prefix) must conform to the Agent Skills
|
|
28
|
+
* spec: 1–64 lowercase alphanumeric characters and hyphens, no leading/
|
|
29
|
+
* trailing/consecutive hyphens.
|
|
30
|
+
*/
|
|
19
31
|
name: string;
|
|
20
32
|
/** Human-readable description of what the CLI does */
|
|
21
33
|
description: string;
|
|
22
34
|
/** Version string for the generated skill bundle */
|
|
23
35
|
version: string;
|
|
36
|
+
/**
|
|
37
|
+
* License name or reference to a bundled license file.
|
|
38
|
+
*
|
|
39
|
+
* Emitted in SKILL.md YAML frontmatter as `license:`.
|
|
40
|
+
*/
|
|
41
|
+
license?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Environment requirements or compatibility notes (max 500 chars per spec).
|
|
44
|
+
*
|
|
45
|
+
* Indicates intended product, required system packages, network access, etc.
|
|
46
|
+
* Emitted in SKILL.md YAML frontmatter as `compatibility:`.
|
|
47
|
+
*
|
|
48
|
+
* @example "Requires deploy-cli installed on PATH"
|
|
49
|
+
*/
|
|
50
|
+
compatibility?: string;
|
|
51
|
+
/**
|
|
52
|
+
* When `true`, prevents agents from automatically loading this skill.
|
|
53
|
+
* Users must invoke it manually with `/skill-name`.
|
|
54
|
+
*
|
|
55
|
+
* Emitted in SKILL.md YAML frontmatter as `disable-model-invocation: true`.
|
|
56
|
+
* @default false
|
|
57
|
+
*/
|
|
58
|
+
disableModelInvocation?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Space-delimited list of pre-approved tools the skill may use.
|
|
61
|
+
*
|
|
62
|
+
* For CLI skills, setting this to `Bash(<cli-name> *)` allows agents to
|
|
63
|
+
* execute the CLI without per-use permission prompts.
|
|
64
|
+
*
|
|
65
|
+
* Emitted in SKILL.md YAML frontmatter as `allowed-tools:`.
|
|
66
|
+
*
|
|
67
|
+
* @example "Bash(my-cli *) Read Grep"
|
|
68
|
+
*/
|
|
69
|
+
allowedTools?: string;
|
|
24
70
|
}
|
|
25
71
|
/** Supported agent targets for skill installation. */
|
|
26
72
|
type AgentTarget = "claude-code" | "opencode";
|
|
@@ -29,6 +75,10 @@ type Scope = "global" | "project";
|
|
|
29
75
|
/**
|
|
30
76
|
* Top-level options for generating a skill bundle from a command tree.
|
|
31
77
|
*
|
|
78
|
+
* The `meta.name` value is automatically prefixed with `use-` for all output
|
|
79
|
+
* paths and metadata. For example, `name: "my-cli"` produces skill directories
|
|
80
|
+
* named `use-my-cli/` and sets the manifest/frontmatter name to `"use-my-cli"`.
|
|
81
|
+
*
|
|
32
82
|
* @example
|
|
33
83
|
* ```ts
|
|
34
84
|
* import { generateSkill } from "@crustjs/skills";
|
|
@@ -37,7 +87,7 @@ type Scope = "global" | "project";
|
|
|
37
87
|
* await generateSkill({
|
|
38
88
|
* command: rootCommand,
|
|
39
89
|
* meta: {
|
|
40
|
-
* name: "my-cli",
|
|
90
|
+
* name: "my-cli", // output: use-my-cli/
|
|
41
91
|
* description: "CLI tool for managing widgets",
|
|
42
92
|
* version: "1.0.0",
|
|
43
93
|
* },
|
|
@@ -63,6 +113,13 @@ interface GenerateOptions {
|
|
|
63
113
|
* @default true
|
|
64
114
|
*/
|
|
65
115
|
clean?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* When `true`, overwrite an existing skill directory even if it was not
|
|
118
|
+
* created by Crust (i.e. has no `crust.json`). Without this flag, a
|
|
119
|
+
* conflict throws a {@link SkillConflictError}.
|
|
120
|
+
* @default false
|
|
121
|
+
*/
|
|
122
|
+
force?: boolean;
|
|
66
123
|
}
|
|
67
124
|
/** Status of an individual agent installation. */
|
|
68
125
|
type InstallStatus = "installed" | "updated" | "up-to-date";
|
|
@@ -145,11 +202,12 @@ interface StatusResult {
|
|
|
145
202
|
* new version is detected. Set `autoInstall: true` to also install skills that
|
|
146
203
|
* are not yet present.
|
|
147
204
|
*
|
|
148
|
-
* **Interactive command
|
|
149
|
-
*
|
|
205
|
+
* **Interactive command** (default): registers a `skill` subcommand that
|
|
206
|
+
* presents a single multiselect prompt for toggling agent installations.
|
|
207
|
+
* Set `command: false` to disable the command.
|
|
150
208
|
*/
|
|
151
209
|
interface SkillPluginOptions {
|
|
152
|
-
/** Skill version string — compared against the installed
|
|
210
|
+
/** Skill version string — compared against the installed crust.json */
|
|
153
211
|
version: string;
|
|
154
212
|
/**
|
|
155
213
|
* Installation scope.
|
|
@@ -170,9 +228,16 @@ interface SkillPluginOptions {
|
|
|
170
228
|
autoUpdate?: boolean;
|
|
171
229
|
/**
|
|
172
230
|
* Register an interactive skill management subcommand on the root command.
|
|
231
|
+
*
|
|
232
|
+
* The command presents a single multiselect prompt listing all detected
|
|
233
|
+
* agents with their current installation status pre-filled. The user
|
|
234
|
+
* toggles agents on/off and the system reconciles the desired state:
|
|
235
|
+
* newly selected agents are installed, deselected agents are uninstalled,
|
|
236
|
+
* and already-correct agents are skipped.
|
|
237
|
+
*
|
|
173
238
|
* - `true`: register with default name `"skill"`
|
|
174
239
|
* - `string`: register with a custom command name
|
|
175
|
-
* @default
|
|
240
|
+
* @default true
|
|
176
241
|
*/
|
|
177
242
|
command?: boolean | string;
|
|
178
243
|
}
|
|
@@ -201,18 +266,78 @@ interface SkillPluginOptions {
|
|
|
201
266
|
* ```
|
|
202
267
|
*/
|
|
203
268
|
declare function detectInstalledAgents(home?: string): Promise<AgentTarget[]>;
|
|
269
|
+
/** Details about the conflict between an existing skill and an incoming one. */
|
|
270
|
+
interface SkillConflictDetails {
|
|
271
|
+
/** The agent where the conflict was detected */
|
|
272
|
+
agent: AgentTarget;
|
|
273
|
+
/** Absolute path to the conflicting skill directory */
|
|
274
|
+
outputDir: string;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Thrown when `generateSkill()` detects that the target skill directory
|
|
278
|
+
* already exists but was not created by Crust (i.e. has no `crust.json`).
|
|
279
|
+
*
|
|
280
|
+
* This prevents Crust from silently overwriting a skill that was manually
|
|
281
|
+
* created or installed by another tool.
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ```ts
|
|
285
|
+
* import { generateSkill, SkillConflictError } from "@crustjs/skills";
|
|
286
|
+
*
|
|
287
|
+
* try {
|
|
288
|
+
* await generateSkill({ command, meta, agents });
|
|
289
|
+
* } catch (err) {
|
|
290
|
+
* if (err instanceof SkillConflictError) {
|
|
291
|
+
* console.error(
|
|
292
|
+
* `Conflict: "${err.details.outputDir}" already exists and was not created by Crust.`,
|
|
293
|
+
* );
|
|
294
|
+
* }
|
|
295
|
+
* }
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
declare class SkillConflictError extends Error {
|
|
299
|
+
readonly name = "SkillConflictError";
|
|
300
|
+
readonly details: SkillConflictDetails;
|
|
301
|
+
constructor(details: SkillConflictDetails);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Validates a resolved skill name against the Agent Skills specification.
|
|
305
|
+
*
|
|
306
|
+
* @param name - The resolved skill name to validate (already has `use-` prefix)
|
|
307
|
+
* @returns `true` if valid, `false` otherwise
|
|
308
|
+
*/
|
|
309
|
+
declare function isValidSkillName(name: string): boolean;
|
|
310
|
+
/**
|
|
311
|
+
* Resolves the canonical skill name by applying the `use-` prefix.
|
|
312
|
+
*
|
|
313
|
+
* All generated output (directory names, crust.json metadata, SKILL.md content)
|
|
314
|
+
* uses the resolved name. Consumers pass the raw CLI name (e.g. `"my-cli"`),
|
|
315
|
+
* and this function returns the prefixed form (e.g. `"use-my-cli"`).
|
|
316
|
+
*
|
|
317
|
+
* @param name - The raw CLI tool name
|
|
318
|
+
* @returns The prefixed skill name
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* ```ts
|
|
322
|
+
* resolveSkillName("my-cli"); // "use-my-cli"
|
|
323
|
+
* ```
|
|
324
|
+
*/
|
|
325
|
+
declare function resolveSkillName(name: string): string;
|
|
204
326
|
/**
|
|
205
327
|
* Generates and installs agent skill bundles from a Crust command tree.
|
|
206
328
|
*
|
|
207
329
|
* For each target agent:
|
|
208
330
|
* 1. Resolves the output directory via {@link resolveAgentPath}
|
|
209
|
-
* 2. Checks the
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
331
|
+
* 2. Checks for conflicts — if the directory exists but has no `crust.json`,
|
|
332
|
+
* it was not created by Crust and a {@link SkillConflictError} is thrown
|
|
333
|
+
* 3. Checks the installed version — skips if up-to-date
|
|
334
|
+
* 4. Builds a canonical manifest from the command tree
|
|
335
|
+
* 5. Renders markdown files + `crust.json`
|
|
336
|
+
* 6. Writes files to the agent's skill directory
|
|
213
337
|
*
|
|
214
338
|
* @param options - Generation options including command, metadata, agents, and scope
|
|
215
339
|
* @returns Per-agent installation results
|
|
340
|
+
* @throws {SkillConflictError} If the output directory exists but was not created by Crust
|
|
216
341
|
*
|
|
217
342
|
* @example
|
|
218
343
|
* ```ts
|
|
@@ -264,10 +389,12 @@ import { CrustPlugin } from "@crustjs/core";
|
|
|
264
389
|
* new version is detected. Set `autoInstall: true` to also install skills that
|
|
265
390
|
* are not yet present.
|
|
266
391
|
*
|
|
267
|
-
* **Interactive command
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
392
|
+
* **Interactive command** (default): registers a `skill` subcommand that
|
|
393
|
+
* presents a single multiselect prompt for toggling agent installations.
|
|
394
|
+
* Detected agents are shown with their current installation status pre-filled.
|
|
395
|
+
* The system reconciles the desired state: newly selected agents are installed,
|
|
396
|
+
* deselected agents are uninstalled, and already-correct agents are skipped.
|
|
397
|
+
* Set `command: false` to disable command injection.
|
|
271
398
|
*
|
|
272
399
|
* @param options - Plugin configuration with version and scope
|
|
273
400
|
* @returns A `CrustPlugin` to register in a command's `plugins` array
|
|
@@ -291,4 +418,4 @@ import { CrustPlugin } from "@crustjs/core";
|
|
|
291
418
|
* ```
|
|
292
419
|
*/
|
|
293
420
|
declare function skillPlugin(options: SkillPluginOptions): CrustPlugin;
|
|
294
|
-
export { uninstallSkill, skillStatus, skillPlugin, generateSkill, detectInstalledAgents, UninstallStatus, UninstallResult, UninstallOptions, StatusResult, StatusOptions, SkillPluginOptions, SkillMeta, Scope, InstallStatus, GenerateResult, GenerateOptions, AgentTarget, AgentResult };
|
|
421
|
+
export { uninstallSkill, skillStatus, skillPlugin, resolveSkillName, isValidSkillName, generateSkill, detectInstalledAgents, UninstallStatus, UninstallResult, UninstallOptions, StatusResult, StatusOptions, SkillPluginOptions, SkillMeta, SkillConflictError, SkillConflictDetails, Scope, InstallStatus, GenerateResult, GenerateOptions, AgentTarget, AgentResult };
|
package/dist/index.js
CHANGED
|
@@ -40,6 +40,16 @@ function resolveAgentConfigDir(home, agent) {
|
|
|
40
40
|
return join(home, ".config", "opencode");
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
// src/errors.ts
|
|
44
|
+
class SkillConflictError extends Error {
|
|
45
|
+
name = "SkillConflictError";
|
|
46
|
+
details;
|
|
47
|
+
constructor(details) {
|
|
48
|
+
const message = `Skill conflict for agent "${details.agent}": ` + `directory "${details.outputDir}" already exists but was not created by Crust ` + `(no crust.json found). Delete or rename the conflicting skill to resolve.`;
|
|
49
|
+
super(message);
|
|
50
|
+
this.details = details;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
43
53
|
// src/generate.ts
|
|
44
54
|
import { access as access2, mkdir, rm, writeFile } from "fs/promises";
|
|
45
55
|
import { dirname, join as join3 } from "path";
|
|
@@ -133,6 +143,15 @@ function serializeDefault(value) {
|
|
|
133
143
|
}
|
|
134
144
|
|
|
135
145
|
// src/render.ts
|
|
146
|
+
function escapeYaml(value) {
|
|
147
|
+
if (/[:#[\]{}&*!|>'"`,@?\\]|^\s|\s$|^---|[\n\r]/.test(value)) {
|
|
148
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r")}"`;
|
|
149
|
+
}
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
function escapeTableCell(value) {
|
|
153
|
+
return value.replace(/(?<!\\)\|/g, "\\|");
|
|
154
|
+
}
|
|
136
155
|
function renderSkill(manifest, meta) {
|
|
137
156
|
const files = [];
|
|
138
157
|
const allNodes = collectNodes(manifest);
|
|
@@ -186,8 +205,20 @@ function relativePath(from, to) {
|
|
|
186
205
|
function renderSkillMd(manifest, meta) {
|
|
187
206
|
const lines = [];
|
|
188
207
|
lines.push("---");
|
|
189
|
-
lines.push(`name: ${meta.name}`);
|
|
190
|
-
lines.push(`description: ${meta.description}`);
|
|
208
|
+
lines.push(`name: ${escapeYaml(meta.name)}`);
|
|
209
|
+
lines.push(`description: ${escapeYaml(meta.description)}`);
|
|
210
|
+
if (meta.license) {
|
|
211
|
+
lines.push(`license: ${escapeYaml(meta.license)}`);
|
|
212
|
+
}
|
|
213
|
+
if (meta.compatibility) {
|
|
214
|
+
lines.push(`compatibility: ${escapeYaml(meta.compatibility)}`);
|
|
215
|
+
}
|
|
216
|
+
if (meta.disableModelInvocation) {
|
|
217
|
+
lines.push("disable-model-invocation: true");
|
|
218
|
+
}
|
|
219
|
+
if (meta.allowedTools) {
|
|
220
|
+
lines.push(`allowed-tools: ${escapeYaml(meta.allowedTools)}`);
|
|
221
|
+
}
|
|
191
222
|
lines.push("metadata:");
|
|
192
223
|
lines.push(` version: "${meta.version}"`);
|
|
193
224
|
lines.push("---");
|
|
@@ -198,13 +229,15 @@ function renderSkillMd(manifest, meta) {
|
|
|
198
229
|
lines.push(manifest.description);
|
|
199
230
|
lines.push("");
|
|
200
231
|
}
|
|
201
|
-
|
|
232
|
+
const cliName = meta.name.startsWith("use-") ? meta.name.slice(4) : meta.name;
|
|
233
|
+
lines.push(`Use this skill when working with \`${cliName}\` commands, or when you need help with \`${cliName}\` syntax, flags, or subcommands.`);
|
|
202
234
|
lines.push("");
|
|
203
|
-
lines.push("
|
|
235
|
+
lines.push("## Command Reference");
|
|
204
236
|
lines.push("");
|
|
205
|
-
lines.push(`For
|
|
237
|
+
lines.push(`For the full list of commands and their documentation paths, see [command-index.md](command-index.md). ` + "**Do not read all command files at once.** Instead:");
|
|
206
238
|
lines.push("");
|
|
207
|
-
lines.push("
|
|
239
|
+
lines.push("1. Check [command-index.md](command-index.md) to find the relevant command");
|
|
240
|
+
lines.push("2. Read only the specific file from the `commands/` directory that you need");
|
|
208
241
|
lines.push("");
|
|
209
242
|
if (manifest.children.length > 0) {
|
|
210
243
|
lines.push("## Available Commands");
|
|
@@ -358,7 +391,7 @@ function renderArgsTable(args) {
|
|
|
358
391
|
for (const arg of args) {
|
|
359
392
|
const name = arg.variadic ? `${arg.name}...` : arg.name;
|
|
360
393
|
const required = arg.required ? "Yes" : "No";
|
|
361
|
-
const desc = formatArgDescription(arg);
|
|
394
|
+
const desc = escapeTableCell(formatArgDescription(arg));
|
|
362
395
|
lines.push(`| \`${name}\` | ${arg.type} | ${required} | ${desc} |`);
|
|
363
396
|
}
|
|
364
397
|
return lines;
|
|
@@ -380,7 +413,7 @@ function renderFlagsTable(flags) {
|
|
|
380
413
|
for (const flag of flags) {
|
|
381
414
|
const name = formatFlagName(flag);
|
|
382
415
|
const required = flag.required ? "Yes" : "No";
|
|
383
|
-
const desc = formatFlagDescription(flag);
|
|
416
|
+
const desc = escapeTableCell(formatFlagDescription(flag));
|
|
384
417
|
lines.push(`| ${name} | ${flag.type} | ${required} | ${desc} |`);
|
|
385
418
|
}
|
|
386
419
|
return lines;
|
|
@@ -450,9 +483,10 @@ function arraysEqual(a, b) {
|
|
|
450
483
|
// src/version.ts
|
|
451
484
|
import { readFile } from "fs/promises";
|
|
452
485
|
import { join as join2 } from "path";
|
|
486
|
+
var CRUST_MANIFEST = "crust.json";
|
|
453
487
|
async function readInstalledVersion(dir) {
|
|
454
488
|
try {
|
|
455
|
-
const raw = await readFile(join2(dir,
|
|
489
|
+
const raw = await readFile(join2(dir, CRUST_MANIFEST), "utf-8");
|
|
456
490
|
const parsed = JSON.parse(raw);
|
|
457
491
|
if (typeof parsed === "object" && parsed !== null && "version" in parsed && typeof parsed.version === "string") {
|
|
458
492
|
return parsed.version;
|
|
@@ -462,28 +496,47 @@ async function readInstalledVersion(dir) {
|
|
|
462
496
|
return null;
|
|
463
497
|
}
|
|
464
498
|
}
|
|
465
|
-
async function checkVersion(dir, newVersion) {
|
|
466
|
-
const installed = await readInstalledVersion(dir);
|
|
467
|
-
if (installed === null) {
|
|
468
|
-
return { status: "installed", installedVersion: null };
|
|
469
|
-
}
|
|
470
|
-
if (installed !== newVersion) {
|
|
471
|
-
return { status: "updated", installedVersion: installed };
|
|
472
|
-
}
|
|
473
|
-
return { status: "up-to-date", installedVersion: installed };
|
|
474
|
-
}
|
|
475
499
|
|
|
476
500
|
// src/generate.ts
|
|
501
|
+
var SKILL_NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
502
|
+
function isValidSkillName(name) {
|
|
503
|
+
return name.length >= 1 && name.length <= 64 && SKILL_NAME_PATTERN.test(name);
|
|
504
|
+
}
|
|
505
|
+
function resolveSkillName(name) {
|
|
506
|
+
return name.startsWith("use-") ? name : `use-${name}`;
|
|
507
|
+
}
|
|
477
508
|
async function generateSkill(options) {
|
|
478
|
-
const {
|
|
509
|
+
const {
|
|
510
|
+
command,
|
|
511
|
+
meta,
|
|
512
|
+
agents,
|
|
513
|
+
scope = "global",
|
|
514
|
+
clean = true,
|
|
515
|
+
force = false
|
|
516
|
+
} = options;
|
|
517
|
+
const resolvedName = resolveSkillName(meta.name);
|
|
518
|
+
if (!isValidSkillName(resolvedName)) {
|
|
519
|
+
throw new Error(`Invalid skill name "${resolvedName}": must be 1\u201364 lowercase ` + `alphanumeric characters and hyphens, no leading/trailing/consecutive ` + `hyphens. Pattern: ${SKILL_NAME_PATTERN.source}`);
|
|
520
|
+
}
|
|
521
|
+
const resolvedMeta = {
|
|
522
|
+
...meta,
|
|
523
|
+
name: resolvedName
|
|
524
|
+
};
|
|
479
525
|
const manifest = buildManifest(command);
|
|
480
|
-
const renderedFiles = renderSkill(manifest,
|
|
481
|
-
const metadataFiles = renderDistributionMetadata(manifest,
|
|
526
|
+
const renderedFiles = renderSkill(manifest, resolvedMeta);
|
|
527
|
+
const metadataFiles = renderDistributionMetadata(manifest, resolvedMeta);
|
|
482
528
|
const allFiles = [...renderedFiles, ...metadataFiles].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
483
529
|
const results = [];
|
|
484
530
|
for (const agent of agents) {
|
|
485
|
-
const outputDir = resolveAgentPath(agent, scope,
|
|
486
|
-
const
|
|
531
|
+
const outputDir = resolveAgentPath(agent, scope, resolvedMeta.name);
|
|
532
|
+
const installedVersion = await readInstalledVersion(outputDir);
|
|
533
|
+
if (installedVersion === null) {
|
|
534
|
+
const dirExists = await access2(outputDir).then(() => true).catch(() => false);
|
|
535
|
+
if (dirExists && !force) {
|
|
536
|
+
throw new SkillConflictError({ agent, outputDir });
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const status = installedVersion === null ? "installed" : installedVersion === resolvedMeta.version ? "up-to-date" : "updated";
|
|
487
540
|
if (status === "up-to-date") {
|
|
488
541
|
results.push({
|
|
489
542
|
agent,
|
|
@@ -510,9 +563,10 @@ async function generateSkill(options) {
|
|
|
510
563
|
}
|
|
511
564
|
async function uninstallSkill(options) {
|
|
512
565
|
const { name, agents, scope = "global" } = options;
|
|
566
|
+
const resolvedName = resolveSkillName(name);
|
|
513
567
|
const results = [];
|
|
514
568
|
for (const agent of agents) {
|
|
515
|
-
const outputDir = resolveAgentPath(agent, scope,
|
|
569
|
+
const outputDir = resolveAgentPath(agent, scope, resolvedName);
|
|
516
570
|
const exists = await access2(outputDir).then(() => true).catch(() => false);
|
|
517
571
|
if (exists) {
|
|
518
572
|
await rm(outputDir, { recursive: true, force: true });
|
|
@@ -525,9 +579,10 @@ async function uninstallSkill(options) {
|
|
|
525
579
|
}
|
|
526
580
|
async function skillStatus(options) {
|
|
527
581
|
const { name, agents, scope = "global" } = options;
|
|
582
|
+
const resolvedName = resolveSkillName(name);
|
|
528
583
|
const results = [];
|
|
529
584
|
for (const agent of agents) {
|
|
530
|
-
const outputDir = resolveAgentPath(agent, scope,
|
|
585
|
+
const outputDir = resolveAgentPath(agent, scope, resolvedName);
|
|
531
586
|
const version = await readInstalledVersion(outputDir);
|
|
532
587
|
results.push({
|
|
533
588
|
agent,
|
|
@@ -541,12 +596,12 @@ async function skillStatus(options) {
|
|
|
541
596
|
function renderDistributionMetadata(manifest, meta) {
|
|
542
597
|
return [
|
|
543
598
|
{
|
|
544
|
-
path:
|
|
545
|
-
content:
|
|
599
|
+
path: CRUST_MANIFEST,
|
|
600
|
+
content: renderCrustJson(manifest, meta)
|
|
546
601
|
}
|
|
547
602
|
];
|
|
548
603
|
}
|
|
549
|
-
function
|
|
604
|
+
function renderCrustJson(manifest, meta) {
|
|
550
605
|
const commands = collectCommandPaths(manifest);
|
|
551
606
|
const obj = {
|
|
552
607
|
name: meta.name,
|
|
@@ -586,7 +641,7 @@ async function writeFiles(baseDir, files) {
|
|
|
586
641
|
}
|
|
587
642
|
// src/plugin.ts
|
|
588
643
|
import { defineCommand } from "@crustjs/core";
|
|
589
|
-
import { confirm, multiselect,
|
|
644
|
+
import { confirm, multiselect, spinner } from "@crustjs/prompts";
|
|
590
645
|
function deriveSkillMeta(command, version) {
|
|
591
646
|
return {
|
|
592
647
|
name: command.meta.name,
|
|
@@ -601,7 +656,7 @@ function skillPlugin(options) {
|
|
|
601
656
|
name: "skills",
|
|
602
657
|
setup(context, actions) {
|
|
603
658
|
rootCmd = context.rootCommand;
|
|
604
|
-
if (options.command) {
|
|
659
|
+
if (options.command !== false) {
|
|
605
660
|
const name = typeof options.command === "string" ? options.command : "skill";
|
|
606
661
|
skillCmd = buildSkillCommand(rootCmd, options);
|
|
607
662
|
actions.addSubCommand(rootCmd, name, skillCmd);
|
|
@@ -633,15 +688,32 @@ function skillPlugin(options) {
|
|
|
633
688
|
return false;
|
|
634
689
|
});
|
|
635
690
|
if (needsUpdate.length > 0) {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
691
|
+
try {
|
|
692
|
+
const result = await generateSkill({
|
|
693
|
+
command: rootCmd,
|
|
694
|
+
meta,
|
|
695
|
+
agents: needsUpdate.map((a) => a.agent),
|
|
696
|
+
scope: options.scope
|
|
697
|
+
});
|
|
698
|
+
const installedAgents = result.agents.filter((a) => a.status === "installed").map((a) => AGENT_LABELS[a.agent]);
|
|
699
|
+
const updatedAgents = result.agents.filter((a) => a.status === "updated").map((a) => AGENT_LABELS[a.agent]);
|
|
700
|
+
if (installedAgents.length > 0) {
|
|
701
|
+
if (options.command !== false) {
|
|
702
|
+
const manageCommand = `${rootCmd.meta.name} skill`;
|
|
703
|
+
console.log(`Auto-installed skill "${meta.name}" v${meta.version} for ${installedAgents.join(", ")}. Manage with \`${manageCommand}\`.`);
|
|
704
|
+
} else {
|
|
705
|
+
console.log(`Auto-installed skill "${meta.name}" v${meta.version} for ${installedAgents.join(", ")}.`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (updatedAgents.length > 0) {
|
|
709
|
+
console.log(`Updated skill "${meta.name}" to v${meta.version} for ${updatedAgents.join(", ")}.`);
|
|
710
|
+
}
|
|
711
|
+
} catch (err) {
|
|
712
|
+
if (err instanceof SkillConflictError) {
|
|
713
|
+
console.warn(`Skill conflict: "${err.details.outputDir}" already exists ` + `but was not created by ${meta.name}. Skipping auto-update. ` + `Delete or rename the conflicting skill to resolve.`);
|
|
714
|
+
} else {
|
|
715
|
+
throw err;
|
|
716
|
+
}
|
|
645
717
|
}
|
|
646
718
|
}
|
|
647
719
|
await next();
|
|
@@ -652,147 +724,121 @@ function buildSkillCommand(rootCmd, options) {
|
|
|
652
724
|
return defineCommand({
|
|
653
725
|
meta: {
|
|
654
726
|
name: "skill",
|
|
655
|
-
description: "Manage agent
|
|
656
|
-
},
|
|
657
|
-
flags: {
|
|
658
|
-
action: {
|
|
659
|
-
type: "string",
|
|
660
|
-
description: 'Action to perform: "install", "uninstall", or "status" (skips prompt)'
|
|
661
|
-
},
|
|
662
|
-
scope: {
|
|
663
|
-
type: "string",
|
|
664
|
-
description: 'Installation scope: "global" or "project" (skips prompt)'
|
|
665
|
-
},
|
|
666
|
-
force: {
|
|
667
|
-
type: "boolean",
|
|
668
|
-
description: "Skip confirmation prompts"
|
|
669
|
-
}
|
|
727
|
+
description: "Manage agent skill installations"
|
|
670
728
|
},
|
|
671
|
-
async run(
|
|
729
|
+
async run() {
|
|
672
730
|
const meta = deriveSkillMeta(rootCmd, options.version);
|
|
673
|
-
const
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
const installedAgents = await detectInstalledAgents();
|
|
677
|
-
if (installedAgents.length === 0) {
|
|
731
|
+
const scope = options.scope ?? "global";
|
|
732
|
+
const detectedAgents = await detectInstalledAgents();
|
|
733
|
+
if (detectedAgents.length === 0) {
|
|
678
734
|
console.log("No supported agents detected. Install Claude Code or OpenCode first.");
|
|
679
735
|
return;
|
|
680
736
|
}
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
{ label: "Uninstall skills", value: "uninstall" },
|
|
686
|
-
{ label: "Check status", value: "status" }
|
|
687
|
-
],
|
|
688
|
-
initial: actionFlag
|
|
737
|
+
const status = await skillStatus({
|
|
738
|
+
name: meta.name,
|
|
739
|
+
agents: detectedAgents,
|
|
740
|
+
scope
|
|
689
741
|
});
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
});
|
|
728
|
-
console.log(`
|
|
742
|
+
const installedAgents = [];
|
|
743
|
+
const choices = status.agents.map((entry) => {
|
|
744
|
+
const hint = entry.installed ? `v${entry.version} installed` : "not installed";
|
|
745
|
+
if (entry.installed) {
|
|
746
|
+
installedAgents.push(entry.agent);
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
label: AGENT_LABELS[entry.agent],
|
|
750
|
+
value: entry.agent,
|
|
751
|
+
hint
|
|
752
|
+
};
|
|
753
|
+
});
|
|
754
|
+
const selected = await multiselect({
|
|
755
|
+
message: "Select agents to install skills for",
|
|
756
|
+
choices,
|
|
757
|
+
default: installedAgents,
|
|
758
|
+
required: false
|
|
759
|
+
});
|
|
760
|
+
const toInstall = selected.filter((agent) => !installedAgents.includes(agent));
|
|
761
|
+
const toUpdate = selected.filter((agent) => {
|
|
762
|
+
const entry = status.agents.find((a) => a.agent === agent);
|
|
763
|
+
return entry?.installed === true && entry.version !== meta.version;
|
|
764
|
+
});
|
|
765
|
+
const toUninstall = installedAgents.filter((agent) => !selected.includes(agent));
|
|
766
|
+
const agentsToGenerate = [...toInstall, ...toUpdate];
|
|
767
|
+
if (agentsToGenerate.length > 0) {
|
|
768
|
+
try {
|
|
769
|
+
const result = await spinner({
|
|
770
|
+
message: "Installing skills...",
|
|
771
|
+
task: async () => generateSkill({
|
|
772
|
+
command: rootCmd,
|
|
773
|
+
meta,
|
|
774
|
+
agents: agentsToGenerate,
|
|
775
|
+
scope
|
|
776
|
+
})
|
|
777
|
+
});
|
|
778
|
+
console.log(`
|
|
729
779
|
Installed "${meta.name}" v${meta.version}`);
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
780
|
+
for (const r of result.agents) {
|
|
781
|
+
console.log(` ${AGENT_LABELS[r.agent]} \u2192 ${r.outputDir}`);
|
|
782
|
+
}
|
|
783
|
+
} catch (err) {
|
|
784
|
+
if (err instanceof SkillConflictError) {
|
|
785
|
+
const overwrite = await confirm({
|
|
786
|
+
message: `"${err.details.outputDir}" already exists but was not ` + `created by Crust. Overwrite?`,
|
|
787
|
+
default: false
|
|
788
|
+
});
|
|
789
|
+
if (overwrite) {
|
|
790
|
+
const result = await spinner({
|
|
791
|
+
message: "Overwriting skill...",
|
|
792
|
+
task: async () => generateSkill({
|
|
793
|
+
command: rootCmd,
|
|
794
|
+
meta,
|
|
795
|
+
agents: [err.details.agent],
|
|
796
|
+
scope,
|
|
797
|
+
force: true
|
|
798
|
+
})
|
|
799
|
+
});
|
|
800
|
+
console.log(`
|
|
801
|
+
Installed "${meta.name}" v${meta.version}`);
|
|
802
|
+
for (const r of result.agents) {
|
|
803
|
+
console.log(` ${AGENT_LABELS[r.agent]} \u2192 ${r.outputDir}`);
|
|
804
|
+
}
|
|
805
|
+
} else {
|
|
806
|
+
console.log(`
|
|
807
|
+
Skipped ${AGENT_LABELS[err.details.agent]}`);
|
|
808
|
+
}
|
|
809
|
+
} else {
|
|
810
|
+
throw err;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (toUninstall.length > 0) {
|
|
815
|
+
const result = await spinner({
|
|
816
|
+
message: "Removing skills...",
|
|
817
|
+
task: async () => uninstallSkill({
|
|
818
|
+
name: meta.name,
|
|
819
|
+
agents: toUninstall,
|
|
820
|
+
scope
|
|
821
|
+
})
|
|
822
|
+
});
|
|
823
|
+
const removed = result.agents.filter((a) => a.status === "removed").map((a) => AGENT_LABELS[a.agent]);
|
|
824
|
+
if (removed.length > 0) {
|
|
825
|
+
console.log(`
|
|
769
826
|
Removed from ${removed.join(", ")}`);
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
const scope = scopeFlag ?? "global";
|
|
777
|
-
const result = await skillStatus({
|
|
778
|
-
name: meta.name,
|
|
779
|
-
agents: installedAgents,
|
|
780
|
-
scope
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (agentsToGenerate.length === 0 && toUninstall.length === 0) {
|
|
830
|
+
console.log("No changes.");
|
|
831
|
+
}
|
|
832
|
+
}
|
|
781
833
|
});
|
|
782
|
-
console.log("");
|
|
783
|
-
console.log(`${"Agent".padEnd(14)}${"Scope".padEnd(10)}${"Version".padEnd(10)}Path`);
|
|
784
|
-
for (const entry of result.agents) {
|
|
785
|
-
const agentName = AGENT_LABELS[entry.agent];
|
|
786
|
-
const version = entry.installed ? entry.version ?? "\u2014" : "(not installed)";
|
|
787
|
-
const path = entry.installed ? entry.outputDir : "";
|
|
788
|
-
console.log(`${agentName.padEnd(14)}${scope.padEnd(10)}${version.padEnd(10)}${path}`);
|
|
789
|
-
}
|
|
790
|
-
console.log("");
|
|
791
834
|
}
|
|
792
835
|
export {
|
|
793
836
|
uninstallSkill,
|
|
794
837
|
skillStatus,
|
|
795
838
|
skillPlugin,
|
|
839
|
+
resolveSkillName,
|
|
840
|
+
isValidSkillName,
|
|
796
841
|
generateSkill,
|
|
797
|
-
detectInstalledAgents
|
|
842
|
+
detectInstalledAgents,
|
|
843
|
+
SkillConflictError
|
|
798
844
|
};
|