@effected/cli 0.12.0 → 0.14.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/CliRuntime.js CHANGED
@@ -1,4 +1,6 @@
1
+ import { Cancelled } from "./Cancelled.js";
1
2
  import { sanitize } from "./Fmt.js";
3
+ import { NotInteractive } from "./NotInteractive.js";
2
4
  import { FailureTargetCell, currentTarget, fallbackTarget, guardConsumerLines, linesOf, plainFailureLines, readSpans, refreshFailureTarget } from "./internal/failureTarget.js";
3
5
  import { CliColor } from "./CliColor.js";
4
6
  import { CliEnv } from "./CliEnv.js";
@@ -173,6 +175,8 @@ var CliRuntime = class CliRuntime {
173
175
  const details = {
174
176
  cause,
175
177
  isDefect: !Cause.hasFails(cause),
178
+ isCancelled: error instanceof Cancelled,
179
+ isNotInteractive: error instanceof NotInteractive,
176
180
  defaultLines,
177
181
  lines: (options) => options?.status === false || options?.spans !== void 0 ? reportLines(options?.status !== false, options?.spans) : defaultLines
178
182
  };
package/Render.js CHANGED
@@ -186,7 +186,7 @@ var Render = class {
186
186
  *
187
187
  * Under GitHub Actions (`neutralizeWorkflowCommands`) the same neutralizing applies, since markdown can be printed
188
188
  * to the log. Markdown escapes `[` in text, so `##[` cannot appear outside code and the headings are untouched
189
- * (a bare `##` is not a command); code spans and blocks, which are not escaped, get the zero-width space, which can
189
+ * (a bare `##` is not a command); code spans and blocks, which are not escaped, get the blank marker, which can
190
190
  * also land inside code or table text where it would otherwise have formed a command.
191
191
  *
192
192
  * GitHub also turns `@user`, `@org/team`, `#123` and commit SHAs in rendered markdown into mentions and references.
@@ -238,8 +238,8 @@ var Render = class {
238
238
  *
239
239
  * The runner has two command parsers, and a line is a command if either accepts it: after its leading whitespace it
240
240
  * starts with `::`, or `##[` occurs ANYWHERE in it (a bare `##` is not one). A document's text must not be able to
241
- * do that (`::add-mask::`, `::error::`, `##[error]`), so such a `::` line gets a zero-width space in front, which
242
- * the runner does not treat as whitespace, and every `##[` gets one between the `##` and the `[`. The text is
241
+ * do that (`::add-mask::`, `::error::`, `##[error]`), so such a `::` line gets a braille pattern blank (U+2800) in
242
+ * front, which the runner neither trims nor skips, and every `##[` gets one before the `[`. The text is
243
243
  * otherwise unchanged. A
244
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
245
  * that check, as the runner splits them. There is no ANSI and `paint` and `link` are never called, and the audience
package/index.d.ts CHANGED
@@ -2568,6 +2568,24 @@ interface FailureDetails {
2568
2568
  * failure from the error channel.
2569
2569
  */
2570
2570
  readonly isDefect: boolean;
2571
+ /**
2572
+ * `true` when the squashed `error` is the kit's {@link Cancelled}: the person quit a prompt or screen (Esc, Ctrl-C).
2573
+ * It is not a bug and there is nothing to report, whichever channel it arrived through.
2574
+ *
2575
+ * @remarks
2576
+ * A cancel from `CliPrompt.fallback` is a defect (`isDefect` is `true`) and one from `CliUi.run` is a typed failure
2577
+ * (`isDefect` is `false`), so a `render` that gives a defect the "please report this" treatment must test this flag
2578
+ * first, or Esc prints an issue-report request. `isDefect` keeps its plain meaning, the absence of a typed failure,
2579
+ * and the exit code is unchanged (`130` for the `interrupt` reason). `defaultLines` and `lines()` already draw it as
2580
+ * its one fixed line.
2581
+ */
2582
+ readonly isCancelled: boolean;
2583
+ /**
2584
+ * `true` when the squashed `error` is the kit's {@link NotInteractive}: the program asked for a prompt or a screen in
2585
+ * a run that cannot show one (a pipe, an agent, a CI). It is a usage problem, not a bug, whichever channel it
2586
+ * arrived through; `isDefect` is as for {@link FailureDetails.isCancelled}.
2587
+ */
2588
+ readonly isNotInteractive: boolean;
2571
2589
  /**
2572
2590
  * The report the kit writes for this failure when there is no `render`: for this run, in this audience, with its
2573
2591
  * colour, links and `displayPath`. A `render` that hands a failure back returns these lines unchanged, and the
@@ -2687,9 +2705,11 @@ interface MainOptions<RP, EP> extends ReportFailuresOptions {
2687
2705
  * one that writes output must use `Console` or `Stdio`, never `Terminal`.
2688
2706
  *
2689
2707
  * Stderr's colour mirrors stdout's terminal check unless `env.stderrIsTerminal` says otherwise, so with stderr
2690
- * redirected and stdout a terminal the failure report is painted into the file. On Node, pass the real check:
2691
- * `env: { stderrIsTerminal: Effect.sync(() => process.stderr.isTTY === true) }` (core's `Stdio` reports only
2692
- * stdout; upstream Effect-TS/effect#8639).
2708
+ * redirected and stdout a terminal the failure report is painted into the file. On Node, pass the real check from
2709
+ * the bin's entry, the one place it reads the host:
2710
+ * `env: { stderrIsTerminal: Effect.sync(() => process.stderr.isTTY === true) }`. Core's `Stdio` reports only
2711
+ * stdout (upstream Effect-TS/effect#8639); once core has a stderr check, this option reads it and the bin passes
2712
+ * nothing.
2693
2713
  */
2694
2714
  readonly env?: CliEnvOptions | undefined;
2695
2715
  /**
@@ -2706,8 +2726,12 @@ interface MainOptions<RP, EP> extends ReportFailuresOptions {
2706
2726
  *
2707
2727
  * Two cases keep help on stdout even under `"stderr"`. A `CliOutput`
2708
2728
  * Formatter or a `Console` provided inside `program` is not seen by
2709
- * `main`, so its help is not rerouted; provide the Formatter through
2710
- * `platform` instead. And with `Command.runWith`'s `renderErrors: false`
2729
+ * `main`, so its help is not rerouted. To change the Formatter, pass
2730
+ * `env.formatter` (for example `formatVersion`): `main` installs its own
2731
+ * Formatter inside the platform, so that is the way in, and a Formatter
2732
+ * provided inside the program is invisible to the routing. A Formatter
2733
+ * the platform provides is shadowed by `main`'s, so pass it through
2734
+ * `env.formatter` as well. And with `Command.runWith`'s `renderErrors: false`
2711
2735
  * no errors are printed, so nothing marks the help as a usage error's.
2712
2736
  */
2713
2737
  readonly helpOnUsageError?: "stdout" | "stderr" | undefined;
@@ -3200,8 +3224,8 @@ interface RenderContext {
3200
3224
  /**
3201
3225
  * Whether the output will be read by the GitHub Actions runner, which has two command parsers: a line is a command
3202
3226
  * if, after .NET whitespace, it starts with `::`, or if `##[` occurs ANYWHERE in it (a bare `##` is not one). When
3203
- * `true`, `plain`, `ansi` and `markdown` put a zero-width space in front of such a `::` line and between `##` and
3204
- * `[` at each `##[`, so a document's text, an error message, say, can never inject a command. Unset or `false`
3227
+ * `true`, `plain`, `ansi` and `markdown` put a braille pattern blank (U+2800, one blank cell) in front of such a
3228
+ * `::` line and before the `[` of each `##[`, so a document's text, an error message, say, can never inject a command. Unset or `false`
3205
3229
  * leaves their text alone. `Render.githubLog` ignores it and always neutralizes: its output is for the runner by
3206
3230
  * definition.
3207
3231
  *
@@ -3394,7 +3418,7 @@ export declare class Render {
3394
3418
  *
3395
3419
  * Under GitHub Actions (`neutralizeWorkflowCommands`) the same neutralizing applies, since markdown can be printed
3396
3420
  * to the log. Markdown escapes `[` in text, so `##[` cannot appear outside code and the headings are untouched
3397
- * (a bare `##` is not a command); code spans and blocks, which are not escaped, get the zero-width space, which can
3421
+ * (a bare `##` is not a command); code spans and blocks, which are not escaped, get the blank marker, which can
3398
3422
  * also land inside code or table text where it would otherwise have formed a command.
3399
3423
  *
3400
3424
  * GitHub also turns `@user`, `@org/team`, `#123` and commit SHAs in rendered markdown into mentions and references.
@@ -3446,8 +3470,8 @@ export declare class Render {
3446
3470
  *
3447
3471
  * The runner has two command parsers, and a line is a command if either accepts it: after its leading whitespace it
3448
3472
  * starts with `::`, or `##[` occurs ANYWHERE in it (a bare `##` is not one). A document's text must not be able to
3449
- * do that (`::add-mask::`, `::error::`, `##[error]`), so such a `::` line gets a zero-width space in front, which
3450
- * the runner does not treat as whitespace, and every `##[` gets one between the `##` and the `[`. The text is
3473
+ * do that (`::add-mask::`, `::error::`, `##[error]`), so such a `::` line gets a braille pattern blank (U+2800) in
3474
+ * front, which the runner neither trims nor skips, and every `##[` gets one before the `[`. The text is
3451
3475
  * otherwise unchanged. A
3452
3476
  * group's title is a command's data, so its `%`, CR and LF are escaped. Lines are split at CR, LF and CRLF before
3453
3477
  * that check, as the runner splits them. There is no ANSI and `paint` and `link` are never called, and the audience
@@ -52,7 +52,7 @@ const codeSpan = (text, mode) => {
52
52
  const longest = Math.max(0, ...[...flat.matchAll(/`+/g)].map((run) => run[0].length));
53
53
  const fence = "`".repeat(longest + 1);
54
54
  const padded = /^`|`$/.test(flat) || /^ .* $/.test(flat) && flat.trim() !== "" || flat === "";
55
- const body = mode === "cell" ? flat.replace(/\|/g, "\\|") : flat;
55
+ const body = mode === "cell" ? flat.split("|").join("\\|") : flat;
56
56
  const pad = padded ? " " : "";
57
57
  return `${fence}${pad}${body === "" ? " " : body}${pad}${fence}`;
58
58
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/cli",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
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,17 +57,17 @@
57
57
  "./package.json": "./package.json"
58
58
  },
59
59
  "dependencies": {
60
- "@effected/github-commands": "^0.1.0"
60
+ "@effected/github-commands": "^0.2.0"
61
61
  },
62
62
  "peerDependencies": {
63
- "@effected/config-file": "^0.14.1",
63
+ "@effected/config-file": "^0.14.2",
64
64
  "@effected/env": "^0.1.0",
65
65
  "@effected/glob": "^0.10.0",
66
66
  "@effected/walker": "^0.15.0",
67
- "@types/react": "^19.2.0",
67
+ "@types/react": "^19.3.0",
68
68
  "effect": "^4.0.0",
69
- "ink": "^7.1.1",
70
- "react": "^19.2.0"
69
+ "ink": "^8.0.0",
70
+ "react": "^19.3.0"
71
71
  },
72
72
  "peerDependenciesMeta": {
73
73
  "@effected/config-file": {
package/ui/UiTheme.js CHANGED
@@ -107,8 +107,9 @@ const useTerminalSize = () => {
107
107
  stdout.off("resize", redraw);
108
108
  };
109
109
  }, [stdout, followsStdout]);
110
- const columns = override?.columns ?? stdout.columns;
111
- const rows = override?.rows ?? stdout.rows;
110
+ const reported = stdout;
111
+ const columns = override?.columns ?? reported.columns;
112
+ const rows = override?.rows ?? reported.rows;
112
113
  return {
113
114
  columns: Math.max(1, known(columns, 80) - 1),
114
115
  rows: Math.max(1, known(rows, 24) - 1)
@@ -218,6 +218,10 @@ const LEADING_MOVES = /^(?:\u001b\[[0-9;?]*[A-Za-ln-z])+/;
218
218
  * moves; a write of moves alone is Ink clearing the frame for a log line, not a frame. stdout and stderr are two
219
219
  * streams, each read alone, and `fake.written` is both in the order written: one terminal, for `written` and the
220
220
  * transcript.
221
+ *
222
+ * Ink calls `onRender` after it writes a render's frame, in the same synchronous turn, so the frame is the last
223
+ * frame-like write of that turn. A production render whose write Ink's throttle defers to its trailing timer has
224
+ * none yet: the next frame-like write is taken instead.
221
225
  */
222
226
  const makeTerminal = (options, settings = { screens: "debug" }) => {
223
227
  const columns = options.columns ?? 80;
@@ -226,25 +230,48 @@ const makeTerminal = (options, settings = { screens: "debug" }) => {
226
230
  const captures = [];
227
231
  let lastWrite = 0;
228
232
  let frameDue = false;
233
+ let turn = [];
234
+ /** A chunk read as a frame in `mode`, or `undefined` for one that is not a frame (brackets or moves alone). */
235
+ const asFrame = (chunk, mode) => {
236
+ if (mode === "debug") return chunk;
237
+ const text = chunk.replace(FRAME_BRACKETS, "");
238
+ if (text === "" || CONTROLS_ONLY.test(text)) return void 0;
239
+ return text.replace(LEADING_MOVES, "").replace(/\n+$/, "");
240
+ };
229
241
  const fake = makeFakeStreams({
230
242
  columns,
231
243
  rows,
232
244
  onStdoutWrite: (chunk) => {
233
245
  lastWrite = Date.now();
246
+ if (turn.length === 0) {
247
+ const opened = turn;
248
+ queueMicrotask(() => {
249
+ if (turn === opened) turn = [];
250
+ });
251
+ }
252
+ turn.push(chunk);
234
253
  const current = captures.at(-1);
235
254
  if (!frameDue || current === void 0 || current.ended) return;
236
- if (current.mode === "debug") {
237
- frameDue = false;
238
- current.raws.push(chunk);
239
- return;
240
- }
241
- const text = chunk.replace(FRAME_BRACKETS, "");
242
- if (text === "") return;
255
+ const frame = asFrame(chunk, current.mode);
256
+ if (frame === void 0) return;
243
257
  frameDue = false;
244
- if (CONTROLS_ONLY.test(text)) return;
245
- current.raws.push(text.replace(LEADING_MOVES, "").replace(/\n+$/, ""));
258
+ current.raws.push(frame);
246
259
  }
247
260
  });
261
+ /** Called as Ink reports a render: take the frame it wrote this turn, or wait for the one its throttle defers. */
262
+ const rendered = () => {
263
+ const current = captures.at(-1);
264
+ if (current === void 0 || current.ended) return;
265
+ for (let index = turn.length - 1; index >= 0; index--) {
266
+ const frame = asFrame(turn[index], current.mode);
267
+ if (frame === void 0) continue;
268
+ turn = [];
269
+ frameDue = false;
270
+ current.raws.push(frame);
271
+ return;
272
+ }
273
+ frameDue = true;
274
+ };
248
275
  const streams = fake.streams;
249
276
  const stream = {
250
277
  isTerminal: true,
@@ -263,9 +290,7 @@ const makeTerminal = (options, settings = { screens: "debug" }) => {
263
290
  }).pipe(Layer.provide(terminal)), CliInteractive.layerTest(options.interactive ?? true), Layer.succeed(UiStreams, streams), Layer.succeed(UiRenderOptions, {
264
291
  ...settings.screens === "debug" ? { debug: true } : {},
265
292
  maxFps: 1e3,
266
- onRender: () => {
267
- frameDue = true;
268
- },
293
+ onRender: rendered,
269
294
  onMount: (kind) => {
270
295
  const mode = kind === "live" ? "production" : settings.screens;
271
296
  captures.push({
@@ -6,8 +6,9 @@ const ESC = String.fromCharCode(27);
6
6
  *
7
7
  * @remarks
8
8
  * It applies printable text, line feeds, the erase and cursor moves Ink's log-update writes (erase line, cursor up,
9
- * cursor to column one), and the clears of Ink's clear-terminal frame: `ESC[2J` blanks the visible screen, `ESC[3J`
10
- * drops the scrollback above it, `ESC[H` homes the cursor to the screen's top left. The visible screen is the last
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
11
12
  * `rows` lines; with `rows` unknown, the whole buffer counts as the screen, so a clear takes everything. Every other
12
13
  * escape is ignored. It does not wrap a line wider than the terminal. Shared by `CliUiTest.live`'s transcript and the
13
14
  * kit's own production-path tests.
@@ -38,8 +39,16 @@ const screenAfter = (written, rows) => {
38
39
  }
39
40
  } else if (command === "K") lines[row] = "";
40
41
  else if (command === "A") row = Math.max(0, row - Number(params === "" ? 1 : params));
41
- else if (command === "G") column = 0;
42
- else if (command === "J" && params === "2") for (let index = screenTop(); index < lines.length; index++) lines[index] = "";
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] = "";
43
52
  else if (command === "J" && params === "3") {
44
53
  const top = screenTop();
45
54
  lines.splice(0, top);