@skilly-hand/skilly-hand 0.23.0 → 0.23.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.
@@ -1,969 +0,0 @@
1
- import React, { useEffect, useMemo, useState } from "react";
2
- import { Box, Text, render, useApp, useInput } from "ink";
3
- import { getBrand } from "../../core/src/ui/brand.js";
4
-
5
- const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
6
-
7
- function stripAnsi(value) {
8
- return String(value || "").replace(ANSI_PATTERN, "");
9
- }
10
-
11
- function wrapText(text, width) {
12
- const safeWidth = Math.max(20, width || 80);
13
- const input = String(text || "").split("\n");
14
- const lines = [];
15
- for (const rawLine of input) {
16
- const line = rawLine.trimEnd();
17
- if (!line) {
18
- lines.push("");
19
- continue;
20
- }
21
- let rest = line;
22
- while (rest.length > safeWidth) {
23
- let split = rest.lastIndexOf(" ", safeWidth);
24
- if (split <= 0) split = safeWidth;
25
- lines.push(rest.slice(0, split));
26
- rest = rest.slice(split).trimStart();
27
- }
28
- lines.push(rest);
29
- }
30
- return lines;
31
- }
32
-
33
- function truncateLine(text, maxWidth) {
34
- const safeWidth = Math.max(4, maxWidth || 40);
35
- const chars = Array.from(String(text || ""));
36
- if (chars.length <= safeWidth) return chars.join("");
37
- if (safeWidth <= 4) return chars.slice(0, safeWidth).join("");
38
- return `${chars.slice(0, safeWidth - 1).join("")}…`;
39
- }
40
-
41
- function formatConfirmPreviewLines(preview, width) {
42
- const rawLines = String(preview || "").split("\n");
43
- const bodyStart = rawLines.findIndex((line) =>
44
- /Install Preflight|Detection Summary|Catalog Summary|Doctor Summary|Findings|Skill Plan/.test(line)
45
- );
46
- const headEnd = bodyStart > 0 ? bodyStart : Math.min(12, rawLines.length);
47
- const head = rawLines.slice(0, headEnd).map((line) => truncateLine(line, width));
48
- const body = rawLines.slice(headEnd).flatMap((line) => wrapText(line, width));
49
- return [...head, ...body];
50
- }
51
-
52
- function formatConfirmPreviewFromDoc(previewDoc, width) {
53
- const blocks = docToBlocks(previewDoc, Math.max(24, width - 2));
54
- const lines = [];
55
- for (let idx = 0; idx < blocks.length; idx += 1) {
56
- lines.push(...blocks[idx].lines.map((line) => line.text));
57
- if (idx < blocks.length - 1) lines.push("");
58
- }
59
- return lines;
60
- }
61
-
62
- function clamp(value, min, max) {
63
- return Math.min(Math.max(value, min), max);
64
- }
65
-
66
- function computeWindow(total, visible, cursor) {
67
- if (total <= 0) return { start: 0, end: 0 };
68
- const safeVisible = Math.max(1, Math.min(visible, total));
69
- const half = Math.floor(safeVisible / 2);
70
- let start = cursor - half;
71
- start = clamp(start, 0, Math.max(0, total - safeVisible));
72
- return { start, end: start + safeVisible };
73
- }
74
-
75
- function fitWindowByLineBudget(blocks, cursor, availableLines) {
76
- const total = blocks.length;
77
- if (total <= 0) return { start: 0, end: 0, usedLines: 0 };
78
-
79
- const safeCursor = clamp(cursor, 0, total - 1);
80
- const budget = Math.max(1, availableLines);
81
-
82
- let start = safeCursor;
83
- const focusLines = Math.max(1, blocks[safeCursor].lineCount);
84
- const desiredTopLines = Math.max(0, Math.floor((budget - focusLines) / 2));
85
-
86
- let consumedTop = 0;
87
- while (start > 0) {
88
- const next = Math.max(1, blocks[start - 1].lineCount);
89
- if (consumedTop + next > desiredTopLines) break;
90
- start -= 1;
91
- consumedTop += next;
92
- }
93
-
94
- let end = start;
95
- let usedLines = 0;
96
- while (end < total) {
97
- const next = Math.max(1, blocks[end].lineCount);
98
- if (usedLines + next > budget && end > start) break;
99
- usedLines += next;
100
- end += 1;
101
- if (usedLines >= budget && end > start) break;
102
- }
103
-
104
- if (safeCursor >= end) {
105
- start = safeCursor;
106
- end = safeCursor;
107
- usedLines = 0;
108
- while (end < total) {
109
- const next = Math.max(1, blocks[end].lineCount);
110
- if (usedLines + next > budget && end > start) break;
111
- usedLines += next;
112
- end += 1;
113
- if (usedLines >= budget && end > start) break;
114
- }
115
- }
116
-
117
- while (start > 0) {
118
- const next = Math.max(1, blocks[start - 1].lineCount);
119
- if (usedLines + next > budget) break;
120
- start -= 1;
121
- usedLines += next;
122
- }
123
-
124
- return { start, end, usedLines };
125
- }
126
-
127
- function useAsyncAction() {
128
- const [busy, setBusy] = useState(false);
129
-
130
- async function run(action) {
131
- setBusy(true);
132
- try {
133
- return await action();
134
- } finally {
135
- setBusy(false);
136
- }
137
- }
138
-
139
- return { busy, run };
140
- }
141
-
142
- function Header({ appVersion }) {
143
- const brand = getBrand();
144
- const logo = brand.logo.unicode;
145
-
146
- return React.createElement(
147
- Box,
148
- { flexDirection: "column", marginBottom: 1 },
149
- ...logo.map((line, idx) => React.createElement(Text, { key: `logo-${idx}`, color: "cyan" }, line)),
150
- React.createElement(
151
- Box,
152
- { marginTop: 1 },
153
- React.createElement(Text, { color: "cyan", bold: true }, `${brand.name}`),
154
- React.createElement(Text, { color: "gray" }, appVersion ? ` v${appVersion}` : ""),
155
- React.createElement(Text, { color: "gray" }, ` ${brand.tagline}`)
156
- )
157
- );
158
- }
159
-
160
- function MenuPanel({ selectedIndex, menuItems }) {
161
- return React.createElement(
162
- Box,
163
- { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 0, width: 32 },
164
- React.createElement(Text, { color: "cyan", bold: true }, "Command Hub"),
165
- ...menuItems.map((item, idx) => {
166
- const active = idx === selectedIndex;
167
- return React.createElement(
168
- Text,
169
- { key: item.value, color: active ? "black" : "white", backgroundColor: active ? "cyan" : undefined },
170
- `${active ? "›" : " "} ${item.label}`
171
- );
172
- }),
173
- React.createElement(Text, { color: "gray" }, ""),
174
- React.createElement(Text, { color: "gray" }, "↑↓ move Enter select q quit")
175
- );
176
- }
177
-
178
- function ResultPanel({ title, lines, busy, maxLines, offset }) {
179
- const viewportLines = Math.max(6, maxLines - 4);
180
- const maxOffset = Math.max(0, lines.length - viewportLines);
181
- const safeOffset = clamp(offset, 0, maxOffset);
182
- const visible = lines.slice(safeOffset, safeOffset + viewportLines);
183
- const from = lines.length ? safeOffset + 1 : 0;
184
- const to = safeOffset + visible.length;
185
-
186
- return React.createElement(
187
- Box,
188
- { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, flexGrow: 1 },
189
- React.createElement(Text, { color: "cyan", bold: true }, title),
190
- busy ? React.createElement(Text, { color: "yellow" }, "Working...") : null,
191
- ...visible.map((line, idx) =>
192
- React.createElement(Text, { key: `line-${idx}`, color: idx === 0 ? "white" : "gray" }, line)
193
- ),
194
- React.createElement(Text, { color: "cyan" }, "─".repeat(Math.max(12, 56))),
195
- React.createElement(
196
- Text,
197
- { color: "gray" },
198
- `Lines ${from}-${to} of ${lines.length} | ↑/↓ or j/k scroll`
199
- )
200
- );
201
- }
202
-
203
- function isResultDoc(value) {
204
- return Boolean(value && typeof value === "object" && value.type === "result-doc");
205
- }
206
-
207
- function padEndPlain(value, width) {
208
- const raw = String(value || "");
209
- if (raw.length >= width) return raw;
210
- return raw + " ".repeat(width - raw.length);
211
- }
212
-
213
- function truncatePlain(value, width) {
214
- const raw = String(value || "");
215
- if (raw.length <= width) return raw;
216
- if (width <= 1) return raw.slice(0, width);
217
- return `${raw.slice(0, width - 1)}…`;
218
- }
219
-
220
- function renderAdaptiveTableLines({ columns, rows, width }) {
221
- const safeWidth = Math.max(40, width);
222
- const headers = columns.map((column) => String(column.header));
223
- const values = rows.map((row) => columns.map((column) => String(row[column.key] ?? "")));
224
-
225
- if (headers.length === 0) return [];
226
-
227
- if (safeWidth >= 90 && headers.length <= 4) {
228
- const desired = headers.map((header, index) => {
229
- const maxCell = values.reduce((max, row) => Math.max(max, row[index]?.length || 0), header.length);
230
- return Math.min(Math.max(10, maxCell), 36);
231
- });
232
- const gap = 3;
233
- let total = desired.reduce((sum, value) => sum + value, 0) + (headers.length - 1) * gap;
234
- while (total > safeWidth && desired.some((value) => value > 10)) {
235
- const idx = desired.findIndex((value) => value === Math.max(...desired));
236
- desired[idx] -= 1;
237
- total -= 1;
238
- }
239
-
240
- const headerLine = headers.map((header, index) => padEndPlain(truncatePlain(header, desired[index]), desired[index])).join(" ");
241
- const sepLine = desired.map((value) => "─".repeat(value)).join(" ");
242
- const lines = [
243
- { text: headerLine, tone: "label" },
244
- { text: sepLine, tone: "muted" }
245
- ];
246
- for (const row of values) {
247
- lines.push({
248
- text: row.map((cell, index) => padEndPlain(truncatePlain(cell, desired[index]), desired[index])).join(" "),
249
- tone: "body"
250
- });
251
- }
252
- return lines;
253
- }
254
-
255
- if (safeWidth >= 68) {
256
- const maxCols = Math.min(headers.length, 3);
257
- const usedHeaders = headers.slice(0, maxCols);
258
- const colWidths = usedHeaders.map((header, index) => {
259
- const maxCell = values.reduce((max, row) => Math.max(max, (row[index] || "").length), header.length);
260
- return Math.min(Math.max(8, maxCell), 26);
261
- });
262
- const headerLine = usedHeaders.map((header, index) => padEndPlain(truncatePlain(header, colWidths[index]), colWidths[index])).join(" | ");
263
- const sepLine = colWidths.map((value) => "─".repeat(value)).join("-+-");
264
- const lines = [
265
- { text: headerLine, tone: "label" },
266
- { text: sepLine, tone: "muted" }
267
- ];
268
- for (const row of values) {
269
- lines.push({
270
- text: usedHeaders.map((_, index) => padEndPlain(truncatePlain(row[index] || "", colWidths[index]), colWidths[index])).join(" | "),
271
- tone: "body"
272
- });
273
- if (headers.length > maxCols) {
274
- for (let idx = maxCols; idx < headers.length; idx += 1) {
275
- lines.push({ text: ` ${headers[idx]}: ${truncatePlain(row[idx] || "", safeWidth - 4)}`, tone: "muted" });
276
- }
277
- }
278
- }
279
- return lines;
280
- }
281
-
282
- const lines = [];
283
- for (const row of values) {
284
- for (let idx = 0; idx < headers.length; idx += 1) {
285
- const wrapped = wrapText(`${headers[idx]}: ${row[idx] || "-"}`, safeWidth - 2);
286
- wrapped.forEach((line, lineIndex) => {
287
- lines.push({ text: lineIndex === 0 ? `• ${line}` : ` ${line}`, tone: lineIndex === 0 ? "body" : "muted" });
288
- });
289
- }
290
- lines.push({ text: "─".repeat(Math.max(12, safeWidth - 4)), tone: "muted" });
291
- }
292
- if (lines.length > 0) lines.pop();
293
- return lines;
294
- }
295
-
296
- function docToBlocks(doc, width) {
297
- const blocks = [];
298
- for (const section of doc.sections || []) {
299
- const lines = [{ text: section.title, tone: "heading" }];
300
- for (const block of section.blocks || []) {
301
- if (block.type === "kv") {
302
- const keyWidth = (block.entries || []).reduce((max, [key]) => Math.max(max, String(key).length), 0);
303
- for (const [key, value] of block.entries || []) {
304
- const prefix = `${padEndPlain(String(key), keyWidth)} : `;
305
- const wrapped = wrapText(String(value || "-"), Math.max(16, width - prefix.length));
306
- wrapped.forEach((line, index) => {
307
- lines.push({
308
- text: index === 0 ? `${prefix}${line}` : `${" ".repeat(prefix.length)}${line}`,
309
- tone: index === 0 ? "body" : "muted"
310
- });
311
- });
312
- }
313
- } else if (block.type === "table") {
314
- lines.push(...renderAdaptiveTableLines({ columns: block.columns || [], rows: block.rows || [], width }));
315
- } else if (block.type === "list") {
316
- for (const item of block.items || []) {
317
- const bullet = block.bullet || "•";
318
- const wrapped = wrapText(String(item), Math.max(20, width - 4));
319
- wrapped.forEach((line, index) => {
320
- lines.push({ text: index === 0 ? `${bullet} ${line}` : ` ${line}`, tone: index === 0 ? "body" : "muted" });
321
- });
322
- }
323
- } else if (block.type === "status") {
324
- const icon = block.level === "success" ? "✓" : block.level === "warn" ? "!" : block.level === "error" ? "x" : "i";
325
- lines.push({ text: `${icon} ${block.message}`, tone: block.level || "info" });
326
- if (block.detail) {
327
- wrapText(String(block.detail), Math.max(20, width - 2)).forEach((line) => lines.push({ text: ` ${line}`, tone: "muted" }));
328
- }
329
- } else if (block.type === "text") {
330
- wrapText(String(block.text || ""), width).forEach((line) => lines.push({ text: line, tone: "body" }));
331
- }
332
- }
333
- blocks.push({ key: section.title, lines, lineCount: lines.length + 1 });
334
- }
335
- return blocks;
336
- }
337
-
338
- function toneToColor(tone) {
339
- if (tone === "heading") return { color: "cyan", bold: true };
340
- if (tone === "label") return { color: "cyan", bold: false };
341
- if (tone === "muted") return { color: "gray", bold: false };
342
- if (tone === "success") return { color: "green", bold: false };
343
- if (tone === "warn") return { color: "yellow", bold: false };
344
- if (tone === "error") return { color: "red", bold: false };
345
- if (tone === "info") return { color: "cyan", bold: false };
346
- return { color: "white", bold: false };
347
- }
348
-
349
- function ResultDocPanel({ title, blocks, busy, maxLines, blockOffset, lineOffset }) {
350
- const availableLines = Math.max(6, maxLines - 4);
351
- const safeBlock = clamp(blockOffset, 0, Math.max(0, blocks.length - 1));
352
- const flatLines = [];
353
- const blockStarts = [];
354
-
355
- for (let idx = 0; idx < blocks.length; idx += 1) {
356
- blockStarts.push(flatLines.length);
357
- flatLines.push(...blocks[idx].lines);
358
- if (idx < blocks.length - 1) {
359
- flatLines.push({ text: "", tone: "muted" });
360
- }
361
- }
362
-
363
- const maxOffset = Math.max(0, flatLines.length - availableLines);
364
- const safeOffset = clamp(lineOffset || 0, 0, maxOffset);
365
- const visible = flatLines.slice(safeOffset, safeOffset + availableLines);
366
- const from = flatLines.length ? safeOffset + 1 : 0;
367
- const to = safeOffset + visible.length;
368
-
369
- return React.createElement(
370
- Box,
371
- { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, flexGrow: 1 },
372
- React.createElement(Text, { color: "cyan", bold: true }, title),
373
- busy ? React.createElement(Text, { color: "yellow" }, "Working...") : null,
374
- ...visible.map((line, idx) => {
375
- const style = toneToColor(line.tone);
376
- return React.createElement(Text, { key: `result-doc-line-${idx}`, color: style.color, bold: style.bold }, line.text);
377
- }),
378
- React.createElement(Text, { color: "cyan" }, "─".repeat(Math.max(12, 56))),
379
- React.createElement(Text, { color: "gray" }, `Lines ${from}-${to} of ${flatLines.length} | Block ${safeBlock + 1} of ${blocks.length} | ↑/↓ menu j/k scroll`)
380
- );
381
- }
382
-
383
- function computeSkillBlock({ skill, focused, chosen, contentWidth }) {
384
- const safeWidth = Math.max(20, contentWidth);
385
- const tagsText = ` tags: ${skill.tags.join(", ") || "none"}`;
386
- const agentsText = ` agents: ${skill.agentSupport.join(", ") || "none"}`;
387
- const tagsLines = wrapText(tagsText, safeWidth);
388
- const agentsLines = wrapText(agentsText, safeWidth);
389
- const header = `${focused ? "›" : " "} [${chosen ? "x" : " "}] ${skill.id} | ${skill.title}`;
390
- const lines = [header, ...tagsLines, ...agentsLines];
391
- return {
392
- skill,
393
- lines,
394
- lineCount: lines.length
395
- };
396
- }
397
-
398
- export function buildSkillBlocksForRender({ skills, cursor, selectedIds, contentWidth }) {
399
- return skills.map((skill, idx) =>
400
- computeSkillBlock({
401
- skill,
402
- focused: idx === cursor,
403
- chosen: selectedIds.has(skill.id),
404
- contentWidth
405
- })
406
- );
407
- }
408
-
409
- export function computeSkillWindow({ blocks, cursor, availableLines }) {
410
- return fitWindowByLineBudget(blocks, cursor, availableLines);
411
- }
412
-
413
- function SkillPickerPanel({ skills, cursor, selectedIds, maxLines, width }) {
414
- const contentWidth = Math.max(20, (width || 80) - 6);
415
- const blocks = buildSkillBlocksForRender({
416
- skills,
417
- cursor,
418
- selectedIds,
419
- contentWidth
420
- });
421
- const availableLines = Math.max(1, maxLines - 5);
422
- const window = computeSkillWindow({ blocks, cursor, availableLines });
423
- const chunk = blocks.slice(window.start, window.end);
424
-
425
- return React.createElement(
426
- Box,
427
- { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, flexGrow: 1 },
428
- React.createElement(Text, { color: "cyan", bold: true }, "Install: Select Skills"),
429
- ...chunk.map((block, localIdx) => {
430
- const idx = window.start + localIdx;
431
- return React.createElement(
432
- Box,
433
- { key: block.skill.id, flexDirection: "column" },
434
- ...block.lines.map((line, lineIdx) =>
435
- React.createElement(Text, {
436
- key: `${block.skill.id}-${lineIdx}`,
437
- color: lineIdx === 0 ? (idx === cursor ? "black" : "white") : "gray",
438
- backgroundColor: lineIdx === 0 && idx === cursor ? "cyan" : undefined
439
- }, line)
440
- )
441
- );
442
- }),
443
- React.createElement(Text, { color: "cyan" }, "─".repeat(Math.max(12, 56))),
444
- React.createElement(
445
- Text,
446
- { color: "gray" },
447
- `Skills ${window.start + 1}-${window.end} of ${skills.length}`
448
- ),
449
- React.createElement(Text, { color: "gray" }, "↑/↓ or j/k move, Space toggle, Enter continue, Backspace cancel")
450
- );
451
- }
452
-
453
- function AgentPickerPanel({ agents, cursor, selected, maxLines }) {
454
- const visibleAgents = Math.max(1, maxLines - 5);
455
- const window = computeWindow(agents.length, visibleAgents, cursor);
456
- const chunk = agents.slice(window.start, window.end);
457
-
458
- return React.createElement(
459
- Box,
460
- { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, flexGrow: 1 },
461
- React.createElement(Text, { color: "cyan", bold: true }, "Install: Select Assistants"),
462
- ...chunk.map((agent, localIdx) => {
463
- const idx = window.start + localIdx;
464
- const focused = idx === cursor;
465
- const chosen = selected.has(agent);
466
- return React.createElement(
467
- Text,
468
- { key: agent, color: focused ? "black" : "white", backgroundColor: focused ? "cyan" : undefined },
469
- `${focused ? "›" : " "} [${chosen ? "x" : " "}] ${agent}`
470
- );
471
- }),
472
- React.createElement(Text, { color: "cyan" }, "─".repeat(Math.max(12, 56))),
473
- React.createElement(
474
- Text,
475
- { color: "gray" },
476
- `Assistants ${window.start + 1}-${window.end} of ${agents.length}`
477
- ),
478
- React.createElement(Text, { color: "gray" }, "↑/↓ or j/k move, Space toggle, Enter continue, Backspace back")
479
- );
480
- }
481
-
482
- function ConfirmPanel({ title, preview, previewDoc, options, selectedIndex, width, maxLines, offset }) {
483
- const previewWidth = Math.max(24, (width || 80) - 10);
484
- const previewLines = previewDoc
485
- ? formatConfirmPreviewFromDoc(previewDoc, previewWidth)
486
- : formatConfirmPreviewLines(preview, previewWidth);
487
- const viewport = Math.max(5, maxLines - 14);
488
- const maxOffset = Math.max(0, previewLines.length - viewport);
489
- const safeOffset = clamp(offset, 0, maxOffset);
490
- const visible = previewLines.slice(safeOffset, safeOffset + viewport);
491
- const from = previewLines.length ? safeOffset + 1 : 0;
492
- const to = safeOffset + visible.length;
493
-
494
- return React.createElement(
495
- Box,
496
- { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, flexGrow: 1 },
497
- React.createElement(Text, { color: "cyan", bold: true }, title),
498
- React.createElement(Text, { color: "cyan" }, "Action"),
499
- ...options.map((opt, idx) => {
500
- const active = idx === selectedIndex;
501
- return React.createElement(
502
- Text,
503
- { key: opt, color: active ? "black" : "white", backgroundColor: active ? "cyan" : undefined },
504
- `${active ? "›" : " "} ${opt}`
505
- );
506
- }),
507
- React.createElement(Text, { color: "cyan" }, "─".repeat(Math.max(12, 56))),
508
- React.createElement(Text, { color: "cyan" }, "Install Snapshot"),
509
- React.createElement(
510
- Box,
511
- { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, paddingY: 0 },
512
- ...visible.map((line, idx) => React.createElement(Text, { key: `preview-${idx}`, color: "gray" }, line))
513
- ),
514
- React.createElement(Text, { color: "cyan" }, "─".repeat(Math.max(12, 56))),
515
- React.createElement(Text, { color: "gray" }, `Preview ${from}-${to} of ${previewLines.length}`),
516
- React.createElement(Text, { color: "gray" }, "↑/↓ choose, Enter confirm, Backspace cancel, j/k scroll")
517
- );
518
- }
519
-
520
- function App({ appVersion, actions, onResolve }) {
521
- const menuItems = [
522
- { value: "install", label: "Install" },
523
- { value: "native-setup", label: "Native Setup" },
524
- { value: "detect", label: "Detect" },
525
- { value: "list", label: "List" },
526
- { value: "doctor", label: "Doctor" },
527
- { value: "uninstall", label: "Uninstall" },
528
- { value: "exit", label: "Exit" }
529
- ];
530
-
531
- const { exit } = useApp();
532
- const [mode, setMode] = useState("menu");
533
- const [menuIndex, setMenuIndex] = useState(0);
534
- const [resultTitle, setResultTitle] = useState("Ready");
535
- const [resultBody, setResultBody] = useState("Choose a command from the left.");
536
- const [resultDoc, setResultDoc] = useState(null);
537
- const [resultOffset, setResultOffset] = useState(0);
538
- const [resultBlockOffset, setResultBlockOffset] = useState(0);
539
- const [resultDocLineOffset, setResultDocLineOffset] = useState(0);
540
-
541
- const [installSkills, setInstallSkills] = useState([]);
542
- const [installAgents, setInstallAgents] = useState([]);
543
- const [selectedSkills, setSelectedSkills] = useState(new Set());
544
- const [selectedAgents, setSelectedAgents] = useState(new Set());
545
- const [cursorIndex, setCursorIndex] = useState(0);
546
- const [confirmChoice, setConfirmChoice] = useState(0);
547
- const [installPreview, setInstallPreview] = useState("");
548
- const [installPreviewDoc, setInstallPreviewDoc] = useState(null);
549
- const [confirmPreviewOffset, setConfirmPreviewOffset] = useState(0);
550
-
551
- const { busy, run } = useAsyncAction();
552
- const stdoutWidth = Number(process.stdout?.columns || 120);
553
- const stdoutRows = Number(process.stdout?.rows || 40);
554
- const contentWidth = Math.max(60, stdoutWidth - 36);
555
- const panelMaxLines = Math.max(12, stdoutRows - 10);
556
- const resultLines = useMemo(() => wrapText(resultBody, contentWidth - 6), [resultBody, contentWidth]);
557
- const resultDocBlocks = useMemo(() => {
558
- if (!isResultDoc(resultDoc)) return [];
559
- return docToBlocks(resultDoc, Math.max(24, contentWidth - 8));
560
- }, [resultDoc, contentWidth]);
561
- const resultDocBlockStarts = useMemo(() => {
562
- const starts = [];
563
- let cursor = 0;
564
- for (let idx = 0; idx < resultDocBlocks.length; idx += 1) {
565
- starts.push(cursor);
566
- cursor += resultDocBlocks[idx].lines.length;
567
- if (idx < resultDocBlocks.length - 1) cursor += 1;
568
- }
569
- return starts;
570
- }, [resultDocBlocks]);
571
- const resultViewport = Math.max(6, panelMaxLines - 4);
572
- const resultMaxOffset = Math.max(0, resultLines.length - resultViewport);
573
-
574
- useEffect(() => {
575
- return () => onResolve?.();
576
- }, [onResolve]);
577
-
578
- async function runMenuAction(value) {
579
- if (value === "exit") {
580
- onResolve?.();
581
- exit();
582
- return;
583
- }
584
-
585
- if (value === "install") {
586
- await run(async () => {
587
- const context = await actions.prepareInstall();
588
- const preSkills = new Set(context.skills.filter((s) => s.checked).map((s) => s.id));
589
- const preAgents = new Set(context.agents.filter((a) => a.checked).map((a) => a.value));
590
- setInstallSkills(context.skills);
591
- setInstallAgents(context.agents.map((a) => a.value));
592
- setSelectedSkills(preSkills);
593
- setSelectedAgents(preAgents);
594
- setCursorIndex(0);
595
- setConfirmPreviewOffset(0);
596
- setInstallPreviewDoc(null);
597
- setMode("install-skills");
598
- });
599
- return;
600
- }
601
-
602
- if (value === "uninstall") {
603
- setConfirmChoice(1);
604
- setMode("confirm-uninstall");
605
- return;
606
- }
607
-
608
- await run(async () => {
609
- const bundle = actions.runCommandBundle ? await actions.runCommandBundle(value) : null;
610
- const doc = bundle?.doc ?? (actions.runCommandDoc ? await actions.runCommandDoc(value) : null);
611
- const body = bundle?.text ?? await actions.runCommand(value);
612
- const menuLabel = menuItems.find((item) => item.value === value)?.label || value;
613
- setResultTitle(menuLabel);
614
- setResultBody(stripAnsi(body));
615
- setResultDoc(isResultDoc(doc) ? doc : null);
616
- setResultOffset(0);
617
- setResultBlockOffset(0);
618
- setResultDocLineOffset(0);
619
- setMode("result");
620
- });
621
- }
622
-
623
- useInput((input, key) => {
624
- if (input === "q") {
625
- onResolve?.();
626
- exit();
627
- return;
628
- }
629
-
630
- if (busy) return;
631
-
632
- if (mode === "menu") {
633
- if (key.upArrow) {
634
- setMenuIndex((current) => (current - 1 + menuItems.length) % menuItems.length);
635
- return;
636
- }
637
- if (key.downArrow) {
638
- setMenuIndex((current) => (current + 1) % menuItems.length);
639
- return;
640
- }
641
- if (key.return) {
642
- void runMenuAction(menuItems[menuIndex].value);
643
- }
644
- return;
645
- }
646
-
647
- if (mode === "result") {
648
- if (key.upArrow) {
649
- setMenuIndex((current) => (current - 1 + menuItems.length) % menuItems.length);
650
- return;
651
- }
652
- if (key.downArrow) {
653
- setMenuIndex((current) => (current + 1) % menuItems.length);
654
- return;
655
- }
656
- if (key.return) {
657
- setMode("menu");
658
- void runMenuAction(menuItems[menuIndex].value);
659
- return;
660
- }
661
- if (key.backspace || key.escape) {
662
- setMode("menu");
663
- return;
664
- }
665
- if (input === "j") {
666
- if (isResultDoc(resultDoc)) {
667
- const totalLines = resultDocBlocks.reduce((sum, block) => sum + block.lines.length, 0) + Math.max(0, resultDocBlocks.length - 1);
668
- const maxLineOffset = Math.max(0, totalLines - Math.max(6, panelMaxLines - 4));
669
- setResultDocLineOffset((current) => {
670
- const next = clamp(current + 1, 0, maxLineOffset);
671
- for (let idx = resultDocBlockStarts.length - 1; idx >= 0; idx -= 1) {
672
- if (next >= (resultDocBlockStarts[idx] || 0)) {
673
- setResultBlockOffset(idx);
674
- break;
675
- }
676
- }
677
- return next;
678
- });
679
- } else {
680
- setResultOffset((current) => clamp(current + 1, 0, resultMaxOffset));
681
- }
682
- return;
683
- }
684
- if (input === "k") {
685
- if (isResultDoc(resultDoc)) {
686
- setResultDocLineOffset((current) => {
687
- const next = clamp(current - 1, 0, Number.MAX_SAFE_INTEGER);
688
- for (let idx = resultDocBlockStarts.length - 1; idx >= 0; idx -= 1) {
689
- if (next >= (resultDocBlockStarts[idx] || 0)) {
690
- setResultBlockOffset(idx);
691
- break;
692
- }
693
- }
694
- return next;
695
- });
696
- } else {
697
- setResultOffset((current) => clamp(current - 1, 0, resultMaxOffset));
698
- }
699
- return;
700
- }
701
- return;
702
- }
703
-
704
- if (mode === "install-skills") {
705
- if (key.upArrow || input === "k") setCursorIndex((i) => clamp(i - 1, 0, Math.max(0, installSkills.length - 1)));
706
- else if (key.downArrow || input === "j") setCursorIndex((i) => clamp(i + 1, 0, Math.max(0, installSkills.length - 1)));
707
- else if (input === " ") {
708
- setSelectedSkills((current) => {
709
- const next = new Set(current);
710
- const id = installSkills[cursorIndex]?.id;
711
- if (!id) return next;
712
- if (next.has(id)) next.delete(id);
713
- else next.add(id);
714
- return next;
715
- });
716
- } else if (key.return) {
717
- setCursorIndex(0);
718
- setMode("install-agents");
719
- } else if (key.backspace || key.escape) {
720
- setCursorIndex(0);
721
- setMode("menu");
722
- }
723
- return;
724
- }
725
-
726
- if (mode === "install-agents") {
727
- if (key.upArrow || input === "k") setCursorIndex((i) => Math.max(0, i - 1));
728
- else if (key.downArrow || input === "j") setCursorIndex((i) => Math.min(installAgents.length - 1, i + 1));
729
- else if (input === " ") {
730
- setSelectedAgents((current) => {
731
- const next = new Set(current);
732
- const id = installAgents[cursorIndex];
733
- if (!id) return next;
734
- if (next.has(id)) next.delete(id);
735
- else next.add(id);
736
- return next;
737
- });
738
- } else if (key.return) {
739
- void run(async () => {
740
- const payload = { selectedSkillIds: [...selectedSkills], selectedAgents: [...selectedAgents] };
741
- const bundle = actions.previewInstallBundle ? await actions.previewInstallBundle(payload) : null;
742
- const preview = bundle?.text ?? await actions.previewInstall(payload);
743
- const doc = bundle?.doc ?? (actions.previewInstallDoc ? await actions.previewInstallDoc(payload) : null);
744
- setInstallPreview(stripAnsi(preview));
745
- setInstallPreviewDoc(isResultDoc(doc) ? doc : null);
746
- setConfirmPreviewOffset(0);
747
- setConfirmChoice(0);
748
- setMode("confirm-install");
749
- });
750
- } else if (key.backspace || key.escape) {
751
- setCursorIndex(0);
752
- setConfirmPreviewOffset(0);
753
- setMode("install-skills");
754
- }
755
- return;
756
- }
757
-
758
- if (mode === "confirm-install") {
759
- if (key.upArrow || key.downArrow) setConfirmChoice((current) => (current === 0 ? 1 : 0));
760
- else if (key.return) {
761
- if (confirmChoice === 1) {
762
- setResultTitle("Ready");
763
- setResultBody("Choose a command from the left.");
764
- setResultDoc(null);
765
- setResultOffset(0);
766
- setResultBlockOffset(0);
767
- setResultDocLineOffset(0);
768
- setMode("menu");
769
- return;
770
- }
771
- void run(async () => {
772
- const payload = { selectedSkillIds: [...selectedSkills], selectedAgents: [...selectedAgents] };
773
- const bundle = actions.applyInstallBundle ? await actions.applyInstallBundle(payload) : null;
774
- const output = bundle?.text ?? await actions.applyInstall(payload);
775
- const doc = bundle?.doc ?? (actions.applyInstallDoc ? await actions.applyInstallDoc(payload) : null);
776
- setResultTitle("Install");
777
- setResultBody(stripAnsi(output));
778
- setResultDoc(isResultDoc(doc) ? doc : null);
779
- setResultOffset(0);
780
- setResultBlockOffset(0);
781
- setResultDocLineOffset(0);
782
- setMode("result");
783
- });
784
- } else if (key.backspace || key.escape) {
785
- setMode("install-agents");
786
- } else if (input === "j") {
787
- setConfirmPreviewOffset((current) => current + 1);
788
- } else if (input === "k") {
789
- setConfirmPreviewOffset((current) => Math.max(0, current - 1));
790
- }
791
- return;
792
- }
793
-
794
- if (mode === "confirm-uninstall") {
795
- if (key.upArrow || key.downArrow) setConfirmChoice((current) => (current === 0 ? 1 : 0));
796
- else if (key.return) {
797
- if (confirmChoice === 1) {
798
- setMode("menu");
799
- return;
800
- }
801
- void run(async () => {
802
- const bundle = actions.runCommandBundle ? await actions.runCommandBundle("uninstall") : null;
803
- const doc = bundle?.doc ?? (actions.runCommandDoc ? await actions.runCommandDoc("uninstall") : null);
804
- const output = bundle?.text ?? await actions.runCommand("uninstall");
805
- setResultTitle("Uninstall");
806
- setResultBody(stripAnsi(output));
807
- setResultDoc(isResultDoc(doc) ? doc : null);
808
- setResultOffset(0);
809
- setResultBlockOffset(0);
810
- setResultDocLineOffset(0);
811
- setMode("result");
812
- });
813
- } else if (key.backspace || key.escape) {
814
- setMode("menu");
815
- } else if (input === "j") {
816
- setConfirmPreviewOffset((current) => current + 1);
817
- } else if (input === "k") {
818
- setConfirmPreviewOffset((current) => Math.max(0, current - 1));
819
- }
820
- }
821
- });
822
-
823
- let rightPanel = isResultDoc(resultDoc)
824
- ? React.createElement(ResultDocPanel, {
825
- title: resultTitle,
826
- blocks: resultDocBlocks,
827
- busy,
828
- maxLines: panelMaxLines,
829
- blockOffset: resultBlockOffset,
830
- lineOffset: resultDocLineOffset
831
- })
832
- : React.createElement(ResultPanel, {
833
- title: resultTitle,
834
- lines: resultLines,
835
- busy,
836
- maxLines: panelMaxLines,
837
- offset: resultOffset
838
- });
839
-
840
- if (mode === "install-skills") {
841
- rightPanel = React.createElement(SkillPickerPanel, {
842
- skills: installSkills,
843
- cursor: cursorIndex,
844
- selectedIds: selectedSkills,
845
- maxLines: panelMaxLines,
846
- width: contentWidth
847
- });
848
- } else if (mode === "install-agents") {
849
- rightPanel = React.createElement(AgentPickerPanel, {
850
- agents: installAgents,
851
- cursor: cursorIndex,
852
- selected: selectedAgents,
853
- maxLines: panelMaxLines
854
- });
855
- } else if (mode === "confirm-install") {
856
- rightPanel = React.createElement(ConfirmPanel, {
857
- title: "Confirm Installation",
858
- preview: installPreview,
859
- previewDoc: installPreviewDoc,
860
- options: ["Apply changes", "Cancel"],
861
- selectedIndex: confirmChoice,
862
- width: contentWidth,
863
- maxLines: panelMaxLines,
864
- offset: confirmPreviewOffset
865
- });
866
- } else if (mode === "confirm-uninstall") {
867
- rightPanel = React.createElement(ConfirmPanel, {
868
- title: "Confirm Uninstall",
869
- preview: "Remove the skilly-hand installation from this project?",
870
- options: ["Remove installation", "Cancel"],
871
- selectedIndex: confirmChoice,
872
- width: contentWidth,
873
- maxLines: panelMaxLines,
874
- offset: confirmPreviewOffset
875
- });
876
- }
877
-
878
- return React.createElement(
879
- Box,
880
- { flexDirection: "column", paddingX: 1, paddingY: 0 },
881
- React.createElement(Header, { appVersion }),
882
- React.createElement(
883
- Box,
884
- { flexDirection: "row" },
885
- React.createElement(MenuPanel, { selectedIndex: menuIndex, menuItems }),
886
- React.createElement(Box, { width: 1 }, React.createElement(Text, null, " ")),
887
- rightPanel
888
- )
889
- );
890
- }
891
-
892
- export async function launchInkApp({ appVersion, actions }) {
893
- return new Promise((resolve, reject) => {
894
- let resolved = false;
895
- const done = () => {
896
- if (resolved) return;
897
- resolved = true;
898
- resolve();
899
- };
900
-
901
- try {
902
- render(
903
- React.createElement(App, {
904
- appVersion,
905
- actions,
906
- onResolve: done
907
- }),
908
- {
909
- exitOnCtrlC: true
910
- }
911
- );
912
- } catch (error) {
913
- reject(error);
914
- }
915
- });
916
- }
917
-
918
- export async function confirmWithInk({ message, defaultValue = false }) {
919
- return new Promise((resolve, reject) => {
920
- function ConfirmApp() {
921
- const { exit } = useApp();
922
- const [choice, setChoice] = useState(defaultValue ? 0 : 1);
923
- useInput((_, key) => {
924
- if (key.leftArrow || key.rightArrow || key.upArrow || key.downArrow) {
925
- setChoice((current) => (current === 0 ? 1 : 0));
926
- return;
927
- }
928
- if (key.return) {
929
- resolve(choice === 0);
930
- exit();
931
- return;
932
- }
933
- if (key.escape) {
934
- resolve(false);
935
- exit();
936
- }
937
- });
938
-
939
- return React.createElement(
940
- Box,
941
- { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 },
942
- React.createElement(Text, { color: "cyan", bold: true }, "Confirmation"),
943
- React.createElement(Text, null, String(message)),
944
- React.createElement(Text, { color: "gray" }, "Use arrows to choose, Enter to confirm."),
945
- React.createElement(
946
- Box,
947
- { marginTop: 1 },
948
- React.createElement(
949
- Text,
950
- { color: choice === 0 ? "black" : "white", backgroundColor: choice === 0 ? "cyan" : undefined },
951
- " Yes "
952
- ),
953
- React.createElement(Text, null, " "),
954
- React.createElement(
955
- Text,
956
- { color: choice === 1 ? "black" : "white", backgroundColor: choice === 1 ? "cyan" : undefined },
957
- " No "
958
- )
959
- )
960
- );
961
- }
962
-
963
- try {
964
- render(React.createElement(ConfirmApp), { exitOnCtrlC: true });
965
- } catch (error) {
966
- reject(error);
967
- }
968
- });
969
- }