@effected/cli 0.23.0 → 0.24.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/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
- /** The format when the flag is not given: `auto` when it is offered, otherwise the first one. */
2372
- readonly default?: F | undefined;
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.Literals` with the kit's names, default and description; the print hands every document
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 its description
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 JSON, one `Console.log` (or `Console.error` for stderr), indented by `indent`.
2435
- * Under GitHub Actions a `##[` inside it is written as the JSON escape `#\u0023[`, which decodes to the same text,
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
- * Formatter or a `Console` provided inside `program` is not seen by
2858
- * `main`, so its help is not rerouted. To change the Formatter, pass
2859
- * `env.formatter` (for example `formatVersion`): `main` installs its own
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. Every other
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` and a `CliError.UserError` whose
3059
- * reported mark is `false`, so it would print twice. Fail with the marked
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)`) applies.
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
@@ -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
- 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
- });
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
- * 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.
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.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
- });
33
+ const routeHelpOnUsageError = (program) => Effect.provideService(program, CliOutput.Presenter, usageErrorsToStderr);
88
34
 
89
35
  export { routeHelpOnUsageError };
@@ -1,6 +1,9 @@
1
- import { SchemaIssue } from "effect";
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" ? "unknown key" : SchemaIssue.defaultLeafHook(issue) });
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 };
@@ -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
- * `##[`: it decodes to the identical text, so the record loses nothing. A `##[` can only sit in a string there.
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.23.0",
3
+ "version": "0.24.0",
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.17.0",
61
- "@effected/engine": "^0.9.0",
62
- "@effected/github-commands": "^0.6.2"
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.21.0",
66
- "@effected/env": "^0.6.0",
67
- "@effected/glob": "^0.15.0",
68
- "@effected/walker": "^0.21.0",
65
+ "@effected/config-file": "^0.22.0",
66
+ "@effected/env": "^0.7.0",
67
+ "@effected/glob": "^0.16.0",
68
+ "@effected/walker": "^0.22.0",
69
69
  "@types/react": "^19.3.0",
70
70
  "effect": "^4.0.0",
71
71
  "ink": "^8.0.0",