@effected/cli 0.15.0 → 0.16.1
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/Doc.js +3 -2
- package/Render.js +3 -2
- package/index.d.ts +6 -4
- package/internal/renderGithubLog.js +17 -5
- package/package.json +3 -3
- package/ui/CliUi.js +2 -1
- package/ui/CliUiLive.js +1 -0
- package/ui/internal/inkConsole.js +5 -0
- package/ui/testing/CliUiTest.js +5 -0
- package/ui/testing/fakeStreams.js +26 -0
- package/ui-testing.d.ts +31 -1
- package/ui.d.ts +32 -1
package/Doc.js
CHANGED
|
@@ -463,8 +463,9 @@ var Doc = class {
|
|
|
463
463
|
* and every other renderer writes nothing.
|
|
464
464
|
*
|
|
465
465
|
* @remarks
|
|
466
|
-
* It is the kit's own command, so `githubLog` does not neutralize
|
|
467
|
-
* no text in them can end the command or start another
|
|
466
|
+
* It is the kit's own command, so `githubLog` does not neutralize the command itself; its message and properties are
|
|
467
|
+
* escaped, so no text in them can end the command or start another, and a `##[` in its message, title or file gets
|
|
468
|
+
* a braille pattern blank (U+2800) before the `[`, as in plain text. It is a command where a line starts: at the top level, as a
|
|
468
469
|
* top-level section's child, or as a direct child of a group's body. Nested deeper, it is dropped.
|
|
469
470
|
*
|
|
470
471
|
* @param options - the level, and the optional file, position and title
|
package/Render.js
CHANGED
|
@@ -232,7 +232,8 @@ var Render = class {
|
|
|
232
232
|
* body, `::endgroup::`. An annotation is a command at the top level, as a top-level section's child, and as a direct
|
|
233
233
|
* child of a group's body; anywhere deeper (inside a list, a callout, or a section within a group) it is nothing,
|
|
234
234
|
* as in plain. Its message and properties are escaped, so no text can end
|
|
235
|
-
* the command or start another, and the kit's own command is never neutralized
|
|
235
|
+
* the command or start another, and the kit's own command is never neutralized, though a `##[` in its message,
|
|
236
|
+
* title or file is. That is a top-level collapsible, or one that is a direct child of a
|
|
236
237
|
* top-level section. GitHub does not nest groups, so a collapsible inside a group, or inside a list or callout
|
|
237
238
|
* (where it would not start a line), keeps plain's rendering: its title on a line and its body indented.
|
|
238
239
|
*
|
|
@@ -241,7 +242,7 @@ var Render = class {
|
|
|
241
242
|
* do that (`::add-mask::`, `::error::`, `##[error]`), so such a `::` line gets a braille pattern blank (U+2800) in
|
|
242
243
|
* front, which the runner neither trims nor skips, and every `##[` gets one before the `[`. The text is
|
|
243
244
|
* otherwise unchanged. A
|
|
244
|
-
* group's title is a command's data, so its `%`, CR and LF are escaped. Lines are split at CR, LF and CRLF before
|
|
245
|
+
* group's title is a command's data, so its `%`, CR and LF are escaped and a `##[` in it is neutralized. Lines are split at CR, LF and CRLF before
|
|
245
246
|
* that check, as the runner splits them. There is no ANSI and `paint` and `link` are never called, and the audience
|
|
246
247
|
* is treated as `agent`, as `plain` does.
|
|
247
248
|
*
|
package/index.d.ts
CHANGED
|
@@ -1382,8 +1382,9 @@ export declare class Doc {
|
|
|
1382
1382
|
* and every other renderer writes nothing.
|
|
1383
1383
|
*
|
|
1384
1384
|
* @remarks
|
|
1385
|
-
* It is the kit's own command, so `githubLog` does not neutralize
|
|
1386
|
-
* no text in them can end the command or start another
|
|
1385
|
+
* It is the kit's own command, so `githubLog` does not neutralize the command itself; its message and properties are
|
|
1386
|
+
* escaped, so no text in them can end the command or start another, and a `##[` in its message, title or file gets
|
|
1387
|
+
* a braille pattern blank (U+2800) before the `[`, as in plain text. It is a command where a line starts: at the top level, as a
|
|
1387
1388
|
* top-level section's child, or as a direct child of a group's body. Nested deeper, it is dropped.
|
|
1388
1389
|
*
|
|
1389
1390
|
* @param options - the level, and the optional file, position and title
|
|
@@ -3474,7 +3475,8 @@ export declare class Render {
|
|
|
3474
3475
|
* body, `::endgroup::`. An annotation is a command at the top level, as a top-level section's child, and as a direct
|
|
3475
3476
|
* child of a group's body; anywhere deeper (inside a list, a callout, or a section within a group) it is nothing,
|
|
3476
3477
|
* as in plain. Its message and properties are escaped, so no text can end
|
|
3477
|
-
* the command or start another, and the kit's own command is never neutralized
|
|
3478
|
+
* the command or start another, and the kit's own command is never neutralized, though a `##[` in its message,
|
|
3479
|
+
* title or file is. That is a top-level collapsible, or one that is a direct child of a
|
|
3478
3480
|
* top-level section. GitHub does not nest groups, so a collapsible inside a group, or inside a list or callout
|
|
3479
3481
|
* (where it would not start a line), keeps plain's rendering: its title on a line and its body indented.
|
|
3480
3482
|
*
|
|
@@ -3483,7 +3485,7 @@ export declare class Render {
|
|
|
3483
3485
|
* do that (`::add-mask::`, `::error::`, `##[error]`), so such a `::` line gets a braille pattern blank (U+2800) in
|
|
3484
3486
|
* front, which the runner neither trims nor skips, and every `##[` gets one before the `[`. The text is
|
|
3485
3487
|
* otherwise unchanged. A
|
|
3486
|
-
* group's title is a command's data, so its `%`, CR and LF are escaped. Lines are split at CR, LF and CRLF before
|
|
3488
|
+
* group's title is a command's data, so its `%`, CR and LF are escaped and a `##[` in it is neutralized. Lines are split at CR, LF and CRLF before
|
|
3487
3489
|
* that check, as the runner splits them. There is no ANSI and `paint` and `link` are never called, and the audience
|
|
3488
3490
|
* is treated as `agent`, as `plain` does.
|
|
3489
3491
|
*
|
|
@@ -5,10 +5,22 @@ import { CommandNeutralizer, WorkflowCommand } from "@effected/github-commands";
|
|
|
5
5
|
//#region src/internal/renderGithubLog.ts
|
|
6
6
|
/** Plain lines, neutralized; a block that draws nothing, such as an empty table, gives no line at all. */
|
|
7
7
|
const plainLines = (blocks, ctx) => renderPlainLines(blocks, ctx).flatMap((line) => CommandNeutralizer.lines(line));
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
/**
|
|
9
|
+
* A command's data, with every `##[` neutralized and its line breaks kept as they were. `WorkflowCommand` escapes the
|
|
10
|
+
* breaks, so the command stays one line, but the runner's legacy parser reads `##[` ANYWHERE in a line, including in
|
|
11
|
+
* a command's message and property values (it is tried when the V2 parser rejects the line), so each `##[` gets the
|
|
12
|
+
* neutralizer's marker before its `[`. The V2 rule does not apply: no part of the data starts a line of the log, so
|
|
13
|
+
* each part is neutralized behind a one-character lead that cannot start a command and is cut off again, which keeps
|
|
14
|
+
* a part that begins `::` as it was. The command itself is not neutralized: only the data that goes into it.
|
|
15
|
+
*/
|
|
16
|
+
const commandData = (text) => text.split(/(\r\n|\r|\n)/).map((part, index) => index % 2 === 0 ? CommandNeutralizer.text(`.${part}`).slice(1) : part).join("");
|
|
17
|
+
/**
|
|
18
|
+
* An annotation as the kit's own command, on the trusted path: escaped by `WorkflowCommand`, and its message, title
|
|
19
|
+
* and file neutralized as data (the line and the columns are numbers, which carry nothing).
|
|
20
|
+
*/
|
|
21
|
+
const annotationLine = (block) => WorkflowCommand[block.level](commandData(sanitize(block.message)), {
|
|
22
|
+
...block.title === void 0 ? {} : { title: commandData(sanitize(block.title)) },
|
|
23
|
+
...block.file === void 0 ? {} : { file: commandData(sanitize(block.file)) },
|
|
12
24
|
...block.line === void 0 ? {} : { startLine: block.line },
|
|
13
25
|
...block.endLine === void 0 ? {} : { endLine: block.endLine },
|
|
14
26
|
...block.col === void 0 ? {} : { startColumn: block.col },
|
|
@@ -20,7 +32,7 @@ const blockLines = (block, ctx) => {
|
|
|
20
32
|
switch (block._tag) {
|
|
21
33
|
case "Annotation": return [annotationLine(block)];
|
|
22
34
|
case "Collapsible": {
|
|
23
|
-
const title = plainInline(block.title, ctx).map((span) => span.text).join("");
|
|
35
|
+
const title = commandData(plainInline(block.title, ctx).map((span) => span.text).join(""));
|
|
24
36
|
return [
|
|
25
37
|
WorkflowCommand.group(title),
|
|
26
38
|
...bodyLines(block.body, ctx),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The presentation boundary of an effect/cli program: audience-aware output, a document IR and renderers, editor links, failure reports and logging, plus opt-in Ink screens, widgets and a live view",
|
|
6
6
|
"keywords": [
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
"./package.json": "./package.json"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@effected/github-commands": "^0.2.
|
|
60
|
+
"@effected/github-commands": "^0.2.1"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"@effected/config-file": "^0.14.2",
|
|
64
|
-
"@effected/env": "^0.1.
|
|
64
|
+
"@effected/env": "^0.1.1",
|
|
65
65
|
"@effected/glob": "^0.10.0",
|
|
66
66
|
"@effected/walker": "^0.15.0",
|
|
67
67
|
"@types/react": "^19.3.0",
|
package/ui/CliUi.js
CHANGED
|
@@ -262,7 +262,8 @@ var CliUi = class CliUi {
|
|
|
262
262
|
* mount to its end, so a `CliUi.run` during a run waits for the run to end, and one between runs mounts at once.
|
|
263
263
|
*
|
|
264
264
|
* While a run is drawn, write logs through `logConsole`, provided around the work the view reports on: its lines
|
|
265
|
-
* land above the frame. A line written to the terminal any other way tears the frame.
|
|
265
|
+
* land above the frame. A line written to the terminal any other way tears the frame. A host forwarding output it
|
|
266
|
+
* did not write itself prints it with `printAbove`, which says whether a frame was mounted to print it above.
|
|
266
267
|
*
|
|
267
268
|
* The view draws on stdout (`UiStreams`), at stdout's colour level and glyphs, and mounts only when the run is
|
|
268
269
|
* interactive (`CliInteractive`).
|
package/ui/CliUiLive.js
CHANGED
|
@@ -135,6 +135,11 @@ const makeInkConsole = Effect.gen(function* () {
|
|
|
135
135
|
if (attached !== void 0) attached.out(data);
|
|
136
136
|
else streams.stdout.write(data);
|
|
137
137
|
},
|
|
138
|
+
printAbove: (stream, text) => {
|
|
139
|
+
if (attached === void 0) return false;
|
|
140
|
+
(stream === "stdout" ? attached.out : attached.err)(`${text}\n`);
|
|
141
|
+
return true;
|
|
142
|
+
},
|
|
138
143
|
Bridge,
|
|
139
144
|
detach: () => {
|
|
140
145
|
attached = void 0;
|
package/ui/testing/CliUiTest.js
CHANGED
|
@@ -714,12 +714,17 @@ var CliUiTest = class {
|
|
|
714
714
|
end: Effect.andThen(Queue.end(queue), handle.done),
|
|
715
715
|
advance: (duration) => settled(TestClock.adjust(duration)),
|
|
716
716
|
resize: (nextColumns, nextRows) => settled(Effect.sync(() => terminal.fake.resize(nextColumns, nextRows))),
|
|
717
|
+
write: (stream, bytes) => Effect.sync(() => {
|
|
718
|
+
terminal.fake.streams[stream].write(bytes);
|
|
719
|
+
}),
|
|
717
720
|
frame: Effect.sync(() => trimLines(styled(last()))),
|
|
718
721
|
rawFrame: Effect.sync(last),
|
|
719
722
|
plainFrame: Effect.sync(() => trimLines(last().replace(ESCAPES, ""))),
|
|
720
723
|
frames: Effect.sync(() => raws().map((raw) => trimLines(styled(raw)))),
|
|
721
724
|
transcript: Effect.sync(() => screenAfter(terminal.fake.written(), terminal.fake.streams.stdout.rows).join("\n")),
|
|
722
725
|
written: Effect.sync(() => terminal.fake.written()),
|
|
726
|
+
stdoutWritten: Effect.sync(() => terminal.fake.stdout()),
|
|
727
|
+
stderrWritten: Effect.sync(() => terminal.fake.stderr()),
|
|
723
728
|
handle
|
|
724
729
|
};
|
|
725
730
|
});
|
|
@@ -1,10 +1,36 @@
|
|
|
1
1
|
import { PassThrough, Writable } from "node:stream";
|
|
2
2
|
|
|
3
3
|
//#region 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
|
+
};
|
|
4
29
|
const capture = (columns, rows, both, onWrite) => {
|
|
5
30
|
const chunks = [];
|
|
6
31
|
const stream = new Writable({ write(chunk, _encoding, callback) {
|
|
7
32
|
const text = chunk.toString();
|
|
33
|
+
if (text.includes(HIDE_CURSOR)) disarmCursorRestore();
|
|
8
34
|
chunks.push(text);
|
|
9
35
|
both.push(text);
|
|
10
36
|
onWrite?.(text);
|
package/ui-testing.d.ts
CHANGED
|
@@ -253,6 +253,25 @@ interface CliUiTestLive<E, S> {
|
|
|
253
253
|
readonly advance: (duration: Duration.Input) => Effect.Effect<void>;
|
|
254
254
|
/** Resize the terminal, then wait as `publish` does. */
|
|
255
255
|
readonly resize: (columns: number, rows: number) => Effect.Effect<void>;
|
|
256
|
+
/**
|
|
257
|
+
* Write `bytes` straight to the terminal's `stream`, past Ink and past the view: what a child process or another
|
|
258
|
+
* library writing to the process's own stdout or stderr does while a frame is drawn.
|
|
259
|
+
*
|
|
260
|
+
* @remarks
|
|
261
|
+
* The bytes go to the same in-memory stream the view draws on, so `written` and `transcript` hold them in the order
|
|
262
|
+
* they were written. Nothing is redrawn and nothing is waited for: a raw line under a mounted frame does its harm at
|
|
263
|
+
* the frame's next redraw (`advance`, `publish`), which erases as many lines as the frame had, counted from the
|
|
264
|
+
* cursor the raw line moved, and so leaves the frame's top row stranded in the scrollback above it. That is the
|
|
265
|
+
* failure to reproduce before proving a fix routes the line through `handle.printAbove` or `handle.logConsole`.
|
|
266
|
+
*
|
|
267
|
+
* Write whole lines (end `bytes` with `\n`) to model a line landing under the frame. A write made while a frame is
|
|
268
|
+
* still due from Ink's throttle can be read as that frame by `frame` and `frames`, which are best-effort;
|
|
269
|
+
* `transcript` and `written` are the authority.
|
|
270
|
+
*
|
|
271
|
+
* @param stream - the terminal stream to write to
|
|
272
|
+
* @param bytes - the bytes to write, as given: escapes are not removed
|
|
273
|
+
*/
|
|
274
|
+
readonly write: (stream: "stdout" | "stderr", bytes: string) => Effect.Effect<void>;
|
|
256
275
|
/**
|
|
257
276
|
* The last frame drawn, as token markup (see {@link CliUiTest.styled}); empty before the first.
|
|
258
277
|
*
|
|
@@ -284,7 +303,18 @@ interface CliUiTestLive<E, S> {
|
|
|
284
303
|
* (a scrollback wipe) anywhere in a run.
|
|
285
304
|
*/
|
|
286
305
|
readonly written: Effect.Effect<string>;
|
|
287
|
-
/**
|
|
306
|
+
/**
|
|
307
|
+
* Every byte written to the terminal's stdout alone, escapes included: {@link CliUiTestLive.written} without stderr.
|
|
308
|
+
*
|
|
309
|
+
* @remarks
|
|
310
|
+
* With {@link CliUiTestLive.stderrWritten}, what to assert a line's stream on. Ink draws the frame, and erases and
|
|
311
|
+
* repaints it around a line logged above it, on stdout whichever stream the line is for, so a line routed to stderr
|
|
312
|
+
* shows in `stderrWritten` and never here.
|
|
313
|
+
*/
|
|
314
|
+
readonly stdoutWritten: Effect.Effect<string>;
|
|
315
|
+
/** Every byte written to the terminal's stderr alone, escapes included: {@link CliUiTestLive.written} without stdout. */
|
|
316
|
+
readonly stderrWritten: Effect.Effect<string>;
|
|
317
|
+
/** The view's own handle: its `state`, its `logConsole`, `printAbove`, `done` and `close`. */
|
|
288
318
|
readonly handle: LiveHandle<S>;
|
|
289
319
|
}
|
|
290
320
|
/**
|
package/ui.d.ts
CHANGED
|
@@ -116,6 +116,36 @@ interface LiveHandle<S> {
|
|
|
116
116
|
* Effect's `Console` (another library's own `process.stderr` writes) still tears the frame.
|
|
117
117
|
*/
|
|
118
118
|
readonly logConsole: Console.Console;
|
|
119
|
+
/**
|
|
120
|
+
* Print one line above the frame if a run's frame is mounted now, and say whether it did: `true` when `line` and a
|
|
121
|
+
* line break went through Ink's writer for `stream`, above the frame, and `false`, having written nothing, when no
|
|
122
|
+
* frame is mounted.
|
|
123
|
+
*
|
|
124
|
+
* @remarks
|
|
125
|
+
* For a host that forwards output it did not write itself (a child process's stderr, a test runner's captured
|
|
126
|
+
* streams) and must know where it went. `logConsole` answers the same question silently, writing to `UiStreams`
|
|
127
|
+
* when no frame is mounted; `printAbove` leaves that case to the caller, who may have somewhere better to send the
|
|
128
|
+
* line, or may already be writing to the stream it would land on.
|
|
129
|
+
*
|
|
130
|
+
* A frame is mounted only during an interactive run: from the run's mount until just before its unmount. Between
|
|
131
|
+
* runs, before the first, after `close`, in a degraded run, and always when the view is not interactive (an agent,
|
|
132
|
+
* CI, a pipe, `TERM=dumb`, which never mounts), it returns `false`. The check and the write are one synchronous step,
|
|
133
|
+
* so the answer is never stale: a separate "is a frame mounted" query could be answered by one run and acted on in
|
|
134
|
+
* the gap before the next. Synchronous, like `logConsole`, so a Node stream's `write` callback can call it.
|
|
135
|
+
*
|
|
136
|
+
* `true` means Ink's writer accepted the line, not that it reached the terminal: while a render has handed the
|
|
137
|
+
* terminal to a child process (`useApp().suspendTerminal`), Ink drops what its writers are handed, and so does
|
|
138
|
+
* `logConsole`.
|
|
139
|
+
*
|
|
140
|
+
* The line is written as given: no formatting, no group indent, and no sanitising. A line break inside `line` is
|
|
141
|
+
* kept, and each of its lines lands above the frame. A cursor movement inside it (a `\r`, a cursor-up, an erase
|
|
142
|
+
* line, as a child's own progress bar writes) moves the cursor Ink repaints the frame from, and tears the frame.
|
|
143
|
+
*
|
|
144
|
+
* @param stream - the stream the line belongs on: Ink's stdout writer or its stderr writer
|
|
145
|
+
* @param line - the text to print, without a trailing line break
|
|
146
|
+
* @returns `true` if Ink's writer above a mounted frame took the line; `false`, with nothing written, otherwise
|
|
147
|
+
*/
|
|
148
|
+
readonly printAbove: (stream: "stdout" | "stderr", line: string) => boolean;
|
|
119
149
|
/**
|
|
120
150
|
* Completes once the events have ended (the stream ended, the subscription's `PubSub` was ended with `PubSub.end` or
|
|
121
151
|
* shut down, or `close` ended them) and the last run's frame is committed. Dies with what the view died of: a `reduce` that threw, or a
|
|
@@ -418,7 +448,8 @@ export declare class CliUi {
|
|
|
418
448
|
* mount to its end, so a `CliUi.run` during a run waits for the run to end, and one between runs mounts at once.
|
|
419
449
|
*
|
|
420
450
|
* While a run is drawn, write logs through `logConsole`, provided around the work the view reports on: its lines
|
|
421
|
-
* land above the frame. A line written to the terminal any other way tears the frame.
|
|
451
|
+
* land above the frame. A line written to the terminal any other way tears the frame. A host forwarding output it
|
|
452
|
+
* did not write itself prints it with `printAbove`, which says whether a frame was mounted to print it above.
|
|
422
453
|
*
|
|
423
454
|
* The view draws on stdout (`UiStreams`), at stdout's colour level and glyphs, and mounts only when the run is
|
|
424
455
|
* interactive (`CliInteractive`).
|