@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.
@@ -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: 0 },
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 = formatConfirmPreviewLines(preview, previewWidth);
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 body = await actions.runCommand(value);
392
- setResultTitle(value[0].toUpperCase() + value.slice(1));
393
- setResultBody(body);
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
- setResultOffset((current) => clamp(current + 1, 0, resultMaxOffset));
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
- setResultOffset((current) => clamp(current - 1, 0, resultMaxOffset));
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 preview = await actions.previewInstall({
489
- selectedSkillIds: [...selectedSkills],
490
- selectedAgents: [...selectedAgents]
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 output = await actions.applyInstall({
514
- selectedSkillIds: [...selectedSkills],
515
- selectedAgents: [...selectedAgents]
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 output = await actions.runCommand("uninstall");
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 = React.createElement(ResultPanel, {
557
- title: resultTitle,
558
- lines: resultLines,
559
- busy,
560
- maxLines: panelMaxLines,
561
- offset: resultOffset
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
+
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skilly-hand/core",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }