@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
package/Status.js ADDED
@@ -0,0 +1,165 @@
1
+ import { sanitize } from "./Fmt.js";
2
+ import { Array, Option } from "effect";
3
+
4
+ // src/Status.ts
5
+ /**
6
+ * An open vocabulary of statuses.
7
+ *
8
+ * @remarks
9
+ * Start from `Status.core` and add your own with `extend`. The names are a type parameter,
10
+ * so `def` and `worst` reject a name the vocabulary does not have at compile time.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { Status } from "@effected/cli-kit"
15
+ *
16
+ * const vocab = Status.extend({
17
+ * timeout: { glyph: "⏱", ascii: "[time]", token: "warning", rank: 85 },
18
+ * })
19
+ * const worst = vocab.worst(["success", "timeout"])
20
+ * // => "timeout"
21
+ * ```
22
+ *
23
+ * @public
24
+ */
25
+ var Status = class Status {
26
+ defs;
27
+ constructor(defs) {
28
+ this.defs = defs;
29
+ }
30
+ /** The core vocabulary: success, skip, pending, info, warning and failure, by rank 10, 20, 30, 40, 60, 90. */
31
+ static core = new Status({
32
+ success: {
33
+ glyph: "✓",
34
+ ascii: "[ok]",
35
+ token: "success",
36
+ rank: 10
37
+ },
38
+ skip: {
39
+ glyph: "↷",
40
+ ascii: "[skip]",
41
+ token: "muted",
42
+ rank: 20
43
+ },
44
+ pending: {
45
+ glyph: "◯",
46
+ ascii: "[ ]",
47
+ token: "muted",
48
+ rank: 30
49
+ },
50
+ info: {
51
+ glyph: "ℹ",
52
+ ascii: "[info]",
53
+ token: "info",
54
+ rank: 40
55
+ },
56
+ warning: {
57
+ glyph: "⚠",
58
+ ascii: "[warn]",
59
+ token: "warning",
60
+ rank: 60
61
+ },
62
+ failure: {
63
+ glyph: "✗",
64
+ ascii: "[FAIL]",
65
+ token: "failure",
66
+ rank: 90
67
+ }
68
+ });
69
+ /**
70
+ * The core vocabulary plus `extra`; an entry that reuses a core name replaces it.
71
+ *
72
+ * @param extra - the statuses to add, by name
73
+ */
74
+ static extend = (extra) => Status.core.extend(extra);
75
+ /**
76
+ * This vocabulary plus `extra`; an entry that reuses a name replaces it.
77
+ *
78
+ * @remarks
79
+ * An entry may replace a core name. Replacing `warning` with a lower rank moves the threshold at which
80
+ * `CliMessage.status` defaults to stderr for this vocabulary, since that threshold is `warning`'s rank in the
81
+ * vocabulary it is given.
82
+ *
83
+ * @param extra - the statuses to add, by name
84
+ */
85
+ extend(extra) {
86
+ return new Status({
87
+ ...this.defs,
88
+ ...extra
89
+ });
90
+ }
91
+ /**
92
+ * The definition of a status.
93
+ *
94
+ * @remarks
95
+ * A name the vocabulary does not have is a defect: it throws an `Error` naming it and the names that exist. The
96
+ * types already reject one, so it is reachable only through a cast.
97
+ *
98
+ * @param name - a name in this vocabulary
99
+ */
100
+ def(name) {
101
+ if (!Object.hasOwn(this.defs, name)) throw new Error(`Unknown status "${name}"; this vocabulary has: ${Object.keys(this.defs).join(", ")}`);
102
+ return this.defs[name];
103
+ }
104
+ /**
105
+ * The full definition of a status as an immutable snapshot, for a caller that stores it.
106
+ *
107
+ * @remarks
108
+ * `def` answers the vocabulary's own entry. `resolve` answers a frozen copy, so a document node that holds
109
+ * the definition stays plain data and editing it cannot change the vocabulary. The copy is shallow: a
110
+ * `token` given as a `Style` keeps its own identity. Throws on an unknown name, as {@link Status.def} does;
111
+ * storing an empty definition in a document instead would fail far from the cause.
112
+ *
113
+ * @param name - a name in this vocabulary
114
+ */
115
+ resolve(name) {
116
+ return Object.freeze({ ...this.def(name) });
117
+ }
118
+ /**
119
+ * A status's glyph from a glyph set: `def.ascii` for an ASCII set, `def.glyph` otherwise. Unpainted, for a caller
120
+ * that draws it itself (an Ink tree, a reporter).
121
+ *
122
+ * @remarks
123
+ * Throws on an unknown name, as {@link Status.def} does. The glyph is sanitised, as text in a document is: escape
124
+ * sequences and control characters in a vocabulary's glyph are removed, so a glyph built from data cannot paint the
125
+ * terminal, plant a hyperlink or move the cursor. Every kit path that draws a status glyph takes it from here.
126
+ *
127
+ * @param name - a name in this vocabulary
128
+ * @param glyphs - the glyph set, such as `Glyphs.unicode`, `Glyphs.ascii` or a theme's
129
+ */
130
+ glyph(name, glyphs) {
131
+ const def = this.def(name);
132
+ return sanitize(glyphs.kind === "ascii" ? def.ascii : def.glyph);
133
+ }
134
+ /**
135
+ * The status with the highest rank; a tie goes to the one that comes first in `names`.
136
+ *
137
+ * @remarks
138
+ * `rank` is SEVERITY, not an aggregation policy: the higher rank wins, so in `Status.core` a `skip` outranks a
139
+ * `success`. A consumer whose aggregate differs (a test run where passes dominate skips, say) folds its own
140
+ * rule over the names instead of reading this.
141
+ *
142
+ * Takes at least one name, so the answer is always a name. For an array that may be empty, use
143
+ * {@link Status.worstOption}. They are two methods because a literal and an array variable are the same
144
+ * array at runtime, so one method could not return a name for one and an `Option` for the other.
145
+ *
146
+ * @param names - the statuses to compare
147
+ */
148
+ worst(names) {
149
+ let worst = names[0];
150
+ for (const name of names) if (this.def(name).rank > this.def(worst).rank) worst = name;
151
+ return worst;
152
+ }
153
+ /**
154
+ * The status with the highest rank of an array that may be empty: `None` when it is, otherwise `Some` of
155
+ * the worst, a tie going to the one that comes first in `names`. Rank is severity, not an aggregation policy; see
156
+ * {@link Status.worst}.
157
+ *
158
+ * @param names - the statuses to compare
159
+ */
160
+ worstOption(names) {
161
+ return Array.isReadonlyArrayNonEmpty(names) ? Option.some(this.worst(names)) : Option.none();
162
+ }
163
+ };
164
+
165
+ export { Status };
@@ -0,0 +1,101 @@
1
+ import { Effect, Layer, Option, Queue, Terminal } from "effect";
2
+
3
+ // src/TestTerminal.ts
4
+ /**
5
+ * A scripted `Terminal` for testing prompts and anything that reads the terminal.
6
+ *
7
+ * @remarks
8
+ * Queue keys with `input` or `type`, run the program under `layer`, then read `output`. `size` behaves as a real
9
+ * terminal's does: each subscription starts with the current size, receives every `resize`, keeps only the latest,
10
+ * and ends when its scope closes. To assert that a code path
11
+ * did NOT touch the terminal, queue some keys first and check `reads` is all zero (no subscription, no key, no
12
+ * line) and `pending` is unchanged afterwards. Only
13
+ * available from `@effected/cli-kit/testing`.
14
+ *
15
+ * @public
16
+ */
17
+ var TestTerminal = class {
18
+ constructor() {}
19
+ /**
20
+ * Build a test terminal.
21
+ *
22
+ * @param options - the reported size; 80 by 24 by default
23
+ */
24
+ static make = (options) => Effect.gen(function* () {
25
+ const queue = yield* Queue.unbounded();
26
+ const written = [];
27
+ let offered = 0;
28
+ let lines = 0;
29
+ let subscriptions = 0;
30
+ let current = {
31
+ columns: options?.columns ?? 80,
32
+ rows: options?.rows ?? 24
33
+ };
34
+ const sizeSubscribers = /* @__PURE__ */ new Set();
35
+ const offer = (inputs) => Effect.suspend(() => {
36
+ offered += inputs.length;
37
+ return Queue.offerAll(queue, inputs);
38
+ }).pipe(Effect.asVoid);
39
+ const terminal = Terminal.make({
40
+ columns: Effect.sync(() => current.columns),
41
+ rows: Effect.sync(() => current.rows),
42
+ size: Effect.acquireRelease(Effect.flatMap(Queue.sliding(1), (subscriber) => Effect.sync(() => {
43
+ Queue.offerUnsafe(subscriber, current);
44
+ sizeSubscribers.add(subscriber);
45
+ return subscriber;
46
+ })), (subscriber) => Effect.sync(() => {
47
+ sizeSubscribers.delete(subscriber);
48
+ Queue.endUnsafe(subscriber);
49
+ })),
50
+ readInput: Effect.sync(() => {
51
+ subscriptions++;
52
+ return queue;
53
+ }),
54
+ readLine: Effect.suspend(() => {
55
+ lines++;
56
+ return Effect.fail(new Terminal.QuitError({}));
57
+ }),
58
+ display: (text) => Effect.sync(() => {
59
+ written.push(text);
60
+ })
61
+ });
62
+ return {
63
+ layer: Layer.succeed(Terminal.Terminal, terminal),
64
+ input: (keys) => offer(keys.map((key) => ({
65
+ input: Option.none(),
66
+ key: {
67
+ name: key.name,
68
+ ctrl: key.ctrl ?? false,
69
+ meta: key.meta ?? false,
70
+ shift: key.shift ?? false
71
+ }
72
+ }))),
73
+ type: (text) => offer(Array.from(text, (char) => ({
74
+ input: Option.some(char),
75
+ key: {
76
+ name: char,
77
+ ctrl: false,
78
+ meta: false,
79
+ shift: false
80
+ }
81
+ }))),
82
+ end: Queue.end(queue).pipe(Effect.asVoid),
83
+ resize: (size) => Effect.sync(() => {
84
+ current = {
85
+ columns: size.columns,
86
+ rows: size.rows
87
+ };
88
+ for (const subscriber of sizeSubscribers) Queue.offerUnsafe(subscriber, current);
89
+ }),
90
+ output: Effect.sync(() => written.join("")),
91
+ pending: Queue.size(queue),
92
+ reads: Effect.map(Queue.size(queue), (size) => ({
93
+ keys: offered - size,
94
+ lines,
95
+ subscriptions
96
+ }))
97
+ };
98
+ });
99
+ };
100
+
101
+ export { TestTerminal };
package/Token.js ADDED
@@ -0,0 +1,68 @@
1
+ // src/Token.ts
2
+ /** The default style of every token. Deeply frozen: the record is shared. */
3
+ const DEFAULTS = Object.freeze({
4
+ success: Object.freeze({ fg: "green" }),
5
+ failure: Object.freeze({ fg: "red" }),
6
+ error: Object.freeze({
7
+ fg: "red",
8
+ bold: true
9
+ }),
10
+ warning: Object.freeze({ fg: "yellow" }),
11
+ info: Object.freeze({ fg: "cyan" }),
12
+ muted: Object.freeze({ dim: true }),
13
+ accent: Object.freeze({ fg: "cyan" }),
14
+ emphasis: Object.freeze({ bold: true })
15
+ });
16
+ /**
17
+ * Constructors for {@link Style} values, and the pure resolution of a token to one.
18
+ *
19
+ * @public
20
+ */
21
+ var Token = class {
22
+ constructor() {}
23
+ /**
24
+ * The default {@link Style} of every token, frozen.
25
+ *
26
+ * @remarks
27
+ * Data, not a service: it is what `CliTheme` starts from, and a renderer with no Effect context (an Ink
28
+ * component) reads it directly.
29
+ */
30
+ static defaults = DEFAULTS;
31
+ /**
32
+ * The style a token or style resolves to, as a pure function: no service, no terminal.
33
+ *
34
+ * @remarks
35
+ * An explicit style resolves to itself. A token name resolves to its override when `overrides` has one, else
36
+ * its default; a name that is not a token (including an `Object.prototype` member) resolves to the empty
37
+ * style. This is the same resolution `CliTheme.paint` applies, which is what `StreamTheme.style` reports.
38
+ *
39
+ * @param token - a token name or an explicit style
40
+ * @param overrides - styles that replace the default of a token, as `CliThemeOptions.tokens` does
41
+ */
42
+ static resolve = (token, overrides) => {
43
+ if (typeof token !== "string") return token;
44
+ const own = overrides !== void 0 && Object.hasOwn(overrides, token) ? overrides[token] : void 0;
45
+ if (own !== void 0) return own;
46
+ return Object.hasOwn(DEFAULTS, token) ? DEFAULTS[token] : {};
47
+ };
48
+ /**
49
+ * A foreground from a hex colour.
50
+ *
51
+ * @param hex - `#rrggbb` (or `#rgb`)
52
+ */
53
+ static hex = (hex) => ({ fg: hex });
54
+ /**
55
+ * A foreground from a named colour.
56
+ *
57
+ * @param color - the colour
58
+ */
59
+ static named = (color) => ({ fg: color });
60
+ /**
61
+ * A style, as written. Exists so a style reads as a token at a call site.
62
+ *
63
+ * @param style - the style
64
+ */
65
+ static style = (style) => style;
66
+ };
67
+
68
+ export { Token };