@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
@@ -0,0 +1,146 @@
1
+ import { inkModules } from "./ink.js";
2
+ import { UiStreams } from "../UiStreams.js";
3
+ import { Effect, Inspectable } from "effect";
4
+
5
+ //#region src/ui/internal/inkConsole.ts
6
+ /** One argument as a console shows it: a string as is, an `Error` with its stack, anything else as data (JSON). */
7
+ const shown = (arg) => arg instanceof Error ? arg.stack ?? String(arg) : Inspectable.toStringUnknown(arg, 0);
8
+ /** A console call's arguments as one line of text, joined by spaces. */
9
+ const textOf = (args) => args.map(shown).join(" ");
10
+ /** `console.table`'s rows, as a plain pipe table: an `(index)` column, then each key, then `Values` for scalars. */
11
+ const tableOf = (data, properties) => {
12
+ if (data === null || typeof data !== "object") return textOf([data]);
13
+ const rows = Object.entries(data);
14
+ const isRecord = (value) => value !== null && typeof value === "object";
15
+ const keys = properties ?? [...new Set(rows.flatMap(([, value]) => isRecord(value) ? Object.keys(value) : []))];
16
+ const scalars = rows.some(([, value]) => !isRecord(value));
17
+ const header = [
18
+ "(index)",
19
+ ...keys,
20
+ ...scalars ? ["Values"] : []
21
+ ];
22
+ const cell = (value) => value === void 0 ? "" : typeof value === "string" ? value : shown(value);
23
+ const body = rows.map(([index, value]) => [
24
+ index,
25
+ ...keys.map((key) => isRecord(value) ? cell(value[key]) : ""),
26
+ ...scalars ? [isRecord(value) ? "" : cell(value)] : []
27
+ ]);
28
+ const widths = header.map((title, column) => Math.max(title.length, ...body.map((row) => (row[column] ?? "").length)));
29
+ const line = (cells) => `| ${cells.map((text, column) => text.padEnd(widths[column] ?? 0)).join(" | ")} |`;
30
+ return [line(header), ...body.map(line)].join("\n");
31
+ };
32
+ /** Milliseconds now, for `time`; the platform clock when there is one. */
33
+ const now = () => globalThis.performance?.now() ?? Date.now();
34
+ /**
35
+ * Build an `InkConsole` over `UiStreams`.
36
+ *
37
+ * @remarks
38
+ * Ink's writers (`useStdout().write`, `useStderr().write`) clear the frame, write the line and repaint the frame, so a
39
+ * line written through them lands above the frame without tearing it; a raw write to the stream lands inside the frame
40
+ * instead. They are reachable only from inside the tree, which is what the `Bridge` is for. Ink drops whatever they
41
+ * are handed once it has unmounted, so the owner detaches before it unmounts, and a line written after goes straight
42
+ * to the stream.
43
+ *
44
+ * Every `Console` method is the bridge's own, none the ambient console's: one that fell through would write to the
45
+ * process's terminal past Ink, tearing the frame and escaping `UiStreams`.
46
+ *
47
+ * @internal
48
+ */
49
+ const makeInkConsole = Effect.gen(function* () {
50
+ const streams = yield* UiStreams;
51
+ let attached;
52
+ let indent = "";
53
+ const counts = /* @__PURE__ */ new Map();
54
+ const timers = /* @__PURE__ */ new Map();
55
+ const emit = (stream, text) => {
56
+ const data = `${text.split("\n").map((line) => indent + line).join("\n")}\n`;
57
+ if (attached !== void 0) (stream === "out" ? attached.out : attached.err)(data);
58
+ else if (stream === "out") streams.stdout.write(data);
59
+ else streams.stderr.write(data);
60
+ };
61
+ const toOut = (...args) => emit("out", textOf(args));
62
+ const toErr = (...args) => emit("err", textOf(args));
63
+ const elapsed = (label, method, extra) => {
64
+ const started = timers.get(label);
65
+ if (started === void 0) {
66
+ emit("err", `Warning: No such label '${label}' for console.${method}()`);
67
+ return;
68
+ }
69
+ const tail = extra.length === 0 ? "" : ` ${textOf(extra)}`;
70
+ emit("out", `${label}: ${(now() - started).toFixed(3)}ms${tail}`);
71
+ };
72
+ const group = (...label) => {
73
+ if (label.length > 0) toOut(...label);
74
+ indent += " ";
75
+ };
76
+ const writer = {
77
+ log: toOut,
78
+ info: toOut,
79
+ debug: toOut,
80
+ dirxml: toOut,
81
+ error: toErr,
82
+ warn: toErr,
83
+ trace: (...args) => {
84
+ const stack = ((/* @__PURE__ */ new Error()).stack ?? "").split("\n").slice(2).join("\n");
85
+ emit("err", `Trace${args.length === 0 ? "" : `: ${textOf(args)}`}${stack === "" ? "" : `\n${stack}`}`);
86
+ },
87
+ dir: (item) => toOut(item),
88
+ table: (data, properties) => emit("out", tableOf(data, properties)),
89
+ assert: (condition, ...args) => {
90
+ if (!condition) emit("err", `Assertion failed${args.length === 0 ? "" : `: ${textOf(args)}`}`);
91
+ },
92
+ count: (label = "default") => {
93
+ const next = (counts.get(label) ?? 0) + 1;
94
+ counts.set(label, next);
95
+ emit("out", `${label}: ${next}`);
96
+ },
97
+ countReset: (label = "default") => {
98
+ counts.delete(label);
99
+ },
100
+ group,
101
+ groupCollapsed: group,
102
+ groupEnd: () => {
103
+ indent = indent.slice(2);
104
+ },
105
+ time: (label = "default") => {
106
+ timers.set(label, now());
107
+ },
108
+ timeLog: (label = "default", ...extra) => elapsed(label, "timeLog", extra),
109
+ timeEnd: (label = "default") => {
110
+ elapsed(label, "timeEnd", []);
111
+ timers.delete(label);
112
+ },
113
+ clear: () => void 0
114
+ };
115
+ const Bridge = (props) => {
116
+ const { ink, react } = inkModules();
117
+ const out = ink.useStdout().write;
118
+ const err = ink.useStderr().write;
119
+ react.useLayoutEffect(() => {
120
+ const writers = {
121
+ out,
122
+ err
123
+ };
124
+ attached = writers;
125
+ return () => {
126
+ if (attached === writers) attached = void 0;
127
+ };
128
+ }, [out, err]);
129
+ return props.children ?? null;
130
+ };
131
+ return {
132
+ writer,
133
+ print: (text) => {
134
+ const data = `${text}\n`;
135
+ if (attached !== void 0) attached.out(data);
136
+ else streams.stdout.write(data);
137
+ },
138
+ Bridge,
139
+ detach: () => {
140
+ attached = void 0;
141
+ }
142
+ };
143
+ });
144
+
145
+ //#endregion
146
+ export { makeInkConsole };
@@ -0,0 +1,74 @@
1
+ import { Effect } from "effect";
2
+
3
+ //#region src/ui/internal/lazyView.ts
4
+ /** Where a lazy view keeps its loader: a `Symbol.for` key, so two copies of the package agree on it. */
5
+ const LOAD = Symbol.for("@effected/cli/ui/lazyView");
6
+ const NOT_LOADED = "@effected/cli/ui: a CliUi.lazyView render was called before its module loaded; only CliUi.live loads it first";
7
+ const kindOf = (value) => value === null ? "null" : Array.isArray(value) ? "an array" : typeof value === "object" ? "an object" : typeof value;
8
+ const EXPECTED = "a view, (state, frame) => ReactElement, or a module whose default export is one: { default: view }";
9
+ /** What a `load` resolved to that is no view: said with what was received and what is expected. */
10
+ const NO_VIEW = (resolved) => {
11
+ if (typeof resolved !== "object" || resolved === null) return `@effected/cli/ui: CliUi.lazyView's load resolved to ${kindOf(resolved)}. Expected ${EXPECTED}`;
12
+ const named = Object.keys(resolved).filter((key) => key !== "default");
13
+ const exports = named.length === 0 ? "" : ` (it exports ${named.join(", ")})`;
14
+ return `@effected/cli/ui: CliUi.lazyView's load resolved to ${Object.hasOwn(resolved, "default") ? `a module whose default export is ${kindOf(resolved.default)}, not a function${exports}; a CommonJS module imported as ESM nests it one level deeper, as default.default` : `a module with no default export${exports}; resolve to the export itself, as .then((module) => module.name)`}. Expected ${EXPECTED}`;
15
+ };
16
+ /**
17
+ * A load that resolved to no view: deterministic, so a lazy view keeps it (one error object for the handle's life) and
18
+ * the live view warns about it once, where a failed import is tried again by the next run.
19
+ *
20
+ * @internal
21
+ */
22
+ var LazyViewShapeError = class extends Error {};
23
+ /** The view `load` resolved to: the value itself when it is a function (a `default` property on it is ignored), else
24
+ * its `default` when that is a function; anything else throws, saying what it got. */
25
+ const pick = (resolved) => {
26
+ if (typeof resolved === "function") return resolved;
27
+ if (typeof resolved === "object" && resolved !== null) {
28
+ const fallback = resolved.default;
29
+ if (typeof fallback === "function") return fallback;
30
+ }
31
+ throw new LazyViewShapeError(NO_VIEW(resolved));
32
+ };
33
+ /**
34
+ * `CliUi.lazyView`: a `render` that draws with what `load` resolves to, loaded on first use: the render itself, or a
35
+ * module whose default export it is. One load is shared: an import that rejected is cleared, so the next run loads
36
+ * again; a load that resolved to no view is kept, a `LazyViewShapeError` every later run gets as is. Calling the render
37
+ * before it has loaded is a defect, since only `CliUi.live` knows to load it first.
38
+ *
39
+ * @internal
40
+ */
41
+ const lazyView = (load) => {
42
+ let loaded;
43
+ let pending;
44
+ const ensure = () => {
45
+ pending ??= load().then((resolved) => {
46
+ loaded = pick(resolved);
47
+ }, (error) => {
48
+ pending = void 0;
49
+ throw error;
50
+ });
51
+ return pending;
52
+ };
53
+ const render = (state, frame) => {
54
+ if (loaded === void 0) throw new Error(NOT_LOADED);
55
+ return loaded(state, frame);
56
+ };
57
+ return Object.assign(render, { [LOAD]: ensure });
58
+ };
59
+ /**
60
+ * Load a lazy view's module before its render is first called; nothing for a render that is not lazy. Fails with what
61
+ * the import failed with.
62
+ *
63
+ * @internal
64
+ */
65
+ const loadView = (render) => {
66
+ const ensure = render[LOAD];
67
+ return ensure === void 0 ? Effect.void : Effect.asVoid(Effect.tryPromise({
68
+ try: ensure,
69
+ catch: (error) => error
70
+ }));
71
+ };
72
+
73
+ //#endregion
74
+ export { LazyViewShapeError, lazyView, loadView };
@@ -0,0 +1,19 @@
1
+ import { sanitize } from "../../Fmt.js";
2
+
3
+ //#region src/ui/internal/lineText.ts
4
+ /**
5
+ * Consumer text made safe for one row of a widget: sanitised as `Fmt.sanitize` does (no escape sequence, no other
6
+ * control character), with each line break (CR, LF or CR LF) folded to a space.
7
+ *
8
+ * @remarks
9
+ * Every string a kit widget draws from data goes through it before it is measured or cut. Ink keeps SGR and OSC
10
+ * sequences it is handed, so unsanitised text would paint colour at colour `none` and could plant a hyperlink whose
11
+ * target differs from its label; and a widget's row budget counts one row per line, so a second line would push the
12
+ * frame past the terminal, where Ink wipes the screen and its scrollback.
13
+ *
14
+ * @internal
15
+ */
16
+ const lineText = (text) => sanitize(text).replace(/\r\n|\r|\n/g, " ");
17
+
18
+ //#endregion
19
+ export { lineText };
@@ -0,0 +1,16 @@
1
+ import { Semaphore } from "effect";
2
+
3
+ //#region src/ui/internal/mountPermit.ts
4
+ /**
5
+ * One Ink mount at a time, process-wide: a `CliUi.run` screen, or one run of a live view.
6
+ *
7
+ * @remarks
8
+ * Ink keys its instances by stdout and owns raw mode on the one terminal, so concurrent mounts are meaningless, and
9
+ * serializing them keeps each mount's save-and-restore of Ink's colour level well nested.
10
+ *
11
+ * @internal
12
+ */
13
+ const mountPermit = Semaphore.makeUnsafe(1);
14
+
15
+ //#endregion
16
+ export { mountPermit };
@@ -0,0 +1,33 @@
1
+ import { Config, Effect } from "effect";
2
+
3
+ //#region src/ui/internal/perfDrain.ts
4
+ /**
5
+ * Whether a drain mode drains: `true` and `false` as given, and `"auto"` unless `NODE_ENV` is exactly `"production"`.
6
+ *
7
+ * @remarks
8
+ * React picks its development build whenever `NODE_ENV` is not exactly `"production"`, and that build records
9
+ * user-timing entries on every render and never clears them. An
10
+ * unset `NODE_ENV`, the common case for a CLI, leaks like `"development"`, so `"auto"` drains then too. `NODE_ENV` is
11
+ * read through `Config`, so a test sets it with a `ConfigProvider` and `./ui` reads no `process`.
12
+ *
13
+ * @internal
14
+ */
15
+ const resolveDrain = (mode) => mode === "auto" ? Config.String("NODE_ENV").pipe(Config.withDefault(""), Effect.map((env) => env !== "production"), Effect.orElseSucceed(() => true)) : Effect.succeed(mode);
16
+ /**
17
+ * Clear every user-timing `measure` entry in the process, when `drain` is true.
18
+ *
19
+ * @remarks
20
+ * The clear is global: it removes a consumer's own measures too, because the platform's `clearMeasures` filters only
21
+ * by name, and React's entries (`Update`, `Mount`, tagged `detail.devtools`) share their names with anything a
22
+ * consumer might call a measure. Marks are left alone: React's development build leaks measures only, never marks,
23
+ * so clearing marks would only take a host's own. A runtime without `performance` is left alone.
24
+ *
25
+ * @internal
26
+ */
27
+ const drainPerformance = (drain) => {
28
+ if (!drain) return;
29
+ globalThis.performance?.clearMeasures?.();
30
+ };
31
+
32
+ //#endregion
33
+ export { drainPerformance, resolveDrain };
@@ -0,0 +1,19 @@
1
+ //#region src/ui/internal/processStreams.ts
2
+ /**
3
+ * The process's own standard streams, the default `UiStreams`.
4
+ *
5
+ * @remarks
6
+ * One of the three files licensed to touch Node: it reads
7
+ * `process.stdin`, `process.stdout` and `process.stderr` and nothing else, and only when called, never at import.
8
+ * The boundary test holds that licence exact.
9
+ *
10
+ * @internal
11
+ */
12
+ const processStreams = () => ({
13
+ stdin: process.stdin,
14
+ stdout: process.stdout,
15
+ stderr: process.stderr
16
+ });
17
+
18
+ //#endregion
19
+ export { processStreams };
@@ -0,0 +1,13 @@
1
+ import { Context } from "effect";
2
+
3
+ //#region src/ui/internal/renderOptions.ts
4
+ /**
5
+ * The render-option overrides in force: none by default. Internal, so `CliUi.run`'s public signature stays as
6
+ * specified; `CliUiTest` sets it.
7
+ *
8
+ * @internal
9
+ */
10
+ var UiRenderOptions = class extends Context.Reference("@effected/cli/ui/UiRenderOptions", { defaultValue: () => ({}) }) {};
11
+
12
+ //#endregion
13
+ export { UiRenderOptions };