cadence-skill-installer 0.1.4 → 0.2.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/README.md CHANGED
@@ -10,6 +10,7 @@ npx cadence-skill-installer
10
10
 
11
11
  The installer shows a multi-select prompt (comma-separated choices) so you can install into multiple tools in one run.
12
12
  If a selected tool already has Cadence installed, the installer prints an update notice and warns that files will be overwritten.
13
+ In TTY terminals, selection is a real interactive TUI: use arrow keys (or `j`/`k`) to move, `space` to toggle, `a` to toggle all, and `enter` to confirm.
13
14
 
14
15
  ## Non-interactive examples
15
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cadence-skill-installer",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Install the Cadence skill into supported AI tool skill directories.",
5
5
  "repository": "https://github.com/snowdamiz/cadence",
6
6
  "private": false,
@@ -22,7 +22,7 @@ const TOOL_TARGETS = [
22
22
  },
23
23
  {
24
24
  key: "windsurf",
25
- label: "Codeium Windsurf",
25
+ label: "Windsurf",
26
26
  relPath: [".codeium", "windsurf", "skills", CADENCE_SKILL_NAME]
27
27
  },
28
28
  {
@@ -238,16 +238,136 @@ function parseInteractiveSelection(selection, targets) {
238
238
  return uniqueIndexes.map((idx) => targets[idx]);
239
239
  }
240
240
 
241
- async function chooseTargets(parsed, targets) {
242
- if (parsed.all) {
243
- return targets;
244
- }
241
+ function renderMultiSelectTui(targets, cursorIndex, selectedIndexes, notice) {
242
+ output.write("\x1b[2J\x1b[H");
243
+ output.write("Select tools to install Cadence skill into (multi-select).\n");
244
+ output.write("Use arrow keys (or j/k) to move, space to toggle, a to toggle all, enter to confirm, q to cancel.\n\n");
245
245
 
246
- if (parsed.tools) {
247
- const selectedKeys = parseToolKeyList(parsed.tools);
248
- return targets.filter((target) => selectedKeys.includes(target.key));
246
+ targets.forEach((target, idx) => {
247
+ const pointer = idx === cursorIndex ? ">" : " ";
248
+ const checked = selectedIndexes.has(idx) ? "x" : " ";
249
+ output.write(`${pointer} [${checked}] ${target.label} (${target.targetDir})\n`);
250
+ });
251
+
252
+ output.write(`\nSelected: ${selectedIndexes.size}\n`);
253
+ if (notice) {
254
+ output.write(`${notice}\n`);
249
255
  }
256
+ }
257
+
258
+ function chooseTargetsWithTui(targets) {
259
+ return new Promise((resolve, reject) => {
260
+ if (!input.isTTY || !output.isTTY) {
261
+ reject(new Error("Interactive TUI requires a TTY."));
262
+ return;
263
+ }
264
+
265
+ let cursorIndex = 0;
266
+ let notice = "";
267
+ const selectedIndexes = new Set();
268
+
269
+ const cleanup = () => {
270
+ input.off("data", onData);
271
+ if (input.isTTY) {
272
+ input.setRawMode(false);
273
+ }
274
+ output.write("\x1b[?25h");
275
+ output.write("\n");
276
+ };
277
+
278
+ const moveCursor = (delta) => {
279
+ const length = targets.length;
280
+ cursorIndex = (cursorIndex + delta + length) % length;
281
+ };
282
+
283
+ const toggleCurrent = () => {
284
+ if (selectedIndexes.has(cursorIndex)) {
285
+ selectedIndexes.delete(cursorIndex);
286
+ } else {
287
+ selectedIndexes.add(cursorIndex);
288
+ }
289
+ };
290
+
291
+ const toggleAll = () => {
292
+ if (selectedIndexes.size === targets.length) {
293
+ selectedIndexes.clear();
294
+ return;
295
+ }
296
+ for (let idx = 0; idx < targets.length; idx += 1) {
297
+ selectedIndexes.add(idx);
298
+ }
299
+ };
300
+
301
+ const finishSelection = () => {
302
+ if (selectedIndexes.size === 0) {
303
+ notice = "Select at least one tool before continuing.";
304
+ renderMultiSelectTui(targets, cursorIndex, selectedIndexes, notice);
305
+ return;
306
+ }
307
+
308
+ const orderedIndexes = [...selectedIndexes].sort((a, b) => a - b);
309
+ const selectedTargets = orderedIndexes.map((idx) => targets[idx]);
310
+ cleanup();
311
+ resolve(selectedTargets);
312
+ };
313
+
314
+ const onData = (chunk) => {
315
+ const key = chunk.toString("utf8");
316
+
317
+ if (key === "\u0003") {
318
+ cleanup();
319
+ reject(new Error("Cancelled by user."));
320
+ return;
321
+ }
322
+
323
+ if (key === "\r" || key === "\n") {
324
+ finishSelection();
325
+ return;
326
+ }
327
+
328
+ if (key === " " || key === "\u001b[3~") {
329
+ toggleCurrent();
330
+ notice = "";
331
+ renderMultiSelectTui(targets, cursorIndex, selectedIndexes, notice);
332
+ return;
333
+ }
334
+
335
+ if (key === "a" || key === "A") {
336
+ toggleAll();
337
+ notice = "";
338
+ renderMultiSelectTui(targets, cursorIndex, selectedIndexes, notice);
339
+ return;
340
+ }
341
+
342
+ if (key === "q" || key === "Q") {
343
+ cleanup();
344
+ reject(new Error("Cancelled by user."));
345
+ return;
346
+ }
347
+
348
+ if (key === "\u001b[A" || key === "k" || key === "K") {
349
+ moveCursor(-1);
350
+ notice = "";
351
+ renderMultiSelectTui(targets, cursorIndex, selectedIndexes, notice);
352
+ return;
353
+ }
354
+
355
+ if (key === "\u001b[B" || key === "j" || key === "J") {
356
+ moveCursor(1);
357
+ notice = "";
358
+ renderMultiSelectTui(targets, cursorIndex, selectedIndexes, notice);
359
+ }
360
+ };
250
361
 
362
+ output.write("\x1b[?25l");
363
+ input.setRawMode(true);
364
+ input.resume();
365
+ input.on("data", onData);
366
+ renderMultiSelectTui(targets, cursorIndex, selectedIndexes, notice);
367
+ });
368
+ }
369
+
370
+ async function chooseTargetsWithTextPrompt(targets) {
251
371
  const rl = readline.createInterface({ input, output });
252
372
  try {
253
373
  output.write("Select tools to install Cadence skill into (multi-select).\n");
@@ -262,6 +382,23 @@ async function chooseTargets(parsed, targets) {
262
382
  }
263
383
  }
264
384
 
385
+ async function chooseTargets(parsed, targets) {
386
+ if (parsed.all) {
387
+ return targets;
388
+ }
389
+
390
+ if (parsed.tools) {
391
+ const selectedKeys = parseToolKeyList(parsed.tools);
392
+ return targets.filter((target) => selectedKeys.includes(target.key));
393
+ }
394
+
395
+ if (input.isTTY && output.isTTY) {
396
+ return chooseTargetsWithTui(targets);
397
+ }
398
+
399
+ return chooseTargetsWithTextPrompt(targets);
400
+ }
401
+
265
402
  async function copyEntry(srcPath, destPath) {
266
403
  const stat = await fs.lstat(srcPath);
267
404