@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/CliMessage.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { underGithubActions } from "./internal/autoFormat.js";
|
|
2
|
-
import { sanitize } from "./Fmt.js";
|
|
3
|
-
import { CliTheme } from "./CliTheme.js";
|
|
4
|
-
import { Status } from "./Status.js";
|
|
5
|
-
import { Console, Effect } from "effect";
|
|
6
|
-
import { Audience } from "@effected/env";
|
|
7
|
-
import { CommandNeutralizer } from "@effected/github-commands";
|
|
8
|
-
|
|
9
|
-
// src/CliMessage.ts
|
|
10
|
-
/**
|
|
11
|
-
* One-line status messages: a glyph and some text, themed for a person and plain for an agent.
|
|
12
|
-
*
|
|
13
|
-
* @remarks
|
|
14
|
-
* Each line goes through `Console`, `log` for stdout and `error` for stderr, never through the logger, so no
|
|
15
|
-
* log level can silence it. Only the glyph is painted; the text stays plain. An `agent` audience gets the glyph
|
|
16
|
-
* and the text and never colour, even when the theme has colour. `success` and `info` go to stdout, `warning`
|
|
17
|
-
* and `failure` to stderr.
|
|
18
|
-
*
|
|
19
|
-
* The text is whatever the caller supplies, so it is sanitised: escape sequences and control characters are removed
|
|
20
|
-
* (a line break is kept as one, a tab becomes a space), as in a document. Under GitHub Actions, where
|
|
21
|
-
* `CurrentRuntimeEnv` says so, a line the runner would read as a workflow command is neutralized as well. The glyph
|
|
22
|
-
* comes from the vocabulary, sanitised too (`Status.glyph`), so a glyph built from data cannot inject an escape either.
|
|
23
|
-
*
|
|
24
|
-
* @public
|
|
25
|
-
*/
|
|
26
|
-
var CliMessage = class CliMessage {
|
|
27
|
-
constructor() {}
|
|
28
|
-
/**
|
|
29
|
-
* Print a status line from a vocabulary.
|
|
30
|
-
*
|
|
31
|
-
* @remarks
|
|
32
|
-
* The stream defaults to stderr when the status ranks at or above `warning` in `vocab`, and stdout
|
|
33
|
-
* otherwise, so a custom status follows its own rank: a `timeout` ranked 85 goes to stderr.
|
|
34
|
-
*
|
|
35
|
-
* @param vocab - the vocabulary the status belongs to
|
|
36
|
-
* @param name - the status
|
|
37
|
-
* @param text - the text after the glyph
|
|
38
|
-
* @param options - the stream override
|
|
39
|
-
*/
|
|
40
|
-
static status = (vocab, name, text, options) => Effect.gen(function* () {
|
|
41
|
-
const theme = yield* CliTheme;
|
|
42
|
-
const audience = yield* Audience;
|
|
43
|
-
const def = vocab.def(name);
|
|
44
|
-
const message = sanitize(text);
|
|
45
|
-
const warning = vocab.def("warning").rank;
|
|
46
|
-
const stream = options?.stream ?? (def.rank >= warning ? "stderr" : "stdout");
|
|
47
|
-
const streamTheme = theme.forStream(stream);
|
|
48
|
-
let line;
|
|
49
|
-
line = CliTheme.forAudience(streamTheme, audience.kind).status(vocab, name, message);
|
|
50
|
-
if (yield* underGithubActions) line = CommandNeutralizer.text(line);
|
|
51
|
-
yield* stream === "stderr" ? Console.error(line) : Console.log(line);
|
|
52
|
-
});
|
|
53
|
-
/**
|
|
54
|
-
* A success line, on stdout.
|
|
55
|
-
*
|
|
56
|
-
* @param text - the text after the glyph
|
|
57
|
-
*/
|
|
58
|
-
static success = (text) => CliMessage.status(Status.core, "success", text);
|
|
59
|
-
/**
|
|
60
|
-
* An informational line, on stdout.
|
|
61
|
-
*
|
|
62
|
-
* @param text - the text after the glyph
|
|
63
|
-
*/
|
|
64
|
-
static info = (text) => CliMessage.status(Status.core, "info", text);
|
|
65
|
-
/**
|
|
66
|
-
* A warning line, on stderr.
|
|
67
|
-
*
|
|
68
|
-
* @param text - the text after the glyph
|
|
69
|
-
*/
|
|
70
|
-
static warning = (text) => CliMessage.status(Status.core, "warning", text);
|
|
71
|
-
/**
|
|
72
|
-
* A failure line, on stderr.
|
|
73
|
-
*
|
|
74
|
-
* @param text - the text after the glyph
|
|
75
|
-
*/
|
|
76
|
-
static failure = (text) => CliMessage.status(Status.core, "failure", text);
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
export { CliMessage };
|
package/CliPrompt.js
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { Cancelled } from "./Cancelled.js";
|
|
2
|
-
import { CliInteractive } from "./CliInteractive.js";
|
|
3
|
-
import { WizardDropped } from "./internal/wizardGate.js";
|
|
4
|
-
import { answerWithoutPerson } from "./internal/fallbackAnswer.js";
|
|
5
|
-
import { Context, Effect, Layer, Queue, Terminal } from "effect";
|
|
6
|
-
import { CliConfig, GlobalFlag, Prompt } from "effect/cli";
|
|
7
|
-
|
|
8
|
-
// src/CliPrompt.ts
|
|
9
|
-
/**
|
|
10
|
-
* Prompts that know whether there is a person to ask.
|
|
11
|
-
*
|
|
12
|
-
* @public
|
|
13
|
-
*/
|
|
14
|
-
var CliPrompt = class {
|
|
15
|
-
constructor() {}
|
|
16
|
-
/**
|
|
17
|
-
* A fallback for `Flag.withFallbackPrompt` or `Argument.withFallbackPrompt` that only prompts when the run is
|
|
18
|
-
* interactive.
|
|
19
|
-
*
|
|
20
|
-
* @remarks
|
|
21
|
-
* Interactive (`CliInteractive`): the prompt runs and its answer is used. Not interactive: `otherwise` is used
|
|
22
|
-
* when given, and the terminal is never read; without it the parameter fails as missing, exactly as it would
|
|
23
|
-
* with no fallback, so core renders its own message and `CliRuntime.main` exits `64`. Name the parameter with
|
|
24
|
-
* `flag` (the name without dashes, as given to `Flag.String`) or `argument` so that error can be built.
|
|
25
|
-
*
|
|
26
|
-
* Why the prompt runs inside this function rather than being handed back to core: core's
|
|
27
|
-
* `withFallbackPrompt` turns a quit, such as Ctrl-C, into the original missing-parameter error, which would
|
|
28
|
-
* exit `64` as if the flag had been forgotten. Here a quit is `Cancelled` with reason `interrupt`, exit `130`.
|
|
29
|
-
* It is raised as a defect because core's parse step turns every typed failure into a usage error. Core then
|
|
30
|
-
* runs the already-answered `Prompt.succeed` it is handed. Because it travels as a defect, a handler's
|
|
31
|
-
* `Effect.catchTag("Cancelled", ...)` cannot see it, and only `CliRuntime.main` (or
|
|
32
|
-
* `CliRuntime.reportFailures`) renders it as one line with exit `130`; under a bare `runMain` it prints a
|
|
33
|
-
* stack.
|
|
34
|
-
*
|
|
35
|
-
* Core runs `Prompt.run` on the answered prompt it is handed, and an answered prompt never reads the terminal's
|
|
36
|
-
* input (core acquires it only when a prompt needs a key), so piped stdin is left untouched without any gate.
|
|
37
|
-
* `CliPrompt.gateTerminal`, which `CliEnv.layer` installs, covers the prompts this function does not wrap.
|
|
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
|
-
*
|
|
46
|
-
* @param prompt - the prompt to show
|
|
47
|
-
* @param options - the parameter it stands in for, and the non-interactive default
|
|
48
|
-
*/
|
|
49
|
-
static fallback = (prompt, options) => Effect.gen(function* () {
|
|
50
|
-
if (yield* CliInteractive) {
|
|
51
|
-
const answer = yield* Prompt.run(prompt).pipe(Effect.catchTag("QuitError", () => Effect.die(new Cancelled({ reason: "interrupt" }))));
|
|
52
|
-
return Prompt.succeed(answer);
|
|
53
|
-
}
|
|
54
|
-
return yield* answerWithoutPerson(options);
|
|
55
|
-
});
|
|
56
|
-
/**
|
|
57
|
-
* Gates core's `Terminal` on `CliInteractive`, so a run that is not interactive never touches the real one.
|
|
58
|
-
*
|
|
59
|
-
* @remarks
|
|
60
|
-
* A prompt that needs a key reads the terminal's input, and the real Node terminal then attaches a readline to
|
|
61
|
-
* stdin, which drops piped input and puts a TTY stdin into raw mode. Core's `withFallbackPrompt`, a `Prompt.run`
|
|
62
|
-
* in a handler and the command wizard all do so whether or not anyone is there to answer. Not interactive, this
|
|
63
|
-
* terminal's input is an already-ended queue, its `readLine` fails as quit and its `display` writes nothing, so
|
|
64
|
-
* any such prompt is quit at once and the real terminal is never read. Its `columns`, `rows` and `size` always come
|
|
65
|
-
* from the real one, so layout keeps working. Interactive, every call passes through to the real terminal.
|
|
66
|
-
*
|
|
67
|
-
* The decision is made on every call, not when the layer is built, so a scope that narrows `CliInteractive`
|
|
68
|
-
* later, such as an audience flag under `CliAudience.runWith`, gates it too.
|
|
69
|
-
*
|
|
70
|
-
* It requires the real `Terminal`. `CliEnv.layer` installs it, after `TerminalEnv` is built from the real
|
|
71
|
-
* terminal, so consumers do not compose it.
|
|
72
|
-
*/
|
|
73
|
-
static gateTerminal = Layer.effect(Terminal.Terminal, Effect.gen(function* () {
|
|
74
|
-
const real = yield* Terminal.Terminal;
|
|
75
|
-
const endedInput = Effect.map(Queue.unbounded(), (queue) => {
|
|
76
|
-
Queue.endUnsafe(queue);
|
|
77
|
-
return queue;
|
|
78
|
-
});
|
|
79
|
-
return Terminal.make({
|
|
80
|
-
columns: real.columns,
|
|
81
|
-
rows: real.rows,
|
|
82
|
-
size: real.size,
|
|
83
|
-
readInput: Effect.flatMap(CliInteractive, (interactive) => interactive ? real.readInput : endedInput),
|
|
84
|
-
readLine: Effect.flatMap(CliInteractive, (interactive) => interactive ? real.readLine : Effect.fail(new Terminal.QuitError({}))),
|
|
85
|
-
display: (text) => Effect.flatMap(CliInteractive, (interactive) => interactive ? real.display(text) : Effect.void)
|
|
86
|
-
});
|
|
87
|
-
}));
|
|
88
|
-
/**
|
|
89
|
-
* Drops core's `--wizard` built-in flag when the run is not interactive.
|
|
90
|
-
*
|
|
91
|
-
* @remarks
|
|
92
|
-
* The wizard prompts, so offering it without a terminal only leads to a dead end. With the flag gone, core
|
|
93
|
-
* treats `--wizard` as an unknown flag, a usage error that exits `64`, and lists it nowhere in `--help`.
|
|
94
|
-
* The layer reads `CliInteractive` and the ambient `CliConfig` when it is built, so provide those first, for
|
|
95
|
-
* example `CliPrompt.gateWizard.pipe(Layer.provide(CliInteractive.layer))`. It filters the ambient `CliConfig`,
|
|
96
|
-
* so a consumer's own `builtIns` survive, and returns it untouched when interactive. An audience flag read
|
|
97
|
-
* later is covered by `CliAudience.runWith`, which drops the wizard too. When this layer removes the flag it records
|
|
98
|
-
* that it did, so `runWith` puts it back only when a flag turns interactivity on where this took it away: a
|
|
99
|
-
* consumer who never had `Wizard` in their `builtIns` keeps it out.
|
|
100
|
-
*/
|
|
101
|
-
static gateWizard = Layer.effectContext(Effect.gen(function* () {
|
|
102
|
-
const ambient = yield* CliConfig.CliConfig;
|
|
103
|
-
const unchanged = Context.make(CliConfig.CliConfig, ambient);
|
|
104
|
-
if ((yield* CliInteractive) || !ambient.builtIns.includes(GlobalFlag.Wizard)) return unchanged;
|
|
105
|
-
const dropped = CliConfig.make({ builtIns: ambient.builtIns.filter((flag) => flag !== GlobalFlag.Wizard) });
|
|
106
|
-
return Context.make(CliConfig.CliConfig, dropped).pipe(Context.add(WizardDropped, dropped));
|
|
107
|
-
}));
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
export { CliPrompt };
|
package/CliRuntime.js
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import { Cancelled } from "./Cancelled.js";
|
|
2
|
-
import { sanitize } from "./Fmt.js";
|
|
3
|
-
import { failureText, tagOf } from "./internal/inspect.js";
|
|
4
|
-
import { NotInteractive } from "./NotInteractive.js";
|
|
5
|
-
import { FailureTargetCell, currentTarget, fallbackTarget, guardConsumerLines, linesOf, plainFailureLines, readSpans, refreshFailureTarget } from "./internal/failureTarget.js";
|
|
6
|
-
import { CliColor } from "./CliColor.js";
|
|
7
|
-
import { CliEnv } from "./CliEnv.js";
|
|
8
|
-
import { isExitCode } from "./internal/isExitCode.js";
|
|
9
|
-
import { CliExit } from "./CliExit.js";
|
|
10
|
-
import { TrustedLine } from "./internal/logSafety.js";
|
|
11
|
-
import { CliLogger, makeCliLogger } from "./CliLogger.js";
|
|
12
|
-
import { CliLog, envBuildLogLayer, platformLogLayer } from "./CliLog.js";
|
|
13
|
-
import { ExitRequested } from "./internal/ExitRequested.js";
|
|
14
|
-
import { routeHelpOnUsageError } from "./internal/HelpRouting.js";
|
|
15
|
-
import { Cause, Effect, Layer, Logger, MutableRef, Option, Runtime } from "effect";
|
|
16
|
-
import { CliError, CliOutput } from "effect/cli";
|
|
17
|
-
import { CommandNeutralizer } from "@effected/github-commands";
|
|
18
|
-
import { withDistribution, withDistributionVersion } from "@effected/engine";
|
|
19
|
-
|
|
20
|
-
// src/CliRuntime.ts
|
|
21
|
-
const isShowHelp = (u) => CliError.isCliError(u) && u._tag === "ShowHelp";
|
|
22
|
-
/** A `UserError` `Command.runWith` already printed: it sets the mark to `false` after rendering. */
|
|
23
|
-
const isRenderedUserError = (u) => CliError.isCliError(u) && u._tag === "UserError" && Runtime.getErrorReported(u) === false;
|
|
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
|
-
}));
|
|
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]")));
|
|
43
|
-
const toLines = (rendered) => typeof rendered === "string" ? [rendered] : rendered;
|
|
44
|
-
/**
|
|
45
|
-
* The error's own exit code when it carries one, otherwise the fallback.
|
|
46
|
-
*
|
|
47
|
-
* @remarks
|
|
48
|
-
* `Runtime.getErrorExitCode` cannot serve alone here: it answers `1` both for
|
|
49
|
-
* an error marked `1` and for an unmarked one, so an `exitCode` option would
|
|
50
|
-
* silently override a deliberate `1`. Testing for the marker keeps "the error
|
|
51
|
-
* chose its code" distinct from "nothing chose".
|
|
52
|
-
*/
|
|
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
|
-
};
|
|
64
|
-
/**
|
|
65
|
-
* Report a CLI program's failures through the program's own logger.
|
|
66
|
-
*
|
|
67
|
-
* @remarks
|
|
68
|
-
* ## What it prevents
|
|
69
|
-
*
|
|
70
|
-
* A platform `runMain` reports an unhandled failure using Effect's **default**
|
|
71
|
-
* logger. That logger sits **outside** the layers the program was provided —
|
|
72
|
-
* `makeRunMain` composes its reporting `tapCause` around the already-provided
|
|
73
|
-
* effect — so a program that carefully installs {@link CliLogger} still prints
|
|
74
|
-
* its failures in the structured format that logger exists to replace, and
|
|
75
|
-
* prints them on **stdout**, the one stream errors must not use.
|
|
76
|
-
*
|
|
77
|
-
* Nothing about the call site suggests this. The program looks correct, the
|
|
78
|
-
* logger is installed, every success path is right, and only a failure reveals
|
|
79
|
-
* it.
|
|
80
|
-
*
|
|
81
|
-
* ## Why a combinator, and not a `runMain`
|
|
82
|
-
*
|
|
83
|
-
* The fix has to happen **inside** the effect, before any `runMain` sees it. So
|
|
84
|
-
* this is a combinator you apply to your program, and you still call your own
|
|
85
|
-
* platform's runner. Wrapping `runMain` itself would drag a platform choice into a library that
|
|
86
|
-
* has no business making one, and would make this package unusable from Bun or
|
|
87
|
-
* Deno for no gain.
|
|
88
|
-
*
|
|
89
|
-
* ## What it does with the failure
|
|
90
|
-
*
|
|
91
|
-
* Renders it through the ambient logger, then **re-fails with a marked error**
|
|
92
|
-
* so the runtime still sees a failure — the exit code is the runtime's to set,
|
|
93
|
-
* and swallowing the failure would make a broken run exit `0`.
|
|
94
|
-
*
|
|
95
|
-
* The marks are core's own, and they are what make this work without a platform
|
|
96
|
-
* import:
|
|
97
|
-
*
|
|
98
|
-
* - `Runtime.errorExitCode` — `defaultTeardown` takes the squashed error's
|
|
99
|
-
* value as the process exit code. An error that already carries one **keeps
|
|
100
|
-
* it**: that was a deliberate choice by whatever raised it, and flattening
|
|
101
|
-
* every failure to a single code would discard it.
|
|
102
|
-
* - `Runtime.errorReported` — set to `false`, which suppresses the runtime's own
|
|
103
|
-
* duplicate report. **The polarity is inverted relative to the name**: the
|
|
104
|
-
* marker means "should this be reported", so the intuitive
|
|
105
|
-
* `errorReported: true` — "I have reported it, stay quiet" — produces exactly
|
|
106
|
-
* the double report it was meant to prevent.
|
|
107
|
-
*
|
|
108
|
-
* An **interrupt is left alone**: it is not a failure to report, and the
|
|
109
|
-
* default teardown already maps an interrupt-only cause to `130`.
|
|
110
|
-
*
|
|
111
|
-
* A `CliError.ShowHelp` is never rendered: `Command.runWith` already printed
|
|
112
|
-
* the help text (and any parse errors) before re-failing with it, so
|
|
113
|
-
* rendering it again would print nothing but a stray "Help requested" line.
|
|
114
|
-
* A `ShowHelp` carrying errors is remapped to `usageExitCode` (default `64`,
|
|
115
|
-
* BSD `EX_USAGE`); a bare `--help` or root invocation — `errors` empty —
|
|
116
|
-
* keeps exit `0`. A `CliError.UserError` whose `Runtime.errorReported` mark
|
|
117
|
-
* is `false` is likewise skipped: that is how `Command.runWith` leaves one it
|
|
118
|
-
* already rendered through its `CliOutput` formatter. It exits with its own
|
|
119
|
-
* `Runtime.errorExitCode` when it carries one, otherwise `usageExitCode`;
|
|
120
|
-
* under runWith's `renderErrors: false` the mark stays set, and it renders
|
|
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
|
|
125
|
-
* error renders even when it already carries the reported mark — a gate
|
|
126
|
-
* failure marked with `CliRuntime.reported` still prints its line. The
|
|
127
|
-
* private `ExitRequested` sentinel `CliRuntime.main`
|
|
128
|
-
* raises is likewise never rendered; it only carries the exit code a
|
|
129
|
-
* successful program recorded through `CliExit`.
|
|
130
|
-
*
|
|
131
|
-
* @example
|
|
132
|
-
* ```ts
|
|
133
|
-
* import { CliRuntime } from "@effected/cli"
|
|
134
|
-
* import { NodeRuntime } from "@effect/platform-node"
|
|
135
|
-
* import { Effect } from "effect"
|
|
136
|
-
*
|
|
137
|
-
* NodeRuntime.runMain(program.pipe(CliRuntime.reportFailures(), Effect.provide(MainLive)))
|
|
138
|
-
* ```
|
|
139
|
-
*
|
|
140
|
-
* @public
|
|
141
|
-
*/
|
|
142
|
-
var CliRuntime = class CliRuntime {
|
|
143
|
-
constructor() {}
|
|
144
|
-
/**
|
|
145
|
-
* What `reportFailures` and `main` render a failure as when no `render` option is given, for a consumer's own
|
|
146
|
-
* `render` to hand a failure back to.
|
|
147
|
-
*
|
|
148
|
-
* @remarks
|
|
149
|
-
* The plain lines of `CliFailure.toDoc(details.cause)`: a failure status line, a `Tree` for a schema failure, a
|
|
150
|
-
* defect's message with its cleaned `stack`, and the one fixed line each for `Cancelled` and `NotInteractive`.
|
|
151
|
-
* It has no terminal to ask, so it is the plain rendering for an agent, with absolute paths; the report `main` writes
|
|
152
|
-
* with no `render` option is the same document in the renderer the audience gets (painted for a person), and that
|
|
153
|
-
* report is `details.defaultLines`: return those to hand a failure back with the run's colour, links and path
|
|
154
|
-
* display. With `status: false` the leading status (the glyph, or `[FAIL]` in plain text) is left off, so a prefix
|
|
155
|
-
* such as the program's name reads cleanly; for that AND the run's settings, use `details.lines({ status: false })`.
|
|
156
|
-
* A custom `render` that only cares about its own errors delegates the rest:
|
|
157
|
-
*
|
|
158
|
-
* ```ts
|
|
159
|
-
* const render = (error: unknown, details: FailureDetails) =>
|
|
160
|
-
* error instanceof MyError ? myLines(error) : CliRuntime.defaultRender(error, details)
|
|
161
|
-
* ```
|
|
162
|
-
*
|
|
163
|
-
* @param error - the squashed failure
|
|
164
|
-
* @param details - what `render` is told about the failure; accepted so a delegating `render` passes both
|
|
165
|
-
* arguments through unchanged (only `cause` and `isDefect` are read)
|
|
166
|
-
* @param options - `status: false` leaves off the leading status glyph or `[FAIL]` tag
|
|
167
|
-
*/
|
|
168
|
-
static defaultRender = (error, details, options) => plainFailureLines(details.cause.reasons.length > 0 ? details.cause : Cause.fail(error), options?.status !== false);
|
|
169
|
-
/**
|
|
170
|
-
* Catch a program's failure, render it through the ambient logger, and re-fail with the exit code and the
|
|
171
|
-
* no-double-report mark.
|
|
172
|
-
*
|
|
173
|
-
* @param options - how to render the failure and which exit codes to use
|
|
174
|
-
*/
|
|
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) => {
|
|
191
|
-
try {
|
|
192
|
-
return linesOf(cause, target, status, spans ?? target.spans);
|
|
193
|
-
} catch {
|
|
194
|
-
try {
|
|
195
|
-
return plainFailureLines(cause, status, spans ?? target.spans);
|
|
196
|
-
} catch {
|
|
197
|
-
return lastResort(error);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
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
|
-
};
|
|
219
|
-
static main(program, options) {
|
|
220
|
-
const env = options.env === void 0 ? void 0 : CliEnv.layer(options.env);
|
|
221
|
-
const envLog = options.env?.log;
|
|
222
|
-
const logger = options.logger ?? (env === void 0 || envLog === void 0 ? CliLogger.layer() : CliLog.layer(envLog).pipe(Layer.provide(env), Layer.provide(Layer.provideMerge(options.platform.pipe(Layer.provide(platformLogLayer(envLog, options.env?.audienceEnvVar))), envBuildLogLayer(envLog, options.env?.audienceEnvVar))), Layer.catchCause(() => CliLogger.layer(envLog.logger))));
|
|
223
|
-
const inside = env === void 0 ? Layer.empty : Layer.mergeAll(CliColor.formatterLayer(options.env?.formatter), Layer.effectDiscard(Effect.gen(function* () {
|
|
224
|
-
const { spans, invalid } = yield* readSpans(options.env?.spans, options.env?.spansEnvVar);
|
|
225
|
-
if (invalid !== void 0) yield* Effect.logWarning(invalid).pipe(Effect.provideService(Logger.CurrentLoggers, /* @__PURE__ */ new Set([makeCliLogger(envLog?.logger)])));
|
|
226
|
-
yield* refreshFailureTarget(void 0, {
|
|
227
|
-
stackFrames: options.env?.stackFrames,
|
|
228
|
-
spans,
|
|
229
|
-
appModule: options.env?.appModule
|
|
230
|
-
});
|
|
231
|
-
}))).pipe(Layer.provideMerge(env));
|
|
232
|
-
const carried = options.distribution === void 0 ? program : withCarrier(program, options.distribution);
|
|
233
|
-
const run = Effect.gen(function* () {
|
|
234
|
-
yield* options.helpOnUsageError === "stderr" ? routeHelpOnUsageError(carried) : carried;
|
|
235
|
-
const exit = yield* CliExit;
|
|
236
|
-
const code = MutableRef.get(exit.code);
|
|
237
|
-
if (!isExitCode(code)) return yield* Effect.die(/* @__PURE__ */ new Error(`CliRuntime.main: CliExit code must be an integer 0..255, received ${code}`));
|
|
238
|
-
if (code !== 0) return yield* Effect.fail(new ExitRequested(code));
|
|
239
|
-
}).pipe(Effect.provide(CliExit.layer), Effect.provide(inside), Effect.provide(options.platform), CliRuntime.reportFailures(options), Effect.provide(logger));
|
|
240
|
-
return Effect.suspend(() => Effect.provideService(run, FailureTargetCell, MutableRef.make(void 0)));
|
|
241
|
-
}
|
|
242
|
-
static reported(error, exitCode = 1) {
|
|
243
|
-
const marked = error instanceof Error ? error : new Error(String(error));
|
|
244
|
-
for (const [key, value] of [[Runtime.errorReported, false], [Runtime.errorExitCode, exitCode]]) Object.defineProperty(marked, key, {
|
|
245
|
-
value,
|
|
246
|
-
writable: true,
|
|
247
|
-
configurable: true,
|
|
248
|
-
enumerable: false
|
|
249
|
-
});
|
|
250
|
-
return marked;
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
export { CliRuntime };
|
package/CliTest.js
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import { Fmt } from "./Fmt.js";
|
|
2
|
-
import { Duration, Effect, Fiber, FileSystem, Option, Path, Stream, SubscriptionRef } from "effect";
|
|
3
|
-
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
4
|
-
|
|
5
|
-
// src/CliTest.ts
|
|
6
|
-
/** The complete lines of `captured` from `offset`, and the rest too once it has ended; with the offset after them. */
|
|
7
|
-
const linesFrom = (offset, captured) => {
|
|
8
|
-
const lines = [];
|
|
9
|
-
let start = offset;
|
|
10
|
-
let end = captured.text.indexOf("\n", start);
|
|
11
|
-
while (end !== -1) {
|
|
12
|
-
lines.push(captured.text.slice(start, captured.text[end - 1] === "\r" ? end - 1 : end));
|
|
13
|
-
start = end + 1;
|
|
14
|
-
end = captured.text.indexOf("\n", start);
|
|
15
|
-
}
|
|
16
|
-
if (captured.done && start < captured.text.length) {
|
|
17
|
-
lines.push(captured.text.slice(start));
|
|
18
|
-
start = captured.text.length;
|
|
19
|
-
}
|
|
20
|
-
return [start, lines];
|
|
21
|
-
};
|
|
22
|
-
/** The bound on a child that ignores its termination signal, when the caller gives none. */
|
|
23
|
-
const FORCE_KILL_AFTER = "2 seconds";
|
|
24
|
-
/** `options.forceKillAfter` or the default; a duration that is not finite is a wiring mistake and throws. */
|
|
25
|
-
const forceKillAfterOf = (options) => {
|
|
26
|
-
const input = options.forceKillAfter ?? FORCE_KILL_AFTER;
|
|
27
|
-
const duration = Duration.fromInput(input);
|
|
28
|
-
if (typeof input === "number" && Number.isNaN(input) || Option.isNone(duration) || !Duration.isFinite(duration.value) || Duration.toMillis(duration.value) < 0) throw new RangeError(`CliTest: forceKillAfter must be a finite, non-negative duration, received ${String(input)}`);
|
|
29
|
-
return duration.value;
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* Core's handle fails `exitCode` for a child a signal ended, naming the signal only in the error's text; this reads it
|
|
33
|
-
* back out, so a signal is an outcome as an exit code is. Any other failure (a failed stdin stream) stays a failure.
|
|
34
|
-
*/
|
|
35
|
-
const SIGNALLED = /receipt of signal: '(SIG[A-Z0-9]+)'/;
|
|
36
|
-
const endingOf = (exitCode) => exitCode.pipe(Effect.map((code) => ({
|
|
37
|
-
exitCode: Number(code),
|
|
38
|
-
signal: null
|
|
39
|
-
})), Effect.catchTag("PlatformError", (error) => {
|
|
40
|
-
const match = SIGNALLED.exec(`${error.message} ${Fmt.causeMessage(error) ?? ""}`);
|
|
41
|
-
return match === null ? Effect.fail(error) : Effect.succeed({
|
|
42
|
-
exitCode: null,
|
|
43
|
-
signal: match[1]
|
|
44
|
-
});
|
|
45
|
-
}));
|
|
46
|
-
const commandOf = (bin, args, options, forceKillAfter) => ChildProcess.make(options.execPath, [bin, ...args], {
|
|
47
|
-
forceKillAfter,
|
|
48
|
-
cwd: options.cwd ?? options.sandbox.root,
|
|
49
|
-
env: {
|
|
50
|
-
...options.sandbox.env,
|
|
51
|
-
...options.env
|
|
52
|
-
},
|
|
53
|
-
stdin: options.stdin === void 0 || options.stdin === "" ? Stream.empty : Stream.make(new TextEncoder().encode(options.stdin))
|
|
54
|
-
});
|
|
55
|
-
const text = (stream) => Stream.mkString(Stream.decodeText(stream));
|
|
56
|
-
/**
|
|
57
|
-
* Spawn a built CLI bin hermetically and read its exit code and streams as data.
|
|
58
|
-
*
|
|
59
|
-
* @example
|
|
60
|
-
* ```ts
|
|
61
|
-
* import * as NodeServices from "@effect/platform-node/NodeServices"
|
|
62
|
-
* import { assert, it } from "@effect/vitest"
|
|
63
|
-
* import { CliTest } from "@effected/cli/testing"
|
|
64
|
-
* import { Effect } from "effect"
|
|
65
|
-
*
|
|
66
|
-
* it.effect("prints its version", () =>
|
|
67
|
-
* Effect.gen(function* () {
|
|
68
|
-
* const sandbox = yield* CliTest.sandbox({ path: process.env.PATH ?? "" })
|
|
69
|
-
* const result = yield* CliTest.run("dist/bin.js", ["--version"], { sandbox, execPath: process.execPath })
|
|
70
|
-
* assert.strictEqual(result.exitCode, 0)
|
|
71
|
-
* }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
|
|
72
|
-
* )
|
|
73
|
-
* ```
|
|
74
|
-
*
|
|
75
|
-
* @public
|
|
76
|
-
*/
|
|
77
|
-
var CliTest = class {
|
|
78
|
-
constructor() {}
|
|
79
|
-
/**
|
|
80
|
-
* A scoped temp directory with a fresh `HOME` and XDG tree, `NO_COLOR=1`,
|
|
81
|
-
* and the `PATH` you pass — the host environment is never inherited.
|
|
82
|
-
*
|
|
83
|
-
* @remarks
|
|
84
|
-
* Each call mints a fresh temp directory; bind the result to a `const`
|
|
85
|
-
* within one test rather than calling this more than once per assertion.
|
|
86
|
-
*/
|
|
87
|
-
static sandbox = (options) => Effect.gen(function* () {
|
|
88
|
-
const fs = yield* FileSystem.FileSystem;
|
|
89
|
-
const path = yield* Path.Path;
|
|
90
|
-
const root = yield* fs.makeTempDirectoryScoped({ prefix: "effected-cli-test-" });
|
|
91
|
-
const home = path.join(root, "home");
|
|
92
|
-
const xdg = {
|
|
93
|
-
XDG_CONFIG_HOME: path.join(home, ".config"),
|
|
94
|
-
XDG_DATA_HOME: path.join(home, ".local", "share"),
|
|
95
|
-
XDG_STATE_HOME: path.join(home, ".local", "state"),
|
|
96
|
-
XDG_CACHE_HOME: path.join(home, ".cache")
|
|
97
|
-
};
|
|
98
|
-
for (const dir of Object.values(xdg)) yield* fs.makeDirectory(dir, { recursive: true });
|
|
99
|
-
return {
|
|
100
|
-
root,
|
|
101
|
-
home,
|
|
102
|
-
env: {
|
|
103
|
-
HOME: home,
|
|
104
|
-
...xdg,
|
|
105
|
-
PATH: options.path,
|
|
106
|
-
NO_COLOR: "1"
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
});
|
|
110
|
-
/**
|
|
111
|
-
* Run `execPath bin ...args`; a non-zero exit is returned, never failed.
|
|
112
|
-
*
|
|
113
|
-
* @remarks
|
|
114
|
-
* `stdin` is never left as an inherited open pipe: when omitted or `""`
|
|
115
|
-
* the child gets an already-ended empty input (`Stream.empty`), so a
|
|
116
|
-
* stdin-reading bin exits instead of hanging on `effect/process`'s
|
|
117
|
-
* default `"pipe"` stdio, which stays open until something writes to and
|
|
118
|
-
* ends it.
|
|
119
|
-
*/
|
|
120
|
-
static run = (bin, args, options) => {
|
|
121
|
-
const forceKillAfter = forceKillAfterOf(options);
|
|
122
|
-
return Effect.scoped(Effect.gen(function* () {
|
|
123
|
-
const handle = yield* (yield* ChildProcessSpawner.ChildProcessSpawner).spawn(commandOf(bin, args, options, forceKillAfter));
|
|
124
|
-
const [stdout, stderr, ending] = yield* Effect.all([
|
|
125
|
-
text(handle.stdout),
|
|
126
|
-
text(handle.stderr),
|
|
127
|
-
endingOf(handle.exitCode)
|
|
128
|
-
], { concurrency: "unbounded" });
|
|
129
|
-
return {
|
|
130
|
-
...ending,
|
|
131
|
-
stdout,
|
|
132
|
-
stderr
|
|
133
|
-
};
|
|
134
|
-
}));
|
|
135
|
-
};
|
|
136
|
-
/**
|
|
137
|
-
* Start `execPath bin ...args` and return at once with a handle on it: its output as lines while it runs, a signal,
|
|
138
|
-
* and its exit, for a long-running command (a `--follow`, a watch mode, a server) that {@link CliTest.run} would wait
|
|
139
|
-
* on forever.
|
|
140
|
-
*
|
|
141
|
-
* @remarks
|
|
142
|
-
* The child lives in the caller's scope: when the scope closes first, the child is sent `SIGTERM`, then `SIGKILL`
|
|
143
|
-
* after `forceKillAfter` (`2 seconds` by default), so a test that ends early leaves nothing running and never waits
|
|
144
|
-
* on a child that ignores the signal. A child a signal ended reports `exitCode: null` and the signal in `result`. Both streams are drained from the start whether or not the test reads them, so a
|
|
145
|
-
* chatty child never blocks on a full pipe, and each stream replays from its first line to every reader. The
|
|
146
|
-
* options are {@link CliTest.run}'s, `stdin` included: the child gets that text, then an ended input.
|
|
147
|
-
*
|
|
148
|
-
* ```ts
|
|
149
|
-
* const child = yield* CliTest.spawn("dist/bin.js", ["logs", "--follow"], { sandbox, execPath: process.execPath })
|
|
150
|
-
* yield* Stream.runHead(child.stdout) // the first line: it is following
|
|
151
|
-
* yield* child.kill("SIGINT")
|
|
152
|
-
* assert.strictEqual(yield* child.exitCode, 0)
|
|
153
|
-
* ```
|
|
154
|
-
*
|
|
155
|
-
* @param bin - the built bin to run
|
|
156
|
-
* @param args - its arguments
|
|
157
|
-
* @param options - the sandbox, the node binary, and the working directory, environment and stdin
|
|
158
|
-
*/
|
|
159
|
-
static spawn = (bin, args, options) => {
|
|
160
|
-
const forceKillAfter = forceKillAfterOf(options);
|
|
161
|
-
return Effect.gen(function* () {
|
|
162
|
-
const handle = yield* (yield* ChildProcessSpawner.ChildProcessSpawner).spawn(commandOf(bin, args, options, forceKillAfter));
|
|
163
|
-
const capture = (stream) => Effect.gen(function* () {
|
|
164
|
-
const ref = yield* SubscriptionRef.make({
|
|
165
|
-
text: "",
|
|
166
|
-
done: false
|
|
167
|
-
});
|
|
168
|
-
return {
|
|
169
|
-
ref,
|
|
170
|
-
pump: yield* Stream.decodeText(stream).pipe(Stream.runForEach((chunk) => SubscriptionRef.update(ref, (s) => ({
|
|
171
|
-
text: s.text + chunk,
|
|
172
|
-
done: false
|
|
173
|
-
}))), Effect.ensuring(SubscriptionRef.update(ref, (s) => ({
|
|
174
|
-
text: s.text,
|
|
175
|
-
done: true
|
|
176
|
-
}))), Effect.forkScoped),
|
|
177
|
-
lines: SubscriptionRef.changes(ref).pipe(Stream.takeUntil((s) => s.done), Stream.mapAccum(() => 0, linesFrom))
|
|
178
|
-
};
|
|
179
|
-
});
|
|
180
|
-
const out = yield* capture(handle.stdout);
|
|
181
|
-
const err = yield* capture(handle.stderr);
|
|
182
|
-
const ending = endingOf(handle.exitCode);
|
|
183
|
-
return {
|
|
184
|
-
stdout: out.lines,
|
|
185
|
-
stderr: err.lines,
|
|
186
|
-
kill: (signal) => handle.kill({
|
|
187
|
-
killSignal: signal ?? "SIGTERM",
|
|
188
|
-
forceKillAfter
|
|
189
|
-
}).pipe(Effect.catchTag("PlatformError", (error) => Effect.flatMap(handle.isRunning, (running) => running ? Effect.fail(error) : Effect.void))),
|
|
190
|
-
exitCode: Effect.map(ending, (end) => end.exitCode),
|
|
191
|
-
result: Effect.gen(function* () {
|
|
192
|
-
const end = yield* ending;
|
|
193
|
-
yield* Fiber.join(out.pump);
|
|
194
|
-
yield* Fiber.join(err.pump);
|
|
195
|
-
return {
|
|
196
|
-
...end,
|
|
197
|
-
stdout: (yield* SubscriptionRef.get(out.ref)).text,
|
|
198
|
-
stderr: (yield* SubscriptionRef.get(err.ref)).text
|
|
199
|
-
};
|
|
200
|
-
})
|
|
201
|
-
};
|
|
202
|
-
});
|
|
203
|
-
};
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
export { CliTest };
|