@skilly-hand/skilly-hand 0.18.0 → 0.20.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.
@@ -0,0 +1,692 @@
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
+ function wrapText(text, width) {
6
+ const safeWidth = Math.max(20, width || 80);
7
+ const input = String(text || "").split("\n");
8
+ const lines = [];
9
+ for (const rawLine of input) {
10
+ const line = rawLine.trimEnd();
11
+ if (!line) {
12
+ lines.push("");
13
+ continue;
14
+ }
15
+ let rest = line;
16
+ while (rest.length > safeWidth) {
17
+ let split = rest.lastIndexOf(" ", safeWidth);
18
+ if (split <= 0) split = safeWidth;
19
+ lines.push(rest.slice(0, split));
20
+ rest = rest.slice(split).trimStart();
21
+ }
22
+ lines.push(rest);
23
+ }
24
+ return lines;
25
+ }
26
+
27
+ function truncateLine(text, maxWidth) {
28
+ const safeWidth = Math.max(4, maxWidth || 40);
29
+ const chars = Array.from(String(text || ""));
30
+ if (chars.length <= safeWidth) return chars.join("");
31
+ if (safeWidth <= 4) return chars.slice(0, safeWidth).join("");
32
+ return `${chars.slice(0, safeWidth - 1).join("")}…`;
33
+ }
34
+
35
+ function formatConfirmPreviewLines(preview, width) {
36
+ const rawLines = String(preview || "").split("\n");
37
+ const bodyStart = rawLines.findIndex((line) =>
38
+ /Install Preflight|Detection Summary|Catalog Summary|Doctor Summary|Findings|Skill Plan/.test(line)
39
+ );
40
+ const headEnd = bodyStart > 0 ? bodyStart : Math.min(12, rawLines.length);
41
+ const head = rawLines.slice(0, headEnd).map((line) => truncateLine(line, width));
42
+ const body = rawLines.slice(headEnd).flatMap((line) => wrapText(line, width));
43
+ return [...head, ...body];
44
+ }
45
+
46
+ function clamp(value, min, max) {
47
+ return Math.min(Math.max(value, min), max);
48
+ }
49
+
50
+ function computeWindow(total, visible, cursor) {
51
+ if (total <= 0) return { start: 0, end: 0 };
52
+ const safeVisible = Math.max(1, Math.min(visible, total));
53
+ const half = Math.floor(safeVisible / 2);
54
+ let start = cursor - half;
55
+ start = clamp(start, 0, Math.max(0, total - safeVisible));
56
+ return { start, end: start + safeVisible };
57
+ }
58
+
59
+ function fitWindowByLineBudget(blocks, cursor, availableLines) {
60
+ const total = blocks.length;
61
+ if (total <= 0) return { start: 0, end: 0, usedLines: 0 };
62
+
63
+ const safeCursor = clamp(cursor, 0, total - 1);
64
+ const budget = Math.max(1, availableLines);
65
+
66
+ let start = safeCursor;
67
+ const focusLines = Math.max(1, blocks[safeCursor].lineCount);
68
+ const desiredTopLines = Math.max(0, Math.floor((budget - focusLines) / 2));
69
+
70
+ let consumedTop = 0;
71
+ while (start > 0) {
72
+ const next = Math.max(1, blocks[start - 1].lineCount);
73
+ if (consumedTop + next > desiredTopLines) break;
74
+ start -= 1;
75
+ consumedTop += next;
76
+ }
77
+
78
+ let end = start;
79
+ let usedLines = 0;
80
+ while (end < total) {
81
+ const next = Math.max(1, blocks[end].lineCount);
82
+ if (usedLines + next > budget && end > start) break;
83
+ usedLines += next;
84
+ end += 1;
85
+ if (usedLines >= budget && end > start) break;
86
+ }
87
+
88
+ if (safeCursor >= end) {
89
+ start = safeCursor;
90
+ end = safeCursor;
91
+ usedLines = 0;
92
+ while (end < total) {
93
+ const next = Math.max(1, blocks[end].lineCount);
94
+ if (usedLines + next > budget && end > start) break;
95
+ usedLines += next;
96
+ end += 1;
97
+ if (usedLines >= budget && end > start) break;
98
+ }
99
+ }
100
+
101
+ while (start > 0) {
102
+ const next = Math.max(1, blocks[start - 1].lineCount);
103
+ if (usedLines + next > budget) break;
104
+ start -= 1;
105
+ usedLines += next;
106
+ }
107
+
108
+ return { start, end, usedLines };
109
+ }
110
+
111
+ function useAsyncAction() {
112
+ const [busy, setBusy] = useState(false);
113
+
114
+ async function run(action) {
115
+ setBusy(true);
116
+ try {
117
+ return await action();
118
+ } finally {
119
+ setBusy(false);
120
+ }
121
+ }
122
+
123
+ return { busy, run };
124
+ }
125
+
126
+ function Header({ appVersion }) {
127
+ const brand = getBrand();
128
+ const logo = brand.logo.unicode;
129
+
130
+ return React.createElement(
131
+ Box,
132
+ { flexDirection: "column", marginBottom: 1 },
133
+ ...logo.map((line, idx) => React.createElement(Text, { key: `logo-${idx}`, color: "cyan" }, line)),
134
+ React.createElement(
135
+ Box,
136
+ { marginTop: 0 },
137
+ React.createElement(Text, { color: "cyan", bold: true }, `${brand.name}`),
138
+ React.createElement(Text, { color: "gray" }, appVersion ? ` v${appVersion}` : ""),
139
+ React.createElement(Text, { color: "gray" }, ` ${brand.tagline}`)
140
+ )
141
+ );
142
+ }
143
+
144
+ function MenuPanel({ selectedIndex, menuItems }) {
145
+ return React.createElement(
146
+ Box,
147
+ { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, paddingY: 0, width: 32 },
148
+ React.createElement(Text, { color: "cyan", bold: true }, "Command Hub"),
149
+ ...menuItems.map((item, idx) => {
150
+ const active = idx === selectedIndex;
151
+ return React.createElement(
152
+ Text,
153
+ { key: item.value, color: active ? "black" : "white", backgroundColor: active ? "cyan" : undefined },
154
+ `${active ? "›" : " "} ${item.label}`
155
+ );
156
+ }),
157
+ React.createElement(Text, { color: "gray" }, ""),
158
+ React.createElement(Text, { color: "gray" }, "↑↓ move Enter select q quit")
159
+ );
160
+ }
161
+
162
+ function ResultPanel({ title, lines, busy, maxLines, offset }) {
163
+ const viewportLines = Math.max(6, maxLines - 4);
164
+ const maxOffset = Math.max(0, lines.length - viewportLines);
165
+ const safeOffset = clamp(offset, 0, maxOffset);
166
+ const visible = lines.slice(safeOffset, safeOffset + viewportLines);
167
+ const from = lines.length ? safeOffset + 1 : 0;
168
+ const to = safeOffset + visible.length;
169
+
170
+ return React.createElement(
171
+ Box,
172
+ { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, flexGrow: 1 },
173
+ React.createElement(Text, { color: "cyan", bold: true }, title),
174
+ busy ? React.createElement(Text, { color: "yellow" }, "Working...") : null,
175
+ ...visible.map((line, idx) =>
176
+ React.createElement(Text, { key: `line-${idx}`, color: idx === 0 ? "white" : "gray" }, line)
177
+ ),
178
+ React.createElement(Text, { color: "cyan" }, "─".repeat(Math.max(12, 56))),
179
+ React.createElement(
180
+ Text,
181
+ { color: "gray" },
182
+ `Lines ${from}-${to} of ${lines.length} | ↑/↓ or j/k scroll`
183
+ )
184
+ );
185
+ }
186
+
187
+ function computeSkillBlock({ skill, focused, chosen, contentWidth }) {
188
+ const safeWidth = Math.max(20, contentWidth);
189
+ const tagsText = ` tags: ${skill.tags.join(", ") || "none"}`;
190
+ const agentsText = ` agents: ${skill.agentSupport.join(", ") || "none"}`;
191
+ const tagsLines = wrapText(tagsText, safeWidth);
192
+ const agentsLines = wrapText(agentsText, safeWidth);
193
+ const header = `${focused ? "›" : " "} [${chosen ? "x" : " "}] ${skill.id} | ${skill.title}`;
194
+ const lines = [header, ...tagsLines, ...agentsLines];
195
+ return {
196
+ skill,
197
+ lines,
198
+ lineCount: lines.length
199
+ };
200
+ }
201
+
202
+ export function buildSkillBlocksForRender({ skills, cursor, selectedIds, contentWidth }) {
203
+ return skills.map((skill, idx) =>
204
+ computeSkillBlock({
205
+ skill,
206
+ focused: idx === cursor,
207
+ chosen: selectedIds.has(skill.id),
208
+ contentWidth
209
+ })
210
+ );
211
+ }
212
+
213
+ export function computeSkillWindow({ blocks, cursor, availableLines }) {
214
+ return fitWindowByLineBudget(blocks, cursor, availableLines);
215
+ }
216
+
217
+ function SkillPickerPanel({ skills, cursor, selectedIds, maxLines, width }) {
218
+ const contentWidth = Math.max(20, (width || 80) - 6);
219
+ const blocks = buildSkillBlocksForRender({
220
+ skills,
221
+ cursor,
222
+ selectedIds,
223
+ contentWidth
224
+ });
225
+ const availableLines = Math.max(1, maxLines - 5);
226
+ const window = computeSkillWindow({ blocks, cursor, availableLines });
227
+ const chunk = blocks.slice(window.start, window.end);
228
+
229
+ return React.createElement(
230
+ Box,
231
+ { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, flexGrow: 1 },
232
+ React.createElement(Text, { color: "cyan", bold: true }, "Install: Select Skills"),
233
+ ...chunk.map((block, localIdx) => {
234
+ const idx = window.start + localIdx;
235
+ return React.createElement(
236
+ Box,
237
+ { key: block.skill.id, flexDirection: "column" },
238
+ ...block.lines.map((line, lineIdx) =>
239
+ React.createElement(Text, {
240
+ key: `${block.skill.id}-${lineIdx}`,
241
+ color: lineIdx === 0 ? (idx === cursor ? "black" : "white") : "gray",
242
+ backgroundColor: lineIdx === 0 && idx === cursor ? "cyan" : undefined
243
+ }, line)
244
+ )
245
+ );
246
+ }),
247
+ React.createElement(Text, { color: "cyan" }, "─".repeat(Math.max(12, 56))),
248
+ React.createElement(
249
+ Text,
250
+ { color: "gray" },
251
+ `Skills ${window.start + 1}-${window.end} of ${skills.length}`
252
+ ),
253
+ React.createElement(Text, { color: "gray" }, "↑/↓ or j/k move, Space toggle, Enter continue, Backspace cancel")
254
+ );
255
+ }
256
+
257
+ function AgentPickerPanel({ agents, cursor, selected, maxLines }) {
258
+ const visibleAgents = Math.max(1, maxLines - 5);
259
+ const window = computeWindow(agents.length, visibleAgents, cursor);
260
+ const chunk = agents.slice(window.start, window.end);
261
+
262
+ return React.createElement(
263
+ Box,
264
+ { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, flexGrow: 1 },
265
+ React.createElement(Text, { color: "cyan", bold: true }, "Install: Select Assistants"),
266
+ ...chunk.map((agent, localIdx) => {
267
+ const idx = window.start + localIdx;
268
+ const focused = idx === cursor;
269
+ const chosen = selected.has(agent);
270
+ return React.createElement(
271
+ Text,
272
+ { key: agent, color: focused ? "black" : "white", backgroundColor: focused ? "cyan" : undefined },
273
+ `${focused ? "›" : " "} [${chosen ? "x" : " "}] ${agent}`
274
+ );
275
+ }),
276
+ React.createElement(Text, { color: "cyan" }, "─".repeat(Math.max(12, 56))),
277
+ React.createElement(
278
+ Text,
279
+ { color: "gray" },
280
+ `Assistants ${window.start + 1}-${window.end} of ${agents.length}`
281
+ ),
282
+ React.createElement(Text, { color: "gray" }, "↑/↓ or j/k move, Space toggle, Enter continue, Backspace back")
283
+ );
284
+ }
285
+
286
+ function ConfirmPanel({ title, preview, options, selectedIndex, width, maxLines, offset }) {
287
+ const previewWidth = Math.max(24, (width || 80) - 10);
288
+ const previewLines = formatConfirmPreviewLines(preview, previewWidth);
289
+ const viewport = Math.max(5, maxLines - 14);
290
+ const maxOffset = Math.max(0, previewLines.length - viewport);
291
+ const safeOffset = clamp(offset, 0, maxOffset);
292
+ const visible = previewLines.slice(safeOffset, safeOffset + viewport);
293
+ const from = previewLines.length ? safeOffset + 1 : 0;
294
+ const to = safeOffset + visible.length;
295
+
296
+ return React.createElement(
297
+ Box,
298
+ { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, flexGrow: 1 },
299
+ React.createElement(Text, { color: "cyan", bold: true }, title),
300
+ React.createElement(Text, { color: "cyan" }, "Action"),
301
+ ...options.map((opt, idx) => {
302
+ const active = idx === selectedIndex;
303
+ return React.createElement(
304
+ Text,
305
+ { key: opt, color: active ? "black" : "white", backgroundColor: active ? "cyan" : undefined },
306
+ `${active ? "›" : " "} ${opt}`
307
+ );
308
+ }),
309
+ React.createElement(Text, { color: "cyan" }, "─".repeat(Math.max(12, 56))),
310
+ React.createElement(Text, { color: "cyan" }, "Install Snapshot"),
311
+ React.createElement(
312
+ Box,
313
+ { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, paddingY: 0 },
314
+ ...visible.map((line, idx) => React.createElement(Text, { key: `preview-${idx}`, color: "gray" }, line))
315
+ ),
316
+ React.createElement(Text, { color: "cyan" }, "─".repeat(Math.max(12, 56))),
317
+ React.createElement(Text, { color: "gray" }, `Preview ${from}-${to} of ${previewLines.length}`),
318
+ React.createElement(Text, { color: "gray" }, "↑/↓ choose, Enter confirm, Backspace cancel, j/k scroll")
319
+ );
320
+ }
321
+
322
+ function App({ appVersion, actions, onResolve }) {
323
+ const menuItems = [
324
+ { value: "install", label: "Install" },
325
+ { value: "detect", label: "Detect" },
326
+ { value: "list", label: "List" },
327
+ { value: "doctor", label: "Doctor" },
328
+ { value: "uninstall", label: "Uninstall" },
329
+ { value: "exit", label: "Exit" }
330
+ ];
331
+
332
+ const { exit } = useApp();
333
+ const [mode, setMode] = useState("menu");
334
+ const [menuIndex, setMenuIndex] = useState(0);
335
+ const [resultTitle, setResultTitle] = useState("Ready");
336
+ const [resultBody, setResultBody] = useState("Choose a command from the left.");
337
+ const [resultOffset, setResultOffset] = useState(0);
338
+
339
+ const [installSkills, setInstallSkills] = useState([]);
340
+ const [installAgents, setInstallAgents] = useState([]);
341
+ const [selectedSkills, setSelectedSkills] = useState(new Set());
342
+ const [selectedAgents, setSelectedAgents] = useState(new Set());
343
+ const [cursorIndex, setCursorIndex] = useState(0);
344
+ const [confirmChoice, setConfirmChoice] = useState(0);
345
+ const [installPreview, setInstallPreview] = useState("");
346
+ const [confirmPreviewOffset, setConfirmPreviewOffset] = useState(0);
347
+
348
+ const { busy, run } = useAsyncAction();
349
+ const stdoutWidth = Number(process.stdout?.columns || 120);
350
+ const stdoutRows = Number(process.stdout?.rows || 40);
351
+ const contentWidth = Math.max(60, stdoutWidth - 36);
352
+ const panelMaxLines = Math.max(12, stdoutRows - 10);
353
+ const resultLines = useMemo(() => wrapText(resultBody, contentWidth - 6), [resultBody, contentWidth]);
354
+ const resultViewport = Math.max(6, panelMaxLines - 4);
355
+ const resultMaxOffset = Math.max(0, resultLines.length - resultViewport);
356
+
357
+ useEffect(() => {
358
+ return () => onResolve?.();
359
+ }, [onResolve]);
360
+
361
+ async function runMenuAction(value) {
362
+ if (value === "exit") {
363
+ onResolve?.();
364
+ exit();
365
+ return;
366
+ }
367
+
368
+ if (value === "install") {
369
+ await run(async () => {
370
+ const context = await actions.prepareInstall();
371
+ const preSkills = new Set(context.skills.filter((s) => s.checked).map((s) => s.id));
372
+ const preAgents = new Set(context.agents.filter((a) => a.checked).map((a) => a.value));
373
+ setInstallSkills(context.skills);
374
+ setInstallAgents(context.agents.map((a) => a.value));
375
+ setSelectedSkills(preSkills);
376
+ setSelectedAgents(preAgents);
377
+ setCursorIndex(0);
378
+ setConfirmPreviewOffset(0);
379
+ setMode("install-skills");
380
+ });
381
+ return;
382
+ }
383
+
384
+ if (value === "uninstall") {
385
+ setConfirmChoice(1);
386
+ setMode("confirm-uninstall");
387
+ return;
388
+ }
389
+
390
+ await run(async () => {
391
+ const body = await actions.runCommand(value);
392
+ setResultTitle(value[0].toUpperCase() + value.slice(1));
393
+ setResultBody(body);
394
+ setResultOffset(0);
395
+ setMode("result");
396
+ });
397
+ }
398
+
399
+ useInput((input, key) => {
400
+ if (input === "q") {
401
+ onResolve?.();
402
+ exit();
403
+ return;
404
+ }
405
+
406
+ if (busy) return;
407
+
408
+ if (mode === "menu") {
409
+ if (key.upArrow) {
410
+ setMenuIndex((current) => (current - 1 + menuItems.length) % menuItems.length);
411
+ return;
412
+ }
413
+ if (key.downArrow) {
414
+ setMenuIndex((current) => (current + 1) % menuItems.length);
415
+ return;
416
+ }
417
+ if (key.return) {
418
+ void runMenuAction(menuItems[menuIndex].value);
419
+ }
420
+ return;
421
+ }
422
+
423
+ if (mode === "result") {
424
+ if (key.upArrow) {
425
+ setMenuIndex((current) => (current - 1 + menuItems.length) % menuItems.length);
426
+ return;
427
+ }
428
+ if (key.downArrow) {
429
+ setMenuIndex((current) => (current + 1) % menuItems.length);
430
+ return;
431
+ }
432
+ if (key.return) {
433
+ setMode("menu");
434
+ void runMenuAction(menuItems[menuIndex].value);
435
+ return;
436
+ }
437
+ if (key.backspace || key.escape) {
438
+ setMode("menu");
439
+ return;
440
+ }
441
+ if (input === "j") {
442
+ setResultOffset((current) => clamp(current + 1, 0, resultMaxOffset));
443
+ return;
444
+ }
445
+ if (input === "k") {
446
+ setResultOffset((current) => clamp(current - 1, 0, resultMaxOffset));
447
+ return;
448
+ }
449
+ return;
450
+ }
451
+
452
+ if (mode === "install-skills") {
453
+ if (key.upArrow || input === "k") setCursorIndex((i) => clamp(i - 1, 0, Math.max(0, installSkills.length - 1)));
454
+ else if (key.downArrow || input === "j") setCursorIndex((i) => clamp(i + 1, 0, Math.max(0, installSkills.length - 1)));
455
+ else if (input === " ") {
456
+ setSelectedSkills((current) => {
457
+ const next = new Set(current);
458
+ const id = installSkills[cursorIndex]?.id;
459
+ if (!id) return next;
460
+ if (next.has(id)) next.delete(id);
461
+ else next.add(id);
462
+ return next;
463
+ });
464
+ } else if (key.return) {
465
+ setCursorIndex(0);
466
+ setMode("install-agents");
467
+ } else if (key.backspace || key.escape) {
468
+ setCursorIndex(0);
469
+ setMode("menu");
470
+ }
471
+ return;
472
+ }
473
+
474
+ if (mode === "install-agents") {
475
+ if (key.upArrow || input === "k") setCursorIndex((i) => Math.max(0, i - 1));
476
+ else if (key.downArrow || input === "j") setCursorIndex((i) => Math.min(installAgents.length - 1, i + 1));
477
+ else if (input === " ") {
478
+ setSelectedAgents((current) => {
479
+ const next = new Set(current);
480
+ const id = installAgents[cursorIndex];
481
+ if (!id) return next;
482
+ if (next.has(id)) next.delete(id);
483
+ else next.add(id);
484
+ return next;
485
+ });
486
+ } else if (key.return) {
487
+ void run(async () => {
488
+ const preview = await actions.previewInstall({
489
+ selectedSkillIds: [...selectedSkills],
490
+ selectedAgents: [...selectedAgents]
491
+ });
492
+ setInstallPreview(preview);
493
+ setConfirmPreviewOffset(0);
494
+ setConfirmChoice(0);
495
+ setMode("confirm-install");
496
+ });
497
+ } else if (key.backspace || key.escape) {
498
+ setCursorIndex(0);
499
+ setConfirmPreviewOffset(0);
500
+ setMode("install-skills");
501
+ }
502
+ return;
503
+ }
504
+
505
+ if (mode === "confirm-install") {
506
+ if (key.upArrow || key.downArrow) setConfirmChoice((current) => (current === 0 ? 1 : 0));
507
+ else if (key.return) {
508
+ if (confirmChoice === 1) {
509
+ setMode("menu");
510
+ return;
511
+ }
512
+ void run(async () => {
513
+ const output = await actions.applyInstall({
514
+ selectedSkillIds: [...selectedSkills],
515
+ selectedAgents: [...selectedAgents]
516
+ });
517
+ setResultTitle("Install");
518
+ setResultBody(output);
519
+ setResultOffset(0);
520
+ setMode("result");
521
+ });
522
+ } else if (key.backspace || key.escape) {
523
+ setMode("install-agents");
524
+ } else if (input === "j") {
525
+ setConfirmPreviewOffset((current) => current + 1);
526
+ } else if (input === "k") {
527
+ setConfirmPreviewOffset((current) => Math.max(0, current - 1));
528
+ }
529
+ return;
530
+ }
531
+
532
+ if (mode === "confirm-uninstall") {
533
+ if (key.upArrow || key.downArrow) setConfirmChoice((current) => (current === 0 ? 1 : 0));
534
+ else if (key.return) {
535
+ if (confirmChoice === 1) {
536
+ setMode("menu");
537
+ return;
538
+ }
539
+ void run(async () => {
540
+ const output = await actions.runCommand("uninstall");
541
+ setResultTitle("Uninstall");
542
+ setResultBody(output);
543
+ setResultOffset(0);
544
+ setMode("result");
545
+ });
546
+ } else if (key.backspace || key.escape) {
547
+ setMode("menu");
548
+ } else if (input === "j") {
549
+ setConfirmPreviewOffset((current) => current + 1);
550
+ } else if (input === "k") {
551
+ setConfirmPreviewOffset((current) => Math.max(0, current - 1));
552
+ }
553
+ }
554
+ });
555
+
556
+ let rightPanel = React.createElement(ResultPanel, {
557
+ title: resultTitle,
558
+ lines: resultLines,
559
+ busy,
560
+ maxLines: panelMaxLines,
561
+ offset: resultOffset
562
+ });
563
+
564
+ if (mode === "install-skills") {
565
+ rightPanel = React.createElement(SkillPickerPanel, {
566
+ skills: installSkills,
567
+ cursor: cursorIndex,
568
+ selectedIds: selectedSkills,
569
+ maxLines: panelMaxLines,
570
+ width: contentWidth
571
+ });
572
+ } else if (mode === "install-agents") {
573
+ rightPanel = React.createElement(AgentPickerPanel, {
574
+ agents: installAgents,
575
+ cursor: cursorIndex,
576
+ selected: selectedAgents,
577
+ maxLines: panelMaxLines
578
+ });
579
+ } else if (mode === "confirm-install") {
580
+ rightPanel = React.createElement(ConfirmPanel, {
581
+ title: "Confirm Installation",
582
+ preview: installPreview,
583
+ options: ["Apply changes", "Cancel"],
584
+ selectedIndex: confirmChoice,
585
+ width: contentWidth,
586
+ maxLines: panelMaxLines,
587
+ offset: confirmPreviewOffset
588
+ });
589
+ } else if (mode === "confirm-uninstall") {
590
+ rightPanel = React.createElement(ConfirmPanel, {
591
+ title: "Confirm Uninstall",
592
+ preview: "Remove the skilly-hand installation from this project?",
593
+ options: ["Remove installation", "Cancel"],
594
+ selectedIndex: confirmChoice,
595
+ width: contentWidth,
596
+ maxLines: panelMaxLines,
597
+ offset: confirmPreviewOffset
598
+ });
599
+ }
600
+
601
+ return React.createElement(
602
+ Box,
603
+ { flexDirection: "column", paddingX: 1, paddingY: 0 },
604
+ React.createElement(Header, { appVersion }),
605
+ React.createElement(
606
+ Box,
607
+ { flexDirection: "row" },
608
+ React.createElement(MenuPanel, { selectedIndex: menuIndex, menuItems }),
609
+ React.createElement(Box, { width: 1 }, React.createElement(Text, null, " ")),
610
+ rightPanel
611
+ )
612
+ );
613
+ }
614
+
615
+ export async function launchInkApp({ appVersion, actions }) {
616
+ return new Promise((resolve, reject) => {
617
+ let resolved = false;
618
+ const done = () => {
619
+ if (resolved) return;
620
+ resolved = true;
621
+ resolve();
622
+ };
623
+
624
+ try {
625
+ render(
626
+ React.createElement(App, {
627
+ appVersion,
628
+ actions,
629
+ onResolve: done
630
+ }),
631
+ {
632
+ exitOnCtrlC: true
633
+ }
634
+ );
635
+ } catch (error) {
636
+ reject(error);
637
+ }
638
+ });
639
+ }
640
+
641
+ export async function confirmWithInk({ message, defaultValue = false }) {
642
+ return new Promise((resolve, reject) => {
643
+ function ConfirmApp() {
644
+ const { exit } = useApp();
645
+ const [choice, setChoice] = useState(defaultValue ? 0 : 1);
646
+ useInput((_, key) => {
647
+ if (key.leftArrow || key.rightArrow || key.upArrow || key.downArrow) {
648
+ setChoice((current) => (current === 0 ? 1 : 0));
649
+ return;
650
+ }
651
+ if (key.return) {
652
+ resolve(choice === 0);
653
+ exit();
654
+ return;
655
+ }
656
+ if (key.escape) {
657
+ resolve(false);
658
+ exit();
659
+ }
660
+ });
661
+
662
+ return React.createElement(
663
+ Box,
664
+ { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 },
665
+ React.createElement(Text, { color: "cyan", bold: true }, "Confirmation"),
666
+ React.createElement(Text, null, String(message)),
667
+ React.createElement(Text, { color: "gray" }, "Use arrows to choose, Enter to confirm."),
668
+ React.createElement(
669
+ Box,
670
+ { marginTop: 1 },
671
+ React.createElement(
672
+ Text,
673
+ { color: choice === 0 ? "black" : "white", backgroundColor: choice === 0 ? "cyan" : undefined },
674
+ " Yes "
675
+ ),
676
+ React.createElement(Text, null, " "),
677
+ React.createElement(
678
+ Text,
679
+ { color: choice === 1 ? "black" : "white", backgroundColor: choice === 1 ? "cyan" : undefined },
680
+ " No "
681
+ )
682
+ )
683
+ );
684
+ }
685
+
686
+ try {
687
+ render(React.createElement(ConfirmApp), { exitOnCtrlC: true });
688
+ } catch (error) {
689
+ reject(error);
690
+ }
691
+ });
692
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skilly-hand/core",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }