@onezhao/skill-sync 1.0.0 → 1.2.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.
Files changed (2) hide show
  1. package/dist/cli.mjs +118 -45
  2. package/package.json +2 -1
package/dist/cli.mjs CHANGED
@@ -6,6 +6,7 @@ import { access, lstat, mkdir, readFile, readlink, rm, symlink } from "fs/promis
6
6
  import { homedir, platform } from "os";
7
7
  import matter from "gray-matter";
8
8
  import { xdgConfig } from "xdg-basedir";
9
+ import { Prompt, isCancel } from "@clack/core";
9
10
  //#region src/skills.ts
10
11
  async function parseSkillMd(skillMdPath, options) {
11
12
  try {
@@ -37,6 +38,34 @@ function getOpenClawGlobalSkillsDir(homeDir = home, pathExists = existsSync) {
37
38
  return join(homeDir, ".openclaw/skills");
38
39
  }
39
40
  const agents = {
41
+ "claude-code": {
42
+ name: "claude-code",
43
+ displayName: "Claude Code",
44
+ skillsDir: ".claude/skills",
45
+ globalSkillsDir: join(claudeHome, "skills"),
46
+ detectInstalled: async () => existsSync(claudeHome)
47
+ },
48
+ "kiro-cli": {
49
+ name: "kiro-cli",
50
+ displayName: "Kiro CLI",
51
+ skillsDir: ".kiro/skills",
52
+ globalSkillsDir: join(home, ".kiro/skills"),
53
+ detectInstalled: async () => existsSync(join(home, ".kiro"))
54
+ },
55
+ trae: {
56
+ name: "trae",
57
+ displayName: "Trae",
58
+ skillsDir: ".trae/skills",
59
+ globalSkillsDir: join(home, ".trae/skills"),
60
+ detectInstalled: async () => existsSync(join(home, ".trae"))
61
+ },
62
+ "trae-cn": {
63
+ name: "trae-cn",
64
+ displayName: "Trae CN",
65
+ skillsDir: ".trae/skills",
66
+ globalSkillsDir: join(home, ".trae-cn/skills"),
67
+ detectInstalled: async () => existsSync(join(home, ".trae-cn"))
68
+ },
40
69
  amp: {
41
70
  name: "amp",
42
71
  displayName: "Amp",
@@ -58,13 +87,6 @@ const agents = {
58
87
  globalSkillsDir: join(home, ".augment/skills"),
59
88
  detectInstalled: async () => existsSync(join(home, ".augment"))
60
89
  },
61
- "claude-code": {
62
- name: "claude-code",
63
- displayName: "Claude Code",
64
- skillsDir: ".claude/skills",
65
- globalSkillsDir: join(claudeHome, "skills"),
66
- detectInstalled: async () => existsSync(claudeHome)
67
- },
68
90
  openclaw: {
69
91
  name: "openclaw",
70
92
  displayName: "OpenClaw",
@@ -184,13 +206,6 @@ const agents = {
184
206
  globalSkillsDir: join(home, ".config/agents/skills"),
185
207
  detectInstalled: async () => existsSync(join(home, ".kimi"))
186
208
  },
187
- "kiro-cli": {
188
- name: "kiro-cli",
189
- displayName: "Kiro CLI",
190
- skillsDir: ".kiro/skills",
191
- globalSkillsDir: join(home, ".kiro/skills"),
192
- detectInstalled: async () => existsSync(join(home, ".kiro"))
193
- },
194
209
  kode: {
195
210
  name: "kode",
196
211
  displayName: "Kode",
@@ -269,20 +284,6 @@ const agents = {
269
284
  globalSkillsDir: join(home, ".roo/skills"),
270
285
  detectInstalled: async () => existsSync(join(home, ".roo"))
271
286
  },
272
- trae: {
273
- name: "trae",
274
- displayName: "Trae",
275
- skillsDir: ".trae/skills",
276
- globalSkillsDir: join(home, ".trae/skills"),
277
- detectInstalled: async () => existsSync(join(home, ".trae"))
278
- },
279
- "trae-cn": {
280
- name: "trae-cn",
281
- displayName: "Trae CN",
282
- skillsDir: ".trae/skills",
283
- globalSkillsDir: join(home, ".trae-cn/skills"),
284
- detectInstalled: async () => existsSync(join(home, ".trae-cn"))
285
- },
286
287
  windsurf: {
287
288
  name: "windsurf",
288
289
  displayName: "Windsurf",
@@ -334,6 +335,79 @@ async function detectInstalledAgents() {
334
335
  })))).filter((r) => r.installed).map((r) => r.type);
335
336
  }
336
337
  //#endregion
338
+ //#region src/multiselect.ts
339
+ const S_STEP_SUBMIT = "◇";
340
+ const S_CHECKBOX_ACTIVE = pc.green("◼");
341
+ const S_CHECKBOX_INACTIVE = "◻";
342
+ const S_BAR = "│";
343
+ const S_BAR_END = "└";
344
+ async function multiselectAll(opts) {
345
+ const realOptions = opts.options;
346
+ const realValues = realOptions.map((o) => o.value);
347
+ const totalCount = realOptions.length + 1;
348
+ let cursor = 0;
349
+ const selected = /* @__PURE__ */ new Set();
350
+ let selectAllChecked = false;
351
+ const prompt = new Prompt({
352
+ validate(value) {
353
+ if (opts.required && (!value || value.length === 0)) return "Please select at least one option.";
354
+ },
355
+ render() {
356
+ const title = `${pc.gray(S_BAR)}\n${pc.cyan("●")} ${opts.message}\n`;
357
+ if (this.state === "submit") {
358
+ const labels = (this.value ?? []).map((v) => {
359
+ return realOptions.find((o) => o.value === v)?.label ?? String(v);
360
+ });
361
+ return `${pc.gray(S_STEP_SUBMIT)} ${opts.message}\n${pc.gray(S_BAR)} ${pc.dim(labels.join(", "))}`;
362
+ }
363
+ if (this.state === "cancel") return `${pc.gray(S_STEP_SUBMIT)} ${opts.message}\n${pc.gray(S_BAR)} ${pc.strikethrough(pc.dim("Cancelled"))}`;
364
+ const saActive = cursor === 0;
365
+ const saCheckbox = selectAllChecked ? S_CHECKBOX_ACTIVE : S_CHECKBOX_INACTIVE;
366
+ const saCursor = saActive ? pc.cyan("›") : " ";
367
+ const saLabel = saActive || selectAllChecked ? pc.bold("Select All") : pc.dim(pc.bold("Select All"));
368
+ const selectAllLine = `${pc.gray(S_BAR)} ${saCursor} ${saCheckbox} ${saLabel} ${pc.dim(`(${realValues.length} items)`)}`;
369
+ const lines = realOptions.map((opt, i) => {
370
+ const isActive = cursor === i + 1;
371
+ const isSelected = selected.has(i);
372
+ const checkbox = isSelected ? S_CHECKBOX_ACTIVE : S_CHECKBOX_INACTIVE;
373
+ const cur = isActive ? pc.cyan("›") : " ";
374
+ const label = isActive || isSelected ? opt.label : pc.dim(opt.label);
375
+ const hint = opt.hint ? pc.dim(` (${opt.hint})`) : "";
376
+ return `${pc.gray(S_BAR)} ${cur} ${checkbox} ${label}${hint}`;
377
+ });
378
+ const error = this.state === "error" && this.error ? `\n${pc.yellow("⚠")} ${this.error}` : "";
379
+ return `${title}${selectAllLine}\n${lines.join("\n")}\n${pc.gray(S_BAR)} ${pc.dim("(space: toggle, enter: confirm)")}${error}\n${pc.gray(S_BAR_END)}`;
380
+ }
381
+ }, false);
382
+ prompt.on("cursor", (action) => {
383
+ if (!action) return;
384
+ if (action === "up") cursor = cursor <= 0 ? totalCount - 1 : cursor - 1;
385
+ else if (action === "down") cursor = cursor >= totalCount - 1 ? 0 : cursor + 1;
386
+ else if (action === "space") {
387
+ if (cursor === 0) if (selectAllChecked) {
388
+ selected.clear();
389
+ selectAllChecked = false;
390
+ } else {
391
+ for (let i = 0; i < realOptions.length; i++) selected.add(i);
392
+ selectAllChecked = true;
393
+ }
394
+ else {
395
+ const idx = cursor - 1;
396
+ if (selected.has(idx)) selected.delete(idx);
397
+ else selected.add(idx);
398
+ selectAllChecked = selected.size === realOptions.length;
399
+ }
400
+ prompt.value = realValues.filter((_, i) => selected.has(i));
401
+ }
402
+ });
403
+ prompt.on("key", (char, key) => {
404
+ if (key?.name === "return") prompt.value = realValues.filter((_, i) => selected.has(i));
405
+ });
406
+ const result = await prompt.prompt();
407
+ if (isCancel(result)) return result;
408
+ return result ?? [];
409
+ }
410
+ //#endregion
337
411
  //#region src/cli.ts
338
412
  const AGENTS_DIR = ".agents";
339
413
  const SKILLS_SUBDIR = "skills";
@@ -545,25 +619,25 @@ async function runSkillSync(args = []) {
545
619
  process.exit(1);
546
620
  }
547
621
  spinner.stop(`Found ${pc.green(skills.length)} skill${skills.length !== 1 ? "s" : ""}`);
548
- const skillChoices = skills.map((s) => ({
549
- value: s,
550
- label: s.name,
551
- hint: s.description.length > 50 ? s.description.slice(0, 47) + "..." : s.description
552
- }));
553
622
  let selectedSkills;
554
623
  if (options.yes) {
555
624
  selectedSkills = skills;
556
625
  p.log.info(`Selected all ${skills.length} skills`);
557
626
  } else {
558
- selectedSkills = await p.multiselect({
627
+ const picked = await multiselectAll({
559
628
  message: "Select skills to sync",
560
- options: skillChoices,
629
+ options: skills.map((s) => ({
630
+ value: s,
631
+ label: s.name,
632
+ hint: s.description.length > 50 ? s.description.slice(0, 47) + "..." : s.description
633
+ })),
561
634
  required: true
562
635
  });
563
- if (p.isCancel(selectedSkills)) {
636
+ if (p.isCancel(picked)) {
564
637
  p.cancel("Cancelled");
565
638
  process.exit(0);
566
639
  }
640
+ selectedSkills = picked;
567
641
  }
568
642
  if (selectedSkills.length === 0) {
569
643
  p.cancel("No skills selected");
@@ -592,21 +666,20 @@ async function runSkillSync(args = []) {
592
666
  agentsToLink = filteredAgents;
593
667
  p.log.info(`Selected all ${filteredAgents.length} available agents`);
594
668
  } else {
595
- const agentChoices = filteredAgents.map((a) => ({
596
- value: a,
597
- label: a.displayName,
598
- hint: a.isInstalled ? `${a.skillsDir} ${pc.green("(installed)")}` : `${a.skillsDir} ${pc.yellow("(not detected)")}`
599
- }));
600
- const selectedAgents = await p.multiselect({
669
+ const picked = await multiselectAll({
601
670
  message: "Select target agents",
602
- options: agentChoices,
671
+ options: filteredAgents.map((a) => ({
672
+ value: a,
673
+ label: a.displayName,
674
+ hint: a.isInstalled ? `${a.skillsDir} ${pc.green("(installed)")}` : `${a.skillsDir} ${pc.yellow("(not detected)")}`
675
+ })),
603
676
  required: true
604
677
  });
605
- if (p.isCancel(selectedAgents)) {
678
+ if (p.isCancel(picked)) {
606
679
  p.cancel("Cancelled");
607
680
  process.exit(0);
608
681
  }
609
- agentsToLink = selectedAgents;
682
+ agentsToLink = picked;
610
683
  if (agentsToLink.length === 0) {
611
684
  p.cancel("No agents selected");
612
685
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onezhao/skill-sync",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Sync skills from .agents/skills to agent-specific directories",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,6 +41,7 @@
41
41
  "author": "Your Name <your@email.com>",
42
42
  "license": "MIT",
43
43
  "dependencies": {
44
+ "@clack/core": "^1.1.0",
44
45
  "@clack/prompts": "^0.11.0",
45
46
  "gray-matter": "^4.0.3",
46
47
  "picocolors": "^1.1.1",