@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.
- package/Cancelled.js +43 -0
- package/CliAudience.js +177 -0
- package/CliColor.js +59 -0
- package/CliEnv.js +101 -0
- package/CliExit.js +64 -0
- package/CliFailure.js +287 -0
- package/CliFormat.js +124 -0
- package/CliInteractive.js +94 -0
- package/CliLinks.js +153 -0
- package/CliLog.js +367 -0
- package/CliLogger.js +91 -0
- package/CliMessage.js +79 -0
- package/CliPrompt.js +110 -0
- package/CliRuntime.js +254 -0
- package/CliTest.js +206 -0
- package/CliTheme.js +142 -0
- package/ConfigIssueRenderer.js +63 -0
- package/Doc.js +548 -0
- package/Fmt.js +335 -0
- package/GithubAnnotation.js +39 -0
- package/Glyphs.js +82 -0
- package/LICENSE +21 -0
- package/NotInteractive.js +41 -0
- package/README.md +215 -2
- package/Render.js +280 -0
- package/SchemaIssueRenderer.js +47 -0
- package/Status.js +165 -0
- package/TestTerminal.js +101 -0
- package/Token.js +68 -0
- package/index.d.ts +3951 -0
- package/index.js +27 -0
- package/internal/ExitRequested.js +21 -0
- package/internal/HelpRouting.js +35 -0
- package/internal/ansi.js +229 -0
- package/internal/autoFormat.js +33 -0
- package/internal/canPrompt.js +14 -0
- package/internal/counts.js +83 -0
- package/internal/diagnostics.js +31 -0
- package/internal/displayPath.js +46 -0
- package/internal/displayWidth.js +42 -0
- package/internal/failureTarget.js +195 -0
- package/internal/fallbackAnswer.js +17 -0
- package/internal/fileSink.js +61 -0
- package/internal/format.js +175 -0
- package/internal/inspect.js +40 -0
- package/internal/isExitCode.js +11 -0
- package/internal/layout.js +249 -0
- package/internal/linkScheme.js +29 -0
- package/internal/linkTarget.js +49 -0
- package/internal/logSafety.js +45 -0
- package/internal/renderAnsi.js +51 -0
- package/internal/renderDoc.js +322 -0
- package/internal/renderGithubLog.js +59 -0
- package/internal/renderMarkdown.js +367 -0
- package/internal/renderPlain.js +49 -0
- package/internal/scanAudience.js +105 -0
- package/internal/splitFrame.js +55 -0
- package/internal/terminalCapabilities.js +23 -0
- package/internal/wizardGate.js +17 -0
- package/package.json +90 -5
- package/testing.d.ts +246 -0
- package/testing.js +4 -0
- package/tsdoc-metadata.json +11 -0
- package/ui/CliUi.js +507 -0
- package/ui/CliUiDetached.js +36 -0
- package/ui/CliUiLive.js +436 -0
- package/ui/CliUiRouteStream.js +72 -0
- package/ui/Confirm.js +244 -0
- package/ui/DocView.js +76 -0
- package/ui/KeyHelp.js +61 -0
- package/ui/KeyTable.js +198 -0
- package/ui/MultiSelect.js +259 -0
- package/ui/Select.js +229 -0
- package/ui/Tabs.js +201 -0
- package/ui/TextInput.js +289 -0
- package/ui/Toggle.js +31 -0
- package/ui/UiKey.js +48 -0
- package/ui/UiProvider.js +62 -0
- package/ui/UiStreams.js +17 -0
- package/ui/UiTheme.js +147 -0
- package/ui/Viewport.js +203 -0
- package/ui/internal/ErrorBoundary.js +29 -0
- package/ui/internal/Holder.js +73 -0
- package/ui/internal/ScreenContext.js +51 -0
- package/ui/internal/UiProviders.js +38 -0
- package/ui/internal/ink.js +121 -0
- package/ui/internal/inkChalk.js +47 -0
- package/ui/internal/inkConsole.js +150 -0
- package/ui/internal/lazyView.js +73 -0
- package/ui/internal/lineRouter.js +70 -0
- package/ui/internal/lineText.js +18 -0
- package/ui/internal/mountPermit.js +15 -0
- package/ui/internal/processStreams.js +18 -0
- package/ui/internal/renderOptions.js +12 -0
- package/ui/testing/CliUiTest.js +792 -0
- package/ui/testing/fakeStreams.js +104 -0
- package/ui/testing/terminalModel.js +67 -0
- package/ui-testing-serializer.d.ts +12 -0
- package/ui-testing-serializer.js +32 -0
- package/ui-testing.d.ts +558 -0
- package/ui-testing.js +3 -0
- package/ui.d.ts +1990 -0
- 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 };
|
package/TestTerminal.js
ADDED
|
@@ -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 };
|