@skilly-hand/skilly-hand 0.21.0 → 0.22.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/CHANGELOG.md +32 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/packages/catalog/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/bin.js +339 -140
- package/packages/cli/src/ink-ui.js +304 -27
- package/packages/cli/src/result-doc.js +58 -0
- package/packages/core/package.json +1 -1
- package/packages/core/src/index.js +312 -14
- package/packages/core/src/terminal.js +5 -3
- package/packages/core/src/ui/layout.js +11 -7
- package/packages/core/src/ui/theme.js +16 -2
- package/packages/detectors/package.json +1 -1
|
@@ -2,6 +2,12 @@ import React, { useEffect, useMemo, useState } from "react";
|
|
|
2
2
|
import { Box, Text, render, useApp, useInput } from "ink";
|
|
3
3
|
import { getBrand } from "../../core/src/ui/brand.js";
|
|
4
4
|
|
|
5
|
+
const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
|
|
6
|
+
|
|
7
|
+
function stripAnsi(value) {
|
|
8
|
+
return String(value || "").replace(ANSI_PATTERN, "");
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
function wrapText(text, width) {
|
|
6
12
|
const safeWidth = Math.max(20, width || 80);
|
|
7
13
|
const input = String(text || "").split("\n");
|
|
@@ -43,6 +49,16 @@ function formatConfirmPreviewLines(preview, width) {
|
|
|
43
49
|
return [...head, ...body];
|
|
44
50
|
}
|
|
45
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
|
+
|
|
46
62
|
function clamp(value, min, max) {
|
|
47
63
|
return Math.min(Math.max(value, min), max);
|
|
48
64
|
}
|
|
@@ -133,7 +149,7 @@ function Header({ appVersion }) {
|
|
|
133
149
|
...logo.map((line, idx) => React.createElement(Text, { key: `logo-${idx}`, color: "cyan" }, line)),
|
|
134
150
|
React.createElement(
|
|
135
151
|
Box,
|
|
136
|
-
{ marginTop:
|
|
152
|
+
{ marginTop: 1 },
|
|
137
153
|
React.createElement(Text, { color: "cyan", bold: true }, `${brand.name}`),
|
|
138
154
|
React.createElement(Text, { color: "gray" }, appVersion ? ` v${appVersion}` : ""),
|
|
139
155
|
React.createElement(Text, { color: "gray" }, ` ${brand.tagline}`)
|
|
@@ -184,6 +200,186 @@ function ResultPanel({ title, lines, busy, maxLines, offset }) {
|
|
|
184
200
|
);
|
|
185
201
|
}
|
|
186
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
|
+
|
|
187
383
|
function computeSkillBlock({ skill, focused, chosen, contentWidth }) {
|
|
188
384
|
const safeWidth = Math.max(20, contentWidth);
|
|
189
385
|
const tagsText = ` tags: ${skill.tags.join(", ") || "none"}`;
|
|
@@ -283,9 +479,11 @@ function AgentPickerPanel({ agents, cursor, selected, maxLines }) {
|
|
|
283
479
|
);
|
|
284
480
|
}
|
|
285
481
|
|
|
286
|
-
function ConfirmPanel({ title, preview, options, selectedIndex, width, maxLines, offset }) {
|
|
482
|
+
function ConfirmPanel({ title, preview, previewDoc, options, selectedIndex, width, maxLines, offset }) {
|
|
287
483
|
const previewWidth = Math.max(24, (width || 80) - 10);
|
|
288
|
-
const previewLines =
|
|
484
|
+
const previewLines = previewDoc
|
|
485
|
+
? formatConfirmPreviewFromDoc(previewDoc, previewWidth)
|
|
486
|
+
: formatConfirmPreviewLines(preview, previewWidth);
|
|
289
487
|
const viewport = Math.max(5, maxLines - 14);
|
|
290
488
|
const maxOffset = Math.max(0, previewLines.length - viewport);
|
|
291
489
|
const safeOffset = clamp(offset, 0, maxOffset);
|
|
@@ -322,6 +520,7 @@ function ConfirmPanel({ title, preview, options, selectedIndex, width, maxLines,
|
|
|
322
520
|
function App({ appVersion, actions, onResolve }) {
|
|
323
521
|
const menuItems = [
|
|
324
522
|
{ value: "install", label: "Install" },
|
|
523
|
+
{ value: "native-setup", label: "Native Setup" },
|
|
325
524
|
{ value: "detect", label: "Detect" },
|
|
326
525
|
{ value: "list", label: "List" },
|
|
327
526
|
{ value: "doctor", label: "Doctor" },
|
|
@@ -334,7 +533,10 @@ function App({ appVersion, actions, onResolve }) {
|
|
|
334
533
|
const [menuIndex, setMenuIndex] = useState(0);
|
|
335
534
|
const [resultTitle, setResultTitle] = useState("Ready");
|
|
336
535
|
const [resultBody, setResultBody] = useState("Choose a command from the left.");
|
|
536
|
+
const [resultDoc, setResultDoc] = useState(null);
|
|
337
537
|
const [resultOffset, setResultOffset] = useState(0);
|
|
538
|
+
const [resultBlockOffset, setResultBlockOffset] = useState(0);
|
|
539
|
+
const [resultDocLineOffset, setResultDocLineOffset] = useState(0);
|
|
338
540
|
|
|
339
541
|
const [installSkills, setInstallSkills] = useState([]);
|
|
340
542
|
const [installAgents, setInstallAgents] = useState([]);
|
|
@@ -343,6 +545,7 @@ function App({ appVersion, actions, onResolve }) {
|
|
|
343
545
|
const [cursorIndex, setCursorIndex] = useState(0);
|
|
344
546
|
const [confirmChoice, setConfirmChoice] = useState(0);
|
|
345
547
|
const [installPreview, setInstallPreview] = useState("");
|
|
548
|
+
const [installPreviewDoc, setInstallPreviewDoc] = useState(null);
|
|
346
549
|
const [confirmPreviewOffset, setConfirmPreviewOffset] = useState(0);
|
|
347
550
|
|
|
348
551
|
const { busy, run } = useAsyncAction();
|
|
@@ -351,6 +554,20 @@ function App({ appVersion, actions, onResolve }) {
|
|
|
351
554
|
const contentWidth = Math.max(60, stdoutWidth - 36);
|
|
352
555
|
const panelMaxLines = Math.max(12, stdoutRows - 10);
|
|
353
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]);
|
|
354
571
|
const resultViewport = Math.max(6, panelMaxLines - 4);
|
|
355
572
|
const resultMaxOffset = Math.max(0, resultLines.length - resultViewport);
|
|
356
573
|
|
|
@@ -376,6 +593,7 @@ function App({ appVersion, actions, onResolve }) {
|
|
|
376
593
|
setSelectedAgents(preAgents);
|
|
377
594
|
setCursorIndex(0);
|
|
378
595
|
setConfirmPreviewOffset(0);
|
|
596
|
+
setInstallPreviewDoc(null);
|
|
379
597
|
setMode("install-skills");
|
|
380
598
|
});
|
|
381
599
|
return;
|
|
@@ -388,10 +606,16 @@ function App({ appVersion, actions, onResolve }) {
|
|
|
388
606
|
}
|
|
389
607
|
|
|
390
608
|
await run(async () => {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
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);
|
|
394
616
|
setResultOffset(0);
|
|
617
|
+
setResultBlockOffset(0);
|
|
618
|
+
setResultDocLineOffset(0);
|
|
395
619
|
setMode("result");
|
|
396
620
|
});
|
|
397
621
|
}
|
|
@@ -439,11 +663,39 @@ function App({ appVersion, actions, onResolve }) {
|
|
|
439
663
|
return;
|
|
440
664
|
}
|
|
441
665
|
if (input === "j") {
|
|
442
|
-
|
|
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
|
+
}
|
|
443
682
|
return;
|
|
444
683
|
}
|
|
445
684
|
if (input === "k") {
|
|
446
|
-
|
|
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
|
+
}
|
|
447
699
|
return;
|
|
448
700
|
}
|
|
449
701
|
return;
|
|
@@ -485,11 +737,12 @@ function App({ appVersion, actions, onResolve }) {
|
|
|
485
737
|
});
|
|
486
738
|
} else if (key.return) {
|
|
487
739
|
void run(async () => {
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
setInstallPreview(preview);
|
|
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);
|
|
493
746
|
setConfirmPreviewOffset(0);
|
|
494
747
|
setConfirmChoice(0);
|
|
495
748
|
setMode("confirm-install");
|
|
@@ -506,17 +759,26 @@ function App({ appVersion, actions, onResolve }) {
|
|
|
506
759
|
if (key.upArrow || key.downArrow) setConfirmChoice((current) => (current === 0 ? 1 : 0));
|
|
507
760
|
else if (key.return) {
|
|
508
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);
|
|
509
768
|
setMode("menu");
|
|
510
769
|
return;
|
|
511
770
|
}
|
|
512
771
|
void run(async () => {
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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);
|
|
517
776
|
setResultTitle("Install");
|
|
518
|
-
setResultBody(output);
|
|
777
|
+
setResultBody(stripAnsi(output));
|
|
778
|
+
setResultDoc(isResultDoc(doc) ? doc : null);
|
|
519
779
|
setResultOffset(0);
|
|
780
|
+
setResultBlockOffset(0);
|
|
781
|
+
setResultDocLineOffset(0);
|
|
520
782
|
setMode("result");
|
|
521
783
|
});
|
|
522
784
|
} else if (key.backspace || key.escape) {
|
|
@@ -537,10 +799,15 @@ function App({ appVersion, actions, onResolve }) {
|
|
|
537
799
|
return;
|
|
538
800
|
}
|
|
539
801
|
void run(async () => {
|
|
540
|
-
const
|
|
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");
|
|
541
805
|
setResultTitle("Uninstall");
|
|
542
|
-
setResultBody(output);
|
|
806
|
+
setResultBody(stripAnsi(output));
|
|
807
|
+
setResultDoc(isResultDoc(doc) ? doc : null);
|
|
543
808
|
setResultOffset(0);
|
|
809
|
+
setResultBlockOffset(0);
|
|
810
|
+
setResultDocLineOffset(0);
|
|
544
811
|
setMode("result");
|
|
545
812
|
});
|
|
546
813
|
} else if (key.backspace || key.escape) {
|
|
@@ -553,13 +820,22 @@ function App({ appVersion, actions, onResolve }) {
|
|
|
553
820
|
}
|
|
554
821
|
});
|
|
555
822
|
|
|
556
|
-
let rightPanel =
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
+
});
|
|
563
839
|
|
|
564
840
|
if (mode === "install-skills") {
|
|
565
841
|
rightPanel = React.createElement(SkillPickerPanel, {
|
|
@@ -580,6 +856,7 @@ function App({ appVersion, actions, onResolve }) {
|
|
|
580
856
|
rightPanel = React.createElement(ConfirmPanel, {
|
|
581
857
|
title: "Confirm Installation",
|
|
582
858
|
preview: installPreview,
|
|
859
|
+
previewDoc: installPreviewDoc,
|
|
583
860
|
options: ["Apply changes", "Cancel"],
|
|
584
861
|
selectedIndex: confirmChoice,
|
|
585
862
|
width: contentWidth,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
function asString(value) {
|
|
2
|
+
if (value === null || value === undefined) return "";
|
|
3
|
+
return String(value);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function createResultDoc(title, sections = []) {
|
|
7
|
+
return { type: "result-doc", title, sections };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function section(title, blocks = []) {
|
|
11
|
+
return { type: "section", title, blocks };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function textBlock(text) {
|
|
15
|
+
return { type: "text", text: asString(text) };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function kvBlock(entries) {
|
|
19
|
+
return { type: "kv", entries: entries || [] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function tableBlock(columns, rows) {
|
|
23
|
+
return { type: "table", columns: columns || [], rows: rows || [] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function listBlock(items, bullet) {
|
|
27
|
+
return { type: "list", items: items || [], bullet };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function statusBlock(level, message, detail = "") {
|
|
31
|
+
return { type: "status", level, message, detail };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderBlock(renderer, block) {
|
|
35
|
+
if (!block) return "";
|
|
36
|
+
if (block.type === "text") return asString(block.text);
|
|
37
|
+
if (block.type === "kv") return renderer.kv(block.entries);
|
|
38
|
+
if (block.type === "table") return renderer.table(block.columns, block.rows);
|
|
39
|
+
if (block.type === "list") return renderer.list(block.items, block.bullet ? { bullet: block.bullet } : {});
|
|
40
|
+
if (block.type === "status") return renderer.status(block.level, block.message, block.detail);
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function renderResultDocText(renderer, appVersion, doc, options = {}) {
|
|
45
|
+
const includeBanner = options.includeBanner !== false;
|
|
46
|
+
const blocks = [];
|
|
47
|
+
if (includeBanner) {
|
|
48
|
+
blocks.push(renderer.banner(appVersion));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const sec of doc.sections || []) {
|
|
52
|
+
const body = renderer.joinBlocks((sec.blocks || []).map((block) => renderBlock(renderer, block)));
|
|
53
|
+
blocks.push(renderer.section(sec.title, body));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return renderer.joinBlocks(blocks);
|
|
57
|
+
}
|
|
58
|
+
|