@onezhao/skill-sync 1.1.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 -58
  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,32 +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 SELECT_ALL_SKILLS = Symbol("select-all-skills");
549
- const skillChoices = [{
550
- value: SELECT_ALL_SKILLS,
551
- label: pc.bold("Select All"),
552
- hint: `All ${skills.length} skills`
553
- }, ...skills.map((s) => ({
554
- value: s,
555
- label: s.name,
556
- hint: s.description.length > 50 ? s.description.slice(0, 47) + "..." : s.description
557
- }))];
558
622
  let selectedSkills;
559
623
  if (options.yes) {
560
624
  selectedSkills = skills;
561
625
  p.log.info(`Selected all ${skills.length} skills`);
562
626
  } else {
563
- const rawSelection = await p.multiselect({
627
+ const picked = await multiselectAll({
564
628
  message: "Select skills to sync",
565
- 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
+ })),
566
634
  required: true
567
635
  });
568
- if (p.isCancel(rawSelection)) {
636
+ if (p.isCancel(picked)) {
569
637
  p.cancel("Cancelled");
570
638
  process.exit(0);
571
639
  }
572
- const selection = rawSelection;
573
- selectedSkills = selection.includes(SELECT_ALL_SKILLS) ? skills : selection.filter((s) => s !== SELECT_ALL_SKILLS);
640
+ selectedSkills = picked;
574
641
  }
575
642
  if (selectedSkills.length === 0) {
576
643
  p.cancel("No skills selected");
@@ -599,27 +666,20 @@ async function runSkillSync(args = []) {
599
666
  agentsToLink = filteredAgents;
600
667
  p.log.info(`Selected all ${filteredAgents.length} available agents`);
601
668
  } else {
602
- const SELECT_ALL_AGENTS = Symbol("select-all-agents");
603
- const agentChoices = [{
604
- value: SELECT_ALL_AGENTS,
605
- label: pc.bold("Select All"),
606
- hint: `All ${filteredAgents.length} agents`
607
- }, ...filteredAgents.map((a) => ({
608
- value: a,
609
- label: a.displayName,
610
- hint: a.isInstalled ? `${a.skillsDir} ${pc.green("(installed)")}` : `${a.skillsDir} ${pc.yellow("(not detected)")}`
611
- }))];
612
- const rawAgents = await p.multiselect({
669
+ const picked = await multiselectAll({
613
670
  message: "Select target agents",
614
- 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
+ })),
615
676
  required: true
616
677
  });
617
- if (p.isCancel(rawAgents)) {
678
+ if (p.isCancel(picked)) {
618
679
  p.cancel("Cancelled");
619
680
  process.exit(0);
620
681
  }
621
- const agentSelection = rawAgents;
622
- agentsToLink = agentSelection.includes(SELECT_ALL_AGENTS) ? filteredAgents : agentSelection.filter((a) => a !== SELECT_ALL_AGENTS);
682
+ agentsToLink = picked;
623
683
  if (agentsToLink.length === 0) {
624
684
  p.cancel("No agents selected");
625
685
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onezhao/skill-sync",
3
- "version": "1.1.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",