@vlandoss/clibuddy 0.6.2-git-e008b4d.0 → 0.7.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/dist/index.d.mts CHANGED
@@ -91,39 +91,29 @@ declare function isNonZeroExitError(value: unknown): value is NonZeroExitError;
91
91
  //#endregion
92
92
  //#region src/task-board.d.ts
93
93
  type TaskOutcome = {
94
- /** The task's verdict typically the wrapped process's exit code === 0. */ok: boolean;
95
- /**
96
- * Output to flush, grouped under the task label, once the board settles. The
97
- * board prints it verbatim whenever it's non-empty (pass or fail) — the caller
98
- * decides what, if anything, to surface.
99
- */
94
+ ok: boolean; /** Output flushed (grouped under the label) once the board settles. */
100
95
  detail?: string;
101
96
  };
102
97
  type BoardTask = {
103
- /** Stable identifier rendered on the row, e.g. a package name. */label: string; /** Runs the task. Must resolve to a `TaskOutcome`; rejections render as failed. */
98
+ label: string; /** Resolve to a `TaskOutcome`; a rejection renders as a failed row. */
104
99
  run: () => Promise<TaskOutcome>;
105
100
  };
106
101
  type BoardOptions = {
107
- /** Section header printed above the rows, e.g. `tsc · 16 packages` (framed multi-row only). */title?: string;
102
+ /** Section header above the rows, e.g. `tsc · 16 packages` (multi-row only). */title?: string;
108
103
  /**
109
- * Force the framed (`┌ │ └`) layout even for a single task. `rr check` sets
110
- * this so its sections stay visually divided; a standalone single-task
111
- * command leaves it unset and renders compactly. Defaults to `tasks.length > 1`.
104
+ * Force the `┌ │ └` frame even for one task. `rr check` sets it to divide its
105
+ * sections; otherwise a board is unframed (compact for one task, plain for many).
112
106
  */
113
107
  frame?: boolean;
114
108
  };
115
109
  type BoardResult = {
116
- /** False when any task ended not-ok. */ok: boolean;
110
+ ok: boolean;
117
111
  outcomes: TaskOutcome[];
118
112
  };
119
113
  /**
120
- * Runs `tasks` in parallel and reports their progress as a board: one row per
121
- * task with a live spinner that collapses to ✔/✖ on settle. On a TTY the rows
122
- * update in place; otherwise (CI, pipes) each row prints once when it settles,
123
- * keeping logs deterministic. After every task settles, each task's captured
124
- * detail is flushed grouped under its label, followed by a one-line summary.
125
- * Parallelism is never sacrificed — the renderer only reflects work that is
126
- * already running.
114
+ * Runs `tasks` in parallel, rendering one row each that collapses to ✔/✖, then
115
+ * flushes their captured detail and a one-line summary. On a TTY the rows update
116
+ * in place; otherwise each prints once on settle, keeping logs deterministic.
127
117
  */
128
118
  declare function runTaskBoard(tasks: BoardTask[], options?: BoardOptions): Promise<BoardResult>;
129
119
  //#endregion
package/dist/index.mjs CHANGED
@@ -237,13 +237,9 @@ const BAR_END = gray("└");
237
237
  /** Failing output past this many lines is truncated with a "+N more" note. */
238
238
  const MAX_DETAIL_LINES = 60;
239
239
  /**
240
- * Runs `tasks` in parallel and reports their progress as a board: one row per
241
- * task with a live spinner that collapses to ✔/✖ on settle. On a TTY the rows
242
- * update in place; otherwise (CI, pipes) each row prints once when it settles,
243
- * keeping logs deterministic. After every task settles, each task's captured
244
- * detail is flushed grouped under its label, followed by a one-line summary.
245
- * Parallelism is never sacrificed — the renderer only reflects work that is
246
- * already running.
240
+ * Runs `tasks` in parallel, rendering one row each that collapses to ✔/✖, then
241
+ * flushes their captured detail and a one-line summary. On a TTY the rows update
242
+ * in place; otherwise each prints once on settle, keeping logs deterministic.
247
243
  */
248
244
  async function runTaskBoard(tasks, options = {}) {
249
245
  const live = hasTTY && !isCI;
@@ -369,11 +365,7 @@ function sharedLeadingLine(details) {
369
365
  function stripLeadingLine(detail, line) {
370
366
  return detail.startsWith(line) ? detail.slice(line.length).replace(/^\n/, "") : detail;
371
367
  }
372
- /**
373
- * The leading decoration for a row. Framed: `│` per row, or `┌` for a framed
374
- * single task (status rides the opening corner). Unframed: a 2-space indent
375
- * under the title for a multi-row board, nothing for a lone compact row.
376
- */
368
+ /** A row's leading decoration: `│` (framed multi), `┌` (framed single, status rides the corner), or a plain indent. */
377
369
  function rowPrefix(framed, multi) {
378
370
  if (framed) return multi ? `${BAR} ` : `${BAR_START} `;
379
371
  return multi ? " " : "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vlandoss/clibuddy",
3
- "version": "0.6.2-git-e008b4d.0",
3
+ "version": "0.7.0",
4
4
  "description": "A helper library to create CLIs in Variable Land",
5
5
  "homepage": "https://github.com/variableland/dx/tree/main/shared/clibuddy#readme",
6
6
  "bugs": {
package/src/task-board.ts CHANGED
@@ -3,47 +3,35 @@ import { palette } from "./colors.ts";
3
3
  import { hasTTY, isCI } from "./env.ts";
4
4
 
5
5
  export type TaskOutcome = {
6
- /** The task's verdict — typically the wrapped process's exit code === 0. */
7
6
  ok: boolean;
8
- /**
9
- * Output to flush, grouped under the task label, once the board settles. The
10
- * board prints it verbatim whenever it's non-empty (pass or fail) — the caller
11
- * decides what, if anything, to surface.
12
- */
7
+ /** Output flushed (grouped under the label) once the board settles. */
13
8
  detail?: string;
14
9
  };
15
10
 
16
11
  export type BoardTask = {
17
- /** Stable identifier rendered on the row, e.g. a package name. */
18
12
  label: string;
19
- /** Runs the task. Must resolve to a `TaskOutcome`; rejections render as failed. */
13
+ /** Resolve to a `TaskOutcome`; a rejection renders as a failed row. */
20
14
  run: () => Promise<TaskOutcome>;
21
15
  };
22
16
 
23
17
  export type BoardOptions = {
24
- /** Section header printed above the rows, e.g. `tsc · 16 packages` (framed multi-row only). */
18
+ /** Section header above the rows, e.g. `tsc · 16 packages` (multi-row only). */
25
19
  title?: string;
26
20
  /**
27
- * Force the framed (`┌ │ └`) layout even for a single task. `rr check` sets
28
- * this so its sections stay visually divided; a standalone single-task
29
- * command leaves it unset and renders compactly. Defaults to `tasks.length > 1`.
21
+ * Force the `┌ │ └` frame even for one task. `rr check` sets it to divide its
22
+ * sections; otherwise a board is unframed (compact for one task, plain for many).
30
23
  */
31
24
  frame?: boolean;
32
25
  };
33
26
 
34
27
  export type BoardResult = {
35
- /** False when any task ended not-ok. */
36
28
  ok: boolean;
37
29
  outcomes: TaskOutcome[];
38
30
  };
39
31
 
40
- // Frame + gutter mirror @clack/prompts (used by `rr plugins`) so the two flows
41
- // read as one family: the ◒◐◓◑ spinner and the gray gutter. The settled
42
- // glyph is ✔/✖ the verdict is the tool's exit code, never parsed from output
43
- // (we can't tell a clean "Found 0 warnings" trailer from a real warning without
44
- // parsing, so we don't pretend to — the tool's own output says which it is).
45
- // The gutter uses the 16-color `gray` (not a fixed hex) so it adapts to the
46
- // terminal theme and degrades on non-truecolor surfaces / CI log viewers.
32
+ // Glyphs mirror @clack/prompts (used by `rr plugins`) so the two flows read as
33
+ // one family: the ◒◐◓◑ spinner and a gray `│└` gutter, settling to ✔/✖. The
34
+ // gray is 16-color (not a fixed hex) so it adapts to the terminal theme.
47
35
  const FRAMES = ["◒", "◐", "◓", "◑"];
48
36
  const TICK_MS = 80;
49
37
  const PASS = green("✔");
@@ -63,19 +51,12 @@ type RowState = {
63
51
  };
64
52
 
65
53
  /**
66
- * Runs `tasks` in parallel and reports their progress as a board: one row per
67
- * task with a live spinner that collapses to ✔/✖ on settle. On a TTY the rows
68
- * update in place; otherwise (CI, pipes) each row prints once when it settles,
69
- * keeping logs deterministic. After every task settles, each task's captured
70
- * detail is flushed grouped under its label, followed by a one-line summary.
71
- * Parallelism is never sacrificed — the renderer only reflects work that is
72
- * already running.
54
+ * Runs `tasks` in parallel, rendering one row each that collapses to ✔/✖, then
55
+ * flushes their captured detail and a one-line summary. On a TTY the rows update
56
+ * in place; otherwise each prints once on settle, keeping logs deterministic.
73
57
  */
74
58
  export async function runTaskBoard(tasks: BoardTask[], options: BoardOptions = {}): Promise<BoardResult> {
75
59
  const live = hasTTY && !isCI;
76
- // The `┌ │ └` frame is reserved for composition — `rr check` sets `frame: true`
77
- // to divide its sections. A standalone command never frames, even a monorepo
78
- // run with many rows (it's still one command): it gets a plain title + summary.
79
60
  const framed = options.frame ?? false;
80
61
  return live ? runLive(tasks, options, framed) : runStatic(tasks, options, framed);
81
62
  }
@@ -122,7 +103,7 @@ async function runLive(tasks: BoardTask[], options: BoardOptions, framed: boolea
122
103
  frame = (frame + 1) % FRAMES.length;
123
104
  render();
124
105
  }
125
- render(); // final frame with every row collapsed
106
+ render(); // collapse every row to its final glyph
126
107
  } finally {
127
108
  out.write("\x1b[?25h"); // restore cursor
128
109
  }
@@ -171,22 +152,19 @@ function finish(rows: RowState[], out: NodeJS.WriteStream, framed: boolean, mult
171
152
  // package ran — so a monorepo shows the command once instead of per package.
172
153
  const shared = blocks.length > 1 ? sharedLeadingLine(blocks.map((b) => b.detail)) : undefined;
173
154
 
174
- // Framed sections lay their body inside the gutter (`│`); a plain board (no
175
- // frame) indents it. The spacer above each is the gutter bar or a blank line.
155
+ // Framed bodies sit inside the `│` gutter; a plain board indents instead.
176
156
  const block = (text: string) => (framed ? gutter(text) : indent(text));
177
157
  const spacer = () => out.write(framed ? `${BAR}\n` : "\n");
178
158
 
179
159
  let flushed = false;
180
160
  if (shared) {
181
161
  spacer();
182
- out.write(`${block(shared)}\n`); // already dim (it's the command), shown once
162
+ out.write(`${block(shared)}\n`);
183
163
  flushed = true;
184
164
  }
185
165
 
186
- // A passing task's output is dimmed — it's the tool's proof-of-work that
187
- // should recede but stay visible; a failing task keeps full brightness so the
188
- // diagnostic reads. A per-task header keeps each block attributable, except a
189
- // single task (the row above already names it).
166
+ // Passing output is dimmed (proof-of-work that recedes); a failure stays bright
167
+ // so the diagnostic reads. Multi-row blocks get a per-task header to attribute them.
190
168
  for (const b of blocks) {
191
169
  const rest = shared ? stripLeadingLine(b.detail, shared) : b.detail;
192
170
  if (!rest.trim()) continue; // only the shared command → nothing package-specific
@@ -201,8 +179,8 @@ function finish(rows: RowState[], out: NodeJS.WriteStream, framed: boolean, mult
201
179
  flushed = true;
202
180
  }
203
181
 
204
- // Summary closes a framed section (└) or a plain multi-row board (a single
205
- // command's compact output needs nonethe row already carried the verdict).
182
+ // A framed section closes with `└ summary`; a plain multi-row board with a bare
183
+ // summary line. One compact task needs neitherits row already showed the verdict.
206
184
  if (framed) {
207
185
  if (flushed || multi) spacer();
208
186
  out.write(`${BAR_END} ${summary(rows)}\n`);
@@ -224,11 +202,7 @@ function stripLeadingLine(detail: string, line: string): string {
224
202
  return detail.startsWith(line) ? detail.slice(line.length).replace(/^\n/, "") : detail;
225
203
  }
226
204
 
227
- /**
228
- * The leading decoration for a row. Framed: `│` per row, or `┌` for a framed
229
- * single task (status rides the opening corner). Unframed: a 2-space indent
230
- * under the title for a multi-row board, nothing for a lone compact row.
231
- */
205
+ /** A row's leading decoration: `│` (framed multi), `┌` (framed single, status rides the corner), or a plain indent. */
232
206
  function rowPrefix(framed: boolean, multi: boolean): string {
233
207
  if (framed) return multi ? `${BAR} ` : `${BAR_START} `;
234
208
  return multi ? " " : "";