@effected/cli 0.23.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CliEnv.js +15 -3
- package/CliFormat.js +71 -16
- package/CliInteractive.js +28 -4
- package/CliPrompt.js +7 -0
- package/CliRuntime.js +10 -1
- package/CliTest.js +134 -22
- package/ConfigIssueRenderer.js +8 -2
- package/Fmt.js +94 -22
- package/index.d.ts +183 -30
- package/internal/HelpRouting.js +21 -75
- package/internal/format.js +74 -3
- package/internal/logSafety.js +1 -1
- package/internal/terminalCapabilities.js +23 -0
- package/package.json +8 -8
- package/testing.d.ts +68 -5
- package/tsdoc-metadata.json +1 -1
- package/ui/CliUi.js +4 -1
- package/ui.d.ts +4 -1
- package/internal/causeMessage.js +0 -57
package/index.d.ts
CHANGED
|
@@ -2156,6 +2156,15 @@ type CliEnvTestServices = TerminalEnv | Audience | CliTheme;
|
|
|
2156
2156
|
* `Console` or `Stdio`, never `Terminal`. It also installs `CliTheme.promptTheme`, so core's prompts follow the
|
|
2157
2157
|
* theme.
|
|
2158
2158
|
*
|
|
2159
|
+
* It sets core's `TerminalCapabilities` from `TerminalEnv` (through `TerminalEnv.capabilities` in `@effected/env`), so
|
|
2160
|
+
* the colour of core's own help, version and error output follows the kit's colour rule, and `canAnimate` is stdout
|
|
2161
|
+
* being a terminal that is not `dumb`. `canPrompt` alone is held at `true` whatever the terminal is: core skips a
|
|
2162
|
+
* fallback prompt entirely when it is `false`, which would lose `CliPrompt.fallback`'s and `CliUi.fallback`'s
|
|
2163
|
+
* `otherwise` in every piped run, and `CliInteractive` with `CliPrompt.gateTerminal` already owns that decision (the
|
|
2164
|
+
* gate quits any other prompt at once when the run is not interactive). A `TerminalCapabilities` with
|
|
2165
|
+
* `canPrompt: false` provided inside the program bypasses the kit's fallbacks: the parameter then fails as missing even
|
|
2166
|
+
* when an `otherwise` is given.
|
|
2167
|
+
*
|
|
2159
2168
|
* @public
|
|
2160
2169
|
*/
|
|
2161
2170
|
export declare class CliEnv {
|
|
@@ -2173,7 +2182,7 @@ export declare class CliEnv {
|
|
|
2173
2182
|
* The environment services a test fixes, needing nothing and reading nothing of the host's: `TerminalEnv` and
|
|
2174
2183
|
* `Audience` from the answers given, `CliTheme` built from them as {@link CliEnv.layer} builds it, and
|
|
2175
2184
|
* `CliInteractive` set from them by the same rule (a human, every stream a terminal, and a `TERM` that is not
|
|
2176
|
-
* `dumb`).
|
|
2185
|
+
* `dumb`), and core's `TerminalCapabilities` from them as `CliEnv.layer` sets it, `canPrompt` held at `true`.
|
|
2177
2186
|
*
|
|
2178
2187
|
* @remarks
|
|
2179
2188
|
* `term` is handed to the theme and interactivity builds alone, through a `ConfigProvider` of their own: the
|
|
@@ -2365,15 +2374,34 @@ type CliFormatName = "auto" | "plain" | "ansi" | "markdown" | "githubLog" | "jso
|
|
|
2365
2374
|
/**
|
|
2366
2375
|
* Options for {@link CliFormat.flag}.
|
|
2367
2376
|
*
|
|
2377
|
+
* @typeParam F - the built-in formats offered under their own names
|
|
2378
|
+
* @typeParam A - the caller's alias names, each standing for a built-in format
|
|
2379
|
+
* @typeParam C - the caller's own formats, which the caller renders itself
|
|
2380
|
+
*
|
|
2368
2381
|
* @public
|
|
2369
2382
|
*/
|
|
2370
|
-
interface CliFormatFlagOptions<F extends CliFormatName> {
|
|
2371
|
-
/**
|
|
2372
|
-
|
|
2383
|
+
interface CliFormatFlagOptions<F extends CliFormatName, A extends Readonly<Record<string, CliFormatName>> = Record<never, CliFormatName>, C extends string = never> {
|
|
2384
|
+
/**
|
|
2385
|
+
* The format when the flag is not given, spelled as on the command line (an alias by its own name): `auto` when it
|
|
2386
|
+
* is offered, otherwise the first one.
|
|
2387
|
+
*/
|
|
2388
|
+
readonly default?: F | Extract<keyof A, string> | C | undefined;
|
|
2373
2389
|
/** The flag's name; `format` by default. */
|
|
2374
2390
|
readonly name?: string | undefined;
|
|
2375
2391
|
/** The flag's description in help; one naming the offered formats by default. */
|
|
2376
2392
|
readonly description?: string | undefined;
|
|
2393
|
+
/**
|
|
2394
|
+
* Names of the caller's own that stand for a built-in format: `{ human: "auto" }` offers `--format human`, whose
|
|
2395
|
+
* value is `auto`. The target need not be offered under its own name, so `["json"]` with `{ human: "auto" }` offers
|
|
2396
|
+
* exactly `json` and `human`. An alias may not reuse a built-in format's name or any other offered name.
|
|
2397
|
+
*/
|
|
2398
|
+
readonly aliases?: A | undefined;
|
|
2399
|
+
/**
|
|
2400
|
+
* Formats the caller renders itself, such as `mermaid` or `dot`: offered under their own names and yielded as
|
|
2401
|
+
* themselves. {@link CliFormat.print} takes none of them; branch on a custom value before printing. A custom name
|
|
2402
|
+
* may not reuse a built-in format's name or any other offered name.
|
|
2403
|
+
*/
|
|
2404
|
+
readonly custom?: ReadonlyArray<C> | undefined;
|
|
2377
2405
|
}
|
|
2378
2406
|
/**
|
|
2379
2407
|
* What {@link CliFormat.print} prints: the command's document and, for `json`, its data.
|
|
@@ -2402,8 +2430,8 @@ interface CliFormatPrintOptions {
|
|
|
2402
2430
|
* once as a value, and never writes per-format output by hand.
|
|
2403
2431
|
*
|
|
2404
2432
|
* @remarks
|
|
2405
|
-
* The flag is core's `Flag.
|
|
2406
|
-
* format to `Doc.print` and writes `json` itself.
|
|
2433
|
+
* The flag is core's `Flag.ChoiceWithValue` with the kit's names, default and description; the print hands every
|
|
2434
|
+
* document format to `Doc.print` and writes `json` itself.
|
|
2407
2435
|
*
|
|
2408
2436
|
* ```ts
|
|
2409
2437
|
* const format = CliFormat.flag(["auto", "markdown", "json"])
|
|
@@ -2417,30 +2445,61 @@ interface CliFormatPrintOptions {
|
|
|
2417
2445
|
export declare class CliFormat {
|
|
2418
2446
|
private constructor();
|
|
2419
2447
|
/**
|
|
2420
|
-
* A `--format` flag offering `formats
|
|
2448
|
+
* A `--format` flag offering `formats`, and the caller's own `aliases` and `custom` formats.
|
|
2421
2449
|
*
|
|
2422
2450
|
* @remarks
|
|
2423
2451
|
* Its default is `options.default`, else `auto` when it is offered, else the first format. Any other value is a
|
|
2424
|
-
* usage error from core's parser, naming the accepted ones.
|
|
2452
|
+
* usage error from core's parser, naming the accepted ones. The value is the built-in format a name stands for
|
|
2453
|
+
* (`human` under `{ human: "auto" }` is `auto`), or a custom name as itself, so the value of a flag with no custom
|
|
2454
|
+
* formats goes straight to {@link CliFormat.print}.
|
|
2455
|
+
*
|
|
2456
|
+
* Two offered names that clash (an alias or custom name reusing a built-in name, or any name offered twice) and a
|
|
2457
|
+
* default that is not offered are programming mistakes: the call throws. A CLI that still accepts a `--json` of its
|
|
2458
|
+
* own declares and resolves that flag itself; {@link CliFormat.printJson} prints the JSON the same way.
|
|
2459
|
+
*
|
|
2460
|
+
* ```ts
|
|
2461
|
+
* // okfit's contract: --format human|json, and a graph command that also draws mermaid and dot
|
|
2462
|
+
* const format = CliFormat.flag(["json"], { aliases: { human: "auto" }, default: "human" })
|
|
2463
|
+
* const graph = CliFormat.flag(["json"], { custom: ["mermaid", "dot"], default: "mermaid" })
|
|
2464
|
+
* ```
|
|
2425
2465
|
*
|
|
2426
|
-
* @param formats - the formats the command offers, at least one
|
|
2427
|
-
* @param options - the default, the flag's name and
|
|
2466
|
+
* @param formats - the built-in formats the command offers under their own names, at least one
|
|
2467
|
+
* @param options - the default, the flag's name and description, aliases and custom formats
|
|
2428
2468
|
*/
|
|
2429
|
-
static readonly flag: <const F extends CliFormatName>(formats: readonly [F, ...ReadonlyArray<F>], options?: CliFormatFlagOptions<F>) => Flag.Flag<F>;
|
|
2469
|
+
static readonly flag: <const F extends CliFormatName, const A extends Readonly<Record<string, CliFormatName>> = Record<never, CliFormatName>, const C extends string = never>(formats: readonly [F, ...ReadonlyArray<F>], options?: CliFormatFlagOptions<F, A, C>) => Flag.Flag<F | A[keyof A] | C>;
|
|
2430
2470
|
/**
|
|
2431
2471
|
* Print a command's output in `format`.
|
|
2432
2472
|
*
|
|
2433
2473
|
* @remarks
|
|
2434
|
-
* `json` writes `output.json()` as
|
|
2435
|
-
*
|
|
2436
|
-
* so the runner cannot read it as a command. A value JSON cannot hold (a cycle, a bigint) is a defect. Every other
|
|
2437
|
-
* format prints `output.doc()` with `Doc.print`, which renders it as `auto` or the named renderer would.
|
|
2474
|
+
* `json` writes `output.json()` exactly as {@link CliFormat.printJson} writes a value, which it delegates to. Every
|
|
2475
|
+
* other format prints `output.doc()` with `Doc.print`, which renders it as `auto` or the named renderer would.
|
|
2438
2476
|
*
|
|
2439
2477
|
* @param format - the chosen format, usually the value of {@link CliFormat.flag}
|
|
2440
2478
|
* @param output - the document and the data
|
|
2441
2479
|
* @param options - the stream and the JSON indent
|
|
2442
2480
|
*/
|
|
2443
2481
|
static readonly print: (format: CliFormatName, output: CliFormatOutput, options?: CliFormatPrintOptions) => Effect.Effect<void, never, CliTheme | TerminalEnv | Audience | CliLinks>;
|
|
2482
|
+
/**
|
|
2483
|
+
* Print `value` as JSON, the way {@link CliFormat.print} prints the `json` format, for a CLI that keeps a JSON flag
|
|
2484
|
+
* of its own.
|
|
2485
|
+
*
|
|
2486
|
+
* @remarks
|
|
2487
|
+
* One `Console.log` (or `Console.error` for stderr), indented by `indent`: one document, never one line per item.
|
|
2488
|
+
* Under GitHub Actions a `##[` inside it is written as the JSON escape `#\u0023[`, which decodes to the same
|
|
2489
|
+
* text, so the runner cannot read it as a command. A value JSON cannot hold (a cycle, a bigint, `undefined`) is a
|
|
2490
|
+
* defect. It needs no theme, terminal or audience: whether this is GitHub Actions is read from `CurrentRuntimeEnv`
|
|
2491
|
+
* when the environment has it, and is "no" otherwise.
|
|
2492
|
+
*
|
|
2493
|
+
* ```ts
|
|
2494
|
+
* const status = Command.make("status", { json: Flag.Boolean("json").pipe(Flag.withDefault(false)) }, ({ json }) =>
|
|
2495
|
+
* json ? CliFormat.printJson(results) : Doc.print(report(results)),
|
|
2496
|
+
* )
|
|
2497
|
+
* ```
|
|
2498
|
+
*
|
|
2499
|
+
* @param value - the data to print
|
|
2500
|
+
* @param options - the stream and the JSON indent
|
|
2501
|
+
*/
|
|
2502
|
+
static readonly printJson: (value: unknown, options?: CliFormatPrintOptions) => Effect.Effect<void>;
|
|
2444
2503
|
}
|
|
2445
2504
|
declare const CliInteractive_base: Context.Reference<boolean>;
|
|
2446
2505
|
/**
|
|
@@ -2461,6 +2520,19 @@ declare const CliInteractive_base: Context.Reference<boolean>;
|
|
|
2461
2520
|
* {@link CliInteractive.layerTest} are typed `Layer<never>`: they set the
|
|
2462
2521
|
* reference rather than provide a service.
|
|
2463
2522
|
*
|
|
2523
|
+
* Both layers also hold core's `TerminalCapabilities.canPrompt` at `true`,
|
|
2524
|
+
* keeping its `canColor` and `canAnimate` as they find them. Core skips a
|
|
2525
|
+
* `Flag.withFallbackPrompt` or `Argument.withFallbackPrompt` fallback entirely
|
|
2526
|
+
* when `canPrompt` is `false`, and decides that from the process's own
|
|
2527
|
+
* terminal, so a kit fallback (`CliPrompt.fallback`, `CliUi.fallback`) would
|
|
2528
|
+
* never be called in a piped run and its `otherwise` would be lost. With the
|
|
2529
|
+
* gate held open, this reference makes the prompt decision instead: the kit's
|
|
2530
|
+
* fallbacks answer without a person when it is `false`, and
|
|
2531
|
+
* `CliPrompt.gateTerminal` quits any other prompt at once. A
|
|
2532
|
+
* `TerminalCapabilities` with `canPrompt: false` provided inside the program,
|
|
2533
|
+
* under these layers, bypasses the kit's fallbacks again: the parameter fails
|
|
2534
|
+
* as missing even when an `otherwise` is given.
|
|
2535
|
+
*
|
|
2464
2536
|
* @public
|
|
2465
2537
|
*/
|
|
2466
2538
|
export declare class CliInteractive extends CliInteractive_base {
|
|
@@ -2474,11 +2546,13 @@ export declare class CliInteractive extends CliInteractive_base {
|
|
|
2474
2546
|
* as `@effected/env` reads the environment, so it adds no requirement; a test fixes it with
|
|
2475
2547
|
* `Effect.provideService(ConfigProvider.ConfigProvider, ConfigProvider.fromUnknown({ TERM: "dumb" }))`.
|
|
2476
2548
|
*
|
|
2549
|
+
* It also holds core's `TerminalCapabilities.canPrompt` at `true`; see {@link CliInteractive}.
|
|
2550
|
+
*
|
|
2477
2551
|
* Bind the layer to a constant and provide it once; `Audience` and `TerminalEnv` come from `@effected/env`.
|
|
2478
2552
|
*/
|
|
2479
2553
|
static readonly layer: Layer.Layer<never, never, Audience | TerminalEnv>;
|
|
2480
2554
|
/**
|
|
2481
|
-
* A fixed answer that needs nothing.
|
|
2555
|
+
* A fixed answer that needs nothing. It holds core's `canPrompt` open too, as {@link CliInteractive.layer} does.
|
|
2482
2556
|
*
|
|
2483
2557
|
* @param value - whether the run is interactive
|
|
2484
2558
|
*/
|
|
@@ -2616,6 +2690,13 @@ export declare class CliPrompt {
|
|
|
2616
2690
|
* input (core acquires it only when a prompt needs a key), so piped stdin is left untouched without any gate.
|
|
2617
2691
|
* `CliPrompt.gateTerminal`, which `CliEnv.layer` installs, covers the prompts this function does not wrap.
|
|
2618
2692
|
*
|
|
2693
|
+
* Core calls a fallback only when its `TerminalCapabilities.canPrompt` is `true`, and its own default decides that
|
|
2694
|
+
* from the process's terminal, so in a piped run it would fail the parameter as missing without calling this
|
|
2695
|
+
* function and `otherwise` would be lost. `CliInteractive.layer` and `CliInteractive.layerTest` (and so
|
|
2696
|
+
* `CliEnv.layer`) hold `canPrompt` at `true` for that reason: the decision is `CliInteractive`'s, made here. A
|
|
2697
|
+
* `TerminalCapabilities` with `canPrompt: false` provided inside the program bypasses this fallback again, and a
|
|
2698
|
+
* program that sets `CliInteractive` with neither layer keeps core's own gate.
|
|
2699
|
+
*
|
|
2619
2700
|
* @param prompt - the prompt to show
|
|
2620
2701
|
* @param options - the parameter it stands in for, and the non-interactive default
|
|
2621
2702
|
*/
|
|
@@ -2853,15 +2934,18 @@ interface MainOptions<RP, EP> extends ReportFailuresOptions {
|
|
|
2853
2934
|
* bare invocation of a command group still print help on stdout: neither
|
|
2854
2935
|
* is an error.
|
|
2855
2936
|
*
|
|
2937
|
+
* `main` routes through core's `CliOutput.Presenter`, so the help and
|
|
2938
|
+
* errors are rendered by whichever `CliOutput` Formatter is in effect
|
|
2939
|
+
* where `Command.runWith` runs, one provided inside `program` included,
|
|
2940
|
+
* and written to whichever `Console` is in effect there. To change the
|
|
2941
|
+
* Formatter `main` installs, pass `env.formatter` (for example
|
|
2942
|
+
* `formatVersion`); a Formatter the platform provides is shadowed by
|
|
2943
|
+
* `main`'s, so pass it through `env.formatter` as well.
|
|
2944
|
+
*
|
|
2856
2945
|
* Two cases keep help on stdout even under `"stderr"`. A `CliOutput`
|
|
2857
|
-
*
|
|
2858
|
-
* `
|
|
2859
|
-
*
|
|
2860
|
-
* Formatter inside the platform, so that is the way in, and a Formatter
|
|
2861
|
-
* provided inside the program is invisible to the routing. A Formatter
|
|
2862
|
-
* the platform provides is shadowed by `main`'s, so pass it through
|
|
2863
|
-
* `env.formatter` as well. And with `Command.runWith`'s `renderErrors: false`
|
|
2864
|
-
* no errors are printed, so nothing marks the help as a usage error's.
|
|
2946
|
+
* Presenter provided inside `program` replaces the routing. And with
|
|
2947
|
+
* `Command.runWith`'s `renderErrors: false` core presents the help as
|
|
2948
|
+
* plain help, with no errors, so it is not a usage error's.
|
|
2865
2949
|
*/
|
|
2866
2950
|
readonly helpOnUsageError?: "stdout" | "stderr" | undefined;
|
|
2867
2951
|
/**
|
|
@@ -2942,7 +3026,10 @@ interface MainOptions<RP, EP> extends ReportFailuresOptions {
|
|
|
2942
3026
|
* already rendered through its `CliOutput` formatter. It exits with its own
|
|
2943
3027
|
* `Runtime.errorExitCode` when it carries one, otherwise `usageExitCode`;
|
|
2944
3028
|
* under runWith's `renderErrors: false` the mark stays set, and it renders
|
|
2945
|
-
* here like any other failure.
|
|
3029
|
+
* here like any other failure. A handler failure carrying a
|
|
3030
|
+
* `CliError.userMessage` is skipped the same way, since `Command.runWith`
|
|
3031
|
+
* renders it and sets the mark to `false`; it keeps the exit code a
|
|
3032
|
+
* handler failure gets (`exitCodes` by tag, then `exitCode`). Every other
|
|
2946
3033
|
* error renders even when it already carries the reported mark — a gate
|
|
2947
3034
|
* failure marked with `CliRuntime.reported` still prints its line. The
|
|
2948
3035
|
* private `ExitRequested` sentinel `CliRuntime.main`
|
|
@@ -3055,8 +3142,10 @@ export declare class CliRuntime {
|
|
|
3055
3142
|
*
|
|
3056
3143
|
* Under `CliRuntime.main` or {@link CliRuntime.reportFailures}, do NOT
|
|
3057
3144
|
* print the failure yourself before failing with it: `reportFailures`
|
|
3058
|
-
* renders every error except a `ShowHelp
|
|
3059
|
-
* reported mark is `false`,
|
|
3145
|
+
* renders every error except a `ShowHelp`, a `CliError.UserError` whose
|
|
3146
|
+
* reported mark is `false`, and any error carrying a string
|
|
3147
|
+
* `[CliError.userMessage]` whose reported mark is `false`, so it would
|
|
3148
|
+
* print twice. Fail with the marked
|
|
3060
3149
|
* error and put any multi-line rendering in the `render` option instead. The
|
|
3061
3150
|
* mark matters for a program run WITHOUT `reportFailures`, where it keeps the
|
|
3062
3151
|
* runtime from reporting a failure the program already printed.
|
|
@@ -3068,6 +3157,12 @@ export declare class CliRuntime {
|
|
|
3068
3157
|
* mark. It does keep the code you pass: `reported(userError, 3)` exits `3`,
|
|
3069
3158
|
* not `usageExitCode`.
|
|
3070
3159
|
*
|
|
3160
|
+
* The same holds for an error carrying a string `[CliError.userMessage]`:
|
|
3161
|
+
* `Command.runWith` prints such a handler failure and leaves the same
|
|
3162
|
+
* `false` mark, so one marked here is treated as already printed and is not
|
|
3163
|
+
* rendered. Mark an error without the annotation, or leave it unmarked, if
|
|
3164
|
+
* the program has not printed it. It keeps the code you pass.
|
|
3165
|
+
*
|
|
3071
3166
|
* The marks are added in place, so a typed error comes back as its own
|
|
3072
3167
|
* type: the `E` overload returns the very instance it was given, and a
|
|
3073
3168
|
* program failing with it keeps `catchTags` narrowing downstream without a
|
|
@@ -3130,6 +3225,12 @@ export declare class ConfigIssueRenderer {
|
|
|
3130
3225
|
* `Effect.catchTag("ConfigValidationError", …)` the error is already this
|
|
3131
3226
|
* type.
|
|
3132
3227
|
*
|
|
3228
|
+
* An unknown key the schema has a near match for at the same path ends in a
|
|
3229
|
+
* hint, `unknown key at theme.colour (did you mean "color"?)`, drawn from the
|
|
3230
|
+
* keys the struct declares there (every branch's, under a union) with
|
|
3231
|
+
* {@link Fmt.suggest}'s threshold. A key with no near match is drawn exactly
|
|
3232
|
+
* as {@link SchemaIssueRenderer} draws it.
|
|
3233
|
+
*
|
|
3133
3234
|
* It cannot throw on a malformed value: the issue tree is validated by
|
|
3134
3235
|
* a guard before it is read, so a renderer on an error path never becomes the
|
|
3135
3236
|
* reason a program dies.
|
|
@@ -3173,6 +3274,15 @@ interface PadOptions {
|
|
|
3173
3274
|
/** The fill, one column wide; a space by default. Anything that is not exactly one column wide is a space. */
|
|
3174
3275
|
readonly fill?: string | undefined;
|
|
3175
3276
|
}
|
|
3277
|
+
/**
|
|
3278
|
+
* Options for {@link Fmt.suggest}.
|
|
3279
|
+
*
|
|
3280
|
+
* @public
|
|
3281
|
+
*/
|
|
3282
|
+
interface SuggestOptions {
|
|
3283
|
+
/** The most edits a suggestion may be away from the input; `2` by default, as `effect/cli` uses. */
|
|
3284
|
+
readonly maxDistance?: number | undefined;
|
|
3285
|
+
}
|
|
3176
3286
|
/**
|
|
3177
3287
|
* Small, pure formatting primitives for terminal output.
|
|
3178
3288
|
*
|
|
@@ -3308,7 +3418,11 @@ export declare class Fmt {
|
|
|
3308
3418
|
* function), or `"[unprintable value]"` when that throws too. A getter that throws is a value with no message.
|
|
3309
3419
|
*
|
|
3310
3420
|
* The text is not sanitised: pass it through {@link Fmt.sanitize} before printing data you do not control. For
|
|
3311
|
-
* the messages along an error's `cause` chain, use {@link Fmt.causeMessage}
|
|
3421
|
+
* the messages along an error's `cause` chain, use {@link Fmt.causeMessage}; for the two combined, use
|
|
3422
|
+
* {@link Fmt.errorText}.
|
|
3423
|
+
*
|
|
3424
|
+
* It is `@effected/engine`'s `ErrorText.describe`, the same function, so a front end that cannot depend on this
|
|
3425
|
+
* package (an MCP server) describes a value with the same words.
|
|
3312
3426
|
*
|
|
3313
3427
|
* @param value - the value
|
|
3314
3428
|
*/
|
|
@@ -3327,11 +3441,50 @@ export declare class Fmt {
|
|
|
3327
3441
|
*
|
|
3328
3442
|
* Never throws: a `message` getter that throws leaves that node without text, and a `cause` getter that throws ends
|
|
3329
3443
|
* the walk with the text found so far, `undefined` when there is none, so a caller's own fallback
|
|
3330
|
-
* (`?? Fmt.describe(value)
|
|
3444
|
+
* (`?? Fmt.describe(value)`, which {@link Fmt.errorText} spells once) applies.
|
|
3445
|
+
*
|
|
3446
|
+
* It is `@effected/engine`'s `ErrorText.causeMessage`, the same function.
|
|
3331
3447
|
*
|
|
3332
3448
|
* @param value - the error, or whatever was thrown
|
|
3333
3449
|
*/
|
|
3334
3450
|
static readonly causeMessage: (value: unknown) => string | undefined;
|
|
3451
|
+
/**
|
|
3452
|
+
* The text of an unknown failure: {@link Fmt.causeMessage} when the chain carries a message, otherwise
|
|
3453
|
+
* {@link Fmt.describe}.
|
|
3454
|
+
*
|
|
3455
|
+
* @remarks
|
|
3456
|
+
* Exactly `Fmt.causeMessage(value) ?? Fmt.describe(value)`, the fallback an I/O site turning a caught value into
|
|
3457
|
+
* text would otherwise spell out by hand at every call. An error whose message is empty and whose chain carries none
|
|
3458
|
+
* is `""`, as `describe` reads it. Never throws, and not sanitised: pass it through {@link Fmt.sanitize} before
|
|
3459
|
+
* printing data you do not control. It is `@effected/engine`'s `ErrorText.of`, the same function.
|
|
3460
|
+
*
|
|
3461
|
+
* @param value - the error, or whatever was thrown
|
|
3462
|
+
*/
|
|
3463
|
+
static readonly errorText: (value: unknown) => string;
|
|
3464
|
+
/**
|
|
3465
|
+
* The candidates nearest to `input`, for a "did you mean" hint: those at the smallest edit distance found, up to
|
|
3466
|
+
* `maxDistance` edits (two by default), in the order given, each once. No candidate that close is `[]`.
|
|
3467
|
+
*
|
|
3468
|
+
* @remarks
|
|
3469
|
+
* The distance is Levenshtein's (one insertion, deletion or substitution per edit), over UTF-16 code units and
|
|
3470
|
+
* case-sensitive, with the same two-edit threshold and the same ties-all-kept rule `effect/cli` uses for an unknown
|
|
3471
|
+
* subcommand or flag, so a program's own suggestions agree with the parser's. A candidate equal to `input` is at
|
|
3472
|
+
* distance zero and is the only one returned.
|
|
3473
|
+
*
|
|
3474
|
+
* The cost is linear in the candidates' total length: a candidate whose length differs from `input`'s by more than
|
|
3475
|
+
* `maxDistance` is skipped unmeasured, and only the band of the edit matrix within `maxDistance` of its diagonal is
|
|
3476
|
+
* measured, so a long typed value cannot make it quadratic. A `maxDistance` that is not a non-negative integer is a programming mistake and
|
|
3477
|
+
* throws a `RangeError`.
|
|
3478
|
+
*
|
|
3479
|
+
* ```ts
|
|
3480
|
+
* Fmt.suggest("stauts", ["status", "stack", "build"]) // => ["status"]
|
|
3481
|
+
* ```
|
|
3482
|
+
*
|
|
3483
|
+
* @param input - what was typed: an unknown key, a profile name, a phase name
|
|
3484
|
+
* @param candidates - the names it could have meant
|
|
3485
|
+
* @param options - the most edits a suggestion may be away
|
|
3486
|
+
*/
|
|
3487
|
+
static readonly suggest: (input: string, candidates: Iterable<string>, options?: SuggestOptions) => ReadonlyArray<string>;
|
|
3335
3488
|
}
|
|
3336
3489
|
/**
|
|
3337
3490
|
* The severity of a GitHub annotation.
|
|
@@ -3794,5 +3947,5 @@ export declare class SchemaIssueRenderer {
|
|
|
3794
3947
|
*/
|
|
3795
3948
|
static readonly render: (issue: unknown) => ReadonlyArray<string>;
|
|
3796
3949
|
}
|
|
3797
|
-
export type { AnnotationLevel, AnnotationOptions, AudienceFlagInput, Block, BlockOf, CliAudienceFlagsOptions, CliDocSource, CliEnvOptions, CliEnvServices, CliEnvTestOptions, CliEnvTestServices, CliExitShape, CliFailureOptions, CliFormatFlagOptions, CliFormatName, CliFormatOutput, CliFormatPrintOptions, CliLinksLinkerOptions, CliLinksOptions, CliLinksShape, CliLogFile, CliLogFileOptions, CliLogOptions, CliLogStatusOptions, CliLoggerOptions, CliMessageOptions, CliPromptFallbackOptions, CliPromptTarget, CliThemeOptions, CliThemeShape, CliThemeTestOptions, Column, CoreStatusName, Counter, CountsOptions, CountsRow, CountsTableOptions, DocPrintOptions, Document, EditorLinks, FailureDetails, GithubAnnotationProperties, GlyphSelectOptions, GlyphSet, Inline, InlineInput, InlineOf, LinkOptions, LinkTarget, ListOptions, MainOptions, NamedColor, OverflowOptions, PadOptions, PercentOptions, RenderContext, RenderContextOfOptions, RenderContextOptions, ReportFailuresOptions, RequiresAudienceFlags, StatusDef, StatusRef, StreamTheme, Style, TableOptions, TokenName, TreeInput, TreeNode, TruncateOptions };
|
|
3950
|
+
export type { AnnotationLevel, AnnotationOptions, AudienceFlagInput, Block, BlockOf, CliAudienceFlagsOptions, CliDocSource, CliEnvOptions, CliEnvServices, CliEnvTestOptions, CliEnvTestServices, CliExitShape, CliFailureOptions, CliFormatFlagOptions, CliFormatName, CliFormatOutput, CliFormatPrintOptions, CliLinksLinkerOptions, CliLinksOptions, CliLinksShape, CliLogFile, CliLogFileOptions, CliLogOptions, CliLogStatusOptions, CliLoggerOptions, CliMessageOptions, CliPromptFallbackOptions, CliPromptTarget, CliThemeOptions, CliThemeShape, CliThemeTestOptions, Column, CoreStatusName, Counter, CountsOptions, CountsRow, CountsTableOptions, DocPrintOptions, Document, EditorLinks, FailureDetails, GithubAnnotationProperties, GlyphSelectOptions, GlyphSet, Inline, InlineInput, InlineOf, LinkOptions, LinkTarget, ListOptions, MainOptions, NamedColor, OverflowOptions, PadOptions, PercentOptions, RenderContext, RenderContextOfOptions, RenderContextOptions, ReportFailuresOptions, RequiresAudienceFlags, StatusDef, StatusRef, StreamTheme, Style, SuggestOptions, TableOptions, TokenName, TreeInput, TreeNode, TruncateOptions };
|
|
3798
3951
|
//# sourceMappingURL=index.d.ts.map
|
package/internal/HelpRouting.js
CHANGED
|
@@ -2,88 +2,34 @@ import { Console, Effect } from "effect";
|
|
|
2
2
|
import { CliOutput } from "effect/cli";
|
|
3
3
|
|
|
4
4
|
// src/internal/HelpRouting.ts
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
warn: true
|
|
23
|
-
});
|
|
5
|
+
/**
|
|
6
|
+
* Core's presenter, with an invalid invocation's help written to stderr beside
|
|
7
|
+
* its errors instead of stdout.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* Core's `defaultPresenter` writes an invalid invocation's help with
|
|
11
|
+
* `Console.log` and its errors with `Console.error`. Here it runs under a
|
|
12
|
+
* `Console` whose `log` is the ambient one's `error`, so the help and errors
|
|
13
|
+
* keep core's rendering, Formatter resolution included, and only the stream
|
|
14
|
+
* moves. An explicit `--help`, a bare group invocation and `--version` are
|
|
15
|
+
* other events, presented by core unchanged on stdout.
|
|
16
|
+
*/
|
|
17
|
+
const usageErrorsToStderr = { present: (event) => event._tag !== "InvalidInvocation" ? CliOutput.defaultPresenter.present(event) : Effect.gen(function* () {
|
|
18
|
+
const sink = yield* Console.Console;
|
|
19
|
+
const toStderr = Object.assign(Object.create(sink), { log: (...args) => sink.error(...args) });
|
|
20
|
+
yield* CliOutput.defaultPresenter.present(event).pipe(Effect.provideService(Console.Console, toStderr));
|
|
21
|
+
}) };
|
|
24
22
|
/**
|
|
25
23
|
* Run `program` so a help document printed together with parse errors goes to
|
|
26
24
|
* stderr, beside the errors, instead of stdout.
|
|
27
25
|
*
|
|
28
26
|
* @remarks
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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.
|
|
27
|
+
* Provides core's `CliOutput.Presenter`, which `Command.runWith` hands each
|
|
28
|
+
* help, invalid-invocation and version event to. A Presenter provided inside
|
|
29
|
+
* `program` replaces this one.
|
|
41
30
|
*
|
|
42
31
|
* @internal
|
|
43
32
|
*/
|
|
44
|
-
const routeHelpOnUsageError = (program) => Effect.
|
|
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
|
-
});
|
|
33
|
+
const routeHelpOnUsageError = (program) => Effect.provideService(program, CliOutput.Presenter, usageErrorsToStderr);
|
|
88
34
|
|
|
89
35
|
export { routeHelpOnUsageError };
|
package/internal/format.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fmt } from "../Fmt.js";
|
|
2
|
+
import { SchemaAST, SchemaIssue } from "effect";
|
|
2
3
|
|
|
3
4
|
// src/internal/format.ts
|
|
5
|
+
/** The message an unknown key renders as: the leaf hook below writes it and the "did you mean" hint reads it. */
|
|
6
|
+
const UNKNOWN_KEY = "unknown key";
|
|
4
7
|
/**
|
|
5
8
|
* Core's structured formatter, with one phrasing override.
|
|
6
9
|
*
|
|
@@ -16,7 +19,7 @@ import { SchemaIssue } from "effect";
|
|
|
16
19
|
*
|
|
17
20
|
* @internal
|
|
18
21
|
*/
|
|
19
|
-
const formatter = SchemaIssue.makeFormatterStandardSchemaV1({ leafHook: (issue) => issue._tag === "UnexpectedKey" ?
|
|
22
|
+
const formatter = SchemaIssue.makeFormatterStandardSchemaV1({ leafHook: (issue) => issue._tag === "UnexpectedKey" ? UNKNOWN_KEY : SchemaIssue.defaultLeafHook(issue) });
|
|
20
23
|
/**
|
|
21
24
|
* Flatten an issue tree to one line per rejected value.
|
|
22
25
|
*
|
|
@@ -39,6 +42,74 @@ const formatter = SchemaIssue.makeFormatterStandardSchemaV1({ leafHook: (issue)
|
|
|
39
42
|
*/
|
|
40
43
|
const formatIssue = (issue) => issueEntries(issue).map((entry) => entry.path.length === 0 ? entry.message : `${entry.message} at ${entry.path.join(".")}`);
|
|
41
44
|
/**
|
|
45
|
+
* {@link formatIssue}, with a "did you mean" hint on an unknown key the schema has a near match for.
|
|
46
|
+
*
|
|
47
|
+
* @remarks
|
|
48
|
+
* Used by `ConfigIssueRenderer` only. The declared keys come from the issue tree itself: core's `UnexpectedKey` leaf
|
|
49
|
+
* carries the AST of the struct being decoded, whose property signatures are the keys the schema accepts at that
|
|
50
|
+
* path, so no schema is passed in and no `@effected/config-file` value is needed. The hint is `Fmt.suggest`'s list,
|
|
51
|
+
* each key sanitised, and a line with no near match is exactly the line {@link formatIssue} draws.
|
|
52
|
+
*
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
const formatIssueWithHints = (issue) => {
|
|
56
|
+
const declared = declaredKeys(issue);
|
|
57
|
+
return issueEntries(issue).map((entry) => {
|
|
58
|
+
if (entry.path.length === 0) return entry.message;
|
|
59
|
+
const line = `${entry.message} at ${entry.path.join(".")}`;
|
|
60
|
+
if (entry.message !== UNKNOWN_KEY) return line;
|
|
61
|
+
const candidates = declared.get(pathKey(entry.path));
|
|
62
|
+
const input = entry.path[entry.path.length - 1];
|
|
63
|
+
if (candidates === void 0 || input === void 0) return line;
|
|
64
|
+
const near = Fmt.suggest(input, candidates);
|
|
65
|
+
if (near.length === 0) return line;
|
|
66
|
+
return `${line} (did you mean ${orList(near.map((key) => `"${Fmt.sanitize(key)}"`))}?)`;
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
/** A path as a map key: `JSON.stringify` keeps `["a.b"]` and `["a", "b"]` apart, which joining on `.` would not. */
|
|
70
|
+
const pathKey = (path) => JSON.stringify(path);
|
|
71
|
+
/** `"a"`, `"a" or "b"`, `"a", "b" or "c"`. */
|
|
72
|
+
const orList = (items) => items.length <= 1 ? items.join("") : `${items.slice(0, -1).join(", ")} or ${items[items.length - 1]}`;
|
|
73
|
+
/**
|
|
74
|
+
* The keys each unknown key's struct declares, by the unknown key's own path.
|
|
75
|
+
*
|
|
76
|
+
* @remarks
|
|
77
|
+
* Walks the tree the way core's formatter does (a `Pointer` extends the path; a `Filter`, `Encoding`, `Composite` or
|
|
78
|
+
* `AnyOf` passes it through), so a path here is the path the formatter gives the same leaf. A union reports one
|
|
79
|
+
* `UnexpectedKey` per branch at one path, and every branch's keys are candidates. Only string keys are offered: a
|
|
80
|
+
* symbol key is never something a person typed into a config file.
|
|
81
|
+
*/
|
|
82
|
+
const declaredKeys = (issue) => {
|
|
83
|
+
const found = /* @__PURE__ */ new Map();
|
|
84
|
+
if (!SchemaIssue.isIssue(issue)) return found;
|
|
85
|
+
const walk = (node, path) => {
|
|
86
|
+
switch (node._tag) {
|
|
87
|
+
case "Filter":
|
|
88
|
+
case "Encoding":
|
|
89
|
+
walk(node.issue, path);
|
|
90
|
+
return;
|
|
91
|
+
case "Pointer":
|
|
92
|
+
walk(node.issue, [...path, ...node.path.map(String)]);
|
|
93
|
+
return;
|
|
94
|
+
case "Composite":
|
|
95
|
+
case "AnyOf":
|
|
96
|
+
for (const child of node.issues) walk(child, path);
|
|
97
|
+
return;
|
|
98
|
+
case "UnexpectedKey": {
|
|
99
|
+
if (!SchemaAST.isObjects(node.ast)) return;
|
|
100
|
+
const key = pathKey(path);
|
|
101
|
+
const keys = found.get(key) ?? /* @__PURE__ */ new Set();
|
|
102
|
+
for (const signature of node.ast.propertySignatures) if (typeof signature.name === "string") keys.add(signature.name);
|
|
103
|
+
found.set(key, keys);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
default: return;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
walk(issue, []);
|
|
110
|
+
return found;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
42
113
|
* The rejected values of an issue tree, once each: what both the lines and the tree are built from.
|
|
43
114
|
*
|
|
44
115
|
* @remarks
|
|
@@ -101,4 +172,4 @@ const issueTreeChildren = (entries) => {
|
|
|
101
172
|
return [...leaves(root), ...[...root.children].map(([name, child]) => named(name, child))];
|
|
102
173
|
};
|
|
103
174
|
|
|
104
|
-
export { formatIssue, issueEntries, issueTreeChildren };
|
|
175
|
+
export { formatIssue, formatIssueWithHints, issueEntries, issueTreeChildren };
|
package/internal/logSafety.js
CHANGED
|
@@ -36,7 +36,7 @@ const sanitizeParts = (message) => {
|
|
|
36
36
|
};
|
|
37
37
|
/**
|
|
38
38
|
* Neutralize the `##[` the runner's legacy parser finds anywhere in a line, inside an NDJSON record, as the JSON escape
|
|
39
|
-
*
|
|
39
|
+
* `#\u0023[`: it decodes to the identical text, so the record loses nothing. A `##[` can only sit in a string there.
|
|
40
40
|
*
|
|
41
41
|
* @internal
|
|
42
42
|
*/
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Config, Effect, Layer, Option } from "effect";
|
|
2
|
+
import { TerminalEnv } from "@effected/env";
|
|
3
|
+
import { TerminalCapabilities } from "effect/cli";
|
|
4
|
+
|
|
5
|
+
// src/internal/terminalCapabilities.ts
|
|
6
|
+
/**
|
|
7
|
+
* Installs core's `TerminalCapabilities` from the ambient `TerminalEnv`, through `TerminalEnv.capabilities`, so core's
|
|
8
|
+
* own colour decision follows the kit's rule. `TERM` is read through `Config`, and a read that fails counts as unset.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* It lives here rather than in `@effected/env` because `effect/cli` exports the reference only through its barrel,
|
|
12
|
+
* and a value import there would load that barrel for every env consumer. `CliInteractive.layer`, built over it,
|
|
13
|
+
* then holds `canPrompt` at `true`.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
const capabilitiesLayer = Layer.effect(TerminalCapabilities.TerminalCapabilities, Effect.gen(function* () {
|
|
18
|
+
const terminal = yield* TerminalEnv;
|
|
19
|
+
const term = yield* Config.option(Config.String("TERM")).pipe(Effect.orElseSucceed(() => Option.none()));
|
|
20
|
+
return TerminalEnv.capabilities(terminal, Option.getOrUndefined(term));
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
export { capabilitiesLayer };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The presentation boundary of an effect/cli program: audience-aware output, a document IR and renderers, editor links, failure reports and logging, plus opt-in Ink screens, widgets and a live view",
|
|
6
6
|
"keywords": [
|
|
@@ -57,15 +57,15 @@
|
|
|
57
57
|
"./package.json": "./package.json"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@effected/commands": "^0.
|
|
61
|
-
"@effected/engine": "^0.
|
|
62
|
-
"@effected/github-commands": "^0.
|
|
60
|
+
"@effected/commands": "^0.18.0",
|
|
61
|
+
"@effected/engine": "^0.10.0",
|
|
62
|
+
"@effected/github-commands": "^0.7.0"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
|
-
"@effected/config-file": "^0.
|
|
66
|
-
"@effected/env": "^0.
|
|
67
|
-
"@effected/glob": "^0.
|
|
68
|
-
"@effected/walker": "^0.
|
|
65
|
+
"@effected/config-file": "^0.22.1",
|
|
66
|
+
"@effected/env": "^0.8.0",
|
|
67
|
+
"@effected/glob": "^0.16.0",
|
|
68
|
+
"@effected/walker": "^0.23.0",
|
|
69
69
|
"@types/react": "^19.3.0",
|
|
70
70
|
"effect": "^4.0.0",
|
|
71
71
|
"ink": "^8.0.0",
|