@robosoft/skillhub-cli 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1
4
+
5
+ - Added configurable install targets: `ai`, `cursor`, and `claude`.
6
+ - Added `skillhub target` command.
7
+ - Added `--target` / `--to` support for `init`, `install`, and `sync`.
8
+ - Cursor skills now install under `.cursor/skills` and update `.cursor/rules/skillhub.mdc`.
9
+ - Claude skills now install under `.claude/skills` and update `CLAUDE.md` plus `.claude/skillhub.md`.
10
+ - Added `cloude` alias for `claude`.
11
+
12
+ # Changelog
13
+
3
14
  ## 0.1.0
4
15
 
5
16
  Initial publishable release of SkillHub CLI.
package/README.md CHANGED
@@ -72,18 +72,46 @@ node ./bin/skillhub.mjs --help
72
72
 
73
73
  ## Use in Any Project
74
74
 
75
- Create SkillHub config inside your target project:
75
+ Create SkillHub config inside your target project. The CLI can now ask where to place skill files:
76
76
 
77
77
  ```bash
78
78
  skillhub init
79
79
  ```
80
80
 
81
+ Or pass the target directly:
82
+
83
+ ```bash
84
+ skillhub init --target cursor
85
+ skillhub init --target claude
86
+ skillhub init --target ai
87
+ ```
88
+
89
+ Targets:
90
+
91
+ ```txt
92
+ ai -> .ai/skills + AGENTS.md
93
+ cursor -> .cursor/skills + .cursor/rules/skillhub.mdc
94
+ claude -> .claude/skills + CLAUDE.md + .claude/skillhub.md
95
+ ```
96
+
81
97
  Install a skill from a local registry:
82
98
 
83
99
  ```bash
84
100
  skillhub install nextjs-clean-architecture --registry ./skillhub-registry
85
101
  ```
86
102
 
103
+ Install directly into Cursor skill location:
104
+
105
+ ```bash
106
+ skillhub install nextjs-clean-architecture --target cursor --registry ./skillhub-registry
107
+ ```
108
+
109
+ Install directly into Claude skill location:
110
+
111
+ ```bash
112
+ skillhub install nextjs-clean-architecture --target claude --registry ./skillhub-registry
113
+ ```
114
+
87
115
  Install a specific version:
88
116
 
89
117
  ```bash
@@ -96,6 +124,14 @@ List installed skills:
96
124
  skillhub list
97
125
  ```
98
126
 
127
+ Change target later:
128
+
129
+ ```bash
130
+ skillhub target cursor
131
+ skillhub target claude
132
+ skillhub target ai
133
+ ```
134
+
99
135
  Sync all skills from `skillhub.json`:
100
136
 
101
137
  ```bash
@@ -149,12 +185,32 @@ npm run demo:validate
149
185
 
150
186
  ## Project Output
151
187
 
152
- After installing skills, the target project gets files like:
188
+ After installing skills, the target project gets files based on the selected target.
189
+
190
+ For `--target cursor`:
191
+
192
+ ```txt
193
+ .cursor/skills/<skill-name>/
194
+ .cursor/rules/skillhub.mdc
195
+ skillhub.json
196
+ skillhub.lock.json
197
+ ```
198
+
199
+ For `--target claude`:
200
+
201
+ ```txt
202
+ .claude/skills/<skill-name>/
203
+ .claude/skillhub.md
204
+ CLAUDE.md
205
+ skillhub.json
206
+ skillhub.lock.json
207
+ ```
208
+
209
+ For `--target ai`:
153
210
 
154
211
  ```txt
155
212
  .ai/skills/<skill-name>/
156
213
  AGENTS.md
157
- .cursor/rules/skillhub.mdc
158
214
  skillhub.json
159
215
  skillhub.lock.json
160
216
  ```
@@ -162,8 +218,9 @@ skillhub.lock.json
162
218
  ## Supported Commands
163
219
 
164
220
  ```txt
165
- skillhub init
166
- skillhub install <skill[@version]>
221
+ skillhub init [--target ai|cursor|claude]
222
+ skillhub target [ai|cursor|claude]
223
+ skillhub install <skill[@version]> [--target ai|cursor|claude]
167
224
  skillhub sync
168
225
  skillhub list
169
226
  skillhub remove <skill>
package/bin/skillhub.mjs CHANGED
@@ -4,10 +4,29 @@ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import crypto from "node:crypto";
6
6
  import process from "node:process";
7
+ import readline from "node:readline/promises";
7
8
 
8
9
  const CONFIG_FILE = "skillhub.json";
9
10
  const LOCK_FILE = "skillhub.lock.json";
10
- const DEFAULT_SKILLS_DIR = ".ai/skills";
11
+ const DEFAULT_TARGET = "ai";
12
+ const TARGETS = {
13
+ ai: {
14
+ label: ".ai/skills",
15
+ skillsDir: ".ai/skills",
16
+ adapters: { agentsMd: true, cursorRules: false, claude: false, githubCopilot: false },
17
+ },
18
+ cursor: {
19
+ label: ".cursor/skills",
20
+ skillsDir: ".cursor/skills",
21
+ adapters: { agentsMd: false, cursorRules: true, claude: false, githubCopilot: false },
22
+ },
23
+ claude: {
24
+ label: ".claude/skills",
25
+ skillsDir: ".claude/skills",
26
+ adapters: { agentsMd: false, cursorRules: false, claude: true, githubCopilot: false },
27
+ },
28
+ };
29
+ const DEFAULT_SKILLS_DIR = TARGETS[DEFAULT_TARGET].skillsDir;
11
30
  const GENERATED_START = "<!-- skillhub:start -->";
12
31
  const GENERATED_END = "<!-- skillhub:end -->";
13
32
 
@@ -114,6 +133,75 @@ function parseFlags(argv) {
114
133
  return { positional, flags };
115
134
  }
116
135
 
136
+ function normalizeTarget(value) {
137
+ if (!value) return null;
138
+
139
+ const normalized = String(value).trim().toLowerCase().replace(/^\./, "");
140
+ const aliases = {
141
+ ai: "ai",
142
+ cursor: "cursor",
143
+ cursorrules: "cursor",
144
+ ".cursor": "cursor",
145
+ claude: "claude",
146
+ cloude: "claude",
147
+ ".claude": "claude",
148
+ ".cloude": "claude",
149
+ };
150
+
151
+ const target = aliases[normalized] || normalized;
152
+ if (!TARGETS[target]) {
153
+ throw new Error(`Invalid target "${value}". Use: ai, cursor, or claude.`);
154
+ }
155
+
156
+ return target;
157
+ }
158
+
159
+ async function askTarget(defaultTarget = DEFAULT_TARGET) {
160
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
161
+ return defaultTarget;
162
+ }
163
+
164
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
165
+ try {
166
+ console.log(paint("bold", "Where should SkillHub install skill files?"));
167
+ console.log(` 1. ${TARGETS.ai.skillsDir} ${paint("dim", "(generic AI folder + AGENTS.md)")}`);
168
+ console.log(` 2. ${TARGETS.cursor.skillsDir} ${paint("dim", "(Cursor rules)")}`);
169
+ console.log(` 3. ${TARGETS.claude.skillsDir} ${paint("dim", "(Claude project instructions)")}`);
170
+
171
+ const answer = await rl.question(`Choose target [1/2/3 or ai/cursor/claude] (${defaultTarget}): `);
172
+ const raw = answer.trim();
173
+ if (!raw) return defaultTarget;
174
+ if (raw === "1") return "ai";
175
+ if (raw === "2") return "cursor";
176
+ if (raw === "3") return "claude";
177
+ return normalizeTarget(raw);
178
+ } finally {
179
+ rl.close();
180
+ }
181
+ }
182
+
183
+ function getConfigTarget(config) {
184
+ if (config?.target) return normalizeTarget(config.target);
185
+
186
+ const skillsDir = config?.skillsDir || DEFAULT_SKILLS_DIR;
187
+ if (skillsDir.startsWith(".cursor/")) return "cursor";
188
+ if (skillsDir.startsWith(".claude/") || skillsDir.startsWith(".cloude/")) return "claude";
189
+ return "ai";
190
+ }
191
+
192
+ function applyTargetToConfig(config, targetValue, { preserveExistingAdapters = false } = {}) {
193
+ const target = normalizeTarget(targetValue) || DEFAULT_TARGET;
194
+ const targetConfig = TARGETS[target];
195
+
196
+ config.target = target;
197
+ config.skillsDir = targetConfig.skillsDir;
198
+ config.adapters = preserveExistingAdapters
199
+ ? { ...targetConfig.adapters, ...(config.adapters || {}) }
200
+ : { ...targetConfig.adapters, ...(target === "ai" ? { agentsMd: true } : {}) };
201
+
202
+ return config;
203
+ }
204
+
117
205
  function parseSkillSpec(spec) {
118
206
  if (!spec || spec.trim().length === 0) {
119
207
  throw new Error("Skill name is required.");
@@ -188,20 +276,18 @@ function checksumFiles(files) {
188
276
  return `sha256-${hash.digest("hex")}`;
189
277
  }
190
278
 
191
- function defaultConfig(projectRoot) {
192
- return {
279
+ function defaultConfig(projectRoot, target = DEFAULT_TARGET) {
280
+ const config = {
193
281
  $schema: "https://skillhub.local/schema/skillhub.schema.json",
194
282
  project: path.basename(projectRoot),
195
283
  registry: "./skillhub-registry",
284
+ target: DEFAULT_TARGET,
196
285
  skillsDir: DEFAULT_SKILLS_DIR,
197
286
  skills: {},
198
- adapters: {
199
- agentsMd: true,
200
- cursorRules: true,
201
- claude: false,
202
- githubCopilot: false,
203
- },
287
+ adapters: {},
204
288
  };
289
+
290
+ return applyTargetToConfig(config, target);
205
291
  }
206
292
 
207
293
  function defaultLock() {
@@ -218,6 +304,10 @@ async function readProjectConfig(projectRoot) {
218
304
  if (!config) {
219
305
  throw new Error(`No ${CONFIG_FILE} found. Run: skillhub init`);
220
306
  }
307
+
308
+ config.target = getConfigTarget(config);
309
+ config.skillsDir = config.skillsDir || TARGETS[config.target].skillsDir;
310
+ config.adapters = config.adapters || TARGETS[config.target].adapters;
221
311
  return config;
222
312
  }
223
313
 
@@ -403,8 +493,9 @@ async function updateCursorRules(projectRoot, skillsDir, installed) {
403
493
  }
404
494
 
405
495
  async function updateClaudeMd(projectRoot, skillsDir, installed) {
406
- const body = `# SkillHub Instructions\n\nFollow the installed skills below:\n\n${generatedSkillList(skillsDir, installed)}`;
496
+ const body = `# SkillHub Instructions\n\nFollow the installed skills below before generating or changing code:\n\n${generatedSkillList(skillsDir, installed)}`;
407
497
  await replaceGeneratedBlock(path.join(projectRoot, "CLAUDE.md"), body, "# Claude Project Instructions\n");
498
+ await replaceGeneratedBlock(path.join(projectRoot, ".claude/skillhub.md"), body, "# Claude SkillHub Instructions\n");
408
499
  }
409
500
 
410
501
  async function updateCopilotInstructions(projectRoot, skillsDir, installed) {
@@ -415,11 +506,13 @@ async function updateCopilotInstructions(projectRoot, skillsDir, installed) {
415
506
  async function commandInit(projectRoot, flags) {
416
507
  const configPath = path.join(projectRoot, CONFIG_FILE);
417
508
  const lockPath = path.join(projectRoot, LOCK_FILE);
509
+ const target = normalizeTarget(flags.target || flags.to) || (flags.yes ? DEFAULT_TARGET : await askTarget(DEFAULT_TARGET));
418
510
 
419
511
  if (await exists(configPath)) {
420
512
  warn(`${CONFIG_FILE} already exists.`);
513
+ info(`To change target later: skillhub target ${target}`);
421
514
  } else {
422
- const config = defaultConfig(projectRoot);
515
+ const config = defaultConfig(projectRoot, target);
423
516
  if (flags.registry) config.registry = flags.registry;
424
517
  await writeJson(configPath, config);
425
518
  ok(`Created ${CONFIG_FILE}`);
@@ -432,8 +525,9 @@ async function commandInit(projectRoot, flags) {
432
525
  ok(`Created ${LOCK_FILE}`);
433
526
  }
434
527
 
435
- await ensureDir(path.join(projectRoot, DEFAULT_SKILLS_DIR));
436
- ok(`Created ${DEFAULT_SKILLS_DIR}`);
528
+ const config = await readProjectConfig(projectRoot);
529
+ await ensureDir(path.join(projectRoot, config.skillsDir));
530
+ ok(`Created ${config.skillsDir}`);
437
531
 
438
532
  info("Next: skillhub install nextjs-clean-architecture");
439
533
  }
@@ -442,6 +536,9 @@ async function commandInstall(projectRoot, spec, flags = {}) {
442
536
  const configPath = path.join(projectRoot, CONFIG_FILE);
443
537
  const lockPath = path.join(projectRoot, LOCK_FILE);
444
538
  const config = await readProjectConfig(projectRoot);
539
+ if (flags.target || flags.to) {
540
+ applyTargetToConfig(config, flags.target || flags.to);
541
+ }
445
542
  const lock = (await readJson(lockPath, null)) || defaultLock();
446
543
  const registry = resolveRegistry(projectRoot, config, flags);
447
544
  const { name, version } = parseSkillSpec(spec);
@@ -476,7 +573,12 @@ async function commandInstall(projectRoot, spec, flags = {}) {
476
573
  }
477
574
 
478
575
  async function commandSync(projectRoot, flags = {}) {
576
+ const configPath = path.join(projectRoot, CONFIG_FILE);
479
577
  const config = await readProjectConfig(projectRoot);
578
+ if (flags.target || flags.to) {
579
+ applyTargetToConfig(config, flags.target || flags.to);
580
+ await writeJson(configPath, config);
581
+ }
480
582
  const skills = Object.entries(config.skills || {});
481
583
 
482
584
  if (skills.length === 0) {
@@ -513,12 +615,13 @@ async function commandRemove(projectRoot, skillName) {
513
615
  const config = await readProjectConfig(projectRoot);
514
616
  const lock = (await readJson(lockPath, null)) || defaultLock();
515
617
  const skillsDir = config.skillsDir || DEFAULT_SKILLS_DIR;
618
+ const installedPath = lock.skills?.[skillName]?.installTo || `${skillsDir}/${skillName}`;
516
619
 
517
620
  delete config.skills?.[skillName];
518
621
  delete lock.skills?.[skillName];
519
622
  lock.updatedAt = now();
520
623
 
521
- await fs.rm(path.join(projectRoot, skillsDir, skillName), { recursive: true, force: true });
624
+ await fs.rm(path.join(projectRoot, installedPath), { recursive: true, force: true });
522
625
  await writeJson(configPath, config);
523
626
  await writeJson(lockPath, lock);
524
627
  await updateAdapters(projectRoot, config, lock);
@@ -526,6 +629,26 @@ async function commandRemove(projectRoot, skillName) {
526
629
  ok(`Removed ${skillName}`);
527
630
  }
528
631
 
632
+ async function commandTarget(projectRoot, targetValue, flags = {}) {
633
+ const configPath = path.join(projectRoot, CONFIG_FILE);
634
+ const lockPath = path.join(projectRoot, LOCK_FILE);
635
+ const config = await readProjectConfig(projectRoot);
636
+ const target = normalizeTarget(targetValue || flags.target || flags.to) || await askTarget(getConfigTarget(config));
637
+
638
+ applyTargetToConfig(config, target);
639
+ await writeJson(configPath, config);
640
+ await ensureDir(path.join(projectRoot, config.skillsDir));
641
+
642
+ const lock = (await readJson(lockPath, null)) || defaultLock();
643
+ await updateAdapters(projectRoot, config, lock);
644
+
645
+ ok(`SkillHub target set to ${target}`);
646
+ info(`Skill files will be installed to ${config.skillsDir}`);
647
+ if (Object.keys(lock.skills || {}).length > 0) {
648
+ info("Run: skillhub sync");
649
+ }
650
+ }
651
+
529
652
  async function commandCreate(projectRoot, name, flags = {}) {
530
653
  if (!name) throw new Error("Skill name is required.");
531
654
 
@@ -684,7 +807,7 @@ async function commandGenerate(projectRoot, skillName, templateName, resourceNam
684
807
  }
685
808
 
686
809
  function printHelp() {
687
- console.log(`\n${paint("bold", "SkillHub CLI")}\n\nInstall reusable AI/project skills into any codebase.\n\n${paint("bold", "Usage")}\n skillhub <command> [options]\n\n${paint("bold", "Commands")}\n init Create skillhub.json, lockfile, and .ai/skills\n install <skill[@version]> Install a skill from the registry\n sync Reinstall all skills from skillhub.json\n list Show installed skills\n remove <skill> Remove an installed skill\n create <skill> Scaffold a new local skill package\n validate <skill|path> Validate skill.json and SKILL.md\n\n${paint("bold", "Options")}\n --registry <path|url> Registry location. Default: ./skillhub-registry\n --version <version> Version used by create. Default: 1.0.0\n --category <name> Category used by create. Default: general\n\n${paint("bold", "Examples")}\n skillhub init\n skillhub install nextjs-clean-architecture\n skillhub install shadcn-crud-generator@1.0.0\n skillhub sync\n skillhub create api-security-rules --category security\n`);
810
+ console.log(`\n${paint("bold", "SkillHub CLI")}\n\nInstall reusable AI/project skills into any codebase.\n\n${paint("bold", "Usage")}\n skillhub <command> [options]\n\n${paint("bold", "Commands")}\n init Create skillhub.json, lockfile, and chosen skills folder\n target [ai|cursor|claude] Change where skills are installed\n install <skill[@version]> Install a skill from the registry\n sync Reinstall all skills from skillhub.json\n list Show installed skills\n remove <skill> Remove an installed skill\n create <skill> Scaffold a new local skill package\n validate <skill|path> Validate skill.json and SKILL.md\n generate <skill> <template> <name> Generate files from a skill template\n\n${paint("bold", "Targets")}\n ai Install to .ai/skills and update AGENTS.md\n cursor Install to .cursor/skills and update .cursor/rules/skillhub.mdc\n claude Install to .claude/skills and update CLAUDE.md + .claude/skillhub.md\n\n${paint("bold", "Options")}\n --target <ai|cursor|claude> Install target. Alias: --to\n --registry <path|url> Registry location. Default: ./skillhub-registry\n --version <version> Version used by create. Default: 1.0.0\n --category <name> Category used by create. Default: general\n --yes Use default init target without prompting\n\n${paint("bold", "Examples")}\n skillhub init --target cursor\n skillhub init --target claude\n skillhub install nextjs-clean-architecture --target cursor\n skillhub target claude\n skillhub sync\n skillhub create api-security-rules --category security\n`);
688
811
  }
689
812
 
690
813
  async function main() {
@@ -705,6 +828,10 @@ async function main() {
705
828
  case "add":
706
829
  await commandInstall(projectRoot, positional[0], flags);
707
830
  break;
831
+ case "target":
832
+ case "configure":
833
+ await commandTarget(projectRoot, positional[0], flags);
834
+ break;
708
835
  case "sync":
709
836
  await commandSync(projectRoot, flags);
710
837
  break;
@@ -742,6 +869,8 @@ export {
742
869
  sortVersions,
743
870
  validateSkillManifest,
744
871
  defaultConfig,
872
+ normalizeTarget,
873
+ applyTargetToConfig,
745
874
  };
746
875
 
747
876
  // Let people run this file through `node packages/skillhub-cli/bin/skillhub.mjs`.
@@ -143,3 +143,26 @@ The enabled adapters are controlled in `skillhub.json`:
143
143
  }
144
144
  }
145
145
  ```
146
+
147
+
148
+ ## Target folders
149
+
150
+ SkillHub can install skill packages into different AI-tool folders instead of only `.ai/skills`.
151
+
152
+ ```bash
153
+ skillhub init --target cursor
154
+ skillhub init --target claude
155
+ skillhub install nextjs-clean-architecture --target cursor
156
+ skillhub target claude
157
+ skillhub sync
158
+ ```
159
+
160
+ Supported targets:
161
+
162
+ ```txt
163
+ ai -> .ai/skills + AGENTS.md
164
+ cursor -> .cursor/skills + .cursor/rules/skillhub.mdc
165
+ claude -> .claude/skills + CLAUDE.md + .claude/skillhub.md
166
+ ```
167
+
168
+ The CLI also accepts `cloude` as an alias for `claude` to avoid failing on the common typo. Because apparently kindness can be implemented in 4 lines of code.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robosoft/skillhub-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Standalone SkillHub CLI for installing reusable AI/project skills into any codebase.",
5
5
  "type": "module",
6
6
  "bin": {