@dealdeploy/skl 0.1.6 → 0.1.7

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 (3) hide show
  1. package/bun.lock +1 -0
  2. package/index.ts +292 -74
  3. package/package.json +2 -1
package/bun.lock CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "lockfileVersion": 1,
3
+ "configVersion": 0,
3
4
  "workspaces": {
4
5
  "": {
5
6
  "name": "skills-manager",
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];
@@ -42,6 +46,7 @@ try {
42
46
 
43
47
  const LIBRARY = join(homedir(), "dotfiles/skills");
44
48
  const GLOBAL_DIR = join(homedir(), ".agents/skills");
49
+ const GLOBAL_CLAUDE_DIR = join(homedir(), ".claude/skills");
45
50
 
46
51
  if (!existsSync(LIBRARY)) {
47
52
  console.error(`skl: library not found at ${LIBRARY}`);
@@ -56,15 +61,17 @@ function ellipsize(text: string, max: number): string {
56
61
  return `${text.slice(0, max - 1)}…`;
57
62
  }
58
63
 
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);
64
+ // Safety: ensure skill dirs aren't symlinks pointing at the library itself
65
+ for (const [dir, label] of [[GLOBAL_DIR, "~/.agents/skills"], [GLOBAL_CLAUDE_DIR, "~/.claude/skills"]] as const) {
66
+ try {
67
+ if (lstatSync(dir).isSymbolicLink() && resolve(readlinkSync(dir)) === resolve(LIBRARY)) {
68
+ console.error(`ERROR: ${label} is a symlink to the library it must be a real directory.`);
69
+ console.error(`Remove it and create a real directory: rm ${label} && mkdir ${label}`);
70
+ process.exit(1);
71
+ }
72
+ } catch {
73
+ // doesn't exist yet, that's fine — toggle will mkdir
65
74
  }
66
- } catch {
67
- // doesn't exist yet, that's fine — toggle will mkdir
68
75
  }
69
76
 
70
77
  function isLibraryLink(dir: string, name: string): boolean {
@@ -114,23 +121,110 @@ function ensureLocalDir(): string {
114
121
  mkdirSync(dir, { recursive: true });
115
122
  localDir = dir;
116
123
  localLabel = dir.replace(homedir(), "~");
117
- colLocal.content = ellipsize(localLabel, 20);
124
+ colLocal.content = ellipsize(localLabel, COL_W - 1);
118
125
  refreshAll();
119
126
  return dir;
120
127
  }
121
128
 
129
+ function findLocalClaudeDir(): string | null {
130
+ const dir = join(process.cwd(), ".claude/skills");
131
+ if (existsSync(dir)) return dir;
132
+ return null;
133
+ }
134
+
135
+ function ensureLocalClaudeDir(): string {
136
+ if (localClaudeDir) return localClaudeDir;
137
+ const dir = join(process.cwd(), ".claude/skills");
138
+ mkdirSync(dir, { recursive: true });
139
+ localClaudeDir = dir;
140
+ localClaudeLabel = dir.replace(homedir(), "~");
141
+ colLocalClaude.content = ellipsize(localClaudeLabel, COL_W - 1);
142
+ refreshAll();
143
+ return dir;
144
+ }
145
+
146
+ // ── Migration check ──────────────────────────────────────────────────
147
+ {
148
+ const dirsToCheck: [string, string][] = [
149
+ [GLOBAL_DIR, "~/.agents/skills"],
150
+ [GLOBAL_CLAUDE_DIR, "~/.claude/skills"],
151
+ ];
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
+
157
+ const migrations: { dir: string; label: string; name: string; conflict: boolean }[] = [];
158
+
159
+ for (const [dir, label] of dirsToCheck) {
160
+ try {
161
+ for (const name of readdirSync(dir)) {
162
+ if (!isLibraryLink(dir, name)) {
163
+ migrations.push({ dir, label, name, conflict: existsSync(join(LIBRARY, name)) });
164
+ }
165
+ }
166
+ } catch {
167
+ // dir doesn't exist
168
+ }
169
+ }
170
+
171
+ if (migrations.length > 0) {
172
+ console.log("\nFound skills not managed by library:");
173
+ for (const m of migrations) {
174
+ console.log(` ${m.label}/${m.name}${m.conflict ? " (conflict — already in library)" : ""}`);
175
+ }
176
+ process.stdout.write(`\nMigrate to ${LIBRARY.replace(homedir(), "~")} and replace with symlinks? [y/N] `);
177
+
178
+ const buf = new Uint8Array(100);
179
+ const n = readSync(0, buf);
180
+ const answer = new TextDecoder().decode(buf.subarray(0, n)).trim().toLowerCase();
181
+
182
+ if (answer === "y" || answer === "yes") {
183
+ let migrated = 0;
184
+ let skipped = 0;
185
+ for (const m of migrations) {
186
+ const src = join(m.dir, m.name);
187
+ const dest = join(LIBRARY, m.name);
188
+ 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 });
198
+ rmSync(src, { recursive: true, force: true });
199
+ }
200
+ symlinkSync(dest, src);
201
+ console.log(` migrated ${m.name}`);
202
+ migrated++;
203
+ }
204
+ console.log(`\n${migrated} migrated${skipped ? `, ${skipped} skipped` : ""}\n`);
205
+ } else {
206
+ console.log("Skipping migration.\n");
207
+ }
208
+ }
209
+ }
210
+
122
211
  const allSkills = readdirSync(LIBRARY).sort();
123
212
 
124
213
  // ── State ───────────────────────────────────────────────────────────
125
214
 
215
+ type ColId = "global" | "globalClaude" | "local" | "localClaude";
216
+
126
217
  let cursor = 0; // index into filteredIndices
127
- let cursorCol: "global" | "local" = "global";
218
+ let cursorCol: ColId = "global";
219
+ let focusArea: "search" | "grid" = "search";
128
220
  let localDir = findLocalDir();
221
+ let localClaudeDir = findLocalClaudeDir();
129
222
  let statusTimeout: ReturnType<typeof setTimeout> | null = null;
130
223
  let pendingDelete: number | null = null; // allSkills index awaiting confirmation
131
224
  const deletedSkills = new Set<number>();
132
225
  let searchQuery = "";
133
226
  let filteredIndices: number[] = allSkills.map((_, i) => i);
227
+ const COL_ORDER: ColId[] = ["global", "globalClaude", "local", "localClaude"];
134
228
 
135
229
  // ── Colors ──────────────────────────────────────────────────────────
136
230
 
@@ -179,15 +273,22 @@ const outer = new BoxRenderable(renderer, {
179
273
  backgroundColor: C.bg,
180
274
  });
181
275
 
182
- // Search bar (always visible at top, type-to-filter)
276
+ // Search bar (lives inside scrollBox as the first row)
277
+ const searchRow = new BoxRenderable(renderer, {
278
+ id: "search-row",
279
+ flexDirection: "row",
280
+ height: 1,
281
+ width: "100%",
282
+ paddingLeft: 1,
283
+ backgroundColor: C.rowBg,
284
+ });
183
285
  const searchBar = new TextRenderable(renderer, {
184
286
  id: "search-bar",
185
287
  content: "",
186
288
  fg: C.search,
187
- width: "100%",
188
289
  height: 1,
189
- visible: true,
190
290
  });
291
+ searchRow.add(searchBar);
191
292
 
192
293
  const colHeaderRow = new BoxRenderable(renderer, {
193
294
  id: "col-header-row",
@@ -203,31 +304,49 @@ const colName = new TextRenderable(renderer, {
203
304
  attributes: TextAttributes.BOLD,
204
305
  width: 34,
205
306
  });
307
+ const COL_W = 14;
206
308
  const colGlobal = new TextRenderable(renderer, {
207
309
  id: "col-global",
208
310
  content: "~/.agents",
209
311
  fg: C.fgDim,
210
312
  attributes: TextAttributes.BOLD,
211
- width: 12,
313
+ width: COL_W,
212
314
  });
213
- let localLabel = localDir ? localDir.replace(homedir(), "~") : ".agents (n/a)";
214
- const localLabelHeader = ellipsize(localLabel, 20);
315
+ const colGlobalClaude = new TextRenderable(renderer, {
316
+ id: "col-global-claude",
317
+ content: "~/.claude",
318
+ fg: C.fgDim,
319
+ attributes: TextAttributes.BOLD,
320
+ width: COL_W,
321
+ });
322
+ let localLabel = localDir ? localDir.replace(homedir(), "~") : ".agents/skills";
215
323
  const colLocal = new TextRenderable(renderer, {
216
324
  id: "col-local",
217
- content: localLabelHeader,
325
+ content: ellipsize(localLabel, COL_W - 1),
326
+ fg: C.fgDim,
327
+ attributes: TextAttributes.BOLD,
328
+ width: COL_W,
329
+ });
330
+ let localClaudeLabel = localClaudeDir ? localClaudeDir.replace(homedir(), "~") : ".claude/skills";
331
+ const colLocalClaude = new TextRenderable(renderer, {
332
+ id: "col-local-claude",
333
+ content: ellipsize(localClaudeLabel, COL_W - 1),
218
334
  fg: C.fgDim,
219
335
  attributes: TextAttributes.BOLD,
220
- width: 20,
336
+ width: COL_W,
221
337
  });
222
338
 
223
339
  colHeaderRow.add(colName);
224
340
  colHeaderRow.add(colGlobal);
341
+ colHeaderRow.add(colGlobalClaude);
225
342
  colHeaderRow.add(colLocal);
343
+ colHeaderRow.add(colLocalClaude);
226
344
 
227
345
  const sep = new TextRenderable(renderer, {
228
346
  id: "sep",
229
- content: "─".repeat(60),
347
+ content: "─".repeat(200),
230
348
  fg: C.border,
349
+ width: "100%",
231
350
  height: 1,
232
351
  });
233
352
 
@@ -241,10 +360,14 @@ type RowRefs = {
241
360
  row: BoxRenderable;
242
361
  nameText: TextRenderable;
243
362
  globalText: TextRenderable;
363
+ globalClaudeText: TextRenderable;
244
364
  localText: TextRenderable;
365
+ localClaudeText: TextRenderable;
245
366
  };
246
367
  const rows: RowRefs[] = [];
247
368
 
369
+ scrollBox.add(searchRow);
370
+
248
371
  for (let i = 0; i < allSkills.length; i++) {
249
372
  const skill = allSkills[i];
250
373
 
@@ -270,7 +393,16 @@ for (let i = 0; i < allSkills.length; i++) {
270
393
  id: `global-${i}`,
271
394
  content: checkboxStr(gChecked, gBlocked),
272
395
  fg: checkboxColor(gChecked, gBlocked, false),
273
- width: 10,
396
+ width: COL_W,
397
+ });
398
+
399
+ const gcChecked = isLibraryLink(GLOBAL_CLAUDE_DIR, skill);
400
+ const gcBlocked = nonLibraryExists(GLOBAL_CLAUDE_DIR, skill);
401
+ const globalClaudeText = new TextRenderable(renderer, {
402
+ id: `global-claude-${i}`,
403
+ content: checkboxStr(gcChecked, gcBlocked),
404
+ fg: checkboxColor(gcChecked, gcBlocked, false),
405
+ width: COL_W,
274
406
  });
275
407
 
276
408
  const lChecked = localDir ? isLibraryLink(localDir, skill) : false;
@@ -279,26 +411,38 @@ for (let i = 0; i < allSkills.length; i++) {
279
411
  id: `local-${i}`,
280
412
  content: localDir ? checkboxStr(lChecked, lBlocked) : " -",
281
413
  fg: localDir ? checkboxColor(lChecked, lBlocked, false) : C.fgDim,
282
- width: 10,
414
+ width: COL_W,
415
+ });
416
+
417
+ const lcChecked = localClaudeDir ? isLibraryLink(localClaudeDir, skill) : false;
418
+ const lcBlocked = localClaudeDir ? nonLibraryExists(localClaudeDir, skill) : false;
419
+ const localClaudeText = new TextRenderable(renderer, {
420
+ id: `local-claude-${i}`,
421
+ content: localClaudeDir ? checkboxStr(lcChecked, lcBlocked) : " -",
422
+ fg: localClaudeDir ? checkboxColor(lcChecked, lcBlocked, false) : C.fgDim,
423
+ width: COL_W,
283
424
  });
284
425
 
285
426
  row.add(nameText);
286
427
  row.add(globalText);
428
+ row.add(globalClaudeText);
287
429
  row.add(localText);
430
+ row.add(localClaudeText);
288
431
  scrollBox.add(row);
289
- rows.push({ row, nameText, globalText, localText });
432
+ rows.push({ row, nameText, globalText, globalClaudeText, localText, localClaudeText });
290
433
  }
291
434
 
292
435
  const footerSep = new TextRenderable(renderer, {
293
436
  id: "footer-sep",
294
- content: "─".repeat(60),
437
+ content: "─".repeat(200),
295
438
  fg: C.border,
439
+ width: "100%",
296
440
  height: 1,
297
441
  });
298
442
 
299
443
  const footer = new TextRenderable(renderer, {
300
444
  id: "footer",
301
- content: " type to filter ↑↓ move ←→/tab col enter toggle ^a all ^e edit ^d del esc quit",
445
+ content: " ↑↓ move ←→/tab col enter toggle ^a all ^e edit ^d del ↑search esc quit",
302
446
  fg: C.footer,
303
447
  height: 1,
304
448
  });
@@ -310,7 +454,6 @@ const statusLine = new TextRenderable(renderer, {
310
454
  height: 1,
311
455
  });
312
456
 
313
- outer.add(searchBar);
314
457
  outer.add(colHeaderRow);
315
458
  outer.add(sep);
316
459
  outer.add(scrollBox);
@@ -342,7 +485,15 @@ function applyFilter() {
342
485
  }
343
486
 
344
487
  function updateSearchBar() {
345
- searchBar.content = ` / ${searchQuery}█`;
488
+ if (focusArea === "search") {
489
+ searchBar.content = `▸ ${searchQuery}█`;
490
+ searchBar.fg = C.search;
491
+ searchRow.backgroundColor = C.cursorBg;
492
+ } else {
493
+ searchBar.content = searchQuery ? ` ${searchQuery}` : ` search...`;
494
+ searchBar.fg = C.fgDim;
495
+ searchRow.backgroundColor = C.rowBg;
496
+ }
346
497
  }
347
498
 
348
499
  // ── Update display ──────────────────────────────────────────────────
@@ -355,7 +506,7 @@ function updateRow(i: number) {
355
506
  const skill = allSkills[i];
356
507
  const r = rows[i];
357
508
  const ci = currentSkillIndex();
358
- const isCursor = ci === i;
509
+ const isCursor = ci === i && focusArea === "grid";
359
510
 
360
511
  const visPos = filteredIndices.indexOf(i);
361
512
  const baseBg = visPos % 2 === 0 ? C.rowBg : C.rowAltBg;
@@ -374,6 +525,14 @@ function updateRow(i: number) {
374
525
  r.globalText.bg = gActive ? C.accentBg : undefined;
375
526
  r.globalText.attributes = gActive ? TextAttributes.BOLD : TextAttributes.NONE;
376
527
 
528
+ const gcChecked = isLibraryLink(GLOBAL_CLAUDE_DIR, skill);
529
+ const gcBlocked = nonLibraryExists(GLOBAL_CLAUDE_DIR, skill);
530
+ const gcActive = isCursor && cursorCol === "globalClaude";
531
+ r.globalClaudeText.content = checkboxStr(gcChecked, gcBlocked);
532
+ r.globalClaudeText.fg = checkboxColor(gcChecked, gcBlocked, gcActive);
533
+ r.globalClaudeText.bg = gcActive ? C.accentBg : undefined;
534
+ r.globalClaudeText.attributes = gcActive ? TextAttributes.BOLD : TextAttributes.NONE;
535
+
377
536
  const lChecked = localDir ? isLibraryLink(localDir, skill) : false;
378
537
  const lBlocked = localDir ? nonLibraryExists(localDir, skill) : false;
379
538
  const lActive = isCursor && cursorCol === "local";
@@ -387,6 +546,20 @@ function updateRow(i: number) {
387
546
  r.localText.fg = C.fgDim;
388
547
  r.localText.bg = lActive ? C.accentBg : undefined;
389
548
  }
549
+
550
+ const lcChecked = localClaudeDir ? isLibraryLink(localClaudeDir, skill) : false;
551
+ const lcBlocked = localClaudeDir ? nonLibraryExists(localClaudeDir, skill) : false;
552
+ const lcActive = isCursor && cursorCol === "localClaude";
553
+ if (localClaudeDir) {
554
+ r.localClaudeText.content = checkboxStr(lcChecked, lcBlocked);
555
+ r.localClaudeText.fg = checkboxColor(lcChecked, lcBlocked, lcActive);
556
+ r.localClaudeText.bg = lcActive ? C.accentBg : undefined;
557
+ r.localClaudeText.attributes = lcActive ? TextAttributes.BOLD : TextAttributes.NONE;
558
+ } else {
559
+ r.localClaudeText.content = " -";
560
+ r.localClaudeText.fg = C.fgDim;
561
+ r.localClaudeText.bg = lcActive ? C.accentBg : undefined;
562
+ }
390
563
  }
391
564
 
392
565
  function setStatus(msg: string, color: string) {
@@ -408,13 +581,25 @@ refreshAll();
408
581
  // ── Scrolling helper ────────────────────────────────────────────────
409
582
 
410
583
  function ensureVisible() {
411
- scrollBox.scrollTo(Math.max(0, cursor - 2));
584
+ if (focusArea === "search") {
585
+ scrollBox.scrollTo(0);
586
+ } else {
587
+ // +1 offset because searchRow is child 0 in scrollBox
588
+ scrollBox.scrollTo(Math.max(0, cursor + 1 - 2));
589
+ }
412
590
  }
413
591
 
414
592
  // ── Toggle all in column ────────────────────────────────────────────
415
593
 
416
- function toggleAllColumn(col: "global" | "local") {
417
- const dir = col === "global" ? GLOBAL_DIR : ensureLocalDir();
594
+ function toggleAllColumn(col: ColId) {
595
+ let dir: string;
596
+ let dirLabel: string;
597
+ switch (col) {
598
+ case "global": dir = GLOBAL_DIR; dirLabel = "~/.agents"; break;
599
+ case "globalClaude": dir = GLOBAL_CLAUDE_DIR; dirLabel = "~/.claude"; break;
600
+ case "local": dir = ensureLocalDir(); dirLabel = localLabel; break;
601
+ case "localClaude": dir = ensureLocalClaudeDir(); dirLabel = localClaudeLabel; break;
602
+ }
418
603
 
419
604
  // Determine intent: if majority of visible skills are linked, unlink all; otherwise link all
420
605
  let linkedCount = 0;
@@ -436,7 +621,6 @@ function toggleAllColumn(col: "global" | "local") {
436
621
  }
437
622
  }
438
623
 
439
- const dirLabel = col === "global" ? "~/.agents" : localLabel;
440
624
  const action = shouldLink ? "linked" : "unlinked";
441
625
  let msg = `${action} ${changed} skills in ${dirLabel}`;
442
626
  if (skipped) msg += ` (${skipped} skipped)`;
@@ -476,7 +660,9 @@ function deleteSkill(idx: number) {
476
660
 
477
661
  // Remove symlinks first
478
662
  if (isLibraryLink(GLOBAL_DIR, skill)) unlinkSync(join(GLOBAL_DIR, skill));
663
+ if (isLibraryLink(GLOBAL_CLAUDE_DIR, skill)) unlinkSync(join(GLOBAL_CLAUDE_DIR, skill));
479
664
  if (localDir && isLibraryLink(localDir, skill)) unlinkSync(join(localDir, skill));
665
+ if (localClaudeDir && isLibraryLink(localClaudeDir, skill)) unlinkSync(join(localClaudeDir, skill));
480
666
 
481
667
  // Remove from library
482
668
  rmSync(join(LIBRARY, skill), { recursive: true, force: true });
@@ -494,6 +680,25 @@ function deleteSkill(idx: number) {
494
680
 
495
681
  // ── Key handler ─────────────────────────────────────────────────────
496
682
 
683
+ function dirForCol(col: ColId): { dir: string; label: string } {
684
+ switch (col) {
685
+ case "global": return { dir: GLOBAL_DIR, label: "~/.agents/skills" };
686
+ case "globalClaude": return { dir: GLOBAL_CLAUDE_DIR, label: "~/.claude/skills" };
687
+ case "local": return { dir: ensureLocalDir(), label: localLabel };
688
+ case "localClaude": return { dir: ensureLocalClaudeDir(), label: localClaudeLabel };
689
+ }
690
+ }
691
+
692
+ function colNext(col: ColId): ColId {
693
+ const i = COL_ORDER.indexOf(col);
694
+ return COL_ORDER[(i + 1) % COL_ORDER.length];
695
+ }
696
+
697
+ function colPrev(col: ColId): ColId {
698
+ const i = COL_ORDER.indexOf(col);
699
+ return COL_ORDER[(i - 1 + COL_ORDER.length) % COL_ORDER.length];
700
+ }
701
+
497
702
  renderer.keyInput.on("keypress", (key: KeyEvent) => {
498
703
  // ── Delete confirmation mode ──
499
704
  if (pendingDelete !== null) {
@@ -508,13 +713,17 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => {
508
713
 
509
714
  const prevIdx = currentSkillIndex();
510
715
 
511
- // ── Escape: clear search or quit ──
716
+ // ── Escape ──
512
717
  if (key.name === "escape") {
513
- if (searchQuery) {
718
+ if (focusArea === "search" && searchQuery) {
514
719
  searchQuery = "";
515
720
  applyFilter();
516
721
  refreshAll();
517
722
  ensureVisible();
723
+ } else if (focusArea === "search") {
724
+ focusArea = "grid";
725
+ refreshAll();
726
+ ensureVisible();
518
727
  } else {
519
728
  renderer.destroy();
520
729
  process.exit(0);
@@ -522,18 +731,41 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => {
522
731
  return;
523
732
  }
524
733
 
525
- // ── Backspace: delete from search ──
526
- if (key.name === "backspace") {
527
- if (searchQuery) {
528
- searchQuery = searchQuery.slice(0, -1);
734
+ // ── Search-focused input ──
735
+ if (focusArea === "search") {
736
+ if (key.name === "backspace") {
737
+ if (searchQuery) {
738
+ searchQuery = searchQuery.slice(0, -1);
739
+ applyFilter();
740
+ cursor = 0;
741
+ refreshAll();
742
+ ensureVisible();
743
+ }
744
+ return;
745
+ }
746
+ if (key.name === "down" || key.name === "return") {
747
+ focusArea = "grid";
748
+ refreshAll();
749
+ ensureVisible();
750
+ return;
751
+ }
752
+ // Printable character → append to search
753
+ if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
754
+ searchQuery += key.sequence;
529
755
  applyFilter();
530
756
  cursor = 0;
531
757
  refreshAll();
532
758
  ensureVisible();
759
+ return;
533
760
  }
534
761
  return;
535
762
  }
536
763
 
764
+ // ── Grid-focused input ──
765
+
766
+ // ── Backspace in grid: do nothing ──
767
+ if (key.name === "backspace") return;
768
+
537
769
  // ── Ctrl combos ──
538
770
  if (key.ctrl) {
539
771
  switch (key.name) {
@@ -559,22 +791,29 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => {
559
791
  return;
560
792
  }
561
793
 
562
- // ── Navigation & actions (arrow keys, enter, tab) ──
794
+ // ── Navigation & actions ──
563
795
  switch (key.name) {
564
796
  case "down":
565
797
  if (cursor < filteredIndices.length - 1) cursor++;
566
798
  break;
567
799
  case "up":
568
- if (cursor > 0) cursor--;
800
+ if (cursor > 0) {
801
+ cursor--;
802
+ } else {
803
+ // At top of grid → move to search
804
+ focusArea = "search";
805
+ refreshAll();
806
+ return;
807
+ }
569
808
  break;
570
809
  case "left":
571
- cursorCol = "global";
810
+ cursorCol = colPrev(cursorCol);
572
811
  break;
573
812
  case "right":
574
- cursorCol = "local";
813
+ cursorCol = colNext(cursorCol);
575
814
  break;
576
815
  case "tab":
577
- cursorCol = cursorCol === "global" ? "local" : "global";
816
+ cursorCol = colNext(cursorCol);
578
817
  break;
579
818
  case "pagedown":
580
819
  cursor = Math.min(filteredIndices.length - 1, cursor + 10);
@@ -588,46 +827,25 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => {
588
827
  case "end":
589
828
  cursor = Math.max(0, filteredIndices.length - 1);
590
829
  break;
830
+ case "space":
591
831
  case "return": {
592
832
  const idx = currentSkillIndex();
593
833
  if (idx === null) break;
594
834
  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
- }
835
+ const { dir, label } = dirForCol(cursorCol);
836
+ const ok = toggle(dir, skill);
837
+ if (ok) {
838
+ const linked = isLibraryLink(dir, skill);
839
+ setStatus(
840
+ linked ? `✓ ${skill} → ${label}` : `✗ ${skill} removed from ${label}`,
841
+ linked ? C.statusOk : C.statusErr
842
+ );
606
843
  } 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
- }
844
+ setStatus(`⚠ ${skill}: non-library file exists in ${label}, skipped`, C.warning);
618
845
  }
619
846
  break;
620
847
  }
621
848
  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
849
  return;
632
850
  }
633
851
 
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@dealdeploy/skl",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "TUI skill manager for Claude Code agents",
5
5
  "module": "index.ts",
6
6
  "bin": {
7
7
  "skl": "index.ts"
8
8
  },
9
9
  "scripts": {
10
+ "dev": "bun run index.ts",
10
11
  "release": "npm version patch && git push && git push --tags",
11
12
  "release:minor": "npm version minor && git push && git push --tags",
12
13
  "release:major": "npm version major && git push && git push --tags"