@dealdeploy/skl 0.1.5 → 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 +317 -104
- package/package.json +2 -1
package/bun.lock
CHANGED
package/index.ts
CHANGED
|
@@ -1,41 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { join as _join, dirname } from "path";
|
|
5
|
-
import { fileURLToPath } from "url";
|
|
6
|
-
|
|
7
|
-
const _pkgPath = _join(dirname(fileURLToPath(import.meta.url)), "package.json");
|
|
8
|
-
const _pkg = JSON.parse(readFileSync(_pkgPath, "utf8"));
|
|
9
|
-
|
|
10
|
-
// ── Flags ────────────────────────────────────────────────────────────
|
|
11
|
-
const arg = process.argv[2];
|
|
12
|
-
if (arg === "-v" || arg === "--version") {
|
|
13
|
-
console.log(`skl ${_pkg.version}`);
|
|
14
|
-
process.exit(0);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// ── Subcommand routing ───────────────────────────────────────────────
|
|
18
|
-
if (arg === "add") {
|
|
19
|
-
await import("./add.ts").catch((err) => {
|
|
20
|
-
console.error(`skl: ${err.message}`);
|
|
21
|
-
process.exit(1);
|
|
22
|
-
});
|
|
23
|
-
process.exit(0);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ── Dynamic import to ensure errors are visible ──────────────────────
|
|
27
|
-
const {
|
|
3
|
+
import {
|
|
28
4
|
createCliRenderer,
|
|
29
5
|
BoxRenderable,
|
|
30
6
|
TextRenderable,
|
|
31
7
|
ScrollBoxRenderable,
|
|
32
8
|
TextAttributes,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
process.exit(1);
|
|
36
|
-
});
|
|
37
|
-
type KeyEvent = import("@opentui/core").KeyEvent;
|
|
38
|
-
|
|
9
|
+
type KeyEvent,
|
|
10
|
+
} from "@opentui/core";
|
|
39
11
|
import {
|
|
40
12
|
readdirSync,
|
|
41
13
|
lstatSync,
|
|
@@ -45,14 +17,36 @@ import {
|
|
|
45
17
|
mkdirSync,
|
|
46
18
|
existsSync,
|
|
47
19
|
rmSync,
|
|
20
|
+
renameSync,
|
|
21
|
+
cpSync,
|
|
22
|
+
readSync,
|
|
48
23
|
} from "fs";
|
|
49
24
|
import { join, resolve } from "path";
|
|
50
25
|
import { homedir } from "os";
|
|
51
26
|
|
|
27
|
+
// @ts-ignore - bun supports JSON imports
|
|
28
|
+
const { version: VERSION } = await import("./package.json");
|
|
29
|
+
|
|
30
|
+
// ── Flags ────────────────────────────────────────────────────────────
|
|
31
|
+
const arg = process.argv[2];
|
|
32
|
+
if (arg === "-v" || arg === "--version") {
|
|
33
|
+
console.log(`skl ${VERSION}`);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Subcommand routing ───────────────────────────────────────────────
|
|
38
|
+
if (arg === "add") {
|
|
39
|
+
await import("./add.ts");
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
|
|
52
45
|
// ── Constants & helpers ─────────────────────────────────────────────
|
|
53
46
|
|
|
54
47
|
const LIBRARY = join(homedir(), "dotfiles/skills");
|
|
55
48
|
const GLOBAL_DIR = join(homedir(), ".agents/skills");
|
|
49
|
+
const GLOBAL_CLAUDE_DIR = join(homedir(), ".claude/skills");
|
|
56
50
|
|
|
57
51
|
if (!existsSync(LIBRARY)) {
|
|
58
52
|
console.error(`skl: library not found at ${LIBRARY}`);
|
|
@@ -67,15 +61,17 @@ function ellipsize(text: string, max: number): string {
|
|
|
67
61
|
return `${text.slice(0, max - 1)}…`;
|
|
68
62
|
}
|
|
69
63
|
|
|
70
|
-
// Safety: ensure
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
76
74
|
}
|
|
77
|
-
} catch {
|
|
78
|
-
// doesn't exist yet, that's fine — toggle will mkdir
|
|
79
75
|
}
|
|
80
76
|
|
|
81
77
|
function isLibraryLink(dir: string, name: string): boolean {
|
|
@@ -125,23 +121,110 @@ function ensureLocalDir(): string {
|
|
|
125
121
|
mkdirSync(dir, { recursive: true });
|
|
126
122
|
localDir = dir;
|
|
127
123
|
localLabel = dir.replace(homedir(), "~");
|
|
128
|
-
colLocal.content = ellipsize(localLabel,
|
|
124
|
+
colLocal.content = ellipsize(localLabel, COL_W - 1);
|
|
125
|
+
refreshAll();
|
|
126
|
+
return dir;
|
|
127
|
+
}
|
|
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);
|
|
129
142
|
refreshAll();
|
|
130
143
|
return dir;
|
|
131
144
|
}
|
|
132
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
|
+
|
|
133
211
|
const allSkills = readdirSync(LIBRARY).sort();
|
|
134
212
|
|
|
135
213
|
// ── State ───────────────────────────────────────────────────────────
|
|
136
214
|
|
|
215
|
+
type ColId = "global" | "globalClaude" | "local" | "localClaude";
|
|
216
|
+
|
|
137
217
|
let cursor = 0; // index into filteredIndices
|
|
138
|
-
let cursorCol:
|
|
218
|
+
let cursorCol: ColId = "global";
|
|
219
|
+
let focusArea: "search" | "grid" = "search";
|
|
139
220
|
let localDir = findLocalDir();
|
|
221
|
+
let localClaudeDir = findLocalClaudeDir();
|
|
140
222
|
let statusTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
141
223
|
let pendingDelete: number | null = null; // allSkills index awaiting confirmation
|
|
142
224
|
const deletedSkills = new Set<number>();
|
|
143
225
|
let searchQuery = "";
|
|
144
226
|
let filteredIndices: number[] = allSkills.map((_, i) => i);
|
|
227
|
+
const COL_ORDER: ColId[] = ["global", "globalClaude", "local", "localClaude"];
|
|
145
228
|
|
|
146
229
|
// ── Colors ──────────────────────────────────────────────────────────
|
|
147
230
|
|
|
@@ -190,15 +273,22 @@ const outer = new BoxRenderable(renderer, {
|
|
|
190
273
|
backgroundColor: C.bg,
|
|
191
274
|
});
|
|
192
275
|
|
|
193
|
-
// 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
|
+
});
|
|
194
285
|
const searchBar = new TextRenderable(renderer, {
|
|
195
286
|
id: "search-bar",
|
|
196
287
|
content: "",
|
|
197
288
|
fg: C.search,
|
|
198
|
-
width: "100%",
|
|
199
289
|
height: 1,
|
|
200
|
-
visible: true,
|
|
201
290
|
});
|
|
291
|
+
searchRow.add(searchBar);
|
|
202
292
|
|
|
203
293
|
const colHeaderRow = new BoxRenderable(renderer, {
|
|
204
294
|
id: "col-header-row",
|
|
@@ -214,31 +304,49 @@ const colName = new TextRenderable(renderer, {
|
|
|
214
304
|
attributes: TextAttributes.BOLD,
|
|
215
305
|
width: 34,
|
|
216
306
|
});
|
|
307
|
+
const COL_W = 14;
|
|
217
308
|
const colGlobal = new TextRenderable(renderer, {
|
|
218
309
|
id: "col-global",
|
|
219
310
|
content: "~/.agents",
|
|
220
311
|
fg: C.fgDim,
|
|
221
312
|
attributes: TextAttributes.BOLD,
|
|
222
|
-
width:
|
|
313
|
+
width: COL_W,
|
|
314
|
+
});
|
|
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,
|
|
223
321
|
});
|
|
224
|
-
let localLabel = localDir ? localDir.replace(homedir(), "~") : ".agents
|
|
225
|
-
const localLabelHeader = ellipsize(localLabel, 20);
|
|
322
|
+
let localLabel = localDir ? localDir.replace(homedir(), "~") : ".agents/skills";
|
|
226
323
|
const colLocal = new TextRenderable(renderer, {
|
|
227
324
|
id: "col-local",
|
|
228
|
-
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),
|
|
229
334
|
fg: C.fgDim,
|
|
230
335
|
attributes: TextAttributes.BOLD,
|
|
231
|
-
width:
|
|
336
|
+
width: COL_W,
|
|
232
337
|
});
|
|
233
338
|
|
|
234
339
|
colHeaderRow.add(colName);
|
|
235
340
|
colHeaderRow.add(colGlobal);
|
|
341
|
+
colHeaderRow.add(colGlobalClaude);
|
|
236
342
|
colHeaderRow.add(colLocal);
|
|
343
|
+
colHeaderRow.add(colLocalClaude);
|
|
237
344
|
|
|
238
345
|
const sep = new TextRenderable(renderer, {
|
|
239
346
|
id: "sep",
|
|
240
|
-
content: "─".repeat(
|
|
347
|
+
content: "─".repeat(200),
|
|
241
348
|
fg: C.border,
|
|
349
|
+
width: "100%",
|
|
242
350
|
height: 1,
|
|
243
351
|
});
|
|
244
352
|
|
|
@@ -252,10 +360,14 @@ type RowRefs = {
|
|
|
252
360
|
row: BoxRenderable;
|
|
253
361
|
nameText: TextRenderable;
|
|
254
362
|
globalText: TextRenderable;
|
|
363
|
+
globalClaudeText: TextRenderable;
|
|
255
364
|
localText: TextRenderable;
|
|
365
|
+
localClaudeText: TextRenderable;
|
|
256
366
|
};
|
|
257
367
|
const rows: RowRefs[] = [];
|
|
258
368
|
|
|
369
|
+
scrollBox.add(searchRow);
|
|
370
|
+
|
|
259
371
|
for (let i = 0; i < allSkills.length; i++) {
|
|
260
372
|
const skill = allSkills[i];
|
|
261
373
|
|
|
@@ -281,7 +393,16 @@ for (let i = 0; i < allSkills.length; i++) {
|
|
|
281
393
|
id: `global-${i}`,
|
|
282
394
|
content: checkboxStr(gChecked, gBlocked),
|
|
283
395
|
fg: checkboxColor(gChecked, gBlocked, false),
|
|
284
|
-
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,
|
|
285
406
|
});
|
|
286
407
|
|
|
287
408
|
const lChecked = localDir ? isLibraryLink(localDir, skill) : false;
|
|
@@ -290,26 +411,38 @@ for (let i = 0; i < allSkills.length; i++) {
|
|
|
290
411
|
id: `local-${i}`,
|
|
291
412
|
content: localDir ? checkboxStr(lChecked, lBlocked) : " -",
|
|
292
413
|
fg: localDir ? checkboxColor(lChecked, lBlocked, false) : C.fgDim,
|
|
293
|
-
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,
|
|
294
424
|
});
|
|
295
425
|
|
|
296
426
|
row.add(nameText);
|
|
297
427
|
row.add(globalText);
|
|
428
|
+
row.add(globalClaudeText);
|
|
298
429
|
row.add(localText);
|
|
430
|
+
row.add(localClaudeText);
|
|
299
431
|
scrollBox.add(row);
|
|
300
|
-
rows.push({ row, nameText, globalText, localText });
|
|
432
|
+
rows.push({ row, nameText, globalText, globalClaudeText, localText, localClaudeText });
|
|
301
433
|
}
|
|
302
434
|
|
|
303
435
|
const footerSep = new TextRenderable(renderer, {
|
|
304
436
|
id: "footer-sep",
|
|
305
|
-
content: "─".repeat(
|
|
437
|
+
content: "─".repeat(200),
|
|
306
438
|
fg: C.border,
|
|
439
|
+
width: "100%",
|
|
307
440
|
height: 1,
|
|
308
441
|
});
|
|
309
442
|
|
|
310
443
|
const footer = new TextRenderable(renderer, {
|
|
311
444
|
id: "footer",
|
|
312
|
-
content: "
|
|
445
|
+
content: " ↑↓ move ←→/tab col enter toggle ^a all ^e edit ^d del ↑search esc quit",
|
|
313
446
|
fg: C.footer,
|
|
314
447
|
height: 1,
|
|
315
448
|
});
|
|
@@ -321,7 +454,6 @@ const statusLine = new TextRenderable(renderer, {
|
|
|
321
454
|
height: 1,
|
|
322
455
|
});
|
|
323
456
|
|
|
324
|
-
outer.add(searchBar);
|
|
325
457
|
outer.add(colHeaderRow);
|
|
326
458
|
outer.add(sep);
|
|
327
459
|
outer.add(scrollBox);
|
|
@@ -353,7 +485,15 @@ function applyFilter() {
|
|
|
353
485
|
}
|
|
354
486
|
|
|
355
487
|
function updateSearchBar() {
|
|
356
|
-
|
|
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
|
+
}
|
|
357
497
|
}
|
|
358
498
|
|
|
359
499
|
// ── Update display ──────────────────────────────────────────────────
|
|
@@ -366,7 +506,7 @@ function updateRow(i: number) {
|
|
|
366
506
|
const skill = allSkills[i];
|
|
367
507
|
const r = rows[i];
|
|
368
508
|
const ci = currentSkillIndex();
|
|
369
|
-
const isCursor = ci === i;
|
|
509
|
+
const isCursor = ci === i && focusArea === "grid";
|
|
370
510
|
|
|
371
511
|
const visPos = filteredIndices.indexOf(i);
|
|
372
512
|
const baseBg = visPos % 2 === 0 ? C.rowBg : C.rowAltBg;
|
|
@@ -385,6 +525,14 @@ function updateRow(i: number) {
|
|
|
385
525
|
r.globalText.bg = gActive ? C.accentBg : undefined;
|
|
386
526
|
r.globalText.attributes = gActive ? TextAttributes.BOLD : TextAttributes.NONE;
|
|
387
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
|
+
|
|
388
536
|
const lChecked = localDir ? isLibraryLink(localDir, skill) : false;
|
|
389
537
|
const lBlocked = localDir ? nonLibraryExists(localDir, skill) : false;
|
|
390
538
|
const lActive = isCursor && cursorCol === "local";
|
|
@@ -398,6 +546,20 @@ function updateRow(i: number) {
|
|
|
398
546
|
r.localText.fg = C.fgDim;
|
|
399
547
|
r.localText.bg = lActive ? C.accentBg : undefined;
|
|
400
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
|
+
}
|
|
401
563
|
}
|
|
402
564
|
|
|
403
565
|
function setStatus(msg: string, color: string) {
|
|
@@ -419,13 +581,25 @@ refreshAll();
|
|
|
419
581
|
// ── Scrolling helper ────────────────────────────────────────────────
|
|
420
582
|
|
|
421
583
|
function ensureVisible() {
|
|
422
|
-
|
|
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
|
+
}
|
|
423
590
|
}
|
|
424
591
|
|
|
425
592
|
// ── Toggle all in column ────────────────────────────────────────────
|
|
426
593
|
|
|
427
|
-
function toggleAllColumn(col:
|
|
428
|
-
|
|
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
|
+
}
|
|
429
603
|
|
|
430
604
|
// Determine intent: if majority of visible skills are linked, unlink all; otherwise link all
|
|
431
605
|
let linkedCount = 0;
|
|
@@ -447,7 +621,6 @@ function toggleAllColumn(col: "global" | "local") {
|
|
|
447
621
|
}
|
|
448
622
|
}
|
|
449
623
|
|
|
450
|
-
const dirLabel = col === "global" ? "~/.agents" : localLabel;
|
|
451
624
|
const action = shouldLink ? "linked" : "unlinked";
|
|
452
625
|
let msg = `${action} ${changed} skills in ${dirLabel}`;
|
|
453
626
|
if (skipped) msg += ` (${skipped} skipped)`;
|
|
@@ -487,7 +660,9 @@ function deleteSkill(idx: number) {
|
|
|
487
660
|
|
|
488
661
|
// Remove symlinks first
|
|
489
662
|
if (isLibraryLink(GLOBAL_DIR, skill)) unlinkSync(join(GLOBAL_DIR, skill));
|
|
663
|
+
if (isLibraryLink(GLOBAL_CLAUDE_DIR, skill)) unlinkSync(join(GLOBAL_CLAUDE_DIR, skill));
|
|
490
664
|
if (localDir && isLibraryLink(localDir, skill)) unlinkSync(join(localDir, skill));
|
|
665
|
+
if (localClaudeDir && isLibraryLink(localClaudeDir, skill)) unlinkSync(join(localClaudeDir, skill));
|
|
491
666
|
|
|
492
667
|
// Remove from library
|
|
493
668
|
rmSync(join(LIBRARY, skill), { recursive: true, force: true });
|
|
@@ -505,6 +680,25 @@ function deleteSkill(idx: number) {
|
|
|
505
680
|
|
|
506
681
|
// ── Key handler ─────────────────────────────────────────────────────
|
|
507
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
|
+
|
|
508
702
|
renderer.keyInput.on("keypress", (key: KeyEvent) => {
|
|
509
703
|
// ── Delete confirmation mode ──
|
|
510
704
|
if (pendingDelete !== null) {
|
|
@@ -519,13 +713,17 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => {
|
|
|
519
713
|
|
|
520
714
|
const prevIdx = currentSkillIndex();
|
|
521
715
|
|
|
522
|
-
// ── Escape
|
|
716
|
+
// ── Escape ──
|
|
523
717
|
if (key.name === "escape") {
|
|
524
|
-
if (searchQuery) {
|
|
718
|
+
if (focusArea === "search" && searchQuery) {
|
|
525
719
|
searchQuery = "";
|
|
526
720
|
applyFilter();
|
|
527
721
|
refreshAll();
|
|
528
722
|
ensureVisible();
|
|
723
|
+
} else if (focusArea === "search") {
|
|
724
|
+
focusArea = "grid";
|
|
725
|
+
refreshAll();
|
|
726
|
+
ensureVisible();
|
|
529
727
|
} else {
|
|
530
728
|
renderer.destroy();
|
|
531
729
|
process.exit(0);
|
|
@@ -533,18 +731,41 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => {
|
|
|
533
731
|
return;
|
|
534
732
|
}
|
|
535
733
|
|
|
536
|
-
// ──
|
|
537
|
-
if (
|
|
538
|
-
if (
|
|
539
|
-
|
|
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;
|
|
540
755
|
applyFilter();
|
|
541
756
|
cursor = 0;
|
|
542
757
|
refreshAll();
|
|
543
758
|
ensureVisible();
|
|
759
|
+
return;
|
|
544
760
|
}
|
|
545
761
|
return;
|
|
546
762
|
}
|
|
547
763
|
|
|
764
|
+
// ── Grid-focused input ──
|
|
765
|
+
|
|
766
|
+
// ── Backspace in grid: do nothing ──
|
|
767
|
+
if (key.name === "backspace") return;
|
|
768
|
+
|
|
548
769
|
// ── Ctrl combos ──
|
|
549
770
|
if (key.ctrl) {
|
|
550
771
|
switch (key.name) {
|
|
@@ -570,22 +791,29 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => {
|
|
|
570
791
|
return;
|
|
571
792
|
}
|
|
572
793
|
|
|
573
|
-
// ── Navigation & actions
|
|
794
|
+
// ── Navigation & actions ──
|
|
574
795
|
switch (key.name) {
|
|
575
796
|
case "down":
|
|
576
797
|
if (cursor < filteredIndices.length - 1) cursor++;
|
|
577
798
|
break;
|
|
578
799
|
case "up":
|
|
579
|
-
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
|
+
}
|
|
580
808
|
break;
|
|
581
809
|
case "left":
|
|
582
|
-
cursorCol =
|
|
810
|
+
cursorCol = colPrev(cursorCol);
|
|
583
811
|
break;
|
|
584
812
|
case "right":
|
|
585
|
-
cursorCol =
|
|
813
|
+
cursorCol = colNext(cursorCol);
|
|
586
814
|
break;
|
|
587
815
|
case "tab":
|
|
588
|
-
cursorCol = cursorCol
|
|
816
|
+
cursorCol = colNext(cursorCol);
|
|
589
817
|
break;
|
|
590
818
|
case "pagedown":
|
|
591
819
|
cursor = Math.min(filteredIndices.length - 1, cursor + 10);
|
|
@@ -599,46 +827,25 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => {
|
|
|
599
827
|
case "end":
|
|
600
828
|
cursor = Math.max(0, filteredIndices.length - 1);
|
|
601
829
|
break;
|
|
830
|
+
case "space":
|
|
602
831
|
case "return": {
|
|
603
832
|
const idx = currentSkillIndex();
|
|
604
833
|
if (idx === null) break;
|
|
605
834
|
const skill = allSkills[idx];
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
} else {
|
|
615
|
-
setStatus(`⚠ ${skill}: non-library file exists in ~/.agents/skills, skipped`, C.warning);
|
|
616
|
-
}
|
|
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
|
+
);
|
|
617
843
|
} else {
|
|
618
|
-
|
|
619
|
-
const ok = toggle(dir, skill);
|
|
620
|
-
if (ok) {
|
|
621
|
-
const linked = isLibraryLink(localDir, skill);
|
|
622
|
-
setStatus(
|
|
623
|
-
linked ? `✓ ${skill} → ${localLabel}` : `✗ ${skill} removed from ${localLabel}`,
|
|
624
|
-
linked ? C.statusOk : C.statusErr
|
|
625
|
-
);
|
|
626
|
-
} else {
|
|
627
|
-
setStatus(`⚠ ${skill}: non-library file exists in ${localLabel}, skipped`, C.warning);
|
|
628
|
-
}
|
|
844
|
+
setStatus(`⚠ ${skill}: non-library file exists in ${label}, skipped`, C.warning);
|
|
629
845
|
}
|
|
630
846
|
break;
|
|
631
847
|
}
|
|
632
848
|
default:
|
|
633
|
-
// ── Printable character → search input ──
|
|
634
|
-
if (key.sequence && key.sequence.length === 1 && !key.meta) {
|
|
635
|
-
searchQuery += key.sequence;
|
|
636
|
-
applyFilter();
|
|
637
|
-
cursor = 0;
|
|
638
|
-
refreshAll();
|
|
639
|
-
ensureVisible();
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
849
|
return;
|
|
643
850
|
}
|
|
644
851
|
|
|
@@ -649,3 +856,9 @@ renderer.keyInput.on("keypress", (key: KeyEvent) => {
|
|
|
649
856
|
if (ci !== null) updateRow(ci);
|
|
650
857
|
ensureVisible();
|
|
651
858
|
});
|
|
859
|
+
|
|
860
|
+
} catch (err: any) {
|
|
861
|
+
console.error(`skl: fatal: ${err?.message ?? err}`);
|
|
862
|
+
if (err?.stack) console.error(err.stack);
|
|
863
|
+
process.exit(1);
|
|
864
|
+
}
|
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"
|