@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.
- package/Cancelled.js +43 -0
- package/CliAudience.js +177 -0
- package/CliColor.js +59 -0
- package/CliEnv.js +101 -0
- package/CliExit.js +64 -0
- package/CliFailure.js +287 -0
- package/CliFormat.js +124 -0
- package/CliInteractive.js +94 -0
- package/CliLinks.js +153 -0
- package/CliLog.js +367 -0
- package/CliLogger.js +91 -0
- package/CliMessage.js +79 -0
- package/CliPrompt.js +110 -0
- package/CliRuntime.js +254 -0
- package/CliTest.js +206 -0
- package/CliTheme.js +142 -0
- package/ConfigIssueRenderer.js +63 -0
- package/Doc.js +548 -0
- package/Fmt.js +335 -0
- package/GithubAnnotation.js +39 -0
- package/Glyphs.js +82 -0
- package/LICENSE +21 -0
- package/NotInteractive.js +41 -0
- package/README.md +215 -2
- package/Render.js +280 -0
- package/SchemaIssueRenderer.js +47 -0
- package/Status.js +165 -0
- package/TestTerminal.js +101 -0
- package/Token.js +68 -0
- package/index.d.ts +3951 -0
- package/index.js +27 -0
- package/internal/ExitRequested.js +21 -0
- package/internal/HelpRouting.js +35 -0
- package/internal/ansi.js +229 -0
- package/internal/autoFormat.js +33 -0
- package/internal/canPrompt.js +14 -0
- package/internal/counts.js +83 -0
- package/internal/diagnostics.js +31 -0
- package/internal/displayPath.js +46 -0
- package/internal/displayWidth.js +42 -0
- package/internal/failureTarget.js +195 -0
- package/internal/fallbackAnswer.js +17 -0
- package/internal/fileSink.js +61 -0
- package/internal/format.js +175 -0
- package/internal/inspect.js +40 -0
- package/internal/isExitCode.js +11 -0
- package/internal/layout.js +249 -0
- package/internal/linkScheme.js +29 -0
- package/internal/linkTarget.js +49 -0
- package/internal/logSafety.js +45 -0
- package/internal/renderAnsi.js +51 -0
- package/internal/renderDoc.js +322 -0
- package/internal/renderGithubLog.js +59 -0
- package/internal/renderMarkdown.js +367 -0
- package/internal/renderPlain.js +49 -0
- package/internal/scanAudience.js +105 -0
- package/internal/splitFrame.js +55 -0
- package/internal/terminalCapabilities.js +23 -0
- package/internal/wizardGate.js +17 -0
- package/package.json +90 -5
- package/testing.d.ts +246 -0
- package/testing.js +4 -0
- package/tsdoc-metadata.json +11 -0
- package/ui/CliUi.js +507 -0
- package/ui/CliUiDetached.js +36 -0
- package/ui/CliUiLive.js +436 -0
- package/ui/CliUiRouteStream.js +72 -0
- package/ui/Confirm.js +244 -0
- package/ui/DocView.js +76 -0
- package/ui/KeyHelp.js +61 -0
- package/ui/KeyTable.js +198 -0
- package/ui/MultiSelect.js +259 -0
- package/ui/Select.js +229 -0
- package/ui/Tabs.js +201 -0
- package/ui/TextInput.js +289 -0
- package/ui/Toggle.js +31 -0
- package/ui/UiKey.js +48 -0
- package/ui/UiProvider.js +62 -0
- package/ui/UiStreams.js +17 -0
- package/ui/UiTheme.js +147 -0
- package/ui/Viewport.js +203 -0
- package/ui/internal/ErrorBoundary.js +29 -0
- package/ui/internal/Holder.js +73 -0
- package/ui/internal/ScreenContext.js +51 -0
- package/ui/internal/UiProviders.js +38 -0
- package/ui/internal/ink.js +121 -0
- package/ui/internal/inkChalk.js +47 -0
- package/ui/internal/inkConsole.js +150 -0
- package/ui/internal/lazyView.js +73 -0
- package/ui/internal/lineRouter.js +70 -0
- package/ui/internal/lineText.js +18 -0
- package/ui/internal/mountPermit.js +15 -0
- package/ui/internal/processStreams.js +18 -0
- package/ui/internal/renderOptions.js +12 -0
- package/ui/testing/CliUiTest.js +792 -0
- package/ui/testing/fakeStreams.js +104 -0
- package/ui/testing/terminalModel.js +67 -0
- package/ui-testing-serializer.d.ts +12 -0
- package/ui-testing-serializer.js +32 -0
- package/ui-testing.d.ts +558 -0
- package/ui-testing.js +3 -0
- package/ui.d.ts +1990 -0
- package/ui.js +17 -0
package/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Cancelled } from "./Cancelled.js";
|
|
2
|
+
import { CliAudience } from "./CliAudience.js";
|
|
3
|
+
import { CliColor } from "./CliColor.js";
|
|
4
|
+
import { CliEnv } from "./CliEnv.js";
|
|
5
|
+
import { CliExit } from "./CliExit.js";
|
|
6
|
+
import { CliDoc, CliFailure } from "./CliFailure.js";
|
|
7
|
+
import { CliFormat } from "./CliFormat.js";
|
|
8
|
+
import { CliInteractive } from "./CliInteractive.js";
|
|
9
|
+
import { CliLinks } from "./CliLinks.js";
|
|
10
|
+
import { CliLog } from "./CliLog.js";
|
|
11
|
+
import { CliLogger } from "./CliLogger.js";
|
|
12
|
+
import { CliMessage } from "./CliMessage.js";
|
|
13
|
+
import { CliPrompt } from "./CliPrompt.js";
|
|
14
|
+
import { CliRuntime } from "./CliRuntime.js";
|
|
15
|
+
import { CliTheme } from "./CliTheme.js";
|
|
16
|
+
import { ConfigIssueRenderer } from "./ConfigIssueRenderer.js";
|
|
17
|
+
import { Doc } from "./Doc.js";
|
|
18
|
+
import { Fmt } from "./Fmt.js";
|
|
19
|
+
import { GithubAnnotation } from "./GithubAnnotation.js";
|
|
20
|
+
import { Glyphs } from "./Glyphs.js";
|
|
21
|
+
import { NotInteractive } from "./NotInteractive.js";
|
|
22
|
+
import { Render } from "./Render.js";
|
|
23
|
+
import { SchemaIssueRenderer } from "./SchemaIssueRenderer.js";
|
|
24
|
+
import { Status } from "./Status.js";
|
|
25
|
+
import { Token } from "./Token.js";
|
|
26
|
+
|
|
27
|
+
export { Cancelled, CliAudience, CliColor, CliDoc, CliEnv, CliExit, CliFailure, CliFormat, CliInteractive, CliLinks, CliLog, CliLogger, CliMessage, CliPrompt, CliRuntime, CliTheme, ConfigIssueRenderer, Doc, Fmt, GithubAnnotation, Glyphs, NotInteractive, Render, SchemaIssueRenderer, Status, Token };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Runtime } from "effect";
|
|
2
|
+
|
|
3
|
+
// src/internal/ExitRequested.ts
|
|
4
|
+
/**
|
|
5
|
+
* The failure `CliRuntime.main` raises when a successful program recorded a
|
|
6
|
+
* non-zero exit code through `CliExit`. Private: nothing outside this package
|
|
7
|
+
* constructs or matches it, and `reportFailures` never renders it.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
var ExitRequested = class extends Error {
|
|
12
|
+
[Runtime.errorReported] = false;
|
|
13
|
+
[Runtime.errorExitCode];
|
|
14
|
+
constructor(code) {
|
|
15
|
+
super(`exit ${code}`);
|
|
16
|
+
this.name = "ExitRequested";
|
|
17
|
+
this[Runtime.errorExitCode] = code;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export { ExitRequested };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Console, Effect } from "effect";
|
|
2
|
+
import { CliOutput } from "effect/cli";
|
|
3
|
+
|
|
4
|
+
// src/internal/HelpRouting.ts
|
|
5
|
+
/**
|
|
6
|
+
* Core's presenter, with an invalid invocation's help written to stderr beside
|
|
7
|
+
* its errors instead of stdout.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* Core's `defaultPresenter` writes an invalid invocation's help with
|
|
11
|
+
* `Console.log` and its errors with `Console.error`. Here it runs under a
|
|
12
|
+
* `Console` whose `log` is the ambient one's `error`, so the help and errors
|
|
13
|
+
* keep core's rendering, Formatter resolution included, and only the stream
|
|
14
|
+
* moves. An explicit `--help`, a bare group invocation and `--version` are
|
|
15
|
+
* other events, presented by core unchanged on stdout.
|
|
16
|
+
*/
|
|
17
|
+
const usageErrorsToStderr = { present: (event) => event._tag !== "InvalidInvocation" ? CliOutput.defaultPresenter.present(event) : Effect.gen(function* () {
|
|
18
|
+
const sink = yield* Console.Console;
|
|
19
|
+
const toStderr = Object.assign(Object.create(sink), { log: (...args) => sink.error(...args) });
|
|
20
|
+
yield* CliOutput.defaultPresenter.present(event).pipe(Effect.provideService(Console.Console, toStderr));
|
|
21
|
+
}) };
|
|
22
|
+
/**
|
|
23
|
+
* Run `program` so a help document printed together with parse errors goes to
|
|
24
|
+
* stderr, beside the errors, instead of stdout.
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* Provides core's `CliOutput.Presenter`, which `Command.runWith` hands each
|
|
28
|
+
* help, invalid-invocation and version event to. A Presenter provided inside
|
|
29
|
+
* `program` replaces this one.
|
|
30
|
+
*
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
const routeHelpOnUsageError = (program) => Effect.provideService(program, CliOutput.Presenter, usageErrorsToStderr);
|
|
34
|
+
|
|
35
|
+
export { routeHelpOnUsageError };
|
package/internal/ansi.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// 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
|
+
export { nearest256, openSequence, paintStyle, parseHex };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
import { CurrentRuntimeEnv } from "@effected/env";
|
|
3
|
+
|
|
4
|
+
// 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
|
+
export { autoFormat, underGithubActions };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Config, Effect, Option } from "effect";
|
|
2
|
+
|
|
3
|
+
// 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
|
+
export { canPrompt };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Fmt } from "../Fmt.js";
|
|
2
|
+
|
|
3
|
+
// 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
|
+
export { columnLabel, counterLabel, countsTableOf, totalOf, visibleCountersOf };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Context, LogLevel, Logger, References } from "effect";
|
|
2
|
+
|
|
3
|
+
// 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-kit/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
|
+
export { Level, formatNdjson, passes };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Option, Path } from "effect";
|
|
2
|
+
|
|
3
|
+
// src/internal/displayPath.ts
|
|
4
|
+
/**
|
|
5
|
+
* The display that shows every path as it is.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
const identity = (absolute) => absolute;
|
|
10
|
+
/**
|
|
11
|
+
* The path display in effect: the identity unless `CliEnv.layer` was given a `displayPath`.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
const CurrentDisplayPath = Context.Reference("@effected/cli-kit/CurrentDisplayPath", { defaultValue: () => identity });
|
|
16
|
+
/**
|
|
17
|
+
* A path relative to `root` when it lies under it, and the path unchanged when it is `root` itself or lies outside it
|
|
18
|
+
* (a `../..` chain says less than the absolute path). With a `Path` service its `relative` decides; without one, a
|
|
19
|
+
* plain prefix test on `/` and `\` does.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
const relativeTo = (root, path) => Option.match(path, {
|
|
24
|
+
onSome: (p) => (absolute) => {
|
|
25
|
+
const rel = p.relative(root, absolute);
|
|
26
|
+
return rel === "" || rel === ".." || rel.startsWith(`..${p.sep}`) || rel.startsWith("../") || p.isAbsolute(rel) ? absolute : rel;
|
|
27
|
+
},
|
|
28
|
+
onNone: () => {
|
|
29
|
+
let end = root.length;
|
|
30
|
+
while (end > 0 && (root[end - 1] === "/" || root[end - 1] === "\\")) end -= 1;
|
|
31
|
+
const base = root.slice(0, end);
|
|
32
|
+
return (absolute) => {
|
|
33
|
+
const next = absolute.charAt(base.length);
|
|
34
|
+
return absolute.startsWith(base) && (next === "/" || next === "\\") && absolute.length > base.length + 1 ? absolute.slice(base.length + 1) : absolute;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* Sets {@link CurrentDisplayPath} from the option, reading `Path` from the context when it is there. No option sets
|
|
40
|
+
* nothing.
|
|
41
|
+
*
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
const displayPathLayer = (option) => option === void 0 ? Layer.empty : Layer.effect(CurrentDisplayPath)(typeof option === "function" ? Effect.succeed(option) : Effect.map(Effect.serviceOption(Path.Path), (path) => relativeTo(option.relativeTo, path)));
|
|
45
|
+
|
|
46
|
+
export { CurrentDisplayPath, displayPathLayer, identity, relativeTo };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/internal/displayWidth.ts
|
|
2
|
+
const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
3
|
+
const ANSI = /\u001B(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007\u001B]*(?:\u0007|\u001B\\)|[PX^_][^\u001B]*\u001B\\|[()*+\-./][0-~]|\$[()*+\-./]?[@-~]|#[3-68]| [FGLMN]|[0-?@-Z\\^_c])/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 escape sequences: CSI (including SGR colour), OSC (including OSC-8 hyperlinks), the DCS, SOS,
|
|
9
|
+
* PM and APC strings, and every other escape (`ESC c`, `ESC 7`, `ESC ( B`).
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
const stripAnsi = (input) => input.replace(ANSI, "");
|
|
14
|
+
/**
|
|
15
|
+
* Whether `input` is nothing but escape sequences, as {@link stripAnsi} reads them; empty text is not. Every sequence
|
|
16
|
+
* starts with ESC, so text that does not is answered without running the strip.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
const isEscapeOnly = (input) => input.charCodeAt(0) === 27 && stripAnsi(input) === "";
|
|
21
|
+
/**
|
|
22
|
+
* The display width of `input` in terminal columns: graphemes, wide East Asian characters and emoji count two,
|
|
23
|
+
* combining marks, control characters and ANSI escapes count none.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
const displayWidth = (input) => {
|
|
28
|
+
let width = 0;
|
|
29
|
+
for (const { segment } of segmenter.segment(stripAnsi(input))) {
|
|
30
|
+
if (ZERO2.test(segment)) continue;
|
|
31
|
+
width += EMOJI.test(segment) || /^\p{RI}{2}/u.test(segment) || WIDE2.test(Array.from(segment)[0] ?? "") ? 2 : 1;
|
|
32
|
+
}
|
|
33
|
+
return width;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The grapheme clusters of `input`, in order; a cluster is never split.
|
|
37
|
+
*
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
const graphemes = (input) => Array.from(segmenter.segment(input), (s) => s.segment);
|
|
41
|
+
|
|
42
|
+
export { displayWidth, graphemes, isEscapeOnly, stripAnsi };
|