@effected/cli 0.8.0 → 0.9.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/CliRuntime.js +7 -2
- package/README.md +2 -2
- package/index.d.ts +43 -3
- package/internal/HelpRouting.js +90 -0
- package/package.json +1 -1
package/CliRuntime.js
CHANGED
|
@@ -2,6 +2,7 @@ import { isExitCode } from "./internal/isExitCode.js";
|
|
|
2
2
|
import { CliExit } from "./CliExit.js";
|
|
3
3
|
import { CliLogger } from "./CliLogger.js";
|
|
4
4
|
import { ExitRequested } from "./internal/ExitRequested.js";
|
|
5
|
+
import { routeHelpOnUsageError } from "./internal/HelpRouting.js";
|
|
5
6
|
import { Cause, Effect, MutableRef, Runtime } from "effect";
|
|
6
7
|
import { CliError } from "effect/unstable/cli";
|
|
7
8
|
|
|
@@ -113,8 +114,12 @@ var CliRuntime = class CliRuntime {
|
|
|
113
114
|
}
|
|
114
115
|
if (isRenderedUserError(error)) return Effect.fail(CliRuntime.reported(error, chooseExitCode(error, options.usageExitCode ?? 64)));
|
|
115
116
|
const render = options.render ?? ((value) => String(value));
|
|
117
|
+
const details = {
|
|
118
|
+
cause,
|
|
119
|
+
isDefect: !Cause.hasFails(cause)
|
|
120
|
+
};
|
|
116
121
|
return Effect.gen(function* () {
|
|
117
|
-
for (const line of toLines(render(error))) yield* Effect.logError(line);
|
|
122
|
+
for (const line of toLines(render(error, details))) yield* Effect.logError(line);
|
|
118
123
|
return yield* Effect.fail(CliRuntime.reported(error, chooseExitCode(error, options.exitCode)));
|
|
119
124
|
});
|
|
120
125
|
}));
|
|
@@ -140,7 +145,7 @@ var CliRuntime = class CliRuntime {
|
|
|
140
145
|
* ```
|
|
141
146
|
*/
|
|
142
147
|
static main = (program, options) => Effect.gen(function* () {
|
|
143
|
-
yield* program;
|
|
148
|
+
yield* options.helpOnUsageError === "stderr" ? routeHelpOnUsageError(program) : program;
|
|
144
149
|
const exit = yield* CliExit;
|
|
145
150
|
const code = MutableRef.get(exit.code);
|
|
146
151
|
if (!isExitCode(code)) return yield* Effect.die(/* @__PURE__ */ new Error(`CliRuntime.main: CliExit code must be an integer 0..255, received ${code}`));
|
package/README.md
CHANGED
|
@@ -149,8 +149,8 @@ $ echo $?
|
|
|
149
149
|
|
|
150
150
|
- `CliLogger.layer(options?)` — replaces the default logger with plain lines, routing every level to stderr by default. The threshold is the `stderrFrom` option (pass `"Error"` to restore the old split), compared ordinally, so a level added upstream lands on the right stream without a change here.
|
|
151
151
|
- `CliLogger.make(options?)` — the `Logger` itself, for composing into a logger set you already have.
|
|
152
|
-
- `CliRuntime.reportFailures(options?)` — reports through your logger, then re-fails with an exit code and the mark that stops the runtime reporting it a second time. Never renders a `CliError.ShowHelp` (already printed by `Command.runWith`) — a `ShowHelp` carrying errors is remapped to `usageExitCode` (default `64`).
|
|
153
|
-
- `CliRuntime.main(program, { platform, logger?, ... })` — assembles a whole program in the one order that reports every failure well: a fresh `CliExit`, the platform layer inside failure reporting, and the logger outermost.
|
|
152
|
+
- `CliRuntime.reportFailures(options?)` — reports through your logger, then re-fails with an exit code and the mark that stops the runtime reporting it a second time. `render(error, details)` receives the squashed error and a `FailureDetails` (`{ cause, isDefect }`), so a defect can render differently from a typed failure. Never renders a `CliError.ShowHelp` (already printed by `Command.runWith`) — a `ShowHelp` carrying errors is remapped to `usageExitCode` (default `64`).
|
|
153
|
+
- `CliRuntime.main(program, { platform, logger?, ... })` — assembles a whole program in the one order that reports every failure well: a fresh `CliExit`, the platform layer inside failure reporting, and the logger outermost. `helpOnUsageError: "stderr"` moves the help printed with a parse error onto stderr beside the error, so a caller piping stdout into `jq` gets nothing on a usage error; `--help` and a bare group invocation still print on stdout.
|
|
154
154
|
- `CliRuntime.reported(error, exitCode?)` — marks an error you reported yourself, so the runtime stays quiet about it. A typed `Error` comes back as its own type (the marks are added in place) when it passes `instanceof Error` at runtime; any other value — including one that only satisfies `Error`'s shape structurally — is wrapped in a plain `Error`. A `CliError.UserError` marked with `reported` is treated as already printed and is not rendered — use a different error type if the program has not printed it. It keeps the code you pass: `reported(userError, 3)` exits `3`, not `usageExitCode`.
|
|
155
155
|
- `CliExit.set(code)` — records a findings exit code from a successful program; the highest code set during the run wins. `CliExit.layer` mints a fresh cell per provide (`Layer.fresh`) — `CliRuntime.main` provides it for you.
|
|
156
156
|
- `CliColor.enabled` — `Effect<boolean, never, Stdio>`, the no-color.org decision: off when stdout is not a terminal, or `NO_COLOR` is a non-empty value. `FORCE_COLOR` is ignored.
|
package/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context, Effect, Layer, LogLevel, Logger, MutableRef, Stdio } from "effect";
|
|
1
|
+
import { Cause, Context, Effect, Layer, LogLevel, Logger, MutableRef, Stdio } from "effect";
|
|
2
2
|
import { CliOutput } from "effect/unstable/cli";
|
|
3
3
|
import { ConfigValidationError } from "@effected/config-file";
|
|
4
4
|
//#region src/CliColor.d.ts
|
|
@@ -223,6 +223,21 @@ export declare class CliLogger {
|
|
|
223
223
|
}
|
|
224
224
|
//#endregion
|
|
225
225
|
//#region src/CliRuntime.d.ts
|
|
226
|
+
/**
|
|
227
|
+
* What `render` is told about a failure beyond the squashed error.
|
|
228
|
+
*
|
|
229
|
+
* @public
|
|
230
|
+
*/
|
|
231
|
+
interface FailureDetails {
|
|
232
|
+
/** The whole cause the program failed with, before squashing. */
|
|
233
|
+
readonly cause: Cause.Cause<unknown>;
|
|
234
|
+
/**
|
|
235
|
+
* `true` when the cause carries no typed failure, so `error` is a defect:
|
|
236
|
+
* a `die`, a thrown exception, a bug. `false` when `error` is a typed
|
|
237
|
+
* failure from the error channel.
|
|
238
|
+
*/
|
|
239
|
+
readonly isDefect: boolean;
|
|
240
|
+
}
|
|
226
241
|
/**
|
|
227
242
|
* How a failure is turned into output and an exit code.
|
|
228
243
|
*
|
|
@@ -235,8 +250,14 @@ interface ReportFailuresOptions {
|
|
|
235
250
|
* @remarks
|
|
236
251
|
* Return several lines to print several: a config error's own message
|
|
237
252
|
* followed by the rendered issue lines, say.
|
|
253
|
+
*
|
|
254
|
+
* `error` is the squashed cause: the first typed failure when there is
|
|
255
|
+
* one, otherwise the first defect. `details` says which it is, so a typed
|
|
256
|
+
* failure can render as one line and a defect as a full report, without
|
|
257
|
+
* guessing from the error's shape. A renderer that takes only `error`
|
|
258
|
+
* still fits.
|
|
238
259
|
*/
|
|
239
|
-
readonly render?: ((error: unknown) => string | ReadonlyArray<string>) | undefined;
|
|
260
|
+
readonly render?: ((error: unknown, details: FailureDetails) => string | ReadonlyArray<string>) | undefined;
|
|
240
261
|
/**
|
|
241
262
|
* The exit code to use when the error does not carry one.
|
|
242
263
|
*
|
|
@@ -276,6 +297,25 @@ interface MainOptions<RP, EP> extends ReportFailuresOptions {
|
|
|
276
297
|
readonly platform: Layer.Layer<RP, EP>;
|
|
277
298
|
/** The logger, provided outermost. Defaults to `CliLogger.layer()`. */
|
|
278
299
|
readonly logger?: Layer.Layer<never> | undefined;
|
|
300
|
+
/**
|
|
301
|
+
* Where the help document goes when it is printed with a usage error:
|
|
302
|
+
* `"stdout"` (the default, core's behaviour) or `"stderr"`, beside the
|
|
303
|
+
* errors.
|
|
304
|
+
*
|
|
305
|
+
* @remarks
|
|
306
|
+
* `"stderr"` keeps stdout clean for a caller that parses it, such as a
|
|
307
|
+
* hook piping JSON into `jq`: an unknown flag, a bad value or an unknown
|
|
308
|
+
* subcommand then writes nothing to stdout. An explicit `--help` and a
|
|
309
|
+
* bare invocation of a command group still print help on stdout: neither
|
|
310
|
+
* is an error.
|
|
311
|
+
*
|
|
312
|
+
* Two cases keep help on stdout even under `"stderr"`. A `CliOutput`
|
|
313
|
+
* Formatter or a `Console` provided inside `program` is not seen by
|
|
314
|
+
* `main`, so its help is not rerouted; provide the Formatter through
|
|
315
|
+
* `platform` instead. And with `Command.runWith`'s `renderErrors: false`
|
|
316
|
+
* no errors are printed, so nothing marks the help as a usage error's.
|
|
317
|
+
*/
|
|
318
|
+
readonly helpOnUsageError?: "stdout" | "stderr" | undefined;
|
|
279
319
|
}
|
|
280
320
|
/**
|
|
281
321
|
* Report a CLI program's failures through the program's own logger.
|
|
@@ -543,5 +583,5 @@ export declare class SchemaIssueRenderer {
|
|
|
543
583
|
static readonly render: (issue: unknown) => ReadonlyArray<string>;
|
|
544
584
|
}
|
|
545
585
|
//#endregion
|
|
546
|
-
export type { CliExitShape, CliLoggerOptions, MainOptions, ReportFailuresOptions };
|
|
586
|
+
export type { CliExitShape, CliLoggerOptions, FailureDetails, MainOptions, ReportFailuresOptions };
|
|
547
587
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Console, Effect } from "effect";
|
|
2
|
+
import { CliOutput } from "effect/unstable/cli";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/HelpRouting.ts
|
|
5
|
+
const OTHER_METHODS = Object.keys({
|
|
6
|
+
assert: true,
|
|
7
|
+
clear: true,
|
|
8
|
+
count: true,
|
|
9
|
+
countReset: true,
|
|
10
|
+
debug: true,
|
|
11
|
+
dir: true,
|
|
12
|
+
dirxml: true,
|
|
13
|
+
group: true,
|
|
14
|
+
groupCollapsed: true,
|
|
15
|
+
groupEnd: true,
|
|
16
|
+
info: true,
|
|
17
|
+
table: true,
|
|
18
|
+
time: true,
|
|
19
|
+
timeEnd: true,
|
|
20
|
+
timeLog: true,
|
|
21
|
+
trace: true,
|
|
22
|
+
warn: true
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Run `program` so a help document printed together with parse errors goes to
|
|
26
|
+
* stderr, beside the errors, instead of stdout.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* Core's `Command.runWith` prints a usage error as `Console.log(help)` then
|
|
30
|
+
* `Console.error(errors)`, and an explicit `--help` or a bare group
|
|
31
|
+
* invocation as the same `Console.log(help)` with nothing after it. The two
|
|
32
|
+
* only differ in what follows, so the help is held: the Formatter records
|
|
33
|
+
* every string its `formatHelpDoc` and `formatErrors` return, and a `log` of
|
|
34
|
+
* a recorded help string waits for the next console call. An `error` of a
|
|
35
|
+
* recorded errors string moves it to stderr; anything else, or the program
|
|
36
|
+
* ending, releases it to stdout. At most one document is held, and output
|
|
37
|
+
* order is kept.
|
|
38
|
+
*
|
|
39
|
+
* A Formatter or Console provided inside `program` is not wrapped, so help
|
|
40
|
+
* stays on stdout there: core's own behaviour.
|
|
41
|
+
*
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
const routeHelpOnUsageError = (program) => Effect.gen(function* () {
|
|
45
|
+
const formatter = yield* CliOutput.Formatter;
|
|
46
|
+
const sink = yield* Console.Console;
|
|
47
|
+
const helps = /* @__PURE__ */ new Set();
|
|
48
|
+
const errors = /* @__PURE__ */ new Set();
|
|
49
|
+
let held;
|
|
50
|
+
const release = () => {
|
|
51
|
+
if (held === void 0) return;
|
|
52
|
+
const help = held;
|
|
53
|
+
held = void 0;
|
|
54
|
+
sink.log(...help);
|
|
55
|
+
};
|
|
56
|
+
const recording = Object.assign(Object.create(formatter), {
|
|
57
|
+
formatHelpDoc: (doc) => {
|
|
58
|
+
const text = formatter.formatHelpDoc(doc);
|
|
59
|
+
helps.add(text);
|
|
60
|
+
return text;
|
|
61
|
+
},
|
|
62
|
+
formatErrors: (list) => {
|
|
63
|
+
const text = formatter.formatErrors(list);
|
|
64
|
+
errors.add(text);
|
|
65
|
+
return text;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
const routing = Object.create(sink);
|
|
69
|
+
for (const method of OTHER_METHODS) routing[method] = (...args) => {
|
|
70
|
+
release();
|
|
71
|
+
return sink[method](...args);
|
|
72
|
+
};
|
|
73
|
+
routing.log = (...args) => {
|
|
74
|
+
release();
|
|
75
|
+
if (args.length === 1 && typeof args[0] === "string" && helps.has(args[0])) held = args;
|
|
76
|
+
else sink.log(...args);
|
|
77
|
+
};
|
|
78
|
+
routing.error = (...args) => {
|
|
79
|
+
if (held !== void 0 && args.length === 1 && typeof args[0] === "string" && errors.has(args[0])) {
|
|
80
|
+
const help = held;
|
|
81
|
+
held = void 0;
|
|
82
|
+
sink.error(...help);
|
|
83
|
+
} else release();
|
|
84
|
+
sink.error(...args);
|
|
85
|
+
};
|
|
86
|
+
return yield* program.pipe(Effect.provideService(CliOutput.Formatter, recording), Effect.provideService(Console.Console, routing), Effect.ensuring(Effect.sync(release)));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
//#endregion
|
|
90
|
+
export { routeHelpOnUsageError };
|
package/package.json
CHANGED