@effected/cli-kit 0.0.0-stage → 0.27.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 (103) hide show
  1. package/Cancelled.js +43 -0
  2. package/CliAudience.js +177 -0
  3. package/CliColor.js +66 -0
  4. package/CliEnv.js +101 -0
  5. package/CliExit.js +117 -0
  6. package/CliFailure.js +287 -0
  7. package/CliFormat.js +124 -0
  8. package/CliInteractive.js +94 -0
  9. package/CliLinks.js +153 -0
  10. package/CliLog.js +367 -0
  11. package/CliLogger.js +91 -0
  12. package/CliMessage.js +79 -0
  13. package/CliPrompt.js +110 -0
  14. package/CliRuntime.js +254 -0
  15. package/CliTest.js +240 -0
  16. package/CliTheme.js +142 -0
  17. package/ConfigIssueRenderer.js +62 -0
  18. package/Doc.js +548 -0
  19. package/Fmt.js +295 -0
  20. package/GithubAnnotation.js +39 -0
  21. package/Glyphs.js +82 -0
  22. package/LICENSE +21 -0
  23. package/NotInteractive.js +41 -0
  24. package/README.md +213 -2
  25. package/Render.js +280 -0
  26. package/SchemaIssueRenderer.js +47 -0
  27. package/Status.js +165 -0
  28. package/TestTerminal.js +107 -0
  29. package/Token.js +70 -0
  30. package/index.d.ts +4035 -0
  31. package/index.js +27 -0
  32. package/internal/ExitRequested.js +21 -0
  33. package/internal/HelpRouting.js +63 -0
  34. package/internal/ansi.js +229 -0
  35. package/internal/autoFormat.js +33 -0
  36. package/internal/canPrompt.js +14 -0
  37. package/internal/counts.js +83 -0
  38. package/internal/diagnostics.js +31 -0
  39. package/internal/displayPath.js +46 -0
  40. package/internal/displayWidth.js +42 -0
  41. package/internal/failureTarget.js +195 -0
  42. package/internal/fallbackAnswer.js +17 -0
  43. package/internal/fileSink.js +61 -0
  44. package/internal/format.js +173 -0
  45. package/internal/inspect.js +40 -0
  46. package/internal/isExitCode.js +11 -0
  47. package/internal/layout.js +249 -0
  48. package/internal/linkScheme.js +29 -0
  49. package/internal/linkTarget.js +49 -0
  50. package/internal/logSafety.js +45 -0
  51. package/internal/renderAnsi.js +51 -0
  52. package/internal/renderDoc.js +322 -0
  53. package/internal/renderGithubLog.js +59 -0
  54. package/internal/renderMarkdown.js +367 -0
  55. package/internal/renderPlain.js +49 -0
  56. package/internal/scanAudience.js +105 -0
  57. package/internal/splitFrame.js +55 -0
  58. package/internal/terminalCapabilities.js +23 -0
  59. package/internal/wizardGate.js +17 -0
  60. package/package.json +87 -5
  61. package/testing.d.ts +283 -0
  62. package/testing.js +4 -0
  63. package/tsdoc-metadata.json +11 -0
  64. package/ui/CliUi.js +509 -0
  65. package/ui/CliUiDetached.js +36 -0
  66. package/ui/CliUiLive.js +436 -0
  67. package/ui/CliUiRouteStream.js +72 -0
  68. package/ui/Confirm.js +244 -0
  69. package/ui/DocView.js +76 -0
  70. package/ui/KeyHelp.js +61 -0
  71. package/ui/KeyTable.js +198 -0
  72. package/ui/MultiSelect.js +259 -0
  73. package/ui/Select.js +229 -0
  74. package/ui/Tabs.js +201 -0
  75. package/ui/TextInput.js +289 -0
  76. package/ui/Toggle.js +31 -0
  77. package/ui/UiKey.js +48 -0
  78. package/ui/UiProvider.js +62 -0
  79. package/ui/UiStreams.js +17 -0
  80. package/ui/UiTheme.js +166 -0
  81. package/ui/Viewport.js +203 -0
  82. package/ui/internal/ErrorBoundary.js +29 -0
  83. package/ui/internal/Holder.js +73 -0
  84. package/ui/internal/ScreenContext.js +51 -0
  85. package/ui/internal/UiProviders.js +38 -0
  86. package/ui/internal/ink.js +121 -0
  87. package/ui/internal/inkChalk.js +47 -0
  88. package/ui/internal/inkConsole.js +150 -0
  89. package/ui/internal/lazyView.js +73 -0
  90. package/ui/internal/lineRouter.js +70 -0
  91. package/ui/internal/lineText.js +18 -0
  92. package/ui/internal/mountPermit.js +15 -0
  93. package/ui/internal/processStreams.js +18 -0
  94. package/ui/internal/renderOptions.js +12 -0
  95. package/ui/testing/CliUiTest.js +792 -0
  96. package/ui/testing/fakeStreams.js +104 -0
  97. package/ui/testing/terminalModel.js +67 -0
  98. package/ui-testing-serializer.d.ts +12 -0
  99. package/ui-testing-serializer.js +32 -0
  100. package/ui-testing.d.ts +558 -0
  101. package/ui-testing.js +3 -0
  102. package/ui.d.ts +2013 -0
  103. package/ui.js +17 -0
@@ -0,0 +1,62 @@
1
+ import { formatIssueWithHints } from "./internal/format.js";
2
+
3
+ // src/ConfigIssueRenderer.ts
4
+ /**
5
+ * Turns a `@effected/config-file` `ConfigValidationError` into one line per rejected value.
6
+ *
7
+ * @remarks
8
+ * `ConfigValidationError` carries the structured `issue` tree rather than a
9
+ * string, so a caller holds a tree it has to turn into sentences. This is that
10
+ * step, the same treatment {@link SchemaIssueRenderer} gives a bare issue.
11
+ *
12
+ * `ConfigValidationError.message` names the file but not the value, and the
13
+ * value is the diagnostic: printing only the message tells a user their config
14
+ * is invalid without saying which value is wrong or how it is shaped.
15
+ *
16
+ * The parameter is {@link ConfigIssueSource}, a structural type, so this module
17
+ * names nothing from `@effected/config-file` and the package is not a peer. A
18
+ * real `ConfigValidationError` is assignable to it.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * import { ConfigIssueRenderer } from "@effected/cli-kit"
23
+ * import { Effect } from "effect"
24
+ *
25
+ * const load = configFile.load.pipe(
26
+ * Effect.catchTag("ConfigValidationError", (error) =>
27
+ * Effect.gen(function* () {
28
+ * yield* Effect.logError(String(error))
29
+ * for (const line of ConfigIssueRenderer.render(error)) yield* Effect.logError(` ${line}`)
30
+ * }),
31
+ * ),
32
+ * )
33
+ * ```
34
+ *
35
+ * @public
36
+ */
37
+ var ConfigIssueRenderer = class {
38
+ constructor() {}
39
+ /**
40
+ * One line per rejected value.
41
+ *
42
+ * @remarks
43
+ * Takes the **error**, not its `issue`, because that is what a `catchTag`
44
+ * hands you and because `issue` is typed `Schema.Defect`, which every call
45
+ * site would otherwise have to reach into. Inside
46
+ * `Effect.catchTag("ConfigValidationError", …)` the error is already this
47
+ * shape.
48
+ *
49
+ * An unknown key the schema has a near match for at the same path ends in a
50
+ * hint, `unknown key at theme.colour (did you mean "color"?)`, drawn from the
51
+ * keys the struct declares there (every branch's, under a union) with
52
+ * {@link Fmt.suggest}'s threshold. A key with no near match is drawn exactly
53
+ * as {@link SchemaIssueRenderer} draws it.
54
+ *
55
+ * It cannot throw on a malformed value: the issue tree is validated by
56
+ * a guard before it is read, so a renderer on an error path never becomes the
57
+ * reason a program dies.
58
+ */
59
+ static render = (error) => formatIssueWithHints(error?.issue);
60
+ };
61
+
62
+ export { ConfigIssueRenderer };
package/Doc.js ADDED
@@ -0,0 +1,548 @@
1
+ import { autoFormat } from "./internal/autoFormat.js";
2
+ import { totalOf, visibleCountersOf } from "./internal/counts.js";
3
+ import { Render } from "./Render.js";
4
+ import { Console, Effect } from "effect";
5
+
6
+ // src/Doc.ts
7
+ const isList = (input) => Array.isArray(input);
8
+ const freeze = (value) => Object.freeze(value);
9
+ const frozenArray = (items) => Object.freeze([...items]);
10
+ const text = (value, token) => freeze({
11
+ _tag: "Text",
12
+ value,
13
+ ...token === void 0 ? {} : { token }
14
+ });
15
+ const inlineOne = (part) => typeof part === "string" ? text(part) : part;
16
+ const inlines = (input) => frozenArray(isList(input) ? input.map(inlineOne) : [inlineOne(input)]);
17
+ const overflowOf = (overflow) => (hidden) => inlines(overflow(hidden));
18
+ const overflowFields = (options) => ({
19
+ ...options?.cap === void 0 ? {} : { cap: options.cap },
20
+ ...options?.overflow === void 0 ? {} : { overflow: overflowOf(options.overflow) }
21
+ });
22
+ const treeNode = (input) => freeze({
23
+ label: inlines(input.label),
24
+ children: frozenArray((input.children ?? []).map(treeNode))
25
+ });
26
+ const counterOf = (counter) => freeze({
27
+ ...counter,
28
+ label: typeof counter.label === "string" ? counter.label : freeze({
29
+ one: counter.label.one,
30
+ other: counter.label.other
31
+ }),
32
+ status: freeze({
33
+ name: counter.status.name,
34
+ def: freeze({ ...counter.status.def })
35
+ })
36
+ });
37
+ /**
38
+ * Constructors for the document IR, and two helpers a renderer shares.
39
+ *
40
+ * @remarks
41
+ * Every constructor returns a frozen node and copies the arrays it is given, so editing an input afterwards
42
+ * cannot change a document. An optional field that is not given is absent from the node, not `undefined`.
43
+ * Content arguments accept a string, an `Inline` or an array of either.
44
+ *
45
+ * A node is plain data: nothing decodes or encodes one, so a function field such as `overflow` or `total` is fine
46
+ * and a document is not meant to be serialised.
47
+ *
48
+ * Freezing covers what a `Doc` constructor builds. A literal you write by hand is not frozen, and a `Style` object
49
+ * given as a token is shared by reference (the freeze of a status definition is shallow for the same reason).
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * import { Doc, Status } from "@effected/cli-kit"
54
+ *
55
+ * const report = [
56
+ * Doc.heading(2, "Results"),
57
+ * Doc.paragraph(Doc.status(Status.core, "success"), " ", "3 checks passed"),
58
+ * Doc.table([{ header: "Check" }, { header: "Time", align: "right" }], [["lint", "1.2s"]]),
59
+ * ]
60
+ * // Written for whoever is reading: `yield* Doc.print(report)`
61
+ * ```
62
+ *
63
+ * @public
64
+ */
65
+ var Doc = class {
66
+ constructor() {}
67
+ /**
68
+ * A run of text.
69
+ *
70
+ * @param value - the text
71
+ * @param token - a semantic token or a style to paint it with
72
+ */
73
+ static text(value, token) {
74
+ return text(value, token);
75
+ }
76
+ /**
77
+ * Code in a monospace span.
78
+ *
79
+ * @param value - the code
80
+ */
81
+ static code(value) {
82
+ return freeze({
83
+ _tag: "Code",
84
+ value
85
+ });
86
+ }
87
+ static link(target, label, options) {
88
+ if (target === void 0) return typeof label === "string" ? text(label) : label;
89
+ const fallback = "url" in target ? target.url : target.file;
90
+ return freeze({
91
+ _tag: "Link",
92
+ target: freeze({ ...target }),
93
+ label: inlines(label ?? fallback),
94
+ ...options?.suffix === void 0 ? {} : { suffix: options.suffix }
95
+ });
96
+ }
97
+ /**
98
+ * A status glyph, holding the resolved definition.
99
+ *
100
+ * @remarks
101
+ * A name the vocabulary does not have is a compile error.
102
+ *
103
+ * @param vocab - the vocabulary the name belongs to
104
+ * @param name - a status name in it
105
+ */
106
+ static status(vocab, name) {
107
+ return freeze({
108
+ _tag: "StatusMark",
109
+ name,
110
+ def: vocab.resolve(name)
111
+ });
112
+ }
113
+ /**
114
+ * Content in bold: markdown `**…**`, bold in `ansi`, and the content as is in plain and `githubLog`.
115
+ *
116
+ * @param content - any number of strings, inlines or arrays of them, in order
117
+ */
118
+ static strong(...content) {
119
+ return freeze({
120
+ _tag: "Strong",
121
+ content: inlines(content.flatMap((part) => isList(part) ? part : [part]))
122
+ });
123
+ }
124
+ /**
125
+ * Content in italic: markdown `*…*` (which GFM reads inside a word too), italic in `ansi`, and the content as is in
126
+ * plain and `githubLog`.
127
+ *
128
+ * @param content - any number of strings, inlines or arrays of them, in order
129
+ */
130
+ static em(...content) {
131
+ return freeze({
132
+ _tag: "Emphasis",
133
+ content: inlines(content.flatMap((part) => isList(part) ? part : [part]))
134
+ });
135
+ }
136
+ /**
137
+ * A file path, shown through the context's `displayPath` and never linked.
138
+ *
139
+ * @param path - the path, usually absolute
140
+ */
141
+ static file(path) {
142
+ return freeze({
143
+ _tag: "File",
144
+ path
145
+ });
146
+ }
147
+ /**
148
+ * A path or breadcrumb; a renderer joins the segments with the audience's separator.
149
+ *
150
+ * @param segments - the segments, in order
151
+ */
152
+ static path(...segments) {
153
+ return freeze({
154
+ _tag: "Path",
155
+ segments: frozenArray(segments)
156
+ });
157
+ }
158
+ /**
159
+ * A heading.
160
+ *
161
+ * @param level - 1 to 4
162
+ * @param content - the heading text
163
+ */
164
+ static heading(level, content) {
165
+ return freeze({
166
+ _tag: "Heading",
167
+ level,
168
+ content: inlines(content)
169
+ });
170
+ }
171
+ /**
172
+ * One logical line of content.
173
+ *
174
+ * @param content - any number of strings, inlines or arrays of them, in order
175
+ */
176
+ static paragraph(...content) {
177
+ return freeze({
178
+ _tag: "Paragraph",
179
+ content: inlines(content.flatMap((part) => isList(part) ? part : [part]))
180
+ });
181
+ }
182
+ /**
183
+ * A list of blocks.
184
+ *
185
+ * @param items - the items
186
+ * @param options - `cap`, `overflow`, and `compact` for no blank lines inside an item
187
+ */
188
+ static list(items, options) {
189
+ return freeze({
190
+ _tag: "List",
191
+ items: frozenArray(items),
192
+ ...overflowFields(options),
193
+ ...options?.compact === void 0 ? {} : { compact: options.compact }
194
+ });
195
+ }
196
+ /**
197
+ * A table.
198
+ *
199
+ * @param columns - the columns: a header and an optional alignment each
200
+ * @param rows - the rows; each cell takes a string, an inline or an array of either
201
+ * @param options - `cap`, `overflow`, and `style: "pipe"` for istanbul's shape in plain and `ansi`
202
+ */
203
+ static table(columns, rows, options) {
204
+ return freeze({
205
+ _tag: "Table",
206
+ columns: frozenArray(columns.map((column) => freeze({
207
+ header: inlines(column.header),
208
+ ...column.align === void 0 ? {} : { align: column.align }
209
+ }))),
210
+ rows: frozenArray(rows.map((row) => frozenArray(row.map(inlines)))),
211
+ ...overflowFields(options),
212
+ ...options?.style === void 0 ? {} : { style: options.style }
213
+ });
214
+ }
215
+ /**
216
+ * A tree of labels.
217
+ *
218
+ * @param root - the root; a node's `children` may be left out
219
+ */
220
+ static tree(root) {
221
+ return freeze({
222
+ _tag: "Tree",
223
+ root: treeNode(root)
224
+ });
225
+ }
226
+ /**
227
+ * A titled body a renderer may fold.
228
+ *
229
+ * @param title - the title
230
+ * @param body - the body
231
+ * @param options - `open` asks for it to start unfolded
232
+ */
233
+ static collapsible(title, body, options) {
234
+ return freeze({
235
+ _tag: "Collapsible",
236
+ title: inlines(title),
237
+ body: frozenArray(body),
238
+ ...options?.open === void 0 ? {} : { open: options.open }
239
+ });
240
+ }
241
+ /**
242
+ * A callout.
243
+ *
244
+ * @param kind - `note`, `tip`, `important`, `warning` or `caution`
245
+ * @param body - the body
246
+ */
247
+ static callout(kind, body) {
248
+ return freeze({
249
+ _tag: "Callout",
250
+ kind,
251
+ body: frozenArray(body)
252
+ });
253
+ }
254
+ /**
255
+ * Preformatted text.
256
+ *
257
+ * @param text - the text
258
+ * @param lang - its language, for a renderer that fences it
259
+ */
260
+ static codeBlock(text, lang) {
261
+ return freeze({
262
+ _tag: "CodeBlock",
263
+ ...lang === void 0 ? {} : { lang },
264
+ text
265
+ });
266
+ }
267
+ /**
268
+ * Expected against received text.
269
+ *
270
+ * @param expected - the expected text
271
+ * @param received - the received text
272
+ * @param options - `cap` limits the lines shown
273
+ */
274
+ static diff(expected, received, options) {
275
+ return freeze({
276
+ _tag: "Diff",
277
+ expected,
278
+ received,
279
+ ...options?.cap === void 0 ? {} : { cap: options.cap }
280
+ });
281
+ }
282
+ /**
283
+ * Children under an optional title.
284
+ *
285
+ * @remarks
286
+ * The children are separated by blank lines (unless the document is compact); a title sits directly above the first.
287
+ * `Doc.section(undefined, blocks)` is the way to space a document's top-level blocks, which are otherwise joined with
288
+ * no blank line.
289
+ *
290
+ * @param title - the title, or `undefined` for none
291
+ * @param children - the blocks
292
+ */
293
+ static section(title, children) {
294
+ return freeze({
295
+ _tag: "Section",
296
+ ...title === void 0 ? {} : { title: inlines(title) },
297
+ children: frozenArray(children)
298
+ });
299
+ }
300
+ /**
301
+ * One counter of a `Counts` block, with its status definition resolved.
302
+ *
303
+ * @remarks
304
+ * A name the vocabulary does not have is a compile error.
305
+ *
306
+ * The label is one string, or `{ one, other }` to pluralise by count: `one` when the count is exactly 1 and `other`
307
+ * for every other count, 0 included. A count standing alone reads by its own `n` (`1 change`, `2 changes`); a
308
+ * headline shown as a share of the total reads by that total, the noun it counts (`1/1 repo`, `1/3 repos`,
309
+ * `2/3 repos`).
310
+ *
311
+ * @param vocab - the vocabulary the status belongs to
312
+ * @param name - a status name in it
313
+ * @param options - the counter's `key`, its `label` (one string, or `{ one, other }`), its count `n`, and `showZero`
314
+ * to keep it when `n` is zero
315
+ */
316
+ static counter(vocab, name, options) {
317
+ return counterOf({
318
+ key: options.key,
319
+ label: options.label,
320
+ n: options.n,
321
+ status: {
322
+ name,
323
+ def: vocab.resolve(name)
324
+ },
325
+ ...options.showZero === void 0 ? {} : { showZero: options.showZero }
326
+ });
327
+ }
328
+ /**
329
+ * Counters in one of three layouts.
330
+ *
331
+ * @param options - the counters, the layout and the optional label, total rule, qualifier and duration
332
+ */
333
+ static counts(options) {
334
+ return freeze({
335
+ _tag: "Counts",
336
+ ...options.label === void 0 ? {} : { label: inlines(options.label) },
337
+ counters: frozenArray(options.counters.map(counterOf)),
338
+ ...options.total === void 0 ? {} : { total: options.total },
339
+ ...options.qualifier === void 0 ? {} : { qualifier: inlines(options.qualifier) },
340
+ ...options.durationMs === void 0 ? {} : { durationMs: options.durationMs },
341
+ layout: options.layout,
342
+ ...options.share === void 0 ? {} : { share: options.share },
343
+ ...options.paint === void 0 ? {} : { paint: options.paint },
344
+ ...options.suffix === void 0 ? {} : { suffix: inlines(options.suffix) }
345
+ });
346
+ }
347
+ /**
348
+ * Counters as a table: a row per entry, a column per counter key (in the order the keys first appear, headed by
349
+ * the counter's label), and an optional total row summing each column.
350
+ *
351
+ * @remarks
352
+ * A row without a counter for some key leaves that cell empty, and it counts as zero in the total. A counter whose
353
+ * `n` is zero shows `0`, as a `Doc.table` cell would: a counter's `showZero` has no effect in a table, only in a
354
+ * `Counts` block, so there is no need to set it. `totalRow`
355
+ * labels the total row with a plain `Total` when `true`, or with the content given: for a bold one, pass
356
+ * `totalRow: Doc.strong("Total")`. A column is headed by its counter's `label`; a counter's status paints its cells
357
+ * in `ansi` and is ignored in markdown, so a plain numbers table may pass any status. `labelHeader` heads the label column, which
358
+ * is otherwise empty. When some row has a `durationMs`, a last column shows it with `Fmt.duration`, headed
359
+ * `durationHeader` (`duration` by default); a row without one has an empty cell there and counts as zero in the
360
+ * total row's summed duration.
361
+ *
362
+ * @param rows - each row's label, counters and optional duration
363
+ * @param options - `totalRow`, to add the summed row; the label and duration column headers
364
+ */
365
+ static countsTable(rows, options) {
366
+ const totalRow = options?.totalRow;
367
+ return freeze({
368
+ _tag: "CountsTable",
369
+ rows: frozenArray(rows.map((row) => freeze({
370
+ label: inlines(row.label),
371
+ counters: frozenArray(row.counters.map(counterOf)),
372
+ ...row.durationMs === void 0 ? {} : { durationMs: row.durationMs }
373
+ }))),
374
+ ...totalRow === void 0 ? {} : { totalRow: typeof totalRow === "boolean" ? totalRow : inlines(totalRow) },
375
+ ...options?.labelHeader === void 0 ? {} : { labelHeader: inlines(options.labelHeader) },
376
+ ...options?.durationHeader === void 0 ? {} : { durationHeader: inlines(options.durationHeader) }
377
+ });
378
+ }
379
+ /**
380
+ * Lines, one per entry, in every renderer: markdown joins them with hard breaks so they never collapse into one.
381
+ *
382
+ * @remarks
383
+ * `truncate` and `wrap: false` hold for every entry as they do for {@link Doc.line}: each entry is cut to the width,
384
+ * or kept whole on one line, rather than wrapped. Markdown keeps every entry whole either way.
385
+ *
386
+ * @param lines - the entries; each takes a string, an inline or an array of either
387
+ * @param options - `truncate`, to cut each entry to the width; `wrap: false`, to keep each whole
388
+ */
389
+ static lines(lines, options) {
390
+ return freeze({
391
+ _tag: "Lines",
392
+ lines: frozenArray(lines.map(inlines)),
393
+ ...options?.truncate === void 0 ? {} : { truncate: options.truncate },
394
+ ...options?.wrap === void 0 ? {} : { wrap: options.wrap }
395
+ });
396
+ }
397
+ /**
398
+ * One line of content; with `truncate`, it is cut to the width with the glyph set's ellipsis instead of wrapping,
399
+ * and with `wrap: false` it is kept whole on one line whatever the width.
400
+ *
401
+ * @remarks
402
+ * By default a line longer than the width wraps. `wrap: false` keeps it atomic in every audience and renderer, still
403
+ * carrying its status glyphs, theme tokens and links, which {@link Doc.verbatim} (a plain string) cannot: the tool for
404
+ * a finding such as `✗ path:line:col rule message` that a reader greps or reads line by line, while the prose around
405
+ * it still wraps. A line break inside it is still a space. With both `truncate` and `wrap: false`, `truncate` wins:
406
+ * the line is cut to the width.
407
+ *
408
+ * @param content - the line
409
+ * @param options - `truncate`, to cut it to the width; `wrap: false`, to keep it whole
410
+ */
411
+ static line(content, options) {
412
+ return freeze({
413
+ _tag: "Line",
414
+ content: inlines(content),
415
+ ...options?.truncate === void 0 ? {} : { truncate: options.truncate },
416
+ ...options?.wrap === void 0 ? {} : { wrap: options.wrap }
417
+ });
418
+ }
419
+ /**
420
+ * A unified diff as given, such as a test runner's: sanitized, its `+` and `-` lines painted `success` and
421
+ * `failure` in `ansi`, and a `diff` fence in markdown.
422
+ *
423
+ * @remarks
424
+ * With `truncate`, plain and `ansi` cut each line to the width with the glyph set's ellipsis instead of wrapping
425
+ * it; an agent's or a CI's width is unbounded, so nothing is cut for them unless the context gives a finite width.
426
+ * Markdown keeps every line whole. Inside a compact list item a blank line of the diff keeps the item's indent.
427
+ *
428
+ * A trailing line break ends the last line, as in a unified diff file, and adds no blank line after it: `"a\n"` is
429
+ * one line. To end on a blank line, end the text with two line breaks.
430
+ *
431
+ * @param unified - the diff
432
+ * @param options - `cap`, the most lines shown; `truncate`, to cut each line to the width
433
+ */
434
+ static diffText(unified, options) {
435
+ return freeze({
436
+ _tag: "DiffText",
437
+ text: unified,
438
+ ...options?.cap === void 0 ? {} : { cap: options.cap },
439
+ ...options?.truncate === void 0 ? {} : { truncate: options.truncate }
440
+ });
441
+ }
442
+ /**
443
+ * Lines kept exactly: each indented by `indent` spaces, sanitized, and never wrapped.
444
+ *
445
+ * @remarks
446
+ * Plain, `ansi` and `githubLog` write the lines as they are; markdown fences them, so the indentation survives.
447
+ *
448
+ * It is the tool for a single line that must never wrap nor be cut, whatever the width: {@link Doc.line} wraps at
449
+ * the width, or cuts with `truncate`, and `verbatim` does neither.
450
+ *
451
+ * @param text - the lines
452
+ * @param options - `indent`, the spaces in front of every line; none by default
453
+ */
454
+ static verbatim(text, options) {
455
+ return freeze({
456
+ _tag: "Verbatim",
457
+ text,
458
+ ...options?.indent === void 0 ? {} : { indent: options.indent }
459
+ });
460
+ }
461
+ /**
462
+ * A GitHub Actions annotation: `Render.githubLog` writes it as one workflow command (`::error file=…::message`),
463
+ * and every other renderer writes nothing.
464
+ *
465
+ * @remarks
466
+ * It is the kit's own command, so `githubLog` does not neutralize the command itself; its message and properties are
467
+ * escaped, so no text in them can end the command or start another, and a `##[` in its message, title or file gets
468
+ * a braille pattern blank (U+2800) before the `[`, as in plain text. It is a command where a line starts: at the top level, as a
469
+ * top-level section's child, or as a direct child of a group's body. Nested deeper, it is dropped.
470
+ *
471
+ * @param options - the level, and the optional file, position and title
472
+ * @param message - what it says
473
+ */
474
+ static annotation(options, message) {
475
+ return freeze({
476
+ _tag: "Annotation",
477
+ level: options.level,
478
+ ...options.file === void 0 ? {} : { file: options.file },
479
+ ...options.line === void 0 ? {} : { line: options.line },
480
+ ...options.col === void 0 ? {} : { col: options.col },
481
+ ...options.endLine === void 0 ? {} : { endLine: options.endLine },
482
+ ...options.endColumn === void 0 ? {} : { endColumn: options.endColumn },
483
+ ...options.title === void 0 ? {} : { title: options.title },
484
+ message
485
+ });
486
+ }
487
+ /**
488
+ * The total of a `Counts` block: the caller's rule when it has one, otherwise the sum of `n` over every counter.
489
+ *
490
+ * @remarks
491
+ * The rule sees every counter, including the ones a renderer hides, so hiding never changes the total.
492
+ *
493
+ * @param block - the `Counts` block
494
+ */
495
+ static total(block) {
496
+ return totalOf(block);
497
+ }
498
+ /**
499
+ * The counters a renderer shows: every one except a zero counter that does not ask for `showZero`.
500
+ *
501
+ * @param block - the `Counts` block
502
+ */
503
+ static visibleCounters(block) {
504
+ return visibleCountersOf(block);
505
+ }
506
+ /**
507
+ * Render a document for whoever is running the program and write it to a stream.
508
+ *
509
+ * @remarks
510
+ * The context is {@link Render.context} for the stream, so the width, the colour, the links and the audience
511
+ * come from the services the program already has, and the text is written with `Console.log` or
512
+ * `Console.error`: a test captures it by swapping the `Console`. With `format: "auto"` the renderer follows
513
+ * the audience, and the width is unbounded for an agent, a CI, and a human whose stream is not a terminal.
514
+ *
515
+ * An agent is never written an escape of any kind, even with an explicit `format: "ansi"`: its context is
516
+ * colourless and its links are off. A document that renders to nothing prints nothing.
517
+ *
518
+ * The whole document is written as one `Console.log` (or `Console.error`) call, with its line breaks embedded, so a
519
+ * captured `Console` holds one entry per document, not one per line. Top-level blocks are joined with no blank
520
+ * line between them; wrap them in `Doc.section(undefined, [...])` to space them.
521
+ *
522
+ * `CurrentRuntimeEnv` is read if the environment has one and is not required: a `ci` audience prints
523
+ * GitHub's log format only when it says GitHub Actions, and plain text otherwise, including when it is
524
+ * absent. An explicit `format` is honoured whatever the audience.
525
+ *
526
+ * The context is built here, at print time, so it carries the fiber's count of open Actions log groups
527
+ * (`RenderContext.groupDepth`): printed inside an `ActionLogger.group` from `@effected/github-actions`, a
528
+ * document's top-level collapsible renders as plain text rather than as a second `::group::`, which would end
529
+ * the open one at the runner.
530
+ *
531
+ * @param doc - the document
532
+ * @param options - the stream and the format
533
+ */
534
+ static print = (doc, options) => Effect.gen(function* () {
535
+ const stream = options?.stream ?? "stdout";
536
+ const ctx = yield* Render.context(stream, {
537
+ ...options?.displayPath === void 0 ? {} : { displayPath: options.displayPath },
538
+ ...options?.width === void 0 ? {} : { width: options.width }
539
+ });
540
+ const requested = options?.format ?? "auto";
541
+ const format = requested === "auto" ? yield* autoFormat(ctx.audience) : requested;
542
+ const text = Render[format](doc, ctx);
543
+ if (text === "") return;
544
+ yield* stream === "stderr" ? Console.error(text) : Console.log(text);
545
+ });
546
+ };
547
+
548
+ export { Doc };