@effected/cli 0.25.0 → 0.27.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/README.md +5 -208
- package/bin/effected.js +7 -0
- package/commands/root.js +14 -0
- package/commands/schema/LiveProgress.js +114 -0
- package/commands/schema/build.js +9 -0
- package/commands/schema/check.js +12 -0
- package/commands/schema/errors.js +121 -0
- package/commands/schema/execute.js +141 -0
- package/commands/schema/flags.js +24 -0
- package/commands/schema/index.js +14 -0
- package/commands/schema/validate.js +58 -0
- package/index.d.ts +85 -3906
- package/index.js +4 -26
- package/main.d.ts +51 -0
- package/main.js +61 -0
- package/package.json +18 -50
- package/render/DiffDoc.js +51 -0
- package/tsdoc-metadata.json +1 -1
- package/version.js +11 -0
- package/Cancelled.js +0 -43
- package/CliAudience.js +0 -177
- package/CliColor.js +0 -59
- package/CliEnv.js +0 -101
- package/CliExit.js +0 -64
- package/CliFailure.js +0 -287
- package/CliFormat.js +0 -124
- package/CliInteractive.js +0 -94
- package/CliLinks.js +0 -153
- package/CliLog.js +0 -367
- package/CliLogger.js +0 -91
- package/CliMessage.js +0 -79
- package/CliPrompt.js +0 -110
- package/CliRuntime.js +0 -254
- package/CliTest.js +0 -206
- package/CliTheme.js +0 -142
- package/ConfigIssueRenderer.js +0 -63
- package/Doc.js +0 -548
- package/Fmt.js +0 -335
- package/GithubAnnotation.js +0 -39
- package/Glyphs.js +0 -82
- package/NotInteractive.js +0 -41
- package/Render.js +0 -280
- package/SchemaIssueRenderer.js +0 -47
- package/Status.js +0 -165
- package/TestTerminal.js +0 -101
- package/Token.js +0 -68
- package/internal/ExitRequested.js +0 -21
- package/internal/HelpRouting.js +0 -35
- package/internal/ansi.js +0 -229
- package/internal/autoFormat.js +0 -33
- package/internal/canPrompt.js +0 -14
- package/internal/counts.js +0 -83
- package/internal/diagnostics.js +0 -31
- package/internal/displayPath.js +0 -46
- package/internal/displayWidth.js +0 -42
- package/internal/failureTarget.js +0 -195
- package/internal/fallbackAnswer.js +0 -17
- package/internal/fileSink.js +0 -61
- package/internal/format.js +0 -175
- package/internal/inspect.js +0 -40
- package/internal/isExitCode.js +0 -11
- package/internal/layout.js +0 -249
- package/internal/linkScheme.js +0 -29
- package/internal/linkTarget.js +0 -49
- package/internal/logSafety.js +0 -45
- package/internal/renderAnsi.js +0 -51
- package/internal/renderDoc.js +0 -322
- package/internal/renderGithubLog.js +0 -59
- package/internal/renderMarkdown.js +0 -367
- package/internal/renderPlain.js +0 -49
- package/internal/scanAudience.js +0 -105
- package/internal/splitFrame.js +0 -55
- package/internal/terminalCapabilities.js +0 -23
- package/internal/wizardGate.js +0 -17
- package/testing.d.ts +0 -246
- package/testing.js +0 -4
- package/ui/CliUi.js +0 -507
- package/ui/CliUiDetached.js +0 -36
- package/ui/CliUiLive.js +0 -436
- package/ui/CliUiRouteStream.js +0 -72
- package/ui/Confirm.js +0 -244
- package/ui/DocView.js +0 -76
- package/ui/KeyHelp.js +0 -61
- package/ui/KeyTable.js +0 -198
- package/ui/MultiSelect.js +0 -259
- package/ui/Select.js +0 -229
- package/ui/Tabs.js +0 -201
- package/ui/TextInput.js +0 -289
- package/ui/Toggle.js +0 -31
- package/ui/UiKey.js +0 -48
- package/ui/UiProvider.js +0 -62
- package/ui/UiStreams.js +0 -17
- package/ui/UiTheme.js +0 -147
- package/ui/Viewport.js +0 -203
- package/ui/internal/ErrorBoundary.js +0 -29
- package/ui/internal/Holder.js +0 -73
- package/ui/internal/ScreenContext.js +0 -51
- package/ui/internal/UiProviders.js +0 -38
- package/ui/internal/ink.js +0 -121
- package/ui/internal/inkChalk.js +0 -47
- package/ui/internal/inkConsole.js +0 -150
- package/ui/internal/lazyView.js +0 -73
- package/ui/internal/lineRouter.js +0 -70
- package/ui/internal/lineText.js +0 -18
- package/ui/internal/mountPermit.js +0 -15
- package/ui/internal/processStreams.js +0 -18
- package/ui/internal/renderOptions.js +0 -12
- package/ui/testing/CliUiTest.js +0 -792
- package/ui/testing/fakeStreams.js +0 -104
- package/ui/testing/terminalModel.js +0 -67
- package/ui-testing-serializer.d.ts +0 -12
- package/ui-testing-serializer.js +0 -32
- package/ui-testing.d.ts +0 -558
- package/ui-testing.js +0 -3
- package/ui.d.ts +0 -1990
- package/ui.js +0 -17
package/CliEnv.js
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { CliInteractive } from "./CliInteractive.js";
|
|
2
|
-
import { ambientLinksLayer } from "./CliLinks.js";
|
|
3
|
-
import { CliTheme } from "./CliTheme.js";
|
|
4
|
-
import { displayPathLayer } from "./internal/displayPath.js";
|
|
5
|
-
import { CliPrompt } from "./CliPrompt.js";
|
|
6
|
-
import { capabilitiesLayer } from "./internal/terminalCapabilities.js";
|
|
7
|
-
import { ConfigProvider, Layer, Option } from "effect";
|
|
8
|
-
import { Audience, CurrentRuntimeEnv, TerminalEnv } from "@effected/env";
|
|
9
|
-
|
|
10
|
-
// src/CliEnv.ts
|
|
11
|
-
/**
|
|
12
|
-
* The environment services a CLI reads, built once and in the right order.
|
|
13
|
-
*
|
|
14
|
-
* @remarks
|
|
15
|
-
* Builds `CurrentRuntimeEnv`, `TerminalEnv`, `Audience`, `CliTheme` and `CliLinks`, and sets `CliInteractive` from them, then
|
|
16
|
-
* installs the two gates for the program: `CliPrompt.gateTerminal`, which replaces `Terminal` with a quiet one when
|
|
17
|
-
* the run is not interactive so no prompt runner ever attaches to stdin, and `CliPrompt.gateWizard`, which drops
|
|
18
|
-
* `--wizard` then. `TerminalEnv` is built from the real terminal first. The layer therefore also outputs
|
|
19
|
-
* `Terminal`, the gated one, and consumers never compose the gates themselves. A
|
|
20
|
-
* `Context.Reference`'s key type is `never`, so the layer's output type does not list `CliInteractive`: it sets
|
|
21
|
-
* the reference rather than providing a service. Every read of the environment goes through `Config` and
|
|
22
|
-
* degrades to "unset" when it fails, so building the layer does not fail on a bad provider; it fails only when
|
|
23
|
-
* `Stdio` or `Terminal` do.
|
|
24
|
-
*
|
|
25
|
-
* `CliLinks` reads `FileSystem` and `Path` from the surrounding context if it has them, and does not require them: a
|
|
26
|
-
* `.vscode/` directory is looked for, and a relative path resolved, only when the platform is provided OUTSIDE this
|
|
27
|
-
* layer, as `CliRuntime.main` does. Without them `auto` is `vscode` on the terminal signal alone.
|
|
28
|
-
*
|
|
29
|
-
* Not interactive, the gated `Terminal`'s `readLine` fails as a quit, its input is already ended and its `display`
|
|
30
|
-
* writes nothing. A program that reads piped data must read `Stdio.stdin`, and one that writes output must use
|
|
31
|
-
* `Console` or `Stdio`, never `Terminal`. It also installs `CliTheme.promptTheme`, so core's prompts follow the
|
|
32
|
-
* theme.
|
|
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
|
-
*
|
|
43
|
-
* @public
|
|
44
|
-
*/
|
|
45
|
-
var CliEnv = class {
|
|
46
|
-
constructor() {}
|
|
47
|
-
/**
|
|
48
|
-
* The environment services for the terminal `Stdio` and `Terminal` describe.
|
|
49
|
-
*
|
|
50
|
-
* @remarks
|
|
51
|
-
* A layer-returning function mints a fresh layer per call: call it once and bind the result to a constant.
|
|
52
|
-
*
|
|
53
|
-
* @param options - the audience env var, the stderr check and the theme options
|
|
54
|
-
*/
|
|
55
|
-
static layer = (options = {}) => {
|
|
56
|
-
const base = Layer.mergeAll(CurrentRuntimeEnv.layer, TerminalEnv.layer(options.stderrIsTerminal === void 0 ? void 0 : { stderrIsTerminal: options.stderrIsTerminal }));
|
|
57
|
-
const withAudience = Audience.layer(options.audienceEnvVar === void 0 ? void 0 : { envVar: options.audienceEnvVar }).pipe(Layer.provideMerge(base));
|
|
58
|
-
const withTheme = CliTheme.layer(options.theme).pipe(Layer.provideMerge(withAudience));
|
|
59
|
-
const withLinks = ambientLinksLayer({
|
|
60
|
-
...options.editorLinks === void 0 ? {} : { editorLinks: options.editorLinks },
|
|
61
|
-
...options.editorLinksEnvVar === void 0 ? {} : { envVar: options.editorLinksEnvVar }
|
|
62
|
-
}).pipe(Layer.provideMerge(withTheme));
|
|
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));
|
|
66
|
-
};
|
|
67
|
-
/**
|
|
68
|
-
* The environment services a test fixes, needing nothing and reading nothing of the host's: `TerminalEnv` and
|
|
69
|
-
* `Audience` from the answers given, `CliTheme` built from them as {@link CliEnv.layer} builds it, and
|
|
70
|
-
* `CliInteractive` set from them by the same rule (a human, every stream a terminal, and a `TERM` that is not
|
|
71
|
-
* `dumb`), and core's `TerminalCapabilities` from them as `CliEnv.layer` sets it, `canPrompt` held at `true`.
|
|
72
|
-
*
|
|
73
|
-
* @remarks
|
|
74
|
-
* `term` is handed to the theme and interactivity builds alone, through a `ConfigProvider` of their own: the
|
|
75
|
-
* program under the layer keeps its own provider, and a host's `TERM` (a test runner in a dumb terminal) never
|
|
76
|
-
* decides. A screen or a live view also needs `UiStreams` from `@effected/cli/ui`, which `CliUiTest` provides; this
|
|
77
|
-
* layer provides no `Terminal` and installs neither of `CliEnv.layer`'s prompt gates.
|
|
78
|
-
*
|
|
79
|
-
* A layer-returning function mints a fresh layer per call: call it once and bind the result to a constant.
|
|
80
|
-
*
|
|
81
|
-
* @param options - whether the streams are terminals, the `TERM`, the audience, the width, the colour and the theme
|
|
82
|
-
*/
|
|
83
|
-
static layerTest = (options = {}) => {
|
|
84
|
-
const tty = options.tty ?? false;
|
|
85
|
-
const stream = {
|
|
86
|
-
isTerminal: tty,
|
|
87
|
-
color: options.color ?? "none",
|
|
88
|
-
columns: options.columns === void 0 ? Option.none() : Option.some(options.columns)
|
|
89
|
-
};
|
|
90
|
-
const facts = Layer.mergeAll(TerminalEnv.layerTest({
|
|
91
|
-
stdinIsTerminal: tty,
|
|
92
|
-
stdout: stream,
|
|
93
|
-
stderr: stream
|
|
94
|
-
}), Audience.layerTest(options.audience ?? "human"));
|
|
95
|
-
const term = ConfigProvider.layer(ConfigProvider.fromUnknown(options.term === void 0 ? {} : { TERM: options.term }));
|
|
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));
|
|
98
|
-
};
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
export { CliEnv };
|
package/CliExit.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { isExitCode } from "./internal/isExitCode.js";
|
|
2
|
-
import { Context, Effect, Layer, MutableRef } from "effect";
|
|
3
|
-
|
|
4
|
-
// src/CliExit.ts
|
|
5
|
-
/**
|
|
6
|
-
* The exit code a successful run wants, for commands whose findings are a
|
|
7
|
-
* result rather than a failure (a linter that found problems, say).
|
|
8
|
-
*
|
|
9
|
-
* @remarks
|
|
10
|
-
* Findings are not a failure — a JSON `tapError` must not fire, and the
|
|
11
|
-
* handler must return normally — yet the process must exit non-zero. Writing
|
|
12
|
-
* `process.exitCode` works only because Node's `runMain` skips
|
|
13
|
-
* `process.exit(0)` on success; `process.exit(n)` in a handler skips every
|
|
14
|
-
* finalizer. `CliRuntime.main` reads this cell after the program
|
|
15
|
-
* succeeds and turns a non-zero code into a marked failure the runtime's
|
|
16
|
-
* teardown honours, on any runtime, with finalizers intact.
|
|
17
|
-
*
|
|
18
|
-
* A `Service`, not a `Reference`: forgetting `CliRuntime.main` is a type error,
|
|
19
|
-
* never a silently ignored global.
|
|
20
|
-
*
|
|
21
|
-
* @public
|
|
22
|
-
*/
|
|
23
|
-
var CliExit = class CliExit extends Context.Service()("@effected/cli/CliExit") {
|
|
24
|
-
/**
|
|
25
|
-
* A fresh cell at `0`; `CliRuntime.main` provides it, and tests provide it
|
|
26
|
-
* directly.
|
|
27
|
-
*
|
|
28
|
-
* @remarks
|
|
29
|
-
* Every provide mints a new cell. Layers memoize by reference across
|
|
30
|
-
* `Effect.provide` calls, so without `Layer.fresh` a second provide of this
|
|
31
|
-
* layer anywhere in the program — a nested `CliRuntime.main`, a test
|
|
32
|
-
* helper — would silently share the first run's cell and inherit its code.
|
|
33
|
-
*
|
|
34
|
-
* A program run under `CliRuntime.main` must NOT provide `CliExit.layer`
|
|
35
|
-
* itself: `main` already provides one, and a second provide mints a second,
|
|
36
|
-
* unrelated cell that `main` never reads back, so `CliExit.set` calls made
|
|
37
|
-
* against it are silently discarded and the run exits `0`.
|
|
38
|
-
*/
|
|
39
|
-
static layer = Layer.fresh(Layer.sync(this, () => ({ code: MutableRef.make(0) })));
|
|
40
|
-
/**
|
|
41
|
-
* Record an exit code; the highest code set during the run wins.
|
|
42
|
-
*
|
|
43
|
-
* @remarks
|
|
44
|
-
* Highest-wins, not last-wins, so a later "clean" step cannot quietly
|
|
45
|
-
* downgrade an earlier finding's code.
|
|
46
|
-
*
|
|
47
|
-
* The code must be an integer in `0..255` — the range a POSIX exit status
|
|
48
|
-
* can carry. Anything else dies as a defect naming the value: `256` would
|
|
49
|
-
* wrap to exit `0` and silently pass a run with findings, and a fraction
|
|
50
|
-
* such as `1.5` makes `process.exit` throw `ERR_OUT_OF_RANGE` after the
|
|
51
|
-
* program has finished.
|
|
52
|
-
*
|
|
53
|
-
* The code only applies to a run that **succeeds**. A program failure beats
|
|
54
|
-
* findings: when the program fails, `CliRuntime.main` never reads this
|
|
55
|
-
* cell, and the failure's own exit code (or the `exitCode` fallback) wins.
|
|
56
|
-
*/
|
|
57
|
-
static set = (code) => Effect.gen(function* () {
|
|
58
|
-
if (!isExitCode(code)) return yield* Effect.die(/* @__PURE__ */ new Error(`CliExit.set: exit code must be an integer 0..255, received ${code}`));
|
|
59
|
-
const exit = yield* CliExit;
|
|
60
|
-
if (code > MutableRef.get(exit.code)) MutableRef.set(exit.code, code);
|
|
61
|
-
});
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export { CliExit };
|
package/CliFailure.js
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
import { Cancelled } from "./Cancelled.js";
|
|
2
|
-
import { Fmt } from "./Fmt.js";
|
|
3
|
-
import { identity } from "./internal/displayPath.js";
|
|
4
|
-
import { Doc } from "./Doc.js";
|
|
5
|
-
import { issueEntries, issueTreeChildren } from "./internal/format.js";
|
|
6
|
-
import { failureText, readProperty, tagOf } from "./internal/inspect.js";
|
|
7
|
-
import { splitFrame } from "./internal/splitFrame.js";
|
|
8
|
-
import { NotInteractive } from "./NotInteractive.js";
|
|
9
|
-
import { Status } from "./Status.js";
|
|
10
|
-
import { Cause, Context, SchemaIssue } from "effect";
|
|
11
|
-
|
|
12
|
-
// src/CliFailure.ts
|
|
13
|
-
/**
|
|
14
|
-
* The protocol an error class implements to say how a failure is shown: a method under this key that returns the
|
|
15
|
-
* document.
|
|
16
|
-
*
|
|
17
|
-
* @remarks
|
|
18
|
-
* `CliFailure.toDoc` calls it for a typed failure that has one, so an application error draws itself (a heading, a
|
|
19
|
-
* table of what went wrong) and the default report uses it with no registration. A `Symbol.for` key, so two copies
|
|
20
|
-
* of this package agree on it.
|
|
21
|
-
*
|
|
22
|
-
* @public
|
|
23
|
-
*/
|
|
24
|
-
const CliDoc = Symbol.for("@effected/cli/CliDoc");
|
|
25
|
-
/** The deepest an `Error.cause` chain, or a stack, is followed. */
|
|
26
|
-
const MAX_DEPTH = 8;
|
|
27
|
-
const MAX_SPANS = 32;
|
|
28
|
-
const hasCliDoc = (value) => typeof value === "object" && value !== null && typeof value[CliDoc] === "function";
|
|
29
|
-
const firstLine = (text) => text.split(/\r\n|\r|\n/, 1)[0] ?? "";
|
|
30
|
-
/** The failure status and the text of a message: one block per line, the status on the first. */
|
|
31
|
-
const failureBlocks = (message) => {
|
|
32
|
-
const [first = "", ...rest] = message.split(/\r\n|\r|\n/);
|
|
33
|
-
return [Doc.paragraph(Doc.status(Status.core, "failure"), " ", first), ...rest.map((line) => Doc.paragraph(line))];
|
|
34
|
-
};
|
|
35
|
-
/** The issue of a schema failure: the value itself, or the `issue` an error carries. */
|
|
36
|
-
const issueOf = (error) => {
|
|
37
|
-
if (SchemaIssue.isIssue(error)) return error;
|
|
38
|
-
const issue = readProperty(error, "issue");
|
|
39
|
-
return SchemaIssue.isIssue(issue) ? issue : void 0;
|
|
40
|
-
};
|
|
41
|
-
const schemaBlocks = (error) => {
|
|
42
|
-
const entries = issueEntries(issueOf(error));
|
|
43
|
-
if (entries.length === 0) return void 0;
|
|
44
|
-
const header = typeof error === "object" && error !== null && !SchemaIssue.isIssue(error) ? firstLine(failureText(readProperty(error, "message") ?? "")) : "";
|
|
45
|
-
const root = {
|
|
46
|
-
label: [
|
|
47
|
-
Doc.status(Status.core, "failure"),
|
|
48
|
-
" ",
|
|
49
|
-
header === "" ? `invalid value (${Fmt.plural(entries.length, "problem")})` : header
|
|
50
|
-
],
|
|
51
|
-
children: issueTreeChildren(entries)
|
|
52
|
-
};
|
|
53
|
-
return [Doc.tree(root)];
|
|
54
|
-
};
|
|
55
|
-
/** The location of a frame as a path: a `file:` URL decoded, or an absolute POSIX or drive path; else `undefined`. */
|
|
56
|
-
const asPath = (location) => {
|
|
57
|
-
if (location.startsWith("file:")) try {
|
|
58
|
-
const path = decodeURIComponent(new URL(location).pathname);
|
|
59
|
-
return /^\/[A-Za-z]:\//.test(path) ? path.slice(1) : path;
|
|
60
|
-
} catch {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
return location.startsWith("/") || /^[A-Za-z]:[\\/]/.test(location) ? location : void 0;
|
|
64
|
-
};
|
|
65
|
-
/** A location without its `:line:col`, and the position when there is one. */
|
|
66
|
-
const splitPosition = (location) => {
|
|
67
|
-
const position = /^(.*):(\d+):(\d+)$/.exec(location);
|
|
68
|
-
return position === null ? { where: location } : {
|
|
69
|
-
where: position[1] ?? "",
|
|
70
|
-
line: Number(position[2]),
|
|
71
|
-
col: Number(position[3])
|
|
72
|
-
};
|
|
73
|
-
};
|
|
74
|
-
const parseFrame = (raw) => {
|
|
75
|
-
const { text, fn, location } = splitFrame(raw);
|
|
76
|
-
const { where, line, col } = splitPosition(location);
|
|
77
|
-
const file = asPath(where);
|
|
78
|
-
return {
|
|
79
|
-
raw: text,
|
|
80
|
-
...fn === void 0 ? {} : { fn },
|
|
81
|
-
...file === void 0 ? {} : { file },
|
|
82
|
-
...file === void 0 || line === void 0 || col === void 0 ? {} : {
|
|
83
|
-
line,
|
|
84
|
-
col
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
};
|
|
88
|
-
/** The file a frame ran in, as a path, or `undefined` for a frame with none: `node:`, `<anonymous>`, `native`, `eval`. */
|
|
89
|
-
const frameFile = (raw) => asPath(splitPosition(splitFrame(raw).location).where);
|
|
90
|
-
/** A frame under `node_modules`: a dependency's, or an installed program's own. Decided by its file alone. */
|
|
91
|
-
const isDependency = (raw) => /[\\/]node_modules[\\/]/.test(frameFile(raw) ?? "");
|
|
92
|
-
/**
|
|
93
|
-
* A frame that is the runtime's or Effect's, never the program's, wherever the program is installed: one with no file
|
|
94
|
-
* (every `node:` frame, `<anonymous>`, native), or one in Effect's own files. Decided by the file alone, never by the
|
|
95
|
-
* function name: V8 names a program's own thunk by Effect's method alias (`boom [as ~effect/Effect/args]`).
|
|
96
|
-
*/
|
|
97
|
-
const isRuntime = (raw) => {
|
|
98
|
-
const file = frameFile(raw);
|
|
99
|
-
return file === void 0 || /[\\/]node_modules[\\/]effect[\\/]/.test(file) || /[\\/]packages[\\/]effect[\\/]src[\\/]/.test(file);
|
|
100
|
-
};
|
|
101
|
-
/** The frames of a stack that belong to the program, and how many were left out. */
|
|
102
|
-
const cleanStack = (stack, mode) => {
|
|
103
|
-
if (typeof stack !== "string") return {
|
|
104
|
-
frames: [],
|
|
105
|
-
hidden: 0
|
|
106
|
-
};
|
|
107
|
-
const lines = stack.split(/\r\n|\r|\n/).filter((line) => /^\s*at\s/.test(line));
|
|
108
|
-
const app = lines.filter((line) => !isRuntime(line) && !isDependency(line));
|
|
109
|
-
const kept = mode === "all" ? lines : app.length > 0 ? app : lines.filter((line) => !isRuntime(line));
|
|
110
|
-
return {
|
|
111
|
-
frames: kept.map(parseFrame),
|
|
112
|
-
hidden: lines.length - kept.length
|
|
113
|
-
};
|
|
114
|
-
};
|
|
115
|
-
const frameBlock = (frame, displayPath) => {
|
|
116
|
-
if (frame.file === void 0) return Doc.paragraph(Doc.text("at ", "muted"), frame.raw);
|
|
117
|
-
const where = `${displayPath(frame.file)}${frame.line === void 0 ? "" : `:${frame.line}:${frame.col}`}`;
|
|
118
|
-
const target = {
|
|
119
|
-
file: frame.file,
|
|
120
|
-
...frame.line === void 0 ? {} : { line: frame.line },
|
|
121
|
-
...frame.col === void 0 ? {} : { col: frame.col }
|
|
122
|
-
};
|
|
123
|
-
return Doc.paragraph(Doc.text("at ", "muted"), ...frame.fn === void 0 ? [] : [`${frame.fn} `], Doc.link(target, where));
|
|
124
|
-
};
|
|
125
|
-
const stackBlock = (defect, displayPath, mode) => {
|
|
126
|
-
const { frames, hidden } = cleanStack(readProperty(defect, "stack"), mode);
|
|
127
|
-
if (frames.length > 0) {
|
|
128
|
-
const shown = frames.map((frame) => frameBlock(frame, displayPath));
|
|
129
|
-
const note = hidden === 0 ? [] : [Doc.paragraph(Doc.text(`(+${hidden} internal frames hidden)`, "muted"))];
|
|
130
|
-
return Doc.collapsible("stack", [...shown, ...note], { open: true });
|
|
131
|
-
}
|
|
132
|
-
const note = hidden === 0 ? "no stack" : `no user frames (${hidden} internal frames hidden)`;
|
|
133
|
-
return Doc.collapsible("stack", [Doc.paragraph(Doc.text(note, "muted"))], { open: true });
|
|
134
|
-
};
|
|
135
|
-
/** The `Error.cause` chain below a defect as a tree of one-line messages, or none. */
|
|
136
|
-
const causeTree = (defect) => {
|
|
137
|
-
const chain = [];
|
|
138
|
-
const seen = /* @__PURE__ */ new Set([defect]);
|
|
139
|
-
for (let current = readProperty(defect, "cause"); current !== void 0 && chain.length < MAX_DEPTH;) {
|
|
140
|
-
if (seen.has(current)) break;
|
|
141
|
-
seen.add(current);
|
|
142
|
-
chain.push(firstLine(failureText(current)));
|
|
143
|
-
current = current instanceof Error ? readProperty(current, "cause") : void 0;
|
|
144
|
-
}
|
|
145
|
-
if (chain.length === 0) return void 0;
|
|
146
|
-
const nest = (index) => index === chain.length - 1 ? { label: chain[index] } : {
|
|
147
|
-
label: chain[index],
|
|
148
|
-
children: [nest(index + 1)]
|
|
149
|
-
};
|
|
150
|
-
return Doc.tree({
|
|
151
|
-
label: firstLine(failureText(defect)),
|
|
152
|
-
children: [nest(0)]
|
|
153
|
-
});
|
|
154
|
-
};
|
|
155
|
-
const dieBlocks = (defect, spans, displayPath, mode) => {
|
|
156
|
-
if (defect instanceof Cancelled || defect instanceof NotInteractive) return [Doc.paragraph(defect.message)];
|
|
157
|
-
const header = failureBlocks(failureText(defect));
|
|
158
|
-
if (!(defect instanceof Error)) return [...header, ...spans];
|
|
159
|
-
const chain = causeTree(defect);
|
|
160
|
-
return [
|
|
161
|
-
...header,
|
|
162
|
-
...spans,
|
|
163
|
-
stackBlock(defect, displayPath, mode),
|
|
164
|
-
...chain === void 0 ? [] : [chain]
|
|
165
|
-
];
|
|
166
|
-
};
|
|
167
|
-
const failBlocks = (error, spans, options) => {
|
|
168
|
-
if (hasCliDoc(error)) try {
|
|
169
|
-
return [...error[CliDoc](), ...spans];
|
|
170
|
-
} catch {}
|
|
171
|
-
const tag = tagOf(error);
|
|
172
|
-
const custom = tag === void 0 || options?.render === void 0 || !Object.hasOwn(options.render, tag) ? void 0 : options.render[tag];
|
|
173
|
-
if (custom !== void 0) try {
|
|
174
|
-
return [...custom(error), ...spans];
|
|
175
|
-
} catch {}
|
|
176
|
-
if (error instanceof Cancelled || error instanceof NotInteractive) return [Doc.paragraph(error.message)];
|
|
177
|
-
const schema = schemaBlocks(error);
|
|
178
|
-
if (schema !== void 0) return [...schema, ...spans];
|
|
179
|
-
return [...failureBlocks(failureText(error)), ...spans];
|
|
180
|
-
};
|
|
181
|
-
/** A file of the kit's own packages or of Effect, as installed: a span defined there is not the program's. */
|
|
182
|
-
const isKitFile = (file) => /[\\/]node_modules[\\/](?:@effected[\\/]|effect[\\/])/.test(file);
|
|
183
|
-
/**
|
|
184
|
-
* The installed package directory that holds `module`: the path through the last `node_modules/<name>` or
|
|
185
|
-
* `node_modules/@scope/name` in it, or `undefined` when the module is not under `node_modules` (or not a path at all).
|
|
186
|
-
* Read from the path alone, with no file system.
|
|
187
|
-
*/
|
|
188
|
-
const packageDirOf = (module) => {
|
|
189
|
-
if (module === void 0) return void 0;
|
|
190
|
-
const path = asPath(module);
|
|
191
|
-
if (path === void 0) return void 0;
|
|
192
|
-
return /^(.*\/node_modules\/(?:@[^/]+\/)?[^/]+)\//.exec(comparable(path))?.[1];
|
|
193
|
-
};
|
|
194
|
-
/**
|
|
195
|
-
* A path in the one spelling both sides of a comparison use: `/` separators, and a Windows drive letter lower-cased. A
|
|
196
|
-
* CommonJS frame on Windows reads `C:\…` where an `import.meta.url` reads `file:///C:/…`, and the drive's case is not
|
|
197
|
-
* fixed (`c:` and `C:` are one drive). Lexical only: no file system, no realpath.
|
|
198
|
-
*/
|
|
199
|
-
const comparable = (path) => path.replace(/\\/g, "/").replace(/^([A-Za-z]):/, (_, drive) => `${drive.toLowerCase()}:`);
|
|
200
|
-
/** Whether `file` is in the program's own package `appDir`, and not in a dependency nested in its `node_modules`. */
|
|
201
|
-
const isAppFile = (file, appDir) => {
|
|
202
|
-
if (appDir === void 0) return false;
|
|
203
|
-
const path = comparable(file);
|
|
204
|
-
if (!path.startsWith(appDir)) return false;
|
|
205
|
-
const rest = path.slice(appDir.length);
|
|
206
|
-
return rest.startsWith("/") && !rest.includes("/node_modules/");
|
|
207
|
-
};
|
|
208
|
-
/** The file a span frame's captured stack points at, or `undefined` when it captured none. */
|
|
209
|
-
const spanFile = (frame) => {
|
|
210
|
-
try {
|
|
211
|
-
const stack = frame.stack();
|
|
212
|
-
if (stack === void 0) return void 0;
|
|
213
|
-
const line = stack.split(/\r\n|\r|\n/).find((candidate) => /^\s*at\s/.test(candidate)) ?? stack;
|
|
214
|
-
return frameFile(line.trim().startsWith("at ") ? line : `at ${line.trim()}`);
|
|
215
|
-
} catch {
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
/**
|
|
220
|
-
* `in: outer › inner`, from the span stack the runtime annotates a reason with, or none. With `app`, a span the kit or
|
|
221
|
-
* Effect defined is left out. An `Effect.fn` call carries its definition as its parent (`name (definition)`): the two
|
|
222
|
-
* are one entry, `name`, judged by the definition's site, so a kit function the program calls goes with its
|
|
223
|
-
* definition.
|
|
224
|
-
*/
|
|
225
|
-
const spanBlocks = (reason, mode, appDir) => {
|
|
226
|
-
if (mode === "off") return [];
|
|
227
|
-
const names = [];
|
|
228
|
-
let frame = Context.getOrUndefined(Cause.reasonAnnotations(reason), Cause.StackTrace);
|
|
229
|
-
for (let seen = 0; frame !== void 0 && seen < MAX_SPANS; seen++) {
|
|
230
|
-
const parent = frame.parent;
|
|
231
|
-
const definition = parent !== void 0 && parent.name === `${frame.name} (definition)` ? parent : void 0;
|
|
232
|
-
const file = (definition === void 0 ? void 0 : spanFile(definition)) ?? spanFile(frame);
|
|
233
|
-
if (mode === "all" || file === void 0 || isAppFile(file, appDir) || !isKitFile(file)) names.push(frame.name);
|
|
234
|
-
frame = definition === void 0 ? parent : definition.parent;
|
|
235
|
-
}
|
|
236
|
-
if (names.length === 0) return [];
|
|
237
|
-
return [Doc.paragraph(Doc.text("in: ", "muted"), Doc.path(...names.reverse()))];
|
|
238
|
-
};
|
|
239
|
-
/**
|
|
240
|
-
* A failure as a document: what the default report prints, and a building block for a custom one.
|
|
241
|
-
*
|
|
242
|
-
* @remarks
|
|
243
|
-
* One run of blocks per `Cause` reason. A typed failure is, in order of preference: the document of an error that
|
|
244
|
-
* implements {@link CliDoc}; the document `options.render` holds for its `_tag`; for `Cancelled` and
|
|
245
|
-
* `NotInteractive`, their one fixed line; a `Tree` of the rejected values, for a schema error or issue; else a failure
|
|
246
|
-
* status line with its message. A defect is its message followed by a collapsible `stack` of the program's own frames,
|
|
247
|
-
* each a file link (so a terminal can open it in an editor) shown through `displayPath`, with the runtime's frames (every
|
|
248
|
-
* `node:` frame and every frame with no file) and every `node_modules` frame (Effect's and any other dependency's)
|
|
249
|
-
* left out unless `stackFrames` is `all`. A frame is classified by its file alone, never by its function name, so a
|
|
250
|
-
* program's own thunk that V8 names by Effect's method alias is still shown. When that would
|
|
251
|
-
* leave no frame at all, as for a program run from its own install under `node_modules`, only the runtime's and
|
|
252
|
-
* Effect's are left out. Then an
|
|
253
|
-
* `Error.cause` chain as a tree. When cleaning leaves no frame the stack says
|
|
254
|
-
* `no user frames (N internal frames hidden)`, never an empty block; when frames survive and some were left out, the
|
|
255
|
-
* count follows them as `(+N internal frames hidden)`. A reason that ran under spans is followed by
|
|
256
|
-
* `in: outer › inner`: by default only the program's own spans, the kit's and Effect's left out (`spans`). Interrupts are not rendered beside a real failure, and a cause with only interrupts is the
|
|
257
|
-
* one line `interrupted`.
|
|
258
|
-
*
|
|
259
|
-
* All text goes through the document, so a control character in a message or a stack frame never reaches the terminal.
|
|
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.
|
|
262
|
-
*
|
|
263
|
-
* @public
|
|
264
|
-
*/
|
|
265
|
-
var CliFailure = class {
|
|
266
|
-
constructor() {}
|
|
267
|
-
/**
|
|
268
|
-
* Build the document of a cause.
|
|
269
|
-
*
|
|
270
|
-
* @param cause - the failure
|
|
271
|
-
* @param options - per-tag documents and a path display function
|
|
272
|
-
*/
|
|
273
|
-
static toDoc = (cause, options) => {
|
|
274
|
-
const reasons = cause.reasons;
|
|
275
|
-
if (reasons.length > 0 && reasons.every(Cause.isInterruptReason)) return [Doc.paragraph("interrupted")];
|
|
276
|
-
const displayPath = options?.displayPath ?? identity;
|
|
277
|
-
return reasons.flatMap((reason) => {
|
|
278
|
-
const spans = options?.spans ?? "app";
|
|
279
|
-
const appDir = packageDirOf(options?.appModule);
|
|
280
|
-
if (Cause.isFailReason(reason)) return failBlocks(reason.error, spanBlocks(reason, spans, appDir), options);
|
|
281
|
-
if (Cause.isDieReason(reason)) return dieBlocks(reason.defect, spanBlocks(reason, spans, appDir), displayPath, options?.stackFrames ?? "app");
|
|
282
|
-
return [];
|
|
283
|
-
});
|
|
284
|
-
};
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
export { CliDoc, CliFailure };
|
package/CliFormat.js
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { canPrompt } from "./internal/canPrompt.js";
|
|
2
|
-
import { Context, Effect, Layer } from "effect";
|
|
3
|
-
import { Audience, TerminalEnv } from "@effected/env";
|
|
4
|
-
import { TerminalCapabilities } from "effect/cli";
|
|
5
|
-
|
|
6
|
-
// src/CliInteractive.ts
|
|
7
|
-
/**
|
|
8
|
-
* Whether this run may prompt a person: a human audience, with a terminal on
|
|
9
|
-
* both standard input and standard output, and a `TERM` that is not `dumb`.
|
|
10
|
-
*
|
|
11
|
-
* @remarks
|
|
12
|
-
* A `Context.Reference`, not a `Context.Service`, for three reasons. It is one
|
|
13
|
-
* boolean with a safe default, which is what a reference is for. A scoped
|
|
14
|
-
* override, {@link CliInteractive.unless}, is a plain
|
|
15
|
-
* `Effect.provideService`, so a command can switch prompting off for one
|
|
16
|
-
* subtree without a layer. And forgetting to provide it is not a type error
|
|
17
|
-
* but a harmless answer, since it reads `false` when no layer is provided: a
|
|
18
|
-
* program that never wired it refuses to prompt rather than hanging on a
|
|
19
|
-
* terminal that is not there. Read it with `yield* CliInteractive`.
|
|
20
|
-
*
|
|
21
|
-
* Because a reference's key type is `never`, {@link CliInteractive.layer} and
|
|
22
|
-
* {@link CliInteractive.layerTest} are typed `Layer<never>`: they set the
|
|
23
|
-
* reference rather than provide a service.
|
|
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
|
-
*
|
|
38
|
-
* @public
|
|
39
|
-
*/
|
|
40
|
-
var CliInteractive = class CliInteractive extends Context.Reference("@effected/cli/CliInteractive", { defaultValue: () => false }) {
|
|
41
|
-
/**
|
|
42
|
-
* Decide from the audience and the terminal: `true` only for a human audience with a terminal on both
|
|
43
|
-
* standard input and standard output, and a `TERM` that is not `dumb`.
|
|
44
|
-
*
|
|
45
|
-
* @remarks
|
|
46
|
-
* A dumb terminal is a terminal, but it cannot move the cursor or take synchronized output, which a prompt or a
|
|
47
|
-
* screen redrawing in place needs: it gets what a pipe gets. `TERM` is read through the ambient `ConfigProvider`,
|
|
48
|
-
* as `@effected/env` reads the environment, so it adds no requirement; a test fixes it with
|
|
49
|
-
* `Effect.provideService(ConfigProvider.ConfigProvider, ConfigProvider.fromUnknown({ TERM: "dumb" }))`.
|
|
50
|
-
*
|
|
51
|
-
* It also holds core's `TerminalCapabilities.canPrompt` at `true`; see {@link CliInteractive}.
|
|
52
|
-
*
|
|
53
|
-
* Bind the layer to a constant and provide it once; `Audience` and `TerminalEnv` come from `@effected/env`.
|
|
54
|
-
*/
|
|
55
|
-
static layer = Layer.effectContext(Effect.gen(function* () {
|
|
56
|
-
const audience = yield* Audience;
|
|
57
|
-
const terminal = yield* TerminalEnv;
|
|
58
|
-
return yield* decided(audience.kind === "human" && (yield* canPrompt(terminal)));
|
|
59
|
-
}));
|
|
60
|
-
/**
|
|
61
|
-
* A fixed answer that needs nothing. It holds core's `canPrompt` open too, as {@link CliInteractive.layer} does.
|
|
62
|
-
*
|
|
63
|
-
* @param value - whether the run is interactive
|
|
64
|
-
*/
|
|
65
|
-
static layerTest = (value) => Layer.effectContext(decided(value));
|
|
66
|
-
/**
|
|
67
|
-
* Run `self` with interactivity switched off when `condition` holds.
|
|
68
|
-
*
|
|
69
|
-
* @remarks
|
|
70
|
-
* It only narrows: `unless(false)` leaves the current value alone and never turns interactivity on, so a
|
|
71
|
-
* non-interactive scope stays non-interactive. The outer value is restored when `self` ends, whether it
|
|
72
|
-
* succeeds, fails or is interrupted.
|
|
73
|
-
*
|
|
74
|
-
* A flag that resolves the audience (`--human`, under `CliAudience.runWith` or `provide`) recomputes interactivity
|
|
75
|
-
* from the terminal facts, so it can override an outer `unless` or a `layerTest(false)`: those narrow the
|
|
76
|
-
* environment's answer, and the flag is a later, explicit one.
|
|
77
|
-
*
|
|
78
|
-
* @param condition - `true` to switch prompting off for `self`
|
|
79
|
-
*/
|
|
80
|
-
static unless = (condition) => (self) => Effect.gen(function* () {
|
|
81
|
-
const current = yield* CliInteractive;
|
|
82
|
-
return yield* Effect.provideService(self, CliInteractive, current && !condition);
|
|
83
|
-
});
|
|
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
|
-
})));
|
|
93
|
-
|
|
94
|
-
export { CliInteractive };
|