@crustjs/skills 0.0.7 → 0.0.9
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 +112 -36
- package/dist/index.d.ts +63 -44
- package/dist/index.js +10 -10
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -31,11 +31,87 @@ const result = await generateSkill({
|
|
|
31
31
|
description: "CLI tool for managing widgets",
|
|
32
32
|
version: "1.0.0",
|
|
33
33
|
},
|
|
34
|
+
agents: ["opencode", "claude-code"],
|
|
34
35
|
});
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
for (const agent of result.agents) {
|
|
38
|
+
console.log(`${agent.agent}: ${agent.status} -> ${agent.outputDir}`);
|
|
39
|
+
}
|
|
37
40
|
```
|
|
38
41
|
|
|
42
|
+
### Runtime Plugin (`autoUpdate`)
|
|
43
|
+
|
|
44
|
+
`skillPlugin()` is a runtime plugin. Register it in `runMain(..., { plugins })`.
|
|
45
|
+
Do not put a `plugins` field inside `defineCommand(...)`.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { defineCommand, runMain } from "@crustjs/core";
|
|
49
|
+
import { skillPlugin } from "@crustjs/skills";
|
|
50
|
+
|
|
51
|
+
const app = defineCommand({
|
|
52
|
+
meta: { name: "my-cli", description: "My CLI" },
|
|
53
|
+
run() {
|
|
54
|
+
console.log("hello");
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
runMain(app, {
|
|
59
|
+
plugins: [
|
|
60
|
+
skillPlugin({
|
|
61
|
+
version: "1.0.0",
|
|
62
|
+
// autoUpdate: true (default) — silently updates installed skills
|
|
63
|
+
// command: true (default) — registers "my-cli skill" subcommand
|
|
64
|
+
}),
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The plugin automatically updates already-installed skills when the version changes. First-time installation is done via the interactive `skill` subcommand, or programmatically using the exported primitives.
|
|
70
|
+
|
|
71
|
+
### Programmatic Auto-Install
|
|
72
|
+
|
|
73
|
+
For full control over first-time installation, use the exported primitives
|
|
74
|
+
directly in your own setup logic:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { defineCommand, runMain } from "@crustjs/core";
|
|
78
|
+
import { detectInstalledAgents, generateSkill, skillStatus } from "@crustjs/skills";
|
|
79
|
+
|
|
80
|
+
const app = defineCommand({
|
|
81
|
+
meta: { name: "my-cli", description: "My CLI" },
|
|
82
|
+
async run() {
|
|
83
|
+
// Detect agents and install skills if not yet present
|
|
84
|
+
const agents = await detectInstalledAgents({ scope: "global" });
|
|
85
|
+
const status = await skillStatus({ name: "my-cli", agents, scope: "global" });
|
|
86
|
+
|
|
87
|
+
const notInstalled = status.agents
|
|
88
|
+
.filter((a) => !a.installed)
|
|
89
|
+
.map((a) => a.agent);
|
|
90
|
+
|
|
91
|
+
if (notInstalled.length > 0) {
|
|
92
|
+
await generateSkill({
|
|
93
|
+
command: app,
|
|
94
|
+
meta: { name: "my-cli", description: "My CLI", version: "1.0.0" },
|
|
95
|
+
agents: notInstalled,
|
|
96
|
+
scope: "global",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
runMain(app);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### Troubleshooting
|
|
106
|
+
|
|
107
|
+
If auto-update does not appear to work:
|
|
108
|
+
|
|
109
|
+
- Ensure plugin is passed to `runMain(..., { plugins: [...] })`.
|
|
110
|
+
- Ensure at least one supported agent is detected for your scope:
|
|
111
|
+
- `scope: "global"` -> `~/.claude` or `~/.config/opencode`
|
|
112
|
+
- `scope: "project"` -> `<cwd>/.claude` or `<cwd>/.opencode` (falls back to global roots)
|
|
113
|
+
- Check for existing conflicting skill directories without `crust.json`.
|
|
114
|
+
|
|
39
115
|
## Recommended Export Pattern
|
|
40
116
|
|
|
41
117
|
To avoid side effects when your command module is imported for generation, guard runtime code with `import.meta.main`:
|
|
@@ -69,20 +145,20 @@ crust skills generate <module> [options]
|
|
|
69
145
|
|
|
70
146
|
### Arguments
|
|
71
147
|
|
|
72
|
-
| Argument | Description
|
|
73
|
-
| -------- |
|
|
148
|
+
| Argument | Description |
|
|
149
|
+
| -------- | ------------------------------------------------ |
|
|
74
150
|
| `module` | Path to the command module (e.g. `./src/cli.ts`) |
|
|
75
151
|
|
|
76
152
|
### Flags
|
|
77
153
|
|
|
78
|
-
| Flag
|
|
79
|
-
|
|
|
80
|
-
| `--name`
|
|
81
|
-
| `--description` | `-d`
|
|
82
|
-
| `--version`
|
|
83
|
-
| `--out-dir`
|
|
84
|
-
| `--clean`
|
|
85
|
-
| `--export`
|
|
154
|
+
| Flag | Alias | Required | Default | Description |
|
|
155
|
+
| --------------- | ----- | -------- | --------- | ---------------------------------------------- |
|
|
156
|
+
| `--name` | `-n` | Yes | - | Skill name (used as directory name) |
|
|
157
|
+
| `--description` | `-d` | Yes | - | Human-readable description |
|
|
158
|
+
| `--version` | `-V` | No | - | Version string |
|
|
159
|
+
| `--out-dir` | `-o` | No | `.` | Output directory |
|
|
160
|
+
| `--clean` | - | No | `true` | Remove existing skill directory before writing |
|
|
161
|
+
| `--export` | `-e` | No | `default` | Named export to use from the module |
|
|
86
162
|
|
|
87
163
|
### Examples
|
|
88
164
|
|
|
@@ -114,13 +190,13 @@ import { generateSkill } from "@crustjs/skills";
|
|
|
114
190
|
const result = await generateSkill({
|
|
115
191
|
command: rootCommand,
|
|
116
192
|
meta: { name: "my-cli", description: "My CLI tool", version: "1.0.0" },
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
193
|
+
agents: ["opencode"],
|
|
194
|
+
scope: "project", // default: "global"
|
|
195
|
+
clean: true, // default: true — removes existing skill dir first
|
|
196
|
+
force: false, // default: false — throws SkillConflictError if dir exists without crust.json
|
|
120
197
|
});
|
|
121
198
|
|
|
122
|
-
// result.
|
|
123
|
-
// result.files — sorted list of written file paths (relative to outputDir)
|
|
199
|
+
// result.agents — per-agent install results
|
|
124
200
|
```
|
|
125
201
|
|
|
126
202
|
### `buildManifest(command)`
|
|
@@ -145,7 +221,7 @@ const manifest = buildManifest(rootCommand);
|
|
|
145
221
|
const files = renderSkill(manifest, { name: "my-cli", description: "My CLI" });
|
|
146
222
|
|
|
147
223
|
for (const file of files) {
|
|
148
|
-
console.log(file.path);
|
|
224
|
+
console.log(file.path); // e.g. "SKILL.md", "commands/serve.md"
|
|
149
225
|
console.log(file.content); // markdown content
|
|
150
226
|
}
|
|
151
227
|
```
|
|
@@ -157,9 +233,9 @@ Validates a skill name against the [Agent Skills spec](https://agentskills.io/sp
|
|
|
157
233
|
```ts
|
|
158
234
|
import { isValidSkillName } from "@crustjs/skills";
|
|
159
235
|
|
|
160
|
-
isValidSkillName("my-cli");
|
|
161
|
-
isValidSkillName("My_CLI");
|
|
162
|
-
isValidSkillName("-leading");
|
|
236
|
+
isValidSkillName("my-cli"); // true
|
|
237
|
+
isValidSkillName("My_CLI"); // false — uppercase and underscores not allowed
|
|
238
|
+
isValidSkillName("-leading"); // false — leading hyphen
|
|
163
239
|
isValidSkillName("a".repeat(65)); // false — exceeds 64 characters
|
|
164
240
|
```
|
|
165
241
|
|
|
@@ -176,19 +252,19 @@ const meta: SkillMeta = {
|
|
|
176
252
|
version: "1.0.0",
|
|
177
253
|
|
|
178
254
|
// Optional fields — emitted in SKILL.md YAML frontmatter when set
|
|
179
|
-
allowedTools: "Bash(my-cli *) Read Grep",
|
|
180
|
-
license: "MIT",
|
|
181
|
-
compatibility: "Requires my-cli on PATH",
|
|
182
|
-
disableModelInvocation: false,
|
|
255
|
+
allowedTools: "Bash(my-cli *) Read Grep", // Pre-approved tools (avoids per-use prompts)
|
|
256
|
+
license: "MIT", // License name or reference
|
|
257
|
+
compatibility: "Requires my-cli on PATH", // Environment requirements (max 500 chars)
|
|
258
|
+
disableModelInvocation: false, // true = agent won't auto-load; user must invoke manually
|
|
183
259
|
};
|
|
184
260
|
```
|
|
185
261
|
|
|
186
|
-
| Field
|
|
187
|
-
|
|
|
188
|
-
| `allowedTools`
|
|
189
|
-
| `license`
|
|
190
|
-
| `compatibility`
|
|
191
|
-
| `disableModelInvocation` | `disable-model-invocation` | When `true`, prevents agents from auto-loading the skill
|
|
262
|
+
| Field | Frontmatter Key | Description |
|
|
263
|
+
| ------------------------ | -------------------------- | ---------------------------------------------------------------------------- |
|
|
264
|
+
| `allowedTools` | `allowed-tools` | Space-delimited list of pre-approved tools (e.g. `Bash(my-cli *) Read Grep`) |
|
|
265
|
+
| `license` | `license` | License name or file reference |
|
|
266
|
+
| `compatibility` | `compatibility` | Environment requirements or compatibility notes |
|
|
267
|
+
| `disableModelInvocation` | `disable-model-invocation` | When `true`, prevents agents from auto-loading the skill |
|
|
192
268
|
|
|
193
269
|
## Escaping
|
|
194
270
|
|
|
@@ -218,12 +294,12 @@ skills/use-my-cli/
|
|
|
218
294
|
|
|
219
295
|
### File Details
|
|
220
296
|
|
|
221
|
-
| File
|
|
222
|
-
|
|
|
223
|
-
| `SKILL.md`
|
|
224
|
-
| `command-index.md` | Markdown table listing every command, its type (runnable/group), and documentation path.
|
|
225
|
-
| `commands/*.md`
|
|
226
|
-
| `crust.json`
|
|
297
|
+
| File | Purpose |
|
|
298
|
+
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
299
|
+
| `SKILL.md` | Agent entrypoint with YAML frontmatter. Directs agents to load specific command files on demand (lazy loading). |
|
|
300
|
+
| `command-index.md` | Markdown table listing every command, its type (runnable/group), and documentation path. |
|
|
301
|
+
| `commands/*.md` | Per-command reference files. Leaf commands include usage, arguments, flags, defaults, and aliases. Group commands list subcommands with links. |
|
|
302
|
+
| `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
303
|
|
|
228
304
|
## Conflict Detection
|
|
229
305
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CommandNode } from "@crustjs/core";
|
|
2
2
|
/**
|
|
3
3
|
* Metadata for the generated skill bundle.
|
|
4
4
|
*
|
|
@@ -97,7 +97,7 @@ type Scope = "global" | "project";
|
|
|
97
97
|
*/
|
|
98
98
|
interface GenerateOptions {
|
|
99
99
|
/** Root command to generate the skill from */
|
|
100
|
-
command:
|
|
100
|
+
command: CommandNode;
|
|
101
101
|
/** Skill metadata for the generated bundle */
|
|
102
102
|
meta: SkillMeta;
|
|
103
103
|
/** Agent targets to install skills for */
|
|
@@ -194,13 +194,20 @@ interface StatusResult {
|
|
|
194
194
|
* The plugin reads `name` and `description` from the root command's `meta`
|
|
195
195
|
* at setup time, so only `version` is required here.
|
|
196
196
|
*
|
|
197
|
-
* Installed agents are detected automatically
|
|
198
|
-
*
|
|
199
|
-
*
|
|
197
|
+
* Installed agents are detected automatically based on the configured scope.
|
|
198
|
+
*
|
|
199
|
+
* - `scope: "global"` checks `~/.claude/` and `~/.config/opencode/`
|
|
200
|
+
* - `scope: "project"` checks `<cwd>/.claude/` / `<cwd>/.opencode/`, then
|
|
201
|
+
* falls back to global roots when local roots are missing
|
|
202
|
+
*
|
|
203
|
+
* Only detected agents are managed.
|
|
200
204
|
*
|
|
201
205
|
* **Auto-update** (default): silently updates already-installed skills when a
|
|
202
|
-
* new version is detected.
|
|
203
|
-
*
|
|
206
|
+
* new version is detected. Disable with `autoUpdate: false`.
|
|
207
|
+
*
|
|
208
|
+
* For first-time installation, use the interactive `skill` subcommand or
|
|
209
|
+
* build custom auto-install logic with the exported primitives
|
|
210
|
+
* (`detectInstalledAgents`, `skillStatus`, `generateSkill`).
|
|
204
211
|
*
|
|
205
212
|
* **Interactive command** (default): registers a `skill` subcommand that
|
|
206
213
|
* presents a single multiselect prompt for toggling agent installations.
|
|
@@ -215,13 +222,6 @@ interface SkillPluginOptions {
|
|
|
215
222
|
*/
|
|
216
223
|
scope?: Scope;
|
|
217
224
|
/**
|
|
218
|
-
* Automatically install skills when not yet present.
|
|
219
|
-
* Set to `true` to install on first CLI invocation without requiring the
|
|
220
|
-
* interactive skill command.
|
|
221
|
-
* @default false
|
|
222
|
-
*/
|
|
223
|
-
autoInstall?: boolean;
|
|
224
|
-
/**
|
|
225
225
|
* Automatically update skills when the installed version is outdated.
|
|
226
226
|
* @default true
|
|
227
227
|
*/
|
|
@@ -242,21 +242,36 @@ interface SkillPluginOptions {
|
|
|
242
242
|
command?: boolean | string;
|
|
243
243
|
}
|
|
244
244
|
/**
|
|
245
|
-
*
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
*
|
|
250
|
-
*
|
|
245
|
+
* Options for detecting installed agents.
|
|
246
|
+
*/
|
|
247
|
+
interface DetectInstalledAgentsOptions {
|
|
248
|
+
/**
|
|
249
|
+
* Detection scope.
|
|
250
|
+
* - `global`: checks global config roots under home directory.
|
|
251
|
+
* - `project`: checks project-local config roots under cwd, then falls back
|
|
252
|
+
* to global roots under home directory when local roots are missing.
|
|
253
|
+
* @default "global"
|
|
254
|
+
*/
|
|
255
|
+
scope?: Scope;
|
|
256
|
+
/** Home directory override used for global detection (tests). */
|
|
257
|
+
home?: string;
|
|
258
|
+
/** Working directory override used for project detection (tests). */
|
|
259
|
+
cwd?: string;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Detects which supported agents are installed by checking for agent
|
|
263
|
+
* configuration roots for the requested scope.
|
|
251
264
|
*
|
|
252
265
|
* Detection table:
|
|
253
|
-
* | Agent | Config
|
|
254
|
-
* | ------------ |
|
|
255
|
-
* | `claude-code`| `<homedir>/.claude/`
|
|
256
|
-
* | `opencode` | `<homedir>/.config/opencode/`
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
266
|
+
* | Scope | Agent | Config directories checked |
|
|
267
|
+
* | --------- | ------------ | ------------------------------------------ |
|
|
268
|
+
* | `global` | `claude-code`| `<homedir>/.claude/` |
|
|
269
|
+
* | `global` | `opencode` | `<homedir>/.config/opencode/` |
|
|
270
|
+
* | `project` | `claude-code`| `<cwd>/.claude/`, fallback `<homedir>/.claude/` |
|
|
271
|
+
* | `project` | `opencode` | `<cwd>/.opencode/`, fallback `<homedir>/.config/opencode/` |
|
|
272
|
+
*
|
|
273
|
+
* @param options - Optional scope/home/cwd overrides. For backwards
|
|
274
|
+
* compatibility, passing a string is treated as `home`.
|
|
260
275
|
* @returns Array of detected agent targets (may be empty)
|
|
261
276
|
*
|
|
262
277
|
* @example
|
|
@@ -265,7 +280,7 @@ interface SkillPluginOptions {
|
|
|
265
280
|
* // ["claude-code"] — only Claude Code config found
|
|
266
281
|
* ```
|
|
267
282
|
*/
|
|
268
|
-
declare function detectInstalledAgents(
|
|
283
|
+
declare function detectInstalledAgents(options?: string | DetectInstalledAgentsOptions): Promise<AgentTarget[]>;
|
|
269
284
|
/** Details about the conflict between an existing skill and an incoming one. */
|
|
270
285
|
interface SkillConflictDetails {
|
|
271
286
|
/** The agent where the conflict was detected */
|
|
@@ -381,13 +396,16 @@ import { CrustPlugin } from "@crustjs/core";
|
|
|
381
396
|
* `name` and `description` are read from the root command's `meta` at setup
|
|
382
397
|
* time — only `version` needs to be supplied in the options.
|
|
383
398
|
*
|
|
384
|
-
* Installed agents are detected automatically
|
|
385
|
-
*
|
|
386
|
-
*
|
|
399
|
+
* Installed agents are detected automatically based on the configured scope.
|
|
400
|
+
* - `scope: "global"` checks global config roots in the home directory
|
|
401
|
+
* - `scope: "project"` checks project-local config roots in the cwd, then
|
|
402
|
+
* falls back to global roots in the home directory
|
|
403
|
+
*
|
|
404
|
+
* Only detected agents are managed by automatic update and the interactive
|
|
405
|
+
* command.
|
|
387
406
|
*
|
|
388
407
|
* **Auto-update** (default): silently updates already-installed skills when a
|
|
389
|
-
* new version is detected.
|
|
390
|
-
* are not yet present.
|
|
408
|
+
* new version is detected. Disable with `autoUpdate: false`.
|
|
391
409
|
*
|
|
392
410
|
* **Interactive command** (default): registers a `skill` subcommand that
|
|
393
411
|
* presents a single multiselect prompt for toggling agent installations.
|
|
@@ -396,25 +414,26 @@ import { CrustPlugin } from "@crustjs/core";
|
|
|
396
414
|
* deselected agents are uninstalled, and already-correct agents are skipped.
|
|
397
415
|
* Set `command: false` to disable command injection.
|
|
398
416
|
*
|
|
417
|
+
* For first-time installation, use the interactive command or build custom
|
|
418
|
+
* auto-install logic with the exported primitives (`detectInstalledAgents`,
|
|
419
|
+
* `skillStatus`, `generateSkill`).
|
|
420
|
+
*
|
|
399
421
|
* @param options - Plugin configuration with version and scope
|
|
400
422
|
* @returns A `CrustPlugin` to register in a command's `plugins` array
|
|
401
423
|
*
|
|
402
424
|
* @example
|
|
403
425
|
* ```ts
|
|
404
|
-
* import {
|
|
426
|
+
* import { Crust } from "@crustjs/core";
|
|
405
427
|
* import { skillPlugin } from "@crustjs/skills";
|
|
406
428
|
*
|
|
407
|
-
* const app =
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
* }),
|
|
414
|
-
* ],
|
|
415
|
-
* });
|
|
429
|
+
* const app = new Crust("my-cli").meta({ description: "My CLI" })
|
|
430
|
+
* .use(skillPlugin({
|
|
431
|
+
* version: "1.0.0",
|
|
432
|
+
* command: true, // registers "my-cli skill" subcommand
|
|
433
|
+
* }))
|
|
434
|
+
* .run(() => { /* ... */ });
|
|
416
435
|
*
|
|
417
|
-
*
|
|
436
|
+
* await app.execute();
|
|
418
437
|
* ```
|
|
419
438
|
*/
|
|
420
439
|
declare function skillPlugin(options: SkillPluginOptions): CrustPlugin;
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{access as
|
|
3
|
-
`)}function
|
|
4
|
-
`)}function
|
|
5
|
-
`)}function
|
|
6
|
-
`)}function
|
|
7
|
-
`}function
|
|
8
|
-
Installed "${
|
|
9
|
-
Installed "${
|
|
10
|
-
Skipped ${
|
|
11
|
-
Removed from ${
|
|
2
|
+
import{access as JQ}from"fs/promises";import{homedir as u}from"os";import{join as z}from"path";var RQ=["claude-code","opencode"],j={"claude-code":"Claude Code",opencode:"OpenCode"};function S(Q,Z,X){let H=Z==="global"?u():process.cwd();switch(Q){case"claude-code":return z(H,".claude","skills",X);case"opencode":if(Z==="global")return z(H,".config","opencode","skills",X);return z(H,".opencode","skills",X)}}async function E(Q){let Z=typeof Q==="string"?{home:Q}:Q??{},X=Z.scope??"global",H=Z.home??u(),W=Z.cwd??process.cwd(),$=[];for(let J of RQ){let R=qQ(J,X,H,W),Y=!1;for(let K of R)if(Y=await JQ(K).then(()=>!0).catch(()=>!1),Y)break;if(Y)$.push(J)}return $}function qQ(Q,Z,X,H){if(Z==="project")switch(Q){case"claude-code":return[z(H,".claude"),z(X,".claude")];case"opencode":return[z(H,".opencode"),z(X,".config","opencode")]}switch(Q){case"claude-code":return[z(X,".claude")];case"opencode":return[z(X,".config","opencode")]}}class M extends Error{name="SkillConflictError";details;constructor(Q){let Z=`Skill conflict for agent "${Q.agent}": directory "${Q.outputDir}" already exists but was not created by Crust (no crust.json found). Delete or rename the conflicting skill to resolve.`;super(Z);this.details=Q}}import{access as n,mkdir as EQ,rm as a,writeFile as LQ}from"fs/promises";import{dirname as TQ,join as t}from"path";function p(Q){return f(Q,[])}function f(Q,Z){let X=YQ(Q.meta.name),H=[...Z,X],W=BQ(Q.args),$=OQ(Q.effectiveFlags),J=GQ(Q.subCommands,H);return{name:X,path:H,description:Q.meta.description,usage:Q.meta.usage,runnable:typeof Q.run==="function",args:W,flags:$,children:J}}function YQ(Q){return Q.trim().toLowerCase()}function BQ(Q){if(!Q||Q.length===0)return[];return Q.map(KQ)}function KQ(Q){let Z={name:Q.name,type:Q.type,required:Q.required===!0,variadic:Q.variadic===!0};if(Q.description!==void 0)Z.description=Q.description;if(Q.default!==void 0)Z.default=c(Q.default);return Z}function OQ(Q){if(!Q)return[];return Object.keys(Q).sort().map((X)=>{return xQ(X,Q[X])})}function xQ(Q,Z){let X={name:Q,type:Z.type,required:Z.required===!0,multiple:Z.multiple===!0,short:Z.short,aliases:Z.aliases?[...Z.aliases].sort():[]};if(Z.description!==void 0)X.description=Z.description;if(Z.default!==void 0)X.default=c(Z.default);return X}function GQ(Q,Z){return Object.keys(Q).sort().map((H)=>{return f(Q[H],Z)})}function c(Q){if(Array.isArray(Q))return JSON.stringify(Q);return String(Q)}function _(Q){if(/[:#[\]{}&*!|>'"`,@?\\]|^\s|\s$|^---|[\n\r]/.test(Q))return`"${Q.replace(/\\/g,"\\\\").replace(/"/g,"\\\"").replace(/\n/g,"\\n").replace(/\r/g,"\\r")}"`;return Q}function g(Q){return Q.replace(/(?<!\\)\|/g,"\\|")}function m(Q,Z){let X=[],H=d(Q);X.push({path:"SKILL.md",content:zQ(Q,Z)}),X.push({path:"command-index.md",content:UQ(Q,H)});for(let W of H){let $=U(W),J=W.children.length>0?MQ(W,Q):jQ(W,Q);X.push({path:$,content:J})}return X}function d(Q){let Z=[Q];for(let X of Q.children)Z.push(...d(X));return Z}function U(Q){if(Q.path.length<=1)return`commands/${Q.name}.md`;return`commands/${Q.path.slice(1).join("/")}.md`}function L(Q){return Q.path.join(" ")}function A(Q,Z){let X=Q.split("/").slice(0,-1),H=Z.split("/"),W=0;while(W<X.length&&W<H.length&&X[W]===H[W])W++;let $=X.length-W,J=H.slice(W);if($===0)return J.join("/");return[...Array.from({length:$},()=>".."),...J].join("/")}function zQ(Q,Z){let X=[];if(X.push("---"),X.push(`name: ${_(Z.name)}`),X.push(`description: ${_(Z.description)}`),Z.license)X.push(`license: ${_(Z.license)}`);if(Z.compatibility)X.push(`compatibility: ${_(Z.compatibility)}`);if(Z.disableModelInvocation)X.push("disable-model-invocation: true");if(Z.allowedTools)X.push(`allowed-tools: ${_(Z.allowedTools)}`);if(X.push("metadata:"),X.push(` version: "${Z.version}"`),X.push("---"),X.push(""),X.push(`# ${Z.name}`),X.push(""),Q.description)X.push(Q.description),X.push("");let H=Z.name.startsWith("use-")?Z.name.slice(4):Z.name;if(X.push(`Use this skill when working with \`${H}\` commands, or when you need help with \`${H}\` syntax, flags, or subcommands.`),X.push(""),X.push("## Command Reference"),X.push(""),X.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:"),X.push(""),X.push("1. Check [command-index.md](command-index.md) to find the relevant command"),X.push("2. Read only the specific file from the `commands/` directory that you need"),X.push(""),Q.children.length>0){X.push("## Available Commands"),X.push("");for(let W of Q.children){let $=U(W),J=W.description?` - ${W.description}`:"";X.push(`- [\`${W.name}\`](${$})${J}`)}X.push("")}if(Q.runnable){X.push("## Usage"),X.push("");let W=U(Q);X.push(`The root command is directly executable. See [${Q.name}](${W}) for usage details.`),X.push("")}return X.join(`
|
|
3
|
+
`)}function UQ(Q,Z){let X=[];X.push("# Command Index"),X.push(""),X.push("| Command | Type | Documentation |"),X.push("| ------- | ---- | ------------- |");for(let H of Z){let W=L(H),$=U(H),J=VQ(H);X.push(`| \`${W}\` | ${J} | [${$}](${$}) |`)}return X.push(""),X.join(`
|
|
4
|
+
`)}function VQ(Q){if(Q.runnable&&Q.children.length>0)return"runnable, group";if(Q.runnable)return"runnable";return"group"}function jQ(Q,Z){let X=[],H=L(Q);if(X.push(`# \`${H}\``),X.push(""),Q.description)X.push(Q.description),X.push("");if(X.push("## Usage"),X.push(""),Q.usage)X.push("```"),X.push(Q.usage),X.push("```");else X.push("```"),X.push(r(Q)),X.push("```");if(X.push(""),Q.args.length>0)X.push("## Arguments"),X.push(""),X.push(...l(Q.args)),X.push("");if(Q.flags.length>0)X.push("## Flags"),X.push(""),X.push(...i(Q.flags)),X.push("");return X.push(...o(Q,Z)),X.join(`
|
|
5
|
+
`)}function MQ(Q,Z){let X=[],H=L(Q),W=U(Q);if(X.push(`# \`${H}\``),X.push(""),Q.description)X.push(Q.description),X.push("");if(Q.runnable){if(X.push("## Usage"),X.push(""),Q.usage)X.push("```"),X.push(Q.usage),X.push("```");else X.push("```"),X.push(r(Q)),X.push("```");if(X.push(""),Q.args.length>0)X.push("## Arguments"),X.push(""),X.push(...l(Q.args)),X.push("");if(Q.flags.length>0)X.push("## Flags"),X.push(""),X.push(...i(Q.flags)),X.push("")}X.push("## Subcommands"),X.push("");for(let $ of Q.children){let J=U($),R=A(W,J),Y=$.description?` - ${$.description}`:"";X.push(`- [\`${$.name}\`](${R})${Y}`)}return X.push(""),X.push(...o(Q,Z)),X.join(`
|
|
6
|
+
`)}function r(Q){let Z=[...Q.path];for(let X of Q.args)if(X.variadic)Z.push(X.required?`<${X.name}...>`:`[${X.name}...]`);else Z.push(X.required?`<${X.name}>`:`[${X.name}]`);if(Q.flags.length>0)Z.push("[options]");return Z.join(" ")}function l(Q){let Z=[];Z.push("| Argument | Type | Required | Description |"),Z.push("| -------- | ---- | -------- | ----------- |");for(let X of Q){let H=X.variadic?`${X.name}...`:X.name,W=X.required?"Yes":"No",$=g(IQ(X));Z.push(`| \`${H}\` | ${X.type} | ${W} | ${$} |`)}return Z}function IQ(Q){let Z=[];if(Q.description)Z.push(Q.description);if(Q.default!==void 0)Z.push(`Default: \`${Q.default}\``);return Z.join(". ")||"-"}function i(Q){let Z=[];Z.push("| Flag | Type | Required | Description |"),Z.push("| ---- | ---- | -------- | ----------- |");for(let X of Q){let H=_Q(X),W=X.required?"Yes":"No",$=g(wQ(X));Z.push(`| ${H} | ${X.type} | ${W} | ${$} |`)}return Z}function _Q(Q){let Z=[`\`--${Q.name}\``];if(Q.short)Z.push(`\`-${Q.short}\``);for(let X of Q.aliases)Z.push(`\`--${X}\``);return Z.join(", ")}function wQ(Q){let Z=[];if(Q.description)Z.push(Q.description);if(Q.multiple)Z.push("Can be specified multiple times");if(Q.default!==void 0)Z.push(`Default: \`${Q.default}\``);return Z.join(". ")||"-"}function o(Q,Z){let X=[],H=U(Q);if(X.push("---"),X.push(""),Q.path.length>1){let $=Q.path.slice(0,-1),J=s(Z,$);if(J){let R=U(J),Y=A(H,R),K=L(J);X.push(`Parent: [\`${K}\`](${Y})`),X.push("")}}let W=A(H,"command-index.md");return X.push(`[Command Index](${W})`),X.push(""),X}function s(Q,Z){if(kQ(Q.path,Z))return Q;for(let X of Q.children){let H=s(X,Z);if(H)return H}return}function kQ(Q,Z){if(Q.length!==Z.length)return!1;for(let X=0;X<Q.length;X++)if(Q[X]!==Z[X])return!1;return!0}import{readFile as FQ}from"fs/promises";import{join as SQ}from"path";var C="crust.json";async function D(Q){try{let Z=await FQ(SQ(Q,C),"utf-8"),X=JSON.parse(Z);if(typeof X==="object"&&X!==null&&"version"in X&&typeof X.version==="string")return X.version;return null}catch{return null}}var e=/^[a-z0-9]+(-[a-z0-9]+)*$/;function QQ(Q){return Q.length>=1&&Q.length<=64&&e.test(Q)}function I(Q){return Q.startsWith("use-")?Q:`use-${Q}`}async function w(Q){let{command:Z,meta:X,agents:H,scope:W="global",clean:$=!0,force:J=!1}=Q,R=I(X.name);if(!QQ(R))throw Error(`Invalid skill name "${R}": must be 1\u201364 lowercase `+`alphanumeric characters and hyphens, no leading/trailing/consecutive hyphens. Pattern: ${e.source}`);let Y={...X,name:R},K=p(Z),O=m(K,Y),y=PQ(K,Y),k=[...O,...y].sort((x,q)=>x.path<q.path?-1:x.path>q.path?1:0),V=[];for(let x of H){let q=S(x,W,Y.name),B=await D(q);if(B===null){if(await n(q).then(()=>!0).catch(()=>!1)&&!J)throw new M({agent:x,outputDir:q})}let G=B===null?"installed":B===Y.version?"up-to-date":"updated";if(G==="up-to-date"){V.push({agent:x,outputDir:q,files:[],status:"up-to-date"});continue}let F=G==="updated"?B??void 0:void 0;if($)await yQ(q);await AQ(q,k),V.push({agent:x,outputDir:q,files:k.map((h)=>h.path),status:G,previousVersion:F})}return{agents:V}}async function N(Q){let{name:Z,agents:X,scope:H="global"}=Q,W=I(Z),$=[];for(let J of X){let R=S(J,H,W);if(await n(R).then(()=>!0).catch(()=>!1))await a(R,{recursive:!0,force:!0}),$.push({agent:J,outputDir:R,status:"removed"});else $.push({agent:J,outputDir:R,status:"not-found"})}return{agents:$}}async function T(Q){let{name:Z,agents:X,scope:H="global"}=Q,W=I(Z),$=[];for(let J of X){let R=S(J,H,W),Y=await D(R);$.push({agent:J,outputDir:R,installed:Y!==null,version:Y??void 0})}return{agents:$}}function PQ(Q,Z){return[{path:C,content:bQ(Q,Z)}]}function bQ(Q,Z){let X=XQ(Q),H={name:Z.name,description:Z.description,version:Z.version,entrypoint:"SKILL.md",commands:X};return`${JSON.stringify(H,null,"\t")}
|
|
7
|
+
`}function XQ(Q){let Z=[Q.path.join(" ")];for(let X of Q.children)Z.push(...XQ(X));return Z}async function yQ(Q){await a(Q,{recursive:!0,force:!0})}async function AQ(Q,Z){let X=new Set;for(let W of Z){let $=t(Q,W.path),J=TQ($);X.add(J)}let H=[...X].sort();for(let W of H)await EQ(W,{recursive:!0});for(let W of Z){let $=t(Q,W.path);await LQ($,W.content,"utf-8")}}import{createCommandNode as CQ,VALIDATION_MODE_ENV as DQ}from"@crustjs/core";import{confirm as NQ,multiselect as vQ,spinner as b}from"@crustjs/prompts";import{bold as v,dim as P,yellow as ZQ}from"@crustjs/style";var HQ="skill",WQ="global";function $Q(Q,Z){return{name:Q.meta.name,description:Q.meta.description??"",version:Z}}async function hQ(Q,Z){let X=Z.scope??WQ,H=await E({scope:X});if(H.length===0)return;let W=$Q(Q,Z.version),J=(await T({name:W.name,agents:H,scope:X})).agents.filter((R)=>R.installed&&R.version!==W.version);if(J.length===0)return;try{await b({message:"Updating skills...",task:async({updateMessage:R})=>{let Y=await w({command:Q,meta:W,agents:J.map((O)=>O.agent),scope:Z.scope}),K=Y.agents.filter((O)=>O.status==="updated").map((O)=>j[O.agent]);if(K.length>0)R(`Updated skill "${I(W.name)}" to v${W.version} for ${K.join(", ")}`);return Y}})}catch(R){if(R instanceof M)console.warn(ZQ(`Skill conflict: "${R.details.outputDir}" already exists but was not created by ${W.name}. Skipping auto-update. Delete or rename the conflicting skill to resolve.`));else throw R}}function uQ(Q){let Z;return{name:"skills",async setup(X,H){Z=X.rootCommand;let W=typeof Q.command==="string"?Q.command:HQ;if(Q.command!==!1)H.addSubCommand(Z,W,pQ(Z,Q));if(process.env[DQ]==="1")return;if(Q.command!==!1&&X.argv[0]===W)return;if(Q.autoUpdate!==!1)await hQ(Z,Q)}}}function pQ(Q,Z){let X=CQ(HQ);return X.meta.description="Manage agent skill installations",X.run=async()=>{let H=$Q(Q,Z.version),W=Z.scope??WQ,$=await E({scope:W});if($.length===0){console.log(ZQ("No supported agents detected. Install Claude Code or OpenCode first."));return}let J=await T({name:H.name,agents:$,scope:W}),R=[],Y=J.agents.map((q)=>{let B=q.installed?`v${q.version} installed`:"not installed";if(q.installed)R.push(q.agent);return{label:j[q.agent],value:q.agent,hint:B}}),K=process.stdin.isTTY,O=await vQ({message:"Select agents to install skills for",choices:Y,default:R,initial:!K?$:void 0,required:!1}),y=O.filter((q)=>!R.includes(q)),k=O.filter((q)=>{let B=J.agents.find((G)=>G.agent===q);return B?.installed===!0&&B.version!==H.version}),V=R.filter((q)=>!O.includes(q)),x=[...y,...k];if(x.length>0)try{let q=await b({message:"Installing skills...",task:async()=>w({command:Q,meta:H,agents:x,scope:W})});console.log(`
|
|
8
|
+
${v(`Installed "${H.name}" v${H.version}`)}`);for(let B of q.agents)console.log(P(` ${j[B.agent]} \u2192 ${B.outputDir}`))}catch(q){if(q instanceof M)if(await NQ({message:`"${q.details.outputDir}" already exists but was not created by Crust. Overwrite?`,default:!1,initial:!K?!1:void 0})){let G=await b({message:"Overwriting skill...",task:async()=>w({command:Q,meta:H,agents:[q.details.agent],scope:W,force:!0})});console.log(`
|
|
9
|
+
${v(`Installed "${H.name}" v${H.version}`)}`);for(let F of G.agents)console.log(P(` ${j[F.agent]} \u2192 ${F.outputDir}`))}else console.log(P(`
|
|
10
|
+
Skipped ${j[q.details.agent]}`));else throw q}if(V.length>0){let B=(await b({message:"Removing skills...",task:async()=>N({name:H.name,agents:V,scope:W})})).agents.filter((G)=>G.status==="removed").map((G)=>j[G.agent]);if(B.length>0)console.log(`
|
|
11
|
+
${v(`Removed from ${B.join(", ")}`)}`)}if(x.length===0&&V.length===0)console.log(P("No changes."))},X}export{N as uninstallSkill,T as skillStatus,uQ as skillPlugin,I as resolveSkillName,QQ as isValidSkillName,w as generateSkill,E as detectInstalledAgents,M as SkillConflictError};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crustjs/skills",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Agent skill generation from Crust command definitions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,15 +41,17 @@
|
|
|
41
41
|
"check:types": "tsc --noEmit",
|
|
42
42
|
"test": "bun test"
|
|
43
43
|
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@crustjs/prompts": "0.0.7",
|
|
46
|
+
"@crustjs/style": "0.0.4"
|
|
47
|
+
},
|
|
44
48
|
"devDependencies": {
|
|
45
49
|
"@crustjs/config": "0.0.0",
|
|
46
|
-
"@crustjs/core": "0.0.
|
|
47
|
-
"@crustjs/prompts": "0.0.7",
|
|
50
|
+
"@crustjs/core": "0.0.10",
|
|
48
51
|
"bunup": "^0.16.29"
|
|
49
52
|
},
|
|
50
53
|
"peerDependencies": {
|
|
51
|
-
"@crustjs/core": "0.0.
|
|
52
|
-
"@crustjs/prompts": "0.0.7",
|
|
54
|
+
"@crustjs/core": "0.0.10",
|
|
53
55
|
"typescript": "^5"
|
|
54
56
|
}
|
|
55
57
|
}
|