@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/CliEnv.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
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";
|
|
6
|
+
import { capabilitiesLayer } from "./internal/terminalCapabilities.js";
|
|
5
7
|
import { ConfigProvider, Layer, Option } from "effect";
|
|
6
8
|
import { Audience, CurrentRuntimeEnv, TerminalEnv } from "@effected/env";
|
|
7
9
|
|
|
@@ -29,6 +31,15 @@ import { Audience, CurrentRuntimeEnv, TerminalEnv } from "@effected/env";
|
|
|
29
31
|
* `Console` or `Stdio`, never `Terminal`. It also installs `CliTheme.promptTheme`, so core's prompts follow the
|
|
30
32
|
* theme.
|
|
31
33
|
*
|
|
34
|
+
* It sets core's `TerminalCapabilities` from `TerminalEnv` (through `TerminalEnv.capabilities` in `@effected/env`), so
|
|
35
|
+
* the colour of core's own help, version and error output follows the kit's colour rule, and `canAnimate` is stdout
|
|
36
|
+
* being a terminal that is not `dumb`. `canPrompt` alone is held at `true` whatever the terminal is: core skips a
|
|
37
|
+
* fallback prompt entirely when it is `false`, which would lose `CliPrompt.fallback`'s and `CliUi.fallback`'s
|
|
38
|
+
* `otherwise` in every piped run, and `CliInteractive` with `CliPrompt.gateTerminal` already owns that decision (the
|
|
39
|
+
* gate quits any other prompt at once when the run is not interactive). A `TerminalCapabilities` with
|
|
40
|
+
* `canPrompt: false` provided inside the program bypasses the kit's fallbacks: the parameter then fails as missing even
|
|
41
|
+
* when an `otherwise` is given.
|
|
42
|
+
*
|
|
32
43
|
* @public
|
|
33
44
|
*/
|
|
34
45
|
var CliEnv = class {
|
|
@@ -49,14 +60,15 @@ var CliEnv = class {
|
|
|
49
60
|
...options.editorLinks === void 0 ? {} : { editorLinks: options.editorLinks },
|
|
50
61
|
...options.editorLinksEnvVar === void 0 ? {} : { envVar: options.editorLinksEnvVar }
|
|
51
62
|
}).pipe(Layer.provideMerge(withTheme));
|
|
52
|
-
const
|
|
53
|
-
|
|
63
|
+
const withCapabilities = capabilitiesLayer.pipe(Layer.provideMerge(withLinks));
|
|
64
|
+
const withInteractive = CliInteractive.layer.pipe(Layer.provideMerge(withCapabilities));
|
|
65
|
+
return Layer.mergeAll(CliPrompt.gateTerminal, CliPrompt.gateWizard, CliTheme.promptTheme, displayPathLayer(options.displayPath)).pipe(Layer.provideMerge(withInteractive));
|
|
54
66
|
};
|
|
55
67
|
/**
|
|
56
68
|
* The environment services a test fixes, needing nothing and reading nothing of the host's: `TerminalEnv` and
|
|
57
69
|
* `Audience` from the answers given, `CliTheme` built from them as {@link CliEnv.layer} builds it, and
|
|
58
70
|
* `CliInteractive` set from them by the same rule (a human, every stream a terminal, and a `TERM` that is not
|
|
59
|
-
* `dumb`).
|
|
71
|
+
* `dumb`), and core's `TerminalCapabilities` from them as `CliEnv.layer` sets it, `canPrompt` held at `true`.
|
|
60
72
|
*
|
|
61
73
|
* @remarks
|
|
62
74
|
* `term` is handed to the theme and interactivity builds alone, through a `ConfigProvider` of their own: the
|
|
@@ -81,7 +93,8 @@ var CliEnv = class {
|
|
|
81
93
|
stderr: stream
|
|
82
94
|
}), Audience.layerTest(options.audience ?? "human"));
|
|
83
95
|
const term = ConfigProvider.layer(ConfigProvider.fromUnknown(options.term === void 0 ? {} : { TERM: options.term }));
|
|
84
|
-
|
|
96
|
+
const interactive = CliInteractive.layer.pipe(Layer.provideMerge(capabilitiesLayer));
|
|
97
|
+
return Layer.mergeAll(CliTheme.layer(options.theme), interactive).pipe(Layer.provide(term), Layer.provideMerge(facts));
|
|
85
98
|
};
|
|
86
99
|
};
|
|
87
100
|
|
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,124 @@
|
|
|
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
|
+
const BUILT_IN = /* @__PURE__ */ new Set([
|
|
9
|
+
"auto",
|
|
10
|
+
"plain",
|
|
11
|
+
"ansi",
|
|
12
|
+
"markdown",
|
|
13
|
+
"githubLog",
|
|
14
|
+
"json"
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* A `--format` flag and the one print that honours it, so a command builds its report once as a `Doc` and its data
|
|
18
|
+
* once as a value, and never writes per-format output by hand.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* The flag is core's `Flag.ChoiceWithValue` with the kit's names, default and description; the print hands every
|
|
22
|
+
* document format to `Doc.print` and writes `json` itself.
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* const format = CliFormat.flag(["auto", "markdown", "json"])
|
|
26
|
+
* const doctor = Command.make("doctor", { format }, ({ format }) =>
|
|
27
|
+
* CliFormat.print(format, { doc: () => report(results), json: () => results }),
|
|
28
|
+
* )
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
var CliFormat = class CliFormat {
|
|
34
|
+
constructor() {}
|
|
35
|
+
/**
|
|
36
|
+
* A `--format` flag offering `formats`, and the caller's own `aliases` and `custom` formats.
|
|
37
|
+
*
|
|
38
|
+
* @remarks
|
|
39
|
+
* Its default is `options.default`, else `auto` when it is offered, else the first format. Any other value is a
|
|
40
|
+
* usage error from core's parser, naming the accepted ones. The value is the built-in format a name stands for
|
|
41
|
+
* (`human` under `{ human: "auto" }` is `auto`), or a custom name as itself, so the value of a flag with no custom
|
|
42
|
+
* formats goes straight to {@link CliFormat.print}.
|
|
43
|
+
*
|
|
44
|
+
* Two offered names that clash (an alias or custom name reusing a built-in name, or any name offered twice) and a
|
|
45
|
+
* default that is not offered are programming mistakes: the call throws. A CLI that still accepts a `--json` of its
|
|
46
|
+
* own declares and resolves that flag itself; {@link CliFormat.printJson} prints the JSON the same way.
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* // okfit's contract: --format human|json, and a graph command that also draws mermaid and dot
|
|
50
|
+
* const format = CliFormat.flag(["json"], { aliases: { human: "auto" }, default: "human" })
|
|
51
|
+
* const graph = CliFormat.flag(["json"], { custom: ["mermaid", "dot"], default: "mermaid" })
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* @param formats - the built-in formats the command offers under their own names, at least one
|
|
55
|
+
* @param options - the default, the flag's name and description, aliases and custom formats
|
|
56
|
+
*/
|
|
57
|
+
static flag = (formats, options) => {
|
|
58
|
+
const name = options?.name ?? "format";
|
|
59
|
+
const choices = [
|
|
60
|
+
...formats.map((format) => [format, format]),
|
|
61
|
+
...Object.entries(options?.aliases ?? {}).map(([alias, target]) => [alias, target]),
|
|
62
|
+
...(options?.custom ?? []).map((custom) => [custom, custom])
|
|
63
|
+
];
|
|
64
|
+
const offered = choices.map(([spelling]) => spelling);
|
|
65
|
+
for (const [index, spelling] of offered.entries()) {
|
|
66
|
+
if (offered.indexOf(spelling) !== index) throw new Error(`CliFormat.flag: the format name "${spelling}" is offered twice`);
|
|
67
|
+
if (index >= formats.length && BUILT_IN.has(spelling)) throw new Error(`CliFormat.flag: "${spelling}" is a built-in format, not an alias or a custom one`);
|
|
68
|
+
}
|
|
69
|
+
const fallback = options?.default ?? (offered.includes("auto") ? "auto" : formats[0]);
|
|
70
|
+
const fallbackChoice = choices.find(([spelling]) => spelling === fallback);
|
|
71
|
+
if (fallbackChoice === void 0) throw new Error(`CliFormat.flag: the default "${fallback}" is not one of ${offered.join(", ")}`);
|
|
72
|
+
const flag = Flag.ChoiceWithValue(name, choices).pipe(Flag.withDescription(options?.description ?? `Output format: ${offered.join(", ")} (default ${fallback})`));
|
|
73
|
+
return Flag.withDefault(flag, fallbackChoice[1]);
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Print a command's output in `format`.
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* `json` writes `output.json()` exactly as {@link CliFormat.printJson} writes a value, which it delegates to. Every
|
|
80
|
+
* other format prints `output.doc()` with `Doc.print`, which renders it as `auto` or the named renderer would.
|
|
81
|
+
*
|
|
82
|
+
* @param format - the chosen format, usually the value of {@link CliFormat.flag}
|
|
83
|
+
* @param output - the document and the data
|
|
84
|
+
* @param options - the stream and the JSON indent
|
|
85
|
+
*/
|
|
86
|
+
static print = (format, output, options) => Effect.gen(function* () {
|
|
87
|
+
const stream = options?.stream ?? "stdout";
|
|
88
|
+
if (format !== "json") return yield* Doc.print(output.doc(), {
|
|
89
|
+
stream,
|
|
90
|
+
format
|
|
91
|
+
});
|
|
92
|
+
const data = output.json;
|
|
93
|
+
if (data === void 0) return yield* Effect.die(/* @__PURE__ */ new Error("CliFormat.print: format json needs output.json"));
|
|
94
|
+
return yield* CliFormat.printJson(data(), options);
|
|
95
|
+
});
|
|
96
|
+
/**
|
|
97
|
+
* Print `value` as JSON, the way {@link CliFormat.print} prints the `json` format, for a CLI that keeps a JSON flag
|
|
98
|
+
* of its own.
|
|
99
|
+
*
|
|
100
|
+
* @remarks
|
|
101
|
+
* One `Console.log` (or `Console.error` for stderr), indented by `indent`: one document, never one line per item.
|
|
102
|
+
* Under GitHub Actions a `##[` inside it is written as the JSON escape `#\u0023[`, which decodes to the same
|
|
103
|
+
* text, so the runner cannot read it as a command. A value JSON cannot hold (a cycle, a bigint, `undefined`) is a
|
|
104
|
+
* defect. It needs no theme, terminal or audience: whether this is GitHub Actions is read from `CurrentRuntimeEnv`
|
|
105
|
+
* when the environment has it, and is "no" otherwise.
|
|
106
|
+
*
|
|
107
|
+
* ```ts
|
|
108
|
+
* const status = Command.make("status", { json: Flag.Boolean("json").pipe(Flag.withDefault(false)) }, ({ json }) =>
|
|
109
|
+
* json ? CliFormat.printJson(results) : Doc.print(report(results)),
|
|
110
|
+
* )
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
113
|
+
* @param value - the data to print
|
|
114
|
+
* @param options - the stream and the JSON indent
|
|
115
|
+
*/
|
|
116
|
+
static printJson = (value, options) => Effect.gen(function* () {
|
|
117
|
+
const text = yield* Effect.sync(() => JSON.stringify(value, null, options?.indent ?? 2));
|
|
118
|
+
if (text === void 0) return yield* Effect.die(/* @__PURE__ */ new Error("CliFormat.printJson: the value is not JSON"));
|
|
119
|
+
const safe = (yield* underGithubActions) ? neutralizeJson(text) : text;
|
|
120
|
+
yield* (options?.stream ?? "stdout") === "stdout" ? Console.log(safe) : Console.error(safe);
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export { CliFormat };
|
package/CliInteractive.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { canPrompt } from "./internal/canPrompt.js";
|
|
2
2
|
import { Context, Effect, Layer } from "effect";
|
|
3
3
|
import { Audience, TerminalEnv } from "@effected/env";
|
|
4
|
+
import { TerminalCapabilities } from "effect/cli";
|
|
4
5
|
|
|
5
6
|
// src/CliInteractive.ts
|
|
6
7
|
/**
|
|
@@ -21,6 +22,19 @@ import { Audience, TerminalEnv } from "@effected/env";
|
|
|
21
22
|
* {@link CliInteractive.layerTest} are typed `Layer<never>`: they set the
|
|
22
23
|
* reference rather than provide a service.
|
|
23
24
|
*
|
|
25
|
+
* Both layers also hold core's `TerminalCapabilities.canPrompt` at `true`,
|
|
26
|
+
* keeping its `canColor` and `canAnimate` as they find them. Core skips a
|
|
27
|
+
* `Flag.withFallbackPrompt` or `Argument.withFallbackPrompt` fallback entirely
|
|
28
|
+
* when `canPrompt` is `false`, and decides that from the process's own
|
|
29
|
+
* terminal, so a kit fallback (`CliPrompt.fallback`, `CliUi.fallback`) would
|
|
30
|
+
* never be called in a piped run and its `otherwise` would be lost. With the
|
|
31
|
+
* gate held open, this reference makes the prompt decision instead: the kit's
|
|
32
|
+
* fallbacks answer without a person when it is `false`, and
|
|
33
|
+
* `CliPrompt.gateTerminal` quits any other prompt at once. A
|
|
34
|
+
* `TerminalCapabilities` with `canPrompt: false` provided inside the program,
|
|
35
|
+
* under these layers, bypasses the kit's fallbacks again: the parameter fails
|
|
36
|
+
* as missing even when an `otherwise` is given.
|
|
37
|
+
*
|
|
24
38
|
* @public
|
|
25
39
|
*/
|
|
26
40
|
var CliInteractive = class CliInteractive extends Context.Reference("@effected/cli/CliInteractive", { defaultValue: () => false }) {
|
|
@@ -34,19 +48,21 @@ var CliInteractive = class CliInteractive extends Context.Reference("@effected/c
|
|
|
34
48
|
* as `@effected/env` reads the environment, so it adds no requirement; a test fixes it with
|
|
35
49
|
* `Effect.provideService(ConfigProvider.ConfigProvider, ConfigProvider.fromUnknown({ TERM: "dumb" }))`.
|
|
36
50
|
*
|
|
51
|
+
* It also holds core's `TerminalCapabilities.canPrompt` at `true`; see {@link CliInteractive}.
|
|
52
|
+
*
|
|
37
53
|
* Bind the layer to a constant and provide it once; `Audience` and `TerminalEnv` come from `@effected/env`.
|
|
38
54
|
*/
|
|
39
|
-
static layer = Layer.
|
|
55
|
+
static layer = Layer.effectContext(Effect.gen(function* () {
|
|
40
56
|
const audience = yield* Audience;
|
|
41
57
|
const terminal = yield* TerminalEnv;
|
|
42
|
-
return audience.kind === "human" && (yield* canPrompt(terminal));
|
|
58
|
+
return yield* decided(audience.kind === "human" && (yield* canPrompt(terminal)));
|
|
43
59
|
}));
|
|
44
60
|
/**
|
|
45
|
-
* A fixed answer that needs nothing.
|
|
61
|
+
* A fixed answer that needs nothing. It holds core's `canPrompt` open too, as {@link CliInteractive.layer} does.
|
|
46
62
|
*
|
|
47
63
|
* @param value - whether the run is interactive
|
|
48
64
|
*/
|
|
49
|
-
static layerTest = (value) => Layer.
|
|
65
|
+
static layerTest = (value) => Layer.effectContext(decided(value));
|
|
50
66
|
/**
|
|
51
67
|
* Run `self` with interactivity switched off when `condition` holds.
|
|
52
68
|
*
|
|
@@ -66,5 +82,13 @@ var CliInteractive = class CliInteractive extends Context.Reference("@effected/c
|
|
|
66
82
|
return yield* Effect.provideService(self, CliInteractive, current && !condition);
|
|
67
83
|
});
|
|
68
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* The context the two layers set: the decision, and core's capabilities as they are found with `canPrompt` held at
|
|
87
|
+
* `true`, so core always hands a missing parameter to its fallback and the kit's decision governs.
|
|
88
|
+
*/
|
|
89
|
+
const decided = (interactive) => Effect.map(TerminalCapabilities.TerminalCapabilities, (capabilities) => Context.make(CliInteractive, interactive).pipe(Context.add(TerminalCapabilities.TerminalCapabilities, {
|
|
90
|
+
...capabilities,
|
|
91
|
+
canPrompt: true
|
|
92
|
+
})));
|
|
69
93
|
|
|
70
94
|
export { CliInteractive };
|
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/CliPrompt.js
CHANGED
|
@@ -36,6 +36,13 @@ var CliPrompt = class {
|
|
|
36
36
|
* input (core acquires it only when a prompt needs a key), so piped stdin is left untouched without any gate.
|
|
37
37
|
* `CliPrompt.gateTerminal`, which `CliEnv.layer` installs, covers the prompts this function does not wrap.
|
|
38
38
|
*
|
|
39
|
+
* Core calls a fallback only when its `TerminalCapabilities.canPrompt` is `true`, and its own default decides that
|
|
40
|
+
* from the process's terminal, so in a piped run it would fail the parameter as missing without calling this
|
|
41
|
+
* function and `otherwise` would be lost. `CliInteractive.layer` and `CliInteractive.layerTest` (and so
|
|
42
|
+
* `CliEnv.layer`) hold `canPrompt` at `true` for that reason: the decision is `CliInteractive`'s, made here. A
|
|
43
|
+
* `TerminalCapabilities` with `canPrompt: false` provided inside the program bypasses this fallback again, and a
|
|
44
|
+
* program that sets `CliInteractive` with neither layer keeps core's own gate.
|
|
45
|
+
*
|
|
39
46
|
* @param prompt - the prompt to show
|
|
40
47
|
* @param options - the parameter it stands in for, and the non-interactive default
|
|
41
48
|
*/
|
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,34 @@ 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
|
+
* A handler failure carrying a `CliError.userMessage` that `Command.runWith` already printed: it renders any such
|
|
26
|
+
* failure and sets the mark to `false`, keeping the error's own tag.
|
|
27
|
+
*/
|
|
28
|
+
const isRenderedUserMessage = (u) => typeof u === "object" && u !== null && CliError.getUserMessage(u) !== void 0 && Runtime.getErrorReported(u) === false;
|
|
29
|
+
/**
|
|
30
|
+
* The program with `CurrentDistribution` set (engine's own `withDistribution`), and `--version` decorated with the
|
|
31
|
+
* carrier's suffix (`withDistributionVersion`), wrapping whatever `formatVersion` is in effect rather than replacing a
|
|
32
|
+
* coloured or custom one.
|
|
33
|
+
*/
|
|
34
|
+
const withCarrier = (program, distribution) => {
|
|
35
|
+
const provided = program.pipe(withDistribution(distribution));
|
|
36
|
+
return distribution === null ? provided : Effect.updateService(provided, CliOutput.Formatter, (formatter) => ({
|
|
37
|
+
...formatter,
|
|
38
|
+
formatVersion: withDistributionVersion(Option.some(distribution))(formatter.formatVersion)
|
|
39
|
+
}));
|
|
31
40
|
};
|
|
41
|
+
/** The last line of defence of a failure report: the error's text, sanitised and neutralized, whatever else broke. */
|
|
42
|
+
const lastResort = (error) => CommandNeutralizer.lines(sanitize(failureText(error, "[unprintable failure]")));
|
|
32
43
|
const toLines = (rendered) => typeof rendered === "string" ? [rendered] : rendered;
|
|
33
44
|
/**
|
|
34
45
|
* The error's own exit code when it carries one, otherwise the fallback.
|
|
@@ -39,7 +50,17 @@ const toLines = (rendered) => typeof rendered === "string" ? [rendered] : render
|
|
|
39
50
|
* silently override a deliberate `1`. Testing for the marker keeps "the error
|
|
40
51
|
* chose its code" distinct from "nothing chose".
|
|
41
52
|
*/
|
|
42
|
-
const chooseExitCode = (error, fallback) =>
|
|
53
|
+
const chooseExitCode = (error, fallback, byTag) => {
|
|
54
|
+
if (typeof error === "object" && error !== null && Runtime.errorExitCode in error) return Runtime.getErrorExitCode(error);
|
|
55
|
+
if (byTag === void 0) return fallback ?? 1;
|
|
56
|
+
const tag = tagOf(error);
|
|
57
|
+
return tag !== void 0 && Object.hasOwn(byTag, tag) ? byTag[tag] : fallback ?? 1;
|
|
58
|
+
};
|
|
59
|
+
/** Every `exitCodes` entry an integer in `1..255`, or a `RangeError` naming the first that is not. */
|
|
60
|
+
const checkExitCodes = (exitCodes) => {
|
|
61
|
+
if (exitCodes === void 0) return;
|
|
62
|
+
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)}`);
|
|
63
|
+
};
|
|
43
64
|
/**
|
|
44
65
|
* Report a CLI program's failures through the program's own logger.
|
|
45
66
|
*
|
|
@@ -97,7 +118,10 @@ const chooseExitCode = (error, fallback) => typeof error === "object" && error !
|
|
|
97
118
|
* already rendered through its `CliOutput` formatter. It exits with its own
|
|
98
119
|
* `Runtime.errorExitCode` when it carries one, otherwise `usageExitCode`;
|
|
99
120
|
* under runWith's `renderErrors: false` the mark stays set, and it renders
|
|
100
|
-
* here like any other failure.
|
|
121
|
+
* here like any other failure. A handler failure carrying a
|
|
122
|
+
* `CliError.userMessage` is skipped the same way, since `Command.runWith`
|
|
123
|
+
* renders it and sets the mark to `false`; it keeps the exit code a
|
|
124
|
+
* handler failure gets (`exitCodes` by tag, then `exitCode`). Every other
|
|
101
125
|
* error renders even when it already carries the reported mark — a gate
|
|
102
126
|
* failure marked with `CliRuntime.reported` still prints its line. The
|
|
103
127
|
* private `ExitRequested` sentinel `CliRuntime.main`
|
|
@@ -148,43 +172,50 @@ var CliRuntime = class CliRuntime {
|
|
|
148
172
|
*
|
|
149
173
|
* @param options - how to render the failure and which exit codes to use
|
|
150
174
|
*/
|
|
151
|
-
static reportFailures = (options = {}) =>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
175
|
+
static reportFailures = (options = {}) => {
|
|
176
|
+
checkExitCodes(options.exitCodes);
|
|
177
|
+
return (effect) => effect.pipe(Effect.catchCause((cause) => {
|
|
178
|
+
if (Cause.hasInterruptsOnly(cause)) return Effect.failCause(cause);
|
|
179
|
+
const error = Cause.squash(cause);
|
|
180
|
+
if (error instanceof ExitRequested) return Effect.fail(error);
|
|
181
|
+
if (isShowHelp(error)) {
|
|
182
|
+
const code = error.errors.length > 0 ? options.usageExitCode ?? 64 : 0;
|
|
183
|
+
return Effect.fail(CliRuntime.reported(error, code));
|
|
184
|
+
}
|
|
185
|
+
if (isRenderedUserError(error)) return Effect.fail(CliRuntime.reported(error, chooseExitCode(error, options.usageExitCode ?? 64)));
|
|
186
|
+
if (isRenderedUserMessage(error)) return Effect.fail(CliRuntime.reported(error, chooseExitCode(error, options.exitCode, options.exitCodes)));
|
|
187
|
+
const render = options.render;
|
|
188
|
+
return Effect.gen(function* () {
|
|
189
|
+
const target = yield* currentTarget.pipe(Effect.catchCause(() => Effect.succeed(fallbackTarget)));
|
|
190
|
+
const reportLines = (status, spans) => {
|
|
167
191
|
try {
|
|
168
|
-
return
|
|
192
|
+
return linesOf(cause, target, status, spans ?? target.spans);
|
|
169
193
|
} catch {
|
|
170
|
-
|
|
194
|
+
try {
|
|
195
|
+
return plainFailureLines(cause, status, spans ?? target.spans);
|
|
196
|
+
} catch {
|
|
197
|
+
return lastResort(error);
|
|
198
|
+
}
|
|
171
199
|
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
200
|
+
};
|
|
201
|
+
const defaultLines = reportLines(true);
|
|
202
|
+
const isDefect = !Cause.hasFails(cause);
|
|
203
|
+
const exitCode = chooseExitCode(error, options.exitCode, isDefect ? void 0 : options.exitCodes);
|
|
204
|
+
const details = {
|
|
205
|
+
cause,
|
|
206
|
+
isDefect,
|
|
207
|
+
exitCode,
|
|
208
|
+
isCancelled: error instanceof Cancelled,
|
|
209
|
+
isNotInteractive: error instanceof NotInteractive,
|
|
210
|
+
defaultLines,
|
|
211
|
+
lines: (options) => options?.status === false || options?.spans !== void 0 ? reportLines(options?.status !== false, options?.spans) : defaultLines
|
|
212
|
+
};
|
|
213
|
+
const lines = render === void 0 ? defaultLines : yield* guardConsumerLines(toLines(render(error, details)));
|
|
214
|
+
for (const line of lines) yield* Effect.logError(line).pipe(Effect.provideService(TrustedLine, true));
|
|
215
|
+
return yield* Effect.fail(CliRuntime.reported(error, exitCode));
|
|
216
|
+
});
|
|
217
|
+
}));
|
|
218
|
+
};
|
|
188
219
|
static main(program, options) {
|
|
189
220
|
const env = options.env === void 0 ? void 0 : CliEnv.layer(options.env);
|
|
190
221
|
const envLog = options.env?.log;
|
|
@@ -193,14 +224,14 @@ var CliRuntime = class CliRuntime {
|
|
|
193
224
|
const { spans, invalid } = yield* readSpans(options.env?.spans, options.env?.spansEnvVar);
|
|
194
225
|
if (invalid !== void 0) yield* Effect.logWarning(invalid).pipe(Effect.provideService(Logger.CurrentLoggers, /* @__PURE__ */ new Set([makeCliLogger(envLog?.logger)])));
|
|
195
226
|
yield* refreshFailureTarget(void 0, {
|
|
196
|
-
displayPath: options.env?.displayPath,
|
|
197
227
|
stackFrames: options.env?.stackFrames,
|
|
198
228
|
spans,
|
|
199
229
|
appModule: options.env?.appModule
|
|
200
230
|
});
|
|
201
231
|
}))).pipe(Layer.provideMerge(env));
|
|
232
|
+
const carried = options.distribution === void 0 ? program : withCarrier(program, options.distribution);
|
|
202
233
|
const run = Effect.gen(function* () {
|
|
203
|
-
yield* options.helpOnUsageError === "stderr" ? routeHelpOnUsageError(
|
|
234
|
+
yield* options.helpOnUsageError === "stderr" ? routeHelpOnUsageError(carried) : carried;
|
|
204
235
|
const exit = yield* CliExit;
|
|
205
236
|
const code = MutableRef.get(exit.code);
|
|
206
237
|
if (!isExitCode(code)) return yield* Effect.die(/* @__PURE__ */ new Error(`CliRuntime.main: CliExit code must be an integer 0..255, received ${code}`));
|