@effected/cli 0.10.0 → 0.12.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 (92) hide show
  1. package/Cancelled.js +44 -0
  2. package/CliAudience.js +178 -0
  3. package/CliColor.js +13 -19
  4. package/CliEnv.js +89 -0
  5. package/CliExit.js +1 -1
  6. package/CliFailure.js +302 -0
  7. package/CliInteractive.js +71 -0
  8. package/CliLinks.js +154 -0
  9. package/CliLog.js +346 -0
  10. package/CliLogger.js +34 -33
  11. package/CliMessage.js +80 -0
  12. package/CliPrompt.js +104 -0
  13. package/CliRuntime.js +110 -54
  14. package/CliTest.js +16 -0
  15. package/CliTheme.js +141 -0
  16. package/ConfigIssueRenderer.js +14 -33
  17. package/Doc.js +536 -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 +145 -131
  23. package/Render.js +255 -0
  24. package/SchemaIssueRenderer.js +7 -10
  25. package/Status.js +166 -0
  26. package/TestTerminal.js +80 -0
  27. package/Token.js +69 -0
  28. package/index.d.ts +3089 -169
  29. package/index.js +19 -1
  30. package/internal/ansi.js +230 -0
  31. package/internal/autoFormat.js +34 -0
  32. package/internal/canPrompt.js +15 -0
  33. package/internal/counts.js +84 -0
  34. package/internal/diagnostics.js +32 -0
  35. package/internal/displayWidth.js +35 -0
  36. package/internal/failureTarget.js +195 -0
  37. package/internal/fallbackAnswer.js +18 -0
  38. package/internal/fileSink.js +62 -0
  39. package/internal/format.js +62 -7
  40. package/internal/layout.js +250 -0
  41. package/internal/linkScheme.js +30 -0
  42. package/internal/linkTarget.js +50 -0
  43. package/internal/logSafety.js +46 -0
  44. package/internal/renderAnsi.js +52 -0
  45. package/internal/renderDoc.js +320 -0
  46. package/internal/renderGithubLog.js +46 -0
  47. package/internal/renderMarkdown.js +368 -0
  48. package/internal/renderPlain.js +50 -0
  49. package/internal/scanAudience.js +106 -0
  50. package/internal/splitFrame.js +56 -0
  51. package/internal/wizardGate.js +18 -0
  52. package/package.json +40 -5
  53. package/testing.d.ts +88 -2
  54. package/testing.js +2 -1
  55. package/ui/CliUi.js +432 -0
  56. package/ui/CliUiLive.js +446 -0
  57. package/ui/Confirm.js +245 -0
  58. package/ui/DocView.js +74 -0
  59. package/ui/KeyHelp.js +62 -0
  60. package/ui/KeyTable.js +199 -0
  61. package/ui/MultiSelect.js +260 -0
  62. package/ui/Select.js +230 -0
  63. package/ui/Tabs.js +202 -0
  64. package/ui/TextInput.js +290 -0
  65. package/ui/Toggle.js +32 -0
  66. package/ui/UiKey.js +44 -0
  67. package/ui/UiProvider.js +60 -0
  68. package/ui/UiStreams.js +18 -0
  69. package/ui/UiTheme.js +119 -0
  70. package/ui/Viewport.js +204 -0
  71. package/ui/internal/ErrorBoundary.js +30 -0
  72. package/ui/internal/Holder.js +74 -0
  73. package/ui/internal/ScreenContext.js +52 -0
  74. package/ui/internal/UiProviders.js +21 -0
  75. package/ui/internal/ink.js +122 -0
  76. package/ui/internal/inkChalk.js +58 -0
  77. package/ui/internal/inkConsole.js +146 -0
  78. package/ui/internal/lazyView.js +74 -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 +760 -0
  85. package/ui/testing/fakeStreams.js +79 -0
  86. package/ui/testing/terminalModel.js +59 -0
  87. package/ui-testing-serializer.d.ts +14 -0
  88. package/ui-testing-serializer.js +33 -0
  89. package/ui-testing.d.ts +527 -0
  90. package/ui-testing.js +3 -0
  91. package/ui.d.ts +1790 -0
  92. package/ui.js +17 -0
package/index.js CHANGED
@@ -1,8 +1,26 @@
1
+ import { Cancelled } from "./Cancelled.js";
2
+ import { CliInteractive } from "./CliInteractive.js";
3
+ import { Fmt } from "./Fmt.js";
4
+ import { CliLinks } from "./CliLinks.js";
5
+ import { Glyphs } from "./Glyphs.js";
6
+ import { Token } from "./Token.js";
7
+ import { CliTheme } from "./CliTheme.js";
8
+ import { Render } from "./Render.js";
9
+ import { Doc } from "./Doc.js";
10
+ import { NotInteractive } from "./NotInteractive.js";
11
+ import { Status } from "./Status.js";
12
+ import { CliDoc, CliFailure } from "./CliFailure.js";
13
+ import { CliAudience } from "./CliAudience.js";
1
14
  import { CliColor } from "./CliColor.js";
15
+ import { CliPrompt } from "./CliPrompt.js";
16
+ import { CliEnv } from "./CliEnv.js";
2
17
  import { CliExit } from "./CliExit.js";
3
18
  import { CliLogger } from "./CliLogger.js";
19
+ import { CliLog } from "./CliLog.js";
20
+ import { CliMessage } from "./CliMessage.js";
4
21
  import { CliRuntime } from "./CliRuntime.js";
5
22
  import { ConfigIssueRenderer } from "./ConfigIssueRenderer.js";
23
+ import { GithubAnnotation } from "./GithubAnnotation.js";
6
24
  import { SchemaIssueRenderer } from "./SchemaIssueRenderer.js";
7
25
 
8
- export { CliColor, CliExit, CliLogger, CliRuntime, ConfigIssueRenderer, SchemaIssueRenderer };
26
+ export { Cancelled, CliAudience, CliColor, CliDoc, CliEnv, CliExit, CliFailure, CliInteractive, CliLinks, CliLog, CliLogger, CliMessage, CliPrompt, CliRuntime, CliTheme, ConfigIssueRenderer, Doc, Fmt, GithubAnnotation, Glyphs, NotInteractive, Render, SchemaIssueRenderer, Status, Token };
@@ -0,0 +1,230 @@
1
+ //#region src/internal/ansi.ts
2
+ /** Foreground SGR parameter for each named colour: 30 to 37, then 90 to 97. */
3
+ const NAMED = {
4
+ black: 30,
5
+ red: 31,
6
+ green: 32,
7
+ yellow: 33,
8
+ blue: 34,
9
+ magenta: 35,
10
+ cyan: 36,
11
+ white: 37,
12
+ blackBright: 90,
13
+ redBright: 91,
14
+ greenBright: 92,
15
+ yellowBright: 93,
16
+ blueBright: 94,
17
+ magentaBright: 95,
18
+ cyanBright: 96,
19
+ whiteBright: 97,
20
+ gray: 90
21
+ };
22
+ /** The 16 ANSI colours as xterm draws them, in SGR order (0 to 7, then bright 8 to 15), for the basic fallback. */
23
+ const PALETTE16 = [
24
+ [
25
+ 0,
26
+ 0,
27
+ 0
28
+ ],
29
+ [
30
+ 205,
31
+ 0,
32
+ 0
33
+ ],
34
+ [
35
+ 0,
36
+ 205,
37
+ 0
38
+ ],
39
+ [
40
+ 205,
41
+ 205,
42
+ 0
43
+ ],
44
+ [
45
+ 0,
46
+ 0,
47
+ 238
48
+ ],
49
+ [
50
+ 205,
51
+ 0,
52
+ 205
53
+ ],
54
+ [
55
+ 0,
56
+ 205,
57
+ 205
58
+ ],
59
+ [
60
+ 229,
61
+ 229,
62
+ 229
63
+ ],
64
+ [
65
+ 127,
66
+ 127,
67
+ 127
68
+ ],
69
+ [
70
+ 255,
71
+ 0,
72
+ 0
73
+ ],
74
+ [
75
+ 0,
76
+ 255,
77
+ 0
78
+ ],
79
+ [
80
+ 255,
81
+ 255,
82
+ 0
83
+ ],
84
+ [
85
+ 92,
86
+ 92,
87
+ 255
88
+ ],
89
+ [
90
+ 255,
91
+ 0,
92
+ 255
93
+ ],
94
+ [
95
+ 0,
96
+ 255,
97
+ 255
98
+ ],
99
+ [
100
+ 255,
101
+ 255,
102
+ 255
103
+ ]
104
+ ];
105
+ /** The six levels of each axis of the xterm 6x6x6 colour cube. */
106
+ const CUBE = [
107
+ 0,
108
+ 95,
109
+ 135,
110
+ 175,
111
+ 215,
112
+ 255
113
+ ];
114
+ const ESC = "\x1B[";
115
+ /** `#rgb` or `#rrggbb` to channels, or `undefined` when it is neither. */
116
+ const parseHex = (hex) => {
117
+ const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(hex);
118
+ if (short !== null) return [
119
+ short[1],
120
+ short[2],
121
+ short[3]
122
+ ].map((c) => Number.parseInt((c ?? "0").repeat(2), 16));
123
+ const long = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
124
+ if (long === null) return void 0;
125
+ return [
126
+ Number.parseInt(long[1] ?? "0", 16),
127
+ Number.parseInt(long[2] ?? "0", 16),
128
+ Number.parseInt(long[3] ?? "0", 16)
129
+ ];
130
+ };
131
+ const distance = (a, b) => (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2;
132
+ /** The index of the cube level nearest a channel value; the lower level wins a tie. */
133
+ const nearestLevel = (value) => {
134
+ let best = 0;
135
+ for (let i = 1; i < CUBE.length; i++) if (Math.abs((CUBE[i] ?? 0) - value) < Math.abs((CUBE[best] ?? 0) - value)) best = i;
136
+ return best;
137
+ };
138
+ /**
139
+ * The nearest xterm 256-colour index for a colour: the closest of the 6x6x6 cube and the 24-step grayscale
140
+ * ramp (232 to 255) by squared RGB distance, the cube winning a tie.
141
+ */
142
+ const nearest256 = (rgb) => {
143
+ const [r, g, b] = [
144
+ nearestLevel(rgb[0]),
145
+ nearestLevel(rgb[1]),
146
+ nearestLevel(rgb[2])
147
+ ];
148
+ const cubeIndex = 16 + 36 * r + 6 * g + b;
149
+ const cubeDistance = distance(rgb, [
150
+ CUBE[r] ?? 0,
151
+ CUBE[g] ?? 0,
152
+ CUBE[b] ?? 0
153
+ ]);
154
+ const average = (rgb[0] + rgb[1] + rgb[2]) / 3;
155
+ const step = Math.min(23, Math.max(0, Math.round((average - 8) / 10)));
156
+ const gray = 8 + 10 * step;
157
+ return distance(rgb, [
158
+ gray,
159
+ gray,
160
+ gray
161
+ ]) < cubeDistance ? 232 + step : cubeIndex;
162
+ };
163
+ /** The SGR parameter of the nearest of the 16 ANSI colours: 30 to 37 or 90 to 97. */
164
+ const nearest16 = (rgb) => {
165
+ let best = 0;
166
+ for (let i = 1; i < PALETTE16.length; i++) if (distance(rgb, PALETTE16[i] ?? [
167
+ 0,
168
+ 0,
169
+ 0
170
+ ]) < distance(rgb, PALETTE16[best] ?? [
171
+ 0,
172
+ 0,
173
+ 0
174
+ ])) best = i;
175
+ return best < 8 ? 30 + best : 90 + (best - 8);
176
+ };
177
+ /** The foreground SGR parameters for a colour at a level, or `undefined` for none. */
178
+ const foreground = (fg, level) => {
179
+ if (level === "none") return void 0;
180
+ if (!fg.startsWith("#")) return Object.hasOwn(NAMED, fg) ? String(NAMED[fg]) : void 0;
181
+ const rgb = parseHex(fg);
182
+ if (rgb === void 0) return void 0;
183
+ if (level === "truecolor") return `38;2;${rgb[0]};${rgb[1]};${rgb[2]}`;
184
+ if (level === "256") return `38;5;${nearest256(rgb)}`;
185
+ return String(nearest16(rgb));
186
+ };
187
+ /** The wraps for a style, innermost first: foreground, then dim, bold, italic and underline outward. */
188
+ const wraps = (style, level) => {
189
+ const result = [];
190
+ const fg = style.fg === void 0 ? void 0 : foreground(style.fg, level);
191
+ if (fg !== void 0) result.push({
192
+ open: `${ESC}${fg}m`,
193
+ close: `${ESC}39m`
194
+ });
195
+ if (style.dim === true) result.push({
196
+ open: `${ESC}2m`,
197
+ close: `${ESC}22m`
198
+ });
199
+ if (style.bold === true) result.push({
200
+ open: `${ESC}1m`,
201
+ close: `${ESC}22m`
202
+ });
203
+ if (style.italic === true) result.push({
204
+ open: `${ESC}3m`,
205
+ close: `${ESC}23m`
206
+ });
207
+ if (style.underline === true) result.push({
208
+ open: `${ESC}4m`,
209
+ close: `${ESC}24m`
210
+ });
211
+ return result;
212
+ };
213
+ /**
214
+ * Render `text` in a style at a colour level; identity at `none`.
215
+ *
216
+ * Each attribute closes with its own code (39, 22, 23, 24), never a blanket reset, and a closer that occurs
217
+ * inside `text` is followed by the opener again, so a painted span nested in a painted span leaves the outer
218
+ * style in force for the text after it.
219
+ */
220
+ const paintStyle = (style, level, text) => {
221
+ if (level === "none" || text === "") return text;
222
+ let out = text;
223
+ for (const { open, close } of wraps(style, level)) out = `${open}${out.replaceAll(close, `${close}${open}`)}${close}`;
224
+ return out;
225
+ };
226
+ /** The raw opening SGR sequence of a style at a level, `""` at `none` or for a style that paints nothing. */
227
+ const openSequence = (style, level) => level === "none" ? "" : wraps(style, level).toReversed().map((wrap) => wrap.open).join("");
228
+
229
+ //#endregion
230
+ export { nearest256, openSequence, paintStyle, parseHex };
@@ -0,0 +1,34 @@
1
+ import { Effect, Option } from "effect";
2
+ import { CurrentRuntimeEnv } from "@effected/env";
3
+
4
+ //#region src/internal/autoFormat.ts
5
+ /**
6
+ * Whether the environment says the program runs under GitHub Actions, whose runner reads workflow commands out of
7
+ * the log.
8
+ *
9
+ * @remarks
10
+ * `CurrentRuntimeEnv` is read if the environment has it and is not required: without it, the answer is no.
11
+ *
12
+ * @internal
13
+ */
14
+ const underGithubActions = Effect.gen(function* () {
15
+ const runtime = yield* Effect.serviceOption(CurrentRuntimeEnv);
16
+ return Option.contains(Option.flatMap(runtime, (env) => env.ci), "github-actions");
17
+ });
18
+ /**
19
+ * The renderer an audience gets when nothing says otherwise: a person is painted, a machine reads plain text.
20
+ *
21
+ * @remarks
22
+ * A CI gets GitHub's log format only where `CurrentRuntimeEnv` says it is GitHub Actions; that service is read if the
23
+ * environment has it and is not required. Shared by `Doc.print` and the failure report.
24
+ *
25
+ * @internal
26
+ */
27
+ const autoFormat = (audience) => Effect.gen(function* () {
28
+ if (audience === "human") return "ansi";
29
+ if (audience === "agent") return "plain";
30
+ return (yield* underGithubActions) ? "githubLog" : "plain";
31
+ });
32
+
33
+ //#endregion
34
+ export { autoFormat, underGithubActions };
@@ -0,0 +1,15 @@
1
+ import { Config, Effect, Option } from "effect";
2
+
3
+ //#region src/internal/canPrompt.ts
4
+ /**
5
+ * Whether the terminal facts let a run prompt: a terminal on standard input and on standard output, and a `TERM` that
6
+ * is not `dumb`. A dumb terminal is a terminal, but it cannot move the cursor or take synchronized output, which every
7
+ * redrawing prompt and screen needs. `TERM` is read through `Config` (never `process`), and only when both streams are
8
+ * terminals; a read that fails counts as unset, as the env package treats every read.
9
+ *
10
+ * @internal
11
+ */
12
+ const canPrompt = (terminal) => terminal.stdinIsTerminal && terminal.stdout.isTerminal ? Effect.map(Config.option(Config.String("TERM")).pipe(Effect.orElseSucceed(() => Option.none())), (term) => !(Option.isSome(term) && term.value === "dumb")) : Effect.succeed(false);
13
+
14
+ //#endregion
15
+ export { canPrompt };
@@ -0,0 +1,84 @@
1
+ import { Fmt } from "../Fmt.js";
2
+
3
+ //#region src/internal/counts.ts
4
+ /**
5
+ * The label a counter shows for a count: its one label, or `one` when the count is exactly 1 and `other` otherwise.
6
+ * The count is the counter's own `n`, except in a share headline (`1/3 repos`), which reads by the denominator, the
7
+ * total, and passes it.
8
+ *
9
+ * @internal
10
+ */
11
+ const counterLabel = (counter, count = counter.n) => typeof counter.label === "string" ? counter.label : count === 1 ? counter.label.one : counter.label.other;
12
+ /**
13
+ * The label a counter's column is headed with in a `CountsTable`: its one label, or its plural form, since a column
14
+ * holds every row's count.
15
+ *
16
+ * @internal
17
+ */
18
+ const columnLabel = (counter) => typeof counter.label === "string" ? counter.label : counter.label.other;
19
+ /**
20
+ * The total of a `Counts` block: the caller's rule when it has one, otherwise the sum of `n` over every counter.
21
+ *
22
+ * @remarks
23
+ * Lives here, not on `Doc`, so a renderer needs nothing from `Doc` at runtime: `Doc.print` imports the renderers, and
24
+ * a renderer importing `Doc` back would make a cycle. `Doc.total` is this function.
25
+ *
26
+ * @internal
27
+ */
28
+ const totalOf = (block) => block.total === void 0 ? block.counters.reduce((sum, counter) => sum + counter.n, 0) : block.total(block.counters);
29
+ /**
30
+ * The counters a renderer shows: every one except a zero counter that does not ask for `showZero`. `Doc.visibleCounters`
31
+ * is this function.
32
+ *
33
+ * @internal
34
+ */
35
+ const visibleCountersOf = (block) => block.counters.filter((counter) => counter.n !== 0 || counter.showZero === true);
36
+ /**
37
+ * A `CountsTable` as the `Table` it renders as: a label column, then a column per counter key in the order the keys
38
+ * first appear, headed by that counter's label; a cell is the count painted with its status token, empty where a row
39
+ * has no counter for the key; a duration column, when some row has a `durationMs`, formatted with `Fmt.duration`; and,
40
+ * with `totalRow`, a last row summing each column (a missing count or duration is zero).
41
+ *
42
+ * @remarks
43
+ * Plain literals, not `Doc` constructors: a renderer needs nothing from `Doc` at runtime (see {@link totalOf}).
44
+ *
45
+ * @internal
46
+ */
47
+ const countsTableOf = (block) => {
48
+ const keys = [];
49
+ for (const row of block.rows) for (const counter of row.counters) if (!keys.some((known) => known.key === counter.key)) keys.push(counter);
50
+ const text = (value, token) => token === void 0 ? {
51
+ _tag: "Text",
52
+ value
53
+ } : {
54
+ _tag: "Text",
55
+ value,
56
+ token
57
+ };
58
+ const countCell = (counter) => counter === void 0 ? [] : [text(String(counter.n), counter.status.def.token)];
59
+ const timed = block.rows.some((row) => row.durationMs !== void 0);
60
+ const durationCell = (ms) => timed ? [ms === void 0 ? [] : [text(Fmt.duration(ms))]] : [];
61
+ const rows = block.rows.map((row) => [
62
+ row.label,
63
+ ...keys.map((key) => countCell(row.counters.find((counter) => counter.key === key.key))),
64
+ ...durationCell(row.durationMs)
65
+ ]);
66
+ const totalLabel = block.totalRow === void 0 || block.totalRow === false ? void 0 : block.totalRow === true ? [text("Total")] : block.totalRow;
67
+ const total = totalLabel === void 0 ? [] : [[
68
+ totalLabel,
69
+ ...keys.map((key) => [text(String(block.rows.reduce((sum, row) => sum + (row.counters.find((counter) => counter.key === key.key)?.n ?? 0), 0)))]),
70
+ ...durationCell(block.rows.reduce((sum, row) => sum + (row.durationMs ?? 0), 0))
71
+ ]];
72
+ return {
73
+ _tag: "Table",
74
+ columns: [
75
+ { header: block.labelHeader ?? [] },
76
+ ...keys.map((key) => ({ header: [text(columnLabel(key))] })),
77
+ ...timed ? [{ header: block.durationHeader ?? [text("duration")] }] : []
78
+ ],
79
+ rows: [...rows, ...total]
80
+ };
81
+ };
82
+
83
+ //#endregion
84
+ export { columnLabel, counterLabel, countsTableOf, totalOf, visibleCountersOf };
@@ -0,0 +1,32 @@
1
+ import { Context, LogLevel, Logger, References } from "effect";
2
+
3
+ //#region src/internal/diagnostics.ts
4
+ /**
5
+ * The diagnostics threshold reference behind `CliLog.Level`. Lives here so the stderr sink and the file sink share
6
+ * one definition without `CliLog` importing itself.
7
+ *
8
+ * @internal
9
+ */
10
+ const Level = Context.Reference("@effected/cli/CliLog/Level", { defaultValue: () => "None" });
11
+ /**
12
+ * Whether a diagnostics sink writes `record`. `installed` is the `MinimumLogLevel` the diagnostics layer put in
13
+ * place: while the fiber still sees that value the sink filters on its own {@link Level}; a different value is taken
14
+ * to be core's `--log-level` flag and the sink follows it. A flag whose value EQUALS `installed` cannot be told from
15
+ * no flag, so the sink keeps filtering on its own level; the plain `CliLogger` prints those records anyway.
16
+ *
17
+ * @internal
18
+ */
19
+ const passes = (record, installed) => {
20
+ const current = record.fiber.getRef(References.MinimumLogLevel);
21
+ const threshold = current === installed ? record.fiber.getRef(Level) : current;
22
+ return LogLevel.isGreaterThanOrEqualTo(record.logLevel, threshold);
23
+ };
24
+ /**
25
+ * The NDJSON line for a record, the one shape every diagnostics sink writes.
26
+ *
27
+ * @internal
28
+ */
29
+ const formatNdjson = (record) => Logger.formatJson.log(record);
30
+
31
+ //#endregion
32
+ export { Level, formatNdjson, passes };
@@ -0,0 +1,35 @@
1
+ //#region src/internal/displayWidth.ts
2
+ const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
3
+ const ANSI = /\u001B\][^\u0007\u001B]*(?:\u0007|\u001B\\)|\u001B\[[0-?]*[ -/]*[@-~]/g;
4
+ const EMOJI = /^\p{RGI_Emoji}$/v;
5
+ const WIDE2 = /[\p{Emoji_Presentation}\u1100-\u115E\u2329\u232A\u2630-\u2637\u268A-\u268F\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u303E\u3041-\u3096\u3099-\u30FF\u3105-\u312F\u3131-\u3163\u3165-\u318E\u3190-\u31E5\u31EF-\u321E\u3220-\u3247\u3250-\u4DBF\u4DC0-\uA48C\uA490-\uA4C6\uA960-\uA97C\uAC00-\uD7A3\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFF01-\uFF60\uFFE0-\uFFE6\u{16FE0}-\u{16FF6}\u{17000}-\u{191FF}\u{1AFF0}-\u{1AFFF}\u{1B000}-\u{1B2FF}\u{1D300}-\u{1D376}\u{1F200}-\u{1F265}\u{20000}-\u{3FFFD}]/u;
6
+ const ZERO2 = /^[\p{Mn}\p{Me}\p{Cf}\p{Cc}\u115F\u1160\u3164\uFFA0]+$/u;
7
+ /**
8
+ * The text without its ANSI escape sequences: CSI (including SGR colour) and OSC (including OSC-8 hyperlinks).
9
+ *
10
+ * @internal
11
+ */
12
+ const stripAnsi = (input) => input.replace(ANSI, "");
13
+ /**
14
+ * The display width of `input` in terminal columns: graphemes, wide East Asian characters and emoji count two,
15
+ * combining marks, control characters and ANSI escapes count none.
16
+ *
17
+ * @internal
18
+ */
19
+ const displayWidth = (input) => {
20
+ let width = 0;
21
+ for (const { segment } of segmenter.segment(stripAnsi(input))) {
22
+ if (ZERO2.test(segment)) continue;
23
+ width += EMOJI.test(segment) || /^\p{RI}{2}/u.test(segment) || WIDE2.test(Array.from(segment)[0] ?? "") ? 2 : 1;
24
+ }
25
+ return width;
26
+ };
27
+ /**
28
+ * The grapheme clusters of `input`, in order; a cluster is never split.
29
+ *
30
+ * @internal
31
+ */
32
+ const graphemes = (input) => Array.from(segmenter.segment(input), (s) => s.segment);
33
+
34
+ //#endregion
35
+ export { displayWidth, graphemes, stripAnsi };
@@ -0,0 +1,195 @@
1
+ import { autoFormat } from "./autoFormat.js";
2
+ import { sanitize } from "../Fmt.js";
3
+ import { CliLinks } from "../CliLinks.js";
4
+ import { Glyphs } from "../Glyphs.js";
5
+ import { CliTheme } from "../CliTheme.js";
6
+ import { Render } from "../Render.js";
7
+ import { CliFailure } from "../CliFailure.js";
8
+ import { Config, Context, Effect, MutableRef, Option } from "effect";
9
+ import { Audience, TerminalEnv } from "@effected/env";
10
+ import { CommandNeutralizer } from "@effected/github-commands";
11
+
12
+ //#region src/internal/failureTarget.ts
13
+ /**
14
+ * A cell `CliRuntime.main` provides outside failure reporting and fills from inside it.
15
+ *
16
+ * @remarks
17
+ * `reportFailures` catches OUTSIDE the layers `main` provides, so the theme, terminal, audience and links a report is
18
+ * rendered with are not in its context when a failure arrives. The environment layer writes the target here as it is
19
+ * built, and an audience flag rewrites it once the flag is read. A `Reference` defaulting to `undefined`, so a
20
+ * program run without `main` finds no cell and falls back, and no state is shared between runs.
21
+ *
22
+ * @internal
23
+ */
24
+ const FailureTargetCell = Context.Reference("@effected/cli/FailureTargetCell", { defaultValue: () => void 0 });
25
+ /** What a report is rendered with when nothing is known about the terminal: plain text, no limit, no escapes. */
26
+ const fallbackTarget = {
27
+ ctx: {
28
+ width: Number.POSITIVE_INFINITY,
29
+ audience: "agent",
30
+ color: "none",
31
+ paint: (_token, text) => text,
32
+ glyphs: Glyphs.ascii,
33
+ link: (_target, label) => label,
34
+ displayPath: (absolute) => absolute,
35
+ neutralizeWorkflowCommands: true
36
+ },
37
+ format: "plain",
38
+ assumed: true
39
+ };
40
+ /**
41
+ * The target for the services in the current context, or `undefined` when it lacks any of the four.
42
+ *
43
+ * @remarks
44
+ * Each is read with `serviceOption`, so this never adds a requirement. `audience` overrides the one in context: an
45
+ * audience flag is provided deeper than the environment layer, where the report cannot see it.
46
+ */
47
+ const build = (audience, settings = {}) => Effect.gen(function* () {
48
+ const theme = yield* Effect.serviceOption(CliTheme);
49
+ const terminal = yield* Effect.serviceOption(TerminalEnv);
50
+ const current = yield* Effect.serviceOption(Audience);
51
+ const links = yield* Effect.serviceOption(CliLinks);
52
+ if (Option.isNone(theme) || Option.isNone(terminal) || Option.isNone(links)) return void 0;
53
+ const shape = audience ?? (Option.isSome(current) ? current.value : void 0);
54
+ if (shape === void 0) return void 0;
55
+ const { displayPath, stackFrames, spans, appModule } = settings;
56
+ const ctx = yield* Render.context("stderr", displayPath === void 0 ? void 0 : { displayPath }).pipe(Effect.provideService(CliTheme, theme.value), Effect.provideService(TerminalEnv, terminal.value), Effect.provideService(CliLinks, links.value), Effect.provideService(Audience, shape));
57
+ return {
58
+ ctx,
59
+ format: yield* autoFormat(ctx.audience),
60
+ ...stackFrames === void 0 ? {} : { stackFrames },
61
+ ...spans === void 0 ? {} : { spans },
62
+ ...appModule === void 0 ? {} : { appModule }
63
+ };
64
+ });
65
+ /**
66
+ * Record the target for the services in context in the cell, if there is one. A no-op without a cell or services.
67
+ *
68
+ * @internal
69
+ */
70
+ const refreshFailureTarget = (audience, settings) => Effect.gen(function* () {
71
+ const cell = yield* FailureTargetCell;
72
+ if (cell === void 0) return;
73
+ const recorded = MutableRef.get(cell);
74
+ const target = yield* build(audience, settings ?? {
75
+ displayPath: recorded?.ctx.displayPath,
76
+ stackFrames: recorded?.stackFrames,
77
+ spans: recorded?.spans,
78
+ appModule: recorded?.appModule
79
+ });
80
+ if (target !== void 0) MutableRef.set(cell, target);
81
+ });
82
+ /**
83
+ * The target a report is rendered with: the cell, else the services in context, else the plain fallback.
84
+ *
85
+ * @internal
86
+ */
87
+ const currentTarget = Effect.gen(function* () {
88
+ const cell = yield* FailureTargetCell;
89
+ const recorded = cell === void 0 ? void 0 : MutableRef.get(cell);
90
+ if (recorded !== void 0) return recorded;
91
+ return (yield* build()) ?? fallbackTarget;
92
+ });
93
+ /** Content with its leading status mark (and the space after it) removed. */
94
+ const dropStatus = (content) => {
95
+ if (content[0]?._tag !== "StatusMark") return content;
96
+ const next = content[1];
97
+ return next?._tag === "Text" && next.value === " " ? content.slice(2) : content.slice(1);
98
+ };
99
+ /**
100
+ * A failure document without its status marks. A failure's status leads its first paragraph, or a schema failure's
101
+ * tree label; nothing else in the document carries one.
102
+ */
103
+ const withoutStatus = (doc) => doc.map((block) => {
104
+ if (block._tag === "Paragraph") return {
105
+ ...block,
106
+ content: dropStatus(block.content)
107
+ };
108
+ if (block._tag === "Tree") return {
109
+ ...block,
110
+ root: {
111
+ ...block.root,
112
+ label: dropStatus(block.root.label)
113
+ }
114
+ };
115
+ return block;
116
+ });
117
+ /**
118
+ * The lines of a failure report for a target, with or without the leading status.
119
+ *
120
+ * @internal
121
+ */
122
+ const linesOf = (cause, target, status = true, spans = target.spans) => {
123
+ const full = CliFailure.toDoc(cause, {
124
+ displayPath: target.ctx.displayPath,
125
+ ...target.stackFrames === void 0 ? {} : { stackFrames: target.stackFrames },
126
+ ...spans === void 0 ? {} : { spans },
127
+ ...target.appModule === void 0 ? {} : { appModule: target.appModule }
128
+ });
129
+ const doc = status ? full : withoutStatus(full);
130
+ const text = Render[target.format](doc, target.ctx);
131
+ return text === "" ? [] : text.split("\n");
132
+ };
133
+ /**
134
+ * A consumer `render`'s lines, made safe: neutralized under GitHub Actions, and stripped of escapes for an agent.
135
+ *
136
+ * @remarks
137
+ * What a consumer's `render` returns is text the kit did not build and cannot vouch for: it interpolates error
138
+ * messages, file names, whatever the failure carried. So it gets the output policy the kit's own report has. Under
139
+ * GitHub Actions (the target says so, and with no environment services at all it is assumed) every line is neutralized,
140
+ * a returned line break splitting it first. For an agent or a CI audience the escapes are removed too (GitHub Actions
141
+ * detects as `ci`, and the kit's own output for it has none). For a person they are kept: the kit cannot tell the consumer's own colour from an injected sequence, so the consumer's `render` is
142
+ * responsible for sanitising what it interpolates. An audience that was only assumed is not an agent.
143
+ *
144
+ * @internal
145
+ */
146
+ const guardConsumerLines = (lines) => Effect.map(currentTarget, (target) => {
147
+ const stripped = target.assumed !== true && (target.ctx.audience === "agent" || target.ctx.audience === "ci") ? lines.map(sanitize) : lines;
148
+ return target.ctx.neutralizeWorkflowCommands === true ? stripped.flatMap((line) => CommandNeutralizer.lines(line)) : stripped;
149
+ });
150
+ /**
151
+ * The plain lines of a failure, for a caller with no services: what `CliRuntime.defaultRender` returns.
152
+ *
153
+ * @internal
154
+ */
155
+ const plainFailureLines = (cause, status = true, spans) => linesOf(cause, fallbackTarget, status, spans);
156
+ const SPAN_SETTINGS = [
157
+ "app",
158
+ "all",
159
+ "off"
160
+ ];
161
+ /**
162
+ * The span trail setting, as `CliLog`'s level is read: the explicit `spans` when given (the variable is then not read
163
+ * at all), else the variable named `envVar` through `Config`, case-insensitive, unset or empty meaning the default.
164
+ * A value that is not a setting is ignored, with the warning to log.
165
+ *
166
+ * @internal
167
+ */
168
+ const readSpans = (explicit, envVar) => Effect.gen(function* () {
169
+ if (explicit !== void 0) return {
170
+ spans: explicit,
171
+ invalid: void 0
172
+ };
173
+ if (envVar === void 0) return {
174
+ spans: void 0,
175
+ invalid: void 0
176
+ };
177
+ const raw = yield* Config.option(Config.String(envVar)).pipe(Effect.orElseSucceed(() => Option.none()));
178
+ if (Option.isNone(raw) || raw.value === "") return {
179
+ spans: void 0,
180
+ invalid: void 0
181
+ };
182
+ const value = raw.value.toLowerCase();
183
+ const spans = SPAN_SETTINGS.find((setting) => setting === value);
184
+ if (spans !== void 0) return {
185
+ spans,
186
+ invalid: void 0
187
+ };
188
+ return {
189
+ spans: void 0,
190
+ invalid: `${envVar}=${raw.value} is not a span setting (${SPAN_SETTINGS.join("|")}); ignoring it`
191
+ };
192
+ });
193
+
194
+ //#endregion
195
+ export { FailureTargetCell, currentTarget, fallbackTarget, guardConsumerLines, linesOf, plainFailureLines, readSpans, refreshFailureTarget };
@@ -0,0 +1,18 @@
1
+ import { Effect } from "effect";
2
+ import { CliError, Prompt } from "effect/cli";
3
+
4
+ //#region src/internal/fallbackAnswer.ts
5
+ /**
6
+ * What a fallback hands core when the run is not interactive: `otherwise`, answered, when given (`undefined` counts
7
+ * as not given); otherwise core's own missing-parameter error, so the parse fails exactly as it would with no
8
+ * fallback and `CliRuntime.main` exits `64`. Shared by `CliPrompt.fallback` and `CliUi.fallback`.
9
+ *
10
+ * @internal
11
+ */
12
+ const answerWithoutPerson = (target) => {
13
+ if ("otherwise" in target && target.otherwise !== void 0) return Effect.succeed(Prompt.succeed(target.otherwise));
14
+ return Effect.fail("flag" in target ? new CliError.MissingOption({ option: target.flag }) : new CliError.MissingArgument({ argument: target.argument }));
15
+ };
16
+
17
+ //#endregion
18
+ export { answerWithoutPerson };