@robosoft/skillhub-cli 0.1.0 → 0.1.2

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,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.2
4
+
5
+ - Fixed `skillhub init --target cursor` when `skillhub.json` already exists.
6
+ - Existing projects now update `target`, `skillsDir`, and adapters instead of continuing to create `.ai/skills`.
7
+ - Added clearer init output showing the active target.
8
+
9
+ ## 0.1.1
10
+
11
+ - Added configurable install targets: `ai`, `cursor`, and `claude`.
12
+ - Added `skillhub target` command.
13
+ - Added `--target` / `--to` support for `init`, `install`, and `sync`.
14
+ - Cursor skills now install under `.cursor/skills` and update `.cursor/rules/skillhub.mdc`.
15
+ - Claude skills now install under `.claude/skills` and update `CLAUDE.md` plus `.claude/skillhub.md`.
16
+ - Added `cloude` alias for `claude`.
17
+
18
+ # Changelog
19
+
3
20
  ## 0.1.0
4
21
 
5
22
  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,34 @@ 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 requestedTarget = normalizeTarget(flags.target || flags.to);
510
+ let config;
418
511
 
419
512
  if (await exists(configPath)) {
513
+ config = await readProjectConfig(projectRoot);
420
514
  warn(`${CONFIG_FILE} already exists.`);
515
+
516
+ if (requestedTarget) {
517
+ const previousTarget = getConfigTarget(config);
518
+ applyTargetToConfig(config, requestedTarget);
519
+ if (flags.registry) config.registry = flags.registry;
520
+ await writeJson(configPath, config);
521
+ if (previousTarget === requestedTarget) {
522
+ ok(`${CONFIG_FILE} already uses target ${requestedTarget}`);
523
+ } else {
524
+ ok(`Updated ${CONFIG_FILE} target: ${previousTarget} → ${requestedTarget}`);
525
+ }
526
+ } else if (flags.registry) {
527
+ config.registry = flags.registry;
528
+ await writeJson(configPath, config);
529
+ ok(`Updated ${CONFIG_FILE} registry`);
530
+ } else {
531
+ info(`Current target: ${getConfigTarget(config)}`);
532
+ info(`To change target: skillhub init --target cursor`);
533
+ }
421
534
  } else {
422
- const config = defaultConfig(projectRoot);
535
+ const target = requestedTarget || (flags.yes ? DEFAULT_TARGET : await askTarget(DEFAULT_TARGET));
536
+ config = defaultConfig(projectRoot, target);
423
537
  if (flags.registry) config.registry = flags.registry;
424
538
  await writeJson(configPath, config);
425
539
  ok(`Created ${CONFIG_FILE}`);
@@ -432,9 +546,10 @@ async function commandInit(projectRoot, flags) {
432
546
  ok(`Created ${LOCK_FILE}`);
433
547
  }
434
548
 
435
- await ensureDir(path.join(projectRoot, DEFAULT_SKILLS_DIR));
436
- ok(`Created ${DEFAULT_SKILLS_DIR}`);
549
+ await ensureDir(path.join(projectRoot, config.skillsDir));
550
+ ok(`Created ${config.skillsDir}`);
437
551
 
552
+ info(`Target: ${config.target}`);
438
553
  info("Next: skillhub install nextjs-clean-architecture");
439
554
  }
440
555
 
@@ -442,6 +557,9 @@ async function commandInstall(projectRoot, spec, flags = {}) {
442
557
  const configPath = path.join(projectRoot, CONFIG_FILE);
443
558
  const lockPath = path.join(projectRoot, LOCK_FILE);
444
559
  const config = await readProjectConfig(projectRoot);
560
+ if (flags.target || flags.to) {
561
+ applyTargetToConfig(config, flags.target || flags.to);
562
+ }
445
563
  const lock = (await readJson(lockPath, null)) || defaultLock();
446
564
  const registry = resolveRegistry(projectRoot, config, flags);
447
565
  const { name, version } = parseSkillSpec(spec);
@@ -476,7 +594,12 @@ async function commandInstall(projectRoot, spec, flags = {}) {
476
594
  }
477
595
 
478
596
  async function commandSync(projectRoot, flags = {}) {
597
+ const configPath = path.join(projectRoot, CONFIG_FILE);
479
598
  const config = await readProjectConfig(projectRoot);
599
+ if (flags.target || flags.to) {
600
+ applyTargetToConfig(config, flags.target || flags.to);
601
+ await writeJson(configPath, config);
602
+ }
480
603
  const skills = Object.entries(config.skills || {});
481
604
 
482
605
  if (skills.length === 0) {
@@ -513,12 +636,13 @@ async function commandRemove(projectRoot, skillName) {
513
636
  const config = await readProjectConfig(projectRoot);
514
637
  const lock = (await readJson(lockPath, null)) || defaultLock();
515
638
  const skillsDir = config.skillsDir || DEFAULT_SKILLS_DIR;
639
+ const installedPath = lock.skills?.[skillName]?.installTo || `${skillsDir}/${skillName}`;
516
640
 
517
641
  delete config.skills?.[skillName];
518
642
  delete lock.skills?.[skillName];
519
643
  lock.updatedAt = now();
520
644
 
521
- await fs.rm(path.join(projectRoot, skillsDir, skillName), { recursive: true, force: true });
645
+ await fs.rm(path.join(projectRoot, installedPath), { recursive: true, force: true });
522
646
  await writeJson(configPath, config);
523
647
  await writeJson(lockPath, lock);
524
648
  await updateAdapters(projectRoot, config, lock);
@@ -526,6 +650,26 @@ async function commandRemove(projectRoot, skillName) {
526
650
  ok(`Removed ${skillName}`);
527
651
  }
528
652
 
653
+ async function commandTarget(projectRoot, targetValue, flags = {}) {
654
+ const configPath = path.join(projectRoot, CONFIG_FILE);
655
+ const lockPath = path.join(projectRoot, LOCK_FILE);
656
+ const config = await readProjectConfig(projectRoot);
657
+ const target = normalizeTarget(targetValue || flags.target || flags.to) || await askTarget(getConfigTarget(config));
658
+
659
+ applyTargetToConfig(config, target);
660
+ await writeJson(configPath, config);
661
+ await ensureDir(path.join(projectRoot, config.skillsDir));
662
+
663
+ const lock = (await readJson(lockPath, null)) || defaultLock();
664
+ await updateAdapters(projectRoot, config, lock);
665
+
666
+ ok(`SkillHub target set to ${target}`);
667
+ info(`Skill files will be installed to ${config.skillsDir}`);
668
+ if (Object.keys(lock.skills || {}).length > 0) {
669
+ info("Run: skillhub sync");
670
+ }
671
+ }
672
+
529
673
  async function commandCreate(projectRoot, name, flags = {}) {
530
674
  if (!name) throw new Error("Skill name is required.");
531
675
 
@@ -684,7 +828,7 @@ async function commandGenerate(projectRoot, skillName, templateName, resourceNam
684
828
  }
685
829
 
686
830
  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`);
831
+ 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
832
  }
689
833
 
690
834
  async function main() {
@@ -705,6 +849,10 @@ async function main() {
705
849
  case "add":
706
850
  await commandInstall(projectRoot, positional[0], flags);
707
851
  break;
852
+ case "target":
853
+ case "configure":
854
+ await commandTarget(projectRoot, positional[0], flags);
855
+ break;
708
856
  case "sync":
709
857
  await commandSync(projectRoot, flags);
710
858
  break;
@@ -742,6 +890,8 @@ export {
742
890
  sortVersions,
743
891
  validateSkillManifest,
744
892
  defaultConfig,
893
+ normalizeTarget,
894
+ applyTargetToConfig,
745
895
  };
746
896
 
747
897
  // 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.2",
4
4
  "description": "Standalone SkillHub CLI for installing reusable AI/project skills into any codebase.",
5
5
  "type": "module",
6
6
  "bin": {