@dealdeploy/skl 0.1.7 → 0.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 (55) hide show
  1. package/.agents/skills/opentui/SKILL.md +198 -0
  2. package/.agents/skills/opentui/references/animation/REFERENCE.md +431 -0
  3. package/.agents/skills/opentui/references/components/REFERENCE.md +143 -0
  4. package/.agents/skills/opentui/references/components/code-diff.md +496 -0
  5. package/.agents/skills/opentui/references/components/containers.md +412 -0
  6. package/.agents/skills/opentui/references/components/inputs.md +531 -0
  7. package/.agents/skills/opentui/references/components/text-display.md +384 -0
  8. package/.agents/skills/opentui/references/core/REFERENCE.md +145 -0
  9. package/.agents/skills/opentui/references/core/api.md +506 -0
  10. package/.agents/skills/opentui/references/core/configuration.md +166 -0
  11. package/.agents/skills/opentui/references/core/gotchas.md +393 -0
  12. package/.agents/skills/opentui/references/core/patterns.md +448 -0
  13. package/.agents/skills/opentui/references/keyboard/REFERENCE.md +511 -0
  14. package/.agents/skills/opentui/references/layout/REFERENCE.md +337 -0
  15. package/.agents/skills/opentui/references/layout/patterns.md +444 -0
  16. package/.agents/skills/opentui/references/react/REFERENCE.md +174 -0
  17. package/.agents/skills/opentui/references/react/api.md +435 -0
  18. package/.agents/skills/opentui/references/react/configuration.md +301 -0
  19. package/.agents/skills/opentui/references/react/gotchas.md +443 -0
  20. package/.agents/skills/opentui/references/react/patterns.md +501 -0
  21. package/.agents/skills/opentui/references/solid/REFERENCE.md +201 -0
  22. package/.agents/skills/opentui/references/solid/api.md +543 -0
  23. package/.agents/skills/opentui/references/solid/configuration.md +315 -0
  24. package/.agents/skills/opentui/references/solid/gotchas.md +415 -0
  25. package/.agents/skills/opentui/references/solid/patterns.md +558 -0
  26. package/.agents/skills/opentui/references/testing/REFERENCE.md +614 -0
  27. package/.claude/skills/opentui/SKILL.md +198 -0
  28. package/.claude/skills/opentui/references/animation/REFERENCE.md +431 -0
  29. package/.claude/skills/opentui/references/components/REFERENCE.md +143 -0
  30. package/.claude/skills/opentui/references/components/code-diff.md +496 -0
  31. package/.claude/skills/opentui/references/components/containers.md +412 -0
  32. package/.claude/skills/opentui/references/components/inputs.md +531 -0
  33. package/.claude/skills/opentui/references/components/text-display.md +384 -0
  34. package/.claude/skills/opentui/references/core/REFERENCE.md +145 -0
  35. package/.claude/skills/opentui/references/core/api.md +506 -0
  36. package/.claude/skills/opentui/references/core/configuration.md +166 -0
  37. package/.claude/skills/opentui/references/core/gotchas.md +393 -0
  38. package/.claude/skills/opentui/references/core/patterns.md +448 -0
  39. package/.claude/skills/opentui/references/keyboard/REFERENCE.md +511 -0
  40. package/.claude/skills/opentui/references/layout/REFERENCE.md +337 -0
  41. package/.claude/skills/opentui/references/layout/patterns.md +444 -0
  42. package/.claude/skills/opentui/references/react/REFERENCE.md +174 -0
  43. package/.claude/skills/opentui/references/react/api.md +435 -0
  44. package/.claude/skills/opentui/references/react/configuration.md +301 -0
  45. package/.claude/skills/opentui/references/react/gotchas.md +443 -0
  46. package/.claude/skills/opentui/references/react/patterns.md +501 -0
  47. package/.claude/skills/opentui/references/solid/REFERENCE.md +201 -0
  48. package/.claude/skills/opentui/references/solid/api.md +543 -0
  49. package/.claude/skills/opentui/references/solid/configuration.md +315 -0
  50. package/.claude/skills/opentui/references/solid/gotchas.md +415 -0
  51. package/.claude/skills/opentui/references/solid/patterns.md +558 -0
  52. package/.claude/skills/opentui/references/testing/REFERENCE.md +614 -0
  53. package/index.ts +169 -38
  54. package/package.json +1 -1
  55. package/update.ts +87 -0
package/index.ts CHANGED
@@ -39,6 +39,10 @@ if (arg === "add") {
39
39
  await import("./add.ts");
40
40
  process.exit(0);
41
41
  }
42
+ if (arg === "update") {
43
+ await import("./update.ts");
44
+ process.exit(0);
45
+ }
42
46
 
43
47
  try {
44
48
 
@@ -95,8 +99,35 @@ function nonLibraryExists(dir: string, name: string): boolean {
95
99
  }
96
100
  }
97
101
 
102
+ function isLocalDir(dir: string): boolean {
103
+ return dir !== GLOBAL_DIR && dir !== GLOBAL_CLAUDE_DIR;
104
+ }
105
+
106
+ function isEnabled(dir: string, name: string): boolean {
107
+ if (isLocalDir(dir)) return existsSync(join(dir, name));
108
+ return isLibraryLink(dir, name);
109
+ }
110
+
111
+ function isBlocked(dir: string, name: string): boolean {
112
+ if (isLocalDir(dir)) return false;
113
+ return nonLibraryExists(dir, name);
114
+ }
115
+
98
116
  function toggle(dir: string, name: string): boolean {
99
117
  const full = join(dir, name);
118
+ if (isLocalDir(dir)) {
119
+ // Copy-based toggle for project-local dirs
120
+ if (existsSync(full)) {
121
+ rmSync(full, { recursive: true, force: true });
122
+ return true;
123
+ }
124
+ // Remove any broken symlink left from old behavior
125
+ try { rmSync(full, { force: true }); } catch {}
126
+ mkdirSync(dir, { recursive: true });
127
+ cpSync(join(LIBRARY, name), full, { recursive: true });
128
+ return true;
129
+ }
130
+ // Symlink-based toggle for global dirs
100
131
  if (isLibraryLink(dir, name)) {
101
132
  unlinkSync(full);
102
133
  return true;
@@ -145,14 +176,11 @@ function ensureLocalClaudeDir(): string {
145
176
 
146
177
  // ── Migration check ──────────────────────────────────────────────────
147
178
  {
179
+ // Only migrate global dirs — project-local dirs use copies, not symlinks
148
180
  const dirsToCheck: [string, string][] = [
149
181
  [GLOBAL_DIR, "~/.agents/skills"],
150
182
  [GLOBAL_CLAUDE_DIR, "~/.claude/skills"],
151
183
  ];
152
- const ld = findLocalDir();
153
- if (ld) dirsToCheck.push([ld, ld.replace(homedir(), "~")]);
154
- const lcd = findLocalClaudeDir();
155
- if (lcd) dirsToCheck.push([lcd, lcd.replace(homedir(), "~")]);
156
184
 
157
185
  const migrations: { dir: string; label: string; name: string; conflict: boolean }[] = [];
158
186
 
@@ -171,7 +199,7 @@ function ensureLocalClaudeDir(): string {
171
199
  if (migrations.length > 0) {
172
200
  console.log("\nFound skills not managed by library:");
173
201
  for (const m of migrations) {
174
- console.log(` ${m.label}/${m.name}${m.conflict ? " (conflict — already in library)" : ""}`);
202
+ console.log(` ${m.label}/${m.name}${m.conflict ? " (already in library)" : ""}`);
175
203
  }
176
204
  process.stdout.write(`\nMigrate to ${LIBRARY.replace(homedir(), "~")} and replace with symlinks? [y/N] `);
177
205
 
@@ -181,33 +209,89 @@ function ensureLocalClaudeDir(): string {
181
209
 
182
210
  if (answer === "y" || answer === "yes") {
183
211
  let migrated = 0;
184
- let skipped = 0;
185
212
  for (const m of migrations) {
186
213
  const src = join(m.dir, m.name);
187
214
  const dest = join(LIBRARY, m.name);
188
215
  if (m.conflict) {
189
- console.log(` skip ${m.name} (already exists in library)`);
190
- skipped++;
191
- continue;
192
- }
193
- try {
194
- renameSync(src, dest);
195
- } catch {
196
- // cross-device fallback
197
- cpSync(src, dest, { recursive: true });
216
+ // Library already has this skill — remove local and symlink to library
198
217
  rmSync(src, { recursive: true, force: true });
218
+ symlinkSync(dest, src);
219
+ console.log(` replaced ${m.name} (library version kept)`);
220
+ } else {
221
+ try {
222
+ renameSync(src, dest);
223
+ } catch {
224
+ // cross-device fallback
225
+ cpSync(src, dest, { recursive: true });
226
+ rmSync(src, { recursive: true, force: true });
227
+ }
228
+ symlinkSync(dest, src);
229
+ console.log(` migrated ${m.name}`);
199
230
  }
200
- symlinkSync(dest, src);
201
- console.log(` migrated ${m.name}`);
202
231
  migrated++;
203
232
  }
204
- console.log(`\n${migrated} migrated${skipped ? `, ${skipped} skipped` : ""}\n`);
233
+ console.log(`\n${migrated} migrated\n`);
205
234
  } else {
206
235
  console.log("Skipping migration.\n");
207
236
  }
208
237
  }
209
238
  }
210
239
 
240
+ // ── Local symlink → copy conversion ─────────────────────────────────
241
+ {
242
+ const localDirsToCheck: [string, string][] = [];
243
+ const ld = findLocalDir();
244
+ if (ld) localDirsToCheck.push([ld, ld.replace(homedir(), "~")]);
245
+ const lcd = findLocalClaudeDir();
246
+ if (lcd) localDirsToCheck.push([lcd, lcd.replace(homedir(), "~")]);
247
+
248
+ const localSymlinks: { dir: string; label: string; name: string; target: string }[] = [];
249
+
250
+ for (const [dir, label] of localDirsToCheck) {
251
+ try {
252
+ for (const name of readdirSync(dir)) {
253
+ const full = join(dir, name);
254
+ try {
255
+ if (lstatSync(full).isSymbolicLink()) {
256
+ localSymlinks.push({ dir, label, name, target: readlinkSync(full) });
257
+ }
258
+ } catch {}
259
+ }
260
+ } catch {}
261
+ }
262
+
263
+ if (localSymlinks.length > 0) {
264
+ console.log("\nFound symlinked skills in project-local dirs:");
265
+ for (const s of localSymlinks) {
266
+ console.log(` ${s.label}/${s.name} → ${s.target}`);
267
+ }
268
+ process.stdout.write("\nConvert to local copies? (recommended for portability) [y/N] ");
269
+
270
+ const buf = new Uint8Array(100);
271
+ const n = readSync(0, buf);
272
+ const answer = new TextDecoder().decode(buf.subarray(0, n)).trim().toLowerCase();
273
+
274
+ if (answer === "y" || answer === "yes") {
275
+ let converted = 0;
276
+ for (const s of localSymlinks) {
277
+ const full = join(s.dir, s.name);
278
+ const libPath = join(LIBRARY, s.name);
279
+ unlinkSync(full);
280
+ if (existsSync(libPath)) {
281
+ cpSync(libPath, full, { recursive: true });
282
+ console.log(` converted ${s.name}`);
283
+ converted++;
284
+ } else {
285
+ console.log(` removed ${s.name} (not in library)`);
286
+ }
287
+ }
288
+ console.log(`\n${converted} converted\n`);
289
+ } else {
290
+ console.log("Skipping conversion.\n");
291
+ }
292
+ }
293
+ }
294
+
211
295
  const allSkills = readdirSync(LIBRARY).sort();
212
296
 
213
297
  // ── State ───────────────────────────────────────────────────────────
@@ -297,14 +381,23 @@ const colHeaderRow = new BoxRenderable(renderer, {
297
381
  paddingLeft: 1,
298
382
  });
299
383
 
384
+ let COL_W = 14;
385
+ let NAME_W = 34;
386
+
387
+ function calcWidths() {
388
+ const w = process.stdout.columns || 80;
389
+ COL_W = Math.max(5, Math.min(14, Math.floor((w - 20) / 4)));
390
+ NAME_W = Math.min(34, Math.max(15, w - COL_W * 4 - 1));
391
+ }
392
+ calcWidths();
393
+
300
394
  const colName = new TextRenderable(renderer, {
301
395
  id: "col-name",
302
396
  content: "Skill",
303
397
  fg: C.fgDim,
304
398
  attributes: TextAttributes.BOLD,
305
- width: 34,
399
+ width: NAME_W,
306
400
  });
307
- const COL_W = 14;
308
401
  const colGlobal = new TextRenderable(renderer, {
309
402
  id: "col-global",
310
403
  content: "~/.agents",
@@ -382,9 +475,9 @@ for (let i = 0; i < allSkills.length; i++) {
382
475
 
383
476
  const nameText = new TextRenderable(renderer, {
384
477
  id: `name-${i}`,
385
- content: ` ${skill}`,
478
+ content: ` ${ellipsize(skill, NAME_W - 3)}`,
386
479
  fg: C.fg,
387
- width: 34,
480
+ width: NAME_W,
388
481
  });
389
482
 
390
483
  const gChecked = isLibraryLink(GLOBAL_DIR, skill);
@@ -405,8 +498,8 @@ for (let i = 0; i < allSkills.length; i++) {
405
498
  width: COL_W,
406
499
  });
407
500
 
408
- const lChecked = localDir ? isLibraryLink(localDir, skill) : false;
409
- const lBlocked = localDir ? nonLibraryExists(localDir, skill) : false;
501
+ const lChecked = localDir ? isEnabled(localDir, skill) : false;
502
+ const lBlocked = localDir ? isBlocked(localDir, skill) : false;
410
503
  const localText = new TextRenderable(renderer, {
411
504
  id: `local-${i}`,
412
505
  content: localDir ? checkboxStr(lChecked, lBlocked) : " -",
@@ -414,8 +507,8 @@ for (let i = 0; i < allSkills.length; i++) {
414
507
  width: COL_W,
415
508
  });
416
509
 
417
- const lcChecked = localClaudeDir ? isLibraryLink(localClaudeDir, skill) : false;
418
- const lcBlocked = localClaudeDir ? nonLibraryExists(localClaudeDir, skill) : false;
510
+ const lcChecked = localClaudeDir ? isEnabled(localClaudeDir, skill) : false;
511
+ const lcBlocked = localClaudeDir ? isBlocked(localClaudeDir, skill) : false;
419
512
  const localClaudeText = new TextRenderable(renderer, {
420
513
  id: `local-claude-${i}`,
421
514
  content: localClaudeDir ? checkboxStr(lcChecked, lcBlocked) : " -",
@@ -442,7 +535,7 @@ const footerSep = new TextRenderable(renderer, {
442
535
 
443
536
  const footer = new TextRenderable(renderer, {
444
537
  id: "footer",
445
- content: " ↑↓ move ←→/tab col enter toggle ^a all ^e edit ^d del search esc quit",
538
+ content: " ↑↓ move ←→/tab col enter toggle ^a all ^e edit ^d del / search q/esc quit",
446
539
  fg: C.footer,
447
540
  height: 1,
448
541
  });
@@ -513,7 +606,7 @@ function updateRow(i: number) {
513
606
  r.row.backgroundColor = isCursor ? C.cursorBg : baseBg;
514
607
 
515
608
  const pointer = isCursor ? "▸" : " ";
516
- r.nameText.content = `${pointer} ${skill}`;
609
+ r.nameText.content = `${pointer} ${ellipsize(skill, NAME_W - 3)}`;
517
610
  r.nameText.fg = isCursor ? "#ffffff" : C.fg;
518
611
  r.nameText.attributes = isCursor ? TextAttributes.BOLD : TextAttributes.NONE;
519
612
 
@@ -533,8 +626,8 @@ function updateRow(i: number) {
533
626
  r.globalClaudeText.bg = gcActive ? C.accentBg : undefined;
534
627
  r.globalClaudeText.attributes = gcActive ? TextAttributes.BOLD : TextAttributes.NONE;
535
628
 
536
- const lChecked = localDir ? isLibraryLink(localDir, skill) : false;
537
- const lBlocked = localDir ? nonLibraryExists(localDir, skill) : false;
629
+ const lChecked = localDir ? isEnabled(localDir, skill) : false;
630
+ const lBlocked = localDir ? isBlocked(localDir, skill) : false;
538
631
  const lActive = isCursor && cursorCol === "local";
539
632
  if (localDir) {
540
633
  r.localText.content = checkboxStr(lChecked, lBlocked);
@@ -547,8 +640,8 @@ function updateRow(i: number) {
547
640
  r.localText.bg = lActive ? C.accentBg : undefined;
548
641
  }
549
642
 
550
- const lcChecked = localClaudeDir ? isLibraryLink(localClaudeDir, skill) : false;
551
- const lcBlocked = localClaudeDir ? nonLibraryExists(localClaudeDir, skill) : false;
643
+ const lcChecked = localClaudeDir ? isEnabled(localClaudeDir, skill) : false;
644
+ const lcBlocked = localClaudeDir ? isBlocked(localClaudeDir, skill) : false;
552
645
  const lcActive = isCursor && cursorCol === "localClaude";
553
646
  if (localClaudeDir) {
554
647
  r.localClaudeText.content = checkboxStr(lcChecked, lcBlocked);
@@ -571,6 +664,30 @@ function setStatus(msg: string, color: string) {
571
664
  }, 3000);
572
665
  }
573
666
 
667
+ function relayout() {
668
+ calcWidths();
669
+ colName.width = NAME_W;
670
+ colGlobal.width = COL_W;
671
+ colGlobalClaude.width = COL_W;
672
+ colLocal.width = COL_W;
673
+ colLocal.content = ellipsize(localLabel, COL_W - 1);
674
+ colLocalClaude.width = COL_W;
675
+ colLocalClaude.content = ellipsize(localClaudeLabel, COL_W - 1);
676
+ for (let i = 0; i < allSkills.length; i++) {
677
+ const r = rows[i];
678
+ r.nameText.width = NAME_W;
679
+ r.globalText.width = COL_W;
680
+ r.globalClaudeText.width = COL_W;
681
+ r.localText.width = COL_W;
682
+ r.localClaudeText.width = COL_W;
683
+ }
684
+ }
685
+
686
+ process.stdout.on("resize", () => {
687
+ relayout();
688
+ refreshAll();
689
+ });
690
+
574
691
  function refreshAll() {
575
692
  for (const i of filteredIndices) updateRow(i);
576
693
  updateSearchBar();
@@ -604,7 +721,7 @@ function toggleAllColumn(col: ColId) {
604
721
  // Determine intent: if majority of visible skills are linked, unlink all; otherwise link all
605
722
  let linkedCount = 0;
606
723
  for (const i of filteredIndices) {
607
- if (isLibraryLink(dir, allSkills[i])) linkedCount++;
724
+ if (isEnabled(dir, allSkills[i])) linkedCount++;
608
725
  }
609
726
  const shouldLink = linkedCount <= filteredIndices.length / 2;
610
727
  let changed = 0;
@@ -612,7 +729,7 @@ function toggleAllColumn(col: ColId) {
612
729
 
613
730
  for (const i of filteredIndices) {
614
731
  const skill = allSkills[i];
615
- const isLinked = isLibraryLink(dir, skill);
732
+ const isLinked = isEnabled(dir, skill);
616
733
  if (shouldLink && !isLinked) {
617
734
  if (toggle(dir, skill)) changed++;
618
735
  else skipped++;
@@ -658,11 +775,11 @@ function cancelPendingDelete() {
658
775
  function deleteSkill(idx: number) {
659
776
  const skill = allSkills[idx];
660
777
 
661
- // Remove symlinks first
778
+ // Remove from all skill dirs (symlinks for global, copies for local)
662
779
  if (isLibraryLink(GLOBAL_DIR, skill)) unlinkSync(join(GLOBAL_DIR, skill));
663
780
  if (isLibraryLink(GLOBAL_CLAUDE_DIR, skill)) unlinkSync(join(GLOBAL_CLAUDE_DIR, skill));
664
- if (localDir && isLibraryLink(localDir, skill)) unlinkSync(join(localDir, skill));
665
- if (localClaudeDir && isLibraryLink(localClaudeDir, skill)) unlinkSync(join(localClaudeDir, skill));
781
+ if (localDir && existsSync(join(localDir, skill))) rmSync(join(localDir, skill), { recursive: true, force: true });
782
+ if (localClaudeDir && existsSync(join(localClaudeDir, skill))) rmSync(join(localClaudeDir, skill), { recursive: true, force: true });
666
783
 
667
784
  // Remove from library
668
785
  rmSync(join(LIBRARY, skill), { recursive: true, force: true });
@@ -791,6 +908,20 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => {
791
908
  return;
792
909
  }
793
910
 
911
+ // ── "q" to quit ──
912
+ if (key.sequence === "q") {
913
+ renderer.destroy();
914
+ process.exit(0);
915
+ }
916
+
917
+ // ── "/" to focus search (vim-style) ──
918
+ if (key.sequence === "/") {
919
+ focusArea = "search";
920
+ refreshAll();
921
+ ensureVisible();
922
+ return;
923
+ }
924
+
794
925
  // ── Navigation & actions ──
795
926
  switch (key.name) {
796
927
  case "down":
@@ -835,7 +966,7 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => {
835
966
  const { dir, label } = dirForCol(cursorCol);
836
967
  const ok = toggle(dir, skill);
837
968
  if (ok) {
838
- const linked = isLibraryLink(dir, skill);
969
+ const linked = isEnabled(dir, skill);
839
970
  setStatus(
840
971
  linked ? `✓ ${skill} → ${label}` : `✗ ${skill} removed from ${label}`,
841
972
  linked ? C.statusOk : C.statusErr
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dealdeploy/skl",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "TUI skill manager for Claude Code agents",
5
5
  "module": "index.ts",
6
6
  "bin": {
package/update.ts ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { readdirSync, existsSync, cpSync, rmSync, lstatSync, readlinkSync, readSync, unlinkSync } from "fs";
4
+ import { join, resolve } from "path";
5
+ import { homedir } from "os";
6
+
7
+ const LIBRARY = join(homedir(), "dotfiles/skills");
8
+
9
+ if (!existsSync(LIBRARY)) {
10
+ console.error(`skl: library not found at ${LIBRARY}`);
11
+ process.exit(1);
12
+ }
13
+
14
+ const localDirs: [string, string][] = [];
15
+ const agentsDir = join(process.cwd(), ".agents/skills");
16
+ if (existsSync(agentsDir)) localDirs.push([agentsDir, ".agents/skills"]);
17
+
18
+ const claudeDir = join(process.cwd(), ".claude/skills");
19
+ if (existsSync(claudeDir)) localDirs.push([claudeDir, ".claude/skills"]);
20
+
21
+ if (localDirs.length === 0) {
22
+ console.log("No local skill directories found.");
23
+ process.exit(0);
24
+ }
25
+
26
+ type Entry = { dir: string; label: string; name: string; kind: "symlink" | "copy" };
27
+
28
+ const symlinks: Entry[] = [];
29
+ const copies: Entry[] = [];
30
+ let skipped = 0;
31
+
32
+ for (const [dir, label] of localDirs) {
33
+ for (const name of readdirSync(dir)) {
34
+ const localPath = join(dir, name);
35
+ const libPath = join(LIBRARY, name);
36
+ if (!existsSync(libPath)) {
37
+ skipped++;
38
+ continue;
39
+ }
40
+ const isSym = lstatSync(localPath).isSymbolicLink();
41
+ const entry: Entry = { dir, label, name, kind: isSym ? "symlink" : "copy" };
42
+ if (isSym) symlinks.push(entry);
43
+ else copies.push(entry);
44
+ }
45
+ }
46
+
47
+ // Handle symlinks → copies (requires confirmation)
48
+ if (symlinks.length > 0) {
49
+ console.log("\nSymlinks to convert to local copies:");
50
+ for (const s of symlinks) {
51
+ console.log(` ${s.label}/${s.name} → ${readlinkSync(join(s.dir, s.name))}`);
52
+ }
53
+ process.stdout.write(`\nReplace ${symlinks.length} symlink(s) with copies from library? [y/N] `);
54
+
55
+ const buf = new Uint8Array(100);
56
+ const n = readSync(0, buf);
57
+ const answer = new TextDecoder().decode(buf.subarray(0, n)).trim().toLowerCase();
58
+
59
+ if (answer === "y" || answer === "yes") {
60
+ for (const s of symlinks) {
61
+ const localPath = join(s.dir, s.name);
62
+ unlinkSync(localPath);
63
+ cpSync(join(LIBRARY, s.name), localPath, { recursive: true });
64
+ console.log(` converted ${s.label}/${s.name}`);
65
+ }
66
+ } else {
67
+ console.log("Skipping symlink conversion.");
68
+ }
69
+ }
70
+
71
+ // Update existing copies
72
+ if (copies.length > 0) {
73
+ console.log(`\nUpdating ${copies.length} local copies from library...`);
74
+ for (const c of copies) {
75
+ const localPath = join(c.dir, c.name);
76
+ rmSync(localPath, { recursive: true, force: true });
77
+ cpSync(join(LIBRARY, c.name), localPath, { recursive: true });
78
+ console.log(` updated ${c.label}/${c.name}`);
79
+ }
80
+ }
81
+
82
+ if (symlinks.length === 0 && copies.length === 0) {
83
+ console.log("Nothing to update.");
84
+ }
85
+ if (skipped > 0) {
86
+ console.log(`${skipped} skipped (not in library)`);
87
+ }