@effected/cli 0.10.0 → 0.12.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.
Files changed (92) hide show
  1. package/Cancelled.js +44 -0
  2. package/CliAudience.js +178 -0
  3. package/CliColor.js +13 -19
  4. package/CliEnv.js +89 -0
  5. package/CliExit.js +1 -1
  6. package/CliFailure.js +302 -0
  7. package/CliInteractive.js +71 -0
  8. package/CliLinks.js +154 -0
  9. package/CliLog.js +346 -0
  10. package/CliLogger.js +34 -33
  11. package/CliMessage.js +80 -0
  12. package/CliPrompt.js +104 -0
  13. package/CliRuntime.js +110 -54
  14. package/CliTest.js +16 -0
  15. package/CliTheme.js +141 -0
  16. package/ConfigIssueRenderer.js +14 -33
  17. package/Doc.js +536 -0
  18. package/Fmt.js +133 -0
  19. package/GithubAnnotation.js +40 -0
  20. package/Glyphs.js +83 -0
  21. package/NotInteractive.js +42 -0
  22. package/README.md +145 -131
  23. package/Render.js +255 -0
  24. package/SchemaIssueRenderer.js +7 -10
  25. package/Status.js +166 -0
  26. package/TestTerminal.js +80 -0
  27. package/Token.js +69 -0
  28. package/index.d.ts +3089 -169
  29. package/index.js +19 -1
  30. package/internal/ansi.js +230 -0
  31. package/internal/autoFormat.js +34 -0
  32. package/internal/canPrompt.js +15 -0
  33. package/internal/counts.js +84 -0
  34. package/internal/diagnostics.js +32 -0
  35. package/internal/displayWidth.js +35 -0
  36. package/internal/failureTarget.js +195 -0
  37. package/internal/fallbackAnswer.js +18 -0
  38. package/internal/fileSink.js +62 -0
  39. package/internal/format.js +62 -7
  40. package/internal/layout.js +250 -0
  41. package/internal/linkScheme.js +30 -0
  42. package/internal/linkTarget.js +50 -0
  43. package/internal/logSafety.js +46 -0
  44. package/internal/renderAnsi.js +52 -0
  45. package/internal/renderDoc.js +320 -0
  46. package/internal/renderGithubLog.js +46 -0
  47. package/internal/renderMarkdown.js +368 -0
  48. package/internal/renderPlain.js +50 -0
  49. package/internal/scanAudience.js +106 -0
  50. package/internal/splitFrame.js +56 -0
  51. package/internal/wizardGate.js +18 -0
  52. package/package.json +40 -5
  53. package/testing.d.ts +88 -2
  54. package/testing.js +2 -1
  55. package/ui/CliUi.js +432 -0
  56. package/ui/CliUiLive.js +446 -0
  57. package/ui/Confirm.js +245 -0
  58. package/ui/DocView.js +74 -0
  59. package/ui/KeyHelp.js +62 -0
  60. package/ui/KeyTable.js +199 -0
  61. package/ui/MultiSelect.js +260 -0
  62. package/ui/Select.js +230 -0
  63. package/ui/Tabs.js +202 -0
  64. package/ui/TextInput.js +290 -0
  65. package/ui/Toggle.js +32 -0
  66. package/ui/UiKey.js +44 -0
  67. package/ui/UiProvider.js +60 -0
  68. package/ui/UiStreams.js +18 -0
  69. package/ui/UiTheme.js +119 -0
  70. package/ui/Viewport.js +204 -0
  71. package/ui/internal/ErrorBoundary.js +30 -0
  72. package/ui/internal/Holder.js +74 -0
  73. package/ui/internal/ScreenContext.js +52 -0
  74. package/ui/internal/UiProviders.js +21 -0
  75. package/ui/internal/ink.js +122 -0
  76. package/ui/internal/inkChalk.js +58 -0
  77. package/ui/internal/inkConsole.js +146 -0
  78. package/ui/internal/lazyView.js +74 -0
  79. package/ui/internal/lineText.js +19 -0
  80. package/ui/internal/mountPermit.js +16 -0
  81. package/ui/internal/perfDrain.js +33 -0
  82. package/ui/internal/processStreams.js +19 -0
  83. package/ui/internal/renderOptions.js +13 -0
  84. package/ui/testing/CliUiTest.js +760 -0
  85. package/ui/testing/fakeStreams.js +79 -0
  86. package/ui/testing/terminalModel.js +59 -0
  87. package/ui-testing-serializer.d.ts +14 -0
  88. package/ui-testing-serializer.js +33 -0
  89. package/ui-testing.d.ts +527 -0
  90. package/ui-testing.js +3 -0
  91. package/ui.d.ts +1790 -0
  92. package/ui.js +17 -0
package/CliRuntime.js CHANGED
@@ -1,15 +1,32 @@
1
+ import { sanitize } from "./Fmt.js";
2
+ import { FailureTargetCell, currentTarget, fallbackTarget, guardConsumerLines, linesOf, plainFailureLines, readSpans, refreshFailureTarget } from "./internal/failureTarget.js";
3
+ import { CliColor } from "./CliColor.js";
4
+ import { CliEnv } from "./CliEnv.js";
1
5
  import { isExitCode } from "./internal/isExitCode.js";
2
6
  import { CliExit } from "./CliExit.js";
3
- import { CliLogger } from "./CliLogger.js";
7
+ import { TrustedLine } from "./internal/logSafety.js";
8
+ import { CliLogger, makeCliLogger } from "./CliLogger.js";
9
+ import { CliLog, envBuildLogLayer, platformLogLayer } from "./CliLog.js";
4
10
  import { ExitRequested } from "./internal/ExitRequested.js";
5
11
  import { routeHelpOnUsageError } from "./internal/HelpRouting.js";
6
- import { Cause, Effect, MutableRef, Runtime } from "effect";
12
+ import { Cause, Effect, Layer, Logger, MutableRef, Runtime } from "effect";
7
13
  import { CliError } from "effect/cli";
14
+ import { CommandNeutralizer } from "@effected/github-commands";
8
15
 
9
16
  //#region src/CliRuntime.ts
10
17
  const isShowHelp = (u) => CliError.isCliError(u) && u._tag === "ShowHelp";
11
18
  /** A `UserError` `Command.runWith` already printed: it sets the mark to `false` after rendering. */
12
19
  const isRenderedUserError = (u) => CliError.isCliError(u) && u._tag === "UserError" && Runtime.getErrorReported(u) === false;
20
+ /** The last line of defence of a failure report: the error's text, sanitised and neutralized, whatever else broke. */
21
+ const lastResort = (error) => {
22
+ let text;
23
+ try {
24
+ text = String(error);
25
+ } catch {
26
+ text = "[unprintable failure]";
27
+ }
28
+ return CommandNeutralizer.lines(sanitize(text));
29
+ };
13
30
  const toLines = (rendered) => typeof rendered === "string" ? [rendered] : rendered;
14
31
  /**
15
32
  * The error's own exit code when it carries one, otherwise the fallback.
@@ -25,7 +42,7 @@ const chooseExitCode = (error, fallback) => typeof error === "object" && error !
25
42
  * Report a CLI program's failures through the program's own logger.
26
43
  *
27
44
  * @remarks
28
- * ## The bug this exists to prevent
45
+ * ## What it prevents
29
46
  *
30
47
  * A platform `runMain` reports an unhandled failure using Effect's **default**
31
48
  * logger. That logger sits **outside** the layers the program was provided —
@@ -42,18 +59,7 @@ const chooseExitCode = (error, fallback) => typeof error === "object" && error !
42
59
  *
43
60
  * The fix has to happen **inside** the effect, before any `runMain` sees it. So
44
61
  * this is a combinator you apply to your program, and you still call your own
45
- * platform's runner:
46
- *
47
- * @example
48
- * ```ts
49
- * import { CliRuntime } from "@effected/cli"
50
- * import { NodeRuntime } from "@effect/platform-node"
51
- * import { Effect } from "effect"
52
- *
53
- * NodeRuntime.runMain(program.pipe(CliRuntime.reportFailures(), Effect.provide(MainLive)))
54
- * ```
55
- *
56
- * Wrapping `runMain` itself would drag a platform choice into a library that
62
+ * platform's runner. Wrapping `runMain` itself would drag a platform choice into a library that
57
63
  * has no business making one, and would make this package unusable from Bun or
58
64
  * Deno for no gain.
59
65
  *
@@ -96,13 +102,49 @@ const chooseExitCode = (error, fallback) => typeof error === "object" && error !
96
102
  * raises is likewise never rendered; it only carries the exit code a
97
103
  * successful program recorded through `CliExit`.
98
104
  *
105
+ * @example
106
+ * ```ts
107
+ * import { CliRuntime } from "@effected/cli"
108
+ * import { NodeRuntime } from "@effect/platform-node"
109
+ * import { Effect } from "effect"
110
+ *
111
+ * NodeRuntime.runMain(program.pipe(CliRuntime.reportFailures(), Effect.provide(MainLive)))
112
+ * ```
113
+ *
99
114
  * @public
100
115
  */
101
116
  var CliRuntime = class CliRuntime {
102
117
  constructor() {}
103
118
  /**
104
- * Catch, render through the ambient logger, and re-fail with the exit code
105
- * and the no-double-report mark.
119
+ * What `reportFailures` and `main` render a failure as when no `render` option is given, for a consumer's own
120
+ * `render` to hand a failure back to.
121
+ *
122
+ * @remarks
123
+ * The plain lines of `CliFailure.toDoc(details.cause)`: a failure status line, a `Tree` for a schema failure, a
124
+ * defect's message with its cleaned `stack`, and the one fixed line each for `Cancelled` and `NotInteractive`.
125
+ * It has no terminal to ask, so it is the plain rendering for an agent, with absolute paths; the report `main` writes
126
+ * with no `render` option is the same document in the renderer the audience gets (painted for a person), and that
127
+ * report is `details.defaultLines`: return those to hand a failure back with the run's colour, links and path
128
+ * display. With `status: false` the leading status (the glyph, or `[FAIL]` in plain text) is left off, so a prefix
129
+ * such as the program's name reads cleanly; for that AND the run's settings, use `details.lines({ status: false })`.
130
+ * A custom `render` that only cares about its own errors delegates the rest:
131
+ *
132
+ * ```ts
133
+ * const render = (error: unknown, details: FailureDetails) =>
134
+ * error instanceof MyError ? myLines(error) : CliRuntime.defaultRender(error, details)
135
+ * ```
136
+ *
137
+ * @param error - the squashed failure
138
+ * @param details - what `render` is told about the failure; accepted so a delegating `render` passes both
139
+ * arguments through unchanged (only `cause` and `isDefect` are read)
140
+ * @param options - `status: false` leaves off the leading status glyph or `[FAIL]` tag
141
+ */
142
+ static defaultRender = (error, details, options) => plainFailureLines(details.cause.reasons.length > 0 ? details.cause : Cause.fail(error), options?.status !== false);
143
+ /**
144
+ * Catch a program's failure, render it through the ambient logger, and re-fail with the exit code and the
145
+ * no-double-report mark.
146
+ *
147
+ * @param options - how to render the failure and which exit codes to use
106
148
  */
107
149
  static reportFailures = (options = {}) => (effect) => effect.pipe(Effect.catchCause((cause) => {
108
150
  if (Cause.hasInterruptsOnly(cause)) return Effect.failCause(cause);
@@ -113,50 +155,64 @@ var CliRuntime = class CliRuntime {
113
155
  return Effect.fail(CliRuntime.reported(error, code));
114
156
  }
115
157
  if (isRenderedUserError(error)) return Effect.fail(CliRuntime.reported(error, chooseExitCode(error, options.usageExitCode ?? 64)));
116
- const render = options.render ?? ((value) => String(value));
117
- const details = {
118
- cause,
119
- isDefect: !Cause.hasFails(cause)
120
- };
158
+ const render = options.render;
121
159
  return Effect.gen(function* () {
122
- for (const line of toLines(render(error, details))) yield* Effect.logError(line);
160
+ const target = yield* currentTarget.pipe(Effect.catchCause(() => Effect.succeed(fallbackTarget)));
161
+ const reportLines = (status, spans) => {
162
+ try {
163
+ return linesOf(cause, target, status, spans ?? target.spans);
164
+ } catch {
165
+ try {
166
+ return plainFailureLines(cause, status, spans ?? target.spans);
167
+ } catch {
168
+ return lastResort(error);
169
+ }
170
+ }
171
+ };
172
+ const defaultLines = reportLines(true);
173
+ const details = {
174
+ cause,
175
+ isDefect: !Cause.hasFails(cause),
176
+ defaultLines,
177
+ lines: (options) => options?.status === false || options?.spans !== void 0 ? reportLines(options?.status !== false, options?.spans) : defaultLines
178
+ };
179
+ const lines = render === void 0 ? defaultLines : yield* guardConsumerLines(toLines(render(error, details)));
180
+ for (const line of lines) yield* Effect.logError(line).pipe(Effect.provideService(TrustedLine, true));
123
181
  return yield* Effect.fail(CliRuntime.reported(error, chooseExitCode(error, options.exitCode)));
124
182
  });
125
183
  }));
126
- /**
127
- * Assemble a CLI program in the one order that reports every failure well.
128
- *
129
- * @remarks
130
- * - `CliExit` is provided fresh, and a non-zero code after success becomes a
131
- * marked failure the teardown honours.
132
- * - The platform layer is provided **inside** failure reporting, so a
133
- * layer-build failure (`HOME` unset, say) renders as one line with the
134
- * fallback code rather than escaping to the runtime's stack trace.
135
- * - The logger is provided **outermost**, so it is present whichever branch
136
- * fails.
137
- *
138
- * You still call your platform's runner:
139
- *
140
- * @example
141
- * ```ts
142
- * NodeRuntime.runMain(
143
- * CliRuntime.main(Command.run(root, { version }), { platform: NodeServices.layer, exitCode: 3 }),
144
- * )
145
- * ```
146
- */
147
- static main = (program, options) => Effect.gen(function* () {
148
- yield* options.helpOnUsageError === "stderr" ? routeHelpOnUsageError(program) : program;
149
- const exit = yield* CliExit;
150
- const code = MutableRef.get(exit.code);
151
- if (!isExitCode(code)) return yield* Effect.die(/* @__PURE__ */ new Error(`CliRuntime.main: CliExit code must be an integer 0..255, received ${code}`));
152
- if (code !== 0) return yield* Effect.fail(new ExitRequested(code));
153
- }).pipe(Effect.provide(CliExit.layer), Effect.provide(options.platform), CliRuntime.reportFailures(options), Effect.provide(options.logger ?? CliLogger.layer()));
184
+ static main(program, options) {
185
+ const env = options.env === void 0 ? void 0 : CliEnv.layer(options.env);
186
+ const envLog = options.env?.log;
187
+ const logger = options.logger ?? (env === void 0 || envLog === void 0 ? CliLogger.layer() : CliLog.layer(envLog).pipe(Layer.provide(env), Layer.provide(Layer.provideMerge(options.platform.pipe(Layer.provide(platformLogLayer(envLog, options.env?.audienceEnvVar))), envBuildLogLayer(envLog, options.env?.audienceEnvVar))), Layer.catchCause(() => CliLogger.layer(envLog.logger))));
188
+ const inside = env === void 0 ? Layer.empty : Layer.mergeAll(CliColor.formatterLayer(options.env?.formatter), Layer.effectDiscard(Effect.gen(function* () {
189
+ const { spans, invalid } = yield* readSpans(options.env?.spans, options.env?.spansEnvVar);
190
+ if (invalid !== void 0) yield* Effect.logWarning(invalid).pipe(Effect.provideService(Logger.CurrentLoggers, /* @__PURE__ */ new Set([makeCliLogger(envLog?.logger)])));
191
+ yield* refreshFailureTarget(void 0, {
192
+ displayPath: options.env?.displayPath,
193
+ stackFrames: options.env?.stackFrames,
194
+ spans,
195
+ appModule: options.env?.appModule
196
+ });
197
+ }))).pipe(Layer.provideMerge(env));
198
+ const run = Effect.gen(function* () {
199
+ yield* options.helpOnUsageError === "stderr" ? routeHelpOnUsageError(program) : program;
200
+ const exit = yield* CliExit;
201
+ const code = MutableRef.get(exit.code);
202
+ if (!isExitCode(code)) return yield* Effect.die(/* @__PURE__ */ new Error(`CliRuntime.main: CliExit code must be an integer 0..255, received ${code}`));
203
+ if (code !== 0) return yield* Effect.fail(new ExitRequested(code));
204
+ }).pipe(Effect.provide(CliExit.layer), Effect.provide(inside), Effect.provide(options.platform), CliRuntime.reportFailures(options), Effect.provide(logger));
205
+ return Effect.suspend(() => Effect.provideService(run, FailureTargetCell, MutableRef.make(void 0)));
206
+ }
154
207
  static reported(error, exitCode = 1) {
155
208
  const marked = error instanceof Error ? error : new Error(String(error));
156
- return Object.assign(marked, {
157
- [Runtime.errorReported]: false,
158
- [Runtime.errorExitCode]: exitCode
209
+ for (const [key, value] of [[Runtime.errorReported, false], [Runtime.errorExitCode, exitCode]]) Object.defineProperty(marked, key, {
210
+ value,
211
+ writable: true,
212
+ configurable: true,
213
+ enumerable: false
159
214
  });
215
+ return marked;
160
216
  }
161
217
  };
162
218
 
package/CliTest.js CHANGED
@@ -6,6 +6,22 @@ const text = (stream) => Stream.mkString(Stream.decodeText(stream));
6
6
  /**
7
7
  * Spawn a built CLI bin hermetically and read its exit code and streams as data.
8
8
  *
9
+ * @example
10
+ * ```ts
11
+ * import * as NodeServices from "@effect/platform-node/NodeServices"
12
+ * import { assert, it } from "@effect/vitest"
13
+ * import { CliTest } from "@effected/cli/testing"
14
+ * import { Effect } from "effect"
15
+ *
16
+ * it.effect("prints its version", () =>
17
+ * Effect.gen(function* () {
18
+ * const sandbox = yield* CliTest.sandbox({ path: process.env.PATH ?? "" })
19
+ * const result = yield* CliTest.run("dist/bin.js", ["--version"], { sandbox, execPath: process.execPath })
20
+ * assert.strictEqual(result.exitCode, 0)
21
+ * }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
22
+ * )
23
+ * ```
24
+ *
9
25
  * @public
10
26
  */
11
27
  var CliTest = class {
package/CliTheme.js ADDED
@@ -0,0 +1,141 @@
1
+ import { Glyphs } from "./Glyphs.js";
2
+ import { openSequence, paintStyle } from "./internal/ansi.js";
3
+ import { Token } from "./Token.js";
4
+ import { Config, Context, Effect, Layer, Option } from "effect";
5
+ import { TerminalEnv } from "@effected/env";
6
+ import { Prompt } from "effect/cli";
7
+
8
+ //#region src/CliTheme.ts
9
+ /**
10
+ * A stream theme at `color`, over a style resolution and a glyph set: the one way a {@link StreamTheme} is built.
11
+ *
12
+ * @internal
13
+ */
14
+ const streamThemeAt = (resolve, glyphs, color) => {
15
+ const paint = (token, text) => paintStyle(resolve(token), color, text);
16
+ return {
17
+ paint,
18
+ style: resolve,
19
+ sgr: (token) => openSequence(resolve(token), color),
20
+ glyphs,
21
+ color,
22
+ status: (vocab, name, text) => {
23
+ const glyph = paint(vocab.def(name).token, vocab.glyph(name, glyphs));
24
+ return text === void 0 || text === "" ? glyph : `${glyph} ${text}`;
25
+ }
26
+ };
27
+ };
28
+ /** The audience rule, shared by `CliTheme.forAudience` and the class's own docs. */
29
+ const forAudience = (theme, audience) => audience === "agent" && theme.color !== "none" ? streamThemeAt(theme.style, theme.glyphs, "none") : theme;
30
+ const make = (colors, glyphs, overrides) => {
31
+ const resolve = (token) => Token.resolve(token, overrides);
32
+ const stdout = streamThemeAt(resolve, glyphs, colors.stdout);
33
+ const stderr = streamThemeAt(resolve, glyphs, colors.stderr);
34
+ return {
35
+ ...stdout,
36
+ forStream: (stream) => stream === "stdout" ? stdout : stderr
37
+ };
38
+ };
39
+ /** The prompt glyphs that differ under ASCII; `Prompt.makeTheme` already holds the Unicode ones. */
40
+ const ASCII_PROMPT_GLYPHS = {
41
+ prefix: "?",
42
+ arrowUp: "^",
43
+ arrowDown: "v",
44
+ checkboxOn: "[x]",
45
+ checkboxOff: "[ ]",
46
+ tick: "+",
47
+ pointerSmall: ">",
48
+ pointer: ">"
49
+ };
50
+ /**
51
+ * The presentation of a CLI: colour tokens, glyphs and statuses, decided once from the terminal.
52
+ *
53
+ * @remarks
54
+ * A `Context.Service`, not a `Reference`. A colour level is a fact about the terminal, not a preference with a
55
+ * safe default, so there is nothing sensible for an unprovided theme to read; requiring it puts `CliTheme` in
56
+ * `R` and a program that forgot to wire it fails to compile rather than printing plain text to a colour
57
+ * terminal. {@link CliTheme.layer} reads `TerminalEnv`.
58
+ *
59
+ * @public
60
+ */
61
+ var CliTheme = class CliTheme extends Context.Service()("@effected/cli/CliTheme") {
62
+ /**
63
+ * The theme for the terminal `TerminalEnv` describes.
64
+ *
65
+ * @remarks
66
+ * Bind the layer to a constant and provide it once. With `glyphs: "auto"` the glyph set is ASCII only when
67
+ * `TERM=dumb`, read through `Config`.
68
+ *
69
+ * @param options - token overrides and the glyph set
70
+ */
71
+ static layer = (options) => Layer.effect(CliTheme, Effect.gen(function* () {
72
+ const terminal = yield* TerminalEnv;
73
+ const choice = options?.glyphs ?? "auto";
74
+ const term = choice === "auto" ? Option.getOrUndefined(yield* Config.option(Config.String("TERM")).pipe(Effect.orElseSucceed(() => Option.none()))) : void 0;
75
+ const glyphs = Glyphs.select({
76
+ ascii: choice === "ascii" ? true : choice === "unicode" ? false : "auto",
77
+ ...term === void 0 ? {} : { term }
78
+ });
79
+ return make({
80
+ stdout: terminal.stdout.color,
81
+ stderr: terminal.stderr.color
82
+ }, glyphs, options?.tokens);
83
+ }));
84
+ /**
85
+ * The theme an audience sees of `theme`: for an agent, the same theme at colour `none` (`paint` the identity, `sgr`
86
+ * empty, `status` unpainted), whatever the terminal could do, because an agent never gets an escape of any kind; for
87
+ * anyone else, or when the audience is not known, `theme` itself.
88
+ *
89
+ * @remarks
90
+ * The one rule the kit applies wherever it paints for an audience: `Render.context` takes its colour and `paint`
91
+ * from it, `CliMessage` and `CliLog.status` paint their glyphs through it, and `./ui` gives it to the trees it
92
+ * mounts, so `useTheme`, `Styled` and the widgets' colour-`none` text markers all agree. A program that paints its
93
+ * own lines applies the same rule with it rather than re-implementing it:
94
+ *
95
+ * ```ts
96
+ * const line = Effect.gen(function* () {
97
+ * const theme = CliTheme.forAudience((yield* CliTheme).forStream("stdout"), (yield* Audience).kind)
98
+ * return theme.status(Status.core, "success", Fmt.sanitize(name))
99
+ * })
100
+ * ```
101
+ *
102
+ * Pure: it reads nothing, so the audience is the caller's to pass, `undefined` when it is not known.
103
+ *
104
+ * @param theme - a stream's theme, such as `CliTheme.forStream("stdout")`
105
+ * @param audience - who the output is for, or `undefined` when that is not known
106
+ */
107
+ static forAudience = forAudience;
108
+ /**
109
+ * A fixed theme that needs nothing; `none` colour and Unicode glyphs unless told otherwise.
110
+ *
111
+ * @param options - the colour level and glyph set
112
+ */
113
+ static layerTest = (options) => Layer.succeed(CliTheme, make({
114
+ stdout: options?.color ?? "none",
115
+ stderr: options?.stderrColor ?? options?.color ?? "none"
116
+ }, options?.glyphs === "ascii" ? Glyphs.ascii : Glyphs.unicode, void 0));
117
+ /**
118
+ * Sets core's `Prompt.Theme` from the tokens, so built-in prompts match the rest of the output.
119
+ *
120
+ * @remarks
121
+ * The colour fields are raw SGR openers and are empty strings when colour is `none`. Under ASCII glyphs the
122
+ * prompt symbols fall back to ASCII too. A colourless theme is not byte-clean: core's `Ansi.annotate`
123
+ * appends a `\x1b[0m` reset, and prompts write cursor and underline codes, whatever the theme says.
124
+ */
125
+ static promptTheme = Layer.effect(Prompt.Theme, Effect.gen(function* () {
126
+ const theme = yield* CliTheme;
127
+ const open = (token) => theme.sgr(token);
128
+ return Prompt.makeTheme({
129
+ ...theme.glyphs.kind === "ascii" ? ASCII_PROMPT_GLYPHS : {},
130
+ ellipsis: theme.glyphs.ellipsis,
131
+ primaryColor: open("accent"),
132
+ mutedColor: open("muted"),
133
+ successColor: open("success"),
134
+ errorColor: open("error"),
135
+ submittedColor: open("emphasis")
136
+ });
137
+ }));
138
+ };
139
+
140
+ //#endregion
141
+ export { CliTheme, streamThemeAt };
@@ -2,35 +2,21 @@ import { formatIssue } from "./internal/format.js";
2
2
 
3
3
  //#region src/ConfigIssueRenderer.ts
4
4
  /**
5
- * Render a `@effected/config-file` validation failure.
5
+ * Turns a `@effected/config-file` `ConfigValidationError` into one line per rejected value.
6
6
  *
7
7
  * @remarks
8
8
  * `ConfigValidationError` carries the structured `issue` tree rather than a
9
- * string, which is the right design and leaves the consumer holding a tree it
10
- * has to turn into sentences. This is that step, and it is the same treatment
11
- * {@link SchemaIssueRenderer} gives a bare issue.
9
+ * string, so a caller holds a tree it has to turn into sentences. This is that
10
+ * step, the same treatment {@link SchemaIssueRenderer} gives a bare issue.
12
11
  *
13
- * **This module is the only thing in the package that references
14
- * `@effected/config-file`, deliberately.** The peer is declared optional, and
15
- * an optional peer whose import is reachable from a shared module is not
16
- * optional — it is a crash for every consumer who took the manifest at its word
17
- * and did not install it. So nothing else in this package imports this module;
18
- * only the entrypoint re-exports it, and the shared rendering lives in
19
- * `internal/format`.
12
+ * `ConfigValidationError.message` names the file but not the value, and the
13
+ * value is the diagnostic: printing only the message tells a user their config
14
+ * is invalid without saying which value is wrong or how it is shaped.
20
15
  *
21
- * The import is additionally `import type`, so it is erased at build time and
22
- * the runtime reach is **zero** — a consumer without `@effected/config-file`
23
- * installed can import this module and call `render` on any value without the
24
- * resolver ever being asked for the package.
25
- *
26
- * ## Why this and not just the error's message
27
- *
28
- * `ConfigValidationError.message` names the file. It does not name the value,
29
- * and the difference is the whole diagnostic: a consumer that printed only the
30
- * message told users "your config is invalid, run the doctor command", and the
31
- * doctor command — which guessed from a hand-written list of known keys — could
32
- * only ever find a *misspelling*. A wrongly **shaped** value left the two
33
- * commands pointing at each other and neither saying what was wrong.
16
+ * `@effected/config-file` is an optional peer, and this module only
17
+ * `import type`s it, so the import is erased at build time. A consumer without
18
+ * the package installed can import this module without the resolver being asked
19
+ * for it.
34
20
  *
35
21
  * @example
36
22
  * ```ts
@@ -56,17 +42,12 @@ var ConfigIssueRenderer = class {
56
42
  *
57
43
  * @remarks
58
44
  * Takes the **error**, not its `issue`, because that is what a `catchTag`
59
- * hands you and because `issue` is typed `Schema.Defect` — reaching into it
60
- * at every call site is exactly the ceremony this removes.
61
- *
62
- * The parameter is the **typed** error rather than `unknown`. An earlier
63
- * draft wrote `ConfigValidationError | unknown` to be accommodating, which
64
- * collapses to plain `unknown` in TypeScript — so it accepted anything, said
65
- * nothing, and left the type import in the `.d.ts` earning nothing. Inside
45
+ * hands you and because `issue` is typed `Schema.Defect`, which every call
46
+ * site would otherwise have to reach into. Inside
66
47
  * `Effect.catchTag("ConfigValidationError", …)` the error is already this
67
- * type, which is where this is called.
48
+ * type.
68
49
  *
69
- * It still cannot throw on a malformed value: the issue tree is validated by
50
+ * It cannot throw on a malformed value: the issue tree is validated by
70
51
  * a guard before it is read, so a renderer on an error path never becomes the
71
52
  * reason a program dies.
72
53
  */