@crustjs/skills 0.0.24 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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 `defineCommand` metadata. The output is a portable skill bundle that developers can download and install into their own agent environments (OpenCode, Claude Code, etc.).
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()` is a runtime plugin. Register it in `runMain(..., { plugins })`.
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 { defineCommand, runMain } from "@crustjs/core";
47
+ import { Crust } from "@crustjs/core";
49
48
  import { skillPlugin } from "@crustjs/skills";
50
49
 
51
- const app = defineCommand({
52
- meta: { name: "my-cli", description: "My CLI" },
53
- run() {
54
- console.log("hello");
55
- },
56
- });
57
-
58
- runMain(app, {
59
- plugins: [
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, use the exported primitives
85
- directly in your own setup logic:
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 { defineCommand, runMain } from "@crustjs/core";
89
- import { detectInstalledAgents, generateSkill, skillStatus } from "@crustjs/skills";
90
-
91
- const app = defineCommand({
92
- meta: { name: "my-cli", description: "My CLI" },
93
- async run() {
94
- // Detect agents and install skills if not yet present
95
- const agents = await detectInstalledAgents();
96
- const status = await skillStatus({ name: "my-cli", agents, scope: "global" });
97
-
98
- const notInstalled = status.agents
99
- .filter((a) => !a.installed)
100
- .map((a) => a.agent);
101
-
102
- if (notInstalled.length > 0) {
103
- await generateSkill({
104
- command: app,
105
- meta: { name: "my-cli", description: "My CLI", version: "1.0.0" },
106
- agents: notInstalled,
107
- scope: "global",
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
- runMain(app);
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 plugin is passed to `runMain(..., { plugins: [...] })`.
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 { defineCommand, runMain } from "@crustjs/core";
207
+ import { Crust } from "@crustjs/core";
130
208
 
131
- // Export the command object — used by skill generation
132
- export const rootCommand = defineCommand({
133
- meta: { name: "my-cli", description: "My CLI tool" },
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 run when executed directly — not when imported for generation
216
+ // Only execute when run directly — not when imported for generation.
140
217
  if (import.meta.main) {
141
- runMain(rootCommand);
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 `runMain`.
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 as an ownership marker. If `generateSkill()` encounters an existing directory without `crust.json`, it throws a `SkillConflictError` to prevent overwriting skills created manually or by other tools.
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
- ### Uninstall Cleanup
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
- 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.
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
- Pass `force: true` to overwrite, or handle the error:
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
- console.error(`Conflict: ${err.details.outputDir}`);
398
- // err.details.agent — the agent where the conflict occurred
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
- /** Agent targets to install skills for */
120
- agents: AgentTarget[];
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
- /** Agent targets to uninstall from */
184
- agents: AgentTarget[];
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
- /** Agent targets to check */
206
- agents: AgentTarget[];
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 `generateSkill()` detects that the target skill directory
369
- * already exists but was not created by Crust (i.e. has no `crust.json`).
739
+ * Thrown when an install entrypoint detects that the target skill directory
740
+ * already exists but cannot be overwritten safely.
370
741
  *
371
- * This prevents Crust from silently overwriting a skill that was manually
372
- * created or installed by another tool.
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
- * console.error(
383
- * `Conflict: "${err.details.outputDir}" already exists and was not created by Crust.`,
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 FZ(Q){let Z=[];Z.push("| Command | Type | Documentation |"),Z.push("| ------- | ---- | ------------- |");for(let X of Q){let $=YQ(X),W=A(X),H=wZ(X);Z.push(`| \`${$}\` | ${H} | [${W}](${W}) |`)}return Z}function wZ(Q){if(Q.runnable&&Q.children.length>0)return"runnable, group";if(Q.runnable)return"runnable";return"group"}function UZ(Q,Z){let X=[];return X.push(...LQ(Q)),X.push(...PQ(Q)),X.push(...CQ(Q)),X.push(...DQ(Q,Z)),X.join(`
4
- `)}function VZ(Q,Z){let X=[],$=A(Q);if(X.push(...LQ(Q)),X.push(...PQ(Q)),Q.runnable)X.push(...CQ(Q));return X.push(...TZ(Q,$)),X.push(...DQ(Q,Z)),X.join(`
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
- ${e(`Installed "${H.name}" v${H.version}`)}`);for(let U of rQ(B.agents))console.log(g(` ${U.label} \u2192 ${U.outputDir}`))}catch(B){if(B instanceof N)if(q?!0:await cZ({message:`"${B.details.outputDir}" already exists but was not created by Crust. Overwrite?`,default:!1})){let V=await s({message:"Overwriting skill...",task:async()=>p({command:Q,meta:H,agents:[B.details.agent],scope:J,force:!0,installMode:Z.installMode})});console.log(`
8
- ${e(`Installed "${H.name}" v${H.version}`)}`);for(let k of rQ(V.agents))console.log(g(` ${k.label} \u2192 ${k.outputDir}`))}else console.log(g(`
9
- Skipped ${D[B.details.agent]}`));else throw B}if(f.length>0){let U=(await s({message:"Removing skills...",task:async()=>KQ({name:H.name,agents:f,scope:J})})).agents.filter((k)=>k.status==="removed").map((k)=>k.agent),V=wQ(U);if(V.length>0)console.log(`
10
- ${e(`Removed from ${V.join(", ")}`)}`)}if(w.length===0&&f.length===0)console.log(g("No changes."))}).command($)._node}function aZ(Q,Z){return new lQ("update").meta({description:"Update installed skills to latest version"}).flags({scope:{type:"string",description:"Update scope (project or global)"}}).run(async(X)=>{let $=await oQ(X.flags.scope,Z),W=c($),H=[...S(),...h()],q=UQ(Q,Z),J=(await l({name:q.name,agents:H,scope:$})).agents.filter((K)=>VQ(K.agent,$,q,K));if(J.length===0){console.log(g(`No updates needed (${W}).`));return}try{let z=(await s({message:`Updating ${W} skills...`,task:async()=>p({command:Q,meta:q,agents:J.map((F)=>F.agent),scope:$,installMode:Z.installMode})})).agents.filter((F)=>F.status==="updated").map((F)=>F.agent),G=wQ(z);if(G.length>0)console.log(`
11
- ${e(`Updated "${q.name}" to v${q.version} for ${G.join(", ")} (${W})`)}`)}catch(K){if(K instanceof N)console.warn(sQ(`Skipped ${D[K.details.agent]}: "${K.details.outputDir}" already exists but was not created by ${q.name}. Delete or rename the conflicting directory to resolve.`));else throw K}})}export{KQ as uninstallSkill,l as skillStatus,iZ as skillPlugin,n as resolveSkillName,C as resolveCanonicalSkillPath,mQ as isValidSkillName,XZ as isUniversalAgent,S as getUniversalAgents,h as getAdditionalAgents,p as generateSkill,XQ as detectInstalledAgents,YZ as annotate,N as SkillConflictError};
2
+ import{accessSync as TX,constants as _X,statSync as OX}from"fs";import{homedir as JQ}from"os";import{delimiter as jX,join as J}from"path";var h=J(".agents","skills"),bX=J(".crust","skills");function PQ(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 EX(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:h,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:h,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:h,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(PQ(Q),"crush","skills"),detectCommands:["crush"]},cursor:{label:"Cursor",class:"universal",projectSkillsDir:h,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:h,globalSkillsDir:f},"github-copilot":{label:"GitHub Copilot",class:"universal",projectSkillsDir:h,globalSkillsDir:f},goose:{label:"Goose",class:"additional",projectSkillsDir:J(".goose","skills"),globalSkillsDir:(Q)=>J(PQ(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:h,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:h,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:h,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 RX(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(IX(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 d(Q,X){if(u(Q)==="project")return J(process.cwd(),bX,X);return J(EX(JQ()),X)}function IX(Q){let Z=(process.env.PATH??"").split(jX).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&&kQ(J(W,Q)))return!0;if(H){for(let Y of $)if(kQ(J(W,Q+Y)))return!0}}return!1}function kQ(Q){try{if(!OX(Q).isFile())return!1;return TX(Q,_X.X_OK),!0}catch{return!1}}import{Crust as CX}from"@crustjs/core";function zQ(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 DQ(Q){let X=Q?.trim();if(!X)return[];return X.split(/\r?\n/)}function VQ(Q){return Q.length>0}var AQ=Symbol("crust.skill.commandAnnotations");function LX(Q){return Q instanceof CX?Q._node:Q}function NX(Q,X){let Z=LX(Q),H=zQ(typeof X==="string"||Array.isArray(X)?X:X.instructions??[]);if(H.length===0)return Q;let $=UQ(Z)?.instructions??[],W=[...new Set([...$,...H])];return Object.defineProperty(Z,AQ,{value:{instructions:W},enumerable:!0,configurable:!0}),Q}function UQ(Q){let X=Q[AQ];if(!X?.instructions||X.instructions.length===0)return;return{instructions:[...X.instructions]}}import{existsSync as $X,statSync as KZ}from"fs";import{readdir as GZ,readFile as WX,realpath as JX,stat as zX}from"fs/promises";import{dirname as YX,isAbsolute as FZ,join as FQ,resolve as GQ,sep as qX}from"path";import{fileURLToPath as xZ}from"url";import{lstat as eX,mkdir as eQ,readlink as QZ,realpath as XZ,rm as qQ,symlink as ZZ,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(PX(Q));this.details=Q}}function PX(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 SQ(Q){return vQ(Q,[])}function vQ(Q,X){let Z=kX(Q.meta.name),H=[...X,Z],$=DX(Q.args),W=SX(Q.effectiveFlags),Y=yX(Q.subCommands,H),G=UQ(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 kX(Q){return Q.trim().toLowerCase()}function DX(Q){if(!Q||Q.length===0)return[];return Q.map(AX)}function AX(Q){let X={name:Q.name,type: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=yQ(Q.default);return X}function SX(Q){if(!Q)return[];return Object.keys(Q).sort().map((Z)=>{return vX(Z,Q[Z])})}function vX(Q,X){let Z={name:Q,type: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=yQ(X.default);return Z}function yX(Q,X){return Object.keys(Q).sort().map((H)=>{return vQ(Q[H],X)})}function yQ(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 hQ(Q){return Q.replace(/(?<!\\)\|/g,"\\|")}function fQ(Q,X){let Z=[],H=gQ(Q);Z.push({path:"SKILL.md",content:hX(Q,X,H)});for(let $ of H){let W=i($),Y=$.children.length>0?uX($,Q):pX($,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 MQ(Q){return Q.path.join(" ")}function wQ(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 hX(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 $=oX(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(""),VQ($))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(...fX(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 fX(Q){let X=[];X.push("| Command | Type | Documentation |"),X.push("| ------- | ---- | ------------- |");for(let Z of Q){let H=MQ(Z),$=i(Z),W=gX(Z);X.push(`| \`${H}\` | ${W} | [${$}](${$}) |`)}return X}function gX(Q){if(Q.runnable&&Q.children.length>0)return"runnable, group";if(Q.runnable)return"runnable";return"group"}function pX(Q,X){let Z=[];return Z.push(...pQ(Q)),Z.push(...uQ(Q)),Z.push(...mQ(Q)),Z.push(...cQ(Q,X)),Z.join(`
4
+ `)}function uX(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(...dX(Q,H)),Z.push(...cQ(Q,X)),Z.join(`
5
+ `)}function mX(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=[`# \`${MQ(Q)}\``,""];if(Q.description)X.push(Q.description,"");return X}function uQ(Q){let X=Q.instructions??[];if(!VQ(X))return[];return["## Command Instructions","",...dQ(X),""]}function mQ(Q){let X=["## Usage","","```",Q.usage??mX(Q),"```",""];if(Q.args.length>0)X.push("## Arguments","",...cX(Q.args),"");if(Q.flags.length>0)X.push("## Flags","",...lX(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 dX(Q,X){let Z=["## Subcommands",""];for(let H of Q.children){let $=i(H),W=wQ(X,$),Y=H.description?` - ${H.description}`:"";Z.push(`- [\`${H.name}\`](${W})${Y}`)}return Z.push(""),Z}function cX(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=hQ(rX(Z));X.push(`| \`${H}\` | ${Z.type} | ${$} | ${W} |`)}return X}function rX(Q){let X=[];if(Q.description)X.push(Q.description);if(Q.default!==void 0)X.push(`Default: \`${Q.default}\``);return X.join(". ")||"-"}function lX(Q){let X=[];X.push("| Flag | Type | Required | Description |"),X.push("| ---- | ---- | -------- | ----------- |");for(let Z of Q){let H=sX(Z),$=Z.required?"Yes":"No",W=hQ(iX(Z));X.push(`| ${H} | ${Z.type} | ${$} | ${W} |`)}return X}function sX(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 iX(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 dQ(Q){return Q.map((X)=>`- ${X}`)}function oX(Q){if(typeof Q==="string")return DQ(Q);return dQ(zQ(Q))}function cQ(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=wQ(H,G),q=MQ(Y);Z.push(`Parent: [\`${q}\`](${x})`),Z.push("")}}let $=wQ(H,"SKILL.md");return Z.push(`[Skill Overview](${$})`),Z.push(""),Z}function rQ(Q,X){if(aX(Q.path,X))return Q;for(let Z of Q.children){let H=rQ(Z,X);if(H)return H}return}function aX(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 nX}from"fs/promises";import{join as tX}from"path";var t="crust.json";async function BQ(Q){let X;try{X=await nX(tX(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 TQ(Q){let X=await BQ(Q);return X.status==="ok"?X.manifest:null}async function YQ(Q){return(await TQ(Q))?.version??null}var HZ="auto";async function $Z(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 jQ(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=HZ}=Q,G=await $Z(Q.agents),x=QQ(Z.name),q=jQ(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=SQ(X),V=fQ(K,F);return bQ({files:V,meta:F,agents:G,scope:H,clean:$,force:W,installMode:Y,kind:"generated",legacyResolvedName:q})}async function bQ(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=BZ(Z,x),V=[...X,...K].sort((z,U)=>z.path<U.path?-1:z.path>U.path?1:0),w=V.map((z)=>z.path),M=new Map;for(let z of H){let U=N(z,$,Z.name),_=M.get(U);if(_)_.push(z);else M.set(U,[z])}let O=d($,Z.name),T=d($,q),b=new Map;for(let[z,U]of M){let _=U[0];if(!_)continue;b.set(z,await RQ({outputDir:z,legacyOutputDir:N(_,$,q),canonicalOutputDir:O,legacyCanonicalOutputDir:T}))}let v=await BQ(O),E=v.status==="ok"?v.manifest:null,P=E?.version??null;if((await EQ(O,O)).exists&&E===null&&!Y)throw new L({agent:F,outputDir:O,manifestMalformed:iQ(v)});if(E!==null&&E.kind!==x&&!Y)throw new L({agent:F,outputDir:O,kindMismatch:{existing:E.kind,attempted:x}});let D=E!==null&&E.kind!==x,g=P!==Z.version||D;if(g){if(W)await IQ(O);await HX(O,V)}let p=[];for(let[z,U]of M){let _=U[0];if(!_)continue;let B=b.get(z);if(!B)continue;if(B.current.inspection.exists&&!B.current.isCrustManaged&&!Y){let $Q=await BQ(z);throw new L({agent:_,outputDir:z,manifestMalformed:iQ($Q)})}if(B.current.manifest!==null&&B.current.manifest.kind!==x&&!Y)throw new L({agent:_,outputDir:z,kindMismatch:{existing:B.current.manifest.kind,attempted:x}});let j=await YZ({outputDir:z,canonicalOutputDir:O,allFiles:V,clean:W,installMode:G,inspection:B.current.inspection,installedVersion:B.preferredVersion,currentVersion:Z.version,installedKind:B.current.manifest?.kind??null,currentKind:x}),R=await JZ(B),C=WZ({installedVersion:B.preferredVersion,currentVersion:Z.version,canonicalChanged:g,pathChanged:j||R||B.preferredOutputDir!==z});for(let $Q of U)p.push({agent:$Q,outputDir:z,files:C==="up-to-date"?[]:w,status:C,previousVersion:C==="updated"?B.preferredVersion??void 0:void 0})}if(q!==Z.name){let z=await YQ(T);if(T!==O&&z!==null&&!await OQ(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=jQ(X),Y=d(Z,$),G=d(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 RQ({outputDir:F,legacyOutputDir:w,canonicalOutputDir:Y,legacyCanonicalOutputDir:G}),O=await _Q(M.current),T=M.legacy.outputDir!==M.current.outputDir?await _Q(M.legacy):!1,b=O||T,v=O?F:T?w:F;for(let E of K)x.push({agent:E,outputDir:v,status:b?"removed":"not-found"})}if(await YQ(Y)!==null&&!await OQ($,Z))await qQ(Y,{recursive:!0,force:!0});{let F=await YQ(G);if(G!==Y&&F!==null&&!await OQ(W,Z))await qQ(G,{recursive:!0,force:!0})}return{agents:x}}async function c(Q){let{name:X,scope:Z="global"}=Q,H=XX(Q.agents),$=QQ(X),W=jQ(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=d(Z,$),w=d(Z,W),M=await RQ({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 WZ(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 YZ(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 qZ({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 EQ(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 IQ(X);return await HX(X,Z),!0}async function qZ(Q){let{outputDir:X,canonicalOutputDir:Z,inspection:H}=Q;if(H.exists&&H.isSymlink&&H.pointsToCanonical)return!1;if(H.exists)await IQ(X);return await zZ(Z,X),!0}async function EQ(Q,X){let Z;try{Z=await eX(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([TQ(Q),EQ(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 RQ(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 _Q(Q){if(!Q.isCrustManaged||!Q.inspection.exists)return!1;return await qQ(Q.outputDir,{recursive:!0,force:!0}),!0}async function JZ(Q){if(Q.legacy.outputDir===Q.current.outputDir)return!1;return _Q(Q.legacy)}async function nQ(Q){try{return await XZ(Q)}catch{return null}}async function tQ(Q){try{return await QZ(Q)}catch{return null}}async function zZ(Q,X){await eQ(QX(X),{recursive:!0});let Z=process.platform==="win32"?"junction":"dir";await ZZ(Q,X,Z)}async function OQ(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 BZ(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 IQ(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 CQ="SKILL.md";function VZ(Q){let X=GQ(Q);if($X(X)&&!KZ(X).isDirectory())X=YX(X);while(!0){if($X(FQ(X,"package.json")))return X;let Z=YX(X);if(Z===X)return null;X=Z}}function UZ(Q){if(Q instanceof URL){if(Q.protocol!=="file:")throw Error(`Bundle URL must use file: protocol, got "${Q.protocol}".`);return xZ(Q)}if(FZ(Q))return GQ(Q);let X=process.argv[1];if(!X)throw Error(`Could not resolve relative bundle path "${Q}" because process.argv[1] is not set. Pass an absolute path or a file: URL sourceDir.`);let Z=VZ(GQ(X));if(!Z)throw Error(`Could not resolve relative bundle path "${Q}" from entrypoint "${X}" because no package.json was found in its parent directories. Pass an absolute path or a file: URL sourceDir.`);return GQ(Z,Q)}function wZ(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]=MZ(x[2]??"")}return X}function MZ(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 TZ(Q,X,Z){let H=X.endsWith(qX)?X:X+qX;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 BX(Q,X,Z,H){let $=await GZ(Q,{withFileTypes:!0}),W=[];for(let Y of $){let G=Y.name,x=FQ(Q,G),q=Z===""?G:`${Z}/${G}`,F=await JX(x);TZ(F,X,x);let K=await zX(F);if(K.isDirectory()){if(H.has(F))continue;H.add(F),W.push(...await BX(x,X,q,H))}else if(K.isFile())W.push({relPath:q,absPath:x})}return W}async function _Z(Q){let X=UZ(Q),Z;try{Z=await JX(X)}catch(F){throw Error(`Bundle source directory "${X}" does not exist or is not accessible.`,{cause:F})}if(!(await zX(Z)).isDirectory())throw Error(`Bundle source path "${Z}" is not a directory.`);let W=await BX(Z,Z,"",new Set([Z])),Y=W.find((F)=>F.relPath===CQ);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 WX(F.absPath)}))),x=await WX(Y.absPath,"utf-8"),q=wZ(x);if(q.name===null||q.name==="")throw Error(`Bundle SKILL.md is missing a top-level \`name:\` field in its YAML frontmatter (at "${FQ(Z,CQ)}"). 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 "${FQ(Z,CQ)}"). 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 _Z(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 bQ({files:[...q],meta:V,agents:Z,scope:$,clean:W,force:Y,installMode:G,kind:"bundle",legacyResolvedName:K})}import{Crust as GX,VALIDATION_MODE_ENV as OZ}from"@crustjs/core";import{spinner as y}from"@crustjs/progress";import{confirm as FX,multiselect as xX,select as jZ}from"@crustjs/prompts";import{bold as l,dim as k,yellow as o}from"@crustjs/style";var bZ="skill",VX="global",r="__universal__";function UX(Q){return Q==="global"||Q==="project"}function EZ(Q){return Q==="auto"||Q==="symlink"||Q==="copy"}function wX(Q){if(Q===void 0)return;if(!UX(Q))throw Error(`Invalid --scope value: ${String(Q)}. Expected "project" or "global".`);return Q}async function MX(Q,X){let Z=wX(Q);if(Z!==void 0)return Z;if(X.defaultScope)return X.defaultScope;return jZ({message:"Select scope",choices:[{label:"Project",value:"project"},{label:"Global",value:"global"}],default:VX})}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 xQ(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 LQ(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 RZ(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&&!UX($.scope))throw Error(`skillPlugin: customSkills[${H}].scope (for "${$.name}") must be "project" or "global", got ${JSON.stringify($.scope)}.`);if($.installMode!==void 0&&!EZ($.installMode))throw Error(`skillPlugin: customSkills[${H}].installMode (for "${$.name}") must be "auto", "symlink", or "copy", got ${JSON.stringify($.installMode)}.`)}return X}function IZ(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 CZ(Q,X){let Z=[...S(),...m()];if(Z.length===0)return;let H=IZ(Q,X),$=Q.installMode??X.installMode,W=Q.version??X.version;for(let Y of H){let x=(await c({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 y({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 NQ(Q,X,Z,H){if(!H.installed)return!1;let $=N(Q,X,Z.name);return H.version!==Z.version||H.outputDir!==$}async function LZ(Q,X,Z){let H=[...S(),...m()];if(H.length===0){await KX(Z,X);return}let $=LQ(Q,X),W=[...new Set(["project","global"].map((Y)=>u(Y)))];for(let Y of W){let x=(await c({name:$.name,agents:H,scope:Y})).agents.filter((q)=>NQ(q.agent,Y,$,q));if(x.length===0)continue;try{await y({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 KX(Z,X)}async function KX(Q,X){for(let Z of Q)try{await CZ(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 NZ(Q){let X;return{name:"skills",async setup(Z,H){X=Z.rootCommand;let $=Q.command??bZ,W=RZ(X.meta.name,Q.customSkills);if(H.addSubCommand(X,$,kZ(X,Q,W,$)),process.env[OZ]==="1")return;if(Z.argv[0]===$)return;if(Q.autoUpdate!==!1)await LZ(X,Q,W)}}}async function PZ(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 c({name:X.name,agents:[...q,...F],scope:H}),V=new Set(K.agents.filter((z)=>z.installed).map((z)=>z.agent)),w=new Set(x),M=new Map(K.agents.map((z)=>[z.agent,z])),O=F.filter((z)=>{if(w.has(z))return!0;return M.get(z)?.installed===!0}),T=O.filter((z)=>V.has(z)),b=[];if(q.length>0){let z=q[0];if(!z)throw Error("Expected at least one universal agent");let _=M.get(z)?.outputDir??"path unavailable";b.push({label:"Universal",value:r,hint:_});let B=q.map((j)=>A[j]).join(", ");if(W&&!$)console.log(k(`Agents supporting universal skills: ${B}`))}for(let z of O){let _=M.get(z)?.outputDir??"path unavailable";b.push({label:A[z],value:z,hint:_})}let v=q.length>0&&q.every((z)=>V.has(z)),E=[...T.filter((z)=>!q.includes(z))];if(v)E.unshift(r);let P;if($)P=[...q,...O];else{let z=b.length===0?[]:await xX({message:`Select agents to install skills for [${X.name}]`,choices:b,default:E,required:!1}),U=new Set(z.filter((_)=>_!==r));if(z.includes(r))for(let _ of q)U.add(_);P=[...U]}let I=P.filter((z)=>!V.has(z)),D=P.filter((z)=>{let U=M.get(z);if(!U?.installed)return!1;let _=N(z,H,X.name);return U.version!==G||U.outputDir!==_}),g=[...V].filter((z)=>!P.includes(z)),p=[...I,...D];if(p.length>0)try{let z=await y({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 xQ(z.agents))console.log(k(` ${U.label} \u2192 ${U.outputDir}`))}catch(z){if(z instanceof L){let U=z.details.kindMismatch?` (existing is a "${z.details.kindMismatch.existing}" skill, attempted "${z.details.kindMismatch.attempted}")`:" but was not created by Crust";if($?!0:await FX({message:`"${z.details.outputDir}" already exists${U}. Overwrite?`,default:!1})){let B=await y({message:`Overwriting bundle [${X.name}]...`,task:async()=>ZQ({sourceDir:X.sourceDir,agents:[z.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 xQ(B.agents))console.log(k(` ${j.label} \u2192 ${j.outputDir}`))}else console.log(k(`
9
+ Skipped ${A[z.details.agent]} [${X.name}]`))}else throw z}if(g.length>0){let U=(await y({message:`Removing skills [${X.name}]...`,task:async()=>KQ({name:X.name,agents:g,scope:H})})).agents.filter((B)=>B.status==="removed").map((B)=>B.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 kZ(Q,X,Z,H){let $=DZ(Q,X,Z);return new GX(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=LQ(Q,X),G=W.flags.all===!0,x=!!process.stdin.isTTY,q=G?wX(W.flags.scope)??X.defaultScope??VX:await MX(W.flags.scope,X),F=await n(),K=S(),V=m(),w=await c({name:Y.name,agents:[...K,...V],scope:q}),M=new Set(w.agents.filter((B)=>B.installed).map((B)=>B.agent)),O=new Set(F),T=new Map(w.agents.map((B)=>[B.agent,B])),b=V.filter((B)=>{if(O.has(B))return!0;return T.get(B)?.installed===!0}),v=b.filter((B)=>M.has(B)),E=[];if(K.length>0){let B=K[0];if(!B)throw Error("Expected at least one universal agent");let R=T.get(B)?.outputDir??"path unavailable";E.push({label:"Universal",value:r,hint:R});let C=K.map(($Q)=>A[$Q]).join(", ");if(x&&!G)console.log(k(`Agents supporting universal skills: ${C}`))}for(let B of b){let R=T.get(B)?.outputDir??"path unavailable";E.push({label:A[B],value:B,hint:R})}let P=K.length>0&&K.every((B)=>M.has(B)),I=[...v.filter((B)=>!K.includes(B))];if(P)I.unshift(r);let D;if(G)D=[...K,...b];else{let B=E.length===0?[]:await xX({message:"Select agents to install skills for",choices:E,default:I,required:!1}),j=new Set(B.filter((R)=>R!==r));if(B.includes(r))for(let R of K)j.add(R);D=[...j]}let g=D.filter((B)=>!M.has(B)),p=D.filter((B)=>{let j=T.get(B);return j!==void 0&&NQ(B,q,Y,j)}),z=[...M].filter((B)=>!D.includes(B)),U=[...g,...p];if(U.length>0)try{let B=await y({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 xQ(B.agents))console.log(k(` ${j.label} \u2192 ${j.outputDir}`))}catch(B){if(B instanceof L)if(G?!0:await FX({message:`"${B.details.outputDir}" already exists but was not created by Crust. Overwrite?`,default:!1})){let R=await y({message:"Overwriting skill...",task:async()=>XQ({command:Q,meta:Y,agents:[B.details.agent],scope:q,force:!0,installMode:X.installMode})});console.log(`
12
+ ${l(`Installed "${Y.name}" v${Y.version}`)}`);for(let C of xQ(R.agents))console.log(k(` ${C.label} \u2192 ${C.outputDir}`))}else console.log(k(`
13
+ Skipped ${A[B.details.agent]}`));else throw B}if(z.length>0){let j=(await y({message:"Removing skills...",task:async()=>KQ({name:Y.name,agents:z,scope:q})})).agents.filter((C)=>C.status==="removed").map((C)=>C.agent),R=HQ(j);if(R.length>0)console.log(`
14
+ ${l(`Removed from ${R.join(", ")}`)}`)}if(U.length===0&&z.length===0)console.log(k("No changes."));let _=[];for(let B of Z){let j=B.scope??q;try{await PZ({entry:B,options:X,scope:j,installAll:G,isInteractive:x})}catch(R){let C=R instanceof Error?R.message:String(R);console.warn(o(`Skill reconciliation failed [${B.name}]: ${C}. Continuing with remaining skills.`)),_.push(B.name)}}if(_.length>0)process.exitCode=1}).command($)._node}function DZ(Q,X,Z){return new GX("update").meta({description:"Update installed skills to latest version"}).flags({scope:{type:"string",description:"Update scope (project or global)"}}).run(async(H)=>{let $=await MX(H.flags.scope,X),W=u($),Y=[...S(),...m()],G=LQ(Q,X),q=(await c({name:G.name,agents:Y,scope:$})).agents.filter((K)=>NQ(K.agent,$,G,K));if(q.length===0)console.log(k(`No updates needed (${W}).`));else try{let V=(await y({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 c({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 E=(await y({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(E);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,c as skillStatus,NZ as skillPlugin,QQ as resolveSkillName,d as resolveCanonicalSkillPath,e as isValidSkillName,RX as isUniversalAgent,ZQ as installSkillBundle,S as getUniversalAgents,m as getAdditionalAgents,XQ as generateSkill,n as detectInstalledAgents,NX as annotate,L as SkillConflictError};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crustjs/skills",
3
- "version": "0.0.24",
3
+ "version": "0.1.0",
4
4
  "description": "Agent skill generation from Crust command definitions",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,17 +43,17 @@
43
43
  "publish": "bun publish --no-git-checks || true"
44
44
  },
45
45
  "dependencies": {
46
- "@crustjs/progress": "0.0.3",
47
- "@crustjs/prompts": "0.0.13",
48
- "@crustjs/style": "0.1.0"
46
+ "@crustjs/progress": "0.0.4",
47
+ "@crustjs/prompts": "0.1.0",
48
+ "@crustjs/style": "0.2.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@crustjs/config": "0.0.0",
52
- "@crustjs/core": "0.0.16",
52
+ "@crustjs/core": "0.0.17",
53
53
  "bunup": "^0.16.31"
54
54
  },
55
55
  "peerDependencies": {
56
- "@crustjs/core": "0.0.16",
56
+ "@crustjs/core": "0.0.17",
57
57
  "typescript": "^6.0.3"
58
58
  }
59
59
  }