@dealdeploy/skl 0.1.6 → 0.1.8

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 (56) 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/settings.local.json +11 -0
  28. package/.claude/skills/opentui/SKILL.md +198 -0
  29. package/.claude/skills/opentui/references/animation/REFERENCE.md +431 -0
  30. package/.claude/skills/opentui/references/components/REFERENCE.md +143 -0
  31. package/.claude/skills/opentui/references/components/code-diff.md +496 -0
  32. package/.claude/skills/opentui/references/components/containers.md +412 -0
  33. package/.claude/skills/opentui/references/components/inputs.md +531 -0
  34. package/.claude/skills/opentui/references/components/text-display.md +384 -0
  35. package/.claude/skills/opentui/references/core/REFERENCE.md +145 -0
  36. package/.claude/skills/opentui/references/core/api.md +506 -0
  37. package/.claude/skills/opentui/references/core/configuration.md +166 -0
  38. package/.claude/skills/opentui/references/core/gotchas.md +393 -0
  39. package/.claude/skills/opentui/references/core/patterns.md +448 -0
  40. package/.claude/skills/opentui/references/keyboard/REFERENCE.md +511 -0
  41. package/.claude/skills/opentui/references/layout/REFERENCE.md +337 -0
  42. package/.claude/skills/opentui/references/layout/patterns.md +444 -0
  43. package/.claude/skills/opentui/references/react/REFERENCE.md +174 -0
  44. package/.claude/skills/opentui/references/react/api.md +435 -0
  45. package/.claude/skills/opentui/references/react/configuration.md +301 -0
  46. package/.claude/skills/opentui/references/react/gotchas.md +443 -0
  47. package/.claude/skills/opentui/references/react/patterns.md +501 -0
  48. package/.claude/skills/opentui/references/solid/REFERENCE.md +201 -0
  49. package/.claude/skills/opentui/references/solid/api.md +543 -0
  50. package/.claude/skills/opentui/references/solid/configuration.md +315 -0
  51. package/.claude/skills/opentui/references/solid/gotchas.md +415 -0
  52. package/.claude/skills/opentui/references/solid/patterns.md +558 -0
  53. package/.claude/skills/opentui/references/testing/REFERENCE.md +614 -0
  54. package/index.ts +429 -86
  55. package/package.json +2 -1
  56. package/update.ts +87 -0
package/index.ts CHANGED
@@ -17,11 +17,15 @@ import {
17
17
  mkdirSync,
18
18
  existsSync,
19
19
  rmSync,
20
+ renameSync,
21
+ cpSync,
22
+ readSync,
20
23
  } from "fs";
21
24
  import { join, resolve } from "path";
22
25
  import { homedir } from "os";
23
26
 
24
- const VERSION = "0.1.5";
27
+ // @ts-ignore - bun supports JSON imports
28
+ const { version: VERSION } = await import("./package.json");
25
29
 
26
30
  // ── Flags ────────────────────────────────────────────────────────────
27
31
  const arg = process.argv[2];
@@ -35,6 +39,10 @@ if (arg === "add") {
35
39
  await import("./add.ts");
36
40
  process.exit(0);
37
41
  }
42
+ if (arg === "update") {
43
+ await import("./update.ts");
44
+ process.exit(0);
45
+ }
38
46
 
39
47
  try {
40
48
 
@@ -42,6 +50,7 @@ try {
42
50
 
43
51
  const LIBRARY = join(homedir(), "dotfiles/skills");
44
52
  const GLOBAL_DIR = join(homedir(), ".agents/skills");
53
+ const GLOBAL_CLAUDE_DIR = join(homedir(), ".claude/skills");
45
54
 
46
55
  if (!existsSync(LIBRARY)) {
47
56
  console.error(`skl: library not found at ${LIBRARY}`);
@@ -56,15 +65,17 @@ function ellipsize(text: string, max: number): string {
56
65
  return `${text.slice(0, max - 1)}…`;
57
66
  }
58
67
 
59
- // Safety: ensure GLOBAL_DIR is a real directory, not a symlink to the library
60
- try {
61
- if (lstatSync(GLOBAL_DIR).isSymbolicLink()) {
62
- console.error(`ERROR: ${GLOBAL_DIR} is a symlink — it must be a real directory.`);
63
- console.error("Remove it and create a real directory: rm ~/.agents/skills && mkdir ~/.agents/skills");
64
- process.exit(1);
68
+ // Safety: ensure skill dirs aren't symlinks pointing at the library itself
69
+ for (const [dir, label] of [[GLOBAL_DIR, "~/.agents/skills"], [GLOBAL_CLAUDE_DIR, "~/.claude/skills"]] as const) {
70
+ try {
71
+ if (lstatSync(dir).isSymbolicLink() && resolve(readlinkSync(dir)) === resolve(LIBRARY)) {
72
+ console.error(`ERROR: ${label} is a symlink to the library it must be a real directory.`);
73
+ console.error(`Remove it and create a real directory: rm ${label} && mkdir ${label}`);
74
+ process.exit(1);
75
+ }
76
+ } catch {
77
+ // doesn't exist yet, that's fine — toggle will mkdir
65
78
  }
66
- } catch {
67
- // doesn't exist yet, that's fine — toggle will mkdir
68
79
  }
69
80
 
70
81
  function isLibraryLink(dir: string, name: string): boolean {
@@ -88,8 +99,35 @@ function nonLibraryExists(dir: string, name: string): boolean {
88
99
  }
89
100
  }
90
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
+
91
116
  function toggle(dir: string, name: string): boolean {
92
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
93
131
  if (isLibraryLink(dir, name)) {
94
132
  unlinkSync(full);
95
133
  return true;
@@ -114,23 +152,163 @@ function ensureLocalDir(): string {
114
152
  mkdirSync(dir, { recursive: true });
115
153
  localDir = dir;
116
154
  localLabel = dir.replace(homedir(), "~");
117
- colLocal.content = ellipsize(localLabel, 20);
155
+ colLocal.content = ellipsize(localLabel, COL_W - 1);
118
156
  refreshAll();
119
157
  return dir;
120
158
  }
121
159
 
160
+ function findLocalClaudeDir(): string | null {
161
+ const dir = join(process.cwd(), ".claude/skills");
162
+ if (existsSync(dir)) return dir;
163
+ return null;
164
+ }
165
+
166
+ function ensureLocalClaudeDir(): string {
167
+ if (localClaudeDir) return localClaudeDir;
168
+ const dir = join(process.cwd(), ".claude/skills");
169
+ mkdirSync(dir, { recursive: true });
170
+ localClaudeDir = dir;
171
+ localClaudeLabel = dir.replace(homedir(), "~");
172
+ colLocalClaude.content = ellipsize(localClaudeLabel, COL_W - 1);
173
+ refreshAll();
174
+ return dir;
175
+ }
176
+
177
+ // ── Migration check ──────────────────────────────────────────────────
178
+ {
179
+ // Only migrate global dirs — project-local dirs use copies, not symlinks
180
+ const dirsToCheck: [string, string][] = [
181
+ [GLOBAL_DIR, "~/.agents/skills"],
182
+ [GLOBAL_CLAUDE_DIR, "~/.claude/skills"],
183
+ ];
184
+
185
+ const migrations: { dir: string; label: string; name: string; conflict: boolean }[] = [];
186
+
187
+ for (const [dir, label] of dirsToCheck) {
188
+ try {
189
+ for (const name of readdirSync(dir)) {
190
+ if (!isLibraryLink(dir, name)) {
191
+ migrations.push({ dir, label, name, conflict: existsSync(join(LIBRARY, name)) });
192
+ }
193
+ }
194
+ } catch {
195
+ // dir doesn't exist
196
+ }
197
+ }
198
+
199
+ if (migrations.length > 0) {
200
+ console.log("\nFound skills not managed by library:");
201
+ for (const m of migrations) {
202
+ console.log(` ${m.label}/${m.name}${m.conflict ? " (already in library)" : ""}`);
203
+ }
204
+ process.stdout.write(`\nMigrate to ${LIBRARY.replace(homedir(), "~")} and replace with symlinks? [y/N] `);
205
+
206
+ const buf = new Uint8Array(100);
207
+ const n = readSync(0, buf);
208
+ const answer = new TextDecoder().decode(buf.subarray(0, n)).trim().toLowerCase();
209
+
210
+ if (answer === "y" || answer === "yes") {
211
+ let migrated = 0;
212
+ for (const m of migrations) {
213
+ const src = join(m.dir, m.name);
214
+ const dest = join(LIBRARY, m.name);
215
+ if (m.conflict) {
216
+ // Library already has this skill — remove local and symlink to library
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}`);
230
+ }
231
+ migrated++;
232
+ }
233
+ console.log(`\n${migrated} migrated\n`);
234
+ } else {
235
+ console.log("Skipping migration.\n");
236
+ }
237
+ }
238
+ }
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
+
122
295
  const allSkills = readdirSync(LIBRARY).sort();
123
296
 
124
297
  // ── State ───────────────────────────────────────────────────────────
125
298
 
299
+ type ColId = "global" | "globalClaude" | "local" | "localClaude";
300
+
126
301
  let cursor = 0; // index into filteredIndices
127
- let cursorCol: "global" | "local" = "global";
302
+ let cursorCol: ColId = "global";
303
+ let focusArea: "search" | "grid" = "search";
128
304
  let localDir = findLocalDir();
305
+ let localClaudeDir = findLocalClaudeDir();
129
306
  let statusTimeout: ReturnType<typeof setTimeout> | null = null;
130
307
  let pendingDelete: number | null = null; // allSkills index awaiting confirmation
131
308
  const deletedSkills = new Set<number>();
132
309
  let searchQuery = "";
133
310
  let filteredIndices: number[] = allSkills.map((_, i) => i);
311
+ const COL_ORDER: ColId[] = ["global", "globalClaude", "local", "localClaude"];
134
312
 
135
313
  // ── Colors ──────────────────────────────────────────────────────────
136
314
 
@@ -179,15 +357,22 @@ const outer = new BoxRenderable(renderer, {
179
357
  backgroundColor: C.bg,
180
358
  });
181
359
 
182
- // Search bar (always visible at top, type-to-filter)
360
+ // Search bar (lives inside scrollBox as the first row)
361
+ const searchRow = new BoxRenderable(renderer, {
362
+ id: "search-row",
363
+ flexDirection: "row",
364
+ height: 1,
365
+ width: "100%",
366
+ paddingLeft: 1,
367
+ backgroundColor: C.rowBg,
368
+ });
183
369
  const searchBar = new TextRenderable(renderer, {
184
370
  id: "search-bar",
185
371
  content: "",
186
372
  fg: C.search,
187
- width: "100%",
188
373
  height: 1,
189
- visible: true,
190
374
  });
375
+ searchRow.add(searchBar);
191
376
 
192
377
  const colHeaderRow = new BoxRenderable(renderer, {
193
378
  id: "col-header-row",
@@ -196,38 +381,65 @@ const colHeaderRow = new BoxRenderable(renderer, {
196
381
  paddingLeft: 1,
197
382
  });
198
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
+
199
394
  const colName = new TextRenderable(renderer, {
200
395
  id: "col-name",
201
396
  content: "Skill",
202
397
  fg: C.fgDim,
203
398
  attributes: TextAttributes.BOLD,
204
- width: 34,
399
+ width: NAME_W,
205
400
  });
206
401
  const colGlobal = new TextRenderable(renderer, {
207
402
  id: "col-global",
208
403
  content: "~/.agents",
209
404
  fg: C.fgDim,
210
405
  attributes: TextAttributes.BOLD,
211
- width: 12,
406
+ width: COL_W,
407
+ });
408
+ const colGlobalClaude = new TextRenderable(renderer, {
409
+ id: "col-global-claude",
410
+ content: "~/.claude",
411
+ fg: C.fgDim,
412
+ attributes: TextAttributes.BOLD,
413
+ width: COL_W,
212
414
  });
213
- let localLabel = localDir ? localDir.replace(homedir(), "~") : ".agents (n/a)";
214
- const localLabelHeader = ellipsize(localLabel, 20);
415
+ let localLabel = localDir ? localDir.replace(homedir(), "~") : ".agents/skills";
215
416
  const colLocal = new TextRenderable(renderer, {
216
417
  id: "col-local",
217
- content: localLabelHeader,
418
+ content: ellipsize(localLabel, COL_W - 1),
218
419
  fg: C.fgDim,
219
420
  attributes: TextAttributes.BOLD,
220
- width: 20,
421
+ width: COL_W,
422
+ });
423
+ let localClaudeLabel = localClaudeDir ? localClaudeDir.replace(homedir(), "~") : ".claude/skills";
424
+ const colLocalClaude = new TextRenderable(renderer, {
425
+ id: "col-local-claude",
426
+ content: ellipsize(localClaudeLabel, COL_W - 1),
427
+ fg: C.fgDim,
428
+ attributes: TextAttributes.BOLD,
429
+ width: COL_W,
221
430
  });
222
431
 
223
432
  colHeaderRow.add(colName);
224
433
  colHeaderRow.add(colGlobal);
434
+ colHeaderRow.add(colGlobalClaude);
225
435
  colHeaderRow.add(colLocal);
436
+ colHeaderRow.add(colLocalClaude);
226
437
 
227
438
  const sep = new TextRenderable(renderer, {
228
439
  id: "sep",
229
- content: "─".repeat(60),
440
+ content: "─".repeat(200),
230
441
  fg: C.border,
442
+ width: "100%",
231
443
  height: 1,
232
444
  });
233
445
 
@@ -241,10 +453,14 @@ type RowRefs = {
241
453
  row: BoxRenderable;
242
454
  nameText: TextRenderable;
243
455
  globalText: TextRenderable;
456
+ globalClaudeText: TextRenderable;
244
457
  localText: TextRenderable;
458
+ localClaudeText: TextRenderable;
245
459
  };
246
460
  const rows: RowRefs[] = [];
247
461
 
462
+ scrollBox.add(searchRow);
463
+
248
464
  for (let i = 0; i < allSkills.length; i++) {
249
465
  const skill = allSkills[i];
250
466
 
@@ -259,9 +475,9 @@ for (let i = 0; i < allSkills.length; i++) {
259
475
 
260
476
  const nameText = new TextRenderable(renderer, {
261
477
  id: `name-${i}`,
262
- content: ` ${skill}`,
478
+ content: ` ${ellipsize(skill, NAME_W - 3)}`,
263
479
  fg: C.fg,
264
- width: 34,
480
+ width: NAME_W,
265
481
  });
266
482
 
267
483
  const gChecked = isLibraryLink(GLOBAL_DIR, skill);
@@ -270,35 +486,56 @@ for (let i = 0; i < allSkills.length; i++) {
270
486
  id: `global-${i}`,
271
487
  content: checkboxStr(gChecked, gBlocked),
272
488
  fg: checkboxColor(gChecked, gBlocked, false),
273
- width: 10,
489
+ width: COL_W,
490
+ });
491
+
492
+ const gcChecked = isLibraryLink(GLOBAL_CLAUDE_DIR, skill);
493
+ const gcBlocked = nonLibraryExists(GLOBAL_CLAUDE_DIR, skill);
494
+ const globalClaudeText = new TextRenderable(renderer, {
495
+ id: `global-claude-${i}`,
496
+ content: checkboxStr(gcChecked, gcBlocked),
497
+ fg: checkboxColor(gcChecked, gcBlocked, false),
498
+ width: COL_W,
274
499
  });
275
500
 
276
- const lChecked = localDir ? isLibraryLink(localDir, skill) : false;
277
- const lBlocked = localDir ? nonLibraryExists(localDir, skill) : false;
501
+ const lChecked = localDir ? isEnabled(localDir, skill) : false;
502
+ const lBlocked = localDir ? isBlocked(localDir, skill) : false;
278
503
  const localText = new TextRenderable(renderer, {
279
504
  id: `local-${i}`,
280
505
  content: localDir ? checkboxStr(lChecked, lBlocked) : " -",
281
506
  fg: localDir ? checkboxColor(lChecked, lBlocked, false) : C.fgDim,
282
- width: 10,
507
+ width: COL_W,
508
+ });
509
+
510
+ const lcChecked = localClaudeDir ? isEnabled(localClaudeDir, skill) : false;
511
+ const lcBlocked = localClaudeDir ? isBlocked(localClaudeDir, skill) : false;
512
+ const localClaudeText = new TextRenderable(renderer, {
513
+ id: `local-claude-${i}`,
514
+ content: localClaudeDir ? checkboxStr(lcChecked, lcBlocked) : " -",
515
+ fg: localClaudeDir ? checkboxColor(lcChecked, lcBlocked, false) : C.fgDim,
516
+ width: COL_W,
283
517
  });
284
518
 
285
519
  row.add(nameText);
286
520
  row.add(globalText);
521
+ row.add(globalClaudeText);
287
522
  row.add(localText);
523
+ row.add(localClaudeText);
288
524
  scrollBox.add(row);
289
- rows.push({ row, nameText, globalText, localText });
525
+ rows.push({ row, nameText, globalText, globalClaudeText, localText, localClaudeText });
290
526
  }
291
527
 
292
528
  const footerSep = new TextRenderable(renderer, {
293
529
  id: "footer-sep",
294
- content: "─".repeat(60),
530
+ content: "─".repeat(200),
295
531
  fg: C.border,
532
+ width: "100%",
296
533
  height: 1,
297
534
  });
298
535
 
299
536
  const footer = new TextRenderable(renderer, {
300
537
  id: "footer",
301
- content: " type to filter ↑↓ move ←→/tab col enter toggle ^a all ^e edit ^d del esc quit",
538
+ content: " ↑↓ move ←→/tab col enter toggle ^a all ^e edit ^d del / search esc quit",
302
539
  fg: C.footer,
303
540
  height: 1,
304
541
  });
@@ -310,7 +547,6 @@ const statusLine = new TextRenderable(renderer, {
310
547
  height: 1,
311
548
  });
312
549
 
313
- outer.add(searchBar);
314
550
  outer.add(colHeaderRow);
315
551
  outer.add(sep);
316
552
  outer.add(scrollBox);
@@ -342,7 +578,15 @@ function applyFilter() {
342
578
  }
343
579
 
344
580
  function updateSearchBar() {
345
- searchBar.content = ` / ${searchQuery}█`;
581
+ if (focusArea === "search") {
582
+ searchBar.content = `▸ ${searchQuery}█`;
583
+ searchBar.fg = C.search;
584
+ searchRow.backgroundColor = C.cursorBg;
585
+ } else {
586
+ searchBar.content = searchQuery ? ` ${searchQuery}` : ` search...`;
587
+ searchBar.fg = C.fgDim;
588
+ searchRow.backgroundColor = C.rowBg;
589
+ }
346
590
  }
347
591
 
348
592
  // ── Update display ──────────────────────────────────────────────────
@@ -355,14 +599,14 @@ function updateRow(i: number) {
355
599
  const skill = allSkills[i];
356
600
  const r = rows[i];
357
601
  const ci = currentSkillIndex();
358
- const isCursor = ci === i;
602
+ const isCursor = ci === i && focusArea === "grid";
359
603
 
360
604
  const visPos = filteredIndices.indexOf(i);
361
605
  const baseBg = visPos % 2 === 0 ? C.rowBg : C.rowAltBg;
362
606
  r.row.backgroundColor = isCursor ? C.cursorBg : baseBg;
363
607
 
364
608
  const pointer = isCursor ? "▸" : " ";
365
- r.nameText.content = `${pointer} ${skill}`;
609
+ r.nameText.content = `${pointer} ${ellipsize(skill, NAME_W - 3)}`;
366
610
  r.nameText.fg = isCursor ? "#ffffff" : C.fg;
367
611
  r.nameText.attributes = isCursor ? TextAttributes.BOLD : TextAttributes.NONE;
368
612
 
@@ -374,8 +618,16 @@ function updateRow(i: number) {
374
618
  r.globalText.bg = gActive ? C.accentBg : undefined;
375
619
  r.globalText.attributes = gActive ? TextAttributes.BOLD : TextAttributes.NONE;
376
620
 
377
- const lChecked = localDir ? isLibraryLink(localDir, skill) : false;
378
- const lBlocked = localDir ? nonLibraryExists(localDir, skill) : false;
621
+ const gcChecked = isLibraryLink(GLOBAL_CLAUDE_DIR, skill);
622
+ const gcBlocked = nonLibraryExists(GLOBAL_CLAUDE_DIR, skill);
623
+ const gcActive = isCursor && cursorCol === "globalClaude";
624
+ r.globalClaudeText.content = checkboxStr(gcChecked, gcBlocked);
625
+ r.globalClaudeText.fg = checkboxColor(gcChecked, gcBlocked, gcActive);
626
+ r.globalClaudeText.bg = gcActive ? C.accentBg : undefined;
627
+ r.globalClaudeText.attributes = gcActive ? TextAttributes.BOLD : TextAttributes.NONE;
628
+
629
+ const lChecked = localDir ? isEnabled(localDir, skill) : false;
630
+ const lBlocked = localDir ? isBlocked(localDir, skill) : false;
379
631
  const lActive = isCursor && cursorCol === "local";
380
632
  if (localDir) {
381
633
  r.localText.content = checkboxStr(lChecked, lBlocked);
@@ -387,6 +639,20 @@ function updateRow(i: number) {
387
639
  r.localText.fg = C.fgDim;
388
640
  r.localText.bg = lActive ? C.accentBg : undefined;
389
641
  }
642
+
643
+ const lcChecked = localClaudeDir ? isEnabled(localClaudeDir, skill) : false;
644
+ const lcBlocked = localClaudeDir ? isBlocked(localClaudeDir, skill) : false;
645
+ const lcActive = isCursor && cursorCol === "localClaude";
646
+ if (localClaudeDir) {
647
+ r.localClaudeText.content = checkboxStr(lcChecked, lcBlocked);
648
+ r.localClaudeText.fg = checkboxColor(lcChecked, lcBlocked, lcActive);
649
+ r.localClaudeText.bg = lcActive ? C.accentBg : undefined;
650
+ r.localClaudeText.attributes = lcActive ? TextAttributes.BOLD : TextAttributes.NONE;
651
+ } else {
652
+ r.localClaudeText.content = " -";
653
+ r.localClaudeText.fg = C.fgDim;
654
+ r.localClaudeText.bg = lcActive ? C.accentBg : undefined;
655
+ }
390
656
  }
391
657
 
392
658
  function setStatus(msg: string, color: string) {
@@ -398,6 +664,30 @@ function setStatus(msg: string, color: string) {
398
664
  }, 3000);
399
665
  }
400
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
+
401
691
  function refreshAll() {
402
692
  for (const i of filteredIndices) updateRow(i);
403
693
  updateSearchBar();
@@ -408,18 +698,30 @@ refreshAll();
408
698
  // ── Scrolling helper ────────────────────────────────────────────────
409
699
 
410
700
  function ensureVisible() {
411
- scrollBox.scrollTo(Math.max(0, cursor - 2));
701
+ if (focusArea === "search") {
702
+ scrollBox.scrollTo(0);
703
+ } else {
704
+ // +1 offset because searchRow is child 0 in scrollBox
705
+ scrollBox.scrollTo(Math.max(0, cursor + 1 - 2));
706
+ }
412
707
  }
413
708
 
414
709
  // ── Toggle all in column ────────────────────────────────────────────
415
710
 
416
- function toggleAllColumn(col: "global" | "local") {
417
- const dir = col === "global" ? GLOBAL_DIR : ensureLocalDir();
711
+ function toggleAllColumn(col: ColId) {
712
+ let dir: string;
713
+ let dirLabel: string;
714
+ switch (col) {
715
+ case "global": dir = GLOBAL_DIR; dirLabel = "~/.agents"; break;
716
+ case "globalClaude": dir = GLOBAL_CLAUDE_DIR; dirLabel = "~/.claude"; break;
717
+ case "local": dir = ensureLocalDir(); dirLabel = localLabel; break;
718
+ case "localClaude": dir = ensureLocalClaudeDir(); dirLabel = localClaudeLabel; break;
719
+ }
418
720
 
419
721
  // Determine intent: if majority of visible skills are linked, unlink all; otherwise link all
420
722
  let linkedCount = 0;
421
723
  for (const i of filteredIndices) {
422
- if (isLibraryLink(dir, allSkills[i])) linkedCount++;
724
+ if (isEnabled(dir, allSkills[i])) linkedCount++;
423
725
  }
424
726
  const shouldLink = linkedCount <= filteredIndices.length / 2;
425
727
  let changed = 0;
@@ -427,7 +729,7 @@ function toggleAllColumn(col: "global" | "local") {
427
729
 
428
730
  for (const i of filteredIndices) {
429
731
  const skill = allSkills[i];
430
- const isLinked = isLibraryLink(dir, skill);
732
+ const isLinked = isEnabled(dir, skill);
431
733
  if (shouldLink && !isLinked) {
432
734
  if (toggle(dir, skill)) changed++;
433
735
  else skipped++;
@@ -436,7 +738,6 @@ function toggleAllColumn(col: "global" | "local") {
436
738
  }
437
739
  }
438
740
 
439
- const dirLabel = col === "global" ? "~/.agents" : localLabel;
440
741
  const action = shouldLink ? "linked" : "unlinked";
441
742
  let msg = `${action} ${changed} skills in ${dirLabel}`;
442
743
  if (skipped) msg += ` (${skipped} skipped)`;
@@ -474,9 +775,11 @@ function cancelPendingDelete() {
474
775
  function deleteSkill(idx: number) {
475
776
  const skill = allSkills[idx];
476
777
 
477
- // Remove symlinks first
778
+ // Remove from all skill dirs (symlinks for global, copies for local)
478
779
  if (isLibraryLink(GLOBAL_DIR, skill)) unlinkSync(join(GLOBAL_DIR, skill));
479
- if (localDir && isLibraryLink(localDir, skill)) unlinkSync(join(localDir, skill));
780
+ if (isLibraryLink(GLOBAL_CLAUDE_DIR, skill)) unlinkSync(join(GLOBAL_CLAUDE_DIR, 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 });
480
783
 
481
784
  // Remove from library
482
785
  rmSync(join(LIBRARY, skill), { recursive: true, force: true });
@@ -494,6 +797,25 @@ function deleteSkill(idx: number) {
494
797
 
495
798
  // ── Key handler ─────────────────────────────────────────────────────
496
799
 
800
+ function dirForCol(col: ColId): { dir: string; label: string } {
801
+ switch (col) {
802
+ case "global": return { dir: GLOBAL_DIR, label: "~/.agents/skills" };
803
+ case "globalClaude": return { dir: GLOBAL_CLAUDE_DIR, label: "~/.claude/skills" };
804
+ case "local": return { dir: ensureLocalDir(), label: localLabel };
805
+ case "localClaude": return { dir: ensureLocalClaudeDir(), label: localClaudeLabel };
806
+ }
807
+ }
808
+
809
+ function colNext(col: ColId): ColId {
810
+ const i = COL_ORDER.indexOf(col);
811
+ return COL_ORDER[(i + 1) % COL_ORDER.length];
812
+ }
813
+
814
+ function colPrev(col: ColId): ColId {
815
+ const i = COL_ORDER.indexOf(col);
816
+ return COL_ORDER[(i - 1 + COL_ORDER.length) % COL_ORDER.length];
817
+ }
818
+
497
819
  renderer.keyInput.on("keypress", (key: KeyEvent) => {
498
820
  // ── Delete confirmation mode ──
499
821
  if (pendingDelete !== null) {
@@ -508,13 +830,17 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => {
508
830
 
509
831
  const prevIdx = currentSkillIndex();
510
832
 
511
- // ── Escape: clear search or quit ──
833
+ // ── Escape ──
512
834
  if (key.name === "escape") {
513
- if (searchQuery) {
835
+ if (focusArea === "search" && searchQuery) {
514
836
  searchQuery = "";
515
837
  applyFilter();
516
838
  refreshAll();
517
839
  ensureVisible();
840
+ } else if (focusArea === "search") {
841
+ focusArea = "grid";
842
+ refreshAll();
843
+ ensureVisible();
518
844
  } else {
519
845
  renderer.destroy();
520
846
  process.exit(0);
@@ -522,18 +848,41 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => {
522
848
  return;
523
849
  }
524
850
 
525
- // ── Backspace: delete from search ──
526
- if (key.name === "backspace") {
527
- if (searchQuery) {
528
- searchQuery = searchQuery.slice(0, -1);
851
+ // ── Search-focused input ──
852
+ if (focusArea === "search") {
853
+ if (key.name === "backspace") {
854
+ if (searchQuery) {
855
+ searchQuery = searchQuery.slice(0, -1);
856
+ applyFilter();
857
+ cursor = 0;
858
+ refreshAll();
859
+ ensureVisible();
860
+ }
861
+ return;
862
+ }
863
+ if (key.name === "down" || key.name === "return") {
864
+ focusArea = "grid";
865
+ refreshAll();
866
+ ensureVisible();
867
+ return;
868
+ }
869
+ // Printable character → append to search
870
+ if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
871
+ searchQuery += key.sequence;
529
872
  applyFilter();
530
873
  cursor = 0;
531
874
  refreshAll();
532
875
  ensureVisible();
876
+ return;
533
877
  }
534
878
  return;
535
879
  }
536
880
 
881
+ // ── Grid-focused input ──
882
+
883
+ // ── Backspace in grid: do nothing ──
884
+ if (key.name === "backspace") return;
885
+
537
886
  // ── Ctrl combos ──
538
887
  if (key.ctrl) {
539
888
  switch (key.name) {
@@ -559,22 +908,37 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => {
559
908
  return;
560
909
  }
561
910
 
562
- // ── Navigation & actions (arrow keys, enter, tab) ──
911
+ // ── "/" to focus search (vim-style) ──
912
+ if (key.sequence === "/") {
913
+ focusArea = "search";
914
+ refreshAll();
915
+ ensureVisible();
916
+ return;
917
+ }
918
+
919
+ // ── Navigation & actions ──
563
920
  switch (key.name) {
564
921
  case "down":
565
922
  if (cursor < filteredIndices.length - 1) cursor++;
566
923
  break;
567
924
  case "up":
568
- if (cursor > 0) cursor--;
925
+ if (cursor > 0) {
926
+ cursor--;
927
+ } else {
928
+ // At top of grid → move to search
929
+ focusArea = "search";
930
+ refreshAll();
931
+ return;
932
+ }
569
933
  break;
570
934
  case "left":
571
- cursorCol = "global";
935
+ cursorCol = colPrev(cursorCol);
572
936
  break;
573
937
  case "right":
574
- cursorCol = "local";
938
+ cursorCol = colNext(cursorCol);
575
939
  break;
576
940
  case "tab":
577
- cursorCol = cursorCol === "global" ? "local" : "global";
941
+ cursorCol = colNext(cursorCol);
578
942
  break;
579
943
  case "pagedown":
580
944
  cursor = Math.min(filteredIndices.length - 1, cursor + 10);
@@ -588,46 +952,25 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => {
588
952
  case "end":
589
953
  cursor = Math.max(0, filteredIndices.length - 1);
590
954
  break;
955
+ case "space":
591
956
  case "return": {
592
957
  const idx = currentSkillIndex();
593
958
  if (idx === null) break;
594
959
  const skill = allSkills[idx];
595
- if (cursorCol === "global") {
596
- const ok = toggle(GLOBAL_DIR, skill);
597
- if (ok) {
598
- const linked = isLibraryLink(GLOBAL_DIR, skill);
599
- setStatus(
600
- linked ? `✓ ${skill} → ~/.agents/skills` : `✗ ${skill} removed from ~/.agents/skills`,
601
- linked ? C.statusOk : C.statusErr
602
- );
603
- } else {
604
- setStatus(`⚠ ${skill}: non-library file exists in ~/.agents/skills, skipped`, C.warning);
605
- }
960
+ const { dir, label } = dirForCol(cursorCol);
961
+ const ok = toggle(dir, skill);
962
+ if (ok) {
963
+ const linked = isEnabled(dir, skill);
964
+ setStatus(
965
+ linked ? `✓ ${skill} → ${label}` : `✗ ${skill} removed from ${label}`,
966
+ linked ? C.statusOk : C.statusErr
967
+ );
606
968
  } else {
607
- const dir = ensureLocalDir();
608
- const ok = toggle(dir, skill);
609
- if (ok) {
610
- const linked = isLibraryLink(localDir, skill);
611
- setStatus(
612
- linked ? `✓ ${skill} → ${localLabel}` : `✗ ${skill} removed from ${localLabel}`,
613
- linked ? C.statusOk : C.statusErr
614
- );
615
- } else {
616
- setStatus(`⚠ ${skill}: non-library file exists in ${localLabel}, skipped`, C.warning);
617
- }
969
+ setStatus(`⚠ ${skill}: non-library file exists in ${label}, skipped`, C.warning);
618
970
  }
619
971
  break;
620
972
  }
621
973
  default:
622
- // ── Printable character → search input ──
623
- if (key.sequence && key.sequence.length === 1 && !key.meta) {
624
- searchQuery += key.sequence;
625
- applyFilter();
626
- cursor = 0;
627
- refreshAll();
628
- ensureVisible();
629
- return;
630
- }
631
974
  return;
632
975
  }
633
976