@effected/cli 0.9.0 → 0.11.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.
Files changed (90) hide show
  1. package/Cancelled.js +44 -0
  2. package/CliAudience.js +178 -0
  3. package/CliColor.js +14 -20
  4. package/CliEnv.js +89 -0
  5. package/CliExit.js +1 -1
  6. package/CliFailure.js +253 -0
  7. package/CliInteractive.js +71 -0
  8. package/CliLinks.js +154 -0
  9. package/CliLog.js +294 -0
  10. package/CliLogger.js +34 -33
  11. package/CliMessage.js +83 -0
  12. package/CliPrompt.js +104 -0
  13. package/CliRuntime.js +104 -54
  14. package/CliTest.js +18 -2
  15. package/CliTheme.js +128 -0
  16. package/ConfigIssueRenderer.js +14 -33
  17. package/Doc.js +512 -0
  18. package/Fmt.js +133 -0
  19. package/GithubAnnotation.js +40 -0
  20. package/Glyphs.js +83 -0
  21. package/NotInteractive.js +42 -0
  22. package/README.md +129 -131
  23. package/Render.js +254 -0
  24. package/SchemaIssueRenderer.js +7 -10
  25. package/Status.js +163 -0
  26. package/TestTerminal.js +80 -0
  27. package/Token.js +69 -0
  28. package/index.d.ts +2923 -171
  29. package/index.js +19 -1
  30. package/internal/HelpRouting.js +1 -1
  31. package/internal/ansi.js +230 -0
  32. package/internal/autoFormat.js +34 -0
  33. package/internal/canPrompt.js +15 -0
  34. package/internal/counts.js +69 -0
  35. package/internal/diagnostics.js +32 -0
  36. package/internal/displayWidth.js +35 -0
  37. package/internal/failureTarget.js +156 -0
  38. package/internal/fallbackAnswer.js +18 -0
  39. package/internal/fileSink.js +62 -0
  40. package/internal/format.js +62 -7
  41. package/internal/layout.js +250 -0
  42. package/internal/linkScheme.js +30 -0
  43. package/internal/linkTarget.js +50 -0
  44. package/internal/logSafety.js +46 -0
  45. package/internal/renderAnsi.js +52 -0
  46. package/internal/renderDoc.js +319 -0
  47. package/internal/renderGithubLog.js +46 -0
  48. package/internal/renderMarkdown.js +367 -0
  49. package/internal/renderPlain.js +50 -0
  50. package/internal/scanAudience.js +106 -0
  51. package/internal/splitFrame.js +56 -0
  52. package/internal/wizardGate.js +18 -0
  53. package/package.json +35 -5
  54. package/testing.d.ts +90 -4
  55. package/testing.js +2 -1
  56. package/ui/CliUi.js +348 -0
  57. package/ui/CliUiLive.js +399 -0
  58. package/ui/Confirm.js +245 -0
  59. package/ui/DocView.js +74 -0
  60. package/ui/KeyHelp.js +62 -0
  61. package/ui/KeyTable.js +199 -0
  62. package/ui/MultiSelect.js +260 -0
  63. package/ui/Select.js +226 -0
  64. package/ui/Tabs.js +202 -0
  65. package/ui/TextInput.js +250 -0
  66. package/ui/Toggle.js +32 -0
  67. package/ui/UiKey.js +44 -0
  68. package/ui/UiProvider.js +60 -0
  69. package/ui/UiStreams.js +18 -0
  70. package/ui/UiTheme.js +119 -0
  71. package/ui/Viewport.js +204 -0
  72. package/ui/internal/ErrorBoundary.js +30 -0
  73. package/ui/internal/Holder.js +74 -0
  74. package/ui/internal/ScreenContext.js +52 -0
  75. package/ui/internal/UiProviders.js +21 -0
  76. package/ui/internal/ink.js +122 -0
  77. package/ui/internal/inkChalk.js +58 -0
  78. package/ui/internal/inkConsole.js +146 -0
  79. package/ui/internal/lineText.js +19 -0
  80. package/ui/internal/mountPermit.js +16 -0
  81. package/ui/internal/perfDrain.js +33 -0
  82. package/ui/internal/processStreams.js +19 -0
  83. package/ui/internal/renderOptions.js +13 -0
  84. package/ui/testing/CliUiTest.js +735 -0
  85. package/ui/testing/fakeStreams.js +76 -0
  86. package/ui/testing/terminalModel.js +59 -0
  87. package/ui-testing.d.ts +446 -0
  88. package/ui-testing.js +3 -0
  89. package/ui.d.ts +1648 -0
  90. package/ui.js +17 -0
@@ -0,0 +1,319 @@
1
+ import { Fmt, sanitize } from "../Fmt.js";
2
+ import { countsTableOf, totalOf, visibleCountersOf } from "./counts.js";
3
+ import { truncateSpans, widthOf, wrapSpans } from "./layout.js";
4
+
5
+ //#region src/internal/renderDoc.ts
6
+ const span = (text, token) => token === void 0 ? { text } : {
7
+ text,
8
+ token
9
+ };
10
+ const textOf = (line) => line.map((s) => s.text).join("");
11
+ /** A link target as plain text: the URL, or `path:line:col` through `displayPath`; a column needs a line. */
12
+ const targetText = (target, ctx) => {
13
+ if ("url" in target) return sanitize(target.url);
14
+ const path = sanitize(ctx.displayPath(target.file));
15
+ if (target.line === void 0) return path;
16
+ return target.col === void 0 ? `${path}:${target.line}` : `${path}:${target.line}:${target.col}`;
17
+ };
18
+ /**
19
+ * Whether a link's target follows its label where the link cannot be followed: the link's own `suffix` when it set one,
20
+ * otherwise only when the label is not already the target's display form.
21
+ */
22
+ const showsSuffix = (suffix, label, target) => suffix ?? label !== target;
23
+ /** Drop trailing spaces, and any span they empty, so a line never ends in padding. */
24
+ const trimLine = (line) => {
25
+ const out = [...line];
26
+ while (out.length > 0) {
27
+ const last = out[out.length - 1];
28
+ if (last.hold === true) break;
29
+ const trimmed = last.text.trimEnd();
30
+ if (trimmed === "") {
31
+ out.pop();
32
+ continue;
33
+ }
34
+ if (trimmed !== last.text) out[out.length - 1] = {
35
+ ...last,
36
+ text: trimmed
37
+ };
38
+ break;
39
+ }
40
+ return out;
41
+ };
42
+ const oneLine = (spans) => spans.map((s) => ({
43
+ ...s,
44
+ text: s.text.replace(/\r\n|\r|\n/g, " ")
45
+ }));
46
+ /** Give every span without a token of its own this one. */
47
+ const toned = (spans, token) => spans.map((s) => s.token === void 0 ? {
48
+ ...s,
49
+ token
50
+ } : s);
51
+ const sliceSpans = (spans, end) => {
52
+ const out = [];
53
+ let left = end;
54
+ for (const s of spans) {
55
+ if (left <= 0) break;
56
+ out.push(s.text.length <= left ? s : {
57
+ ...s,
58
+ text: s.text.slice(0, left)
59
+ });
60
+ left -= s.text.length;
61
+ }
62
+ return out;
63
+ };
64
+ /** The lines of a raw text, sanitized, without the empty line a trailing break would add. */
65
+ const textLines = (text) => {
66
+ const lines = sanitize(text).split(/\r\n|\r|\n/);
67
+ return lines.length > 1 && lines[lines.length - 1] === "" ? lines.slice(0, -1) : lines;
68
+ };
69
+ /** Join chunks with a separator, skipping empty ones. */
70
+ const joinChunks = (chunks, separator) => chunks.filter((chunk) => chunk.length > 0).flatMap((chunk, index) => index === 0 ? chunk : [span(separator), ...chunk]);
71
+ /** Join table cells with a separator, keeping an empty one: a column that is empty in every row still takes its place. */
72
+ const joinCells = (cells, separator) => cells.flatMap((cell, index) => index === 0 ? cell : [span(separator), ...cell]);
73
+ const blank = (width) => span(" ".repeat(width));
74
+ const pad = (spans, width, align) => {
75
+ const gap = Math.max(0, width - widthOf(spans));
76
+ const before = align === "right" ? gap : align === "center" ? Math.floor(gap / 2) : 0;
77
+ const after = gap - before;
78
+ return [
79
+ ...before > 0 ? [blank(before)] : [],
80
+ ...spans,
81
+ ...after > 0 ? [blank(after)] : []
82
+ ];
83
+ };
84
+ /** Put `first` before the first line and `rest` before the others. */
85
+ const hang = (lines, first, rest) => lines.length === 0 ? [trimLine(first)] : lines.map((line, index) => trimLine([...index === 0 ? first : rest, ...line]));
86
+ /** A cap as a whole number of at least zero, or `undefined` for none. */
87
+ const capOf = (cap) => cap === void 0 || Number.isNaN(cap) ? void 0 : Math.max(0, Math.floor(cap));
88
+ const shrink = (widths, limit) => {
89
+ const gaps = 2 * Math.max(0, widths.length - 1);
90
+ const total = () => widths.reduce((sum, w) => sum + w, 0) + gaps;
91
+ while (total() > limit) {
92
+ let widest = 0;
93
+ for (let i = 1; i < widths.length; i++) if (widths[i] > widths[widest]) widest = i;
94
+ if ((widths[widest] ?? 0) <= 1) return;
95
+ widths[widest] = widths[widest] - 1;
96
+ }
97
+ };
98
+ const callouts = {
99
+ note: "info",
100
+ tip: "success",
101
+ important: "accent",
102
+ warning: "warning",
103
+ caution: "error"
104
+ };
105
+ const inline = ({ ctx, flavour }, inlines) => flavour.inline(inlines, ctx);
106
+ const overflowLine = (walk, overflow, hidden) => toned(overflow === void 0 ? [span(`${walk.ctx.glyphs.ellipsis} ${hidden} more`)] : oneLine(inline(walk, overflow(hidden))), "muted");
107
+ /** A table cell is one line: a line break is width 0 to a measure but breaks the row, so a cell keeps its first line. */
108
+ const cellOf = (walk, inlines) => {
109
+ if (inlines === void 0) return [];
110
+ const spans = inline(walk, inlines);
111
+ const text = textOf(spans);
112
+ let end = text.length;
113
+ while (end > 0 && (text[end - 1] === "\n" || text[end - 1] === "\r")) end--;
114
+ const joined = text.slice(0, end);
115
+ const at = joined.search(/\r\n|\r|\n/);
116
+ if (at < 0) return sliceSpans(spans, joined.length);
117
+ return [...sliceSpans(spans, at), span(walk.ctx.glyphs.ellipsis, "muted")];
118
+ };
119
+ const tableLines = (walk, block, width) => {
120
+ const columns = Math.max(block.columns.length, ...block.rows.map((row) => row.length));
121
+ if (columns === 0) return [];
122
+ const cap = capOf(block.cap);
123
+ const shown = cap === void 0 ? block.rows : block.rows.slice(0, cap);
124
+ const header = Array.from({ length: columns }, (_, index) => toned(cellOf(walk, block.columns[index]?.header), "emphasis"));
125
+ const body = shown.map((row) => Array.from({ length: columns }, (_, index) => cellOf(walk, row[index])));
126
+ const showHeader = header.some((cell) => cell.length > 0);
127
+ const hidden = block.rows.length - shown.length;
128
+ if (!showHeader && body.length === 0) return hidden > 0 ? [overflowLine(walk, block.overflow, hidden)] : [];
129
+ const widths = Array.from({ length: columns }, (_, index) => Math.max(0, ...[...showHeader ? [header] : [], ...body].map((row) => widthOf(row[index] ?? []))));
130
+ shrink(widths, width);
131
+ const render = (row) => trimLine(joinCells(row.map((cell, index) => {
132
+ const columnWidth = widths[index];
133
+ const cut = widthOf(cell) > columnWidth ? truncateSpans(cell, columnWidth, walk.ctx.glyphs.ellipsis) : cell;
134
+ return pad(cut, columnWidth, block.columns[index]?.align ?? "left");
135
+ }), " "));
136
+ const rule = joinCells(widths.map((w) => [span("-".repeat(w), "muted")]), " ");
137
+ if (block.style === "pipe") {
138
+ const pipeRule = [span(widths.map((w, index) => "-".repeat(w + (index === 0 ? 1 : 2))).join("|"), "muted")];
139
+ const pipeRow = (row) => trimLine(joinCells(row.map((cell, index) => {
140
+ const columnWidth = widths[index];
141
+ const cut = widthOf(cell) > columnWidth ? truncateSpans(cell, columnWidth, walk.ctx.glyphs.ellipsis) : cell;
142
+ return pad(cut, columnWidth, block.columns[index]?.align ?? "left");
143
+ }), " | "));
144
+ const piped = [
145
+ ...showHeader ? [pipeRule, pipeRow(header)] : [],
146
+ pipeRule,
147
+ ...body.map(pipeRow),
148
+ pipeRule
149
+ ];
150
+ return hidden > 0 ? [...piped, overflowLine(walk, block.overflow, hidden)] : piped;
151
+ }
152
+ const lines = [...showHeader ? [render(header), rule] : [], ...body.map(render)];
153
+ return hidden > 0 ? [...lines, overflowLine(walk, block.overflow, hidden)] : lines;
154
+ };
155
+ const treeLines = (walk, block) => {
156
+ const glyphs = walk.ctx.glyphs.tree;
157
+ const lines = [trimLine(oneLine(inline(walk, block.root.label)))];
158
+ const visit = (children, prefix) => {
159
+ children.forEach((child, index) => {
160
+ const last = index === children.length - 1;
161
+ lines.push(trimLine([span(prefix + (last ? glyphs.last : glyphs.branch), "muted"), ...oneLine(inline(walk, child.label))]));
162
+ visit(child.children, prefix + (last ? glyphs.blank : glyphs.pipe));
163
+ });
164
+ };
165
+ visit(block.root.children, "");
166
+ return lines;
167
+ };
168
+ const diffSide = (walk, marker, token, text, cap) => {
169
+ const lines = textLines(text);
170
+ const shown = cap === void 0 ? lines : lines.slice(0, cap);
171
+ const hidden = lines.length - shown.length;
172
+ return [...shown.map((line) => trimLine([span(`${marker} ${line}`, token)])), ...hidden > 0 ? [[span(` ${walk.ctx.glyphs.ellipsis} ${hidden} more lines`, "muted")]] : []];
173
+ };
174
+ /** What a `Counts` block's `paint` keeps: every token, only a status glyph's, or none. */
175
+ const keepPaint = (paint, lines) => {
176
+ if (paint === void 0 || paint === "all") return lines;
177
+ const unpainted = (s) => {
178
+ const { token: _token, ...rest } = s;
179
+ return rest;
180
+ };
181
+ return lines.map((line) => line.map((s) => paint === "glyph" && s.glyph === true ? s : unpainted(s)));
182
+ };
183
+ /** An annotation is only `githubLog`'s; every other renderer skips it, without leaving a gap where it stood. */
184
+ const isAnnotation = (block) => block._tag === "Annotation";
185
+ const countsLines = (walk, block) => keepPaint(block.paint, countsLayout(walk, block));
186
+ const countsLayout = (walk, block) => {
187
+ const visible = visibleCountersOf(block);
188
+ const total = totalOf(block);
189
+ const label = block.label === void 0 ? [] : toned(oneLine(inline(walk, block.label)), "emphasis");
190
+ const qualifier = block.qualifier === void 0 ? [] : toned(oneLine(inline(walk, block.qualifier)), "muted");
191
+ const duration = block.durationMs === void 0 ? "" : Fmt.duration(block.durationMs);
192
+ const suffix = block.suffix === void 0 ? [] : toned(oneLine(inline(walk, block.suffix)), "muted");
193
+ const nameOf = (counter) => sanitize(counter.label).replace(/\r\n|\r|\n/g, " ");
194
+ const counters = visible.map((counter, index) => {
195
+ const name = nameOf(counter);
196
+ const share = index === 0 && block.share !== false;
197
+ return [span(share ? `${counter.n}/${total} ${name}` : `${counter.n} ${name}`, counter.status.def.token)];
198
+ });
199
+ if (block.layout === "row") return [trimLine(joinChunks([
200
+ label,
201
+ ...counters,
202
+ qualifier,
203
+ duration === "" ? [] : [span(duration, "muted")],
204
+ suffix
205
+ ], " "))];
206
+ if (block.layout === "columns") {
207
+ const labelWidth = Math.max(0, ...visible.map((counter) => widthOf([span(nameOf(counter))])));
208
+ const numberWidth = Math.max(0, ...visible.map((counter) => String(counter.n).length));
209
+ return [
210
+ ...label.length === 0 ? [] : [label],
211
+ ...visible.map((counter) => trimLine([
212
+ ...pad([span(nameOf(counter), counter.status.def.token)], labelWidth, "left"),
213
+ span(" "),
214
+ ...pad([span(String(counter.n), counter.status.def.token)], numberWidth, "right")
215
+ ])),
216
+ ...qualifier.length === 0 ? [] : [qualifier],
217
+ ...duration === "" ? [] : [[span(duration, "muted")]],
218
+ ...suffix.length === 0 ? [] : [suffix]
219
+ ];
220
+ }
221
+ const head = label.length === 0 ? [] : [...label, span(":")];
222
+ const tally = joinChunks(counters, ", ");
223
+ return [trimLine(joinChunks([
224
+ joinChunks([head, tally], " "),
225
+ qualifier,
226
+ duration === "" ? [] : [span(`(${duration})`, "muted")],
227
+ suffix
228
+ ], " "))];
229
+ };
230
+ /**
231
+ * A block as lines. `compact` is set on a compact list's item: a section there joins its title and children with no
232
+ * blank lines between them.
233
+ */
234
+ const blockLines = (walk, block, width, compact = false) => {
235
+ switch (block._tag) {
236
+ case "Heading": return [trimLine(toned(oneLine(inline(walk, block.content)), "emphasis"))];
237
+ case "Paragraph": {
238
+ const spans = inline(walk, block.content);
239
+ if (spans.length === 0) return [[]];
240
+ return wrapSpans(spans, width, { hardBreak: false }).map(trimLine);
241
+ }
242
+ case "List": {
243
+ const cap = capOf(block.cap);
244
+ const items = block.items.filter((item) => !isAnnotation(item));
245
+ const shown = cap === void 0 ? items : items.slice(0, cap);
246
+ const compactItems = block.compact === true;
247
+ const lines = shown.flatMap((item) => hang(blockLines(walk, item, width - 2, compactItems), [span("- ")], [span(" ")]).map((line, index) => compactItems && index > 0 && line.length === 0 ? [{
248
+ text: " ",
249
+ hold: true
250
+ }] : line));
251
+ const hidden = items.length - shown.length;
252
+ return hidden > 0 ? [...lines, overflowLine(walk, block.overflow, hidden)] : lines;
253
+ }
254
+ case "Table": return tableLines(walk, block, width);
255
+ case "Tree": return treeLines(walk, block);
256
+ case "Collapsible": return [trimLine(toned(oneLine(inline(walk, block.title)), "emphasis")), ...block.body.flatMap((child) => blockLines(walk, child, width - 2)).map((line) => trimLine([span(" "), ...line]))];
257
+ case "Callout": {
258
+ const label = `${block.kind.toUpperCase()}:`;
259
+ const hanging = label.length + 1;
260
+ const body = block.body.flatMap((child) => blockLines(walk, child, width - hanging));
261
+ const head = span(label, callouts[block.kind]);
262
+ return body.length === 0 ? [[head]] : hang(body, [head, span(" ")], [blank(hanging)]);
263
+ }
264
+ case "CodeBlock": return textLines(block.text).map((line) => trimLine([span(` ${line}`)]));
265
+ case "Diff": {
266
+ const cap = capOf(block.cap);
267
+ return [...diffSide(walk, "-", "failure", block.expected, cap), ...diffSide(walk, "+", "success", block.received, cap)];
268
+ }
269
+ case "Section": return [...block.title === void 0 ? [] : [[trimLine(toned(oneLine(inline(walk, block.title)), "emphasis"))]], ...block.children.filter((child) => !isAnnotation(child)).map((child) => blockLines(walk, child, width))].flatMap((group, index) => index === 0 || compact ? group : [[], ...group]);
270
+ case "Counts": return countsLines(walk, block);
271
+ case "Verbatim": {
272
+ const indent = " ".repeat(Math.max(0, Math.floor(block.indent ?? 0)));
273
+ return textLines(block.text).map((line) => trimLine([span(`${indent}${line}`)]));
274
+ }
275
+ case "Annotation": return [];
276
+ case "CountsTable": return tableLines(walk, countsTableOf(block), width);
277
+ case "Lines": return block.lines.flatMap((entry) => {
278
+ const spans = inline(walk, entry);
279
+ return spans.length === 0 ? [[]] : wrapSpans(spans, width, { hardBreak: false }).map(trimLine);
280
+ });
281
+ case "Line": {
282
+ const spans = oneLine(inline(walk, block.content));
283
+ if (block.truncate === true) return [trimLine(truncateSpans(spans, width, walk.ctx.glyphs.ellipsis))];
284
+ return spans.length === 0 ? [[]] : wrapSpans(spans, width, { hardBreak: false }).map(trimLine);
285
+ }
286
+ case "DiffText": {
287
+ const cap = capOf(block.cap);
288
+ const lines = textLines(block.text);
289
+ const shown = cap === void 0 ? lines : lines.slice(0, cap);
290
+ const hidden = lines.length - shown.length;
291
+ const tone = (line) => line.startsWith("+") ? "success" : line.startsWith("-") ? "failure" : void 0;
292
+ const cut = (spans) => block.truncate === true ? truncateSpans(spans, width, walk.ctx.glyphs.ellipsis) : spans;
293
+ return [...shown.map((line) => trimLine(cut([span(line, tone(line))]))), ...hidden > 0 ? [[span(`${walk.ctx.glyphs.ellipsis} ${hidden} more lines`, "muted")]] : []];
294
+ }
295
+ }
296
+ };
297
+ /**
298
+ * Render a document to its finished lines in the given flavour: a block that draws nothing contributes no line, where
299
+ * the joined string could not tell it from one empty line.
300
+ *
301
+ * @internal
302
+ */
303
+ const renderDocLines = (doc, ctx, flavour) => {
304
+ const width = Number.isNaN(ctx.width) ? 80 : Math.max(1, ctx.width);
305
+ const walk = {
306
+ ctx,
307
+ flavour
308
+ };
309
+ return doc.flatMap((block) => blockLines(walk, block, width)).map((line) => flavour.finish(trimLine(line), ctx));
310
+ };
311
+ /**
312
+ * Render a document to lines of text in the given flavour.
313
+ *
314
+ * @internal
315
+ */
316
+ const renderDoc = (doc, ctx, flavour) => renderDocLines(doc, ctx, flavour).join("\n");
317
+
318
+ //#endregion
319
+ export { capOf, isAnnotation, renderDoc, renderDocLines, showsSuffix, targetText, textLines, trimLine };
@@ -0,0 +1,46 @@
1
+ import { sanitize } from "../Fmt.js";
2
+ import { plainInline, renderPlainLines } from "./renderPlain.js";
3
+ import { CommandNeutralizer, WorkflowCommand } from "@effected/github-commands";
4
+
5
+ //#region src/internal/renderGithubLog.ts
6
+ /** Plain lines, neutralized; a block that draws nothing, such as an empty table, gives no line at all. */
7
+ const plainLines = (blocks, ctx) => renderPlainLines(blocks, ctx).flatMap((line) => CommandNeutralizer.lines(line));
8
+ /** An annotation as the kit's own command, on the trusted path: escaped by `WorkflowCommand`, never neutralized. */
9
+ const annotationLine = (block) => WorkflowCommand[block.level](sanitize(block.message), {
10
+ ...block.title === void 0 ? {} : { title: sanitize(block.title) },
11
+ ...block.file === void 0 ? {} : { file: sanitize(block.file) },
12
+ ...block.line === void 0 ? {} : { startLine: block.line },
13
+ ...block.endLine === void 0 ? {} : { endLine: block.endLine },
14
+ ...block.col === void 0 ? {} : { startColumn: block.col },
15
+ ...block.endColumn === void 0 ? {} : { endColumn: block.endColumn }
16
+ });
17
+ /** A group's body: plain text, except an annotation, which is still a command inside a group. */
18
+ const bodyLines = (blocks, ctx) => blocks.flatMap((block) => block._tag === "Annotation" ? [annotationLine(block)] : plainLines([block], ctx));
19
+ const blockLines = (block, ctx) => {
20
+ switch (block._tag) {
21
+ case "Annotation": return [annotationLine(block)];
22
+ case "Collapsible": {
23
+ const title = plainInline(block.title, ctx).map((span) => span.text).join("");
24
+ return [
25
+ WorkflowCommand.group(title),
26
+ ...bodyLines(block.body, ctx),
27
+ WorkflowCommand.endGroup()
28
+ ];
29
+ }
30
+ case "Section": return [...block.title === void 0 ? [] : [plainLines([{
31
+ _tag: "Heading",
32
+ level: 1,
33
+ content: block.title
34
+ }], ctx)], ...block.children.map((child) => blockLines(child, ctx))].flatMap((group, index) => index === 0 ? group : ["", ...group]);
35
+ default: return plainLines([block], ctx);
36
+ }
37
+ };
38
+ /**
39
+ * Render a document for a GitHub Actions log: plain text, with a top-level collapsible as a group.
40
+ *
41
+ * @internal
42
+ */
43
+ const renderGithubLog = (doc, ctx) => doc.flatMap((block) => blockLines(block, ctx)).join("\n");
44
+
45
+ //#endregion
46
+ export { renderGithubLog };