@effected/cli-kit 0.0.0-stage → 0.26.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 +59 -0
  4. package/CliEnv.js +101 -0
  5. package/CliExit.js +64 -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 +206 -0
  16. package/CliTheme.js +142 -0
  17. package/ConfigIssueRenderer.js +63 -0
  18. package/Doc.js +548 -0
  19. package/Fmt.js +335 -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 +215 -2
  25. package/Render.js +280 -0
  26. package/SchemaIssueRenderer.js +47 -0
  27. package/Status.js +165 -0
  28. package/TestTerminal.js +101 -0
  29. package/Token.js +68 -0
  30. package/index.d.ts +3951 -0
  31. package/index.js +27 -0
  32. package/internal/ExitRequested.js +21 -0
  33. package/internal/HelpRouting.js +35 -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 +175 -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 +90 -5
  61. package/testing.d.ts +246 -0
  62. package/testing.js +4 -0
  63. package/tsdoc-metadata.json +11 -0
  64. package/ui/CliUi.js +507 -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 +147 -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 +1990 -0
  103. package/ui.js +17 -0
package/Fmt.js ADDED
@@ -0,0 +1,335 @@
1
+ import { displayWidth, graphemes, isEscapeOnly, stripAnsi } from "./internal/displayWidth.js";
2
+ import { Duration, Option } from "effect";
3
+ import { ErrorText } from "@effected/kit";
4
+
5
+ // src/Fmt.ts
6
+ /**
7
+ * The Levenshtein distance between `a` and `b` when it is at most `limit`, and `limit + 1` otherwise.
8
+ *
9
+ * Only the diagonal band of the matrix within `limit` of it is computed (a cell outside it is past `limit` already),
10
+ * over two rows reused in turn, and a row whose every cell is past `limit` ends the walk, since no later row can come
11
+ * back under it: the cost is the length of `a` times `2 * limit + 1`.
12
+ */
13
+ const boundedDistance = (a, b, limit) => {
14
+ const over = limit + 1;
15
+ if (Math.abs(a.length - b.length) > limit) return over;
16
+ const n = b.length;
17
+ const at = (row, j) => row[j] ?? over;
18
+ let previous = Array.from({ length: n + 1 }, (_, j) => j <= limit ? j : over);
19
+ let current = new Array(n + 1).fill(over);
20
+ for (let i = 1; i <= a.length; i++) {
21
+ const lo = Math.max(1, i - limit);
22
+ const hi = Math.min(n, i + limit);
23
+ const edge = lo === 1 && i <= limit ? i : over;
24
+ current[lo - 1] = edge;
25
+ let rowMin = edge;
26
+ for (let j = lo; j <= hi; j++) {
27
+ const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
28
+ const cell = Math.min(at(previous, j) + 1, at(current, j - 1) + 1, at(previous, j - 1) + cost, over);
29
+ current[j] = cell;
30
+ if (cell < rowMin) rowMin = cell;
31
+ }
32
+ if (hi < n) current[hi + 1] = over;
33
+ if (rowMin > limit) return over;
34
+ [previous, current] = [current, previous];
35
+ }
36
+ return Math.min(at(previous, n), over);
37
+ };
38
+ const CONTROL = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g;
39
+ /**
40
+ * Text made safe to lay out and print: complete ANSI and OSC sequences removed, every remaining control character
41
+ * removed (a stray ESC, BEL, BS, DEL, the C1 range) except line feed and carriage return, and a tab turned into a
42
+ * space.
43
+ *
44
+ * @remarks
45
+ * Stripping complete sequences is not enough on its own: a lone ESC survives it, and so does whatever would
46
+ * complete a sequence once two adjacent pieces of text are joined (`ESC` in one node, `[31m` in the next). With
47
+ * every ESC and C0 or C1 control gone after the strip, nothing can reassemble, so the width of the text equals what
48
+ * a terminal shows. A tab counts as no columns but draws up to eight, hence the space.
49
+ *
50
+ * @internal
51
+ */
52
+ const sanitize = (input) => stripAnsi(input).replace(/\t/g, " ").replace(CONTROL, "");
53
+ /**
54
+ * Small, pure formatting primitives for terminal output.
55
+ *
56
+ * @public
57
+ */
58
+ var Fmt = class {
59
+ constructor() {}
60
+ /**
61
+ * Text made safe to lay out and print, exactly as the renderers make it: complete ANSI and OSC sequences removed,
62
+ * every other control character removed (a stray ESC, BEL, BS, DEL, the C1 range) except line feed and carriage
63
+ * return, and a tab turned into a space.
64
+ *
65
+ * @remarks
66
+ * For a `render` or any other text a program builds by hand from data it does not control: an error message, a
67
+ * file name, a value from the environment. Line breaks are kept, so split on them if the text must be one line.
68
+ *
69
+ * @param text - the text
70
+ */
71
+ static sanitize = sanitize;
72
+ /**
73
+ * The display width of `text` in terminal columns: wide East Asian characters and emoji count two,
74
+ * combining marks and ANSI escapes count none.
75
+ *
76
+ * @param text - the text to measure
77
+ */
78
+ static width = (text) => displayWidth(text);
79
+ /**
80
+ * Cut `text` to at most `width` columns, ending in `ellipsis` when it was cut.
81
+ *
82
+ * @remarks
83
+ * Grapheme-safe: a grapheme, such as a wide character or an emoji ZWJ family, is kept whole or dropped
84
+ * whole, so the result is never wider than `width`. Text that already fits is returned unchanged, colour
85
+ * escapes included. Text that must be cut is cut as plain text: its ANSI escapes are dropped rather than cut
86
+ * in half. When the ellipsis cannot fit, it is omitted and the text is cut to `width`; a `width` of 0 or
87
+ * less gives `""`.
88
+ *
89
+ * @param text - the text to truncate
90
+ * @param width - the most columns the result may take
91
+ * @param options - the ellipsis
92
+ */
93
+ static truncate = (text, width, options) => {
94
+ const limit = Number.isFinite(width) ? Math.floor(width) : width === Number.POSITIVE_INFINITY ? Infinity : 0;
95
+ if (limit <= 0) return "";
96
+ if (displayWidth(text) <= limit) return text;
97
+ const plain = stripAnsi(text);
98
+ const ellipsis = options?.ellipsis ?? "…";
99
+ const ellipsisWidth = displayWidth(ellipsis);
100
+ const useEllipsis = ellipsisWidth <= limit && ellipsis !== "";
101
+ const budget = useEllipsis ? limit - ellipsisWidth : limit;
102
+ let out = "";
103
+ let used = 0;
104
+ for (const grapheme of graphemes(plain)) {
105
+ const w = displayWidth(grapheme);
106
+ if (used + w > budget) break;
107
+ out += grapheme;
108
+ used += w;
109
+ }
110
+ return useEllipsis ? `${out}${ellipsis}` : out;
111
+ };
112
+ /**
113
+ * A duration as short human text: `250ms`, `1.2s`, `2s`, `1m 3s`, `2m`, `1h 2m`, `2h`.
114
+ *
115
+ * @remarks
116
+ * Under a second it is whole milliseconds; under a minute, seconds to one decimal with a trailing `.0`
117
+ * dropped; under an hour, minutes and whole seconds, with the seconds dropped when zero; from an hour, hours and
118
+ * whole minutes, the seconds dropped. A value that rounds up to the next unit (`999.6` to a second, `59999` to a
119
+ * minute, `3599999` to an hour) is written in that unit, so `1000ms`, `60s` and `60m` never appear. There is no
120
+ * days unit. The format is bespoke: core's `Duration.format` renders differently.
121
+ *
122
+ * The input is anything `Duration.Input` admits. A plain number is milliseconds and is used as given, never
123
+ * converted through a `Duration`, which rounds a fractional millisecond to whole nanoseconds and would move a value
124
+ * such as `999.4999999` across the rounding boundary. A `bigint` is nanoseconds, as in core. A negative, infinite
125
+ * or invalid input (a string `Duration.Input` cannot parse) is `0ms`: a formatter in a render path never throws.
126
+ *
127
+ * @param input - the duration; a number is milliseconds
128
+ */
129
+ static duration = (input) => {
130
+ const ms = typeof input === "number" ? input : Option.match(Duration.fromInput(input), {
131
+ onNone: () => 0,
132
+ onSome: Duration.toMillis
133
+ });
134
+ const value = Number.isFinite(ms) ? Math.max(0, ms) : 0;
135
+ const whole = Math.round(value);
136
+ if (whole < 1e3) return `${whole}ms`;
137
+ const tenths = Math.round(value / 100);
138
+ if (tenths < 600) return `${Number((tenths / 10).toFixed(1))}s`;
139
+ const seconds = Math.round(value / 1e3);
140
+ if (seconds >= 3600) {
141
+ const hours = Math.floor(seconds / 3600);
142
+ const remaining = Math.floor(seconds % 3600 / 60);
143
+ return remaining === 0 ? `${hours}h` : `${hours}h ${remaining}m`;
144
+ }
145
+ const minutes = Math.floor(seconds / 60);
146
+ const rest = seconds % 60;
147
+ return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`;
148
+ };
149
+ /**
150
+ * A ratio from 0 to 1 as a percentage, or a number already from 0 to 100 with `scale: 100`: `83.3%`, `50%`,
151
+ * `100%`.
152
+ *
153
+ * @remarks
154
+ * Rounded to `digits` decimal places (one by default) with trailing zeros dropped, so whole values print
155
+ * without a decimal. A value that rounds to zero prints `0%`, never `-0%`. The input is not validated.
156
+ *
157
+ * @param n - the ratio, 0 to 1 (0 to 100 with `scale: 100`)
158
+ * @param options - the decimal places and the scale
159
+ */
160
+ static percent = (n, options) => {
161
+ const digits = Math.min(10, Math.max(0, Math.floor(options?.digits ?? 1)));
162
+ const percent = options?.scale === 100 ? n : n * 100;
163
+ return `${Number(percent.toFixed(digits))}%`;
164
+ };
165
+ /**
166
+ * A count with its noun: `1 test`, `2 tests`, `0 tests`.
167
+ *
168
+ * @param n - the count
169
+ * @param singular - the noun for exactly one
170
+ * @param plural - the noun otherwise; `singular` with an `s` appended by default
171
+ */
172
+ static plural = (n, singular, plural) => `${n} ${n === 1 ? singular : plural ?? `${singular}s`}`;
173
+ /**
174
+ * Pad `text` to `width` terminal columns, measured as {@link Fmt.width} measures: a wide character counts two and
175
+ * an escape sequence none, so coloured text pads to the same column as plain text.
176
+ *
177
+ * @remarks
178
+ * Text already `width` columns or wider is returned unchanged: compose with {@link Fmt.truncate} to cut it. A
179
+ * `width` that is not a finite number pads nothing. Never throws.
180
+ *
181
+ * @param text - the text to pad
182
+ * @param width - the columns the result should take
183
+ * @param options - the alignment and the fill
184
+ */
185
+ static pad = (text, width, options) => {
186
+ const missing = Number.isFinite(width) ? Math.floor(width) - displayWidth(text) : 0;
187
+ if (missing <= 0) return text;
188
+ const fill = options?.fill !== void 0 && displayWidth(options.fill) === 1 && stripAnsi(options.fill) === options.fill ? options.fill : " ";
189
+ switch (options?.align) {
190
+ case "right": return `${fill.repeat(missing)}${text}`;
191
+ case "center": {
192
+ const before = Math.floor(missing / 2);
193
+ return `${fill.repeat(before)}${text}${fill.repeat(missing - before)}`;
194
+ }
195
+ default: return `${text}${fill.repeat(missing)}`;
196
+ }
197
+ };
198
+ /**
199
+ * `text` without its escape sequences: CSI (colour, cursor moves, erases), OSC (titles, OSC 8 hyperlinks) ending in
200
+ * BEL or ST, the DCS, SOS, PM and APC strings, and every other escape, the nF sequences (`ESC ( B`) and the
201
+ * two-byte ones (`ESC 7`, `ESC 8`, `ESC =`, `ESC c`).
202
+ *
203
+ * @remarks
204
+ * The same strip {@link Fmt.width} and {@link Fmt.sanitize} start from. Only complete sequences go: a lone ESC, or
205
+ * any other control character, is kept, and so is an ESC in front of a lowercase letter other than `c`, which a
206
+ * stray ESC before a word would otherwise cost the word its first letter. An unterminated CSI or OSC keeps its `[`
207
+ * or `]`. Use {@link Fmt.sanitize} for text that will be laid out and printed.
208
+ *
209
+ * @param text - the text
210
+ */
211
+ static stripAnsi = (text) => stripAnsi(text);
212
+ /**
213
+ * Whether `text` is nothing but escape sequences, as {@link Fmt.stripAnsi} reads them: a cursor hide or show, a
214
+ * clear-screen, an erase-scrollback. Empty text is not escape-only, and neither is a bare control character
215
+ * (`\r`, BEL), which is not an escape sequence.
216
+ *
217
+ * @param text - the text
218
+ */
219
+ static isEscapeOnly = (text) => isEscapeOnly(text);
220
+ /**
221
+ * Integers as compact ranges: `[1, 2, 3, 5, 10, 11]` is `"1-3,5,10-11"`, a run of consecutive numbers written
222
+ * `start-end` and the rest one by one, joined with commas.
223
+ *
224
+ * @remarks
225
+ * The input is sorted and its duplicates dropped first, so order does not matter. Anything that is not a finite
226
+ * integer is left out, and no input is `""`. Never throws.
227
+ *
228
+ * @param numbers - the integers, such as line numbers
229
+ */
230
+ static ranges = (numbers) => {
231
+ const sorted = [...new Set(Array.from(numbers).filter((n) => Number.isInteger(n)))].sort((a, b) => a - b);
232
+ const parts = [];
233
+ let index = 0;
234
+ while (index < sorted.length) {
235
+ const start = sorted[index];
236
+ let end = start;
237
+ while (index + 1 < sorted.length && sorted[index + 1] === end + 1) end = sorted[++index];
238
+ parts.push(start === end ? `${start}` : `${start}-${end}`);
239
+ index++;
240
+ }
241
+ return parts.join(",");
242
+ };
243
+ /**
244
+ * Any value as text, without ever throwing.
245
+ *
246
+ * @remarks
247
+ * A string is itself; a number, boolean, bigint or symbol is `String(value)`; `undefined` and `null` are
248
+ * `"undefined"` and `"null"`. A value with a string `message` (an `Error`, one from another realm, a plain
249
+ * `{ message }`) is that message. Anything else is its JSON, or `String(value)` when it has none (a cycle, a
250
+ * function), or `"[unprintable value]"` when that throws too. A getter that throws is a value with no message.
251
+ *
252
+ * The text is not sanitised: pass it through {@link Fmt.sanitize} before printing data you do not control. For
253
+ * the messages along an error's `cause` chain, use {@link Fmt.causeMessage}; for the two combined, use
254
+ * {@link Fmt.errorText}.
255
+ *
256
+ * It is `@effected/kit`'s `ErrorText.describe`, the same function, so a front end that cannot depend on this
257
+ * package (an MCP server) describes a value with the same words.
258
+ *
259
+ * @param value - the value
260
+ */
261
+ static describe = ErrorText.describe;
262
+ /**
263
+ * Every distinct non-empty message along a value's `cause` chain, nearest first and joined with `: `, or
264
+ * `undefined` when nothing along it has one.
265
+ *
266
+ * @remarks
267
+ * The kit's one guarded cause-message read, the one its own errors render a preserved cause with. A node's message
268
+ * is a thrown string or a non-empty string `message` property, read without `instanceof Error`, so an error from
269
+ * another realm or a plain `{ message }` object counts. A deeper message the joined text already contains is not
270
+ * repeated, and one that contains the joined text replaces it, so a `SqlError` wrapping SQLite's own text reads
271
+ * `Failed to execute statement: SQLITE_BUSY: database is locked`. It is the whole chain, not the innermost
272
+ * message alone. The walk stops after eight nodes, so a cyclic chain terminates.
273
+ *
274
+ * Never throws: a `message` getter that throws leaves that node without text, and a `cause` getter that throws ends
275
+ * the walk with the text found so far, `undefined` when there is none, so a caller's own fallback
276
+ * (`?? Fmt.describe(value)`, which {@link Fmt.errorText} spells once) applies.
277
+ *
278
+ * It is `@effected/kit`'s `ErrorText.causeMessage`, the same function.
279
+ *
280
+ * @param value - the error, or whatever was thrown
281
+ */
282
+ static causeMessage = ErrorText.causeMessage;
283
+ /**
284
+ * The text of an unknown failure: {@link Fmt.causeMessage} when the chain carries a message, otherwise
285
+ * {@link Fmt.describe}.
286
+ *
287
+ * @remarks
288
+ * Exactly `Fmt.causeMessage(value) ?? Fmt.describe(value)`, the fallback an I/O site turning a caught value into
289
+ * text would otherwise spell out by hand at every call. An error whose message is empty and whose chain carries none
290
+ * is `""`, as `describe` reads it. Never throws, and not sanitised: pass it through {@link Fmt.sanitize} before
291
+ * printing data you do not control. It is `@effected/kit`'s `ErrorText.of`, the same function.
292
+ *
293
+ * @param value - the error, or whatever was thrown
294
+ */
295
+ static errorText = ErrorText.of;
296
+ /**
297
+ * The candidates nearest to `input`, for a "did you mean" hint: those at the smallest edit distance found, up to
298
+ * `maxDistance` edits (two by default), in the order given, each once. No candidate that close is `[]`.
299
+ *
300
+ * @remarks
301
+ * The distance is Levenshtein's (one insertion, deletion or substitution per edit), over UTF-16 code units and
302
+ * case-sensitive, with the same two-edit threshold and the same ties-all-kept rule `effect/cli` uses for an unknown
303
+ * subcommand or flag, so a program's own suggestions agree with the parser's. A candidate equal to `input` is at
304
+ * distance zero and is the only one returned.
305
+ *
306
+ * The cost is linear in the candidates' total length: a candidate whose length differs from `input`'s by more than
307
+ * `maxDistance` is skipped unmeasured, and only the band of the edit matrix within `maxDistance` of its diagonal is
308
+ * measured, so a long typed value cannot make it quadratic. A `maxDistance` that is not a non-negative integer is a programming mistake and
309
+ * throws a `RangeError`.
310
+ *
311
+ * ```ts
312
+ * Fmt.suggest("stauts", ["status", "stack", "build"]) // => ["status"]
313
+ * ```
314
+ *
315
+ * @param input - what was typed: an unknown key, a profile name, a phase name
316
+ * @param candidates - the names it could have meant
317
+ * @param options - the most edits a suggestion may be away
318
+ */
319
+ static suggest = (input, candidates, options) => {
320
+ const max = options?.maxDistance ?? 2;
321
+ if (!Number.isInteger(max) || max < 0) throw new RangeError(`Fmt.suggest: maxDistance must be a non-negative integer, received ${max}`);
322
+ let best = max + 1;
323
+ let found = [];
324
+ for (const candidate of new Set(candidates)) {
325
+ const distance = boundedDistance(input, candidate, Math.min(max, best));
326
+ if (distance < best) {
327
+ best = distance;
328
+ found = [candidate];
329
+ } else if (distance === best) found.push(candidate);
330
+ }
331
+ return best <= max ? found : [];
332
+ };
333
+ };
334
+
335
+ export { Fmt, sanitize };
@@ -0,0 +1,39 @@
1
+ import { WorkflowCommand } from "@effected/github-commands";
2
+
3
+ // src/GithubAnnotation.ts
4
+ /**
5
+ * GitHub Actions annotations, as workflow commands.
6
+ *
7
+ * @public
8
+ */
9
+ var GithubAnnotation = class {
10
+ constructor() {}
11
+ /**
12
+ * Format an annotation as a workflow command: `::error title=T,file=F,line=1,endLine=2,col=3,endColumn=4::message`.
13
+ *
14
+ * @remarks
15
+ * The message escapes `%`, CR and LF; a property value escapes those and `:` and `,`, per GitHub's
16
+ * [workflow-command documentation](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions).
17
+ * The percent sign is escaped first, so an escape that was just written is never escaped again. An unescaped line
18
+ * break in a message would let the text after it be read as a new command, which is why the escaping is not optional.
19
+ *
20
+ * A property that is not given is left out, and the properties are written in the order `title`, `file`, `line`,
21
+ * `endLine`, `col`, `endColumn`, the same as `@effected/github-commands`' `WorkflowCommand`, which this renders
22
+ * through.
23
+ *
24
+ * @param annotation - the level and the optional file, position and title
25
+ * @param message - the annotation's text
26
+ */
27
+ static format = (annotation, message) => {
28
+ return WorkflowCommand.render(annotation.level, {
29
+ title: annotation.title,
30
+ file: annotation.file,
31
+ line: annotation.line,
32
+ endLine: annotation.endLine,
33
+ col: annotation.col,
34
+ endColumn: annotation.endColumn
35
+ }, message);
36
+ };
37
+ };
38
+
39
+ export { GithubAnnotation };
package/Glyphs.js ADDED
@@ -0,0 +1,82 @@
1
+ // src/Glyphs.ts
2
+ /**
3
+ * The two glyph sets: Unicode, and a plain-ASCII fallback for terminals that cannot draw it.
4
+ *
5
+ * @remarks
6
+ * The sets are shared, so they and their nested values are frozen.
7
+ *
8
+ * @public
9
+ */
10
+ var Glyphs = class Glyphs {
11
+ constructor() {}
12
+ /** Unicode symbols. */
13
+ static unicode = Object.freeze({
14
+ kind: "unicode",
15
+ ellipsis: "…",
16
+ spinner: Object.freeze([
17
+ "⠋",
18
+ "⠙",
19
+ "⠹",
20
+ "⠸",
21
+ "⠼",
22
+ "⠴",
23
+ "⠦",
24
+ "⠧",
25
+ "⠇",
26
+ "⠏"
27
+ ]),
28
+ bullet: "•",
29
+ arrow: "→",
30
+ pathSeparator: Object.freeze({
31
+ human: "›",
32
+ agent: " > "
33
+ }),
34
+ spinnerIntervalMs: 80,
35
+ tree: Object.freeze({
36
+ branch: "├─ ",
37
+ last: "└─ ",
38
+ pipe: "│ ",
39
+ blank: " "
40
+ })
41
+ });
42
+ /**
43
+ * Pick a glyph set without a service: the one `CliTheme` uses, as a pure function.
44
+ *
45
+ * @remarks
46
+ * `CliTheme.layer` reads `TERM` through `Config` and calls this, so the two agree. `StreamEnv` carries no
47
+ * `TERM`, and nothing else in it decides ASCII, so the caller passes `term` when it wants `auto` to mean
48
+ * something.
49
+ *
50
+ * @param options - whether to force ASCII or Unicode, and the `TERM` value `auto` reads
51
+ */
52
+ static select = (options) => {
53
+ const ascii = options?.ascii ?? "auto";
54
+ return ascii === true || ascii === "auto" && options?.term === "dumb" ? Glyphs.ascii : Glyphs.unicode;
55
+ };
56
+ /** ASCII-only symbols. */
57
+ static ascii = Object.freeze({
58
+ kind: "ascii",
59
+ ellipsis: "...",
60
+ spinner: Object.freeze([
61
+ "-",
62
+ "\\",
63
+ "|",
64
+ "/"
65
+ ]),
66
+ bullet: "*",
67
+ arrow: "->",
68
+ pathSeparator: Object.freeze({
69
+ human: ">",
70
+ agent: " > "
71
+ }),
72
+ spinnerIntervalMs: 80,
73
+ tree: Object.freeze({
74
+ branch: "|-- ",
75
+ last: "\\-- ",
76
+ pipe: "| ",
77
+ blank: " "
78
+ })
79
+ });
80
+ };
81
+
82
+ export { Glyphs };
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 C. Spencer Beggs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,41 @@
1
+ import { Runtime, Schema } from "effect";
2
+
3
+ // src/NotInteractive.ts
4
+ /**
5
+ * A command needed to prompt, but there is no terminal to prompt on.
6
+ *
7
+ * @remarks
8
+ * Exits `64` (BSD `EX_USAGE`) through core's `Runtime.errorExitCode` marker:
9
+ * the caller invoked the command the wrong way, so the fix is to run it in a
10
+ * terminal or pass the flag that supplies the answer. Its default rendering is
11
+ * one line, `not interactive: run in a terminal or pass the flag`, which is the error's `message`, so a consumer
12
+ * `render` can print `error.message` and keep it.
13
+ *
14
+ * @public
15
+ */
16
+ var NotInteractive = class extends Schema.TaggedError()("NotInteractive", {}) {
17
+ /**
18
+ * The one line, `not interactive: run in a terminal or pass the flag`.
19
+ *
20
+ * @remarks
21
+ * A prototype getter, not a field, so it is not part of the encoded form, equality or a JSON dump. Assigning to
22
+ * it is ignored: a library that rewrites `error.message` must not make this error throw, which a getter-only
23
+ * property does in strict mode. The line is fixed.
24
+ */
25
+ get message() {
26
+ return "not interactive: run in a terminal or pass the flag";
27
+ }
28
+ set message(_value) {}
29
+ /**
30
+ * The process exit code: `64`.
31
+ *
32
+ * @remarks
33
+ * A prototype getter rather than an own field, so a JSON or logger dump of the error does not carry the
34
+ * runtime marker. It is the error's own code, so `CliRuntime`'s `usageExitCode` option does not change it.
35
+ */
36
+ get [Runtime.errorExitCode]() {
37
+ return 64;
38
+ }
39
+ };
40
+
41
+ export { NotInteractive };