@crustjs/skills 0.0.2 → 0.0.3

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
@@ -149,10 +149,10 @@ for (const file of files) {
149
149
 
150
150
  ## Output Structure
151
151
 
152
- Generated output goes to `<outDir>/skills/<name>/`:
152
+ Generated output goes to `<outDir>/skills/use-<name>/` (the `use-` prefix is applied automatically):
153
153
 
154
154
  ```
155
- skills/my-cli/
155
+ skills/use-my-cli/
156
156
  SKILL.md # Entrypoint — loaded by the agent
157
157
  command-index.md # Maps all commands to documentation file paths
158
158
  commands/ # Per-command documentation mirroring the CLI hierarchy
@@ -162,7 +162,6 @@ skills/my-cli/
162
162
  migrate.md # Nested subcommand
163
163
  seed.md
164
164
  manifest.json # Machine-readable bundle metadata
165
- README.md # Install instructions for consumers
166
165
  ```
167
166
 
168
167
  ### File Details
@@ -173,7 +172,6 @@ skills/my-cli/
173
172
  | `command-index.md` | Markdown table listing every command, its type (runnable/group), and documentation path. |
174
173
  | `commands/*.md` | Per-command reference files. Leaf commands include usage, arguments, flags, defaults, and aliases. Group commands list subcommands with links. |
175
174
  | `manifest.json` | JSON metadata: name, description, version, entrypoint, and list of all command paths. |
176
- | `README.md` | Human-readable install instructions for OpenCode and Claude Code. |
177
175
 
178
176
  ## Installing Generated Skills
179
177
 
@@ -182,13 +180,13 @@ After generating a skill bundle, consumers can install it by copying the skill d
182
180
  ### OpenCode
183
181
 
184
182
  ```sh
185
- cp -r skills/my-cli/ .opencode/skills/my-cli/
183
+ cp -r skills/use-my-cli/ .opencode/skills/use-my-cli/
186
184
  ```
187
185
 
188
186
  ### Claude Code
189
187
 
190
188
  ```sh
191
- cp -r skills/my-cli/ .claude/skills/my-cli/
189
+ cp -r skills/use-my-cli/ .claude/skills/use-my-cli/
192
190
  ```
193
191
 
194
192
  The agent will discover the skill from `SKILL.md` and load command documentation on demand from the `commands/` directory.
package/dist/index.d.ts CHANGED
@@ -12,10 +12,18 @@ import { AnyCommand } from "@crustjs/core";
12
12
  * description: "CLI tool for managing widgets",
13
13
  * version: "1.0.0",
14
14
  * };
15
+ * // generateSkill() will output to `use-my-cli/` with name "use-my-cli"
15
16
  * ```
16
17
  */
17
18
  interface SkillMeta {
18
- /** Skill name — used as the directory name and in frontmatter */
19
+ /**
20
+ * Skill name — the user-facing CLI name (e.g. `"my-cli"`).
21
+ *
22
+ * `generateSkill()`, `uninstallSkill()`, and `skillStatus()` automatically
23
+ * prefix `use-` to this name for output directory paths, SKILL.md frontmatter,
24
+ * and manifest.json metadata. For example, `name: "my-cli"` produces output
25
+ * under `use-my-cli/`.
26
+ */
19
27
  name: string;
20
28
  /** Human-readable description of what the CLI does */
21
29
  description: string;
@@ -29,6 +37,10 @@ type Scope = "global" | "project";
29
37
  /**
30
38
  * Top-level options for generating a skill bundle from a command tree.
31
39
  *
40
+ * The `meta.name` value is automatically prefixed with `use-` for all output
41
+ * paths and metadata. For example, `name: "my-cli"` produces skill directories
42
+ * named `use-my-cli/` and sets the manifest/frontmatter name to `"use-my-cli"`.
43
+ *
32
44
  * @example
33
45
  * ```ts
34
46
  * import { generateSkill } from "@crustjs/skills";
@@ -37,7 +49,7 @@ type Scope = "global" | "project";
37
49
  * await generateSkill({
38
50
  * command: rootCommand,
39
51
  * meta: {
40
- * name: "my-cli",
52
+ * name: "my-cli", // output: use-my-cli/
41
53
  * description: "CLI tool for managing widgets",
42
54
  * version: "1.0.0",
43
55
  * },
@@ -146,7 +158,7 @@ interface StatusResult {
146
158
  * are not yet present.
147
159
  *
148
160
  * **Interactive command**: set `command: true` to register a `skill` subcommand
149
- * on the root command for manual install/uninstall/status management.
161
+ * that presents a single multiselect prompt for toggling agent installations.
150
162
  */
151
163
  interface SkillPluginOptions {
152
164
  /** Skill version string — compared against the installed manifest */
@@ -170,6 +182,13 @@ interface SkillPluginOptions {
170
182
  autoUpdate?: boolean;
171
183
  /**
172
184
  * Register an interactive skill management subcommand on the root command.
185
+ *
186
+ * The command presents a single multiselect prompt listing all detected
187
+ * agents with their current installation status pre-filled. The user
188
+ * toggles agents on/off and the system reconciles the desired state:
189
+ * newly selected agents are installed, deselected agents are uninstalled,
190
+ * and already-correct agents are skipped.
191
+ *
173
192
  * - `true`: register with default name `"skill"`
174
193
  * - `string`: register with a custom command name
175
194
  * @default false
@@ -202,6 +221,22 @@ interface SkillPluginOptions {
202
221
  */
203
222
  declare function detectInstalledAgents(home?: string): Promise<AgentTarget[]>;
204
223
  /**
224
+ * Resolves the canonical skill name by applying the `use-` prefix.
225
+ *
226
+ * All generated output (directory names, manifest metadata, SKILL.md content)
227
+ * uses the resolved name. Consumers pass the raw CLI name (e.g. `"my-cli"`),
228
+ * and this function returns the prefixed form (e.g. `"use-my-cli"`).
229
+ *
230
+ * @param name - The raw CLI tool name
231
+ * @returns The prefixed skill name
232
+ *
233
+ * @example
234
+ * ```ts
235
+ * resolveSkillName("my-cli"); // "use-my-cli"
236
+ * ```
237
+ */
238
+ declare function resolveSkillName(name: string): string;
239
+ /**
205
240
  * Generates and installs agent skill bundles from a Crust command tree.
206
241
  *
207
242
  * For each target agent:
@@ -265,9 +300,10 @@ import { CrustPlugin } from "@crustjs/core";
265
300
  * are not yet present.
266
301
  *
267
302
  * **Interactive command**: set `command: true` to register a `skill` subcommand
268
- * on the root command for manual install/uninstall/status management. The
269
- * subcommand is injected via `addSubCommand` during `setup()` — if the user
270
- * already defines a subcommand with the same name, theirs takes priority.
303
+ * that presents a single multiselect prompt for toggling agent installations.
304
+ * Detected agents are shown with their current installation status pre-filled.
305
+ * The system reconciles the desired state: newly selected agents are installed,
306
+ * deselected agents are uninstalled, and already-correct agents are skipped.
271
307
  *
272
308
  * @param options - Plugin configuration with version and scope
273
309
  * @returns A `CrustPlugin` to register in a command's `plugins` array
@@ -291,4 +327,4 @@ import { CrustPlugin } from "@crustjs/core";
291
327
  * ```
292
328
  */
293
329
  declare function skillPlugin(options: SkillPluginOptions): CrustPlugin;
294
- export { uninstallSkill, skillStatus, skillPlugin, generateSkill, detectInstalledAgents, UninstallStatus, UninstallResult, UninstallOptions, StatusResult, StatusOptions, SkillPluginOptions, SkillMeta, Scope, InstallStatus, GenerateResult, GenerateOptions, AgentTarget, AgentResult };
330
+ export { uninstallSkill, skillStatus, skillPlugin, resolveSkillName, generateSkill, detectInstalledAgents, UninstallStatus, UninstallResult, UninstallOptions, StatusResult, StatusOptions, SkillPluginOptions, SkillMeta, Scope, InstallStatus, GenerateResult, GenerateOptions, AgentTarget, AgentResult };
package/dist/index.js CHANGED
@@ -474,16 +474,23 @@ async function checkVersion(dir, newVersion) {
474
474
  }
475
475
 
476
476
  // src/generate.ts
477
+ function resolveSkillName(name) {
478
+ return name.startsWith("use-") ? name : `use-${name}`;
479
+ }
477
480
  async function generateSkill(options) {
478
481
  const { command, meta, agents, scope = "global", clean = true } = options;
482
+ const resolvedMeta = {
483
+ ...meta,
484
+ name: resolveSkillName(meta.name)
485
+ };
479
486
  const manifest = buildManifest(command);
480
- const renderedFiles = renderSkill(manifest, meta);
481
- const metadataFiles = renderDistributionMetadata(manifest, meta);
487
+ const renderedFiles = renderSkill(manifest, resolvedMeta);
488
+ const metadataFiles = renderDistributionMetadata(manifest, resolvedMeta);
482
489
  const allFiles = [...renderedFiles, ...metadataFiles].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
483
490
  const results = [];
484
491
  for (const agent of agents) {
485
- const outputDir = resolveAgentPath(agent, scope, meta.name);
486
- const { status, installedVersion } = await checkVersion(outputDir, meta.version);
492
+ const outputDir = resolveAgentPath(agent, scope, resolvedMeta.name);
493
+ const { status, installedVersion } = await checkVersion(outputDir, resolvedMeta.version);
487
494
  if (status === "up-to-date") {
488
495
  results.push({
489
496
  agent,
@@ -510,9 +517,10 @@ async function generateSkill(options) {
510
517
  }
511
518
  async function uninstallSkill(options) {
512
519
  const { name, agents, scope = "global" } = options;
520
+ const resolvedName = resolveSkillName(name);
513
521
  const results = [];
514
522
  for (const agent of agents) {
515
- const outputDir = resolveAgentPath(agent, scope, name);
523
+ const outputDir = resolveAgentPath(agent, scope, resolvedName);
516
524
  const exists = await access2(outputDir).then(() => true).catch(() => false);
517
525
  if (exists) {
518
526
  await rm(outputDir, { recursive: true, force: true });
@@ -525,9 +533,10 @@ async function uninstallSkill(options) {
525
533
  }
526
534
  async function skillStatus(options) {
527
535
  const { name, agents, scope = "global" } = options;
536
+ const resolvedName = resolveSkillName(name);
528
537
  const results = [];
529
538
  for (const agent of agents) {
530
- const outputDir = resolveAgentPath(agent, scope, name);
539
+ const outputDir = resolveAgentPath(agent, scope, resolvedName);
531
540
  const version = await readInstalledVersion(outputDir);
532
541
  results.push({
533
542
  agent,
@@ -586,7 +595,7 @@ async function writeFiles(baseDir, files) {
586
595
  }
587
596
  // src/plugin.ts
588
597
  import { defineCommand } from "@crustjs/core";
589
- import { confirm, multiselect, select, spinner } from "@crustjs/prompts";
598
+ import { multiselect, spinner } from "@crustjs/prompts";
590
599
  function deriveSkillMeta(command, version) {
591
600
  return {
592
601
  name: command.meta.name,
@@ -652,147 +661,88 @@ function buildSkillCommand(rootCmd, options) {
652
661
  return defineCommand({
653
662
  meta: {
654
663
  name: "skill",
655
- description: "Manage agent skills (install, uninstall, status)"
664
+ description: "Manage agent skill installations"
656
665
  },
657
- flags: {
658
- action: {
659
- type: "string",
660
- description: 'Action to perform: "install", "uninstall", or "status" (skips prompt)'
661
- },
662
- scope: {
663
- type: "string",
664
- description: 'Installation scope: "global" or "project" (skips prompt)'
665
- },
666
- force: {
667
- type: "boolean",
668
- description: "Skip confirmation prompts"
669
- }
670
- },
671
- async run({ flags }) {
666
+ async run() {
672
667
  const meta = deriveSkillMeta(rootCmd, options.version);
673
- const actionFlag = flags.action;
674
- const scopeFlag = flags.scope;
675
- const forceFlag = flags.force ?? false;
676
- const installedAgents = await detectInstalledAgents();
677
- if (installedAgents.length === 0) {
668
+ const scope = options.scope ?? "global";
669
+ const detectedAgents = await detectInstalledAgents();
670
+ if (detectedAgents.length === 0) {
678
671
  console.log("No supported agents detected. Install Claude Code or OpenCode first.");
679
672
  return;
680
673
  }
681
- const action = await select({
682
- message: "What would you like to do?",
683
- choices: [
684
- { label: "Install / update skills", value: "install" },
685
- { label: "Uninstall skills", value: "uninstall" },
686
- { label: "Check status", value: "status" }
687
- ],
688
- initial: actionFlag
674
+ const status = await skillStatus({
675
+ name: meta.name,
676
+ agents: detectedAgents,
677
+ scope
689
678
  });
690
- if (action === "install") {
691
- await handleInstall(rootCmd, meta, options, installedAgents, scopeFlag);
692
- } else if (action === "uninstall") {
693
- await handleUninstall(meta, installedAgents, scopeFlag, forceFlag);
694
- } else if (action === "status") {
695
- await handleStatus(meta, installedAgents, scopeFlag);
696
- }
697
- }
698
- });
699
- }
700
- async function handleInstall(rootCmd, meta, options, installedAgents, scopeFlag) {
701
- const agents = await multiselect({
702
- message: "Which agents?",
703
- choices: installedAgents.map((a) => ({
704
- label: AGENT_LABELS[a],
705
- value: a
706
- })),
707
- default: installedAgents,
708
- required: true
709
- });
710
- const scope = await select({
711
- message: "Scope?",
712
- choices: [
713
- { label: "Global", value: "global", hint: "recommended" },
714
- { label: "Project", value: "project" }
715
- ],
716
- default: options.scope ?? "global",
717
- initial: scopeFlag
718
- });
719
- const result = await spinner({
720
- message: "Installing skills...",
721
- task: async () => generateSkill({
722
- command: rootCmd,
723
- meta,
724
- agents,
725
- scope
726
- })
727
- });
728
- console.log(`
679
+ const installedAgents = [];
680
+ const choices = status.agents.map((entry) => {
681
+ const hint = entry.installed ? `v${entry.version} installed` : "not installed";
682
+ if (entry.installed) {
683
+ installedAgents.push(entry.agent);
684
+ }
685
+ return {
686
+ label: AGENT_LABELS[entry.agent],
687
+ value: entry.agent,
688
+ hint
689
+ };
690
+ });
691
+ const selected = await multiselect({
692
+ message: "Select agents to install skills for",
693
+ choices,
694
+ default: installedAgents,
695
+ required: false
696
+ });
697
+ const toInstall = selected.filter((agent) => !installedAgents.includes(agent));
698
+ const toUpdate = selected.filter((agent) => {
699
+ const entry = status.agents.find((a) => a.agent === agent);
700
+ return entry?.installed === true && entry.version !== meta.version;
701
+ });
702
+ const toUninstall = installedAgents.filter((agent) => !selected.includes(agent));
703
+ const agentsToGenerate = [...toInstall, ...toUpdate];
704
+ if (agentsToGenerate.length > 0) {
705
+ const result = await spinner({
706
+ message: "Installing skills...",
707
+ task: async () => generateSkill({
708
+ command: rootCmd,
709
+ meta,
710
+ agents: agentsToGenerate,
711
+ scope
712
+ })
713
+ });
714
+ console.log(`
729
715
  Installed "${meta.name}" v${meta.version}`);
730
- for (const r of result.agents) {
731
- if (r.status !== "up-to-date") {
732
- console.log(` ${AGENT_LABELS[r.agent]} \u2192 ${r.outputDir}`);
733
- } else {
734
- console.log(` ${AGENT_LABELS[r.agent]} \u2014 already up-to-date`);
735
- }
736
- }
737
- }
738
- async function handleUninstall(meta, installedAgents, scopeFlag, forceFlag) {
739
- const agents = await multiselect({
740
- message: "Which agents to uninstall from?",
741
- choices: installedAgents.map((a) => ({
742
- label: AGENT_LABELS[a],
743
- value: a
744
- })),
745
- default: installedAgents,
746
- required: true
747
- });
748
- const agentNames = agents.map((a) => AGENT_LABELS[a]).join(", ");
749
- const confirmed = await confirm({
750
- message: `Remove "${meta.name}" skills from ${agentNames}?`,
751
- initial: forceFlag ? true : undefined
752
- });
753
- if (!confirmed) {
754
- console.log("Cancelled.");
755
- return;
756
- }
757
- const scope = scopeFlag ?? "global";
758
- const result = await spinner({
759
- message: "Removing skills...",
760
- task: async () => uninstallSkill({
761
- name: meta.name,
762
- agents,
763
- scope
764
- })
765
- });
766
- const removed = result.agents.filter((a) => a.status === "removed").map((a) => AGENT_LABELS[a.agent]);
767
- if (removed.length > 0) {
768
- console.log(`
716
+ for (const r of result.agents) {
717
+ console.log(` ${AGENT_LABELS[r.agent]} \u2192 ${r.outputDir}`);
718
+ }
719
+ }
720
+ if (toUninstall.length > 0) {
721
+ const result = await spinner({
722
+ message: "Removing skills...",
723
+ task: async () => uninstallSkill({
724
+ name: meta.name,
725
+ agents: toUninstall,
726
+ scope
727
+ })
728
+ });
729
+ const removed = result.agents.filter((a) => a.status === "removed").map((a) => AGENT_LABELS[a.agent]);
730
+ if (removed.length > 0) {
731
+ console.log(`
769
732
  Removed from ${removed.join(", ")}`);
770
- } else {
771
- console.log(`
772
- No installed skills found.`);
773
- }
774
- }
775
- async function handleStatus(meta, installedAgents, scopeFlag) {
776
- const scope = scopeFlag ?? "global";
777
- const result = await skillStatus({
778
- name: meta.name,
779
- agents: installedAgents,
780
- scope
733
+ }
734
+ }
735
+ if (agentsToGenerate.length === 0 && toUninstall.length === 0) {
736
+ console.log("No changes.");
737
+ }
738
+ }
781
739
  });
782
- console.log("");
783
- console.log(`${"Agent".padEnd(14)}${"Scope".padEnd(10)}${"Version".padEnd(10)}Path`);
784
- for (const entry of result.agents) {
785
- const agentName = AGENT_LABELS[entry.agent];
786
- const version = entry.installed ? entry.version ?? "\u2014" : "(not installed)";
787
- const path = entry.installed ? entry.outputDir : "";
788
- console.log(`${agentName.padEnd(14)}${scope.padEnd(10)}${version.padEnd(10)}${path}`);
789
- }
790
- console.log("");
791
740
  }
792
741
  export {
793
742
  uninstallSkill,
794
743
  skillStatus,
795
744
  skillPlugin,
745
+ resolveSkillName,
796
746
  generateSkill,
797
747
  detectInstalledAgents
798
748
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crustjs/skills",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Agent skill generation from Crust command definitions",
5
5
  "type": "module",
6
6
  "license": "MIT",