@effected/cli 0.10.0 → 0.11.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 (89) 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 +253 -0
  7. package/CliInteractive.js +71 -0
  8. package/CliLinks.js +154 -0
  9. package/CliLog.js +294 -0
  10. package/CliLogger.js +34 -33
  11. package/CliMessage.js +83 -0
  12. package/CliPrompt.js +104 -0
  13. package/CliRuntime.js +103 -53
  14. package/CliTest.js +16 -0
  15. package/CliTheme.js +128 -0
  16. package/ConfigIssueRenderer.js +14 -33
  17. package/Doc.js +512 -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 +129 -131
  23. package/Render.js +254 -0
  24. package/SchemaIssueRenderer.js +7 -10
  25. package/Status.js +163 -0
  26. package/TestTerminal.js +80 -0
  27. package/Token.js +69 -0
  28. package/index.d.ts +2923 -171
  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 +69 -0
  34. package/internal/diagnostics.js +32 -0
  35. package/internal/displayWidth.js +35 -0
  36. package/internal/failureTarget.js +156 -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 +319 -0
  46. package/internal/renderGithubLog.js +46 -0
  47. package/internal/renderMarkdown.js +367 -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 +35 -5
  53. package/testing.d.ts +88 -2
  54. package/testing.js +2 -1
  55. package/ui/CliUi.js +348 -0
  56. package/ui/CliUiLive.js +399 -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 +226 -0
  63. package/ui/Tabs.js +202 -0
  64. package/ui/TextInput.js +250 -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/lineText.js +19 -0
  79. package/ui/internal/mountPermit.js +16 -0
  80. package/ui/internal/perfDrain.js +33 -0
  81. package/ui/internal/processStreams.js +19 -0
  82. package/ui/internal/renderOptions.js +13 -0
  83. package/ui/testing/CliUiTest.js +735 -0
  84. package/ui/testing/fakeStreams.js +76 -0
  85. package/ui/testing/terminalModel.js +59 -0
  86. package/ui-testing.d.ts +446 -0
  87. package/ui-testing.js +3 -0
  88. package/ui.d.ts +1648 -0
  89. package/ui.js +17 -0
package/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Effect, FileSystem, Path, PlatformError, Scope } from "effect";
1
+ import { Effect, FileSystem, Layer, Path, PlatformError, Scope, Terminal } from "effect";
2
2
  import { ChildProcessSpawner } from "effect/process";
3
3
  //#region src/CliTest.d.ts
4
4
  /**
@@ -57,6 +57,22 @@ interface RunResult {
57
57
  /**
58
58
  * Spawn a built CLI bin hermetically and read its exit code and streams as data.
59
59
  *
60
+ * @example
61
+ * ```ts
62
+ * import * as NodeServices from "@effect/platform-node/NodeServices"
63
+ * import { assert, it } from "@effect/vitest"
64
+ * import { CliTest } from "@effected/cli/testing"
65
+ * import { Effect } from "effect"
66
+ *
67
+ * it.effect("prints its version", () =>
68
+ * Effect.gen(function* () {
69
+ * const sandbox = yield* CliTest.sandbox({ path: process.env.PATH ?? "" })
70
+ * const result = yield* CliTest.run("dist/bin.js", ["--version"], { sandbox, execPath: process.execPath })
71
+ * assert.strictEqual(result.exitCode, 0)
72
+ * }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
73
+ * )
74
+ * ```
75
+ *
60
76
  * @public
61
77
  */
62
78
  export declare class CliTest {
@@ -90,5 +106,75 @@ export declare class CliTest {
90
106
  static readonly run: (bin: string, args: ReadonlyArray<string>, options: RunOptions) => Effect.Effect<RunResult, PlatformError.PlatformError, ChildProcessSpawner.ChildProcessSpawner>;
91
107
  }
92
108
  //#endregion
93
- export type { RunOptions, RunResult, Sandbox };
109
+ //#region src/TestTerminal.d.ts
110
+ /**
111
+ * One key press for {@link TestTerminal}.
112
+ *
113
+ * @public
114
+ */
115
+ interface KeyInput {
116
+ /** The key name the prompts switch on: `down`, `up`, `enter`, `space`, `escape`, or a character. */
117
+ readonly name: string;
118
+ /** Whether Ctrl is held. */
119
+ readonly ctrl?: boolean | undefined;
120
+ /** Whether Meta is held. */
121
+ readonly meta?: boolean | undefined;
122
+ /** Whether Shift is held. */
123
+ readonly shift?: boolean | undefined;
124
+ }
125
+ /**
126
+ * What {@link TestTerminal.make} builds: the `Terminal` layer and the means to drive and inspect it.
127
+ *
128
+ * @public
129
+ */
130
+ interface TestTerminalHandle {
131
+ /** Provides `Terminal` backed by this double. */
132
+ readonly layer: Layer.Layer<Terminal.Terminal>;
133
+ /** Queue key presses, as if the user had typed them. */
134
+ readonly input: (keys: ReadonlyArray<KeyInput>) => Effect.Effect<void>;
135
+ /** Queue `text` one character at a time. */
136
+ readonly type: (text: string) => Effect.Effect<void>;
137
+ /** End the input, as Ctrl-C or end-of-file does: a prompt waiting for a key is quit. */
138
+ readonly end: Effect.Effect<void>;
139
+ /** Everything written to the terminal so far, prompt frames and escape codes included. */
140
+ readonly output: Effect.Effect<string>;
141
+ /** How many queued key presses nobody has taken yet. */
142
+ readonly pending: Effect.Effect<number>;
143
+ /**
144
+ * What the program did with the input: `keys` taken from it, `lines` read with `readLine`, and
145
+ * `subscriptions` to `readInput`. All `0` proves a code path never touched the terminal's input. Counting the
146
+ * subscription matters: on a real terminal merely subscribing attaches a reader to stdin, even when no key is
147
+ * ever taken.
148
+ */
149
+ readonly reads: Effect.Effect<{
150
+ readonly keys: number;
151
+ readonly lines: number;
152
+ readonly subscriptions: number;
153
+ }>;
154
+ }
155
+ /**
156
+ * A scripted `Terminal` for testing prompts and anything that reads the terminal.
157
+ *
158
+ * @remarks
159
+ * Queue keys with `input` or `type`, run the program under `layer`, then read `output`. To assert that a code path
160
+ * did NOT touch the terminal, queue some keys first and check `reads` is all zero (no subscription, no key, no
161
+ * line) and `pending` is unchanged afterwards. Only
162
+ * available from `@effected/cli/testing`.
163
+ *
164
+ * @public
165
+ */
166
+ export declare class TestTerminal {
167
+ private constructor();
168
+ /**
169
+ * Build a test terminal.
170
+ *
171
+ * @param options - the reported size; 80 by 24 by default
172
+ */
173
+ static readonly make: (options?: {
174
+ readonly columns?: number | undefined;
175
+ readonly rows?: number | undefined;
176
+ }) => Effect.Effect<TestTerminalHandle>;
177
+ }
178
+ //#endregion
179
+ export type { KeyInput, RunOptions, RunResult, Sandbox, TestTerminalHandle };
94
180
  //# sourceMappingURL=testing.d.ts.map
package/testing.js CHANGED
@@ -1,3 +1,4 @@
1
1
  import { CliTest } from "./CliTest.js";
2
+ import { TestTerminal } from "./TestTerminal.js";
2
3
 
3
- export { CliTest };
4
+ export { CliTest, TestTerminal };
package/ui/CliUi.js ADDED
@@ -0,0 +1,348 @@
1
+ import { Cancelled } from "../Cancelled.js";
2
+ import { CliInteractive } from "../CliInteractive.js";
3
+ import { underGithubActions } from "../internal/autoFormat.js";
4
+ import { CliTheme, themeForAudience } from "../CliTheme.js";
5
+ import { NotInteractive } from "../NotInteractive.js";
6
+ import { answerWithoutPerson } from "../internal/fallbackAnswer.js";
7
+ import { inkModules, loadInk, withInkColour } from "./internal/ink.js";
8
+ import { errorBoundary } from "./internal/ErrorBoundary.js";
9
+ import { UiStreams } from "./UiStreams.js";
10
+ import { mountPermit } from "./internal/mountPermit.js";
11
+ import { UiRenderOptions } from "./internal/renderOptions.js";
12
+ import { useScreenGuard } from "./internal/ScreenContext.js";
13
+ import { uiProviders } from "./internal/UiProviders.js";
14
+ import { live } from "./CliUiLive.js";
15
+ import { KeyTable, useKeys } from "./KeyTable.js";
16
+ import { Cause, Deferred, Effect, Exit, Option, Semaphore } from "effect";
17
+ import { Audience } from "@effected/env";
18
+ import { Prompt } from "effect/cli";
19
+
20
+ //#region src/ui/CliUi.ts
21
+ /**
22
+ * stdout's theme as the audience sees it: colourless for an agent (as `Render.context` makes it), so a screen's
23
+ * `useTheme`, `Styled` and the widgets' colour-none markers never carry an escape for one. `Audience` is read only when
24
+ * provided, so it stays out of the requirements.
25
+ */
26
+ const audienceTheme = Effect.gen(function* () {
27
+ const audience = yield* Effect.serviceOption(Audience);
28
+ return themeForAudience((yield* CliTheme).forStream("stdout"), Option.isSome(audience) ? audience.value.kind : void 0);
29
+ });
30
+ /** The root keys: Esc cancels with `"escape"`, Ctrl-C with `"interrupt"`. `q` belongs to widgets, never here. */
31
+ const RootKeys = (props) => {
32
+ useKeys(KeyTable.root, props.cancel);
33
+ const guard = useScreenGuard();
34
+ inkModules().ink.usePaste(guard(() => void 0));
35
+ return props.children;
36
+ };
37
+ const SCREEN_EXITED = "@effected/cli/ui: the screen exited without resolving or cancelling";
38
+ const mount = (screen, theme, clear, crash, neutralize) => Effect.gen(function* () {
39
+ const overrides = yield* UiRenderOptions;
40
+ yield* Effect.acquireRelease(Effect.sync(() => overrides.onMount?.()), (_, exit) => Effect.sync(() => {
41
+ const died = Exit.isFailure(exit) ? exit.cause.reasons.find(Cause.isDieReason) : void 0;
42
+ const interrupted = Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause);
43
+ overrides.onUnmount?.(died !== void 0 ? { defect: died.defect } : interrupted ? void 0 : crash.current);
44
+ }));
45
+ const { ink, react } = yield* loadInk;
46
+ const streams = yield* UiStreams;
47
+ yield* withInkColour(theme.color);
48
+ const result = yield* Deferred.make();
49
+ const control = {
50
+ resolve: (value) => {
51
+ Deferred.doneUnsafe(result, Exit.succeed(value));
52
+ },
53
+ cancel: (reason) => {
54
+ Deferred.doneUnsafe(result, Exit.fail(new Cancelled({ reason })));
55
+ }
56
+ };
57
+ const element = yield* Effect.promise(async () => screen(control));
58
+ const die = (error) => {
59
+ if (crash.current === void 0) crash.current = { defect: error };
60
+ Deferred.doneUnsafe(result, Exit.die(error));
61
+ };
62
+ const tree = react.createElement(errorBoundary(), {
63
+ onError: die,
64
+ children: uiProviders({
65
+ cancel: control.cancel,
66
+ die,
67
+ theme,
68
+ glyphs: theme.glyphs,
69
+ ...neutralize ? { neutralizeWorkflowCommands: true } : {}
70
+ }, react.createElement(RootKeys, {
71
+ cancel: control.cancel,
72
+ children: element
73
+ }))
74
+ });
75
+ const instance = yield* Effect.acquireRelease(Effect.sync(() => ink.render(tree, {
76
+ stdin: streams.stdin,
77
+ stdout: streams.stdout,
78
+ stderr: streams.stderr,
79
+ interactive: true,
80
+ exitOnCtrlC: false,
81
+ patchConsole: false,
82
+ ...overrides.debug === true ? { debug: true } : {},
83
+ ...overrides.onRender === void 0 ? {} : { onRender: overrides.onRender }
84
+ })), (instance) => Effect.promise(async () => {
85
+ if (clear) instance.clear();
86
+ const exited = instance.waitUntilExit();
87
+ instance.unmount();
88
+ await exited.catch(() => void 0);
89
+ }));
90
+ const exited = Effect.tryPromise({
91
+ try: () => instance.waitUntilExit(),
92
+ catch: (cause) => cause
93
+ }).pipe(Effect.orDie, Effect.flatMap(() => Effect.flatMap(Deferred.isDone(result), (done) => done ? Deferred.await(result) : Effect.die(/* @__PURE__ */ new Error(SCREEN_EXITED)))));
94
+ return yield* Effect.raceFirst(Deferred.await(result), exited);
95
+ });
96
+ /**
97
+ * Interactive screens drawn with Ink, mounted as scoped resources.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * import { CliUi, Confirm } from "@effected/cli/ui"
102
+ * import { Effect } from "effect"
103
+ *
104
+ * // Answers `otherwise` when there is no person to ask, and fails with `Cancelled` when they back out.
105
+ * const ask = CliUi.prompt(Confirm.screen({ message: "Overwrite the config?" }), {
106
+ * otherwise: { confirmed: false, toggles: {} },
107
+ * })
108
+ * ```
109
+ *
110
+ * @public
111
+ */
112
+ var CliUi = class CliUi {
113
+ constructor() {}
114
+ /**
115
+ * Mount `screen` and wait for it to resolve or cancel.
116
+ *
117
+ * @remarks
118
+ * When `CliInteractive` is false it fails with `NotInteractive` and mounts nothing; Ink and React are not even
119
+ * loaded. Otherwise it loads them, holds Ink's colour level at stdout's, and mounts the screen on
120
+ * `UiStreams` with Ink's own Ctrl-C exit off: Ctrl-C cancels with `"interrupt"` and Esc with `"escape"`.
121
+ *
122
+ * Mounting is one scoped resource. However the screen ends (resolved, cancelled, crashed, or the fiber
123
+ * interrupted), it is unmounted, raw mode and bracketed paste are off, the cursor is shown, and the colour level is
124
+ * restored. A
125
+ * component that throws is a defect, never a hang or a typed failure, and nothing of Ink's crash screen reaches
126
+ * stdout. So is a `useKeys` handler that throws; a handler a consumer registers with Ink's own `useInput` or
127
+ * `usePaste` is outside the kit, and what it throws escapes as Ink leaves it. A crash wins over an end in the same
128
+ * tick: a handler that cancels or resolves and then throws, or a component that throws before the screen has
129
+ * unmounted, is a defect, never the `Cancelled` or the value; a defect raised while the screen unmounts stays beside
130
+ * that crash in the cause rather than replacing it. An interrupt stays an interrupt, even when the tree reports a
131
+ * crash as it unmounts.
132
+ *
133
+ * A screen draws on stdout (`UiStreams`), and mounts only when `CliInteractive` is true: a human audience, a
134
+ * terminal on both stdin and stdout, and a `TERM` that is not `dumb`. `CliInteractive` reads `false` until a layer
135
+ * sets it (`CliRuntime.main`'s `env` does), so a program that never provides one always gets `NotInteractive`.
136
+ *
137
+ * Screens run one at a time, process-wide: Ink owns raw mode on the one terminal, so a second `run` waits until the
138
+ * first is released; so does a `run` while a {@link CliUi.live} view has a run drawn. A screen that itself awaits
139
+ * another `CliUi.run` therefore deadlocks, and nothing guards against it.
140
+ *
141
+ * Do not log while a screen is mounted. Ink redraws its frame by counting the lines it last wrote, and it is
142
+ * mounted with `patchConsole` off, so a line written to the terminal from elsewhere (an `Effect.log`, `CliLog`, a
143
+ * background fiber) lands inside the frame and tears it. Log before the screen mounts or after it resolves.
144
+ *
145
+ * With `clear` the last frame is erased as the screen unmounts, so a wizard of several screens leaves only what the
146
+ * program prints; without it the last frame stays, with the highlight where the answer was.
147
+ *
148
+ * @param screen - builds the element to mount from its {@link ScreenControl}
149
+ * @param options - whether to erase the last frame
150
+ */
151
+ static run = (screen, options) => Effect.gen(function* () {
152
+ if (!(yield* CliInteractive)) return yield* Effect.fail(new NotInteractive());
153
+ const theme = yield* audienceTheme;
154
+ const neutralize = yield* underGithubActions;
155
+ const crash = { current: void 0 };
156
+ const exit = yield* Effect.exit(Semaphore.withPermit(mountPermit, Effect.scoped(mount(screen, theme, options?.clear === true, crash, neutralize))));
157
+ const crashed = crash.current;
158
+ const interrupted = Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause);
159
+ if (crashed === void 0 || interrupted) return yield* exit;
160
+ const dies = Exit.isFailure(exit) ? exit.cause.reasons.filter(Cause.isDieReason) : [];
161
+ if (dies.some((reason) => reason.defect === crashed.defect)) return yield* exit;
162
+ return yield* Effect.failCause(Cause.fromReasons([Cause.makeDieReason(crashed.defect), ...dies]));
163
+ });
164
+ /**
165
+ * The kit's context for an Ink tree the kit did not mount: stdout's theme and glyph set.
166
+ *
167
+ * @remarks
168
+ * Hand it to {@link UiProvider}. It loads Ink and React, as a screen's mount does, so the provider and the kit's
169
+ * hooks can render; a missing peer is a defect naming both. It is the only way to get a `UiContextValue`.
170
+ *
171
+ * Ink loads asynchronously, so this is an `Effect` that must run before the first render. A renderer that is itself
172
+ * synchronous (a test helper, a report-time `renderToString`) runs it once, with a top-level `await` at module
173
+ * scope, and then renders synchronously from the value as often as it likes:
174
+ *
175
+ * ```ts
176
+ * const value = await Effect.runPromise(CliUi.context.pipe(Effect.provide(themeLayer)))
177
+ *
178
+ * // later, synchronously:
179
+ * const text = renderToString(createElement(UiProvider, { value: { ...value, size: { columns, rows } } }, tree), {
180
+ * columns,
181
+ * })
182
+ * ```
183
+ */
184
+ static context = Effect.gen(function* () {
185
+ const theme = yield* audienceTheme;
186
+ const neutralize = yield* underGithubActions;
187
+ yield* loadInk;
188
+ return {
189
+ "~@effected/cli/ui/UiContextValue": true,
190
+ theme,
191
+ glyphs: theme.glyphs,
192
+ ...neutralize ? { neutralizeWorkflowCommands: true } : {}
193
+ };
194
+ });
195
+ /**
196
+ * A live view over a stream: fold `events` into state, and draw it with Ink while a run is going, for the caller's
197
+ * scope.
198
+ *
199
+ * @remarks
200
+ * `events` is a `PubSub` subscription or a stream, folded in a fiber of the caller's scope. A subscription, made
201
+ * before the first publish, is the surest: nothing published after it is missed. `live` makes a stream's first pull
202
+ * before it returns, so one that subscribes on its first pull without forking (`Stream.fromPubSub`) is subscribed by
203
+ * then; one that forks its upstream (`Stream.merge`, `buffer`, a concurrent `flatMap`) subscribes later, and loses
204
+ * what is published before.
205
+ *
206
+ * `live` returns its handle at once, before Ink has loaded: it loads Ink when a run first mounts (or, when not
207
+ * interactive, when an owned run prints its final frame), and it waits on nothing asynchronous before returning, so
208
+ * a host outside Effect can take the handle with `Effect.runSync`. The handle works from the start: a `close`
209
+ * before any run has mounted folds what is queued and ends the view as the events ending would, waiting for a mount
210
+ * already under way, and one with no run to end loads nothing.
211
+ *
212
+ * End the view with `handle.close`: it stops taking events, folds what is still queued (a subscription's queued
213
+ * messages included), commits or prints the run as the events ending would, and waits for `done`. Then close the
214
+ * scope. A publisher may instead end a subscription with `PubSub.end(pubsub, last)`, which keeps everything: the view
215
+ * folds what is buffered and `last` once, then ends. A `PubSub.shutdown` drops what the view has not taken yet, and
216
+ * closing the scope by itself stops the fold at once: both lose a run's tail. Closing the scope unmounts whatever is drawn: the terminal is restored (the
217
+ * cursor shown, Ink's colour level put back) and nothing more is written. `done` completes when the events end.
218
+ *
219
+ * A run begins at an `isStart` event (or wherever `begins` says, given the state before and after the event) and ends
220
+ * at an `isTerminal` event; an event while no run is going that begins none is folded and not drawn, so what a program
221
+ * reports after a run ends never mounts a second copy of it. A run mounts the view; its end unmounts it, which leaves
222
+ * its last frame on the terminal, and the next run mounts afresh below it. A start while a run is drawn redraws in
223
+ * place: the frame is never cleared, so nothing above it is erased. The state is never reset by the kit: a reducer that wants a fresh run
224
+ * resets it on the start.
225
+ *
226
+ * The frame is at most the terminal's rows less one, re-read on every render and on a resize, so a tall frame never
227
+ * makes Ink wipe the scrollback; its width is Ink's own. The clamp
228
+ * lags one paint when the terminal gets shorter: Ink re-lays out and repaints the tree it already has on a resize,
229
+ * before React re-renders with the new row count, so a frame already at the old height can be drawn once taller
230
+ * than the terminal, which Ink answers by clearing the screen and its scrollback. Only a shrink in height while
231
+ * the frame is at its full height does it; a frame that keeps a few rows spare never meets it.
232
+ *
233
+ * While a run is drawn the view also redraws on a tick of `tickMillis` (80 by default), a schedule in the run's
234
+ * scope: interrupted with the run or the scope, it never outlives them. Its timer is not unref'd, so while a run is
235
+ * drawn it keeps the process alive: the run's terminal event, or the scope's close, is what lets the process exit.
236
+ * The frame index never steps back. Events that arrive at
237
+ * once, in one chunk or in several the view had not yet caught up with, are folded together and drawn once. The view
238
+ * takes events from `events` as fast as the stream yields them, so a stream that applies backpressure buffers in the
239
+ * view while it draws.
240
+ *
241
+ * A run whose drawing fails (a `render` that throws, or a mount that fails) degrades rather than ending the view:
242
+ * it is unmounted, leaving its last good frame on the terminal, then one warning is logged (`Effect.logWarning`),
243
+ * and the fold goes on. At its terminal event, a run with no frame left on the terminal (it never painted, or its
244
+ * last good frame threw too) writes its final frame once, as a string. The next run mounts afresh, and so does a
245
+ * start that comes while a degraded run is going: it ends that run as its terminal event would. A `reduce` that
246
+ * throws, or an `events` stream that dies, unmounts the run, then `done` dies with the error.
247
+ *
248
+ * When the run is not interactive, nothing is mounted and Ink is loaded only when a string is due. In the `owned`
249
+ * mode (the default) each run's final frame is written once to stdout, as a string laid out at stdout's width (80
250
+ * when it reports none) with no height to fit, at its terminal event or when the stream ends. It is escape-free at
251
+ * colour `none`, and for an agent audience (`Audience`, when provided) whatever the terminal could do. In the
252
+ * `hosted` mode nothing is written.
253
+ *
254
+ * No input is mounted: the view reads no keys and never enters raw mode, so Ctrl-C stays the platform's SIGINT,
255
+ * which interrupts the program and so closes the scope. Each run holds the process-wide mount permit from its
256
+ * mount to its end, so a `CliUi.run` during a run waits for the run to end, and one between runs mounts at once.
257
+ *
258
+ * While a run is drawn, write logs through `logConsole`, provided around the work the view reports on: its lines
259
+ * land above the frame. A line written to the terminal any other way tears the frame.
260
+ *
261
+ * The view draws on stdout (`UiStreams`), at stdout's colour level and glyphs, and mounts only when the run is
262
+ * interactive (`CliInteractive`).
263
+ *
264
+ * @param options - the events, the fold, the drawing, and what starts and ends a run
265
+ */
266
+ static live = live;
267
+ /**
268
+ * Run `screen` from a handler when the run is interactive; otherwise answer with `otherwise`, or fail with
269
+ * `NotInteractive` when there is none.
270
+ *
271
+ * @remarks
272
+ * `CliUi.run` with a default: not interactive, it returns `otherwise` and Ink and React are never loaded.
273
+ * Interactive, it mounts the screen, and a cancel is the typed `Cancelled` a handler can catch, which
274
+ * `CliRuntime.main` otherwise renders as one line with exit `130`. A missing Ink in an interactive run is a defect
275
+ * naming the peers, never a silent `otherwise`. Screens in sequence make a wizard: discover the defaults first,
276
+ * pass each as an `otherwise`, and a non-interactive run returns exactly them.
277
+ *
278
+ * As with `CliUi.run`, do not log while the screen is mounted: a line written to the terminal from elsewhere tears
279
+ * the frame.
280
+ *
281
+ * @param screen - the screen to show
282
+ * @param options - the non-interactive default, and whether to erase the last frame
283
+ */
284
+ static prompt = (screen, options) => CliUi.run(screen, options?.clear === true ? { clear: true } : void 0).pipe(Effect.catchTag("NotInteractive", (error) => {
285
+ const otherwise = options?.otherwise;
286
+ return otherwise === void 0 ? Effect.fail(error) : Effect.succeed(otherwise);
287
+ }));
288
+ /**
289
+ * A fallback for `Flag.withFallbackPrompt` or `Argument.withFallbackPrompt` that shows a screen when the run is
290
+ * interactive: `CliPrompt.fallback` for screens.
291
+ *
292
+ * @remarks
293
+ * Interactive, the screen mounts and its answer is the parameter's value. Not interactive, `otherwise` is used when
294
+ * given and Ink and React are never loaded; without it the parameter fails as missing, exactly as with no fallback,
295
+ * so core renders its own message and `CliRuntime.main` exits `64`. Name the parameter with `flag` (the name
296
+ * without dashes) or `argument` so that error can be built.
297
+ *
298
+ * The options are {@link @effected/cli!CliPromptFallbackOptions}, the same as `CliPrompt.fallback`'s, and `clear`
299
+ * as for {@link CliUi.run}.
300
+ *
301
+ * It runs during parsing, whose environment is core's alone, so it reads `CliTheme` if one is there: with
302
+ * `CliRuntime.main`'s `env` (`CliEnv.layer`), or provided around the program. With no theme it treats the run as
303
+ * not interactive, and when `CliInteractive` is on it says so once, at debug level. Interactivity is
304
+ * `CliInteractive`, which an audience flag can set before parsing under `CliAudience`.
305
+ *
306
+ * As with `CliPrompt.fallback`, the screen runs here rather than being handed to core, whose fallback runner
307
+ * turns a quit into the missing-parameter error, which would exit `64`. A cancel (Esc, Ctrl-C) is `Cancelled`,
308
+ * raised as a defect because core's parse step turns every typed failure into a usage error, so only
309
+ * `CliRuntime.main` (or `CliRuntime.reportFailures`) renders it, as one line with exit `130`. A missing Ink in an
310
+ * interactive run is a defect naming the peers, never a silent `otherwise`.
311
+ *
312
+ * After the screen has unmounted, core still runs the answered `Prompt.succeed` it is handed against the
313
+ * terminal, exactly as it does for `CliPrompt.fallback`: `Prompt.run` opens the terminal's input in a scope (on
314
+ * Node a readline over stdin, in raw mode) before looking at the prompt. That is harmless. The prompt is already
315
+ * answered, so nothing is read and no key is waited for; the scope closes at once, restoring the mode and closing
316
+ * the reader; and Ink has already let go of stdin, so the two never hold it together. Not interactive, the screen
317
+ * never mounts, and `CliPrompt.gateTerminal`, which `CliEnv.layer` installs, keeps that subscription off the real
318
+ * terminal altogether.
319
+ *
320
+ * @param screen - the screen to show
321
+ * @param options - the parameter it stands in for, the non-interactive default, and whether to erase the last frame
322
+ */
323
+ static fallback = (screen, options) => {
324
+ let explained = false;
325
+ return Effect.gen(function* () {
326
+ const theme = yield* Effect.serviceOption(CliTheme);
327
+ if (Option.isNone(theme)) {
328
+ if (!explained && (yield* CliInteractive)) {
329
+ explained = true;
330
+ const name = "flag" in options ? `--${options.flag}` : `<${options.argument}>`;
331
+ yield* Effect.logDebug(`@effected/cli/ui: CliUi.fallback for ${name} answered without its screen: CliInteractive is on, but no CliTheme is provided around parsing (CliRuntime.main's env provides one)`);
332
+ }
333
+ return yield* answerWithoutPerson(options);
334
+ }
335
+ return yield* CliUi.run(screen, options.clear === true ? { clear: true } : void 0).pipe(Effect.provideService(CliTheme, theme.value), Effect.map((answer) => Prompt.succeed(answer)), Effect.catchTag("Cancelled", (cancelled) => Effect.die(cancelled)), Effect.catchTag("NotInteractive", () => answerWithoutPerson(options)));
336
+ });
337
+ };
338
+ /**
339
+ * A screen whose module is loaded only when it mounts, so importing the command that uses it loads neither the
340
+ * screen's own code nor React.
341
+ *
342
+ * @param load - imports the module whose default export is the screen
343
+ */
344
+ static lazy = (load) => async (control) => (await load()).default(control);
345
+ };
346
+
347
+ //#endregion
348
+ export { CliUi };