@dealdeploy/skl 1.10.1 → 1.10.2

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/add-tui.ts CHANGED
@@ -269,3 +269,294 @@ export function createAddTui(renderer: CliRenderer, deps: AddTuiDeps) {
269
269
  },
270
270
  };
271
271
  }
272
+
273
+ // ── Multi-repo add TUI ──────────────────────────────────────────────
274
+
275
+ export type AddSkillGroup = {
276
+ repo: string;
277
+ skills: AddSkillEntry[];
278
+ };
279
+
280
+ export type MultiRepoAddTuiDeps = {
281
+ groups: AddSkillGroup[];
282
+ onConfirm: (selected: Map<string, AddSkillEntry[]>) => Promise<void>;
283
+ onCancel: () => void;
284
+ };
285
+
286
+ type DisplayItem =
287
+ | { type: "header"; repo: string }
288
+ | { type: "skill"; skill: AddSkillEntry; groupIndex: number };
289
+
290
+ export function createMultiRepoAddTui(renderer: CliRenderer, deps: MultiRepoAddTuiDeps) {
291
+ const { groups } = deps;
292
+
293
+ const C = {
294
+ bg: "#1a1a2e",
295
+ rowBg: "#1a1a2e",
296
+ rowAltBg: "#1f1f38",
297
+ cursorBg: "#2a2a5a",
298
+ border: "#444477",
299
+ fg: "#ccccdd",
300
+ fgDim: "#666688",
301
+ checked: "#66ff88",
302
+ unchecked: "#555566",
303
+ warning: "#ffaa44",
304
+ accent: "#8888ff",
305
+ title: "#aaaaff",
306
+ footer: "#888899",
307
+ statusOk: "#66ff88",
308
+ statusErr: "#ff6666",
309
+ headerFg: "#9999bb",
310
+ };
311
+
312
+ // Build flat display list
313
+ const items: DisplayItem[] = [];
314
+ for (let gi = 0; gi < groups.length; gi++) {
315
+ const group = groups[gi]!;
316
+ items.push({ type: "header", repo: group.repo });
317
+ for (const skill of group.skills) {
318
+ items.push({ type: "skill", skill, groupIndex: gi });
319
+ }
320
+ }
321
+
322
+ let cursor = items.findIndex((it) => it.type === "skill");
323
+ if (cursor < 0) cursor = 0;
324
+ const checked = new Set<number>();
325
+
326
+ const outer = new BoxRenderable(renderer, {
327
+ id: "outer",
328
+ width: "100%",
329
+ height: "100%",
330
+ flexDirection: "column",
331
+ backgroundColor: C.bg,
332
+ });
333
+
334
+ const header = new TextRenderable(renderer, {
335
+ id: "header",
336
+ content: ` New skills (0 selected)`,
337
+ fg: C.title,
338
+ attributes: TextAttributes.BOLD,
339
+ height: 1,
340
+ });
341
+
342
+ const sep = new TextRenderable(renderer, {
343
+ id: "sep",
344
+ content: "\u2500".repeat(60),
345
+ fg: C.border,
346
+ height: 1,
347
+ });
348
+
349
+ const scrollBox = new ScrollBoxRenderable(renderer, {
350
+ id: "skill-list",
351
+ flexGrow: 1,
352
+ width: "100%",
353
+ });
354
+
355
+ type RowRef = {
356
+ row: BoxRenderable;
357
+ text: TextRenderable;
358
+ checkText?: TextRenderable;
359
+ };
360
+ const rows: RowRef[] = [];
361
+
362
+ for (let i = 0; i < items.length; i++) {
363
+ const item = items[i]!;
364
+
365
+ if (item.type === "header") {
366
+ const row = new BoxRenderable(renderer, {
367
+ id: `row-${i}`,
368
+ flexDirection: "row",
369
+ height: 1,
370
+ width: "100%",
371
+ paddingLeft: 1,
372
+ backgroundColor: C.bg,
373
+ });
374
+ const text = new TextRenderable(renderer, {
375
+ id: `text-${i}`,
376
+ content: ` ${item.repo}`,
377
+ fg: C.headerFg,
378
+ attributes: TextAttributes.BOLD,
379
+ });
380
+ row.add(text);
381
+ scrollBox.add(row);
382
+ rows.push({ row, text });
383
+ } else {
384
+ const row = new BoxRenderable(renderer, {
385
+ id: `row-${i}`,
386
+ flexDirection: "row",
387
+ height: 1,
388
+ width: "100%",
389
+ paddingLeft: 1,
390
+ backgroundColor: i % 2 === 0 ? C.rowBg : C.rowAltBg,
391
+ });
392
+ const checkText = new TextRenderable(renderer, {
393
+ id: `check-${i}`,
394
+ content: "[ ]",
395
+ fg: C.unchecked,
396
+ width: 4,
397
+ });
398
+ const text = new TextRenderable(renderer, {
399
+ id: `name-${i}`,
400
+ content: ` ${item.skill.name}`,
401
+ fg: C.fg,
402
+ width: 40,
403
+ });
404
+ row.add(checkText);
405
+ row.add(text);
406
+ scrollBox.add(row);
407
+ rows.push({ row, text, checkText });
408
+ }
409
+ }
410
+
411
+ const footerSep = new TextRenderable(renderer, {
412
+ id: "footer-sep",
413
+ content: "\u2500".repeat(60),
414
+ fg: C.border,
415
+ height: 1,
416
+ });
417
+
418
+ const footer = new TextRenderable(renderer, {
419
+ id: "footer",
420
+ content: " j/k move space toggle a all enter confirm q cancel",
421
+ fg: C.footer,
422
+ height: 1,
423
+ });
424
+
425
+ const statusLine = new TextRenderable(renderer, {
426
+ id: "status",
427
+ content: "",
428
+ fg: C.statusOk,
429
+ height: 1,
430
+ });
431
+
432
+ outer.add(header);
433
+ outer.add(sep);
434
+ outer.add(scrollBox);
435
+ outer.add(footerSep);
436
+ outer.add(footer);
437
+ outer.add(statusLine);
438
+ renderer.root.add(outer);
439
+
440
+ let statusTimeout: ReturnType<typeof setTimeout> | null = null;
441
+
442
+ function setStatus(msg: string, color: string) {
443
+ statusLine.content = ` ${msg}`;
444
+ statusLine.fg = color;
445
+ if (statusTimeout) clearTimeout(statusTimeout);
446
+ statusTimeout = setTimeout(() => {
447
+ statusLine.content = "";
448
+ }, 3000);
449
+ }
450
+
451
+ function updateHeader() {
452
+ header.content = ` New skills (${checked.size} selected)`;
453
+ }
454
+
455
+ function updateRow(i: number) {
456
+ const item = items[i]!;
457
+ const r = rows[i]!;
458
+ const isCursor = cursor === i;
459
+
460
+ if (item.type === "header") return;
461
+
462
+ const baseBg = i % 2 === 0 ? C.rowBg : C.rowAltBg;
463
+ r.row.backgroundColor = isCursor ? C.cursorBg : baseBg;
464
+
465
+ const isChecked = checked.has(i);
466
+ r.checkText!.content = isChecked ? "[x]" : "[ ]";
467
+ r.checkText!.fg = isCursor ? C.accent : (isChecked ? C.checked : C.unchecked);
468
+ const pointer = isCursor ? "\u25b8" : " ";
469
+ r.text.content = `${pointer} ${item.skill.name}`;
470
+ r.text.fg = isCursor ? "#ffffff" : C.fg;
471
+ r.text.attributes = isCursor ? TextAttributes.BOLD : TextAttributes.NONE;
472
+ }
473
+
474
+ function refreshAll() {
475
+ for (let i = 0; i < items.length; i++) updateRow(i);
476
+ updateHeader();
477
+ }
478
+
479
+ function ensureVisible() {
480
+ scrollBox.scrollTo(Math.max(0, cursor - 2));
481
+ }
482
+
483
+ function nextSkill(from: number, dir: 1 | -1): number {
484
+ let pos = from + dir;
485
+ while (pos >= 0 && pos < items.length) {
486
+ if (items[pos]!.type === "skill") return pos;
487
+ pos += dir;
488
+ }
489
+ return from;
490
+ }
491
+
492
+ refreshAll();
493
+
494
+ renderer.keyInput.on("keypress", (key: KeyEvent) => {
495
+ const prevCursor = cursor;
496
+
497
+ const navKey = (key.ctrl && { h: "left", j: "down", k: "up", l: "right" }[key.name]) || key.name;
498
+
499
+ switch (navKey) {
500
+ case "j":
501
+ case "down":
502
+ cursor = nextSkill(cursor, 1);
503
+ break;
504
+ case "k":
505
+ case "up":
506
+ cursor = nextSkill(cursor, -1);
507
+ break;
508
+ case "space": {
509
+ if (items[cursor]?.type !== "skill") break;
510
+ if (checked.has(cursor)) {
511
+ checked.delete(cursor);
512
+ } else {
513
+ checked.add(cursor);
514
+ }
515
+ break;
516
+ }
517
+ case "a": {
518
+ const skillIndices = items
519
+ .map((it, i) => (it.type === "skill" ? i : -1))
520
+ .filter((i) => i >= 0);
521
+ const allChecked = skillIndices.every((i) => checked.has(i));
522
+ if (allChecked) {
523
+ for (const i of skillIndices) checked.delete(i);
524
+ } else {
525
+ for (const i of skillIndices) checked.add(i);
526
+ }
527
+ for (const i of skillIndices) updateRow(i);
528
+ updateHeader();
529
+ ensureVisible();
530
+ return;
531
+ }
532
+ case "return": {
533
+ const selected = new Map<string, AddSkillEntry[]>();
534
+ for (const idx of checked) {
535
+ const item = items[idx]!;
536
+ if (item.type !== "skill") continue;
537
+ const repo = groups[item.groupIndex]!.repo;
538
+ const list = selected.get(repo) ?? [];
539
+ list.push(item.skill);
540
+ selected.set(repo, list);
541
+ }
542
+ if (selected.size === 0) {
543
+ setStatus("Nothing selected \u2014 use space to toggle", C.warning);
544
+ return;
545
+ }
546
+ deps.onConfirm(selected);
547
+ return;
548
+ }
549
+ case "q":
550
+ case "escape":
551
+ deps.onCancel();
552
+ return;
553
+ default:
554
+ return;
555
+ }
556
+
557
+ if (prevCursor !== cursor) updateRow(prevCursor);
558
+ updateRow(cursor);
559
+ updateHeader();
560
+ ensureVisible();
561
+ });
562
+ }
package/index.ts CHANGED
@@ -30,7 +30,8 @@ Usage:
30
30
  skl Open the interactive TUI
31
31
  skl add <repo> Add skills from a GitHub repo (interactive)
32
32
  skl add <repo> --all Add all skills from a repo (non-interactive)
33
- skl update Update all remote skills
33
+ skl update Update all remote skills
34
+ skl update --new, -n Update and discover new skills from repos
34
35
  skl migrate Migrate old symlinks to use the catalog
35
36
  skl migrate -l Migrate local project symlinks
36
37
  skl help Show this help message
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dealdeploy/skl",
3
- "version": "1.10.1",
3
+ "version": "1.10.2",
4
4
  "description": "TUI skill manager for Claude Code agents",
5
5
  "module": "index.ts",
6
6
  "bin": {
package/update.ts CHANGED
@@ -1,10 +1,13 @@
1
- import { rmSync, mkdirSync } from "fs";
1
+ import { rmSync, mkdirSync, lstatSync } from "fs";
2
2
  import { join } from "path";
3
3
  import {
4
4
  readLock, writeLock, catalogDir, downloadSkillFiles,
5
- groupByRepo, planUpdates, type TreeEntry,
5
+ groupByRepo, planUpdates, findSkillEntries, addToLock,
6
+ type TreeEntry, type LockEntry,
6
7
  } from "./lib.ts";
7
8
 
9
+ const newFlag = process.argv.includes("--new") || process.argv.includes("-n");
10
+
8
11
  const lock = readLock();
9
12
  const byRepo = groupByRepo(lock);
10
13
 
@@ -31,6 +34,9 @@ let totalUpToDate = 0;
31
34
  let totalErrors = 0;
32
35
  const CATALOG = catalogDir();
33
36
 
37
+ type RepoTreeInfo = { repo: string; branch: string; tree: TreeEntry[] };
38
+ const repoTrees: RepoTreeInfo[] = [];
39
+
34
40
  for (const [repo, skills] of byRepo) {
35
41
  process.stdout.write(`Checking ${repo}...`);
36
42
 
@@ -71,6 +77,8 @@ for (const [repo, skills] of byRepo) {
71
77
 
72
78
  console.log("");
73
79
 
80
+ if (newFlag) repoTrees.push({ repo, branch, tree });
81
+
74
82
  const plan = planUpdates(skills, tree);
75
83
 
76
84
  totalUpToDate += plan.upToDate.length;
@@ -104,3 +112,87 @@ if (totalUpdated > 0) parts.push(`Updated ${totalUpdated} skill(s)`);
104
112
  if (totalUpToDate > 0) parts.push(`${totalUpToDate} already up to date`);
105
113
  if (totalErrors > 0) parts.push(`${totalErrors} error(s)`);
106
114
  console.log(`\n${parts.join(", ")}`);
115
+
116
+ // ── New-skills phase ──────────────────────────────────────────────────
117
+ if (newFlag && repoTrees.length > 0) {
118
+ const { createMultiRepoAddTui } = await import("./add-tui.ts");
119
+
120
+ const freshLock = readLock();
121
+ const groups: { repo: string; branch: string; skills: import("./add-tui.ts").AddSkillEntry[] }[] = [];
122
+
123
+ for (const { repo, branch, tree } of repoTrees) {
124
+ const entries = findSkillEntries(tree);
125
+ const newSkills = entries.filter((e) => !freshLock.skills[e.name]);
126
+ if (newSkills.length === 0) continue;
127
+
128
+ groups.push({
129
+ repo,
130
+ branch,
131
+ skills: newSkills.map((e) => ({
132
+ name: e.name,
133
+ prefix: e.prefix,
134
+ treeSHA: e.treeSHA,
135
+ exists: false,
136
+ })),
137
+ });
138
+ }
139
+
140
+ if (groups.length === 0) {
141
+ console.log("\nNo new skills found.");
142
+ } else {
143
+ const total = groups.reduce((sum, g) => sum + g.skills.length, 0);
144
+ console.log(`\nFound ${total} new skill(s) across ${groups.length} repo(s).`);
145
+
146
+ const { createCliRenderer } = await import("@opentui/core");
147
+ const renderer = await createCliRenderer({ exitOnCtrlC: true });
148
+
149
+ await new Promise<void>((resolve) => {
150
+ createMultiRepoAddTui(renderer, {
151
+ groups,
152
+ async onConfirm(selected) {
153
+ renderer.destroy();
154
+ mkdirSync(CATALOG, { recursive: true });
155
+ let added = 0;
156
+
157
+ for (const [repoKey, skills] of selected) {
158
+ const repoInfo = repoTrees.find((r) => r.repo === repoKey)!;
159
+ for (const skill of skills) {
160
+ const skillDir = join(CATALOG, skill.name);
161
+ try {
162
+ if (lstatSync(skillDir).isSymbolicLink()) {
163
+ rmSync(skillDir, { force: true });
164
+ }
165
+ } catch {}
166
+ mkdirSync(skillDir, { recursive: true });
167
+
168
+ try {
169
+ const fileCount = await downloadSkillFiles(
170
+ repoKey, repoInfo.branch, skill.prefix, skillDir, repoInfo.tree,
171
+ );
172
+ const entry: LockEntry = {
173
+ source: repoKey,
174
+ sourceUrl: `https://github.com/${repoKey}`,
175
+ skillPath: skill.prefix,
176
+ treeSHA: skill.treeSHA,
177
+ addedAt: new Date().toISOString(),
178
+ };
179
+ addToLock(skill.name, entry);
180
+ console.log(` ${skill.name} — added (${fileCount} files)`);
181
+ added++;
182
+ } catch (e: any) {
183
+ console.log(` ${skill.name} — failed (${e.message})`);
184
+ }
185
+ }
186
+ }
187
+
188
+ console.log(`\nAdded ${added} new skill(s) to ~/.skl/catalog/`);
189
+ resolve();
190
+ },
191
+ onCancel() {
192
+ renderer.destroy();
193
+ resolve();
194
+ },
195
+ });
196
+ });
197
+ }
198
+ }
@@ -1,18 +0,0 @@
1
- name: Publish
2
-
3
- on:
4
- push:
5
- tags:
6
- - "v*"
7
-
8
- jobs:
9
- publish:
10
- runs-on: ubuntu-latest
11
- steps:
12
- - uses: actions/checkout@v4
13
- - uses: oven-sh/setup-bun@v2
14
- - run: bun install
15
- - run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
16
- env:
17
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
18
- - run: npm publish --access public