@effected/cli 0.22.2 → 0.23.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 +2 -1
- package/CliFailure.js +15 -29
- package/CliFormat.js +69 -0
- package/CliLog.js +38 -16
- package/CliRuntime.js +70 -48
- package/Fmt.js +123 -2
- package/Render.js +5 -2
- package/index.d.ts +272 -5
- package/index.js +2 -1
- package/internal/causeMessage.js +57 -0
- package/internal/displayPath.js +46 -0
- package/internal/displayWidth.js +11 -3
- package/internal/failureTarget.js +2 -1
- package/internal/inspect.js +40 -0
- package/package.json +7 -5
- package/ui/CliUi.js +86 -14
- 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 +200 -3
package/CliEnv.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CliInteractive } from "./CliInteractive.js";
|
|
2
2
|
import { ambientLinksLayer } from "./CliLinks.js";
|
|
3
3
|
import { CliTheme } from "./CliTheme.js";
|
|
4
|
+
import { displayPathLayer } from "./internal/displayPath.js";
|
|
4
5
|
import { CliPrompt } from "./CliPrompt.js";
|
|
5
6
|
import { ConfigProvider, Layer, Option } from "effect";
|
|
6
7
|
import { Audience, CurrentRuntimeEnv, TerminalEnv } from "@effected/env";
|
|
@@ -50,7 +51,7 @@ var CliEnv = class {
|
|
|
50
51
|
...options.editorLinksEnvVar === void 0 ? {} : { envVar: options.editorLinksEnvVar }
|
|
51
52
|
}).pipe(Layer.provideMerge(withTheme));
|
|
52
53
|
const withInteractive = CliInteractive.layer.pipe(Layer.provideMerge(withLinks));
|
|
53
|
-
return Layer.mergeAll(CliPrompt.gateTerminal, CliPrompt.gateWizard, CliTheme.promptTheme).pipe(Layer.provideMerge(withInteractive));
|
|
54
|
+
return Layer.mergeAll(CliPrompt.gateTerminal, CliPrompt.gateWizard, CliTheme.promptTheme, displayPathLayer(options.displayPath)).pipe(Layer.provideMerge(withInteractive));
|
|
54
55
|
};
|
|
55
56
|
/**
|
|
56
57
|
* The environment services a test fixes, needing nothing and reading nothing of the host's: `TerminalEnv` and
|
package/CliFailure.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Cancelled } from "./Cancelled.js";
|
|
2
2
|
import { Fmt } from "./Fmt.js";
|
|
3
|
+
import { identity } from "./internal/displayPath.js";
|
|
3
4
|
import { Doc } from "./Doc.js";
|
|
4
5
|
import { issueEntries, issueTreeChildren } from "./internal/format.js";
|
|
6
|
+
import { failureText, readProperty, tagOf } from "./internal/inspect.js";
|
|
5
7
|
import { splitFrame } from "./internal/splitFrame.js";
|
|
6
8
|
import { NotInteractive } from "./NotInteractive.js";
|
|
7
9
|
import { Status } from "./Status.js";
|
|
@@ -24,22 +26,7 @@ const CliDoc = Symbol.for("@effected/cli/CliDoc");
|
|
|
24
26
|
const MAX_DEPTH = 8;
|
|
25
27
|
const MAX_SPANS = 32;
|
|
26
28
|
const hasCliDoc = (value) => typeof value === "object" && value !== null && typeof value[CliDoc] === "function";
|
|
27
|
-
const describe = (value) => {
|
|
28
|
-
try {
|
|
29
|
-
const text = String(value);
|
|
30
|
-
if (text !== "[object Object]") return text;
|
|
31
|
-
const message = value?.message;
|
|
32
|
-
return typeof message === "string" ? message : text;
|
|
33
|
-
} catch {
|
|
34
|
-
return "[unprintable value]";
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
29
|
const firstLine = (text) => text.split(/\r\n|\r|\n/, 1)[0] ?? "";
|
|
38
|
-
const tagOf = (value) => {
|
|
39
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
40
|
-
const tag = value._tag;
|
|
41
|
-
return typeof tag === "string" ? tag : void 0;
|
|
42
|
-
};
|
|
43
30
|
/** The failure status and the text of a message: one block per line, the status on the first. */
|
|
44
31
|
const failureBlocks = (message) => {
|
|
45
32
|
const [first = "", ...rest] = message.split(/\r\n|\r|\n/);
|
|
@@ -48,15 +35,13 @@ const failureBlocks = (message) => {
|
|
|
48
35
|
/** The issue of a schema failure: the value itself, or the `issue` an error carries. */
|
|
49
36
|
const issueOf = (error) => {
|
|
50
37
|
if (SchemaIssue.isIssue(error)) return error;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (SchemaIssue.isIssue(issue)) return issue;
|
|
54
|
-
}
|
|
38
|
+
const issue = readProperty(error, "issue");
|
|
39
|
+
return SchemaIssue.isIssue(issue) ? issue : void 0;
|
|
55
40
|
};
|
|
56
41
|
const schemaBlocks = (error) => {
|
|
57
42
|
const entries = issueEntries(issueOf(error));
|
|
58
43
|
if (entries.length === 0) return void 0;
|
|
59
|
-
const header = typeof error === "object" && error !== null && !SchemaIssue.isIssue(error) ? firstLine(
|
|
44
|
+
const header = typeof error === "object" && error !== null && !SchemaIssue.isIssue(error) ? firstLine(failureText(readProperty(error, "message") ?? "")) : "";
|
|
60
45
|
const root = {
|
|
61
46
|
label: [
|
|
62
47
|
Doc.status(Status.core, "failure"),
|
|
@@ -138,7 +123,7 @@ const frameBlock = (frame, displayPath) => {
|
|
|
138
123
|
return Doc.paragraph(Doc.text("at ", "muted"), ...frame.fn === void 0 ? [] : [`${frame.fn} `], Doc.link(target, where));
|
|
139
124
|
};
|
|
140
125
|
const stackBlock = (defect, displayPath, mode) => {
|
|
141
|
-
const { frames, hidden } = cleanStack(defect
|
|
126
|
+
const { frames, hidden } = cleanStack(readProperty(defect, "stack"), mode);
|
|
142
127
|
if (frames.length > 0) {
|
|
143
128
|
const shown = frames.map((frame) => frameBlock(frame, displayPath));
|
|
144
129
|
const note = hidden === 0 ? [] : [Doc.paragraph(Doc.text(`(+${hidden} internal frames hidden)`, "muted"))];
|
|
@@ -151,11 +136,11 @@ const stackBlock = (defect, displayPath, mode) => {
|
|
|
151
136
|
const causeTree = (defect) => {
|
|
152
137
|
const chain = [];
|
|
153
138
|
const seen = /* @__PURE__ */ new Set([defect]);
|
|
154
|
-
for (let current = defect
|
|
139
|
+
for (let current = readProperty(defect, "cause"); current !== void 0 && chain.length < MAX_DEPTH;) {
|
|
155
140
|
if (seen.has(current)) break;
|
|
156
141
|
seen.add(current);
|
|
157
|
-
chain.push(firstLine(
|
|
158
|
-
current = current instanceof Error ? current
|
|
142
|
+
chain.push(firstLine(failureText(current)));
|
|
143
|
+
current = current instanceof Error ? readProperty(current, "cause") : void 0;
|
|
159
144
|
}
|
|
160
145
|
if (chain.length === 0) return void 0;
|
|
161
146
|
const nest = (index) => index === chain.length - 1 ? { label: chain[index] } : {
|
|
@@ -163,13 +148,13 @@ const causeTree = (defect) => {
|
|
|
163
148
|
children: [nest(index + 1)]
|
|
164
149
|
};
|
|
165
150
|
return Doc.tree({
|
|
166
|
-
label: firstLine(
|
|
151
|
+
label: firstLine(failureText(defect)),
|
|
167
152
|
children: [nest(0)]
|
|
168
153
|
});
|
|
169
154
|
};
|
|
170
155
|
const dieBlocks = (defect, spans, displayPath, mode) => {
|
|
171
156
|
if (defect instanceof Cancelled || defect instanceof NotInteractive) return [Doc.paragraph(defect.message)];
|
|
172
|
-
const header = failureBlocks(
|
|
157
|
+
const header = failureBlocks(failureText(defect));
|
|
173
158
|
if (!(defect instanceof Error)) return [...header, ...spans];
|
|
174
159
|
const chain = causeTree(defect);
|
|
175
160
|
return [
|
|
@@ -191,7 +176,7 @@ const failBlocks = (error, spans, options) => {
|
|
|
191
176
|
if (error instanceof Cancelled || error instanceof NotInteractive) return [Doc.paragraph(error.message)];
|
|
192
177
|
const schema = schemaBlocks(error);
|
|
193
178
|
if (schema !== void 0) return [...schema, ...spans];
|
|
194
|
-
return [...failureBlocks(
|
|
179
|
+
return [...failureBlocks(failureText(error)), ...spans];
|
|
195
180
|
};
|
|
196
181
|
/** A file of the kit's own packages or of Effect, as installed: a span defined there is not the program's. */
|
|
197
182
|
const isKitFile = (file) => /[\\/]node_modules[\\/](?:@effected[\\/]|effect[\\/])/.test(file);
|
|
@@ -272,7 +257,8 @@ const spanBlocks = (reason, mode, appDir) => {
|
|
|
272
257
|
* one line `interrupted`.
|
|
273
258
|
*
|
|
274
259
|
* All text goes through the document, so a control character in a message or a stack frame never reaches the terminal.
|
|
275
|
-
*
|
|
260
|
+
* Building it never throws: an error whose `_tag`, `message`, `stack`, `cause` or `issue` getter throws is drawn without
|
|
261
|
+
* that part (an unreadable `stack` as `no stack`, an unreadable `cause` ending the chain there). Render it with `Render.context` and `Render.plain`, `ansi`, `markdown` or `githubLog`, or `Doc.print` it.
|
|
276
262
|
*
|
|
277
263
|
* @public
|
|
278
264
|
*/
|
|
@@ -287,7 +273,7 @@ var CliFailure = class {
|
|
|
287
273
|
static toDoc = (cause, options) => {
|
|
288
274
|
const reasons = cause.reasons;
|
|
289
275
|
if (reasons.length > 0 && reasons.every(Cause.isInterruptReason)) return [Doc.paragraph("interrupted")];
|
|
290
|
-
const displayPath = options?.displayPath ??
|
|
276
|
+
const displayPath = options?.displayPath ?? identity;
|
|
291
277
|
return reasons.flatMap((reason) => {
|
|
292
278
|
const spans = options?.spans ?? "app";
|
|
293
279
|
const appDir = packageDirOf(options?.appModule);
|
package/CliFormat.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { underGithubActions } from "./internal/autoFormat.js";
|
|
2
|
+
import { Doc } from "./Doc.js";
|
|
3
|
+
import { neutralizeJson } from "./internal/logSafety.js";
|
|
4
|
+
import { Console, Effect } from "effect";
|
|
5
|
+
import { Flag } from "effect/cli";
|
|
6
|
+
|
|
7
|
+
// src/CliFormat.ts
|
|
8
|
+
/**
|
|
9
|
+
* A `--format` flag and the one print that honours it, so a command builds its report once as a `Doc` and its data
|
|
10
|
+
* once as a value, and never writes per-format output by hand.
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* The flag is core's `Flag.Literals` with the kit's names, default and description; the print hands every document
|
|
14
|
+
* format to `Doc.print` and writes `json` itself.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* const format = CliFormat.flag(["auto", "markdown", "json"])
|
|
18
|
+
* const doctor = Command.make("doctor", { format }, ({ format }) =>
|
|
19
|
+
* CliFormat.print(format, { doc: () => report(results), json: () => results }),
|
|
20
|
+
* )
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @public
|
|
24
|
+
*/
|
|
25
|
+
var CliFormat = class {
|
|
26
|
+
constructor() {}
|
|
27
|
+
/**
|
|
28
|
+
* A `--format` flag offering `formats`.
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* Its default is `options.default`, else `auto` when it is offered, else the first format. Any other value is a
|
|
32
|
+
* usage error from core's parser, naming the accepted ones.
|
|
33
|
+
*
|
|
34
|
+
* @param formats - the formats the command offers, at least one
|
|
35
|
+
* @param options - the default, the flag's name and its description
|
|
36
|
+
*/
|
|
37
|
+
static flag = (formats, options) => {
|
|
38
|
+
const fallback = options?.default ?? (formats.includes("auto") ? "auto" : formats[0]);
|
|
39
|
+
return Flag.Literals(options?.name ?? "format", formats).pipe(Flag.withDefault(fallback), Flag.withDescription(options?.description ?? `Output format: ${formats.join(", ")} (default ${fallback})`));
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Print a command's output in `format`.
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* `json` writes `output.json()` as JSON, one `Console.log` (or `Console.error` for stderr), indented by `indent`.
|
|
46
|
+
* Under GitHub Actions a `##[` inside it is written as the JSON escape `#\u0023[`, which decodes to the same text,
|
|
47
|
+
* so the runner cannot read it as a command. A value JSON cannot hold (a cycle, a bigint) is a defect. Every other
|
|
48
|
+
* format prints `output.doc()` with `Doc.print`, which renders it as `auto` or the named renderer would.
|
|
49
|
+
*
|
|
50
|
+
* @param format - the chosen format, usually the value of {@link CliFormat.flag}
|
|
51
|
+
* @param output - the document and the data
|
|
52
|
+
* @param options - the stream and the JSON indent
|
|
53
|
+
*/
|
|
54
|
+
static print = (format, output, options) => Effect.gen(function* () {
|
|
55
|
+
const stream = options?.stream ?? "stdout";
|
|
56
|
+
if (format !== "json") return yield* Doc.print(output.doc(), {
|
|
57
|
+
stream,
|
|
58
|
+
format
|
|
59
|
+
});
|
|
60
|
+
const data = output.json;
|
|
61
|
+
if (data === void 0) return yield* Effect.die(/* @__PURE__ */ new Error("CliFormat.print: format json needs output.json"));
|
|
62
|
+
const text = yield* Effect.sync(() => JSON.stringify(data(), null, options?.indent ?? 2));
|
|
63
|
+
if (text === void 0) return yield* Effect.die(/* @__PURE__ */ new Error("CliFormat.print: output.json() is not JSON"));
|
|
64
|
+
const safe = (yield* underGithubActions) ? neutralizeJson(text) : text;
|
|
65
|
+
yield* stream === "stdout" ? Console.log(safe) : Console.error(safe);
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export { CliFormat };
|
package/CliLog.js
CHANGED
|
@@ -19,18 +19,20 @@ const indentOf = (indent) => {
|
|
|
19
19
|
if (typeof indent === "number") return Number.isFinite(indent) && indent > 0 ? " ".repeat(Math.min(MAX_INDENT, Math.floor(indent))) : "";
|
|
20
20
|
return sanitize(indent).replace(/\r\n|\r|\n/g, "");
|
|
21
21
|
};
|
|
22
|
-
/** The accepted spellings of a level, lower-cased, to the level they mean. */
|
|
23
|
-
const LEVELS =
|
|
24
|
-
all
|
|
25
|
-
trace
|
|
26
|
-
debug
|
|
27
|
-
info
|
|
28
|
-
warn
|
|
29
|
-
warning
|
|
30
|
-
error
|
|
31
|
-
fatal
|
|
32
|
-
none
|
|
33
|
-
|
|
22
|
+
/** The accepted spellings of a level, lower-cased, to the level they mean. A `Map`, so no inherited key matches. */
|
|
23
|
+
const LEVELS = /* @__PURE__ */ new Map([
|
|
24
|
+
["all", "All"],
|
|
25
|
+
["trace", "Trace"],
|
|
26
|
+
["debug", "Debug"],
|
|
27
|
+
["info", "Info"],
|
|
28
|
+
["warn", "Warn"],
|
|
29
|
+
["warning", "Warn"],
|
|
30
|
+
["error", "Error"],
|
|
31
|
+
["fatal", "Fatal"],
|
|
32
|
+
["none", "None"]
|
|
33
|
+
]);
|
|
34
|
+
/** A level's spelling, in any case, to the level it means. */
|
|
35
|
+
const parseLevel = (text) => Option.fromUndefinedOr(LEVELS.get(text.toLowerCase()));
|
|
34
36
|
const LEVEL_STYLES = {
|
|
35
37
|
FATAL: {
|
|
36
38
|
fg: "red",
|
|
@@ -57,14 +59,14 @@ const readLevel = (explicit, envVar) => Effect.gen(function* () {
|
|
|
57
59
|
level: "None",
|
|
58
60
|
invalid: void 0
|
|
59
61
|
};
|
|
60
|
-
const level =
|
|
61
|
-
if (level
|
|
62
|
-
level,
|
|
62
|
+
const level = parseLevel(raw.value);
|
|
63
|
+
if (Option.isSome(level)) return {
|
|
64
|
+
level: level.value,
|
|
63
65
|
invalid: void 0
|
|
64
66
|
};
|
|
65
67
|
return {
|
|
66
68
|
level: "None",
|
|
67
|
-
invalid: `${envVar}=${raw.value} is not a log level (${
|
|
69
|
+
invalid: `${envVar}=${raw.value} is not a log level (${[...LEVELS.keys()].join("|")}); ignoring it`
|
|
68
70
|
};
|
|
69
71
|
});
|
|
70
72
|
/**
|
|
@@ -149,6 +151,26 @@ var CliLog = class CliLog {
|
|
|
149
151
|
* cannot lower it below the level the layer installed, because Effect has already dropped those records.
|
|
150
152
|
*/
|
|
151
153
|
static Level = Level;
|
|
154
|
+
/**
|
|
155
|
+
* Read a log level the way `CliLog.layer` reads its environment variable: `Option.some` of the level, or
|
|
156
|
+
* `Option.none()` when the text is not one.
|
|
157
|
+
*
|
|
158
|
+
* @remarks
|
|
159
|
+
* Case-insensitive, and not trimmed. The spellings are `all`, `trace`, `debug`, `info`, `warn` (or `warning`),
|
|
160
|
+
* `error`, `fatal` and `none`. For a host that reads a level from its own flag or config and wants the same answer
|
|
161
|
+
* the layer would give.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```ts
|
|
165
|
+
* CliLog.parseLevel("WARNING") // Option.some("Warn")
|
|
166
|
+
* CliLog.parseLevel("loud") // Option.none()
|
|
167
|
+
* ```
|
|
168
|
+
*
|
|
169
|
+
* @param text - the level's spelling
|
|
170
|
+
*/
|
|
171
|
+
static parseLevel(text) {
|
|
172
|
+
return parseLevel(text);
|
|
173
|
+
}
|
|
152
174
|
static layer(options = {}) {
|
|
153
175
|
const file = "file" in options ? options.file : void 0;
|
|
154
176
|
const format = options.format ?? "auto";
|
package/CliRuntime.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Cancelled } from "./Cancelled.js";
|
|
2
2
|
import { sanitize } from "./Fmt.js";
|
|
3
|
+
import { failureText, tagOf } from "./internal/inspect.js";
|
|
3
4
|
import { NotInteractive } from "./NotInteractive.js";
|
|
4
5
|
import { FailureTargetCell, currentTarget, fallbackTarget, guardConsumerLines, linesOf, plainFailureLines, readSpans, refreshFailureTarget } from "./internal/failureTarget.js";
|
|
5
6
|
import { CliColor } from "./CliColor.js";
|
|
@@ -11,24 +12,29 @@ import { CliLogger, makeCliLogger } from "./CliLogger.js";
|
|
|
11
12
|
import { CliLog, envBuildLogLayer, platformLogLayer } from "./CliLog.js";
|
|
12
13
|
import { ExitRequested } from "./internal/ExitRequested.js";
|
|
13
14
|
import { routeHelpOnUsageError } from "./internal/HelpRouting.js";
|
|
14
|
-
import { Cause, Effect, Layer, Logger, MutableRef, Runtime } from "effect";
|
|
15
|
-
import { CliError } from "effect/cli";
|
|
15
|
+
import { Cause, Effect, Layer, Logger, MutableRef, Option, Runtime } from "effect";
|
|
16
|
+
import { CliError, CliOutput } from "effect/cli";
|
|
16
17
|
import { CommandNeutralizer } from "@effected/github-commands";
|
|
18
|
+
import { withDistribution, withDistributionVersion } from "@effected/engine";
|
|
17
19
|
|
|
18
20
|
// src/CliRuntime.ts
|
|
19
21
|
const isShowHelp = (u) => CliError.isCliError(u) && u._tag === "ShowHelp";
|
|
20
22
|
/** A `UserError` `Command.runWith` already printed: it sets the mark to `false` after rendering. */
|
|
21
23
|
const isRenderedUserError = (u) => CliError.isCliError(u) && u._tag === "UserError" && Runtime.getErrorReported(u) === false;
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The program with `CurrentDistribution` set (engine's own `withDistribution`), and `--version` decorated with the
|
|
26
|
+
* carrier's suffix (`withDistributionVersion`), wrapping whatever `formatVersion` is in effect rather than replacing a
|
|
27
|
+
* coloured or custom one.
|
|
28
|
+
*/
|
|
29
|
+
const withCarrier = (program, distribution) => {
|
|
30
|
+
const provided = program.pipe(withDistribution(distribution));
|
|
31
|
+
return distribution === null ? provided : Effect.updateService(provided, CliOutput.Formatter, (formatter) => ({
|
|
32
|
+
...formatter,
|
|
33
|
+
formatVersion: withDistributionVersion(Option.some(distribution))(formatter.formatVersion)
|
|
34
|
+
}));
|
|
31
35
|
};
|
|
36
|
+
/** The last line of defence of a failure report: the error's text, sanitised and neutralized, whatever else broke. */
|
|
37
|
+
const lastResort = (error) => CommandNeutralizer.lines(sanitize(failureText(error, "[unprintable failure]")));
|
|
32
38
|
const toLines = (rendered) => typeof rendered === "string" ? [rendered] : rendered;
|
|
33
39
|
/**
|
|
34
40
|
* The error's own exit code when it carries one, otherwise the fallback.
|
|
@@ -39,7 +45,17 @@ const toLines = (rendered) => typeof rendered === "string" ? [rendered] : render
|
|
|
39
45
|
* silently override a deliberate `1`. Testing for the marker keeps "the error
|
|
40
46
|
* chose its code" distinct from "nothing chose".
|
|
41
47
|
*/
|
|
42
|
-
const chooseExitCode = (error, fallback) =>
|
|
48
|
+
const chooseExitCode = (error, fallback, byTag) => {
|
|
49
|
+
if (typeof error === "object" && error !== null && Runtime.errorExitCode in error) return Runtime.getErrorExitCode(error);
|
|
50
|
+
if (byTag === void 0) return fallback ?? 1;
|
|
51
|
+
const tag = tagOf(error);
|
|
52
|
+
return tag !== void 0 && Object.hasOwn(byTag, tag) ? byTag[tag] : fallback ?? 1;
|
|
53
|
+
};
|
|
54
|
+
/** Every `exitCodes` entry an integer in `1..255`, or a `RangeError` naming the first that is not. */
|
|
55
|
+
const checkExitCodes = (exitCodes) => {
|
|
56
|
+
if (exitCodes === void 0) return;
|
|
57
|
+
for (const [tag, code] of Object.entries(exitCodes)) if (!isExitCode(code) || code === 0) throw new RangeError(`CliRuntime: exitCodes.${tag} must be an integer 1..255, received ${String(code)}`);
|
|
58
|
+
};
|
|
43
59
|
/**
|
|
44
60
|
* Report a CLI program's failures through the program's own logger.
|
|
45
61
|
*
|
|
@@ -148,43 +164,49 @@ var CliRuntime = class CliRuntime {
|
|
|
148
164
|
*
|
|
149
165
|
* @param options - how to render the failure and which exit codes to use
|
|
150
166
|
*/
|
|
151
|
-
static reportFailures = (options = {}) =>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
} catch {
|
|
167
|
+
static reportFailures = (options = {}) => {
|
|
168
|
+
checkExitCodes(options.exitCodes);
|
|
169
|
+
return (effect) => effect.pipe(Effect.catchCause((cause) => {
|
|
170
|
+
if (Cause.hasInterruptsOnly(cause)) return Effect.failCause(cause);
|
|
171
|
+
const error = Cause.squash(cause);
|
|
172
|
+
if (error instanceof ExitRequested) return Effect.fail(error);
|
|
173
|
+
if (isShowHelp(error)) {
|
|
174
|
+
const code = error.errors.length > 0 ? options.usageExitCode ?? 64 : 0;
|
|
175
|
+
return Effect.fail(CliRuntime.reported(error, code));
|
|
176
|
+
}
|
|
177
|
+
if (isRenderedUserError(error)) return Effect.fail(CliRuntime.reported(error, chooseExitCode(error, options.usageExitCode ?? 64)));
|
|
178
|
+
const render = options.render;
|
|
179
|
+
return Effect.gen(function* () {
|
|
180
|
+
const target = yield* currentTarget.pipe(Effect.catchCause(() => Effect.succeed(fallbackTarget)));
|
|
181
|
+
const reportLines = (status, spans) => {
|
|
167
182
|
try {
|
|
168
|
-
return
|
|
183
|
+
return linesOf(cause, target, status, spans ?? target.spans);
|
|
169
184
|
} catch {
|
|
170
|
-
|
|
185
|
+
try {
|
|
186
|
+
return plainFailureLines(cause, status, spans ?? target.spans);
|
|
187
|
+
} catch {
|
|
188
|
+
return lastResort(error);
|
|
189
|
+
}
|
|
171
190
|
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
191
|
+
};
|
|
192
|
+
const defaultLines = reportLines(true);
|
|
193
|
+
const isDefect = !Cause.hasFails(cause);
|
|
194
|
+
const exitCode = chooseExitCode(error, options.exitCode, isDefect ? void 0 : options.exitCodes);
|
|
195
|
+
const details = {
|
|
196
|
+
cause,
|
|
197
|
+
isDefect,
|
|
198
|
+
exitCode,
|
|
199
|
+
isCancelled: error instanceof Cancelled,
|
|
200
|
+
isNotInteractive: error instanceof NotInteractive,
|
|
201
|
+
defaultLines,
|
|
202
|
+
lines: (options) => options?.status === false || options?.spans !== void 0 ? reportLines(options?.status !== false, options?.spans) : defaultLines
|
|
203
|
+
};
|
|
204
|
+
const lines = render === void 0 ? defaultLines : yield* guardConsumerLines(toLines(render(error, details)));
|
|
205
|
+
for (const line of lines) yield* Effect.logError(line).pipe(Effect.provideService(TrustedLine, true));
|
|
206
|
+
return yield* Effect.fail(CliRuntime.reported(error, exitCode));
|
|
207
|
+
});
|
|
208
|
+
}));
|
|
209
|
+
};
|
|
188
210
|
static main(program, options) {
|
|
189
211
|
const env = options.env === void 0 ? void 0 : CliEnv.layer(options.env);
|
|
190
212
|
const envLog = options.env?.log;
|
|
@@ -193,14 +215,14 @@ var CliRuntime = class CliRuntime {
|
|
|
193
215
|
const { spans, invalid } = yield* readSpans(options.env?.spans, options.env?.spansEnvVar);
|
|
194
216
|
if (invalid !== void 0) yield* Effect.logWarning(invalid).pipe(Effect.provideService(Logger.CurrentLoggers, /* @__PURE__ */ new Set([makeCliLogger(envLog?.logger)])));
|
|
195
217
|
yield* refreshFailureTarget(void 0, {
|
|
196
|
-
displayPath: options.env?.displayPath,
|
|
197
218
|
stackFrames: options.env?.stackFrames,
|
|
198
219
|
spans,
|
|
199
220
|
appModule: options.env?.appModule
|
|
200
221
|
});
|
|
201
222
|
}))).pipe(Layer.provideMerge(env));
|
|
223
|
+
const carried = options.distribution === void 0 ? program : withCarrier(program, options.distribution);
|
|
202
224
|
const run = Effect.gen(function* () {
|
|
203
|
-
yield* options.helpOnUsageError === "stderr" ? routeHelpOnUsageError(
|
|
225
|
+
yield* options.helpOnUsageError === "stderr" ? routeHelpOnUsageError(carried) : carried;
|
|
204
226
|
const exit = yield* CliExit;
|
|
205
227
|
const code = MutableRef.get(exit.code);
|
|
206
228
|
if (!isExitCode(code)) return yield* Effect.die(/* @__PURE__ */ new Error(`CliRuntime.main: CliExit code must be an integer 0..255, received ${code}`));
|
package/Fmt.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { causeMessage } from "./internal/causeMessage.js";
|
|
2
|
+
import { displayWidth, graphemes, isEscapeOnly, stripAnsi } from "./internal/displayWidth.js";
|
|
3
|
+
import { Duration, Option, Predicate } from "effect";
|
|
3
4
|
|
|
4
5
|
// src/Fmt.ts
|
|
5
6
|
const CONTROL = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g;
|
|
@@ -137,6 +138,126 @@ var Fmt = class {
|
|
|
137
138
|
* @param plural - the noun otherwise; `singular` with an `s` appended by default
|
|
138
139
|
*/
|
|
139
140
|
static plural = (n, singular, plural) => `${n} ${n === 1 ? singular : plural ?? `${singular}s`}`;
|
|
141
|
+
/**
|
|
142
|
+
* Pad `text` to `width` terminal columns, measured as {@link Fmt.width} measures: a wide character counts two and
|
|
143
|
+
* an escape sequence none, so coloured text pads to the same column as plain text.
|
|
144
|
+
*
|
|
145
|
+
* @remarks
|
|
146
|
+
* Text already `width` columns or wider is returned unchanged: compose with {@link Fmt.truncate} to cut it. A
|
|
147
|
+
* `width` that is not a finite number pads nothing. Never throws.
|
|
148
|
+
*
|
|
149
|
+
* @param text - the text to pad
|
|
150
|
+
* @param width - the columns the result should take
|
|
151
|
+
* @param options - the alignment and the fill
|
|
152
|
+
*/
|
|
153
|
+
static pad = (text, width, options) => {
|
|
154
|
+
const missing = Number.isFinite(width) ? Math.floor(width) - displayWidth(text) : 0;
|
|
155
|
+
if (missing <= 0) return text;
|
|
156
|
+
const fill = options?.fill !== void 0 && displayWidth(options.fill) === 1 && stripAnsi(options.fill) === options.fill ? options.fill : " ";
|
|
157
|
+
switch (options?.align) {
|
|
158
|
+
case "right": return `${fill.repeat(missing)}${text}`;
|
|
159
|
+
case "center": {
|
|
160
|
+
const before = Math.floor(missing / 2);
|
|
161
|
+
return `${fill.repeat(before)}${text}${fill.repeat(missing - before)}`;
|
|
162
|
+
}
|
|
163
|
+
default: return `${text}${fill.repeat(missing)}`;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* `text` without its escape sequences: CSI (colour, cursor moves, erases), OSC (titles, OSC 8 hyperlinks) ending in
|
|
168
|
+
* BEL or ST, the DCS, SOS, PM and APC strings, and every other escape, the nF sequences (`ESC ( B`) and the
|
|
169
|
+
* two-byte ones (`ESC 7`, `ESC 8`, `ESC =`, `ESC c`).
|
|
170
|
+
*
|
|
171
|
+
* @remarks
|
|
172
|
+
* The same strip {@link Fmt.width} and {@link Fmt.sanitize} start from. Only complete sequences go: a lone ESC, or
|
|
173
|
+
* any other control character, is kept, and so is an ESC in front of a lowercase letter other than `c`, which a
|
|
174
|
+
* stray ESC before a word would otherwise cost the word its first letter. An unterminated CSI or OSC keeps its `[`
|
|
175
|
+
* or `]`. Use {@link Fmt.sanitize} for text that will be laid out and printed.
|
|
176
|
+
*
|
|
177
|
+
* @param text - the text
|
|
178
|
+
*/
|
|
179
|
+
static stripAnsi = (text) => stripAnsi(text);
|
|
180
|
+
/**
|
|
181
|
+
* Whether `text` is nothing but escape sequences, as {@link Fmt.stripAnsi} reads them: a cursor hide or show, a
|
|
182
|
+
* clear-screen, an erase-scrollback. Empty text is not escape-only, and neither is a bare control character
|
|
183
|
+
* (`\r`, BEL), which is not an escape sequence.
|
|
184
|
+
*
|
|
185
|
+
* @param text - the text
|
|
186
|
+
*/
|
|
187
|
+
static isEscapeOnly = (text) => isEscapeOnly(text);
|
|
188
|
+
/**
|
|
189
|
+
* Integers as compact ranges: `[1, 2, 3, 5, 10, 11]` is `"1-3,5,10-11"`, a run of consecutive numbers written
|
|
190
|
+
* `start-end` and the rest one by one, joined with commas.
|
|
191
|
+
*
|
|
192
|
+
* @remarks
|
|
193
|
+
* The input is sorted and its duplicates dropped first, so order does not matter. Anything that is not a finite
|
|
194
|
+
* integer is left out, and no input is `""`. Never throws.
|
|
195
|
+
*
|
|
196
|
+
* @param numbers - the integers, such as line numbers
|
|
197
|
+
*/
|
|
198
|
+
static ranges = (numbers) => {
|
|
199
|
+
const sorted = [...new Set(Array.from(numbers).filter((n) => Number.isInteger(n)))].sort((a, b) => a - b);
|
|
200
|
+
const parts = [];
|
|
201
|
+
let index = 0;
|
|
202
|
+
while (index < sorted.length) {
|
|
203
|
+
const start = sorted[index];
|
|
204
|
+
let end = start;
|
|
205
|
+
while (index + 1 < sorted.length && sorted[index + 1] === end + 1) end = sorted[++index];
|
|
206
|
+
parts.push(start === end ? `${start}` : `${start}-${end}`);
|
|
207
|
+
index++;
|
|
208
|
+
}
|
|
209
|
+
return parts.join(",");
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* Any value as text, without ever throwing.
|
|
213
|
+
*
|
|
214
|
+
* @remarks
|
|
215
|
+
* A string is itself; a number, boolean, bigint or symbol is `String(value)`; `undefined` and `null` are
|
|
216
|
+
* `"undefined"` and `"null"`. A value with a string `message` (an `Error`, one from another realm, a plain
|
|
217
|
+
* `{ message }`) is that message. Anything else is its JSON, or `String(value)` when it has none (a cycle, a
|
|
218
|
+
* function), or `"[unprintable value]"` when that throws too. A getter that throws is a value with no message.
|
|
219
|
+
*
|
|
220
|
+
* The text is not sanitised: pass it through {@link Fmt.sanitize} before printing data you do not control. For
|
|
221
|
+
* the messages along an error's `cause` chain, use {@link Fmt.causeMessage}.
|
|
222
|
+
*
|
|
223
|
+
* @param value - the value
|
|
224
|
+
*/
|
|
225
|
+
static describe = (value) => {
|
|
226
|
+
if (Predicate.isString(value)) return value;
|
|
227
|
+
if (value === void 0 || value === null) return String(value);
|
|
228
|
+
if (typeof value !== "object" && typeof value !== "function") return String(value);
|
|
229
|
+
try {
|
|
230
|
+
if (Predicate.hasProperty(value, "message") && Predicate.isString(value.message)) return value.message;
|
|
231
|
+
} catch {}
|
|
232
|
+
try {
|
|
233
|
+
const json = JSON.stringify(value);
|
|
234
|
+
if (json !== void 0) return json;
|
|
235
|
+
} catch {}
|
|
236
|
+
try {
|
|
237
|
+
return String(value);
|
|
238
|
+
} catch {
|
|
239
|
+
return "[unprintable value]";
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
/**
|
|
243
|
+
* Every distinct non-empty message along a value's `cause` chain, nearest first and joined with `: `, or
|
|
244
|
+
* `undefined` when nothing along it has one.
|
|
245
|
+
*
|
|
246
|
+
* @remarks
|
|
247
|
+
* The kit's one guarded cause-message read, the one its own errors render a preserved cause with. A node's message
|
|
248
|
+
* is a thrown string or a non-empty string `message` property, read without `instanceof Error`, so an error from
|
|
249
|
+
* another realm or a plain `{ message }` object counts. A deeper message the joined text already contains is not
|
|
250
|
+
* repeated, and one that contains the joined text replaces it, so a `SqlError` wrapping SQLite's own text reads
|
|
251
|
+
* `Failed to execute statement: SQLITE_BUSY: database is locked`. It is the whole chain, not the innermost
|
|
252
|
+
* message alone. The walk stops after eight nodes, so a cyclic chain terminates.
|
|
253
|
+
*
|
|
254
|
+
* Never throws: a `message` getter that throws leaves that node without text, and a `cause` getter that throws ends
|
|
255
|
+
* the walk with the text found so far, `undefined` when there is none, so a caller's own fallback
|
|
256
|
+
* (`?? Fmt.describe(value)`) applies.
|
|
257
|
+
*
|
|
258
|
+
* @param value - the error, or whatever was thrown
|
|
259
|
+
*/
|
|
260
|
+
static causeMessage = (value) => causeMessage(value);
|
|
140
261
|
};
|
|
141
262
|
|
|
142
263
|
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 }
|