@dealdeploy/skl 1.0.0 → 1.1.0
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.test.ts +338 -0
- package/add-tui.ts +269 -0
- package/add.ts +68 -306
- package/index.ts +18 -20
- package/lib.test.ts +446 -85
- package/lib.ts +116 -0
- package/package.json +1 -1
- package/skills-lock.json +10 -0
- package/update.ts +28 -43
package/add.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import { existsSync, lstatSync, mkdirSync, rmSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
|
+
import { createCliRenderer } from "@opentui/core";
|
|
3
4
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
type
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
catalogDir,
|
|
6
|
+
addToLock,
|
|
7
|
+
downloadSkillFiles,
|
|
8
|
+
findSkillEntries,
|
|
9
|
+
parseRepoArg,
|
|
10
|
+
type LockEntry,
|
|
11
|
+
type TreeEntry,
|
|
12
|
+
} from "./lib.ts";
|
|
13
|
+
import { createAddTui, type AddSkillEntry } from "./add-tui.ts";
|
|
12
14
|
|
|
13
15
|
const CATALOG = catalogDir();
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (!repo || !/^[^/]+\/[^/]+$/.test(repo)) {
|
|
21
|
-
console.error("Usage: skl add owner/repo");
|
|
17
|
+
const allFlag = process.argv.includes("--all");
|
|
18
|
+
const repoArg = process.argv.slice(3).find((a) => !a.startsWith("-")) ?? "";
|
|
19
|
+
const repo = parseRepoArg(repoArg);
|
|
20
|
+
if (!repo) {
|
|
21
|
+
console.error("Usage: skl add owner/repo [--all]");
|
|
22
22
|
process.exit(1);
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -47,27 +47,6 @@ function skillExists(name: string): boolean {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
// ── Colors ───────────────────────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
const C = {
|
|
53
|
-
bg: "#1a1a2e",
|
|
54
|
-
rowBg: "#1a1a2e",
|
|
55
|
-
rowAltBg: "#1f1f38",
|
|
56
|
-
cursorBg: "#2a2a5a",
|
|
57
|
-
accentBg: "#3a3a7a",
|
|
58
|
-
border: "#444477",
|
|
59
|
-
fg: "#ccccdd",
|
|
60
|
-
fgDim: "#666688",
|
|
61
|
-
checked: "#66ff88",
|
|
62
|
-
unchecked: "#555566",
|
|
63
|
-
warning: "#ffaa44",
|
|
64
|
-
accent: "#8888ff",
|
|
65
|
-
title: "#aaaaff",
|
|
66
|
-
footer: "#888899",
|
|
67
|
-
statusOk: "#66ff88",
|
|
68
|
-
statusErr: "#ff6666",
|
|
69
|
-
};
|
|
70
|
-
|
|
71
50
|
// ── Fetch phase ─────────────────────────────────────────────────────
|
|
72
51
|
|
|
73
52
|
console.log(`Fetching ${repo}...`);
|
|
@@ -84,21 +63,9 @@ try {
|
|
|
84
63
|
const treeJson = JSON.parse(
|
|
85
64
|
await gh(`repos/${repo}/git/trees/${branch}?recursive=1`)
|
|
86
65
|
);
|
|
87
|
-
const tree:
|
|
66
|
+
const tree: TreeEntry[] = treeJson.tree;
|
|
88
67
|
|
|
89
|
-
|
|
90
|
-
const skillEntries = tree
|
|
91
|
-
.filter((e) => e.type === "blob" && e.path.endsWith("/SKILL.md"))
|
|
92
|
-
.map((e) => {
|
|
93
|
-
const prefix = e.path.replace(/\/SKILL\.md$/, "");
|
|
94
|
-
const name = prefix.split("/").pop()!;
|
|
95
|
-
// Find the tree SHA for this skill's directory
|
|
96
|
-
const dirEntry = tree.find(
|
|
97
|
-
(t) => t.path === prefix && t.type === "tree"
|
|
98
|
-
);
|
|
99
|
-
return { prefix, name, treeSHA: dirEntry?.sha ?? "" };
|
|
100
|
-
})
|
|
101
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
68
|
+
const skillEntries = findSkillEntries(tree);
|
|
102
69
|
|
|
103
70
|
if (skillEntries.length === 0) {
|
|
104
71
|
console.error(`No skills found in ${repo} (no directories with SKILL.md)`);
|
|
@@ -107,206 +74,28 @@ if (skillEntries.length === 0) {
|
|
|
107
74
|
|
|
108
75
|
// ── Classify skills ──────────────────────────────────────────────────
|
|
109
76
|
|
|
110
|
-
|
|
111
|
-
const skills: SkillEntry[] = skillEntries.map(({ name, prefix, treeSHA }) => ({
|
|
77
|
+
const skills: AddSkillEntry[] = skillEntries.map(({ name, prefix, treeSHA }) => ({
|
|
112
78
|
name,
|
|
113
79
|
prefix,
|
|
114
80
|
treeSHA,
|
|
115
81
|
exists: skillExists(name),
|
|
116
82
|
}));
|
|
117
83
|
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
84
|
+
const addable = skills.filter((s) => !s.exists);
|
|
85
|
+
if (addable.length === 0) {
|
|
120
86
|
console.log("All skills already exist in your catalog.");
|
|
121
87
|
process.exit(0);
|
|
122
88
|
}
|
|
123
89
|
|
|
124
|
-
// ──
|
|
125
|
-
|
|
126
|
-
let cursor = 0;
|
|
127
|
-
const checked = new Set<number>();
|
|
128
|
-
let statusTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
129
|
-
let exitResolve: () => void;
|
|
130
|
-
|
|
131
|
-
for (let i = 0; i < skills.length; i++) {
|
|
132
|
-
if (!skills[i]!.exists) { cursor = i; break; }
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ── Build TUI ────────────────────────────────────────────────────────
|
|
136
|
-
|
|
137
|
-
const renderer = await createCliRenderer({ exitOnCtrlC: true });
|
|
138
|
-
|
|
139
|
-
const outer = new BoxRenderable(renderer, {
|
|
140
|
-
id: "outer",
|
|
141
|
-
width: "100%",
|
|
142
|
-
height: "100%",
|
|
143
|
-
flexDirection: "column",
|
|
144
|
-
backgroundColor: C.bg,
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
const header = new TextRenderable(renderer, {
|
|
148
|
-
id: "header",
|
|
149
|
-
content: ` Add skills from ${repo}`,
|
|
150
|
-
fg: C.title,
|
|
151
|
-
attributes: TextAttributes.BOLD,
|
|
152
|
-
height: 1,
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
const sep = new TextRenderable(renderer, {
|
|
156
|
-
id: "sep",
|
|
157
|
-
content: "\u2500".repeat(60),
|
|
158
|
-
fg: C.border,
|
|
159
|
-
height: 1,
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
const scrollBox = new ScrollBoxRenderable(renderer, {
|
|
163
|
-
id: "skill-list",
|
|
164
|
-
flexGrow: 1,
|
|
165
|
-
width: "100%",
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
type RowRefs = {
|
|
169
|
-
row: BoxRenderable;
|
|
170
|
-
checkText: TextRenderable;
|
|
171
|
-
nameText: TextRenderable;
|
|
172
|
-
};
|
|
173
|
-
const rows: RowRefs[] = [];
|
|
174
|
-
|
|
175
|
-
for (let i = 0; i < skills.length; i++) {
|
|
176
|
-
const skill = skills[i]!;
|
|
177
|
-
|
|
178
|
-
const row = new BoxRenderable(renderer, {
|
|
179
|
-
id: `row-${i}`,
|
|
180
|
-
flexDirection: "row",
|
|
181
|
-
height: 1,
|
|
182
|
-
width: "100%",
|
|
183
|
-
paddingLeft: 1,
|
|
184
|
-
backgroundColor: i % 2 === 0 ? C.rowBg : C.rowAltBg,
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const checkText = new TextRenderable(renderer, {
|
|
188
|
-
id: `check-${i}`,
|
|
189
|
-
content: skill.exists ? "[*]" : "[ ]",
|
|
190
|
-
fg: skill.exists ? C.fgDim : C.unchecked,
|
|
191
|
-
width: 4,
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
const label = skill.exists ? `${skill.name} (exists)` : skill.name;
|
|
195
|
-
const nameText = new TextRenderable(renderer, {
|
|
196
|
-
id: `name-${i}`,
|
|
197
|
-
content: ` ${label}`,
|
|
198
|
-
fg: skill.exists ? C.fgDim : C.fg,
|
|
199
|
-
width: 40,
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
row.add(checkText);
|
|
203
|
-
row.add(nameText);
|
|
204
|
-
scrollBox.add(row);
|
|
205
|
-
rows.push({ row, checkText, nameText });
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const footerSep = new TextRenderable(renderer, {
|
|
209
|
-
id: "footer-sep",
|
|
210
|
-
content: "\u2500".repeat(60),
|
|
211
|
-
fg: C.border,
|
|
212
|
-
height: 1,
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
const footer = new TextRenderable(renderer, {
|
|
216
|
-
id: "footer",
|
|
217
|
-
content: " j/k move space toggle a all enter confirm q cancel",
|
|
218
|
-
fg: C.footer,
|
|
219
|
-
height: 1,
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
const statusLine = new TextRenderable(renderer, {
|
|
223
|
-
id: "status",
|
|
224
|
-
content: "",
|
|
225
|
-
fg: C.statusOk,
|
|
226
|
-
height: 1,
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
outer.add(header);
|
|
230
|
-
outer.add(sep);
|
|
231
|
-
outer.add(scrollBox);
|
|
232
|
-
outer.add(footerSep);
|
|
233
|
-
outer.add(footer);
|
|
234
|
-
outer.add(statusLine);
|
|
235
|
-
renderer.root.add(outer);
|
|
236
|
-
|
|
237
|
-
// ── Display helpers ──────────────────────────────────────────────────
|
|
238
|
-
|
|
239
|
-
function updateRow(i: number) {
|
|
240
|
-
const skill = skills[i]!;
|
|
241
|
-
const r = rows[i]!;
|
|
242
|
-
const isCursor = cursor === i;
|
|
243
|
-
|
|
244
|
-
const baseBg = i % 2 === 0 ? C.rowBg : C.rowAltBg;
|
|
245
|
-
r.row.backgroundColor = isCursor ? C.cursorBg : baseBg;
|
|
246
|
-
|
|
247
|
-
if (skill.exists) {
|
|
248
|
-
r.checkText.content = "[*]";
|
|
249
|
-
r.checkText.fg = C.fgDim;
|
|
250
|
-
const pointer = isCursor ? "\u25b8" : " ";
|
|
251
|
-
r.nameText.content = `${pointer} ${skill.name} (exists)`;
|
|
252
|
-
r.nameText.fg = C.fgDim;
|
|
253
|
-
r.nameText.attributes = isCursor ? TextAttributes.BOLD : TextAttributes.NONE;
|
|
254
|
-
} else {
|
|
255
|
-
const isChecked = checked.has(i);
|
|
256
|
-
r.checkText.content = isChecked ? "[x]" : "[ ]";
|
|
257
|
-
r.checkText.fg = isCursor ? C.accent : (isChecked ? C.checked : C.unchecked);
|
|
258
|
-
const pointer = isCursor ? "\u25b8" : " ";
|
|
259
|
-
r.nameText.content = `${pointer} ${skill.name}`;
|
|
260
|
-
r.nameText.fg = isCursor ? "#ffffff" : C.fg;
|
|
261
|
-
r.nameText.attributes = isCursor ? TextAttributes.BOLD : TextAttributes.NONE;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function setStatus(msg: string, color: string) {
|
|
266
|
-
statusLine.content = ` ${msg}`;
|
|
267
|
-
statusLine.fg = color;
|
|
268
|
-
if (statusTimeout) clearTimeout(statusTimeout);
|
|
269
|
-
statusTimeout = setTimeout(() => {
|
|
270
|
-
statusLine.content = "";
|
|
271
|
-
}, 3000);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function selectedCount(): number {
|
|
275
|
-
return checked.size;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function updateHeader() {
|
|
279
|
-
header.content = ` Add skills from ${repo} (${selectedCount()} selected)`;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function refreshAll() {
|
|
283
|
-
for (let i = 0; i < skills.length; i++) updateRow(i);
|
|
284
|
-
updateHeader();
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function ensureVisible() {
|
|
288
|
-
scrollBox.scrollTo(Math.max(0, cursor - 2));
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
refreshAll();
|
|
292
|
-
|
|
293
|
-
// ── Confirm & download ───────────────────────────────────────────────
|
|
294
|
-
|
|
295
|
-
async function confirmAndDownload() {
|
|
296
|
-
const selected = [...checked].map((i) => skills[i]!);
|
|
297
|
-
if (selected.length === 0) {
|
|
298
|
-
setStatus("Nothing selected \u2014 use space to toggle", C.warning);
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
renderer.destroy();
|
|
90
|
+
// ── Download helper ─────────────────────────────────────────────────
|
|
303
91
|
|
|
92
|
+
async function downloadSelected(selected: AddSkillEntry[]) {
|
|
304
93
|
mkdirSync(CATALOG, { recursive: true });
|
|
94
|
+
let added = 0;
|
|
305
95
|
|
|
306
96
|
for (const skill of selected) {
|
|
307
97
|
const skillDir = join(CATALOG, skill.name);
|
|
308
98
|
|
|
309
|
-
// Remove broken remnant if present
|
|
310
99
|
try {
|
|
311
100
|
if (lstatSync(skillDir).isSymbolicLink()) {
|
|
312
101
|
rmSync(skillDir, { force: true });
|
|
@@ -314,87 +103,60 @@ async function confirmAndDownload() {
|
|
|
314
103
|
} catch {}
|
|
315
104
|
|
|
316
105
|
mkdirSync(skillDir, { recursive: true });
|
|
106
|
+
try {
|
|
107
|
+
const fileCount = await downloadSkillFiles(
|
|
108
|
+
repo, branch, skill.prefix, skillDir, tree,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const entry: LockEntry = {
|
|
112
|
+
source: repo,
|
|
113
|
+
sourceUrl: `https://github.com/${repo}`,
|
|
114
|
+
skillPath: skill.prefix,
|
|
115
|
+
treeSHA: skill.treeSHA,
|
|
116
|
+
addedAt: new Date().toISOString(),
|
|
117
|
+
};
|
|
118
|
+
addToLock(skill.name, entry);
|
|
119
|
+
|
|
120
|
+
console.log(` ${skill.name} — added (${fileCount} files)`);
|
|
121
|
+
added++;
|
|
122
|
+
} catch (e: any) {
|
|
123
|
+
console.log(` ${skill.name} — failed (${e.message})`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
317
126
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const fileCount = await downloadSkillFiles(
|
|
321
|
-
repo!, branch, skill.prefix, skillDir, tree,
|
|
322
|
-
);
|
|
127
|
+
console.log(`\nAdded ${added}/${selected.length} skill(s) to ~/.skl/catalog/`);
|
|
128
|
+
}
|
|
323
129
|
|
|
324
|
-
|
|
325
|
-
const entry: LockEntry = {
|
|
326
|
-
source: repo!,
|
|
327
|
-
sourceUrl: `https://github.com/${repo}`,
|
|
328
|
-
skillPath: skill.prefix,
|
|
329
|
-
treeSHA: skill.treeSHA,
|
|
330
|
-
addedAt: new Date().toISOString(),
|
|
331
|
-
};
|
|
332
|
-
addToLock(skill.name, entry);
|
|
130
|
+
// ── --all: skip TUI ─────────────────────────────────────────────────
|
|
333
131
|
|
|
334
|
-
|
|
132
|
+
if (allFlag) {
|
|
133
|
+
const skipped = skills.filter((s) => s.exists);
|
|
134
|
+
for (const s of skipped) {
|
|
135
|
+
console.log(` ${s.name} — skipped (already exists)`);
|
|
335
136
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
137
|
+
console.log(`Adding ${addable.length} skill(s)...`);
|
|
138
|
+
await downloadSelected(addable);
|
|
139
|
+
process.exit(0);
|
|
339
140
|
}
|
|
340
141
|
|
|
341
|
-
// ──
|
|
142
|
+
// ── TUI ─────────────────────────────────────────────────────────────
|
|
342
143
|
|
|
343
|
-
|
|
344
|
-
const prevCursor = cursor;
|
|
144
|
+
let exitResolve: () => void;
|
|
345
145
|
|
|
346
|
-
|
|
347
|
-
case "j":
|
|
348
|
-
case "down":
|
|
349
|
-
if (cursor < skills.length - 1) cursor++;
|
|
350
|
-
break;
|
|
351
|
-
case "k":
|
|
352
|
-
case "up":
|
|
353
|
-
if (cursor > 0) cursor--;
|
|
354
|
-
break;
|
|
355
|
-
case "space": {
|
|
356
|
-
if (skills[cursor]!.exists) {
|
|
357
|
-
setStatus(`${skills[cursor]!.name} already exists`, C.warning);
|
|
358
|
-
break;
|
|
359
|
-
}
|
|
360
|
-
if (checked.has(cursor)) {
|
|
361
|
-
checked.delete(cursor);
|
|
362
|
-
} else {
|
|
363
|
-
checked.add(cursor);
|
|
364
|
-
}
|
|
365
|
-
break;
|
|
366
|
-
}
|
|
367
|
-
case "a": {
|
|
368
|
-
const addable = skills
|
|
369
|
-
.map((s, i) => (!s.exists ? i : -1))
|
|
370
|
-
.filter((i) => i >= 0);
|
|
371
|
-
const allChecked = addable.every((i) => checked.has(i));
|
|
372
|
-
if (allChecked) {
|
|
373
|
-
for (const i of addable) checked.delete(i);
|
|
374
|
-
} else {
|
|
375
|
-
for (const i of addable) checked.add(i);
|
|
376
|
-
}
|
|
377
|
-
for (const i of addable) updateRow(i);
|
|
378
|
-
updateHeader();
|
|
379
|
-
ensureVisible();
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
case "return":
|
|
383
|
-
confirmAndDownload();
|
|
384
|
-
return;
|
|
385
|
-
case "q":
|
|
386
|
-
case "escape":
|
|
387
|
-
renderer.destroy();
|
|
388
|
-
exitResolve();
|
|
389
|
-
return;
|
|
390
|
-
default:
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
146
|
+
const renderer = await createCliRenderer({ exitOnCtrlC: true });
|
|
393
147
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
148
|
+
createAddTui(renderer, {
|
|
149
|
+
repo,
|
|
150
|
+
skills,
|
|
151
|
+
async onConfirm(selected) {
|
|
152
|
+
renderer.destroy();
|
|
153
|
+
await downloadSelected(selected);
|
|
154
|
+
exitResolve();
|
|
155
|
+
},
|
|
156
|
+
onCancel() {
|
|
157
|
+
renderer.destroy();
|
|
158
|
+
exitResolve();
|
|
159
|
+
},
|
|
398
160
|
});
|
|
399
161
|
|
|
400
162
|
await new Promise<void>((resolve) => {
|
package/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { createCliRenderer } from "@opentui/core";
|
|
|
4
4
|
import { existsSync, rmSync, cpSync, mkdirSync, readdirSync } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { homedir } from "os";
|
|
7
|
-
import { getCatalogSkills,
|
|
7
|
+
import { getCatalogSkills, removeFromLock, catalogDir, buildAddArgs, parseSkillsListOutput } from "./lib.ts";
|
|
8
8
|
import { createTui, type ColId } from "./tui.ts";
|
|
9
9
|
|
|
10
10
|
// @ts-ignore - bun supports JSON imports
|
|
@@ -16,6 +16,21 @@ if (arg === "-v" || arg === "--version") {
|
|
|
16
16
|
console.log(`skl ${VERSION}`);
|
|
17
17
|
process.exit(0);
|
|
18
18
|
}
|
|
19
|
+
if (arg === "-h" || arg === "--help" || arg === "help") {
|
|
20
|
+
console.log(`skl ${VERSION} — TUI skill manager for Claude Code agents
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
skl Open the interactive TUI
|
|
24
|
+
skl add <repo> Add skills from a GitHub repo (interactive)
|
|
25
|
+
skl add <repo> --all Add all skills from a repo (non-interactive)
|
|
26
|
+
skl update Update all remote skills
|
|
27
|
+
skl help Show this help message
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
-h, --help Show this help message
|
|
31
|
+
-v, --version Show version`);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
19
34
|
|
|
20
35
|
// ── Subcommand routing ───────────────────────────────────────────────
|
|
21
36
|
if (arg === "add") {
|
|
@@ -59,10 +74,7 @@ async function listInstalled(global: boolean): Promise<Set<string>> {
|
|
|
59
74
|
const out = await new Response(proc.stdout).text();
|
|
60
75
|
const code = await proc.exited;
|
|
61
76
|
if (code !== 0) return new Set();
|
|
62
|
-
|
|
63
|
-
if (Array.isArray(data)) {
|
|
64
|
-
return new Set(data.map((s: { name: string }) => s.name));
|
|
65
|
-
}
|
|
77
|
+
return new Set(parseSkillsListOutput(out.trim()));
|
|
66
78
|
} catch {}
|
|
67
79
|
return new Set();
|
|
68
80
|
}
|
|
@@ -74,20 +86,6 @@ const [globalInstalled, localInstalled] = await Promise.all([
|
|
|
74
86
|
]);
|
|
75
87
|
console.log(" done");
|
|
76
88
|
|
|
77
|
-
// ── Build add/remove args ────────────────────────────────────────────
|
|
78
|
-
|
|
79
|
-
function buildAddArgs(name: string, global: boolean): string[] {
|
|
80
|
-
const lockEntry = getLockEntry(name);
|
|
81
|
-
if (lockEntry) {
|
|
82
|
-
const args = ["add", lockEntry.sourceUrl, "--skill", name, "-y"];
|
|
83
|
-
if (global) args.push("-g");
|
|
84
|
-
return args;
|
|
85
|
-
}
|
|
86
|
-
const args = ["add", join(CATALOG, name), "-y"];
|
|
87
|
-
if (global) args.push("-g");
|
|
88
|
-
return args;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
89
|
// ── Create TUI ──────────────────────────────────────────────────────
|
|
92
90
|
|
|
93
91
|
const renderer = await createCliRenderer({ exitOnCtrlC: true });
|
|
@@ -101,7 +99,7 @@ const tui = createTui(renderer, {
|
|
|
101
99
|
async onToggle(col: ColId, name: string, enable: boolean) {
|
|
102
100
|
const isGlobal = col === "global";
|
|
103
101
|
const args = enable
|
|
104
|
-
? buildAddArgs(name, isGlobal)
|
|
102
|
+
? buildAddArgs(CATALOG, name, isGlobal)
|
|
105
103
|
: ["remove", name, "-y", ...(isGlobal ? ["-g"] : [])];
|
|
106
104
|
const proc = Bun.spawn(["npx", "-y", "skills", ...args], {
|
|
107
105
|
stdout: "pipe",
|