@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.
- package/bun.lock +1 -0
- package/index.ts +292 -74
- package/package.json +2 -1
package/bun.lock
CHANGED
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
|
-
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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,
|
|
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:
|
|
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 (
|
|
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:
|
|
313
|
+
width: COL_W,
|
|
212
314
|
});
|
|
213
|
-
|
|
214
|
-
|
|
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:
|
|
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:
|
|
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(
|
|
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:
|
|
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:
|
|
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(
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
417
|
-
|
|
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
|
|
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
|
-
// ──
|
|
526
|
-
if (
|
|
527
|
-
if (
|
|
528
|
-
|
|
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
|
|
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)
|
|
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 =
|
|
810
|
+
cursorCol = colPrev(cursorCol);
|
|
572
811
|
break;
|
|
573
812
|
case "right":
|
|
574
|
-
cursorCol =
|
|
813
|
+
cursorCol = colNext(cursorCol);
|
|
575
814
|
break;
|
|
576
815
|
case "tab":
|
|
577
|
-
cursorCol = cursorCol
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|