@effected/cli 0.8.0 → 0.10.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/CliColor.js +1 -1
- package/CliRuntime.js +8 -3
- package/CliTest.js +2 -2
- package/README.md +6 -6
- package/index.d.ts +44 -4
- package/internal/HelpRouting.js +90 -0
- package/package.json +4 -4
- package/testing.d.ts +2 -2
package/CliColor.js
CHANGED
package/CliRuntime.js
CHANGED
|
@@ -2,8 +2,9 @@ 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
|
-
import { CliError } from "effect/
|
|
7
|
+
import { CliError } from "effect/cli";
|
|
7
8
|
|
|
8
9
|
//#region src/CliRuntime.ts
|
|
9
10
|
const isShowHelp = (u) => CliError.isCliError(u) && u._tag === "ShowHelp";
|
|
@@ -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/CliTest.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect, FileSystem, Path, Stream } from "effect";
|
|
2
|
-
import { ChildProcess, ChildProcessSpawner } from "effect/
|
|
2
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
3
3
|
|
|
4
4
|
//#region src/CliTest.ts
|
|
5
5
|
const text = (stream) => Stream.mkString(Stream.decodeText(stream));
|
|
@@ -47,7 +47,7 @@ var CliTest = class {
|
|
|
47
47
|
* @remarks
|
|
48
48
|
* `stdin` is never left as an inherited open pipe: when omitted or `""`
|
|
49
49
|
* the child gets an already-ended empty input (`Stream.empty`), so a
|
|
50
|
-
* stdin-reading bin exits instead of hanging on `effect/
|
|
50
|
+
* stdin-reading bin exits instead of hanging on `effect/process`'s
|
|
51
51
|
* default `"pipe"` stdio, which stays open until something writes to and
|
|
52
52
|
* ends it.
|
|
53
53
|
*/
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://nodejs.org/)
|
|
6
6
|
[](https://www.typescriptlang.org/)
|
|
7
7
|
|
|
8
|
-
The boundary layer of a command-line program built on `effect/
|
|
8
|
+
The boundary layer of a command-line program built on `effect/cli`: how output reaches a human, how a failure is reported, and how a schema issue becomes a sentence someone can act on. `CliLogger` renders log records as plain lines and routes diagnostics to stderr, reading the `Console` off the fiber so it needs no platform package and the stream split is actually testable. `CliRuntime.reportFailures` catches inside your program so a failure prints through *your* logger instead of Effect's default one on stdout, then re-fails with the exit code and the no-double-report mark; `CliRuntime.main` assembles a whole program — platform layer, a fresh `CliExit`, failure reporting, and the logger — in the one order that reports every failure well. `CliExit` lets a findings command (a linter that found problems, say) succeed with a non-zero exit code, with finalizers intact on any runtime. `CliColor` decides once, per the no-color.org rule, whether output carries ANSI colour, and hands core's own `CliOutput.Formatter` the same decision. `SchemaIssueRenderer` and `ConfigIssueRenderer` turn issue trees into `unknown key at groups.g.rulesetz`. The `./testing` subpath's `CliTest` spawns a built bin hermetically for a test that wants a real subprocess.
|
|
9
9
|
|
|
10
10
|
> **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
|
|
11
11
|
> development against a single pinned Effect v4 prerelease. Packages graduate to
|
|
@@ -25,7 +25,7 @@ Everything here shares one property: **you only discover you needed it by shippi
|
|
|
25
25
|
|
|
26
26
|
Effect's default logger emits `[00:33:56.619] INFO (#2): message`. That is correct for a service being scraped and wrong for a tool someone is watching — it turns a formatted table into noise — and nothing at the call site suggests it. A platform `runMain` then reports an unhandled failure through that *same* default logger, which sits outside the layers your program was provided, so a program that carefully installs a CLI logger still prints its failures in the format that logger exists to replace, on **stdout**, the one stream errors must not use. And a decode failure arrives as a structured tree when what a user needs is a sentence naming the key they got wrong; core does ship formatters for this, but they live on `SchemaIssue` rather than `SchemaError`, are named `makeFormatter*`, and are not referenced by `SchemaError.message` — two engineers searched for two rounds and concluded they did not exist.
|
|
27
27
|
|
|
28
|
-
This package is **not a CLI framework**. `effect/
|
|
28
|
+
This package is **not a CLI framework**. `effect/cli` owns argument parsing, flags, the command tree and help, and this package must never grow a second one.
|
|
29
29
|
|
|
30
30
|
## Install
|
|
31
31
|
|
|
@@ -104,13 +104,13 @@ Print-then-`reported` is for a program run **without** `CliRuntime.main` or `rep
|
|
|
104
104
|
|
|
105
105
|
A findings command — one whose non-zero exit reports a result rather than a
|
|
106
106
|
crash — wires `CliRuntime.main`, `CliExit.set` and `CliColor.formatterLayer`
|
|
107
|
-
around an ordinary `effect/
|
|
107
|
+
around an ordinary `effect/cli` command:
|
|
108
108
|
|
|
109
109
|
```ts
|
|
110
110
|
import { CliColor, CliExit, CliRuntime } from "@effected/cli";
|
|
111
111
|
import { NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
112
112
|
import { Console, Effect, Layer } from "effect";
|
|
113
|
-
import { Command, Flag } from "effect/
|
|
113
|
+
import { Command, Flag } from "effect/cli";
|
|
114
114
|
|
|
115
115
|
const findProblems = (strict: boolean): ReadonlyArray<string> =>
|
|
116
116
|
strict ? ["missing changeset", "unpinned dependency"] : ["missing changeset"];
|
|
@@ -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,5 +1,5 @@
|
|
|
1
|
-
import { Context, Effect, Layer, LogLevel, Logger, MutableRef, Stdio } from "effect";
|
|
2
|
-
import { CliOutput } from "effect/
|
|
1
|
+
import { Cause, Context, Effect, Layer, LogLevel, Logger, MutableRef, Stdio } from "effect";
|
|
2
|
+
import { CliOutput } from "effect/cli";
|
|
3
3
|
import { ConfigValidationError } from "@effected/config-file";
|
|
4
4
|
//#region src/CliColor.d.ts
|
|
5
5
|
/**
|
|
@@ -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/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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "The boundary layer of an effect/
|
|
5
|
+
"description": "The boundary layer of an effect/cli program: plain CLI output, failure reporting, and schema-issue rendering",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"effect",
|
|
8
8
|
"cli",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"./package.json": "./package.json"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@effected/config-file": "^0.
|
|
44
|
-
"effect": "4.0.0-rc.
|
|
43
|
+
"@effected/config-file": "^0.13.0",
|
|
44
|
+
"effect": "4.0.0-rc.118"
|
|
45
45
|
},
|
|
46
46
|
"peerDependenciesMeta": {
|
|
47
47
|
"@effected/config-file": {
|
package/testing.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect, FileSystem, Path, PlatformError, Scope } from "effect";
|
|
2
|
-
import { ChildProcessSpawner } from "effect/
|
|
2
|
+
import { ChildProcessSpawner } from "effect/process";
|
|
3
3
|
//#region src/CliTest.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* A hermetic temp directory minted by {@link CliTest.sandbox}, removed when its
|
|
@@ -83,7 +83,7 @@ export declare class CliTest {
|
|
|
83
83
|
* @remarks
|
|
84
84
|
* `stdin` is never left as an inherited open pipe: when omitted or `""`
|
|
85
85
|
* the child gets an already-ended empty input (`Stream.empty`), so a
|
|
86
|
-
* stdin-reading bin exits instead of hanging on `effect/
|
|
86
|
+
* stdin-reading bin exits instead of hanging on `effect/process`'s
|
|
87
87
|
* default `"pipe"` stdio, which stays open until something writes to and
|
|
88
88
|
* ends it.
|
|
89
89
|
*/
|