@crustjs/skills 0.0.24 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +316 -59
- package/dist/index.d.ts +397 -14
- package/dist/index.js +15 -10
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Generate distributable AI agent skills from [Crust](https://crustjs.com) command definitions.
|
|
4
4
|
|
|
5
|
-
Instead of hand-maintaining skill files for AI coding agents, generate them from your
|
|
5
|
+
Instead of hand-maintaining skill files for AI coding agents, generate them from your Crust command metadata. The output is a portable skill bundle that developers can download and install into their own agent environments (OpenCode, Claude Code, etc.).
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -41,22 +41,15 @@ for (const agent of result.agents) {
|
|
|
41
41
|
|
|
42
42
|
### Runtime Plugin (`autoUpdate`)
|
|
43
43
|
|
|
44
|
-
`skillPlugin()`
|
|
45
|
-
Do not put a `plugins` field inside `defineCommand(...)`.
|
|
44
|
+
Register `skillPlugin()` on your `Crust` builder with `.use()`:
|
|
46
45
|
|
|
47
46
|
```ts
|
|
48
|
-
import {
|
|
47
|
+
import { Crust } from "@crustjs/core";
|
|
49
48
|
import { skillPlugin } from "@crustjs/skills";
|
|
50
49
|
|
|
51
|
-
const app =
|
|
52
|
-
meta
|
|
53
|
-
|
|
54
|
-
console.log("hello");
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
runMain(app, {
|
|
59
|
-
plugins: [
|
|
50
|
+
const app = new Crust("my-cli")
|
|
51
|
+
.meta({ description: "My CLI" })
|
|
52
|
+
.use(
|
|
60
53
|
skillPlugin({
|
|
61
54
|
version: "1.0.0",
|
|
62
55
|
instructions: `
|
|
@@ -71,53 +64,138 @@ Prefer readonly commands before mutating project state.
|
|
|
71
64
|
// defaultScope: "global" | "project" — skip scope prompt when set
|
|
72
65
|
// installMode: "auto" | "symlink" | "copy" (default: "auto")
|
|
73
66
|
}),
|
|
74
|
-
|
|
75
|
-
|
|
67
|
+
)
|
|
68
|
+
.run(() => {
|
|
69
|
+
console.log("hello");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await app.execute();
|
|
76
73
|
```
|
|
77
74
|
|
|
78
75
|
The plugin automatically updates already-installed skills when the version changes, checking both project and global paths for the current working directory. If the current working directory is the home directory, `project` scope is normalized to `global` so installs, updates, and status checks use the global skill locations. First-time installation is done via the interactive `skill` subcommand (or `skill update` for update-only flows), or programmatically using the exported primitives.
|
|
79
76
|
|
|
80
77
|
Generated bundles are written once to a canonical store (`.crust/skills` for project scope, `~/.crust/skills` for global scope) and then installed into agent paths via symlink or copy depending on `installMode`.
|
|
81
78
|
|
|
79
|
+
#### Hand-authored bundles via `customSkills`
|
|
80
|
+
|
|
81
|
+
The plugin can also manage **hand-authored** skill bundles alongside the
|
|
82
|
+
auto-generated command-reference skill. Pass an array of
|
|
83
|
+
`CustomSkillConfig` entries via `customSkills`; each entry is reconciled
|
|
84
|
+
through the same lifecycle as the main skill.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { Crust } from "@crustjs/core";
|
|
88
|
+
import { skillPlugin } from "@crustjs/skills";
|
|
89
|
+
import pkg from "./package.json" with { type: "json" };
|
|
90
|
+
|
|
91
|
+
const app = new Crust("my-cli")
|
|
92
|
+
.meta({ description: "My CLI" })
|
|
93
|
+
.use(
|
|
94
|
+
skillPlugin({
|
|
95
|
+
version: pkg.version,
|
|
96
|
+
customSkills: [
|
|
97
|
+
// Inherits `version: pkg.version` from the plugin — the typical
|
|
98
|
+
// case when the bundle ships in the same package as the CLI.
|
|
99
|
+
{
|
|
100
|
+
name: "funnel-builder",
|
|
101
|
+
// Resolved against the nearest package.json walking up from
|
|
102
|
+
// process.argv[1] — same rules as installSkillBundle().
|
|
103
|
+
sourceDir: "skills/funnel-builder",
|
|
104
|
+
},
|
|
105
|
+
// Explicit override for an independently-versioned bundle.
|
|
106
|
+
{
|
|
107
|
+
name: "vendored-toolkit",
|
|
108
|
+
sourceDir: "skills/vendored-toolkit",
|
|
109
|
+
version: "0.3.0",
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
}),
|
|
113
|
+
)
|
|
114
|
+
.run(() => {});
|
|
115
|
+
|
|
116
|
+
await app.execute();
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- **`name`** must satisfy `isValidSkillName` (1–64 lowercase alphanumeric
|
|
120
|
+
characters and hyphens, no leading/trailing/consecutive hyphens), must
|
|
121
|
+
be unique within the array, and must not collide with the main skill's
|
|
122
|
+
name. The bundle's `SKILL.md` frontmatter must declare a matching
|
|
123
|
+
`name:` field — mismatches are rejected at install time.
|
|
124
|
+
- **`sourceDir`** accepts a `URL` (`file:` protocol), an absolute path, or
|
|
125
|
+
a relative string resolved from the nearest `package.json`. Resolution
|
|
126
|
+
errors surface at install time, not at plugin setup.
|
|
127
|
+
- **`version`** is optional. When omitted, the bundle inherits the
|
|
128
|
+
plugin's top-level `version` — the typical case when the bundle ships
|
|
129
|
+
alongside the CLI. Pass an explicit value when the bundle's release
|
|
130
|
+
cadence is independent of the consuming CLI (for example, vendored
|
|
131
|
+
from another package). Identical-version reinstalls are skipped, so
|
|
132
|
+
bump the effective version whenever bundle contents change. The
|
|
133
|
+
bundle's `SKILL.md` frontmatter `version:` / `metadata.version`, if
|
|
134
|
+
any, is intentionally ignored — see the [`installSkillBundle()` note](#installing-hand-authored-bundles).
|
|
135
|
+
- **`scope`** and **`installMode`** are optional per-entry overrides;
|
|
136
|
+
unset values inherit from the plugin's `defaultScope` / `installMode`.
|
|
137
|
+
|
|
138
|
+
The interactive `skill` command shows one multiselect prompt per skill in
|
|
139
|
+
order: the main auto-generated skill first, then each `customSkills`
|
|
140
|
+
entry with its name in the prompt header (e.g. `"Select agents to install
|
|
141
|
+
skills for [funnel-builder]"`). Each prompt is independent — selecting
|
|
142
|
+
or deselecting agents reconciles only that skill's installs. `skill
|
|
143
|
+
--all` skips every prompt and installs every skill for the full agent
|
|
144
|
+
set; `skill update` updates outdated installs across main + every bundle.
|
|
145
|
+
|
|
146
|
+
Auto-update on plugin startup is per-skill: only outdated installs are
|
|
147
|
+
rewritten, and a single bundle's failure (e.g. missing `sourceDir`) does
|
|
148
|
+
not abort the others. Pass `autoUpdate: false` to disable startup auto-
|
|
149
|
+
update for both the main skill and all bundles.
|
|
150
|
+
|
|
82
151
|
### Programmatic Auto-Install
|
|
83
152
|
|
|
84
|
-
For full control over first-time installation,
|
|
85
|
-
directly
|
|
153
|
+
For full control over first-time installation, call `generateSkill()`
|
|
154
|
+
directly from your handler. With `agents` omitted, it installs into every
|
|
155
|
+
universal agent plus every additional agent whose CLI is on `PATH`, and
|
|
156
|
+
returns `up-to-date` for targets that already match the current version —
|
|
157
|
+
so the same call is safe to run on every invocation. Pass `agents: []` to
|
|
158
|
+
opt out, or an explicit array to scope the install.
|
|
86
159
|
|
|
87
160
|
```ts
|
|
88
|
-
import {
|
|
89
|
-
import {
|
|
90
|
-
|
|
91
|
-
const app =
|
|
92
|
-
meta
|
|
93
|
-
async
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
});
|
|
161
|
+
import { Crust } from "@crustjs/core";
|
|
162
|
+
import { generateSkill } from "@crustjs/skills";
|
|
163
|
+
|
|
164
|
+
export const app = new Crust("my-cli")
|
|
165
|
+
.meta({ description: "My CLI" })
|
|
166
|
+
.run(async (ctx) => {
|
|
167
|
+
// Defaults to universal + agents detected on PATH. Idempotent: targets
|
|
168
|
+
// that already match the current version are returned as `up-to-date`.
|
|
169
|
+
const result = await generateSkill({
|
|
170
|
+
command: ctx.command,
|
|
171
|
+
meta: {
|
|
172
|
+
name: ctx.command.meta.name,
|
|
173
|
+
description: ctx.command.meta.description ?? "",
|
|
174
|
+
version: "1.0.0",
|
|
175
|
+
},
|
|
176
|
+
scope: "global",
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const changed = result.agents.filter((a) => a.status !== "up-to-date");
|
|
180
|
+
if (changed.length > 0) {
|
|
181
|
+
console.log(`Installed or updated skills for ${changed.length} target(s).`);
|
|
109
182
|
}
|
|
110
|
-
}
|
|
111
|
-
});
|
|
183
|
+
});
|
|
112
184
|
|
|
113
|
-
|
|
185
|
+
if (import.meta.main) {
|
|
186
|
+
await app.execute();
|
|
187
|
+
}
|
|
114
188
|
```
|
|
115
189
|
|
|
190
|
+
`getUniversalAgents()`, `getAdditionalAgents()`, and
|
|
191
|
+
`detectInstalledAgents()` remain exported for callers that want to compose
|
|
192
|
+
their own agent list.
|
|
193
|
+
|
|
116
194
|
#### Troubleshooting
|
|
117
195
|
|
|
118
196
|
If auto-update does not appear to work:
|
|
119
197
|
|
|
120
|
-
- Ensure
|
|
198
|
+
- Ensure `skillPlugin(...)` is registered on the `Crust` builder via `.use()`.
|
|
121
199
|
- Ensure at least one supported agent is detected. Auto-update checks both project and global install paths, with home-directory `project` scope treated as `global`.
|
|
122
200
|
- Check for existing conflicting skill directories without `crust.json`.
|
|
123
201
|
|
|
@@ -126,19 +204,18 @@ If auto-update does not appear to work:
|
|
|
126
204
|
To avoid side effects when your command module is imported for generation, guard runtime code with `import.meta.main`:
|
|
127
205
|
|
|
128
206
|
```ts
|
|
129
|
-
import {
|
|
207
|
+
import { Crust } from "@crustjs/core";
|
|
130
208
|
|
|
131
|
-
// Export the command
|
|
132
|
-
export const rootCommand =
|
|
133
|
-
meta
|
|
134
|
-
run({ args }) {
|
|
209
|
+
// Export the command — used by skill generation.
|
|
210
|
+
export const rootCommand = new Crust("my-cli")
|
|
211
|
+
.meta({ description: "My CLI tool" })
|
|
212
|
+
.run(({ args }) => {
|
|
135
213
|
console.log("Hello from my-cli!");
|
|
136
|
-
}
|
|
137
|
-
});
|
|
214
|
+
});
|
|
138
215
|
|
|
139
|
-
// Only
|
|
216
|
+
// Only execute when run directly — not when imported for generation.
|
|
140
217
|
if (import.meta.main) {
|
|
141
|
-
|
|
218
|
+
await rootCommand.execute();
|
|
142
219
|
}
|
|
143
220
|
```
|
|
144
221
|
|
|
@@ -189,7 +266,7 @@ Read command docs before suggesting exact flags.
|
|
|
189
266
|
.command(deploy);
|
|
190
267
|
```
|
|
191
268
|
|
|
192
|
-
This pattern lets `crust skills generate` import the command definition without triggering `
|
|
269
|
+
This pattern lets `crust skills generate` import the command definition without triggering `app.execute()`.
|
|
193
270
|
|
|
194
271
|
## CLI Usage
|
|
195
272
|
|
|
@@ -379,13 +456,30 @@ skills/my-cli/
|
|
|
379
456
|
|
|
380
457
|
## Conflict Detection
|
|
381
458
|
|
|
382
|
-
Each skill directory contains a `crust.json` file that acts
|
|
459
|
+
Each Crust-managed skill directory contains a `crust.json` file that acts
|
|
460
|
+
as an ownership marker. Both `generateSkill()` and `installSkillBundle()`
|
|
461
|
+
refuse to overwrite a target directory in three cases:
|
|
383
462
|
|
|
384
|
-
|
|
463
|
+
1. **No `crust.json`** — the directory exists but was not created by Crust
|
|
464
|
+
(e.g. manually authored or installed by another tool).
|
|
465
|
+
2. **Kind mismatch** — `crust.json` records a different
|
|
466
|
+
[`kind`](#kind-field-on-crustjson) than the install attempt (e.g. an
|
|
467
|
+
existing `generated` skill collides with an incoming `bundle`).
|
|
468
|
+
3. **Malformed manifest** — `crust.json` is present but cannot be
|
|
469
|
+
interpreted (invalid JSON, top-level non-object, missing `version`, or
|
|
470
|
+
an unrecognized `kind` value such as a hand-edit typo).
|
|
385
471
|
|
|
386
|
-
|
|
472
|
+
All three throw `SkillConflictError`. Pass `force: true` to overwrite, or
|
|
473
|
+
uninstall the existing skill first.
|
|
474
|
+
|
|
475
|
+
`SkillConflictError.details` carries optional discriminators:
|
|
387
476
|
|
|
388
|
-
|
|
477
|
+
- `details.kindMismatch?: { existing, attempted }` — set on kind mismatch.
|
|
478
|
+
- `details.manifestMalformed?: { reason, rawKind? }` — set on malformed
|
|
479
|
+
`crust.json`. `reason` is one of `"parse-error"`, `"not-an-object"`,
|
|
480
|
+
`"missing-version"`, or `"unknown-kind"`. `rawKind` is populated only
|
|
481
|
+
when `reason === "unknown-kind"`.
|
|
482
|
+
- Neither field set — the original "directory exists with no `crust.json`" case.
|
|
389
483
|
|
|
390
484
|
```ts
|
|
391
485
|
import { generateSkill, SkillConflictError } from "@crustjs/skills";
|
|
@@ -393,13 +487,31 @@ import { generateSkill, SkillConflictError } from "@crustjs/skills";
|
|
|
393
487
|
try {
|
|
394
488
|
await generateSkill({ command, meta, agents });
|
|
395
489
|
} catch (err) {
|
|
396
|
-
if (err instanceof SkillConflictError)
|
|
397
|
-
|
|
398
|
-
|
|
490
|
+
if (!(err instanceof SkillConflictError)) throw err;
|
|
491
|
+
|
|
492
|
+
if (err.details.kindMismatch) {
|
|
493
|
+
const { existing, attempted } = err.details.kindMismatch;
|
|
494
|
+
console.error(
|
|
495
|
+
`Cannot install ${attempted} skill at ${err.details.outputDir} — ` +
|
|
496
|
+
`existing skill was installed as ${existing}.`,
|
|
497
|
+
);
|
|
498
|
+
} else if (err.details.manifestMalformed) {
|
|
499
|
+
console.error(
|
|
500
|
+
`crust.json at ${err.details.outputDir} is malformed: ` +
|
|
501
|
+
`${err.details.manifestMalformed.reason}.`,
|
|
502
|
+
);
|
|
503
|
+
} else {
|
|
504
|
+
console.error(
|
|
505
|
+
`${err.details.outputDir} exists but was not created by Crust.`,
|
|
506
|
+
);
|
|
399
507
|
}
|
|
400
508
|
}
|
|
401
509
|
```
|
|
402
510
|
|
|
511
|
+
### Uninstall Cleanup
|
|
512
|
+
|
|
513
|
+
When `uninstallSkill()` removes agent install paths, it also checks whether any other agent paths still reference the skill. If no agent installs remain, the canonical store entry (`.crust/skills/<skill>` or `~/.crust/skills/<skill>`) is automatically removed.
|
|
514
|
+
|
|
403
515
|
## Installing Generated Skills
|
|
404
516
|
|
|
405
517
|
After generating a skill bundle, consumers can install it by copying the skill directory.
|
|
@@ -424,6 +536,151 @@ cp -r skills/my-cli/ .claude/skills/my-cli/
|
|
|
424
536
|
|
|
425
537
|
The agent will discover the skill from `SKILL.md` and load command documentation on demand from the `commands/` directory.
|
|
426
538
|
|
|
539
|
+
## Installing Hand-Authored Bundles
|
|
540
|
+
|
|
541
|
+
`generateSkill()` produces a skill bundle from a Crust command tree.
|
|
542
|
+
`installSkillBundle()` is the dual entrypoint for **hand-authored** bundles —
|
|
543
|
+
use it when you have a directory containing `SKILL.md` and any supporting
|
|
544
|
+
files that you want to install through the same canonical-store + agent
|
|
545
|
+
fan-out pipeline.
|
|
546
|
+
|
|
547
|
+
Use `installSkillBundle()` when:
|
|
548
|
+
|
|
549
|
+
- You ship a published CLI package that bundles authored skill directories
|
|
550
|
+
alongside generated ones.
|
|
551
|
+
- The skill's `SKILL.md` is hand-curated (or produced by your own renderer)
|
|
552
|
+
and Crust just needs to handle install plumbing — canonical storage,
|
|
553
|
+
symlink/copy fan-out, version tracking, and conflict detection.
|
|
554
|
+
|
|
555
|
+
```ts
|
|
556
|
+
import { installSkillBundle } from "@crustjs/skills";
|
|
557
|
+
import pkg from "./package.json" with { type: "json" };
|
|
558
|
+
|
|
559
|
+
await installSkillBundle({
|
|
560
|
+
// Resolved relative to the nearest package.json walking up from
|
|
561
|
+
// process.argv[1]. You can also pass an absolute string or a file: URL.
|
|
562
|
+
sourceDir: "skills/funnel-builder",
|
|
563
|
+
agents: ["claude-code", "opencode"],
|
|
564
|
+
version: pkg.version,
|
|
565
|
+
});
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Where `name`, `description`, and `version` come from
|
|
569
|
+
|
|
570
|
+
The bundle's `SKILL.md` frontmatter is the source of truth for `name` and
|
|
571
|
+
`description` — Crust reads them but never rewrites the file. Both fields
|
|
572
|
+
are required:
|
|
573
|
+
|
|
574
|
+
```yaml
|
|
575
|
+
---
|
|
576
|
+
name: funnel-builder
|
|
577
|
+
description: Build a sales funnel
|
|
578
|
+
---
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
`version` is supplied by the caller and recorded in `crust.json`. Wiring
|
|
582
|
+
it to the consuming package's `package.json` `version` (as in the example
|
|
583
|
+
above) is the typical pattern; pass any string explicitly when one package
|
|
584
|
+
publishes multiple bundles with independent versions:
|
|
585
|
+
|
|
586
|
+
```ts
|
|
587
|
+
await installSkillBundle({
|
|
588
|
+
sourceDir: "skills/funnel-builder",
|
|
589
|
+
agents: ["claude-code"],
|
|
590
|
+
version: "2.0.0",
|
|
591
|
+
});
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
> **Note:** `metadata.version` declared inside the bundle's SKILL.md
|
|
595
|
+
> frontmatter is **not** read — the `version` option is the sole source of
|
|
596
|
+
> truth for `crust.json` and update detection. If you keep a
|
|
597
|
+
> `metadata.version` in your SKILL.md for Agent Skills spec compliance,
|
|
598
|
+
> keep it in sync with the value you pass here.
|
|
599
|
+
|
|
600
|
+
### Options
|
|
601
|
+
|
|
602
|
+
| Option | Type | Default | Description |
|
|
603
|
+
| ------------- | ----------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
604
|
+
| `sourceDir` | `string \| URL` | — required | Bundle directory. Absolute path, `file:` URL, or relative path resolved from the nearest `package.json`. |
|
|
605
|
+
| `agents` | `AgentTarget[]` | — required | Agents to install for. `[]` validates the bundle without installing (no auto-detection — unlike `generateSkill()`). |
|
|
606
|
+
| `version` | `string` | — required | Recorded in `crust.json` and compared on subsequent installs. |
|
|
607
|
+
| `scope` | `"global" \| "project"` | `"global"` | Install scope. When `process.cwd()` is the home directory, `"project"` normalizes to `"global"`. |
|
|
608
|
+
| `installMode` | `"auto" \| "symlink" \| "copy"` | `"auto"` | Same semantics as `generateSkill()`. `"auto"` symlinks from the canonical store, falling back to copy. |
|
|
609
|
+
| `clean` | `boolean` | `true` | Remove the existing skill directory before writing. |
|
|
610
|
+
| `force` | `boolean` | `false` | Overwrite a conflicting directory (no `crust.json`, kind mismatch, or malformed manifest) instead of throwing. |
|
|
611
|
+
|
|
612
|
+
### What gets copied
|
|
613
|
+
|
|
614
|
+
The bundle's `SKILL.md` plus every supporting file is copied into the
|
|
615
|
+
canonical Crust store — markdown, configs, scripts, images, fonts, and other
|
|
616
|
+
assets. Bundle files are copied as raw bytes; `SKILL.md` is also parsed as
|
|
617
|
+
UTF-8 to read its required frontmatter.
|
|
618
|
+
|
|
619
|
+
Bundle content changes do not propagate without a `version` bump:
|
|
620
|
+
identical-version reinstalls report `up-to-date` and leave the canonical
|
|
621
|
+
store untouched. Pass a fresh `version` (typically wired to the consuming
|
|
622
|
+
package's `package.json` `version`) whenever the bundle contents change.
|
|
623
|
+
|
|
624
|
+
Bundle contents are copied as authored — no implicit name-based filtering.
|
|
625
|
+
Dotfiles, `node_modules/`, `.DS_Store`, and editor cruft are all copied if
|
|
626
|
+
present. Keep `sourceDir` clean. The only reserved filename is
|
|
627
|
+
`crust.json` at the bundle root (Crust generates this and rejects bundles
|
|
628
|
+
that ship one).
|
|
629
|
+
|
|
630
|
+
### Publishing a bundle to npm
|
|
631
|
+
|
|
632
|
+
Two gotchas trip up bundle authors who publish to npm:
|
|
633
|
+
|
|
634
|
+
1. **Include the bundle directory in the published tarball.** Add the path
|
|
635
|
+
to your `package.json` `files` array and verify with `npm pack --dry-run`
|
|
636
|
+
before publishing. Local installs work even when the directory would be
|
|
637
|
+
excluded from the tarball, but consumers will hit a missing-`SKILL.md`
|
|
638
|
+
error.
|
|
639
|
+
|
|
640
|
+
```json
|
|
641
|
+
{
|
|
642
|
+
"name": "acme-skills",
|
|
643
|
+
"version": "1.0.0",
|
|
644
|
+
"files": ["dist", "skills"]
|
|
645
|
+
}
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
2. **Consumers point at the published path with `import.meta.resolve`.**
|
|
649
|
+
Relative `sourceDir` resolution walks up from the consumer's
|
|
650
|
+
`process.argv[1]`, so it lands in the consumer's package — not yours.
|
|
651
|
+
Consumers should use a `file:` URL via `import.meta.resolve`:
|
|
652
|
+
|
|
653
|
+
```ts
|
|
654
|
+
import skillsPkg from "acme-skills/package.json" with { type: "json" };
|
|
655
|
+
|
|
656
|
+
await installSkillBundle({
|
|
657
|
+
sourceDir: new URL(import.meta.resolve("acme-skills/skills/funnel-builder")),
|
|
658
|
+
agents: ["claude-code"],
|
|
659
|
+
version: skillsPkg.version,
|
|
660
|
+
});
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
For this to work, the bundle directory must be reachable from your
|
|
664
|
+
package's `exports` (or accessible as a subpath of the package root).
|
|
665
|
+
Bundle authors who want explicit subpath access can declare it in
|
|
666
|
+
`package.json` `exports`.
|
|
667
|
+
|
|
668
|
+
### `kind` field on `crust.json`
|
|
669
|
+
|
|
670
|
+
Every installed bundle records its origin in `crust.json` as a `kind`
|
|
671
|
+
field: `"generated"` for `generateSkill()` output, `"bundle"` for
|
|
672
|
+
`installSkillBundle()`. This prevents accidental cross-overwrites:
|
|
673
|
+
|
|
674
|
+
- Trying to install a bundle on top of a generated skill (or vice versa) at
|
|
675
|
+
the same name throws a `SkillConflictError` whose `details.kindMismatch`
|
|
676
|
+
carries `{ existing, attempted }`.
|
|
677
|
+
- To proceed anyway, uninstall the existing skill first or pass
|
|
678
|
+
`force: true`.
|
|
679
|
+
|
|
680
|
+
Legacy `crust.json` files written before this field existed are read as
|
|
681
|
+
`kind: "generated"` for backward compatibility — generated installs continue
|
|
682
|
+
to update cleanly with no migration step.
|
|
683
|
+
|
|
427
684
|
## Documentation
|
|
428
685
|
|
|
429
686
|
See the full docs at [crustjs.com](https://crustjs.com).
|
package/dist/index.d.ts
CHANGED
|
@@ -89,6 +89,20 @@ type Scope = "global" | "project";
|
|
|
89
89
|
/** Installation strategy for agent skill output paths. */
|
|
90
90
|
type SkillInstallMode = "auto" | "symlink" | "copy";
|
|
91
91
|
/**
|
|
92
|
+
* Origin of an installed skill bundle.
|
|
93
|
+
*
|
|
94
|
+
* Recorded in `crust.json` as the top-level `kind` field so Crust can detect
|
|
95
|
+
* when a generated and a hand-authored bundle would collide on the same name.
|
|
96
|
+
*
|
|
97
|
+
* - `"generated"` — produced by {@link generateSkill} from a Crust command tree.
|
|
98
|
+
* - `"bundle"` — installed by {@link installSkillBundle} from a hand-authored
|
|
99
|
+
* directory containing a `SKILL.md` and supporting files.
|
|
100
|
+
*
|
|
101
|
+
* Legacy `crust.json` files (written before this field existed) are read as
|
|
102
|
+
* `"generated"` for backward compatibility.
|
|
103
|
+
*/
|
|
104
|
+
type SkillKind = "generated" | "bundle";
|
|
105
|
+
/**
|
|
92
106
|
* Top-level options for generating a skill bundle from a command tree.
|
|
93
107
|
*
|
|
94
108
|
* The `meta.name` value is used directly for all output paths and metadata.
|
|
@@ -116,8 +130,19 @@ interface GenerateOptions {
|
|
|
116
130
|
command: CommandNode;
|
|
117
131
|
/** Skill metadata for the generated bundle */
|
|
118
132
|
meta: SkillMeta;
|
|
119
|
-
/**
|
|
120
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Agent targets to install skills for.
|
|
135
|
+
*
|
|
136
|
+
* When omitted (or explicitly `undefined`), defaults to
|
|
137
|
+
* `[...getUniversalAgents(), ...await detectInstalledAgents()]` — the union
|
|
138
|
+
* of always-included universal agents and additional agents whose CLI is
|
|
139
|
+
* detected on `PATH`. Pass an explicit array to override; `agents: []`
|
|
140
|
+
* is treated as a no-op (no install performed).
|
|
141
|
+
*
|
|
142
|
+
* **Note:** Omitting this field performs filesystem I/O via
|
|
143
|
+
* `detectInstalledAgents()` to probe `PATH` for installed agent CLIs.
|
|
144
|
+
*/
|
|
145
|
+
agents?: AgentTarget[];
|
|
121
146
|
/**
|
|
122
147
|
* Installation strategy for agent output paths.
|
|
123
148
|
*
|
|
@@ -152,6 +177,111 @@ interface GenerateOptions {
|
|
|
152
177
|
*/
|
|
153
178
|
force?: boolean;
|
|
154
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Top-level options for installing a hand-authored skill bundle.
|
|
182
|
+
*
|
|
183
|
+
* Unlike {@link GenerateOptions}, the bundle entrypoint does not render
|
|
184
|
+
* `SKILL.md` from a command tree — it copies a directory the caller has
|
|
185
|
+
* already authored. The bundle's `SKILL.md` frontmatter is the source of
|
|
186
|
+
* truth for `name` and `description`; Crust reads them but does not rewrite
|
|
187
|
+
* the file. A fresh `crust.json` is written alongside the bundle for
|
|
188
|
+
* ownership and version tracking.
|
|
189
|
+
*
|
|
190
|
+
* Bundle files are copied as raw bytes. `SKILL.md` is also parsed as UTF-8
|
|
191
|
+
* to read its required frontmatter.
|
|
192
|
+
*
|
|
193
|
+
* Bundle content changes do not propagate without a `version` bump:
|
|
194
|
+
* identical-version reinstalls report `up-to-date` and leave the canonical
|
|
195
|
+
* store untouched. Pass a fresh `version` whenever the bundle contents
|
|
196
|
+
* change (e.g. wire it to the consuming package's `package.json` `version`).
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```ts
|
|
200
|
+
* import { installSkillBundle } from "@crustjs/skills";
|
|
201
|
+
* import pkg from "./package.json" with { type: "json" };
|
|
202
|
+
*
|
|
203
|
+
* // SKILL.md frontmatter supplies name + description; the caller passes
|
|
204
|
+
* // the version explicitly (typically wired to package.json).
|
|
205
|
+
* await installSkillBundle({
|
|
206
|
+
* sourceDir: "skills/funnel-builder",
|
|
207
|
+
* agents: ["claude-code"],
|
|
208
|
+
* version: pkg.version,
|
|
209
|
+
* });
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
interface InstallSkillBundleOptions {
|
|
213
|
+
/**
|
|
214
|
+
* Source directory containing the bundle to install.
|
|
215
|
+
*
|
|
216
|
+
* Resolution rules (mirror `@crustjs/create`'s `scaffold({ template })`):
|
|
217
|
+
* - `URL` — must use `file:` protocol; resolved via `fileURLToPath()`.
|
|
218
|
+
* - Absolute string path — used as-is via `path.resolve()`.
|
|
219
|
+
* - Relative string path — resolved from the nearest `package.json`
|
|
220
|
+
* directory walking up from `process.argv[1]`. Throws if `process.argv[1]`
|
|
221
|
+
* is unset or no `package.json` is found.
|
|
222
|
+
*
|
|
223
|
+
* The directory must contain a `SKILL.md` whose YAML frontmatter declares
|
|
224
|
+
* top-level `name:` and `description:` fields.
|
|
225
|
+
*/
|
|
226
|
+
sourceDir: string | URL;
|
|
227
|
+
/**
|
|
228
|
+
* Agent targets to install the bundle for.
|
|
229
|
+
*
|
|
230
|
+
* Required — unlike {@link GenerateOptions.agents}, the bundle entrypoint
|
|
231
|
+
* does not auto-detect agents. Pass `[]` for a validated no-op: no install
|
|
232
|
+
* is performed, but `sourceDir`, `SKILL.md`, bundle paths, frontmatter, and
|
|
233
|
+
* skill name are still validated.
|
|
234
|
+
*/
|
|
235
|
+
agents: AgentTarget[];
|
|
236
|
+
/**
|
|
237
|
+
* Version string recorded for this install and compared on subsequent
|
|
238
|
+
* installs to decide between `installed` / `updated` / `up-to-date`.
|
|
239
|
+
*
|
|
240
|
+
* Required. Typically wired to the consuming package's `package.json`
|
|
241
|
+
* `version` (e.g. via `import pkg from "./package.json" with { type:
|
|
242
|
+
* "json" }`). Identical-version reinstalls report `up-to-date` and skip
|
|
243
|
+
* the canonical-store rewrite, so bump this whenever bundle contents
|
|
244
|
+
* change.
|
|
245
|
+
*/
|
|
246
|
+
version: string;
|
|
247
|
+
/**
|
|
248
|
+
* Installation strategy for agent output paths.
|
|
249
|
+
* @default "auto"
|
|
250
|
+
*/
|
|
251
|
+
installMode?: SkillInstallMode;
|
|
252
|
+
/**
|
|
253
|
+
* Installation scope — global (home directory) or project (cwd).
|
|
254
|
+
* @default "global"
|
|
255
|
+
*/
|
|
256
|
+
scope?: Scope;
|
|
257
|
+
/**
|
|
258
|
+
* When `true`, removes the existing skill directory before writing.
|
|
259
|
+
* @default true
|
|
260
|
+
*/
|
|
261
|
+
clean?: boolean;
|
|
262
|
+
/**
|
|
263
|
+
* When `true`, overwrite an existing skill directory even if it conflicts
|
|
264
|
+
* (no `crust.json`, or a `crust.json` whose `kind` differs from `"bundle"`).
|
|
265
|
+
* @default false
|
|
266
|
+
*/
|
|
267
|
+
force?: boolean;
|
|
268
|
+
/**
|
|
269
|
+
* When set, the bundle's `SKILL.md` frontmatter `name:` must equal this
|
|
270
|
+
* string. A mismatch throws before any filesystem write.
|
|
271
|
+
*
|
|
272
|
+
* Used by `skillPlugin`'s `customSkills` reconciliation to keep the
|
|
273
|
+
* config-level `name` (used for status / uninstall lookups) in lockstep
|
|
274
|
+
* with the frontmatter `name` (the canonical install path), preventing
|
|
275
|
+
* orphan installs.
|
|
276
|
+
*/
|
|
277
|
+
expectedName?: string;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Result returned by `installSkillBundle` after writing files to disk.
|
|
281
|
+
*
|
|
282
|
+
* Type alias of {@link GenerateResult} — the per-agent shape is identical.
|
|
283
|
+
*/
|
|
284
|
+
type InstallSkillBundleResult = GenerateResult;
|
|
155
285
|
/** Status of an individual agent installation. */
|
|
156
286
|
type InstallStatus = "installed" | "updated" | "up-to-date";
|
|
157
287
|
/** Status of an individual agent uninstallation. */
|
|
@@ -180,8 +310,19 @@ interface GenerateResult {
|
|
|
180
310
|
interface UninstallOptions {
|
|
181
311
|
/** Skill name to uninstall */
|
|
182
312
|
name: string;
|
|
183
|
-
/**
|
|
184
|
-
|
|
313
|
+
/**
|
|
314
|
+
* Agent targets to uninstall from.
|
|
315
|
+
*
|
|
316
|
+
* When omitted (or explicitly `undefined`), defaults to every supported
|
|
317
|
+
* agent so the uninstall sweep covers any path that may hold an install,
|
|
318
|
+
* regardless of what is on the current machine's `PATH`. Pass an explicit
|
|
319
|
+
* array to scope the uninstall; `agents: []` is treated as a no-op (no
|
|
320
|
+
* paths are touched).
|
|
321
|
+
*
|
|
322
|
+
* Default resolution does not perform `PATH` I/O — the entrypoint already
|
|
323
|
+
* stats each per-agent path during the sweep.
|
|
324
|
+
*/
|
|
325
|
+
agents?: AgentTarget[];
|
|
185
326
|
/**
|
|
186
327
|
* Installation scope to uninstall from.
|
|
187
328
|
* When `process.cwd()` is the home directory, `"project"` is treated as `"global"`.
|
|
@@ -202,8 +343,19 @@ interface UninstallResult {
|
|
|
202
343
|
interface StatusOptions {
|
|
203
344
|
/** Skill name to check */
|
|
204
345
|
name: string;
|
|
205
|
-
/**
|
|
206
|
-
|
|
346
|
+
/**
|
|
347
|
+
* Agent targets to check.
|
|
348
|
+
*
|
|
349
|
+
* When omitted (or explicitly `undefined`), defaults to every supported
|
|
350
|
+
* agent so the status sweep reports an entry for any path that may hold
|
|
351
|
+
* an install, regardless of what is on the current machine's `PATH`. Pass
|
|
352
|
+
* an explicit array to scope the check; `agents: []` is treated as a no-op
|
|
353
|
+
* (returns an empty result).
|
|
354
|
+
*
|
|
355
|
+
* Default resolution does not perform `PATH` I/O — the entrypoint already
|
|
356
|
+
* stats each per-agent path during the sweep.
|
|
357
|
+
*/
|
|
358
|
+
agents?: AgentTarget[];
|
|
207
359
|
/**
|
|
208
360
|
* Installation scope to check.
|
|
209
361
|
* When `process.cwd()` is the home directory, `"project"` is treated as `"global"`.
|
|
@@ -222,6 +374,88 @@ interface StatusResult {
|
|
|
222
374
|
}>;
|
|
223
375
|
}
|
|
224
376
|
/**
|
|
377
|
+
* Configuration for a single hand-authored skill bundle managed by
|
|
378
|
+
* {@link skillPlugin} alongside the auto-generated command-reference skill.
|
|
379
|
+
*
|
|
380
|
+
* Each entry is reconciled through the same plugin lifecycle as the main
|
|
381
|
+
* skill — auto-update on version change, surfaced in the interactive `skill`
|
|
382
|
+
* subcommand multiselect, supports uninstall via the same toggle UX, and
|
|
383
|
+
* respects `autoUpdate: false` and `--all` non-interactive mode. Bundles
|
|
384
|
+
* inherit `version`, `defaultScope`, and `installMode` from the plugin
|
|
385
|
+
* unless overridden per-entry.
|
|
386
|
+
*
|
|
387
|
+
* The bundle's `SKILL.md` frontmatter remains the source of truth for the
|
|
388
|
+
* display `name` and `description` (validated by {@link installSkillBundle}
|
|
389
|
+
* at install time). The duplicated `name` field on this config is what the
|
|
390
|
+
* plugin uses for cheap collision-detection, status lookups, and uninstall
|
|
391
|
+
* paths without having to read the bundle's frontmatter at plugin setup.
|
|
392
|
+
*
|
|
393
|
+
* @example
|
|
394
|
+
* ```ts
|
|
395
|
+
* import { skillPlugin } from "@crustjs/skills";
|
|
396
|
+
* import pkg from "./package.json" with { type: "json" };
|
|
397
|
+
*
|
|
398
|
+
* skillPlugin({
|
|
399
|
+
* version: pkg.version,
|
|
400
|
+
* customSkills: [
|
|
401
|
+
* // Inherits `version: pkg.version` from the plugin.
|
|
402
|
+
* { name: "funnel-builder", sourceDir: "skills/funnel-builder" },
|
|
403
|
+
* // Explicit override for an independently-versioned bundle.
|
|
404
|
+
* {
|
|
405
|
+
* name: "vendored-toolkit",
|
|
406
|
+
* sourceDir: "skills/vendored-toolkit",
|
|
407
|
+
* version: "0.3.0",
|
|
408
|
+
* },
|
|
409
|
+
* ],
|
|
410
|
+
* });
|
|
411
|
+
* ```
|
|
412
|
+
*/
|
|
413
|
+
interface CustomSkillConfig extends Pick<InstallSkillBundleOptions, "sourceDir" | "scope" | "installMode"> {
|
|
414
|
+
/**
|
|
415
|
+
* Skill name used by the plugin for collision detection, status lookups,
|
|
416
|
+
* and uninstall paths.
|
|
417
|
+
*
|
|
418
|
+
* Must satisfy `isValidSkillName` (1–64 lowercase alphanumeric characters
|
|
419
|
+
* and hyphens, no leading/trailing/consecutive hyphens), must be unique
|
|
420
|
+
* within the `customSkills` array, and must not collide with the main
|
|
421
|
+
* skill's name (derived from the root command's `meta`).
|
|
422
|
+
*
|
|
423
|
+
* The bundle's `SKILL.md` frontmatter `name:` must match this value —
|
|
424
|
+
* mismatches are rejected at install time so plugin status / uninstall
|
|
425
|
+
* paths can never drift from the canonical install location.
|
|
426
|
+
*/
|
|
427
|
+
name: string;
|
|
428
|
+
/**
|
|
429
|
+
* Version override. When omitted, the bundle inherits the plugin's
|
|
430
|
+
* top-level {@link SkillPluginOptions.version}. Drives auto-update
|
|
431
|
+
* detection: a bundle is reinstalled when its recorded `crust.json`
|
|
432
|
+
* version differs from the effective (entry-or-plugin) version.
|
|
433
|
+
*
|
|
434
|
+
* Inheriting from the plugin matches the typical case where the bundle
|
|
435
|
+
* ships in the same package as the consuming CLI — one `pkg.version`
|
|
436
|
+
* drives the main skill and every bundle. Pass an explicit value when a
|
|
437
|
+
* bundle's release cadence is independent of the consuming CLI (for
|
|
438
|
+
* example, vendored from another package).
|
|
439
|
+
*
|
|
440
|
+
* The bundle's `SKILL.md` frontmatter `version:` / `metadata.version`,
|
|
441
|
+
* if any, is intentionally not read — this option (or its plugin-level
|
|
442
|
+
* fallback) is the sole source of truth.
|
|
443
|
+
*/
|
|
444
|
+
version?: InstallSkillBundleOptions["version"];
|
|
445
|
+
/**
|
|
446
|
+
* Installation scope override. When omitted, the bundle inherits
|
|
447
|
+
* {@link SkillPluginOptions.defaultScope} resolution: explicit `--scope`
|
|
448
|
+
* flag wins, else `defaultScope`, else the interactive scope prompt
|
|
449
|
+
* (or `"global"` in non-interactive mode).
|
|
450
|
+
*/
|
|
451
|
+
scope?: InstallSkillBundleOptions["scope"];
|
|
452
|
+
/**
|
|
453
|
+
* Installation strategy override. When omitted, inherits
|
|
454
|
+
* {@link SkillPluginOptions.installMode} (default `"auto"`).
|
|
455
|
+
*/
|
|
456
|
+
installMode?: InstallSkillBundleOptions["installMode"];
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
225
459
|
* Options for the skill plugin.
|
|
226
460
|
*
|
|
227
461
|
* The plugin reads `name` and `description` from the root command's `meta`
|
|
@@ -297,6 +531,44 @@ interface SkillPluginOptions {
|
|
|
297
531
|
*/
|
|
298
532
|
disableModelInvocation?: boolean;
|
|
299
533
|
/**
|
|
534
|
+
* Hand-authored skill bundles to manage alongside the auto-generated
|
|
535
|
+
* command-reference skill.
|
|
536
|
+
*
|
|
537
|
+
* Each entry is reconciled through the same plugin lifecycle as the main
|
|
538
|
+
* skill — auto-update on version change, surfaced in the interactive
|
|
539
|
+
* `skill` subcommand multiselect (one prompt per bundle, in array order,
|
|
540
|
+
* after the main-skill prompt), supports uninstall via the same toggle
|
|
541
|
+
* UX, and respects `autoUpdate: false` and `--all` non-interactive mode.
|
|
542
|
+
*
|
|
543
|
+
* Bundles share the canonical `.crust/skills` store with the main skill
|
|
544
|
+
* via {@link installSkillBundle} and inherit `defaultScope` /
|
|
545
|
+
* `installMode` resolution unless overridden per-entry.
|
|
546
|
+
*
|
|
547
|
+
* Each entry's effective `version` drives auto-update detection (compared
|
|
548
|
+
* against the recorded `crust.json` version). When the entry omits
|
|
549
|
+
* `version`, the plugin's top-level {@link SkillPluginOptions.version} is
|
|
550
|
+
* used — the typical case when the bundle ships in the same package as
|
|
551
|
+
* the consuming CLI.
|
|
552
|
+
*
|
|
553
|
+
* Setup-time validation enforces:
|
|
554
|
+
* - Each `name` satisfies `isValidSkillName`.
|
|
555
|
+
* - No `name` collides with the main skill's name.
|
|
556
|
+
* - All `name` values are unique within the array.
|
|
557
|
+
* - When set, `version` is a non-empty string.
|
|
558
|
+
* - Each `sourceDir` is a `string` or `URL`.
|
|
559
|
+
*
|
|
560
|
+
* `sourceDir` resolution-time errors (non-`file:` URL, missing source
|
|
561
|
+
* directory, missing `SKILL.md`, etc.) defer to the underlying
|
|
562
|
+
* `installSkillBundle` invocation and surface there with descriptive
|
|
563
|
+
* messages.
|
|
564
|
+
*
|
|
565
|
+
* When omitted or empty, plugin behavior is byte-identical to running
|
|
566
|
+
* without the option — only the auto-generated main skill is managed.
|
|
567
|
+
*
|
|
568
|
+
* @default []
|
|
569
|
+
*/
|
|
570
|
+
customSkills?: CustomSkillConfig[];
|
|
571
|
+
/**
|
|
300
572
|
* Register an interactive skill management subcommand on the root command.
|
|
301
573
|
*
|
|
302
574
|
* The command presents a single multiselect prompt listing all detected
|
|
@@ -357,19 +629,123 @@ type SkillCommandTarget = CommandNode2 | Crust<any, any, any>;
|
|
|
357
629
|
* with the same text is a safe no-op.
|
|
358
630
|
*/
|
|
359
631
|
declare function annotate<T extends SkillCommandTarget>(target: T, annotations: string | string[] | SkillCommandAnnotations): T;
|
|
632
|
+
/**
|
|
633
|
+
* Installs a hand-authored skill bundle through the same canonical-store and
|
|
634
|
+
* agent-fan-out pipeline used by {@link generateSkill}.
|
|
635
|
+
*
|
|
636
|
+
* Unlike `generateSkill`, this entrypoint does not render any markdown — it
|
|
637
|
+
* copies the directory at `sourceDir` as authored (subject to a
|
|
638
|
+
* path-traversal guard against symlink escapes and a cycle guard) and
|
|
639
|
+
* writes a fresh `crust.json` recording `kind: "bundle"`. Bundle authors
|
|
640
|
+
* are responsible for keeping `sourceDir` clean — `crust.json` at the
|
|
641
|
+
* bundle root is reserved and will throw if present in the source.
|
|
642
|
+
*
|
|
643
|
+
* The bundle's `SKILL.md` frontmatter is the source of truth for `name` and
|
|
644
|
+
* `description`; both are required and read by Crust without rewriting the
|
|
645
|
+
* file. The caller supplies `version` explicitly — typically wired to the
|
|
646
|
+
* consuming package's `package.json` `version`.
|
|
647
|
+
*
|
|
648
|
+
* Bundles and generated skills cannot share a name unless the existing
|
|
649
|
+
* install is removed first. To overwrite a kind-mismatched install, pass
|
|
650
|
+
* `force: true`.
|
|
651
|
+
*
|
|
652
|
+
* @param options - Bundle install options (see {@link InstallSkillBundleOptions})
|
|
653
|
+
* @returns Per-agent install results
|
|
654
|
+
* @throws {SkillConflictError} If the canonical store exists with a different
|
|
655
|
+
* kind or with no `crust.json` (and `force` is not set).
|
|
656
|
+
* @throws {Error} If `SKILL.md` is missing, its frontmatter lacks `name:` or
|
|
657
|
+
* `description:`, the declared `name` is not a valid skill name, the
|
|
658
|
+
* declared `name` does not match `expectedName` when set, the source
|
|
659
|
+
* directory escapes itself via symlink, or `sourceDir` cannot be resolved.
|
|
660
|
+
*
|
|
661
|
+
* @example
|
|
662
|
+
* ```ts
|
|
663
|
+
* import { installSkillBundle } from "@crustjs/skills";
|
|
664
|
+
* import pkg from "./package.json" with { type: "json" };
|
|
665
|
+
*
|
|
666
|
+
* await installSkillBundle({
|
|
667
|
+
* sourceDir: "skills/funnel-builder",
|
|
668
|
+
* agents: ["claude-code"],
|
|
669
|
+
* version: pkg.version,
|
|
670
|
+
* });
|
|
671
|
+
* ```
|
|
672
|
+
*/
|
|
673
|
+
declare function installSkillBundle(options: InstallSkillBundleOptions): Promise<InstallSkillBundleResult>;
|
|
674
|
+
/**
|
|
675
|
+
* Why an installed manifest could not be interpreted.
|
|
676
|
+
*
|
|
677
|
+
* - `parse-error`: `crust.json` is present but is not valid JSON.
|
|
678
|
+
* - `not-an-object`: top-level JSON value is not an object.
|
|
679
|
+
* - `missing-version`: `version` field is absent or not a string.
|
|
680
|
+
* - `unknown-kind`: `kind` is present but is neither `"bundle"` nor `"generated"` —
|
|
681
|
+
* typically a hand-edit typo or a forward-compatible value emitted by a
|
|
682
|
+
* newer Crust release.
|
|
683
|
+
*/
|
|
684
|
+
type InstalledManifestMalformedReason = "parse-error" | "not-an-object" | "missing-version" | "unknown-kind";
|
|
685
|
+
/**
|
|
686
|
+
* Describes a kind mismatch between an existing installed bundle and an
|
|
687
|
+
* incoming install attempt.
|
|
688
|
+
*
|
|
689
|
+
* Set on {@link SkillConflictDetails.kindMismatch} when {@link generateSkill}
|
|
690
|
+
* or {@link installSkillBundle} discovers an existing `crust.json` whose
|
|
691
|
+
* `kind` differs from the kind being installed (e.g. a generated skill
|
|
692
|
+
* already lives at the target path and a bundle install was attempted).
|
|
693
|
+
*/
|
|
694
|
+
interface SkillKindMismatch {
|
|
695
|
+
/** Kind recorded in the existing `crust.json` */
|
|
696
|
+
existing: SkillKind;
|
|
697
|
+
/** Kind requested by the current install attempt */
|
|
698
|
+
attempted: SkillKind;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Describes a malformed `crust.json` discovered at the conflicting skill
|
|
702
|
+
* directory.
|
|
703
|
+
*
|
|
704
|
+
* Set on {@link SkillConflictDetails.manifestMalformed} when the directory
|
|
705
|
+
* contains a `crust.json` that exists but cannot be interpreted — e.g. it is
|
|
706
|
+
* not valid JSON, lacks a `version`, or has an unrecognized `kind` value
|
|
707
|
+
* (a hand-edit typo like `"bundel"`, or a forward-compatible value emitted by
|
|
708
|
+
* a newer Crust release). Distinct from a missing `crust.json`, which keeps
|
|
709
|
+
* the original "not created by Crust" semantics.
|
|
710
|
+
*/
|
|
711
|
+
interface SkillManifestMalformed {
|
|
712
|
+
/** Why the manifest could not be interpreted. */
|
|
713
|
+
reason: InstalledManifestMalformedReason;
|
|
714
|
+
/** Raw `kind` value when `reason === "unknown-kind"`. */
|
|
715
|
+
rawKind?: string;
|
|
716
|
+
}
|
|
360
717
|
/** Details about the conflict between an existing skill and an incoming one. */
|
|
361
718
|
interface SkillConflictDetails {
|
|
362
719
|
/** The agent where the conflict was detected */
|
|
363
720
|
agent: AgentTarget;
|
|
364
721
|
/** Absolute path to the conflicting skill directory */
|
|
365
722
|
outputDir: string;
|
|
723
|
+
/**
|
|
724
|
+
* Set when the conflict is a `kind` mismatch (existing `crust.json`
|
|
725
|
+
* reports a different `kind` than the one being installed).
|
|
726
|
+
*
|
|
727
|
+
* Absent for "no-crust.json" conflicts (the original case).
|
|
728
|
+
*/
|
|
729
|
+
kindMismatch?: SkillKindMismatch;
|
|
730
|
+
/**
|
|
731
|
+
* Set when `crust.json` is present at the conflicting directory but cannot
|
|
732
|
+
* be interpreted (invalid JSON, missing version, unrecognized `kind`,
|
|
733
|
+
* etc.). Lets the error message distinguish a Crust-owned-but-broken
|
|
734
|
+
* manifest from a directory that simply was never managed by Crust.
|
|
735
|
+
*/
|
|
736
|
+
manifestMalformed?: SkillManifestMalformed;
|
|
366
737
|
}
|
|
367
738
|
/**
|
|
368
|
-
* Thrown when
|
|
369
|
-
* already exists but
|
|
739
|
+
* Thrown when an install entrypoint detects that the target skill directory
|
|
740
|
+
* already exists but cannot be overwritten safely.
|
|
370
741
|
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
742
|
+
* Two flavours:
|
|
743
|
+
* - **No `crust.json`** — directory exists but was not created by Crust.
|
|
744
|
+
* This prevents Crust from silently overwriting a skill that was manually
|
|
745
|
+
* created or installed by another tool.
|
|
746
|
+
* - **Kind mismatch** — directory was created by Crust but with a different
|
|
747
|
+
* {@link SkillKind} (e.g. an existing `generated` skill collides with an
|
|
748
|
+
* incoming `bundle` install). `force: true` bypasses both cases.
|
|
373
749
|
*
|
|
374
750
|
* @example
|
|
375
751
|
* ```ts
|
|
@@ -379,9 +755,16 @@ interface SkillConflictDetails {
|
|
|
379
755
|
* await generateSkill({ command, meta, agents });
|
|
380
756
|
* } catch (err) {
|
|
381
757
|
* if (err instanceof SkillConflictError) {
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
758
|
+
* if (err.details.kindMismatch) {
|
|
759
|
+
* console.error(
|
|
760
|
+
* `Cannot install ${err.details.kindMismatch.attempted} skill — ` +
|
|
761
|
+
* `${err.details.kindMismatch.existing} skill already at "${err.details.outputDir}".`,
|
|
762
|
+
* );
|
|
763
|
+
* } else {
|
|
764
|
+
* console.error(
|
|
765
|
+
* `Conflict: "${err.details.outputDir}" already exists and was not created by Crust.`,
|
|
766
|
+
* );
|
|
767
|
+
* }
|
|
385
768
|
* }
|
|
386
769
|
* }
|
|
387
770
|
* ```
|
|
@@ -506,4 +889,4 @@ import { CrustPlugin } from "@crustjs/core";
|
|
|
506
889
|
* ```
|
|
507
890
|
*/
|
|
508
891
|
declare function skillPlugin(options: SkillPluginOptions): CrustPlugin;
|
|
509
|
-
export { uninstallSkill, skillStatus, skillPlugin, resolveSkillName, resolveCanonicalSkillPath, isValidSkillName, isUniversalAgent, getUniversalAgents, getAdditionalAgents, generateSkill, detectInstalledAgents, annotate, UninstallStatus, UninstallResult, UninstallOptions, StatusResult, StatusOptions, SkillPluginOptions, SkillMeta, SkillInstallMode, SkillConflictError, SkillConflictDetails, SkillCommandAnnotations, Scope, InstallStatus, GenerateResult, GenerateOptions, AgentTarget, AgentResult, AgentClass };
|
|
892
|
+
export { uninstallSkill, skillStatus, skillPlugin, resolveSkillName, resolveCanonicalSkillPath, isValidSkillName, isUniversalAgent, installSkillBundle, getUniversalAgents, getAdditionalAgents, generateSkill, detectInstalledAgents, annotate, UninstallStatus, UninstallResult, UninstallOptions, StatusResult, StatusOptions, SkillPluginOptions, SkillMeta, SkillManifestMalformed, SkillKindMismatch, SkillKind, SkillInstallMode, SkillConflictError, SkillConflictDetails, SkillCommandAnnotations, Scope, InstallStatus, InstallSkillBundleResult, InstallSkillBundleOptions, GenerateResult, GenerateOptions, CustomSkillConfig, AgentTarget, AgentResult, AgentClass };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{accessSync as nQ,constants as tQ}from"fs";import{homedir as o}from"os";import{delimiter as eQ,join as Y}from"path";var j=Y(".agents","skills"),QZ=Y(".crust","skills");function TQ(Q){if(Q!==o())return Y(Q,".config");let Z=process.env.XDG_CONFIG_HOME?.trim();return Z&&Z.length>0?Z:Y(Q,".config")}function I(Q){return Y(Q,".agents","skills")}function ZZ(Q){return Y(Q,".crust","skills")}function c(Q){return Q==="project"&&process.cwd()===o()?"global":Q}var y={amp:{label:"Amp",class:"universal",projectSkillsDir:j,globalSkillsDir:I},adal:{label:"AdaL",class:"additional",projectSkillsDir:Y(".adal","skills"),globalSkillsDir:(Q)=>Y(Q,".adal","skills"),detectCommands:["adal"]},antigravity:{label:"Antigravity",class:"additional",projectSkillsDir:Y(".agent","skills"),globalSkillsDir:(Q)=>Y(Q,".gemini","antigravity","skills"),detectCommands:["antigravity"]},augment:{label:"Augment",class:"additional",projectSkillsDir:Y(".augment","skills"),globalSkillsDir:(Q)=>Y(Q,".augment","skills"),detectCommands:["augment"]},"claude-code":{label:"Claude Code",class:"additional",projectSkillsDir:Y(".claude","skills"),globalSkillsDir:(Q)=>Y(process.env.CLAUDE_CONFIG_DIR?.trim()||Y(Q,".claude"),"skills"),detectCommands:["claude","claude-code"]},cline:{label:"Cline",class:"universal",projectSkillsDir:j,globalSkillsDir:I},codebuddy:{label:"CodeBuddy",class:"additional",projectSkillsDir:Y(".codebuddy","skills"),globalSkillsDir:(Q)=>Y(Q,".codebuddy","skills"),detectCommands:["codebuddy"]},codex:{label:"Codex",class:"universal",projectSkillsDir:j,globalSkillsDir:I},"command-code":{label:"Command Code",class:"additional",projectSkillsDir:Y(".commandcode","skills"),globalSkillsDir:(Q)=>Y(Q,".commandcode","skills"),detectCommands:["command-code","commandcode"]},continue:{label:"Continue",class:"additional",projectSkillsDir:Y(".continue","skills"),globalSkillsDir:(Q)=>Y(Q,".continue","skills"),detectCommands:["continue"]},cortex:{label:"Cortex Code",class:"additional",projectSkillsDir:Y(".cortex","skills"),globalSkillsDir:(Q)=>Y(Q,".snowflake","cortex","skills"),detectCommands:["cortex"]},crush:{label:"Crush",class:"additional",projectSkillsDir:Y(".crush","skills"),globalSkillsDir:(Q)=>Y(TQ(Q),"crush","skills"),detectCommands:["crush"]},cursor:{label:"Cursor",class:"universal",projectSkillsDir:j,globalSkillsDir:I},droid:{label:"Droid",class:"additional",projectSkillsDir:Y(".factory","skills"),globalSkillsDir:(Q)=>Y(Q,".factory","skills"),detectCommands:["droid"]},"gemini-cli":{label:"Gemini CLI",class:"universal",projectSkillsDir:j,globalSkillsDir:I},"github-copilot":{label:"GitHub Copilot",class:"universal",projectSkillsDir:j,globalSkillsDir:I},goose:{label:"Goose",class:"additional",projectSkillsDir:Y(".goose","skills"),globalSkillsDir:(Q)=>Y(TQ(Q),"goose","skills"),detectCommands:["goose"]},"iflow-cli":{label:"iFlow CLI",class:"additional",projectSkillsDir:Y(".iflow","skills"),globalSkillsDir:(Q)=>Y(Q,".iflow","skills"),detectCommands:["iflow","iflow-cli"]},junie:{label:"Junie",class:"additional",projectSkillsDir:Y(".junie","skills"),globalSkillsDir:(Q)=>Y(Q,".junie","skills"),detectCommands:["junie"]},kilo:{label:"Kilo Code",class:"additional",projectSkillsDir:Y(".kilocode","skills"),globalSkillsDir:(Q)=>Y(Q,".kilocode","skills"),detectCommands:["kilo","kilocode"]},"kimi-cli":{label:"Kimi Code CLI",class:"universal",projectSkillsDir:j,globalSkillsDir:I},"kiro-cli":{label:"Kiro CLI",class:"additional",projectSkillsDir:Y(".kiro","skills"),globalSkillsDir:(Q)=>Y(Q,".kiro","skills"),detectCommands:["kiro","kiro-cli"]},kode:{label:"Kode",class:"additional",projectSkillsDir:Y(".kode","skills"),globalSkillsDir:(Q)=>Y(Q,".kode","skills"),detectCommands:["kode"]},mcpjam:{label:"MCPJam",class:"additional",projectSkillsDir:Y(".mcpjam","skills"),globalSkillsDir:(Q)=>Y(Q,".mcpjam","skills"),detectCommands:["mcpjam"]},"mistral-vibe":{label:"Mistral Vibe",class:"additional",projectSkillsDir:Y(".vibe","skills"),globalSkillsDir:(Q)=>Y(Q,".vibe","skills"),detectCommands:["mistral-vibe","vibe"]},mux:{label:"Mux",class:"additional",projectSkillsDir:Y(".mux","skills"),globalSkillsDir:(Q)=>Y(Q,".mux","skills"),detectCommands:["mux"]},neovate:{label:"Neovate",class:"additional",projectSkillsDir:Y(".neovate","skills"),globalSkillsDir:(Q)=>Y(Q,".neovate","skills"),detectCommands:["neovate"]},opencode:{label:"OpenCode",class:"universal",projectSkillsDir:j,globalSkillsDir:I},openclaw:{label:"OpenClaw",class:"additional",projectSkillsDir:"skills",globalSkillsDir:(Q)=>Y(Q,".openclaw","skills"),detectCommands:["openclaw"]},openhands:{label:"OpenHands",class:"additional",projectSkillsDir:Y(".openhands","skills"),globalSkillsDir:(Q)=>Y(Q,".openhands","skills"),detectCommands:["openhands"]},pi:{label:"Pi",class:"additional",projectSkillsDir:Y(".pi","skills"),globalSkillsDir:(Q)=>Y(Q,".pi","agent","skills"),detectCommands:["pi"]},pochi:{label:"Pochi",class:"additional",projectSkillsDir:Y(".pochi","skills"),globalSkillsDir:(Q)=>Y(Q,".pochi","skills"),detectCommands:["pochi"]},qoder:{label:"Qoder",class:"additional",projectSkillsDir:Y(".qoder","skills"),globalSkillsDir:(Q)=>Y(Q,".qoder","skills"),detectCommands:["qoder"]},"qwen-code":{label:"Qwen Code",class:"additional",projectSkillsDir:Y(".qwen","skills"),globalSkillsDir:(Q)=>Y(Q,".qwen","skills"),detectCommands:["qwen","qwen-code"]},replit:{label:"Replit",class:"universal",projectSkillsDir:j,globalSkillsDir:I},roo:{label:"Roo Code",class:"additional",projectSkillsDir:Y(".roo","skills"),globalSkillsDir:(Q)=>Y(Q,".roo","skills"),detectCommands:["roo","roo-code"]},trae:{label:"Trae",class:"additional",projectSkillsDir:Y(".trae","skills"),globalSkillsDir:(Q)=>Y(Q,".trae","skills"),detectCommands:["trae"]},"trae-cn":{label:"Trae CN",class:"additional",projectSkillsDir:Y(".trae","skills"),globalSkillsDir:(Q)=>Y(Q,".trae-cn","skills"),detectCommands:["trae-cn","trae"]},windsurf:{label:"Windsurf",class:"additional",projectSkillsDir:Y(".windsurf","skills"),globalSkillsDir:(Q)=>Y(Q,".codeium","windsurf","skills"),detectCommands:["windsurf"]},zencoder:{label:"Zencoder",class:"additional",projectSkillsDir:Y(".zencoder","skills"),globalSkillsDir:(Q)=>Y(Q,".zencoder","skills"),detectCommands:["zencoder"]}},m=Object.keys(y),D=Object.fromEntries(m.map((Q)=>[Q,y[Q].label]));function S(){return m.filter((Q)=>y[Q].class==="universal")}function h(){return m.filter((Q)=>y[Q].class==="additional")}function XZ(Q){return y[Q].class==="universal"}async function XQ(Q){let Z=typeof Q==="string"?{home:Q}:Q??{},X=Z.cwd??process.cwd(),$=Z.commandChecker??((H)=>Promise.resolve($Z(H))),W=[];for(let H of h()){let q=y[H].detectCommands??[],x=!1;for(let J of q)if(await $(J,X)){x=!0;break}if(x)W.push(H)}return W}function E(Q,Z,X){let $=c(Z),W=y[Q];if($==="project")return Y(process.cwd(),W.projectSkillsDir,X);return Y(W.globalSkillsDir(o()),X)}function C(Q,Z){if(c(Q)==="project")return Y(process.cwd(),QZ,Z);return Y(ZZ(o()),Z)}function $Z(Q){let X=(process.env.PATH??"").split(eQ).filter((H)=>H.length>0),$=process.platform==="win32",W=$?(process.env.PATHEXT??".EXE;.CMD;.BAT;.COM").split(";").filter((H)=>H.length>0):[];for(let H of X){if(!$&&_Q(Y(H,Q)))return!0;if($){for(let q of W)if(_Q(Y(H,Q+q)))return!0}}return!1}function _Q(Q){try{return nQ(Q,tQ.X_OK),!0}catch{return!1}}import{Crust as WZ}from"@crustjs/core";function a(Q){if(Q===void 0)return[];return(Array.isArray(Q)?Q:[Q]).flatMap((X)=>X.split(/\r?\n/)).map((X)=>X.trim()).filter((X)=>X.length>0)}function bQ(Q){let Z=Q?.trim();if(!Z)return[];return Z.split(/\r?\n/)}function $Q(Q){return Q.length>0}var OQ=Symbol("crust.skill.commandAnnotations");function HZ(Q){return Q instanceof WZ?Q._node:Q}function YZ(Q,Z){let X=HZ(Q),$=a(typeof Z==="string"||Array.isArray(Z)?Z:Z.instructions??[]);if($.length===0)return Q;let W=WQ(X)?.instructions??[],H=[...new Set([...W,...$])];return Object.defineProperty(X,OQ,{value:{instructions:H},enumerable:!0,configurable:!0}),Q}function WQ(Q){let Z=Q[OQ];if(!Z?.instructions||Z.instructions.length===0)return;return{instructions:[...Z.instructions]}}class N 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{lstat as LZ,mkdir as gQ,readlink as PZ,realpath as CZ,rm as r,symlink as yZ,writeFile as DZ}from"fs/promises";import{dirname as uQ,join as AQ}from"path";function RQ(Q){return kQ(Q,[])}function kQ(Q,Z){let X=qZ(Q.meta.name),$=[...Z,X],W=BZ(Q.args),H=zZ(Q.effectiveFlags),q=xZ(Q.subCommands,$),x=WQ(Q);return{name:X,path:$,description:Q.meta.description,usage:Q.meta.usage,instructions:x?.instructions,runnable:typeof Q.run==="function",args:W,flags:H,children:q}}function qZ(Q){return Q.trim().toLowerCase()}function BZ(Q){if(!Q||Q.length===0)return[];return Q.map(JZ)}function JZ(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=EQ(Q.default);return Z}function zZ(Q){if(!Q)return[];return Object.keys(Q).sort().map((X)=>{return KZ(X,Q[X])})}function KZ(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=EQ(Z.default);return X}function xZ(Q,Z){return Object.keys(Q).sort().map(($)=>{return kQ(Q[$],Z)})}function EQ(Q){if(Array.isArray(Q))return JSON.stringify(Q);return String(Q)}function d(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 jQ(Q){return Q.replace(/(?<!\\)\|/g,"\\|")}function IQ(Q,Z){let X=[],$=NQ(Q);X.push({path:"SKILL.md",content:GZ(Q,Z,$)});for(let W of $){let H=A(W),q=W.children.length>0?VZ(W,Q):UZ(W,Q);X.push({path:H,content:q})}return X}function NQ(Q){let Z=[Q];for(let X of Q.children)Z.push(...NQ(X));return Z}function A(Q){if(Q.path.length<=1)return`commands/${Q.name}.md`;return`commands/${Q.path.slice(1).join("/")}.md`}function YQ(Q){return Q.path.join(" ")}function HQ(Q,Z){let X=Q.split("/").slice(0,-1),$=Z.split("/"),W=0;while(W<X.length&&W<$.length&&X[W]===$[W])W++;let H=X.length-W,q=$.slice(W);if(H===0)return q.join("/");return[...Array.from({length:H},()=>".."),...q].join("/")}function GZ(Q,Z,X){let $=[];if($.push("---"),$.push(`name: ${d(Z.name)}`),$.push(`description: ${d(Z.description)}`),Z.license)$.push(`license: ${d(Z.license)}`);if(Z.compatibility)$.push(`compatibility: ${d(Z.compatibility)}`);if(Z.disableModelInvocation)$.push("disable-model-invocation: true");if(Z.allowedTools)$.push(`allowed-tools: ${d(Z.allowedTools)}`);if($.push("metadata:"),$.push(` version: "${Z.version}"`),$.push("---"),$.push(""),$.push(`# ${Z.name}`),$.push(""),Q.description)$.push(Q.description),$.push("");$.push(`You should use this skill when you need accurate help with \`${Z.name}\` commands, including command selection, syntax, arguments, flags, defaults, and subcommands.`),$.push("");let W=EZ(Z.instructions);if($.push("## How to Use This Skill"),$.push(""),$.push("1. You must find the command that best matches the user's task from the Command Reference below."),$.push("2. You must check the `Type` column before suggesting execution: `runnable` and `runnable, group` commands can be executed, while `group` commands are organizational only."),$.push("3. You should read only the linked file or files you need from `commands/`."),$.push("4. You must read a command's file before answering a command-specific question or suggesting that command."),$.push("5. You must treat the command file as the source of truth for usage, arguments, flags, aliases, and defaults."),$.push("6. If a flag, argument, alias, or default is not documented there, you must say it is not documented instead of guessing."),$.push(""),$Q(W))$.push("## General Guidance"),$.push(""),$.push(...W),$.push("");if($.push("## Command Reference"),$.push(""),$.push("You should use this table to locate the command file you need."),$.push(""),$.push(...FZ(X)),$.push(""),Q.runnable){$.push("## Usage"),$.push("");let H=A(Q);$.push(`The root command is directly executable. You should see [${Q.name}](${H}) for usage details.`),$.push("")}return $.join(`
|
|
3
|
-
`)}function
|
|
4
|
-
`)}function
|
|
5
|
-
`)}function MZ(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 LQ(Q){let Z=[`# \`${YQ(Q)}\``,""];if(Q.description)Z.push(Q.description,"");return Z}function PQ(Q){let Z=Q.instructions??[];if(!$Q(Z))return[];return["## Command Instructions","",...yQ(Z),""]}function CQ(Q){let Z=["## Usage","","```",Q.usage??MZ(Q),"```",""];if(Q.args.length>0)Z.push("## Arguments","",..._Z(Q.args),"");if(Q.flags.length>0)Z.push("## Flags","",...OZ(Q.flags),"");return Z.push("## Command Documentation Authority","","You must treat only the arguments, flags, options, aliases, and defaults documented in this file as supported for this command.","You must not infer or invent additional command-line options.",""),Z}function TZ(Q,Z){let X=["## Subcommands",""];for(let $ of Q.children){let W=A($),H=HQ(Z,W),q=$.description?` - ${$.description}`:"";X.push(`- [\`${$.name}\`](${H})${q}`)}return X.push(""),X}function _Z(Q){let Z=[];Z.push("| Argument | Type | Required | Description |"),Z.push("| -------- | ---- | -------- | ----------- |");for(let X of Q){let $=X.variadic?`${X.name}...`:X.name,W=X.required?"Yes":"No",H=jQ(bZ(X));Z.push(`| \`${$}\` | ${X.type} | ${W} | ${H} |`)}return Z}function bZ(Q){let Z=[];if(Q.description)Z.push(Q.description);if(Q.default!==void 0)Z.push(`Default: \`${Q.default}\``);return Z.join(". ")||"-"}function OZ(Q){let Z=[];Z.push("| Flag | Type | Required | Description |"),Z.push("| ---- | ---- | -------- | ----------- |");for(let X of Q){let $=RZ(X),W=X.required?"Yes":"No",H=jQ(kZ(X));Z.push(`| ${$} | ${X.type} | ${W} | ${H} |`)}return Z}function RZ(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 kZ(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 yQ(Q){return Q.map((Z)=>`- ${Z}`)}function EZ(Q){if(typeof Q==="string")return bQ(Q);return yQ(a(Q))}function DQ(Q,Z){let X=[],$=A(Q);if(X.push("---"),X.push(""),Q.path.length>1){let H=Q.path.slice(0,-1),q=SQ(Z,H);if(q){let x=A(q),J=HQ($,x),K=YQ(q);X.push(`Parent: [\`${K}\`](${J})`),X.push("")}}let W=HQ($,"SKILL.md");return X.push(`[Skill Overview](${W})`),X.push(""),X}function SQ(Q,Z){if(jZ(Q.path,Z))return Q;for(let X of Q.children){let $=SQ(X,Z);if($)return $}return}function jZ(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 IZ}from"fs/promises";import{join as NZ}from"path";var qQ="crust.json";async function v(Q){try{let Z=await IZ(NZ(Q,qQ),"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 SZ="auto",cQ=/^[a-z0-9]+(-[a-z0-9]+)*$/;function mQ(Q){return Q.length>=1&&Q.length<=64&&cQ.test(Q)}function n(Q){return Q}function zQ(Q){return Q.startsWith("use-")?Q:`use-${Q}`}async function p(Q){let{command:Z,meta:X,agents:$,scope:W="global",clean:H=!0,force:q=!1,installMode:x=SZ}=Q,J=n(X.name),K=zQ(X.name);if(!mQ(J))throw Error(`Invalid skill name "${J}": must be 1\u201364 lowercase `+`alphanumeric characters and hyphens, no leading/trailing/consecutive hyphens. Pattern: ${cQ.source}`);let z=$[0];if(!z)return{agents:[]};let G={...X,name:J},F=RQ(Z),_=IQ(F,G),b=gZ(G),T=[..._,...b].sort((w,B)=>w.path<B.path?-1:w.path>B.path?1:0),O=T.map((w)=>w.path),R=new Map;for(let w of $){let B=E(w,W,G.name),U=R.get(B);if(U)U.push(w);else R.set(B,[w])}let M=C(W,G.name),L=C(W,K),u=new Map;for(let[w,B]of R){let U=B[0];if(!U)continue;u.set(w,await GQ({outputDir:w,legacyOutputDir:E(U,W,K),canonicalOutputDir:M,legacyCanonicalOutputDir:L}))}let P=await v(M);if((await xQ(M,M)).exists&&P===null&&!q)throw new N({agent:z,outputDir:M});let i=P!==G.version;if(i){if(H)await FQ(M);await dQ(M,T)}let f=[];for(let[w,B]of R){let U=B[0];if(!U)continue;let V=u.get(w);if(!V)continue;if(V.current.inspection.exists&&!V.current.isCrustManaged&&!q)throw new N({agent:U,outputDir:w});let k=await vZ({outputDir:w,canonicalOutputDir:M,allFiles:T,clean:H,installMode:x,inspection:V.current.inspection,installedVersion:V.preferredVersion,currentVersion:G.version}),QQ=await hZ(V),ZQ=AZ({installedVersion:V.preferredVersion,currentVersion:G.version,canonicalChanged:i,pathChanged:k||QQ||V.preferredOutputDir!==w});for(let aQ of B)f.push({agent:aQ,outputDir:w,files:ZQ==="up-to-date"?[]:O,status:ZQ,previousVersion:ZQ==="updated"?V.preferredVersion??void 0:void 0})}{let w=await v(L);if(L!==M&&w!==null&&!await JQ(K,W))await r(L,{recursive:!0,force:!0})}return{agents:f}}async function KQ(Q){let{name:Z,agents:X,scope:$="global"}=Q,W=n(Z),H=zQ(Z),q=C($,W),x=C($,H),J=[],K=new Map;for(let z of X){let G=E(z,$,W),F=K.get(G);if(F)F.push(z);else K.set(G,[z])}for(let[z,G]of K){let F=G[0];if(!F)continue;let _=E(F,$,H),b=await GQ({outputDir:z,legacyOutputDir:_,canonicalOutputDir:q,legacyCanonicalOutputDir:x}),T=await BQ(b.current),O=b.legacy.outputDir!==b.current.outputDir?await BQ(b.legacy):!1,R=T||O,M=T?z:O?_:z;for(let L of G)J.push({agent:L,outputDir:M,status:R?"removed":"not-found"})}if(await v(q)!==null&&!await JQ(W,$))await r(q,{recursive:!0,force:!0});{let z=await v(x);if(x!==q&&z!==null&&!await JQ(H,$))await r(x,{recursive:!0,force:!0})}return{agents:J}}async function l(Q){let{name:Z,agents:X,scope:$="global"}=Q,W=n(Z),H=zQ(Z),q=[],x=new Map;for(let J of X){let K=E(J,$,W),z=x.get(K);if(z)z.push(J);else x.set(K,[J])}for(let[J,K]of x){let z=K[0];if(!z)continue;let G=E(z,$,H),F=C($,W),_=C($,H),b=await GQ({outputDir:J,legacyOutputDir:G,canonicalOutputDir:F,legacyCanonicalOutputDir:_}),T=b.preferredOutputDir??J,O=b.preferredVersion;for(let R of K)q.push({agent:R,outputDir:T,installed:O!==null,version:O??void 0})}return{agents:q}}function AZ(Q){let{installedVersion:Z,currentVersion:X,canonicalChanged:$,pathChanged:W}=Q;if(Z===null)return"installed";if(Z===X&&!$&&!W)return"up-to-date";return"updated"}async function vZ(Q){let{outputDir:Z,canonicalOutputDir:X,allFiles:$,clean:W,installMode:H,inspection:q,installedVersion:x,currentVersion:J}=Q;if(H==="copy")return vQ({outputDir:Z,allFiles:$,clean:W,inspection:q,installedVersion:x,currentVersion:J});try{return await fZ({outputDir:Z,canonicalOutputDir:X,inspection:q})}catch(K){if(H==="symlink")throw Error(`Failed to create symlink at "${Z}" (installMode: symlink).`,{cause:K});let z=await xQ(Z,X);return vQ({outputDir:Z,allFiles:$,clean:W,inspection:z,installedVersion:x,currentVersion:J})}}async function vQ(Q){let{outputDir:Z,allFiles:X,clean:$,inspection:W,installedVersion:H,currentVersion:q}=Q;if(!(!W.exists||W.isSymlink||H!==q))return!1;if(W.isSymlink||$)await FQ(Z);return await dQ(Z,X),!0}async function fZ(Q){let{outputDir:Z,canonicalOutputDir:X,inspection:$}=Q;if($.exists&&$.isSymlink&&$.pointsToCanonical)return!1;if($.exists)await FQ(Z);return await pZ(X,Z),!0}async function xQ(Q,Z){let X;try{X=await LZ(Q)}catch{return{exists:!1,isSymlink:!1,pointsToCanonical:!1}}let $=process.platform==="win32"&&X.isDirectory()&&await pQ(Q)!==null;if(!(X.isSymbolicLink()||$))return{exists:!0,isSymlink:!1,pointsToCanonical:!1};let[H,q,x]=await Promise.all([hQ(Q),hQ(Z),pQ(Q)]);return{exists:!0,isSymlink:!0,pointsToCanonical:H!==null&&q!==null&&H===q||x===Z}}async function fQ(Q,Z){let[X,$]=await Promise.all([v(Q),xQ(Q,Z)]),W=X!==null||$.exists&&$.isSymlink&&$.pointsToCanonical;return{outputDir:Q,version:X,inspection:$,isCrustManaged:W}}async function GQ(Q){let{outputDir:Z,legacyOutputDir:X,canonicalOutputDir:$,legacyCanonicalOutputDir:W}=Q,H=await fQ(Z,$),q=X===Z?H:await fQ(X,W);if(H.isCrustManaged)return{current:H,legacy:q,preferredVersion:H.version,preferredOutputDir:H.outputDir};if(q.isCrustManaged)return{current:H,legacy:q,preferredVersion:q.version,preferredOutputDir:q.outputDir};return{current:H,legacy:q,preferredVersion:null,preferredOutputDir:null}}async function BQ(Q){if(!Q.isCrustManaged||!Q.inspection.exists)return!1;return await r(Q.outputDir,{recursive:!0,force:!0}),!0}async function hZ(Q){if(Q.legacy.outputDir===Q.current.outputDir)return!1;return BQ(Q.legacy)}async function hQ(Q){try{return await CZ(Q)}catch{return null}}async function pQ(Q){try{return await PZ(Q)}catch{return null}}async function pZ(Q,Z){await gQ(uQ(Z),{recursive:!0});let X=process.platform==="win32"?"junction":"dir";await yZ(Q,Z,X)}async function JQ(Q,Z){let X=new Set;for(let $ of m)X.add(E($,Z,Q));for(let $ of X)if(await v($)!==null)return!0;return!1}function gZ(Q){let Z={name:Q.name,description:Q.description,version:Q.version};return[{path:qQ,content:`${JSON.stringify(Z,null,"\t")}
|
|
6
|
-
`}]}async function FQ(Q){await r(Q,{recursive:!0,force:!0})}async function dQ(Q,Z){let X=new Set;for(let W of Z){let H=AQ(Q,W.path),q=uQ(H);X.add(q)}let $=[...X].sort();for(let W of $)await gQ(W,{recursive:!0});for(let W of Z){let H=AQ(Q,W.path);await DZ(H,W.content,"utf-8")}}import{Crust as lQ,VALIDATION_MODE_ENV as uZ}from"@crustjs/core";import{spinner as s}from"@crustjs/progress";import{confirm as cZ,multiselect as mZ,select as dZ}from"@crustjs/prompts";import{bold as e,dim as g,yellow as sQ}from"@crustjs/style";var rZ="skill",iQ="global",t="__universal__";function lZ(Q){return Q==="global"||Q==="project"}async function oQ(Q,Z){if(Q!==void 0){if(!lZ(Q))throw Error(`Invalid --scope value: ${String(Q)}. Expected "project" or "global".`);return Q}if(Z.defaultScope)return Z.defaultScope;return dZ({message:"Select scope",choices:[{label:"Project",value:"project"},{label:"Global",value:"global"}],default:iQ})}function wQ(Q){let Z=new Set(S()),X=[];if(Q.some(($)=>Z.has($)))X.push("Universal");for(let $ of Q){if(Z.has($))continue;X.push(D[$])}return X}function rQ(Q){let Z=new Set(S()),X=[],$=Q.find((W)=>Z.has(W.agent));if($)X.push({label:"Universal",outputDir:$.outputDir});for(let W of Q){if(Z.has(W.agent))continue;X.push({label:D[W.agent],outputDir:W.outputDir})}return X}function UQ(Q,Z){return{name:Q.meta.name,description:Q.meta.description??"",version:Z.version,instructions:Z.instructions,license:Z.license,allowedTools:Z.allowedTools,compatibility:Z.compatibility,disableModelInvocation:Z.disableModelInvocation}}function VQ(Q,Z,X,$){if(!$.installed)return!1;let W=E(Q,Z,X.name);return $.version!==X.version||$.outputDir!==W}async function sZ(Q,Z){let X=[...S(),...h()];if(X.length===0)return;let $=UQ(Q,Z),W=[...new Set(["project","global"].map((H)=>c(H)))];for(let H of W){let x=(await l({name:$.name,agents:X,scope:H})).agents.filter((J)=>VQ(J.agent,H,$,J));if(x.length===0)continue;try{await s({message:`Updating ${H} skills...`,task:async({updateMessage:J})=>{let K=await p({command:Q,meta:$,agents:x.map((F)=>F.agent),scope:H,installMode:Z.installMode}),z=K.agents.filter((F)=>F.status==="updated").map((F)=>F.agent),G=wQ(z);if(G.length>0)J(`Updated skill "${$.name}" to v${$.version} for ${G.join(", ")} (${H})`);return K}})}catch(J){if(J instanceof N)console.warn(sQ(`Skill conflict: "${J.details.outputDir}" already exists but was not created by ${$.name}. Skipping auto-update for ${H}. Delete or rename the conflicting skill to resolve.`));else throw J}}}function iZ(Q){let Z;return{name:"skills",async setup(X,$){Z=X.rootCommand;let W=Q.command??rZ;if($.addSubCommand(Z,W,oZ(Z,Q,W)),process.env[uZ]==="1")return;if(X.argv[0]===W)return;if(Q.autoUpdate!==!1)await sZ(Z,Q)}}}function oZ(Q,Z,X){let $=aZ(Q,Z);return new lQ(X).meta({description:"Manage agent skill installations"}).flags({scope:{type:"string",description:"Install scope (project or global)"},all:{type:"boolean",description:"Install for all detected agents non-interactively (universal + detected)"}}).run(async(W)=>{let H=UQ(Q,Z),q=W.flags.all===!0,x=!!process.stdin.isTTY,J=q?Z.defaultScope??iQ:await oQ(W.flags.scope,Z),K=await XQ(),z=S(),G=h(),F=await l({name:H.name,agents:[...z,...G],scope:J}),_=new Set(F.agents.filter((B)=>B.installed).map((B)=>B.agent)),b=new Set(K),T=new Map(F.agents.map((B)=>[B.agent,B])),O=G.filter((B)=>{if(b.has(B))return!0;return T.get(B)?.installed===!0}),R=O.filter((B)=>_.has(B)),M=[];if(z.length>0){let B=z[0];if(!B)throw Error("Expected at least one universal agent");let V=T.get(B)?.outputDir??"path unavailable";M.push({label:"Universal",value:t,hint:V});let k=z.map((QQ)=>D[QQ]).join(", ");if(x&&!q)console.log(g(`Agents supporting universal skills: ${k}`))}for(let B of O){let V=T.get(B)?.outputDir??"path unavailable";M.push({label:D[B],value:B,hint:V})}let L=z.length>0&&z.every((B)=>_.has(B)),u=[...R.filter((B)=>!z.includes(B))];if(L)u.unshift(t);let P;if(q)P=[...z,...O];else{let B=M.length===0?[]:await mZ({message:"Select agents to install skills for",choices:M,default:u,required:!1}),U=new Set(B.filter((V)=>V!==t));if(B.includes(t))for(let V of z)U.add(V);P=[...U]}let MQ=P.filter((B)=>!_.has(B)),i=P.filter((B)=>{let U=T.get(B);return U!==void 0&&VQ(B,J,H,U)}),f=[..._].filter((B)=>!P.includes(B)),w=[...MQ,...i];if(w.length>0)try{let B=await s({message:"Installing skills...",task:async()=>p({command:Q,meta:H,agents:w,scope:J,installMode:Z.installMode})});console.log(`
|
|
7
|
-
${
|
|
8
|
-
${
|
|
9
|
-
Skipped ${
|
|
10
|
-
${
|
|
11
|
-
${
|
|
2
|
+
import{accessSync as wX,constants as MX,statSync as TX}from"fs";import{homedir as JQ}from"os";import{delimiter as _X,join as J}from"path";var y=J(".agents","skills"),OX=J(".crust","skills");function NQ(Q){if(Q!==JQ())return J(Q,".config");let X=process.env.XDG_CONFIG_HOME?.trim();return X&&X.length>0?X:J(Q,".config")}function f(Q){return J(Q,".agents","skills")}function jX(Q){return J(Q,".crust","skills")}function u(Q){return Q==="project"&&process.cwd()===JQ()?"global":Q}var s={amp:{label:"Amp",class:"universal",projectSkillsDir:y,globalSkillsDir:f},adal:{label:"AdaL",class:"additional",projectSkillsDir:J(".adal","skills"),globalSkillsDir:(Q)=>J(Q,".adal","skills"),detectCommands:["adal"]},antigravity:{label:"Antigravity",class:"additional",projectSkillsDir:J(".agent","skills"),globalSkillsDir:(Q)=>J(Q,".gemini","antigravity","skills"),detectCommands:["antigravity"]},augment:{label:"Augment",class:"additional",projectSkillsDir:J(".augment","skills"),globalSkillsDir:(Q)=>J(Q,".augment","skills"),detectCommands:["augment"]},"claude-code":{label:"Claude Code",class:"additional",projectSkillsDir:J(".claude","skills"),globalSkillsDir:(Q)=>J(process.env.CLAUDE_CONFIG_DIR?.trim()||J(Q,".claude"),"skills"),detectCommands:["claude","claude-code"]},cline:{label:"Cline",class:"universal",projectSkillsDir:y,globalSkillsDir:f},codebuddy:{label:"CodeBuddy",class:"additional",projectSkillsDir:J(".codebuddy","skills"),globalSkillsDir:(Q)=>J(Q,".codebuddy","skills"),detectCommands:["codebuddy"]},codex:{label:"Codex",class:"universal",projectSkillsDir:y,globalSkillsDir:f},"command-code":{label:"Command Code",class:"additional",projectSkillsDir:J(".commandcode","skills"),globalSkillsDir:(Q)=>J(Q,".commandcode","skills"),detectCommands:["command-code","commandcode"]},continue:{label:"Continue",class:"additional",projectSkillsDir:J(".continue","skills"),globalSkillsDir:(Q)=>J(Q,".continue","skills"),detectCommands:["continue"]},cortex:{label:"Cortex Code",class:"additional",projectSkillsDir:J(".cortex","skills"),globalSkillsDir:(Q)=>J(Q,".snowflake","cortex","skills"),detectCommands:["cortex"]},crush:{label:"Crush",class:"additional",projectSkillsDir:J(".crush","skills"),globalSkillsDir:(Q)=>J(NQ(Q),"crush","skills"),detectCommands:["crush"]},cursor:{label:"Cursor",class:"universal",projectSkillsDir:y,globalSkillsDir:f},droid:{label:"Droid",class:"additional",projectSkillsDir:J(".factory","skills"),globalSkillsDir:(Q)=>J(Q,".factory","skills"),detectCommands:["droid"]},"gemini-cli":{label:"Gemini CLI",class:"universal",projectSkillsDir:y,globalSkillsDir:f},"github-copilot":{label:"GitHub Copilot",class:"universal",projectSkillsDir:y,globalSkillsDir:f},goose:{label:"Goose",class:"additional",projectSkillsDir:J(".goose","skills"),globalSkillsDir:(Q)=>J(NQ(Q),"goose","skills"),detectCommands:["goose"]},"iflow-cli":{label:"iFlow CLI",class:"additional",projectSkillsDir:J(".iflow","skills"),globalSkillsDir:(Q)=>J(Q,".iflow","skills"),detectCommands:["iflow","iflow-cli"]},junie:{label:"Junie",class:"additional",projectSkillsDir:J(".junie","skills"),globalSkillsDir:(Q)=>J(Q,".junie","skills"),detectCommands:["junie"]},kilo:{label:"Kilo Code",class:"additional",projectSkillsDir:J(".kilocode","skills"),globalSkillsDir:(Q)=>J(Q,".kilocode","skills"),detectCommands:["kilo","kilocode"]},"kimi-cli":{label:"Kimi Code CLI",class:"universal",projectSkillsDir:y,globalSkillsDir:f},"kiro-cli":{label:"Kiro CLI",class:"additional",projectSkillsDir:J(".kiro","skills"),globalSkillsDir:(Q)=>J(Q,".kiro","skills"),detectCommands:["kiro","kiro-cli"]},kode:{label:"Kode",class:"additional",projectSkillsDir:J(".kode","skills"),globalSkillsDir:(Q)=>J(Q,".kode","skills"),detectCommands:["kode"]},mcpjam:{label:"MCPJam",class:"additional",projectSkillsDir:J(".mcpjam","skills"),globalSkillsDir:(Q)=>J(Q,".mcpjam","skills"),detectCommands:["mcpjam"]},"mistral-vibe":{label:"Mistral Vibe",class:"additional",projectSkillsDir:J(".vibe","skills"),globalSkillsDir:(Q)=>J(Q,".vibe","skills"),detectCommands:["mistral-vibe","vibe"]},mux:{label:"Mux",class:"additional",projectSkillsDir:J(".mux","skills"),globalSkillsDir:(Q)=>J(Q,".mux","skills"),detectCommands:["mux"]},neovate:{label:"Neovate",class:"additional",projectSkillsDir:J(".neovate","skills"),globalSkillsDir:(Q)=>J(Q,".neovate","skills"),detectCommands:["neovate"]},opencode:{label:"OpenCode",class:"universal",projectSkillsDir:y,globalSkillsDir:f},openclaw:{label:"OpenClaw",class:"additional",projectSkillsDir:"skills",globalSkillsDir:(Q)=>J(Q,".openclaw","skills"),detectCommands:["openclaw"]},openhands:{label:"OpenHands",class:"additional",projectSkillsDir:J(".openhands","skills"),globalSkillsDir:(Q)=>J(Q,".openhands","skills"),detectCommands:["openhands"]},pi:{label:"Pi",class:"additional",projectSkillsDir:J(".pi","skills"),globalSkillsDir:(Q)=>J(Q,".pi","agent","skills"),detectCommands:["pi"]},pochi:{label:"Pochi",class:"additional",projectSkillsDir:J(".pochi","skills"),globalSkillsDir:(Q)=>J(Q,".pochi","skills"),detectCommands:["pochi"]},qoder:{label:"Qoder",class:"additional",projectSkillsDir:J(".qoder","skills"),globalSkillsDir:(Q)=>J(Q,".qoder","skills"),detectCommands:["qoder"]},"qwen-code":{label:"Qwen Code",class:"additional",projectSkillsDir:J(".qwen","skills"),globalSkillsDir:(Q)=>J(Q,".qwen","skills"),detectCommands:["qwen","qwen-code"]},replit:{label:"Replit",class:"universal",projectSkillsDir:y,globalSkillsDir:f},roo:{label:"Roo Code",class:"additional",projectSkillsDir:J(".roo","skills"),globalSkillsDir:(Q)=>J(Q,".roo","skills"),detectCommands:["roo","roo-code"]},trae:{label:"Trae",class:"additional",projectSkillsDir:J(".trae","skills"),globalSkillsDir:(Q)=>J(Q,".trae","skills"),detectCommands:["trae"]},"trae-cn":{label:"Trae CN",class:"additional",projectSkillsDir:J(".trae","skills"),globalSkillsDir:(Q)=>J(Q,".trae-cn","skills"),detectCommands:["trae-cn","trae"]},windsurf:{label:"Windsurf",class:"additional",projectSkillsDir:J(".windsurf","skills"),globalSkillsDir:(Q)=>J(Q,".codeium","windsurf","skills"),detectCommands:["windsurf"]},zencoder:{label:"Zencoder",class:"additional",projectSkillsDir:J(".zencoder","skills"),globalSkillsDir:(Q)=>J(Q,".zencoder","skills"),detectCommands:["zencoder"]}},a=Object.keys(s),A=Object.fromEntries(a.map((Q)=>[Q,s[Q].label]));function S(){return a.filter((Q)=>s[Q].class==="universal")}function m(){return a.filter((Q)=>s[Q].class==="additional")}function bX(Q){return s[Q].class==="universal"}async function n(Q){let X=typeof Q==="string"?{home:Q}:Q??{},Z=X.cwd??process.cwd(),H=X.commandChecker??((W)=>Promise.resolve(RX(W))),$=[];for(let W of m()){let Y=s[W].detectCommands??[],G=!1;for(let x of Y)if(await H(x,Z)){G=!0;break}if(G)$.push(W)}return $}function N(Q,X,Z){let H=u(X),$=s[Q];if(H==="project")return J(process.cwd(),$.projectSkillsDir,Z);return J($.globalSkillsDir(JQ()),Z)}function c(Q,X){if(u(Q)==="project")return J(process.cwd(),OX,X);return J(jX(JQ()),X)}function RX(Q){let Z=(process.env.PATH??"").split(_X).filter((W)=>W.length>0),H=process.platform==="win32",$=H?(process.env.PATHEXT??".EXE;.CMD;.BAT;.COM").split(";").filter((W)=>W.length>0):[];for(let W of Z){if(!H&&PQ(J(W,Q)))return!0;if(H){for(let Y of $)if(PQ(J(W,Q+Y)))return!0}}return!1}function PQ(Q){try{if(!TX(Q).isFile())return!1;return wX(Q,MX.X_OK),!0}catch{return!1}}import{Crust as EX}from"@crustjs/core";function BQ(Q){if(Q===void 0)return[];return(Array.isArray(Q)?Q:[Q]).flatMap((Z)=>Z.split(/\r?\n/)).map((Z)=>Z.trim()).filter((Z)=>Z.length>0)}function kQ(Q){let X=Q?.trim();if(!X)return[];return X.split(/\r?\n/)}function FQ(Q){return Q.length>0}var DQ=Symbol("crust.skill.commandAnnotations");function IX(Q){return Q instanceof EX?Q._node:Q}function CX(Q,X){let Z=IX(Q),H=BQ(typeof X==="string"||Array.isArray(X)?X:X.instructions??[]);if(H.length===0)return Q;let $=xQ(Z)?.instructions??[],W=[...new Set([...$,...H])];return Object.defineProperty(Z,DQ,{value:{instructions:W},enumerable:!0,configurable:!0}),Q}function xQ(Q){let X=Q[DQ];if(!X?.instructions||X.instructions.length===0)return;return{instructions:[...X.instructions]}}import{readdir as BZ,readFile as $X,realpath as YX,stat as qX}from"fs/promises";import{join as IQ,sep as WX}from"path";import{resolveSourceDir as zZ}from"@crustjs/utils";import{lstat as nX,mkdir as eQ,readlink as tX,realpath as eX,rm as qQ,symlink as QZ,writeFile as lQ}from"fs/promises";import{dirname as QX,join as sQ}from"path";class L extends Error{name="SkillConflictError";details;constructor(Q){super(LX(Q));this.details=Q}}function LX(Q){let X=`Skill conflict for agent "${Q.agent}": directory "${Q.outputDir}"`;if(Q.kindMismatch)return`${X} was installed as a "${Q.kindMismatch.existing}" skill but "${Q.kindMismatch.attempted}" was attempted. Use force: true to overwrite, or uninstall the existing skill first.`;if(Q.manifestMalformed){let{reason:Z,rawKind:H}=Q.manifestMalformed;switch(Z){case"unknown-kind":return`${X} was created by Crust but its crust.json declares an unrecognized kind "${H??"<unknown>"}" \u2014 likely a hand-edit typo or a `+"crust.json written by a newer Crust release. Fix the kind field, upgrade Crust, or pass force: true to overwrite.";case"parse-error":return`${X} contains a crust.json that is not valid JSON. Repair the file, or pass force: true to overwrite the directory.`;case"not-an-object":return`${X} contains a crust.json whose top-level value is not a JSON object. Repair the file, or pass force: true to overwrite the directory.`;case"missing-version":return`${X} contains a crust.json with no "version" string. Repair the file, or pass force: true to overwrite the directory.`}}return`${X} already exists but was not created by Crust (no crust.json found). Delete or rename the conflicting skill to resolve.`}function AQ(Q){return SQ(Q,[])}function SQ(Q,X){let Z=NX(Q.meta.name),H=[...X,Z],$=PX(Q.args),W=DX(Q.effectiveFlags),Y=SX(Q.subCommands,H),G=xQ(Q);return{name:Z,path:H,description:Q.meta.description,usage:Q.meta.usage,instructions:G?.instructions,runnable:typeof Q.run==="function",args:$,flags:W,children:Y}}function NX(Q){return Q.trim().toLowerCase()}function PX(Q){if(!Q||Q.length===0)return[];return Q.map(kX)}function kX(Q){let X={name:Q.name,type:vQ(Q.type),required:Q.required===!0,variadic:Q.variadic===!0};if(Q.description!==void 0)X.description=Q.description;if(Q.default!==void 0)X.default=hQ(Q.default);return X}function DX(Q){if(!Q)return[];return Object.keys(Q).sort().map((Z)=>{return AX(Z,Q[Z])})}function AX(Q,X){let Z={name:Q,type:vQ(X.type),required:X.required===!0,multiple:X.multiple===!0,short:X.short,aliases:X.aliases?[...X.aliases].sort():[]};if(X.description!==void 0)Z.description=X.description;if(X.default!==void 0)Z.default=hQ(X.default);return Z}function SX(Q,X){return Object.keys(Q).sort().map((H)=>{return SQ(Q[H],X)})}function vQ(Q){if(Q==="number"||Q==="boolean")return Q;return"string"}function hQ(Q){if(Array.isArray(Q))return JSON.stringify(Q);return String(Q)}function WQ(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 yQ(Q){return Q.replace(/(?<!\\)\|/g,"\\|")}function fQ(Q,X){let Z=[],H=gQ(Q);Z.push({path:"SKILL.md",content:vX(Q,X,H)});for(let $ of H){let W=i($),Y=$.children.length>0?gX($,Q):fX($,Q);Z.push({path:W,content:Y})}return Z}function gQ(Q){let X=[Q];for(let Z of Q.children)X.push(...gQ(Z));return X}function i(Q){if(Q.path.length<=1)return`commands/${Q.name}.md`;return`commands/${Q.path.slice(1).join("/")}.md`}function UQ(Q){return Q.path.join(" ")}function VQ(Q,X){let Z=Q.split("/").slice(0,-1),H=X.split("/"),$=0;while($<Z.length&&$<H.length&&Z[$]===H[$])$++;let W=Z.length-$,Y=H.slice($);if(W===0)return Y.join("/");return[...Array.from({length:W},()=>".."),...Y].join("/")}function vX(Q,X,Z){let H=[];if(H.push("---"),H.push(`name: ${WQ(X.name)}`),H.push(`description: ${WQ(X.description)}`),X.license)H.push(`license: ${WQ(X.license)}`);if(X.compatibility)H.push(`compatibility: ${WQ(X.compatibility)}`);if(X.disableModelInvocation)H.push("disable-model-invocation: true");if(X.allowedTools)H.push(`allowed-tools: ${WQ(X.allowedTools)}`);if(H.push("metadata:"),H.push(` version: "${X.version}"`),H.push("---"),H.push(""),H.push(`# ${X.name}`),H.push(""),Q.description)H.push(Q.description),H.push("");H.push(`You should use this skill when you need accurate help with \`${X.name}\` commands, including command selection, syntax, arguments, flags, defaults, and subcommands.`),H.push("");let $=sX(X.instructions);if(H.push("## How to Use This Skill"),H.push(""),H.push("1. You must find the command that best matches the user's task from the Command Reference below."),H.push("2. You must check the `Type` column before suggesting execution: `runnable` and `runnable, group` commands can be executed, while `group` commands are organizational only."),H.push("3. You should read only the linked file or files you need from `commands/`."),H.push("4. You must read a command's file before answering a command-specific question or suggesting that command."),H.push("5. You must treat the command file as the source of truth for usage, arguments, flags, aliases, and defaults."),H.push("6. If a flag, argument, alias, or default is not documented there, you must say it is not documented instead of guessing."),H.push(""),FQ($))H.push("## General Guidance"),H.push(""),H.push(...$),H.push("");if(H.push("## Command Reference"),H.push(""),H.push("You should use this table to locate the command file you need."),H.push(""),H.push(...hX(Z)),H.push(""),Q.runnable){H.push("## Usage"),H.push("");let W=i(Q);H.push(`The root command is directly executable. You should see [${Q.name}](${W}) for usage details.`),H.push("")}return H.join(`
|
|
3
|
+
`)}function hX(Q){let X=[];X.push("| Command | Type | Documentation |"),X.push("| ------- | ---- | ------------- |");for(let Z of Q){let H=UQ(Z),$=i(Z),W=yX(Z);X.push(`| \`${H}\` | ${W} | [${$}](${$}) |`)}return X}function yX(Q){if(Q.runnable&&Q.children.length>0)return"runnable, group";if(Q.runnable)return"runnable";return"group"}function fX(Q,X){let Z=[];return Z.push(...pQ(Q)),Z.push(...uQ(Q)),Z.push(...mQ(Q)),Z.push(...dQ(Q,X)),Z.join(`
|
|
4
|
+
`)}function gX(Q,X){let Z=[],H=i(Q);if(Z.push(...pQ(Q)),Z.push(...uQ(Q)),Q.runnable)Z.push(...mQ(Q));return Z.push(...uX(Q,H)),Z.push(...dQ(Q,X)),Z.join(`
|
|
5
|
+
`)}function pX(Q){let X=[...Q.path];for(let Z of Q.args)if(Z.variadic)X.push(Z.required?`<${Z.name}...>`:`[${Z.name}...]`);else X.push(Z.required?`<${Z.name}>`:`[${Z.name}]`);if(Q.flags.length>0)X.push("[options]");return X.join(" ")}function pQ(Q){let X=[`# \`${UQ(Q)}\``,""];if(Q.description)X.push(Q.description,"");return X}function uQ(Q){let X=Q.instructions??[];if(!FQ(X))return[];return["## Command Instructions","",...cQ(X),""]}function mQ(Q){let X=["## Usage","","```",Q.usage??pX(Q),"```",""];if(Q.args.length>0)X.push("## Arguments","",...mX(Q.args),"");if(Q.flags.length>0)X.push("## Flags","",...dX(Q.flags),"");return X.push("## Command Documentation Authority","","You must treat only the arguments, flags, options, aliases, and defaults documented in this file as supported for this command.","You must not infer or invent additional command-line options.",""),X}function uX(Q,X){let Z=["## Subcommands",""];for(let H of Q.children){let $=i(H),W=VQ(X,$),Y=H.description?` - ${H.description}`:"";Z.push(`- [\`${H.name}\`](${W})${Y}`)}return Z.push(""),Z}function mX(Q){let X=[];X.push("| Argument | Type | Required | Description |"),X.push("| -------- | ---- | -------- | ----------- |");for(let Z of Q){let H=Z.variadic?`${Z.name}...`:Z.name,$=Z.required?"Yes":"No",W=yQ(cX(Z));X.push(`| \`${H}\` | ${Z.type} | ${$} | ${W} |`)}return X}function cX(Q){let X=[];if(Q.description)X.push(Q.description);if(Q.default!==void 0)X.push(`Default: \`${Q.default}\``);return X.join(". ")||"-"}function dX(Q){let X=[];X.push("| Flag | Type | Required | Description |"),X.push("| ---- | ---- | -------- | ----------- |");for(let Z of Q){let H=rX(Z),$=Z.required?"Yes":"No",W=yQ(lX(Z));X.push(`| ${H} | ${Z.type} | ${$} | ${W} |`)}return X}function rX(Q){let X=[`\`--${Q.name}\``];if(Q.short)X.push(`\`-${Q.short}\``);for(let Z of Q.aliases)X.push(`\`--${Z}\``);return X.join(", ")}function lX(Q){let X=[];if(Q.description)X.push(Q.description);if(Q.multiple)X.push("Can be specified multiple times");if(Q.default!==void 0)X.push(`Default: \`${Q.default}\``);return X.join(". ")||"-"}function cQ(Q){return Q.map((X)=>`- ${X}`)}function sX(Q){if(typeof Q==="string")return kQ(Q);return cQ(BQ(Q))}function dQ(Q,X){let Z=[],H=i(Q);if(Z.push("---"),Z.push(""),Q.path.length>1){let W=Q.path.slice(0,-1),Y=rQ(X,W);if(Y){let G=i(Y),x=VQ(H,G),q=UQ(Y);Z.push(`Parent: [\`${q}\`](${x})`),Z.push("")}}let $=VQ(H,"SKILL.md");return Z.push(`[Skill Overview](${$})`),Z.push(""),Z}function rQ(Q,X){if(iX(Q.path,X))return Q;for(let Z of Q.children){let H=rQ(Z,X);if(H)return H}return}function iX(Q,X){if(Q.length!==X.length)return!1;for(let Z=0;Z<Q.length;Z++)if(Q[Z]!==X[Z])return!1;return!0}import{readFile as oX}from"fs/promises";import{join as aX}from"path";var t="crust.json";async function zQ(Q){let X;try{X=await oX(aX(Q,t),"utf-8")}catch{return{status:"absent"}}let Z;try{Z=JSON.parse(X)}catch{return{status:"malformed",reason:"parse-error"}}if(typeof Z!=="object"||Z===null)return{status:"malformed",reason:"not-an-object"};let H=Z;if(typeof H.version!=="string")return{status:"malformed",reason:"missing-version"};let{version:$,kind:W}=H;if(W===void 0)return{status:"ok",manifest:{version:$,kind:"generated"}};if(W==="bundle"||W==="generated")return{status:"ok",manifest:{version:$,kind:W}};return{status:"malformed",reason:"unknown-kind",rawKind:typeof W==="string"?W:JSON.stringify(W)}}async function wQ(Q){let X=await zQ(Q);return X.status==="ok"?X.manifest:null}async function YQ(Q){return(await wQ(Q))?.version??null}var XZ="auto";async function ZZ(Q){if(Q!==void 0)return Q;return[...S(),...await n()]}function XX(Q){if(Q!==void 0)return Q;return[...a]}var ZX=/^[a-z0-9]+(-[a-z0-9]+)*$/;function e(Q){return Q.length>=1&&Q.length<=64&&ZX.test(Q)}function QQ(Q){return Q}function _Q(Q){return Q.startsWith("use-")?Q:`use-${Q}`}async function XQ(Q){let{command:X,meta:Z,scope:H="global",clean:$=!0,force:W=!1,installMode:Y=XZ}=Q,G=await ZZ(Q.agents),x=QQ(Z.name),q=_Q(Z.name);if(!e(x))throw Error(`Invalid skill name "${x}": must be 1\u201364 lowercase `+`alphanumeric characters and hyphens, no leading/trailing/consecutive hyphens. Pattern: ${ZX.source}`);if(G.length===0)return{agents:[]};let F={...Z,name:x},K=AQ(X),V=fQ(K,F);return OQ({files:V,meta:F,agents:G,scope:H,clean:$,force:W,installMode:Y,kind:"generated",legacyResolvedName:q})}async function OQ(Q){let{files:X,meta:Z,agents:H,scope:$,clean:W,force:Y,installMode:G,kind:x,legacyResolvedName:q}=Q,F=H[0];if(!F)return{agents:[]};let K=JZ(Z,x),V=[...X,...K].sort((B,U)=>B.path<U.path?-1:B.path>U.path?1:0),w=V.map((B)=>B.path),M=new Map;for(let B of H){let U=N(B,$,Z.name),_=M.get(U);if(_)_.push(B);else M.set(U,[B])}let O=c($,Z.name),T=c($,q),b=new Map;for(let[B,U]of M){let _=U[0];if(!_)continue;b.set(B,await bQ({outputDir:B,legacyOutputDir:N(_,$,q),canonicalOutputDir:O,legacyCanonicalOutputDir:T}))}let v=await zQ(O),R=v.status==="ok"?v.manifest:null,P=R?.version??null;if((await jQ(O,O)).exists&&R===null&&!Y)throw new L({agent:F,outputDir:O,manifestMalformed:iQ(v)});if(R!==null&&R.kind!==x&&!Y)throw new L({agent:F,outputDir:O,kindMismatch:{existing:R.kind,attempted:x}});let D=R!==null&&R.kind!==x,g=P!==Z.version||D;if(g){if(W)await RQ(O);await HX(O,V)}let p=[];for(let[B,U]of M){let _=U[0];if(!_)continue;let z=b.get(B);if(!z)continue;if(z.current.inspection.exists&&!z.current.isCrustManaged&&!Y){let $Q=await zQ(B);throw new L({agent:_,outputDir:B,manifestMalformed:iQ($Q)})}if(z.current.manifest!==null&&z.current.manifest.kind!==x&&!Y)throw new L({agent:_,outputDir:B,kindMismatch:{existing:z.current.manifest.kind,attempted:x}});let j=await $Z({outputDir:B,canonicalOutputDir:O,allFiles:V,clean:W,installMode:G,inspection:z.current.inspection,installedVersion:z.preferredVersion,currentVersion:Z.version,installedKind:z.current.manifest?.kind??null,currentKind:x}),E=await YZ(z),C=HZ({installedVersion:z.preferredVersion,currentVersion:Z.version,canonicalChanged:g,pathChanged:j||E||z.preferredOutputDir!==B});for(let $Q of U)p.push({agent:$Q,outputDir:B,files:C==="up-to-date"?[]:w,status:C,previousVersion:C==="updated"?z.preferredVersion??void 0:void 0})}if(q!==Z.name){let B=await YQ(T);if(T!==O&&B!==null&&!await TQ(q,$))await qQ(T,{recursive:!0,force:!0})}return{agents:p}}async function KQ(Q){let{name:X,scope:Z="global"}=Q,H=XX(Q.agents),$=QQ(X),W=_Q(X),Y=c(Z,$),G=c(Z,W),x=[],q=new Map;for(let F of H){let K=N(F,Z,$),V=q.get(K);if(V)V.push(F);else q.set(K,[F])}for(let[F,K]of q){let V=K[0];if(!V)continue;let w=N(V,Z,W),M=await bQ({outputDir:F,legacyOutputDir:w,canonicalOutputDir:Y,legacyCanonicalOutputDir:G}),O=await MQ(M.current),T=M.legacy.outputDir!==M.current.outputDir?await MQ(M.legacy):!1,b=O||T,v=O?F:T?w:F;for(let R of K)x.push({agent:R,outputDir:v,status:b?"removed":"not-found"})}if(await YQ(Y)!==null&&!await TQ($,Z))await qQ(Y,{recursive:!0,force:!0});{let F=await YQ(G);if(G!==Y&&F!==null&&!await TQ(W,Z))await qQ(G,{recursive:!0,force:!0})}return{agents:x}}async function d(Q){let{name:X,scope:Z="global"}=Q,H=XX(Q.agents),$=QQ(X),W=_Q(X),Y=[],G=new Map;for(let x of H){let q=N(x,Z,$),F=G.get(q);if(F)F.push(x);else G.set(q,[x])}for(let[x,q]of G){let F=q[0];if(!F)continue;let K=N(F,Z,W),V=c(Z,$),w=c(Z,W),M=await bQ({outputDir:x,legacyOutputDir:K,canonicalOutputDir:V,legacyCanonicalOutputDir:w}),O=M.preferredOutputDir??x,T=M.preferredVersion;for(let b of q)Y.push({agent:b,outputDir:O,installed:T!==null,version:T??void 0})}return{agents:Y}}function iQ(Q){if(Q.status!=="malformed")return;return Q.rawKind!==void 0?{reason:Q.reason,rawKind:Q.rawKind}:{reason:Q.reason}}function HZ(Q){let{installedVersion:X,currentVersion:Z,canonicalChanged:H,pathChanged:$}=Q;if(X===null)return"installed";if(X===Z&&!H&&!$)return"up-to-date";return"updated"}async function $Z(Q){let{outputDir:X,canonicalOutputDir:Z,allFiles:H,clean:$,installMode:W,inspection:Y,installedVersion:G,currentVersion:x,installedKind:q,currentKind:F}=Q;if(W==="copy")return oQ({outputDir:X,allFiles:H,clean:$,inspection:Y,installedVersion:G,currentVersion:x,installedKind:q,currentKind:F});try{return await WZ({outputDir:X,canonicalOutputDir:Z,inspection:Y})}catch(K){if(W==="symlink")throw Error(`Failed to create symlink at "${X}" (installMode: symlink).`,{cause:K});let V=await jQ(X,Z);return oQ({outputDir:X,allFiles:H,clean:$,inspection:V,installedVersion:G,currentVersion:x,installedKind:q,currentKind:F})}}async function oQ(Q){let{outputDir:X,allFiles:Z,clean:H,inspection:$,installedVersion:W,currentVersion:Y,installedKind:G,currentKind:x}=Q,q=G!==null&&G!==x;if(!(!$.exists||$.isSymlink||W!==Y||q))return!1;if($.isSymlink||H)await RQ(X);return await HX(X,Z),!0}async function WZ(Q){let{outputDir:X,canonicalOutputDir:Z,inspection:H}=Q;if(H.exists&&H.isSymlink&&H.pointsToCanonical)return!1;if(H.exists)await RQ(X);return await qZ(Z,X),!0}async function jQ(Q,X){let Z;try{Z=await nX(Q)}catch{return{exists:!1,isSymlink:!1,pointsToCanonical:!1}}let H=process.platform==="win32"&&Z.isDirectory()&&await tQ(Q)!==null;if(!(Z.isSymbolicLink()||H))return{exists:!0,isSymlink:!1,pointsToCanonical:!1};let[W,Y,G]=await Promise.all([nQ(Q),nQ(X),tQ(Q)]);return{exists:!0,isSymlink:!0,pointsToCanonical:W!==null&&Y!==null&&W===Y||G===X}}async function aQ(Q,X){let[Z,H]=await Promise.all([wQ(Q),jQ(Q,X)]),$=Z?.version??null,W=$!==null||H.exists&&H.isSymlink&&H.pointsToCanonical;return{outputDir:Q,version:$,manifest:Z,inspection:H,isCrustManaged:W}}async function bQ(Q){let{outputDir:X,legacyOutputDir:Z,canonicalOutputDir:H,legacyCanonicalOutputDir:$}=Q,W=await aQ(X,H),Y=Z===X?W:await aQ(Z,$);if(W.isCrustManaged)return{current:W,legacy:Y,preferredVersion:W.version,preferredOutputDir:W.outputDir};if(Y.isCrustManaged)return{current:W,legacy:Y,preferredVersion:Y.version,preferredOutputDir:Y.outputDir};return{current:W,legacy:Y,preferredVersion:null,preferredOutputDir:null}}async function MQ(Q){if(!Q.isCrustManaged||!Q.inspection.exists)return!1;return await qQ(Q.outputDir,{recursive:!0,force:!0}),!0}async function YZ(Q){if(Q.legacy.outputDir===Q.current.outputDir)return!1;return MQ(Q.legacy)}async function nQ(Q){try{return await eX(Q)}catch{return null}}async function tQ(Q){try{return await tX(Q)}catch{return null}}async function qZ(Q,X){await eQ(QX(X),{recursive:!0});let Z=process.platform==="win32"?"junction":"dir";await QZ(Q,X,Z)}async function TQ(Q,X){let Z=new Set;for(let H of a)Z.add(N(H,X,Q));for(let H of Z)if(await YQ(H)!==null)return!0;return!1}function JZ(Q,X){let Z={name:Q.name,description:Q.description,version:Q.version,kind:X};return[{path:t,content:`${JSON.stringify(Z,null,"\t")}
|
|
6
|
+
`}]}async function RQ(Q){await qQ(Q,{recursive:!0,force:!0})}async function HX(Q,X){let Z=new Set;for(let $ of X){let W=sQ(Q,$.path),Y=QX(W);Z.add(Y)}let H=[...Z].sort();for(let $ of H)await eQ($,{recursive:!0});for(let $ of X){let W=sQ(Q,$.path);if(typeof $.content==="string")await lQ(W,$.content,"utf-8");else await lQ(W,$.content)}}var EQ="SKILL.md";function KZ(Q){let X={name:null,description:null},H=(Q.startsWith("\uFEFF")?Q.slice(1):Q).split(/\r?\n/,51),$=0;while($<H.length&&H[$]?.trim()==="")$++;if($>=H.length||H[$]!=="---")return X;$++;let W=Math.min(50,H.length);for(let Y=$;Y<W;Y++){let G=H[Y];if(G===void 0)break;if(/^---\s*$/.test(G))break;let x=G.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*?)\s*$/);if(!x)continue;let q=x[1];if(q!=="name"&&q!=="description")continue;if(X[q]!==null)continue;X[q]=GZ(x[2]??"")}return X}function GZ(Q){let X=Q[0];if(X==='"'||X==="'"){let H=Q.indexOf(X,1);if(H!==-1)return Q.slice(1,H)}let Z=Q.search(/(^|\s)#/);return Z===-1?Q:Q.slice(0,Z).trimEnd()}function FZ(Q,X,Z){let H=X.endsWith(WX)?X:X+WX;if(Q!==X&&!Q.startsWith(H))throw Error(`Bundle path traversal rejected: "${Z}" resolves to "${Q}", which is outside the bundle root "${X}".`)}async function JX(Q,X,Z,H){let $=await BZ(Q,{withFileTypes:!0}),W=[];for(let Y of $){let G=Y.name,x=IQ(Q,G),q=Z===""?G:`${Z}/${G}`,F=await YX(x);FZ(F,X,x);let K=await qX(F);if(K.isDirectory()){if(H.has(F))continue;H.add(F),W.push(...await JX(x,X,q,H))}else if(K.isFile())W.push({relPath:q,absPath:x})}return W}async function xZ(Q){let X=zZ(Q),Z;try{Z=await YX(X)}catch(F){throw Error(`Bundle source directory "${X}" does not exist or is not accessible.`,{cause:F})}if(!(await qX(Z)).isDirectory())throw Error(`Bundle source path "${Z}" is not a directory.`);let W=await JX(Z,Z,"",new Set([Z])),Y=W.find((F)=>F.relPath===EQ);if(!Y)throw Error(`Bundle is missing SKILL.md at the bundle root "${Z}". Every skill bundle must contain a top-level SKILL.md file.`);if(W.some((F)=>F.relPath===t))throw Error(`Bundle source at "${Z}" contains a reserved file "${t}" at the root. Crust regenerates this file during installation; remove it from your bundle source.`);let G=await Promise.all(W.map(async(F)=>({path:F.relPath,content:await $X(F.absPath)}))),x=await $X(Y.absPath,"utf-8"),q=KZ(x);if(q.name===null||q.name==="")throw Error(`Bundle SKILL.md is missing a top-level \`name:\` field in its YAML frontmatter (at "${IQ(Z,EQ)}"). Add \`name: <skill-name>\` to the frontmatter block.`);if(q.description===null||q.description==="")throw Error(`Bundle SKILL.md is missing a top-level \`description:\` field in its YAML frontmatter (at "${IQ(Z,EQ)}"). Add \`description: <one-line summary>\` to the frontmatter block.`);return{files:G,frontmatter:{name:q.name,description:q.description}}}async function ZQ(Q){let{sourceDir:X,agents:Z,version:H,scope:$="global",clean:W=!0,force:Y=!1,installMode:G="auto",expectedName:x}=Q,{files:q,frontmatter:F}=await xZ(X),K=QQ(F.name);if(!e(K))throw Error(`Invalid skill name "${K}" in SKILL.md frontmatter: must be 1\u201364 lowercase `+"alphanumeric characters and hyphens, no leading/trailing/consecutive hyphens.");if(x!==void 0&&K!==x)throw Error(`Bundle SKILL.md frontmatter name "${K}" does not match the expected name "${x}". Update the bundle's SKILL.md frontmatter \`name:\` field, or change the configured \`name\` to match.`);if(Z.length===0)return{agents:[]};let V={name:K,description:F.description,version:H};return OQ({files:[...q],meta:V,agents:Z,scope:$,clean:W,force:Y,installMode:G,kind:"bundle",legacyResolvedName:K})}import{Crust as zX,VALIDATION_MODE_ENV as VZ}from"@crustjs/core";import{spinner as h}from"@crustjs/progress";import{confirm as KX,multiselect as GX,select as UZ}from"@crustjs/prompts";import{bold as l,dim as k,yellow as o}from"@crustjs/style";var wZ="skill",FX="global",r="__universal__";function xX(Q){return Q==="global"||Q==="project"}function MZ(Q){return Q==="auto"||Q==="symlink"||Q==="copy"}function VX(Q){if(Q===void 0)return;if(!xX(Q))throw Error(`Invalid --scope value: ${String(Q)}. Expected "project" or "global".`);return Q}async function UX(Q,X){let Z=VX(Q);if(Z!==void 0)return Z;if(X.defaultScope)return X.defaultScope;return UZ({message:"Select scope",choices:[{label:"Project",value:"project"},{label:"Global",value:"global"}],default:FX})}function HQ(Q){let X=new Set(S()),Z=[];if(Q.some((H)=>X.has(H)))Z.push("Universal");for(let H of Q){if(X.has(H))continue;Z.push(A[H])}return Z}function GQ(Q){let X=new Set(S()),Z=[],H=Q.find(($)=>X.has($.agent));if(H)Z.push({label:"Universal",outputDir:H.outputDir});for(let $ of Q){if(X.has($.agent))continue;Z.push({label:A[$.agent],outputDir:$.outputDir})}return Z}function CQ(Q,X){return{name:Q.meta.name,description:Q.meta.description??"",version:X.version,instructions:X.instructions,license:X.license,allowedTools:X.allowedTools,compatibility:X.compatibility,disableModelInvocation:X.disableModelInvocation}}function TZ(Q,X){if(X===void 0)return[];if(!Array.isArray(X))throw Error(`skillPlugin: customSkills must be an array, got ${X===null?"null":typeof X}.`);if(X.length===0)return[];let Z=new Set;for(let H=0;H<X.length;H++){let $=X[H];if(!$||typeof $!=="object")throw Error(`skillPlugin: customSkills[${H}] must be an object, got ${$===null?"null":typeof $}.`);if(typeof $.name!=="string"||$.name.length===0)throw Error(`skillPlugin: customSkills[${H}].name must be a non-empty string.`);if(!e($.name))throw Error(`skillPlugin: customSkills[${H}].name "${$.name}" is not a valid skill name. `+"Must be 1\u201364 lowercase alphanumeric characters and hyphens, "+"no leading/trailing/consecutive hyphens.");if($.name===Q)throw Error(`skillPlugin: customSkills[${H}].name "${$.name}" collides with the main skill name. Custom skill bundle names must differ from the root command name.`);if(Z.has($.name))throw Error(`skillPlugin: customSkills contains duplicate name "${$.name}". Each entry must declare a unique name.`);if(Z.add($.name),$.version!==void 0&&(typeof $.version!=="string"||$.version.length===0))throw Error(`skillPlugin: customSkills[${H}].version (for "${$.name}") must be a non-empty string when set, or omitted to inherit the plugin's \`version\`.`);if(typeof $.sourceDir!=="string"&&!($.sourceDir instanceof URL))throw Error(`skillPlugin: customSkills[${H}].sourceDir (for "${$.name}") must be a string or URL, got ${typeof $.sourceDir}.`);if($.scope!==void 0&&!xX($.scope))throw Error(`skillPlugin: customSkills[${H}].scope (for "${$.name}") must be "project" or "global", got ${JSON.stringify($.scope)}.`);if($.installMode!==void 0&&!MZ($.installMode))throw Error(`skillPlugin: customSkills[${H}].installMode (for "${$.name}") must be "auto", "symlink", or "copy", got ${JSON.stringify($.installMode)}.`)}return X}function _Z(Q,X){let Z=Q.scope??X.defaultScope;if(Z!==void 0)return[u(Z)];return[...new Set(["project","global"].map((H)=>u(H)))]}async function OZ(Q,X){let Z=[...S(),...m()];if(Z.length===0)return;let H=_Z(Q,X),$=Q.installMode??X.installMode,W=Q.version??X.version;for(let Y of H){let x=(await d({name:Q.name,agents:Z,scope:Y})).agents.filter((q)=>{if(!q.installed)return!1;let F=N(q.agent,Y,Q.name);return q.version!==W||q.outputDir!==F});if(x.length===0)continue;try{await h({message:`Updating ${Y} skills [${Q.name}]...`,task:async({updateMessage:q})=>{let F=await ZQ({sourceDir:Q.sourceDir,agents:x.map((w)=>w.agent),version:W,scope:Y,installMode:$,expectedName:Q.name}),K=F.agents.filter((w)=>w.status==="updated").map((w)=>w.agent),V=HQ(K);if(V.length>0)q(`Updated bundle "${Q.name}" to v${W} for ${V.join(", ")} (${Y})`);return F}})}catch(q){if(q instanceof L){let F=q.details.kindMismatch?` (existing skill is "${q.details.kindMismatch.existing}", attempted "${q.details.kindMismatch.attempted}")`:"";console.warn(o(`Skill conflict [${Q.name}]: "${q.details.outputDir}" already exists but conflicts with the requested install${F}. Skipping auto-update for ${Y}. Delete or rename the conflicting skill to resolve.`))}else throw q}}}function LQ(Q,X,Z,H){if(!H.installed)return!1;let $=N(Q,X,Z.name);return H.version!==Z.version||H.outputDir!==$}async function jZ(Q,X,Z){let H=[...S(),...m()];if(H.length===0){await BX(Z,X);return}let $=CQ(Q,X),W=[...new Set(["project","global"].map((Y)=>u(Y)))];for(let Y of W){let x=(await d({name:$.name,agents:H,scope:Y})).agents.filter((q)=>LQ(q.agent,Y,$,q));if(x.length===0)continue;try{await h({message:`Updating ${Y} skills...`,task:async({updateMessage:q})=>{let F=await XQ({command:Q,meta:$,agents:x.map((w)=>w.agent),scope:Y,installMode:X.installMode}),K=F.agents.filter((w)=>w.status==="updated").map((w)=>w.agent),V=HQ(K);if(V.length>0)q(`Updated skill "${$.name}" to v${$.version} for ${V.join(", ")} (${Y})`);return F}})}catch(q){if(q instanceof L)console.warn(o(`Skill conflict: "${q.details.outputDir}" already exists but was not created by ${$.name}. Skipping auto-update for ${Y}. Delete or rename the conflicting skill to resolve.`));else throw q}}await BX(Z,X)}async function BX(Q,X){for(let Z of Q)try{await OZ(Z,X)}catch(H){let $=H instanceof Error?H.message:String(H);console.warn(o(`Skill auto-update failed [${Z.name}]: ${$}. Continuing with remaining skills.`))}}function bZ(Q){let X;return{name:"skills",async setup(Z,H){X=Z.rootCommand;let $=Q.command??wZ,W=TZ(X.meta.name,Q.customSkills);if(H.addSubCommand(X,$,EZ(X,Q,W,$)),process.env[VZ]==="1")return;if(Z.argv[0]===$)return;if(Q.autoUpdate!==!1)await jZ(X,Q,W)}}}async function RZ(Q){let{entry:X,options:Z,scope:H,installAll:$,isInteractive:W}=Q,Y=X.installMode??Z.installMode,G=X.version??Z.version,x=await n(),q=S(),F=m(),K=await d({name:X.name,agents:[...q,...F],scope:H}),V=new Set(K.agents.filter((B)=>B.installed).map((B)=>B.agent)),w=new Set(x),M=new Map(K.agents.map((B)=>[B.agent,B])),O=F.filter((B)=>{if(w.has(B))return!0;return M.get(B)?.installed===!0}),T=O.filter((B)=>V.has(B)),b=[];if(q.length>0){let B=q[0];if(!B)throw Error("Expected at least one universal agent");let _=M.get(B)?.outputDir??"path unavailable";b.push({label:"Universal",value:r,hint:_});let z=q.map((j)=>A[j]).join(", ");if(W&&!$)console.log(k(`Agents supporting universal skills: ${z}`))}for(let B of O){let _=M.get(B)?.outputDir??"path unavailable";b.push({label:A[B],value:B,hint:_})}let v=q.length>0&&q.every((B)=>V.has(B)),R=[...T.filter((B)=>!q.includes(B))];if(v)R.unshift(r);let P;if($)P=[...q,...O];else{let B=b.length===0?[]:await GX({message:`Select agents to install skills for [${X.name}]`,choices:b,default:R,required:!1}),U=new Set(B.filter((_)=>_!==r));if(B.includes(r))for(let _ of q)U.add(_);P=[...U]}let I=P.filter((B)=>!V.has(B)),D=P.filter((B)=>{let U=M.get(B);if(!U?.installed)return!1;let _=N(B,H,X.name);return U.version!==G||U.outputDir!==_}),g=[...V].filter((B)=>!P.includes(B)),p=[...I,...D];if(p.length>0)try{let B=await h({message:`Installing skills [${X.name}]...`,task:async()=>ZQ({sourceDir:X.sourceDir,agents:p,version:G,scope:H,installMode:Y,expectedName:X.name})});console.log(`
|
|
7
|
+
${l(`Installed bundle "${X.name}" v${G}`)}`);for(let U of GQ(B.agents))console.log(k(` ${U.label} \u2192 ${U.outputDir}`))}catch(B){if(B instanceof L){let U=B.details.kindMismatch?` (existing is a "${B.details.kindMismatch.existing}" skill, attempted "${B.details.kindMismatch.attempted}")`:" but was not created by Crust";if($?!0:await KX({message:`"${B.details.outputDir}" already exists${U}. Overwrite?`,default:!1})){let z=await h({message:`Overwriting bundle [${X.name}]...`,task:async()=>ZQ({sourceDir:X.sourceDir,agents:[B.details.agent],version:G,scope:H,force:!0,installMode:Y,expectedName:X.name})});console.log(`
|
|
8
|
+
${l(`Installed bundle "${X.name}" v${G}`)}`);for(let j of GQ(z.agents))console.log(k(` ${j.label} \u2192 ${j.outputDir}`))}else console.log(k(`
|
|
9
|
+
Skipped ${A[B.details.agent]} [${X.name}]`))}else throw B}if(g.length>0){let U=(await h({message:`Removing skills [${X.name}]...`,task:async()=>KQ({name:X.name,agents:g,scope:H})})).agents.filter((z)=>z.status==="removed").map((z)=>z.agent),_=HQ(U);if(_.length>0)console.log(`
|
|
10
|
+
${l(`Removed bundle "${X.name}" from ${_.join(", ")}`)}`)}if(p.length===0&&g.length===0)console.log(k(`No changes [${X.name}].`))}function EZ(Q,X,Z,H){let $=IZ(Q,X,Z);return new zX(H).meta({description:"Manage agent skill installations"}).flags({scope:{type:"string",description:"Install scope (project or global)"},all:{type:"boolean",description:"Install for all detected agents non-interactively (universal + detected)"}}).run(async(W)=>{let Y=CQ(Q,X),G=W.flags.all===!0,x=!!process.stdin.isTTY,q=G?VX(W.flags.scope)??X.defaultScope??FX:await UX(W.flags.scope,X),F=await n(),K=S(),V=m(),w=await d({name:Y.name,agents:[...K,...V],scope:q}),M=new Set(w.agents.filter((z)=>z.installed).map((z)=>z.agent)),O=new Set(F),T=new Map(w.agents.map((z)=>[z.agent,z])),b=V.filter((z)=>{if(O.has(z))return!0;return T.get(z)?.installed===!0}),v=b.filter((z)=>M.has(z)),R=[];if(K.length>0){let z=K[0];if(!z)throw Error("Expected at least one universal agent");let E=T.get(z)?.outputDir??"path unavailable";R.push({label:"Universal",value:r,hint:E});let C=K.map(($Q)=>A[$Q]).join(", ");if(x&&!G)console.log(k(`Agents supporting universal skills: ${C}`))}for(let z of b){let E=T.get(z)?.outputDir??"path unavailable";R.push({label:A[z],value:z,hint:E})}let P=K.length>0&&K.every((z)=>M.has(z)),I=[...v.filter((z)=>!K.includes(z))];if(P)I.unshift(r);let D;if(G)D=[...K,...b];else{let z=R.length===0?[]:await GX({message:"Select agents to install skills for",choices:R,default:I,required:!1}),j=new Set(z.filter((E)=>E!==r));if(z.includes(r))for(let E of K)j.add(E);D=[...j]}let g=D.filter((z)=>!M.has(z)),p=D.filter((z)=>{let j=T.get(z);return j!==void 0&&LQ(z,q,Y,j)}),B=[...M].filter((z)=>!D.includes(z)),U=[...g,...p];if(U.length>0)try{let z=await h({message:"Installing skills...",task:async()=>XQ({command:Q,meta:Y,agents:U,scope:q,installMode:X.installMode})});console.log(`
|
|
11
|
+
${l(`Installed "${Y.name}" v${Y.version}`)}`);for(let j of GQ(z.agents))console.log(k(` ${j.label} \u2192 ${j.outputDir}`))}catch(z){if(z instanceof L)if(G?!0:await KX({message:`"${z.details.outputDir}" already exists but was not created by Crust. Overwrite?`,default:!1})){let E=await h({message:"Overwriting skill...",task:async()=>XQ({command:Q,meta:Y,agents:[z.details.agent],scope:q,force:!0,installMode:X.installMode})});console.log(`
|
|
12
|
+
${l(`Installed "${Y.name}" v${Y.version}`)}`);for(let C of GQ(E.agents))console.log(k(` ${C.label} \u2192 ${C.outputDir}`))}else console.log(k(`
|
|
13
|
+
Skipped ${A[z.details.agent]}`));else throw z}if(B.length>0){let j=(await h({message:"Removing skills...",task:async()=>KQ({name:Y.name,agents:B,scope:q})})).agents.filter((C)=>C.status==="removed").map((C)=>C.agent),E=HQ(j);if(E.length>0)console.log(`
|
|
14
|
+
${l(`Removed from ${E.join(", ")}`)}`)}if(U.length===0&&B.length===0)console.log(k("No changes."));let _=[];for(let z of Z){let j=z.scope??q;try{await RZ({entry:z,options:X,scope:j,installAll:G,isInteractive:x})}catch(E){let C=E instanceof Error?E.message:String(E);console.warn(o(`Skill reconciliation failed [${z.name}]: ${C}. Continuing with remaining skills.`)),_.push(z.name)}}if(_.length>0)process.exitCode=1}).command($)._node}function IZ(Q,X,Z){return new zX("update").meta({description:"Update installed skills to latest version"}).flags({scope:{type:"string",description:"Update scope (project or global)"}}).run(async(H)=>{let $=await UX(H.flags.scope,X),W=u($),Y=[...S(),...m()],G=CQ(Q,X),q=(await d({name:G.name,agents:Y,scope:$})).agents.filter((K)=>LQ(K.agent,$,G,K));if(q.length===0)console.log(k(`No updates needed (${W}).`));else try{let V=(await h({message:`Updating ${W} skills...`,task:async()=>XQ({command:Q,meta:G,agents:q.map((M)=>M.agent),scope:$,installMode:X.installMode})})).agents.filter((M)=>M.status==="updated").map((M)=>M.agent),w=HQ(V);if(w.length>0)console.log(`
|
|
15
|
+
${l(`Updated "${G.name}" to v${G.version} for ${w.join(", ")} (${W})`)}`)}catch(K){if(K instanceof L)console.warn(o(`Skipped ${A[K.details.agent]}: "${K.details.outputDir}" already exists but was not created by ${G.name}. Delete or rename the conflicting directory to resolve.`));else throw K}let F=[];for(let K of Z){let V=K.scope??$,w=u(V),M=K.installMode??X.installMode,O=K.version??X.version;try{let b=(await d({name:K.name,agents:Y,scope:V})).agents.filter((I)=>{if(!I.installed)return!1;let D=N(I.agent,V,K.name);return I.version!==O||I.outputDir!==D});if(b.length===0){console.log(k(`No updates needed [${K.name}] (${w}).`));continue}let R=(await h({message:`Updating ${w} skills [${K.name}]...`,task:async()=>ZQ({sourceDir:K.sourceDir,agents:b.map((I)=>I.agent),version:O,scope:V,installMode:M,expectedName:K.name})})).agents.filter((I)=>I.status==="updated").map((I)=>I.agent),P=HQ(R);if(P.length>0)console.log(`
|
|
16
|
+
${l(`Updated bundle "${K.name}" to v${O} for ${P.join(", ")} (${w})`)}`)}catch(T){if(T instanceof L){let b=T.details.kindMismatch?` (existing is a "${T.details.kindMismatch.existing}" skill, attempted "${T.details.kindMismatch.attempted}")`:"";console.warn(o(`Skipped ${A[T.details.agent]} [${K.name}]: "${T.details.outputDir}" already exists${b}. Delete or rename the conflicting directory to resolve.`))}else{let b=T instanceof Error?T.message:String(T);console.warn(o(`Skill update failed [${K.name}]: ${b}. Continuing with remaining skills.`)),F.push(K.name)}}}if(F.length>0)process.exitCode=1})}export{KQ as uninstallSkill,d as skillStatus,bZ as skillPlugin,QQ as resolveSkillName,c as resolveCanonicalSkillPath,e as isValidSkillName,bX as isUniversalAgent,ZQ as installSkillBundle,S as getUniversalAgents,m as getAdditionalAgents,XQ as generateSkill,n as detectInstalledAgents,CX as annotate,L as SkillConflictError};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crustjs/skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Agent skill generation from Crust command definitions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,17 +43,18 @@
|
|
|
43
43
|
"publish": "bun publish --no-git-checks || true"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@crustjs/progress": "0.0.
|
|
47
|
-
"@crustjs/prompts": "0.0
|
|
48
|
-
"@crustjs/style": "0.
|
|
46
|
+
"@crustjs/progress": "0.0.4",
|
|
47
|
+
"@crustjs/prompts": "0.1.0",
|
|
48
|
+
"@crustjs/style": "0.2.0",
|
|
49
|
+
"@crustjs/utils": "0.0.2"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"@crustjs/config": "0.0.0",
|
|
52
|
-
"@crustjs/core": "0.0.
|
|
53
|
+
"@crustjs/core": "0.0.18",
|
|
53
54
|
"bunup": "^0.16.31"
|
|
54
55
|
},
|
|
55
56
|
"peerDependencies": {
|
|
56
|
-
"@crustjs/core": "0.0.
|
|
57
|
+
"@crustjs/core": "0.0.18",
|
|
57
58
|
"typescript": "^6.0.3"
|
|
58
59
|
}
|
|
59
60
|
}
|