@effected/cli 0.11.0 → 0.13.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
@@ -493,7 +493,9 @@ export declare class Status<Names extends string> {
493
493
  * that draws it itself (an Ink tree, a reporter).
494
494
  *
495
495
  * @remarks
496
- * Throws on an unknown name, as {@link Status.def} does.
496
+ * Throws on an unknown name, as {@link Status.def} does. The glyph is sanitised, as text in a document is: escape
497
+ * sequences and control characters in a vocabulary's glyph are removed, so a glyph built from data cannot paint the
498
+ * terminal, plant a hyperlink or move the cursor. Every kit path that draws a status glyph takes it from here.
497
499
  *
498
500
  * @param name - a name in this vocabulary
499
501
  * @param glyphs - the glyph set, such as `Glyphs.unicode`, `Glyphs.ascii` or a theme's
@@ -567,7 +569,10 @@ interface StreamTheme {
567
569
  readonly glyphs: GlyphSet;
568
570
  /** The colour level of the stream. */
569
571
  readonly color: ColorLevel;
570
- /** Render a status from a vocabulary: its glyph, painted with its token, then `text` when given. */
572
+ /**
573
+ * Render a status from a vocabulary: its glyph, sanitised (see `Status.glyph`) and painted with its token, then
574
+ * `text` when given, as it is: sanitising `text` is the caller's, as `CliMessage` and `CliLog.status` do.
575
+ */
571
576
  readonly status: <N extends string>(vocab: Status<N>, name: N, text?: string) => string;
572
577
  }
573
578
  /**
@@ -617,6 +622,30 @@ export declare class CliTheme extends CliTheme_base {
617
622
  * @param options - token overrides and the glyph set
618
623
  */
619
624
  static readonly layer: (options?: CliThemeOptions) => Layer.Layer<CliTheme, never, TerminalEnv>;
625
+ /**
626
+ * The theme an audience sees of `theme`: for an agent, the same theme at colour `none` (`paint` the identity, `sgr`
627
+ * empty, `status` unpainted), whatever the terminal could do, because an agent never gets an escape of any kind; for
628
+ * anyone else, or when the audience is not known, `theme` itself.
629
+ *
630
+ * @remarks
631
+ * The one rule the kit applies wherever it paints for an audience: `Render.context` takes its colour and `paint`
632
+ * from it, `CliMessage` and `CliLog.status` paint their glyphs through it, and `./ui` gives it to the trees it
633
+ * mounts, so `useTheme`, `Styled` and the widgets' colour-`none` text markers all agree. A program that paints its
634
+ * own lines applies the same rule with it rather than re-implementing it:
635
+ *
636
+ * ```ts
637
+ * const line = Effect.gen(function* () {
638
+ * const theme = CliTheme.forAudience((yield* CliTheme).forStream("stdout"), (yield* Audience).kind)
639
+ * return theme.status(Status.core, "success", Fmt.sanitize(name))
640
+ * })
641
+ * ```
642
+ *
643
+ * Pure: it reads nothing, so the audience is the caller's to pass, `undefined` when it is not known.
644
+ *
645
+ * @param theme - a stream's theme, such as `CliTheme.forStream("stdout")`
646
+ * @param audience - who the output is for, or `undefined` when that is not known
647
+ */
648
+ static readonly forAudience: (theme: StreamTheme, audience: AudienceKind | undefined) => StreamTheme;
620
649
  /**
621
650
  * A fixed theme that needs nothing; `none` colour and Unicode glyphs unless told otherwise.
622
651
  *
@@ -738,8 +767,16 @@ interface Column {
738
767
  interface Counter {
739
768
  /** A stable identifier, for a caller's total rule. */
740
769
  readonly key: string;
741
- /** What the counter is called when shown. */
742
- readonly label: string;
770
+ /**
771
+ * What the counter is called when shown: one label, or a singular and a plural form, `one` for a count of exactly 1
772
+ * and `other` for any other, 0 included. The count is the counter's own `n`, except in a share headline
773
+ * (`1/3 repos`), which reads by the total. A `CountsTable` heads its column with `other`, since the column holds
774
+ * every row's count.
775
+ */
776
+ readonly label: string | {
777
+ readonly one: string;
778
+ readonly other: string;
779
+ };
743
780
  /** The count. */
744
781
  readonly n: number;
745
782
  /** The status the count is painted with. */
@@ -767,7 +804,7 @@ interface Counter {
767
804
  * - `CountsTable`: a table of `Counts` rows, a column per counter key, a `duration` column when some row has one, and
768
805
  * an optional summed total row.
769
806
  * - `Lines`: one line per entry; markdown keeps them apart with hard breaks.
770
- * - `Line`: one line, which `truncate` cuts to the width instead of wrapping.
807
+ * - `Line`: one line, which `truncate` cuts to the width instead of wrapping, and `wrap: false` keeps whole.
771
808
  * - `DiffText`: a unified diff, as given; `truncate` cuts each line to the width.
772
809
  * - A `List` may be `compact`, with no blank lines between an item's children (a blank line of an item's own content
773
810
  * keeps the item's indent in plain and `ansi`), and a `Table` may be `style: "pipe"`.
@@ -846,6 +883,7 @@ type Block = {
846
883
  readonly _tag: "Line";
847
884
  readonly content: ReadonlyArray<Inline>;
848
885
  readonly truncate?: boolean;
886
+ readonly wrap?: boolean;
849
887
  } | {
850
888
  readonly _tag: "DiffText";
851
889
  readonly text: string;
@@ -1208,6 +1246,11 @@ export declare class Doc {
1208
1246
  /**
1209
1247
  * Children under an optional title.
1210
1248
  *
1249
+ * @remarks
1250
+ * The children are separated by blank lines (unless the document is compact); a title sits directly above the first.
1251
+ * `Doc.section(undefined, blocks)` is the way to space a document's top-level blocks, which are otherwise joined with
1252
+ * no blank line.
1253
+ *
1211
1254
  * @param title - the title, or `undefined` for none
1212
1255
  * @param children - the blocks
1213
1256
  */
@@ -1218,13 +1261,22 @@ export declare class Doc {
1218
1261
  * @remarks
1219
1262
  * A name the vocabulary does not have is a compile error.
1220
1263
  *
1264
+ * The label is one string, or `{ one, other }` to pluralise by count: `one` when the count is exactly 1 and `other`
1265
+ * for every other count, 0 included. A count standing alone reads by its own `n` (`1 change`, `2 changes`); a
1266
+ * headline shown as a share of the total reads by that total, the noun it counts (`1/1 repo`, `1/3 repos`,
1267
+ * `2/3 repos`).
1268
+ *
1221
1269
  * @param vocab - the vocabulary the status belongs to
1222
1270
  * @param name - a status name in it
1223
- * @param options - the counter's `key`, `label` and count `n`, and `showZero` to keep it when `n` is zero
1271
+ * @param options - the counter's `key`, its `label` (one string, or `{ one, other }`), its count `n`, and `showZero`
1272
+ * to keep it when `n` is zero
1224
1273
  */
1225
1274
  static counter<N extends string>(vocab: Status<N>, name: NoInfer<N>, options: {
1226
1275
  readonly key: string;
1227
- readonly label: string;
1276
+ readonly label: string | {
1277
+ readonly one: string;
1278
+ readonly other: string;
1279
+ };
1228
1280
  readonly n: number;
1229
1281
  readonly showZero?: boolean;
1230
1282
  }): Counter;
@@ -1264,17 +1316,22 @@ export declare class Doc {
1264
1316
  */
1265
1317
  static lines(lines: ReadonlyArray<InlineInput>): BlockOf<"Lines">;
1266
1318
  /**
1267
- * One line of content; with `truncate`, it is cut to the width with the glyph set's ellipsis instead of wrapping.
1319
+ * One line of content; with `truncate`, it is cut to the width with the glyph set's ellipsis instead of wrapping,
1320
+ * and with `wrap: false` it is kept whole on one line whatever the width.
1268
1321
  *
1269
1322
  * @remarks
1270
- * Without `truncate` a line longer than the width wraps. For a single line that must never wrap nor be cut, such
1271
- * as a test's full name used as a title, use {@link Doc.verbatim}.
1323
+ * By default a line longer than the width wraps. `wrap: false` keeps it atomic in every audience and renderer, still
1324
+ * carrying its status glyphs, theme tokens and links, which {@link Doc.verbatim} (a plain string) cannot: the tool for
1325
+ * a finding such as `✗ path:line:col rule message` that a reader greps or reads line by line, while the prose around
1326
+ * it still wraps. A line break inside it is still a space. With both `truncate` and `wrap: false`, `truncate` wins:
1327
+ * the line is cut to the width.
1272
1328
  *
1273
1329
  * @param content - the line
1274
- * @param options - `truncate`
1330
+ * @param options - `truncate`, to cut it to the width; `wrap: false`, to keep it whole
1275
1331
  */
1276
1332
  static line(content: InlineInput, options?: {
1277
1333
  readonly truncate?: boolean;
1334
+ readonly wrap?: boolean;
1278
1335
  }): BlockOf<"Line">;
1279
1336
  /**
1280
1337
  * A unified diff as given, such as a test runner's: sanitized, its `+` and `-` lines painted `success` and
@@ -1345,11 +1402,15 @@ export declare class Doc {
1345
1402
  * The context is {@link Render.context} for the stream, so the width, the colour, the links and the audience
1346
1403
  * come from the services the program already has, and the text is written with `Console.log` or
1347
1404
  * `Console.error`: a test captures it by swapping the `Console`. With `format: "auto"` the renderer follows
1348
- * the audience, and the width is unbounded for an agent and a CI.
1405
+ * the audience, and the width is unbounded for an agent, a CI, and a human whose stream is not a terminal.
1349
1406
  *
1350
1407
  * An agent is never written an escape of any kind, even with an explicit `format: "ansi"`: its context is
1351
1408
  * colourless and its links are off. A document that renders to nothing prints nothing.
1352
1409
  *
1410
+ * The whole document is written as one `Console.log` (or `Console.error`) call, with its line breaks embedded, so a
1411
+ * captured `Console` holds one entry per document, not one per line. Top-level blocks are joined with no blank
1412
+ * line between them; wrap them in `Doc.section(undefined, [...])` to space them.
1413
+ *
1353
1414
  * `CurrentRuntimeEnv` is read if the environment has one and is not required: a `ci` audience prints
1354
1415
  * GitHub's log format only when it says GitHub Actions, and plain text otherwise, including when it is
1355
1416
  * absent. An explicit `format` is honoured whatever the audience.
@@ -1565,6 +1626,24 @@ export declare class CliLogger {
1565
1626
  }
1566
1627
  //#endregion
1567
1628
  //#region src/CliLog.d.ts
1629
+ /**
1630
+ * Options for `CliLog.status`.
1631
+ *
1632
+ * @public
1633
+ */
1634
+ interface CliLogStatusOptions {
1635
+ /**
1636
+ * The level the line is logged at. By default it follows the status's rank in its vocabulary: `Error` at or above
1637
+ * `failure`'s rank, `Warn` at or above `warning`'s, and `Info` below that.
1638
+ */
1639
+ readonly level?: LogLevel.Severity | undefined;
1640
+ /**
1641
+ * What the line starts with, before the glyph, so an indented report line keeps its place in a block: a number of
1642
+ * spaces (floored, at most 64; zero, a negative or a non-finite number is none), or a string. A string is sanitised as text is (escapes and controls removed, a tab a space) and its line
1643
+ * breaks dropped, so the indent can never carry an escape onto the trusted line. None by default.
1644
+ */
1645
+ readonly indent?: number | string | undefined;
1646
+ }
1568
1647
  /**
1569
1648
  * Options for `CliLog.layer`.
1570
1649
  *
@@ -1861,6 +1940,41 @@ export declare class CliLog {
1861
1940
  * @param options - the level, the env var, the format, the `CliLogger` options and the file sink
1862
1941
  */
1863
1942
  static layer(options: CliLogFileOptions): Layer.Layer<never, never, Audience | TerminalEnv | FileSystem.FileSystem | Path.Path>;
1943
+ /**
1944
+ * Log a status line: its glyph painted through the theme, then `text`, at a level that follows the status.
1945
+ *
1946
+ * @remarks
1947
+ * The line goes through the logger, so it is a diagnostic like any `Effect.log*` call: filtered by the level in
1948
+ * force (`--log-level`, `CliLog.Level`), routed by `CliLogger`'s `stderrFrom` (stderr by default) and neutralized
1949
+ * under GitHub Actions. What differs is the glyph: the logger sanitises every line a program logs, which strips a
1950
+ * colour a program painted itself, so a glyph on the log channel was always drawn bare. Here the kit paints it and
1951
+ * marks the line as its own, so the plain `CliLogger` line keeps the colour, while `text` is still sanitised:
1952
+ * escape sequences and control characters in it are removed, as in every line the kit writes.
1953
+ *
1954
+ * The glyph is painted with stderr's theme (where diagnostics go) through {@link CliTheme.forAudience}, so an agent
1955
+ * gets it unpainted, and an `Audience` is read only when provided, so it stays out of the requirements. ASCII glyphs
1956
+ * give the status's ASCII form. A diagnostics record carries the line as its message as any record does: the `CliLog`
1957
+ * sink's pretty line sanitises it (the glyph is drawn bare there), and NDJSON keeps it, JSON-escaped.
1958
+ *
1959
+ * The level defaults to the status's rank in `vocab`: `Error` at or above `failure`'s, `Warn` at or above
1960
+ * `warning`'s, `Info` below, so a custom status follows its own rank. Pass `level` to choose it, and `indent` (a
1961
+ * number of spaces, or a string, sanitised) to start the line inside an indented block: ` ✗ error x: red`.
1962
+ *
1963
+ * @example
1964
+ * ```ts
1965
+ * import { CliLog, Status } from "@effected/cli"
1966
+ *
1967
+ * // ✗ in the failure colour, then the message: on stderr, filtered by the log level.
1968
+ * const reportError = (resource: string, message: string) =>
1969
+ * CliLog.status(Status.core, "failure", `${resource}: ${message}`)
1970
+ * ```
1971
+ *
1972
+ * @param vocab - the vocabulary the status belongs to
1973
+ * @param name - the status
1974
+ * @param text - the text after the glyph, sanitised
1975
+ * @param options - the level to log at, and the indent before the glyph
1976
+ */
1977
+ static readonly status: <N extends string>(vocab: Status<N>, name: N, text: string, options?: CliLogStatusOptions) => Effect.Effect<void, never, CliTheme>;
1864
1978
  /**
1865
1979
  * Mark the log records an effect emits as coming from `name`.
1866
1980
  *
@@ -1935,6 +2049,27 @@ interface CliEnvOptions {
1935
2049
  * `CliRuntime.main` reads this.
1936
2050
  */
1937
2051
  readonly stackFrames?: "app" | "all" | undefined;
2052
+ /**
2053
+ * Which spans the default failure report's `in: outer › inner` trail names: `app`, the default, leaves out the spans
2054
+ * the kit's own packages and Effect define (judged by the file of each span's definition site under `node_modules`),
2055
+ * `all` shows every span, and `off` drops the trail. Applies to the report `main` writes,
2056
+ * `FailureDetails.defaultLines` and `FailureDetails.lines`. Only `CliRuntime.main` reads this.
2057
+ */
2058
+ readonly spans?: "app" | "all" | "off" | undefined;
2059
+ /**
2060
+ * The environment variable that sets `spans` at run time (`app`, `all` or `off`, case-insensitive), read through
2061
+ * `Config`, as `log.envVar` sets the log level: so a user filing a bug can run `TOOL_SPANS=all tool …` without a
2062
+ * rebuild. `spans` beats it (the variable is then not read at all); unset or empty is the default; a value that is
2063
+ * not a setting is ignored with one warning. Not read unless named. Only `CliRuntime.main` reads this.
2064
+ */
2065
+ readonly spansEnvVar?: string | undefined;
2066
+ /**
2067
+ * A module of the running program itself, as a `file:` URL or an absolute path: pass the bin's `import.meta.url`.
2068
+ * `spans: "app"` keeps the spans of the package that holds it, even when it is installed under
2069
+ * `node_modules/@effected/` (a kit companion's bin); see `CliFailureOptions.appModule`. Only `CliRuntime.main`
2070
+ * reads this.
2071
+ */
2072
+ readonly appModule?: string | undefined;
1938
2073
  /** Whether file links open in an editor; `auto` by default. See {@link CliLinks}. */
1939
2074
  readonly editorLinks?: EditorLinks | undefined;
1940
2075
  /** The environment variable that overrides `editorLinks`, read through `Config`. Not read unless named. */
@@ -2137,6 +2272,32 @@ interface CliFailureOptions {
2137
2272
  * shows every frame, unfiltered.
2138
2273
  */
2139
2274
  readonly stackFrames?: "app" | "all" | undefined;
2275
+ /**
2276
+ * Which spans the `in: outer › inner` trail after a failure names. `app`, the default, leaves out the spans the kit
2277
+ * and Effect define themselves: a span whose definition site (an `Effect.fn`'s, or where the span was opened) is a
2278
+ * file under `node_modules/@effected/` or `node_modules/effect/`. A config that fails to decode under
2279
+ * `@effected/config-file` then reports no `ConfigFile.loadFrom` trail, while the program's own spans stay. `all`
2280
+ * shows every span, and `off` drops the trail. Under either, an `Effect.fn` call and its definition are one entry,
2281
+ * `name`, never `name (definition) › name`.
2282
+ *
2283
+ * @remarks
2284
+ * The rule reads file paths, never span names, and it fails open: when it cannot tell, it shows the span. A span
2285
+ * with no captured stack is kept. A kit package linked into a workspace (`link:`, `workspace:`) runs from its own
2286
+ * checkout rather than from `node_modules`, so its spans show as the program's. A bundled program is one file, so
2287
+ * `app` shows what `all` does, unless the bundle is itself installed under `node_modules/@effected/`. A program that
2288
+ * is installed under `node_modules/@effected/` (a kit companion's bin) names itself with `appModule`, or its own
2289
+ * spans are left out with the kit's.
2290
+ */
2291
+ readonly spans?: "app" | "all" | "off" | undefined;
2292
+ /**
2293
+ * A module of the running program itself, as a `file:` URL or an absolute path: its bin's `import.meta.url`. With
2294
+ * `spans: "app"`, a span defined in the installed package that holds this module is the program's, kept even under
2295
+ * `node_modules/@effected/`, while the packages it depends on (beside it, or nested in its own `node_modules`) are
2296
+ * still left out. The package is the path through the last `node_modules/<name>` (or `node_modules/@scope/name`)
2297
+ * in it, read from the path alone with no file system. A module not under `node_modules` changes nothing: the rule
2298
+ * already keeps its spans. Unset by default.
2299
+ */
2300
+ readonly appModule?: string | undefined;
2140
2301
  }
2141
2302
  /**
2142
2303
  * A failure as a document: what the default report prints, and a building block for a custom one.
@@ -2155,7 +2316,7 @@ interface CliFailureOptions {
2155
2316
  * `Error.cause` chain as a tree. When cleaning leaves no frame the stack says
2156
2317
  * `no user frames (N internal frames hidden)`, never an empty block; when frames survive and some were left out, the
2157
2318
  * count follows them as `(+N internal frames hidden)`. A reason that ran under spans is followed by
2158
- * `in: outer › inner`. Interrupts are not rendered beside a real failure, and a cause with only interrupts is the
2319
+ * `in: outer › inner`: by default only the program's own spans, the kit's and Effect's left out (`spans`). Interrupts are not rendered beside a real failure, and a cause with only interrupts is the
2159
2320
  * one line `interrupted`.
2160
2321
  *
2161
2322
  * All text goes through the document, so a control character in a message or a stack frame never reaches the terminal.
@@ -2257,8 +2418,8 @@ interface CliMessageOptions {
2257
2418
  *
2258
2419
  * The text is whatever the caller supplies, so it is sanitised: escape sequences and control characters are removed
2259
2420
  * (a line break is kept as one, a tab becomes a space), as in a document. Under GitHub Actions, where
2260
- * `CurrentRuntimeEnv` says so, a line the runner would read as a workflow command is neutralized as well. The glyphs
2261
- * come from the vocabulary, which is configuration, and are not.
2421
+ * `CurrentRuntimeEnv` says so, a line the runner would read as a workflow command is neutralized as well. The glyph
2422
+ * comes from the vocabulary, sanitised too (`Status.glyph`), so a glyph built from data cannot inject an escape either.
2262
2423
  *
2263
2424
  * @public
2264
2425
  */
@@ -2407,6 +2568,24 @@ interface FailureDetails {
2407
2568
  * failure from the error channel.
2408
2569
  */
2409
2570
  readonly isDefect: boolean;
2571
+ /**
2572
+ * `true` when the squashed `error` is the kit's {@link Cancelled}: the person quit a prompt or screen (Esc, Ctrl-C).
2573
+ * It is not a bug and there is nothing to report, whichever channel it arrived through.
2574
+ *
2575
+ * @remarks
2576
+ * A cancel from `CliPrompt.fallback` is a defect (`isDefect` is `true`) and one from `CliUi.run` is a typed failure
2577
+ * (`isDefect` is `false`), so a `render` that gives a defect the "please report this" treatment must test this flag
2578
+ * first, or Esc prints an issue-report request. `isDefect` keeps its plain meaning, the absence of a typed failure,
2579
+ * and the exit code is unchanged (`130` for the `interrupt` reason). `defaultLines` and `lines()` already draw it as
2580
+ * its one fixed line.
2581
+ */
2582
+ readonly isCancelled: boolean;
2583
+ /**
2584
+ * `true` when the squashed `error` is the kit's {@link NotInteractive}: the program asked for a prompt or a screen in
2585
+ * a run that cannot show one (a pipe, an agent, a CI). It is a usage problem, not a bug, whichever channel it
2586
+ * arrived through; `isDefect` is as for {@link FailureDetails.isCancelled}.
2587
+ */
2588
+ readonly isNotInteractive: boolean;
2410
2589
  /**
2411
2590
  * The report the kit writes for this failure when there is no `render`: for this run, in this audience, with its
2412
2591
  * colour, links and `displayPath`. A `render` that hands a failure back returns these lines unchanged, and the
@@ -2429,10 +2608,14 @@ interface FailureDetails {
2429
2608
  * details.lines({ status: false }).map((line, i) => (i === 0 ? `prog: ${line}` : line))
2430
2609
  * ```
2431
2610
  *
2432
- * @param options - `status: false` leaves off the leading status
2611
+ * `spans` chooses the `in:` trail for these lines alone, as `CliFailureOptions.spans` does (`app`, `all` or
2612
+ * `off`); without it the run's own setting (`env.spans`, `app` by default) applies.
2613
+ *
2614
+ * @param options - `status: false` leaves off the leading status; `spans` chooses the span trail
2433
2615
  */
2434
2616
  readonly lines: (options?: {
2435
2617
  readonly status?: boolean | undefined;
2618
+ readonly spans?: "app" | "all" | "off" | undefined;
2436
2619
  }) => ReadonlyArray<string>;
2437
2620
  }
2438
2621
  /**
@@ -2522,9 +2705,11 @@ interface MainOptions<RP, EP> extends ReportFailuresOptions {
2522
2705
  * one that writes output must use `Console` or `Stdio`, never `Terminal`.
2523
2706
  *
2524
2707
  * Stderr's colour mirrors stdout's terminal check unless `env.stderrIsTerminal` says otherwise, so with stderr
2525
- * redirected and stdout a terminal the failure report is painted into the file. On Node, pass the real check:
2526
- * `env: { stderrIsTerminal: Effect.sync(() => process.stderr.isTTY === true) }` (core's `Stdio` reports only
2527
- * stdout; upstream Effect-TS/effect#8639).
2708
+ * redirected and stdout a terminal the failure report is painted into the file. On Node, pass the real check from
2709
+ * the bin's entry, the one place it reads the host:
2710
+ * `env: { stderrIsTerminal: Effect.sync(() => process.stderr.isTTY === true) }`. Core's `Stdio` reports only
2711
+ * stdout (upstream Effect-TS/effect#8639); once core has a stderr check, this option reads it and the bin passes
2712
+ * nothing.
2528
2713
  */
2529
2714
  readonly env?: CliEnvOptions | undefined;
2530
2715
  /**
@@ -2541,8 +2726,12 @@ interface MainOptions<RP, EP> extends ReportFailuresOptions {
2541
2726
  *
2542
2727
  * Two cases keep help on stdout even under `"stderr"`. A `CliOutput`
2543
2728
  * Formatter or a `Console` provided inside `program` is not seen by
2544
- * `main`, so its help is not rerouted; provide the Formatter through
2545
- * `platform` instead. And with `Command.runWith`'s `renderErrors: false`
2729
+ * `main`, so its help is not rerouted. To change the Formatter, pass
2730
+ * `env.formatter` (for example `formatVersion`): `main` installs its own
2731
+ * Formatter inside the platform, so that is the way in, and a Formatter
2732
+ * provided inside the program is invisible to the routing. A Formatter
2733
+ * the platform provides is shadowed by `main`'s, so pass it through
2734
+ * `env.formatter` as well. And with `Command.runWith`'s `renderErrors: false`
2546
2735
  * no errors are printed, so nothing marks the help as a usage error's.
2547
2736
  */
2548
2737
  readonly helpOnUsageError?: "stdout" | "stderr" | undefined;
@@ -3007,8 +3196,9 @@ interface RenderContext {
3007
3196
  *
3008
3197
  * @remarks
3009
3198
  * `Infinity` is no limit. A renderer clamps what it is given: zero or a negative width is 1, and `NaN` is 80.
3010
- * `Render.context` gives a human `TerminalEnv.width()`, which is the terminal's columns as stdout reports them, even
3011
- * for a context built for `"stderr"` (core's `Terminal` has one width); pass `width` to override it.
3199
+ * `Render.context` gives a human writing to a terminal `TerminalEnv.width()`, which is the terminal's columns as
3200
+ * stdout reports them, even for a context built for `"stderr"` (core's `Terminal` has one width), and gives no limit
3201
+ * when the stream is not a terminal; pass `width` to override it.
3012
3202
  */
3013
3203
  readonly width: number;
3014
3204
  /** Who the output is for. */
@@ -3088,8 +3278,9 @@ interface RenderContextOfOptions {
3088
3278
  */
3089
3279
  interface RenderContextOptions {
3090
3280
  /**
3091
- * The display columns to lay out at. By default a human gets `TerminalEnv.width()`, the terminal's columns as
3092
- * stdout reports them even when the stream is `"stderr"`, and an agent or a CI gets no limit at all.
3281
+ * The display columns to lay out at. By default a human writing to a terminal gets `TerminalEnv.width()`, the
3282
+ * terminal's columns as stdout reports them even when the stream is `"stderr"`; a human whose stream is not a
3283
+ * terminal (a pipe, a file), an agent and a CI get no limit at all.
3093
3284
  */
3094
3285
  readonly width?: number | undefined;
3095
3286
  /** Turns an absolute path into its display form, for example relative to the working directory; the identity by default. */
@@ -3131,8 +3322,9 @@ export declare class Render {
3131
3322
  * gets an escape and a terminal without OSC 8 gets the label;
3132
3323
  * - `neutralizeWorkflowCommands` is set when `CurrentRuntimeEnv` says GitHub Actions (read if present, not
3133
3324
  * required), for every audience, since the runner reads whatever is written there;
3134
- * - `width` is the option, else `TerminalEnv.width()` for a human, and **unbounded** (`Infinity`) for an agent
3135
- * or a CI, so nothing a reader needs is truncated or wrapped for a terminal that is not there.
3325
+ * - `width` is the option, else `TerminalEnv.width()` for a human whose stream is a terminal, and **unbounded**
3326
+ * (`Infinity`) for a human whose stream is not one (`tool | grep`, `tool > out.txt`), an agent or a CI, so nothing
3327
+ * a reader needs is truncated or wrapped for a terminal that is not there: a pipe has no width to honour.
3136
3328
  *
3137
3329
  * @param stream - the stream the output is for
3138
3330
  * @param options - an explicit width and a path display function
@@ -3335,5 +3527,5 @@ export declare class SchemaIssueRenderer {
3335
3527
  static readonly render: (issue: unknown) => ReadonlyArray<string>;
3336
3528
  }
3337
3529
  //#endregion
3338
- export type { AnnotationLevel, AnnotationOptions, AudienceFlagInput, Block, BlockOf, CliAudienceFlagsOptions, CliDocSource, CliEnvOptions, CliEnvServices, CliEnvTestOptions, CliEnvTestServices, CliExitShape, CliFailureOptions, CliLinksLinkerOptions, CliLinksOptions, CliLinksShape, CliLogFile, CliLogFileOptions, CliLogOptions, 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, PercentOptions, RenderContext, RenderContextOfOptions, RenderContextOptions, ReportFailuresOptions, RequiresAudienceFlags, StatusDef, StatusRef, StreamTheme, Style, TableOptions, TokenName, TreeInput, TreeNode, TruncateOptions };
3530
+ export type { AnnotationLevel, AnnotationOptions, AudienceFlagInput, Block, BlockOf, CliAudienceFlagsOptions, CliDocSource, CliEnvOptions, CliEnvServices, CliEnvTestOptions, CliEnvTestServices, CliExitShape, CliFailureOptions, 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, PercentOptions, RenderContext, RenderContextOfOptions, RenderContextOptions, ReportFailuresOptions, RequiresAudienceFlags, StatusDef, StatusRef, StreamTheme, Style, TableOptions, TokenName, TreeInput, TreeNode, TruncateOptions };
3339
3531
  //# sourceMappingURL=index.d.ts.map
@@ -2,6 +2,21 @@ import { Fmt } from "../Fmt.js";
2
2
 
3
3
  //#region src/internal/counts.ts
4
4
  /**
5
+ * The label a counter shows for a count: its one label, or `one` when the count is exactly 1 and `other` otherwise.
6
+ * The count is the counter's own `n`, except in a share headline (`1/3 repos`), which reads by the denominator, the
7
+ * total, and passes it.
8
+ *
9
+ * @internal
10
+ */
11
+ const counterLabel = (counter, count = counter.n) => typeof counter.label === "string" ? counter.label : count === 1 ? counter.label.one : counter.label.other;
12
+ /**
13
+ * The label a counter's column is headed with in a `CountsTable`: its one label, or its plural form, since a column
14
+ * holds every row's count.
15
+ *
16
+ * @internal
17
+ */
18
+ const columnLabel = (counter) => typeof counter.label === "string" ? counter.label : counter.label.other;
19
+ /**
5
20
  * The total of a `Counts` block: the caller's rule when it has one, otherwise the sum of `n` over every counter.
6
21
  *
7
22
  * @remarks
@@ -58,7 +73,7 @@ const countsTableOf = (block) => {
58
73
  _tag: "Table",
59
74
  columns: [
60
75
  { header: block.labelHeader ?? [] },
61
- ...keys.map((key) => ({ header: [text(key.label)] })),
76
+ ...keys.map((key) => ({ header: [text(columnLabel(key))] })),
62
77
  ...timed ? [{ header: block.durationHeader ?? [text("duration")] }] : []
63
78
  ],
64
79
  rows: [...rows, ...total]
@@ -66,4 +81,4 @@ const countsTableOf = (block) => {
66
81
  };
67
82
 
68
83
  //#endregion
69
- export { countsTableOf, totalOf, visibleCountersOf };
84
+ export { columnLabel, counterLabel, countsTableOf, totalOf, visibleCountersOf };
@@ -5,7 +5,7 @@ import { Glyphs } from "../Glyphs.js";
5
5
  import { CliTheme } from "../CliTheme.js";
6
6
  import { Render } from "../Render.js";
7
7
  import { CliFailure } from "../CliFailure.js";
8
- import { Context, Effect, MutableRef, Option } from "effect";
8
+ import { Config, Context, Effect, MutableRef, Option } from "effect";
9
9
  import { Audience, TerminalEnv } from "@effected/env";
10
10
  import { CommandNeutralizer } from "@effected/github-commands";
11
11
 
@@ -52,16 +52,14 @@ const build = (audience, settings = {}) => Effect.gen(function* () {
52
52
  if (Option.isNone(theme) || Option.isNone(terminal) || Option.isNone(links)) return void 0;
53
53
  const shape = audience ?? (Option.isSome(current) ? current.value : void 0);
54
54
  if (shape === void 0) return void 0;
55
- const { displayPath, stackFrames } = settings;
55
+ const { displayPath, stackFrames, spans, appModule } = settings;
56
56
  const ctx = yield* Render.context("stderr", displayPath === void 0 ? void 0 : { displayPath }).pipe(Effect.provideService(CliTheme, theme.value), Effect.provideService(TerminalEnv, terminal.value), Effect.provideService(CliLinks, links.value), Effect.provideService(Audience, shape));
57
- const format = yield* autoFormat(ctx.audience);
58
- return stackFrames === void 0 ? {
57
+ return {
59
58
  ctx,
60
- format
61
- } : {
62
- ctx,
63
- format,
64
- stackFrames
59
+ format: yield* autoFormat(ctx.audience),
60
+ ...stackFrames === void 0 ? {} : { stackFrames },
61
+ ...spans === void 0 ? {} : { spans },
62
+ ...appModule === void 0 ? {} : { appModule }
65
63
  };
66
64
  });
67
65
  /**
@@ -75,7 +73,9 @@ const refreshFailureTarget = (audience, settings) => Effect.gen(function* () {
75
73
  const recorded = MutableRef.get(cell);
76
74
  const target = yield* build(audience, settings ?? {
77
75
  displayPath: recorded?.ctx.displayPath,
78
- stackFrames: recorded?.stackFrames
76
+ stackFrames: recorded?.stackFrames,
77
+ spans: recorded?.spans,
78
+ appModule: recorded?.appModule
79
79
  });
80
80
  if (target !== void 0) MutableRef.set(cell, target);
81
81
  });
@@ -119,10 +119,12 @@ const withoutStatus = (doc) => doc.map((block) => {
119
119
  *
120
120
  * @internal
121
121
  */
122
- const linesOf = (cause, target, status = true) => {
122
+ const linesOf = (cause, target, status = true, spans = target.spans) => {
123
123
  const full = CliFailure.toDoc(cause, {
124
124
  displayPath: target.ctx.displayPath,
125
- ...target.stackFrames === void 0 ? {} : { stackFrames: target.stackFrames }
125
+ ...target.stackFrames === void 0 ? {} : { stackFrames: target.stackFrames },
126
+ ...spans === void 0 ? {} : { spans },
127
+ ...target.appModule === void 0 ? {} : { appModule: target.appModule }
126
128
  });
127
129
  const doc = status ? full : withoutStatus(full);
128
130
  const text = Render[target.format](doc, target.ctx);
@@ -150,7 +152,44 @@ const guardConsumerLines = (lines) => Effect.map(currentTarget, (target) => {
150
152
  *
151
153
  * @internal
152
154
  */
153
- const plainFailureLines = (cause, status = true) => linesOf(cause, fallbackTarget, status);
155
+ const plainFailureLines = (cause, status = true, spans) => linesOf(cause, fallbackTarget, status, spans);
156
+ const SPAN_SETTINGS = [
157
+ "app",
158
+ "all",
159
+ "off"
160
+ ];
161
+ /**
162
+ * The span trail setting, as `CliLog`'s level is read: the explicit `spans` when given (the variable is then not read
163
+ * at all), else the variable named `envVar` through `Config`, case-insensitive, unset or empty meaning the default.
164
+ * A value that is not a setting is ignored, with the warning to log.
165
+ *
166
+ * @internal
167
+ */
168
+ const readSpans = (explicit, envVar) => Effect.gen(function* () {
169
+ if (explicit !== void 0) return {
170
+ spans: explicit,
171
+ invalid: void 0
172
+ };
173
+ if (envVar === void 0) return {
174
+ spans: void 0,
175
+ invalid: void 0
176
+ };
177
+ const raw = yield* Config.option(Config.String(envVar)).pipe(Effect.orElseSucceed(() => Option.none()));
178
+ if (Option.isNone(raw) || raw.value === "") return {
179
+ spans: void 0,
180
+ invalid: void 0
181
+ };
182
+ const value = raw.value.toLowerCase();
183
+ const spans = SPAN_SETTINGS.find((setting) => setting === value);
184
+ if (spans !== void 0) return {
185
+ spans,
186
+ invalid: void 0
187
+ };
188
+ return {
189
+ spans: void 0,
190
+ invalid: `${envVar}=${raw.value} is not a span setting (${SPAN_SETTINGS.join("|")}); ignoring it`
191
+ };
192
+ });
154
193
 
155
194
  //#endregion
156
- export { FailureTargetCell, currentTarget, fallbackTarget, guardConsumerLines, linesOf, plainFailureLines, refreshFailureTarget };
195
+ export { FailureTargetCell, currentTarget, fallbackTarget, guardConsumerLines, linesOf, plainFailureLines, readSpans, refreshFailureTarget };
@@ -1,5 +1,5 @@
1
1
  import { Fmt, sanitize } from "../Fmt.js";
2
- import { countsTableOf, totalOf, visibleCountersOf } from "./counts.js";
2
+ import { counterLabel, countsTableOf, totalOf, visibleCountersOf } from "./counts.js";
3
3
  import { truncateSpans, widthOf, wrapSpans } from "./layout.js";
4
4
 
5
5
  //#region src/internal/renderDoc.ts
@@ -190,10 +190,10 @@ const countsLayout = (walk, block) => {
190
190
  const qualifier = block.qualifier === void 0 ? [] : toned(oneLine(inline(walk, block.qualifier)), "muted");
191
191
  const duration = block.durationMs === void 0 ? "" : Fmt.duration(block.durationMs);
192
192
  const suffix = block.suffix === void 0 ? [] : toned(oneLine(inline(walk, block.suffix)), "muted");
193
- const nameOf = (counter) => sanitize(counter.label).replace(/\r\n|\r|\n/g, " ");
193
+ const nameOf = (counter, count) => sanitize(counterLabel(counter, count)).replace(/\r\n|\r|\n/g, " ");
194
194
  const counters = visible.map((counter, index) => {
195
- const name = nameOf(counter);
196
195
  const share = index === 0 && block.share !== false;
196
+ const name = nameOf(counter, share ? total : counter.n);
197
197
  return [span(share ? `${counter.n}/${total} ${name}` : `${counter.n} ${name}`, counter.status.def.token)];
198
198
  });
199
199
  if (block.layout === "row") return [trimLine(joinChunks([
@@ -281,6 +281,7 @@ const blockLines = (walk, block, width, compact = false) => {
281
281
  case "Line": {
282
282
  const spans = oneLine(inline(walk, block.content));
283
283
  if (block.truncate === true) return [trimLine(truncateSpans(spans, width, walk.ctx.glyphs.ellipsis))];
284
+ if (block.wrap === false) return [trimLine(spans)];
284
285
  return spans.length === 0 ? [[]] : wrapSpans(spans, width, { hardBreak: false }).map(trimLine);
285
286
  }
286
287
  case "DiffText": {
@@ -1,5 +1,5 @@
1
1
  import { Fmt, sanitize } from "../Fmt.js";
2
- import { countsTableOf, totalOf, visibleCountersOf } from "./counts.js";
2
+ import { counterLabel, countsTableOf, totalOf, visibleCountersOf } from "./counts.js";
3
3
  import { isAllowedLinkUrl } from "./linkScheme.js";
4
4
  import { DRIVE, encodePath, fileUrlPath } from "./linkTarget.js";
5
5
  import { flatten } from "./layout.js";
@@ -52,7 +52,7 @@ const codeSpan = (text, mode) => {
52
52
  const longest = Math.max(0, ...[...flat.matchAll(/`+/g)].map((run) => run[0].length));
53
53
  const fence = "`".repeat(longest + 1);
54
54
  const padded = /^`|`$/.test(flat) || /^ .* $/.test(flat) && flat.trim() !== "" || flat === "";
55
- const body = mode === "cell" ? flat.replace(/\|/g, "\\|") : flat;
55
+ const body = mode === "cell" ? flat.split("|").join("\\|") : flat;
56
56
  const pad = padded ? " " : "";
57
57
  return `${fence}${pad}${body === "" ? " " : body}${pad}${fence}`;
58
58
  };
@@ -194,9 +194,10 @@ const countsMd = (walk, block) => {
194
194
  const qualifier = block.qualifier === void 0 ? "" : inlineMd(block.qualifier, ctx, "line").trim();
195
195
  const duration = block.durationMs === void 0 ? "" : Fmt.duration(block.durationMs);
196
196
  const suffix = block.suffix === void 0 ? "" : inlineMd(block.suffix, ctx, "line").trim();
197
- const name = (counter) => escapeText(sanitize(counter.label).replace(/\r\n|\r|\n/g, " "));
197
+ const name = (counter, count) => escapeText(sanitize(counterLabel(counter, count)).replace(/\r\n|\r|\n/g, " "));
198
+ const headlineName = (counter, index) => index === 0 && block.share !== false ? name(counter, total) : name(counter);
198
199
  if (block.layout === "row") {
199
- const header = [...visible.map(name), ...duration === "" ? [] : ["duration"]];
200
+ const header = [...visible.map(headlineName), ...duration === "" ? [] : ["duration"]];
200
201
  const cells = [...visible.map((counter, index) => index === 0 && block.share !== false ? `${counter.n}/${total}` : String(counter.n)), ...duration === "" ? [] : [duration]];
201
202
  const after = [qualifier, suffix].filter((part) => part !== "").join(" ");
202
203
  return [
@@ -212,7 +213,7 @@ const countsMd = (walk, block) => {
212
213
  ...duration === "" ? [] : [[duration]],
213
214
  ...suffix === "" ? [] : [flowLines(suffix)]
214
215
  ];
215
- const tally = visible.map((counter, index) => index === 0 && block.share !== false ? `${counter.n}/${total} ${name(counter)}` : `${counter.n} ${name(counter)}`).join(", ");
216
+ const tally = visible.map((counter, index) => index === 0 && block.share !== false ? `${counter.n}/${total} ${headlineName(counter, index)}` : `${counter.n} ${name(counter)}`).join(", ");
216
217
  const line = [
217
218
  [label === "" ? "" : `${label}:`, tally].filter((part) => part !== "").join(" "),
218
219
  qualifier,