@effected/cli 0.22.2 → 0.24.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/CliEnv.js +17 -4
- package/CliFailure.js +15 -29
- package/CliFormat.js +124 -0
- package/CliInteractive.js +28 -4
- package/CliLog.js +38 -16
- package/CliPrompt.js +7 -0
- package/CliRuntime.js +80 -49
- package/CliTest.js +134 -22
- package/ConfigIssueRenderer.js +8 -2
- package/Fmt.js +194 -1
- package/Render.js +5 -2
- package/index.d.ts +438 -18
- package/index.js +2 -1
- package/internal/HelpRouting.js +21 -75
- package/internal/displayPath.js +46 -0
- package/internal/displayWidth.js +11 -3
- package/internal/failureTarget.js +2 -1
- package/internal/format.js +74 -3
- package/internal/inspect.js +40 -0
- package/internal/logSafety.js +1 -1
- package/internal/terminalCapabilities.js +23 -0
- package/package.json +8 -6
- package/testing.d.ts +68 -5
- package/ui/CliUi.js +90 -15
- package/ui/CliUiDetached.js +36 -0
- package/ui/CliUiLive.js +6 -9
- package/ui/CliUiRouteStream.js +72 -0
- package/ui/DocView.js +7 -4
- package/ui/UiProvider.js +4 -1
- package/ui/internal/UiProviders.js +19 -1
- package/ui/internal/inkChalk.js +24 -34
- package/ui/internal/lineRouter.js +70 -0
- package/ui/testing/CliUiTest.js +4 -1
- package/ui.d.ts +204 -4
package/CliTest.js
CHANGED
|
@@ -1,7 +1,57 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fmt } from "./Fmt.js";
|
|
2
|
+
import { Duration, Effect, Fiber, FileSystem, Option, Path, Stream, SubscriptionRef } from "effect";
|
|
2
3
|
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
3
4
|
|
|
4
5
|
// src/CliTest.ts
|
|
6
|
+
/** The complete lines of `captured` from `offset`, and the rest too once it has ended; with the offset after them. */
|
|
7
|
+
const linesFrom = (offset, captured) => {
|
|
8
|
+
const lines = [];
|
|
9
|
+
let start = offset;
|
|
10
|
+
let end = captured.text.indexOf("\n", start);
|
|
11
|
+
while (end !== -1) {
|
|
12
|
+
lines.push(captured.text.slice(start, captured.text[end - 1] === "\r" ? end - 1 : end));
|
|
13
|
+
start = end + 1;
|
|
14
|
+
end = captured.text.indexOf("\n", start);
|
|
15
|
+
}
|
|
16
|
+
if (captured.done && start < captured.text.length) {
|
|
17
|
+
lines.push(captured.text.slice(start));
|
|
18
|
+
start = captured.text.length;
|
|
19
|
+
}
|
|
20
|
+
return [start, lines];
|
|
21
|
+
};
|
|
22
|
+
/** The bound on a child that ignores its termination signal, when the caller gives none. */
|
|
23
|
+
const FORCE_KILL_AFTER = "2 seconds";
|
|
24
|
+
/** `options.forceKillAfter` or the default; a duration that is not finite is a wiring mistake and throws. */
|
|
25
|
+
const forceKillAfterOf = (options) => {
|
|
26
|
+
const input = options.forceKillAfter ?? FORCE_KILL_AFTER;
|
|
27
|
+
const duration = Duration.fromInput(input);
|
|
28
|
+
if (typeof input === "number" && Number.isNaN(input) || Option.isNone(duration) || !Duration.isFinite(duration.value) || Duration.toMillis(duration.value) < 0) throw new RangeError(`CliTest: forceKillAfter must be a finite, non-negative duration, received ${String(input)}`);
|
|
29
|
+
return duration.value;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Core's handle fails `exitCode` for a child a signal ended, naming the signal only in the error's text; this reads it
|
|
33
|
+
* back out, so a signal is an outcome as an exit code is. Any other failure (a failed stdin stream) stays a failure.
|
|
34
|
+
*/
|
|
35
|
+
const SIGNALLED = /receipt of signal: '(SIG[A-Z0-9]+)'/;
|
|
36
|
+
const endingOf = (exitCode) => exitCode.pipe(Effect.map((code) => ({
|
|
37
|
+
exitCode: Number(code),
|
|
38
|
+
signal: null
|
|
39
|
+
})), Effect.catchTag("PlatformError", (error) => {
|
|
40
|
+
const match = SIGNALLED.exec(`${error.message} ${Fmt.causeMessage(error) ?? ""}`);
|
|
41
|
+
return match === null ? Effect.fail(error) : Effect.succeed({
|
|
42
|
+
exitCode: null,
|
|
43
|
+
signal: match[1]
|
|
44
|
+
});
|
|
45
|
+
}));
|
|
46
|
+
const commandOf = (bin, args, options, forceKillAfter) => ChildProcess.make(options.execPath, [bin, ...args], {
|
|
47
|
+
forceKillAfter,
|
|
48
|
+
cwd: options.cwd ?? options.sandbox.root,
|
|
49
|
+
env: {
|
|
50
|
+
...options.sandbox.env,
|
|
51
|
+
...options.env
|
|
52
|
+
},
|
|
53
|
+
stdin: options.stdin === void 0 || options.stdin === "" ? Stream.empty : Stream.make(new TextEncoder().encode(options.stdin))
|
|
54
|
+
});
|
|
5
55
|
const text = (stream) => Stream.mkString(Stream.decodeText(stream));
|
|
6
56
|
/**
|
|
7
57
|
* Spawn a built CLI bin hermetically and read its exit code and streams as data.
|
|
@@ -67,28 +117,90 @@ var CliTest = class {
|
|
|
67
117
|
* default `"pipe"` stdio, which stays open until something writes to and
|
|
68
118
|
* ends it.
|
|
69
119
|
*/
|
|
70
|
-
static run = (bin, args, options) =>
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
120
|
+
static run = (bin, args, options) => {
|
|
121
|
+
const forceKillAfter = forceKillAfterOf(options);
|
|
122
|
+
return Effect.scoped(Effect.gen(function* () {
|
|
123
|
+
const handle = yield* (yield* ChildProcessSpawner.ChildProcessSpawner).spawn(commandOf(bin, args, options, forceKillAfter));
|
|
124
|
+
const [stdout, stderr, ending] = yield* Effect.all([
|
|
125
|
+
text(handle.stdout),
|
|
126
|
+
text(handle.stderr),
|
|
127
|
+
endingOf(handle.exitCode)
|
|
128
|
+
], { concurrency: "unbounded" });
|
|
129
|
+
return {
|
|
130
|
+
...ending,
|
|
131
|
+
stdout,
|
|
132
|
+
stderr
|
|
133
|
+
};
|
|
134
|
+
}));
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Start `execPath bin ...args` and return at once with a handle on it: its output as lines while it runs, a signal,
|
|
138
|
+
* and its exit, for a long-running command (a `--follow`, a watch mode, a server) that {@link CliTest.run} would wait
|
|
139
|
+
* on forever.
|
|
140
|
+
*
|
|
141
|
+
* @remarks
|
|
142
|
+
* The child lives in the caller's scope: when the scope closes first, the child is sent `SIGTERM`, then `SIGKILL`
|
|
143
|
+
* after `forceKillAfter` (`2 seconds` by default), so a test that ends early leaves nothing running and never waits
|
|
144
|
+
* on a child that ignores the signal. A child a signal ended reports `exitCode: null` and the signal in `result`. Both streams are drained from the start whether or not the test reads them, so a
|
|
145
|
+
* chatty child never blocks on a full pipe, and each stream replays from its first line to every reader. The
|
|
146
|
+
* options are {@link CliTest.run}'s, `stdin` included: the child gets that text, then an ended input.
|
|
147
|
+
*
|
|
148
|
+
* ```ts
|
|
149
|
+
* const child = yield* CliTest.spawn("dist/bin.js", ["logs", "--follow"], { sandbox, execPath: process.execPath })
|
|
150
|
+
* yield* Stream.runHead(child.stdout) // the first line: it is following
|
|
151
|
+
* yield* child.kill("SIGINT")
|
|
152
|
+
* assert.strictEqual(yield* child.exitCode, 0)
|
|
153
|
+
* ```
|
|
154
|
+
*
|
|
155
|
+
* @param bin - the built bin to run
|
|
156
|
+
* @param args - its arguments
|
|
157
|
+
* @param options - the sandbox, the node binary, and the working directory, environment and stdin
|
|
158
|
+
*/
|
|
159
|
+
static spawn = (bin, args, options) => {
|
|
160
|
+
const forceKillAfter = forceKillAfterOf(options);
|
|
161
|
+
return Effect.gen(function* () {
|
|
162
|
+
const handle = yield* (yield* ChildProcessSpawner.ChildProcessSpawner).spawn(commandOf(bin, args, options, forceKillAfter));
|
|
163
|
+
const capture = (stream) => Effect.gen(function* () {
|
|
164
|
+
const ref = yield* SubscriptionRef.make({
|
|
165
|
+
text: "",
|
|
166
|
+
done: false
|
|
167
|
+
});
|
|
168
|
+
return {
|
|
169
|
+
ref,
|
|
170
|
+
pump: yield* Stream.decodeText(stream).pipe(Stream.runForEach((chunk) => SubscriptionRef.update(ref, (s) => ({
|
|
171
|
+
text: s.text + chunk,
|
|
172
|
+
done: false
|
|
173
|
+
}))), Effect.ensuring(SubscriptionRef.update(ref, (s) => ({
|
|
174
|
+
text: s.text,
|
|
175
|
+
done: true
|
|
176
|
+
}))), Effect.forkScoped),
|
|
177
|
+
lines: SubscriptionRef.changes(ref).pipe(Stream.takeUntil((s) => s.done), Stream.mapAccum(() => 0, linesFrom))
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
const out = yield* capture(handle.stdout);
|
|
181
|
+
const err = yield* capture(handle.stderr);
|
|
182
|
+
const ending = endingOf(handle.exitCode);
|
|
183
|
+
return {
|
|
184
|
+
stdout: out.lines,
|
|
185
|
+
stderr: err.lines,
|
|
186
|
+
kill: (signal) => handle.kill({
|
|
187
|
+
killSignal: signal ?? "SIGTERM",
|
|
188
|
+
forceKillAfter
|
|
189
|
+
}).pipe(Effect.catchTag("PlatformError", (error) => Effect.flatMap(handle.isRunning, (running) => running ? Effect.fail(error) : Effect.void))),
|
|
190
|
+
exitCode: Effect.map(ending, (end) => end.exitCode),
|
|
191
|
+
result: Effect.gen(function* () {
|
|
192
|
+
const end = yield* ending;
|
|
193
|
+
yield* Fiber.join(out.pump);
|
|
194
|
+
yield* Fiber.join(err.pump);
|
|
195
|
+
return {
|
|
196
|
+
...end,
|
|
197
|
+
stdout: (yield* SubscriptionRef.get(out.ref)).text,
|
|
198
|
+
stderr: (yield* SubscriptionRef.get(err.ref)).text
|
|
199
|
+
};
|
|
200
|
+
})
|
|
201
|
+
};
|
|
79
202
|
});
|
|
80
|
-
|
|
81
|
-
const [stdout, stderr, exitCode] = yield* Effect.all([
|
|
82
|
-
text(handle.stdout),
|
|
83
|
-
text(handle.stderr),
|
|
84
|
-
handle.exitCode
|
|
85
|
-
], { concurrency: "unbounded" });
|
|
86
|
-
return {
|
|
87
|
-
exitCode: Number(exitCode),
|
|
88
|
-
stdout,
|
|
89
|
-
stderr
|
|
90
|
-
};
|
|
91
|
-
}));
|
|
203
|
+
};
|
|
92
204
|
};
|
|
93
205
|
|
|
94
206
|
export { CliTest };
|
package/ConfigIssueRenderer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatIssueWithHints } from "./internal/format.js";
|
|
2
2
|
|
|
3
3
|
// src/ConfigIssueRenderer.ts
|
|
4
4
|
/**
|
|
@@ -47,11 +47,17 @@ var ConfigIssueRenderer = class {
|
|
|
47
47
|
* `Effect.catchTag("ConfigValidationError", …)` the error is already this
|
|
48
48
|
* type.
|
|
49
49
|
*
|
|
50
|
+
* An unknown key the schema has a near match for at the same path ends in a
|
|
51
|
+
* hint, `unknown key at theme.colour (did you mean "color"?)`, drawn from the
|
|
52
|
+
* keys the struct declares there (every branch's, under a union) with
|
|
53
|
+
* {@link Fmt.suggest}'s threshold. A key with no near match is drawn exactly
|
|
54
|
+
* as {@link SchemaIssueRenderer} draws it.
|
|
55
|
+
*
|
|
50
56
|
* It cannot throw on a malformed value: the issue tree is validated by
|
|
51
57
|
* a guard before it is read, so a renderer on an error path never becomes the
|
|
52
58
|
* reason a program dies.
|
|
53
59
|
*/
|
|
54
|
-
static render = (error) =>
|
|
60
|
+
static render = (error) => formatIssueWithHints(error?.issue);
|
|
55
61
|
};
|
|
56
62
|
|
|
57
63
|
export { ConfigIssueRenderer };
|
package/Fmt.js
CHANGED
|
@@ -1,7 +1,40 @@
|
|
|
1
|
-
import { displayWidth, graphemes, stripAnsi } from "./internal/displayWidth.js";
|
|
1
|
+
import { displayWidth, graphemes, isEscapeOnly, stripAnsi } from "./internal/displayWidth.js";
|
|
2
2
|
import { Duration, Option } from "effect";
|
|
3
|
+
import { ErrorText } from "@effected/engine";
|
|
3
4
|
|
|
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
|
+
};
|
|
5
38
|
const CONTROL = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g;
|
|
6
39
|
/**
|
|
7
40
|
* Text made safe to lay out and print: complete ANSI and OSC sequences removed, every remaining control character
|
|
@@ -137,6 +170,166 @@ var Fmt = class {
|
|
|
137
170
|
* @param plural - the noun otherwise; `singular` with an `s` appended by default
|
|
138
171
|
*/
|
|
139
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/engine`'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/engine`'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/engine`'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
|
+
};
|
|
140
333
|
};
|
|
141
334
|
|
|
142
335
|
export { Fmt, sanitize };
|
package/Render.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Glyphs } from "./Glyphs.js";
|
|
|
4
4
|
import { paintStyle } from "./internal/ansi.js";
|
|
5
5
|
import { Token } from "./Token.js";
|
|
6
6
|
import { CliTheme } from "./CliTheme.js";
|
|
7
|
+
import { CurrentDisplayPath, identity } from "./internal/displayPath.js";
|
|
7
8
|
import { renderAnsi } from "./internal/renderAnsi.js";
|
|
8
9
|
import { renderPlain } from "./internal/renderPlain.js";
|
|
9
10
|
import { renderGithubLog } from "./internal/renderGithubLog.js";
|
|
@@ -62,6 +63,8 @@ var Render = class {
|
|
|
62
63
|
* - `groupDepth` is the fiber's count of open Actions log groups (the fiber-local keyed by
|
|
63
64
|
* `WorkflowCommand.groupDepthKey`, which `ActionLogger.group` provides), set when it is above `0`, so a
|
|
64
65
|
* document printed inside a group opens no second one; see {@link RenderContext.groupDepth}.
|
|
66
|
+
* - `displayPath` is the option, else the run's own path display (`CliEnvOptions.displayPath`, which
|
|
67
|
+
* `CliEnv.layer` sets), else the identity.
|
|
65
68
|
*
|
|
66
69
|
* @param stream - the stream the output is for
|
|
67
70
|
* @param options - an explicit width and a path display function
|
|
@@ -87,7 +90,7 @@ var Render = class {
|
|
|
87
90
|
hyperlinks: terminal[stream].hyperlinks,
|
|
88
91
|
audience: kind
|
|
89
92
|
}),
|
|
90
|
-
displayPath: options?.displayPath ?? (
|
|
93
|
+
displayPath: options?.displayPath ?? (yield* CurrentDisplayPath)
|
|
91
94
|
};
|
|
92
95
|
});
|
|
93
96
|
/**
|
|
@@ -117,7 +120,7 @@ var Render = class {
|
|
|
117
120
|
hyperlinks: true,
|
|
118
121
|
audience: options.audience
|
|
119
122
|
}),
|
|
120
|
-
displayPath: options.displayPath ??
|
|
123
|
+
displayPath: options.displayPath ?? identity,
|
|
121
124
|
...options.neutralizeWorkflowCommands ?? options.audience === "ci" ? { neutralizeWorkflowCommands: true } : options.neutralizeWorkflowCommands === false ? { neutralizeWorkflowCommands: false } : {},
|
|
122
125
|
...options.linkBase === void 0 ? {} : { linkBase: options.linkBase },
|
|
123
126
|
...options.groupDepth === void 0 ? {} : { groupDepth: options.groupDepth }
|