@effected/cli-kit 0.0.0-stage → 0.26.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 (103) hide show
  1. package/Cancelled.js +43 -0
  2. package/CliAudience.js +177 -0
  3. package/CliColor.js +59 -0
  4. package/CliEnv.js +101 -0
  5. package/CliExit.js +64 -0
  6. package/CliFailure.js +287 -0
  7. package/CliFormat.js +124 -0
  8. package/CliInteractive.js +94 -0
  9. package/CliLinks.js +153 -0
  10. package/CliLog.js +367 -0
  11. package/CliLogger.js +91 -0
  12. package/CliMessage.js +79 -0
  13. package/CliPrompt.js +110 -0
  14. package/CliRuntime.js +254 -0
  15. package/CliTest.js +206 -0
  16. package/CliTheme.js +142 -0
  17. package/ConfigIssueRenderer.js +63 -0
  18. package/Doc.js +548 -0
  19. package/Fmt.js +335 -0
  20. package/GithubAnnotation.js +39 -0
  21. package/Glyphs.js +82 -0
  22. package/LICENSE +21 -0
  23. package/NotInteractive.js +41 -0
  24. package/README.md +215 -2
  25. package/Render.js +280 -0
  26. package/SchemaIssueRenderer.js +47 -0
  27. package/Status.js +165 -0
  28. package/TestTerminal.js +101 -0
  29. package/Token.js +68 -0
  30. package/index.d.ts +3951 -0
  31. package/index.js +27 -0
  32. package/internal/ExitRequested.js +21 -0
  33. package/internal/HelpRouting.js +35 -0
  34. package/internal/ansi.js +229 -0
  35. package/internal/autoFormat.js +33 -0
  36. package/internal/canPrompt.js +14 -0
  37. package/internal/counts.js +83 -0
  38. package/internal/diagnostics.js +31 -0
  39. package/internal/displayPath.js +46 -0
  40. package/internal/displayWidth.js +42 -0
  41. package/internal/failureTarget.js +195 -0
  42. package/internal/fallbackAnswer.js +17 -0
  43. package/internal/fileSink.js +61 -0
  44. package/internal/format.js +175 -0
  45. package/internal/inspect.js +40 -0
  46. package/internal/isExitCode.js +11 -0
  47. package/internal/layout.js +249 -0
  48. package/internal/linkScheme.js +29 -0
  49. package/internal/linkTarget.js +49 -0
  50. package/internal/logSafety.js +45 -0
  51. package/internal/renderAnsi.js +51 -0
  52. package/internal/renderDoc.js +322 -0
  53. package/internal/renderGithubLog.js +59 -0
  54. package/internal/renderMarkdown.js +367 -0
  55. package/internal/renderPlain.js +49 -0
  56. package/internal/scanAudience.js +105 -0
  57. package/internal/splitFrame.js +55 -0
  58. package/internal/terminalCapabilities.js +23 -0
  59. package/internal/wizardGate.js +17 -0
  60. package/package.json +90 -5
  61. package/testing.d.ts +246 -0
  62. package/testing.js +4 -0
  63. package/tsdoc-metadata.json +11 -0
  64. package/ui/CliUi.js +507 -0
  65. package/ui/CliUiDetached.js +36 -0
  66. package/ui/CliUiLive.js +436 -0
  67. package/ui/CliUiRouteStream.js +72 -0
  68. package/ui/Confirm.js +244 -0
  69. package/ui/DocView.js +76 -0
  70. package/ui/KeyHelp.js +61 -0
  71. package/ui/KeyTable.js +198 -0
  72. package/ui/MultiSelect.js +259 -0
  73. package/ui/Select.js +229 -0
  74. package/ui/Tabs.js +201 -0
  75. package/ui/TextInput.js +289 -0
  76. package/ui/Toggle.js +31 -0
  77. package/ui/UiKey.js +48 -0
  78. package/ui/UiProvider.js +62 -0
  79. package/ui/UiStreams.js +17 -0
  80. package/ui/UiTheme.js +147 -0
  81. package/ui/Viewport.js +203 -0
  82. package/ui/internal/ErrorBoundary.js +29 -0
  83. package/ui/internal/Holder.js +73 -0
  84. package/ui/internal/ScreenContext.js +51 -0
  85. package/ui/internal/UiProviders.js +38 -0
  86. package/ui/internal/ink.js +121 -0
  87. package/ui/internal/inkChalk.js +47 -0
  88. package/ui/internal/inkConsole.js +150 -0
  89. package/ui/internal/lazyView.js +73 -0
  90. package/ui/internal/lineRouter.js +70 -0
  91. package/ui/internal/lineText.js +18 -0
  92. package/ui/internal/mountPermit.js +15 -0
  93. package/ui/internal/processStreams.js +18 -0
  94. package/ui/internal/renderOptions.js +12 -0
  95. package/ui/testing/CliUiTest.js +792 -0
  96. package/ui/testing/fakeStreams.js +104 -0
  97. package/ui/testing/terminalModel.js +67 -0
  98. package/ui-testing-serializer.d.ts +12 -0
  99. package/ui-testing-serializer.js +32 -0
  100. package/ui-testing.d.ts +558 -0
  101. package/ui-testing.js +3 -0
  102. package/ui.d.ts +1990 -0
  103. package/ui.js +17 -0
@@ -0,0 +1,104 @@
1
+ import { PassThrough, Writable } from "node:stream";
2
+
3
+ // src/ui/testing/fakeStreams.ts
4
+ /** The hide-cursor escape `cli-cursor` writes to a TTY stream as it arms the restore. */
5
+ const HIDE_CURSOR = `${String.fromCharCode(27)}[?25l`;
6
+ /** A cursor-show escape in a listener's source, as `restore-cursor` writes it (`'\u001B[?25h'`) or as the raw byte. */
7
+ const CURSOR_SHOW = /\[\?25h/;
8
+ /**
9
+ * Disarm the real terminal's cursor restore that a fake TTY arms, so no byte of a run on the fakes reaches the
10
+ * process's own streams (#983). The fake streams call it as the hide escape is written to them.
11
+ *
12
+ * @remarks
13
+ * Ink's frame writer hides the cursor through `cli-cursor` on its first render, and `cli-cursor` does it only for a
14
+ * stream that says it is a TTY, which the fakes do. Hiding it also arms `restore-cursor`, once per loaded copy of it
15
+ * (once per test file where the runner isolates modules): a `signal-exit` hook (`alwaysLast`, so on the `afterexit`
16
+ * event) that writes the cursor-show escape to the REAL `process.stderr` when the process exits or is signalled,
17
+ * whatever stream Ink was given and whether or not stderr is a terminal. Nothing on the fakes hid the real cursor, so
18
+ * that restore is only ever a stray byte in the test runner's output: this removes every `afterexit` listener that
19
+ * writes a cursor-show escape, right after the copy that armed it wrote its hide escape. The emitter is `signal-exit` 3's process-wide one, the version `restore-cursor` 4
20
+ * (the one Ink 8's `cli-cursor` takes) loads; without one there is nothing to disarm.
21
+ *
22
+ * The one read of `process` in the testing fakes, under the process-streams licence: it touches no stream.
23
+ */
24
+ const disarmCursorRestore = () => {
25
+ const emitter = process.__signal_exit_emitter__;
26
+ if (emitter === void 0) return;
27
+ for (const listener of emitter.listeners("afterexit")) if (CURSOR_SHOW.test(String(listener))) emitter.removeListener("afterexit", listener);
28
+ };
29
+ const capture = (columns, rows, both, onWrite) => {
30
+ const chunks = [];
31
+ const stream = new Writable({ write(chunk, _encoding, callback) {
32
+ const text = chunk.toString();
33
+ if (text.includes(HIDE_CURSOR)) disarmCursorRestore();
34
+ chunks.push(text);
35
+ both.push(text);
36
+ onWrite?.(text);
37
+ callback();
38
+ } });
39
+ Object.assign(stream, {
40
+ isTTY: true,
41
+ columns,
42
+ rows
43
+ });
44
+ return {
45
+ stream,
46
+ text: () => chunks.join("")
47
+ };
48
+ };
49
+ /**
50
+ * Make in-memory stdin, stdout and stderr that satisfy Ink's stream contract: TTYs with a size, a recorded
51
+ * `setRawMode`, `ref` and `unref`, and captured writes.
52
+ *
53
+ * @remarks
54
+ * The third file licensed to touch Node, testing only: Ink's stream
55
+ * contract is Node's, so the fakes are `node:stream` streams rather than a hand-rolled emitter that would have to
56
+ * reproduce `readable`, `read()`, `setEncoding` and the write-callback barrier Ink waits on at unmount.
57
+ *
58
+ * @internal
59
+ */
60
+ const makeFakeStreams = (options = {}) => {
61
+ const columns = options.columns ?? 80;
62
+ const rows = options.rows ?? 24;
63
+ const rawModes = [];
64
+ const stdin = new PassThrough();
65
+ Object.assign(stdin, {
66
+ isTTY: true,
67
+ setRawMode: (mode) => {
68
+ rawModes.push(mode);
69
+ return stdin;
70
+ },
71
+ ref: () => stdin,
72
+ unref: () => stdin
73
+ });
74
+ const both = [];
75
+ const stdout = capture(columns, rows, both, options.onStdoutWrite);
76
+ const stderr = capture(columns, rows, both);
77
+ return {
78
+ streams: {
79
+ stdin,
80
+ stdout: stdout.stream,
81
+ stderr: stderr.stream
82
+ },
83
+ rawModes,
84
+ stdout: stdout.text,
85
+ stderr: stderr.text,
86
+ written: () => both.join(""),
87
+ input: (data) => {
88
+ stdin.write(data);
89
+ },
90
+ resize: (nextColumns, nextRows) => {
91
+ Object.assign(stdout.stream, {
92
+ columns: nextColumns,
93
+ rows: nextRows
94
+ });
95
+ Object.assign(stderr.stream, {
96
+ columns: nextColumns,
97
+ rows: nextRows
98
+ });
99
+ stdout.stream.emit("resize");
100
+ }
101
+ };
102
+ };
103
+
104
+ export { makeFakeStreams };
@@ -0,0 +1,67 @@
1
+ // src/ui/testing/terminalModel.ts
2
+ const ESC = String.fromCharCode(27);
3
+ /**
4
+ * A small terminal model for the production render path: what a terminal shows after `written`, as its non-empty
5
+ * lines.
6
+ *
7
+ * @remarks
8
+ * It applies printable text, line feeds, the erase and cursor moves Ink's log-update writes (erase line, cursor up,
9
+ * down and forward, cursor to a column, cursor to the next line), and the clears of Ink's full-clear frame: `ESC[J`
10
+ * erases from the cursor to the end of the screen, `ESC[2J` blanks the visible screen, `ESC[3J` drops the scrollback
11
+ * above it, `ESC[H` homes the cursor to the screen's top left. The visible screen is the last
12
+ * `rows` lines; with `rows` unknown, the whole buffer counts as the screen, so a clear takes everything. Every other
13
+ * escape is ignored. It does not wrap a line wider than the terminal. Shared by `CliUiTest.live`'s transcript and the
14
+ * kit's own production-path tests.
15
+ *
16
+ * @param written - every byte written to the terminal
17
+ * @param rows - the terminal's height, which decides how much of the buffer a clear takes as the screen
18
+ *
19
+ * @internal
20
+ */
21
+ const screenAfter = (written, rows) => {
22
+ const lines = [""];
23
+ let row = 0;
24
+ let column = 0;
25
+ const screenTop = () => rows === void 0 ? 0 : Math.max(0, lines.length - rows);
26
+ const sequence = new RegExp(`${ESC}\\[([0-9;?]*)([A-Za-z])|${ESC}\\][^\\u0007]*\\u0007|([\\s\\S])`, "g");
27
+ for (const match of written.matchAll(sequence)) {
28
+ const [, params, command, character] = match;
29
+ if (character !== void 0) {
30
+ if (character === "\n") {
31
+ row++;
32
+ column = 0;
33
+ while (lines.length <= row) lines.push("");
34
+ } else if (character === "\r") column = 0;
35
+ else if (character >= " ") {
36
+ const line = lines[row] ?? "";
37
+ lines[row] = `${line.slice(0, column).padEnd(column)}${character}${line.slice(column + 1)}`;
38
+ column++;
39
+ }
40
+ } else if (command === "K") lines[row] = "";
41
+ else if (command === "A") row = Math.max(0, row - Number(params === "" ? 1 : params));
42
+ else if (command === "B" || command === "E") {
43
+ row += Number(params === "" ? 1 : params);
44
+ if (command === "E") column = 0;
45
+ while (lines.length <= row) lines.push("");
46
+ } else if (command === "C") column += Number(params === "" ? 1 : params);
47
+ else if (command === "G") column = Math.max(0, Number(params === "" ? 1 : params) - 1);
48
+ else if (command === "J" && (params === "" || params === "0")) {
49
+ lines[row] = (lines[row] ?? "").slice(0, column);
50
+ for (let index = row + 1; index < lines.length; index++) lines[index] = "";
51
+ } else if (command === "J" && params === "2") for (let index = screenTop(); index < lines.length; index++) lines[index] = "";
52
+ else if (command === "J" && params === "3") {
53
+ const top = screenTop();
54
+ lines.splice(0, top);
55
+ row = Math.max(0, row - top);
56
+ if (lines.length === 0) lines.push("");
57
+ } else if (command === "H") {
58
+ const [line, col] = params === "" ? [1, 1] : params.split(";").map((part) => Number(part === "" ? 1 : part));
59
+ row = screenTop() + (line ?? 1) - 1;
60
+ column = (col ?? 1) - 1;
61
+ while (lines.length <= row) lines.push("");
62
+ }
63
+ }
64
+ return lines.map((line) => line.trimEnd()).filter((line) => line !== "");
65
+ };
66
+
67
+ export { screenAfter };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `CliUiTest.serializer`: it claims a string carrying escapes or token markup and prints it as token markup with each
3
+ * line's trailing spaces trimmed. See `CliUiTest.serializer` for what it claims.
4
+ *
5
+ * @public
6
+ */
7
+ declare const serializer: {
8
+ readonly test: (value: unknown) => boolean;
9
+ readonly serialize: (value: unknown) => string;
10
+ };
11
+ export { serializer as default };
12
+ //# sourceMappingURL=ui-testing-serializer.d.ts.map
@@ -0,0 +1,32 @@
1
+ import { CliUiTest } from "./ui/testing/CliUiTest.js";
2
+
3
+ // src/ui-testing-serializer.ts
4
+ /**
5
+ * `CliUiTest.serializer` as a module's default export, for Vitest's `snapshotSerializers` config.
6
+ *
7
+ * @remarks
8
+ * Vitest's `test.snapshotSerializers` takes module paths whose default export is a serializer, so registering this
9
+ * one needs no `expect.addSnapshotSerializer` call and no shim file of your own:
10
+ *
11
+ * ```ts
12
+ * // vitest.config.ts
13
+ * import { defineConfig } from "vitest/config"
14
+ *
15
+ * export default defineConfig({
16
+ * test: { snapshotSerializers: ["@effected/cli-kit/ui/testing/serializer"] },
17
+ * })
18
+ * ```
19
+ *
20
+ * A separate entrypoint so a program's runtime import graph never loads test code.
21
+ *
22
+ * @packageDocumentation
23
+ */
24
+ /**
25
+ * `CliUiTest.serializer`: it claims a string carrying escapes or token markup and prints it as token markup with each
26
+ * line's trailing spaces trimmed. See `CliUiTest.serializer` for what it claims.
27
+ *
28
+ * @public
29
+ */
30
+ const serializer = CliUiTest.serializer;
31
+
32
+ export { serializer as default };