rman 2.10.0 → 2.11.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.
@@ -4,9 +4,10 @@ import { inspect } from 'node:util';
4
4
  import colors from 'ansi-colors';
5
5
  import { tokenize } from 'fast-tokenizer';
6
6
  import { Task } from 'power-tasks';
7
+ import { LOG_LEVELS, Logger, resolveRootLogLevel } from '../core/classes/logger.js';
8
+ import { PanelReporter, PlainReporter } from '../core/classes/run-reporters.js';
7
9
  import { Service } from '../core/classes/service.js';
8
10
  import { exec } from '../utils/exec.js';
9
- import { LOG_LEVELS, Logger, resolveRootLogLevel } from '../utils/logger.js';
10
11
  import { filterPackages } from '../utils/package-filter.js';
11
12
  import { ProgressPanel } from '../utils/progress-panel.js';
12
13
  import { runBin } from '../utils/run-bin.js';
@@ -44,7 +45,7 @@ export class RunService extends Service {
44
45
  }
45
46
  const concurrency = resolveConcurrency(options.parallel, resolveNumber(undefined, repository.rootPackage, script, 'concurrency', os.cpus().length));
46
47
  const progress = resolveBool(options.progress, repository.rootPackage, script, 'progress', true);
47
- const panel = new ProgressPanel(`RUN ${script}`, !!process.stdout.isTTY && progress);
48
+ const panel = new ProgressPanel(`RUN ${script}`, !!process.stdout.isTTY && progress && !repository.app.logSink?.json);
48
49
  /** Shared across all `if: changed[ = hash]` evaluations so the same reference is only `git`-queried once. */
49
50
  const ifStatusCache = new Map();
50
51
  /** Repo-wide bookend: root's own pre/post hooks run once each, exclusively, around every package
@@ -123,11 +124,11 @@ export class RunService extends Service {
123
124
  * repository the root *is* the one package, and is covered. */
124
125
  const definedSomewhere = repository.getPackages().some(pkg => getScriptSteps(pkg, script).length > 0);
125
126
  if (definedSomewhere) {
126
- console.log(colors.gray(`Nothing to run - every package was filtered out of "${script}".`));
127
+ this.say('info', `Nothing to run - every package was filtered out of "${script}".`);
127
128
  return;
128
129
  }
129
130
  const message = `No package defines a "${script}" script.`;
130
- console.log(colors.red(message));
131
+ this.say('error', message);
131
132
  const err = new Error(message);
132
133
  err.logged = true;
133
134
  throw err;
@@ -147,6 +148,16 @@ export class RunService extends Service {
147
148
  */
148
149
  async schedule(options) {
149
150
  const { panel, concurrency, commandName } = options;
151
+ /** Every event of the run is produced once, here, and rendered by each reporter - the screen
152
+ * (panel or plain lines, or neither under `--json`) and the log. See `Reporter`. */
153
+ const reporters = this.reportersFor(commandName, panel);
154
+ const emit = (event, origin) => {
155
+ for (const reporter of reporters)
156
+ reporter.report(event, origin);
157
+ };
158
+ /** What a child is started with so the reporters can render it - colour where one prints it. */
159
+ const childEnv = Object.assign({}, ...reporters.map(r => r.childEnv ?? {}));
160
+ const runStarted = Date.now();
150
161
  /** Set once the aggregate Task exists, so a package's own failure can trigger a manual
151
162
  * abort using *its own* resolved bail setting (see `runSteps` below) - power-tasks' own
152
163
  * `bail` is a single blanket policy for the whole batch, it can't vary per package. */
@@ -184,50 +195,60 @@ export class RunService extends Service {
184
195
  ctx.currentCommand = command ?? stepLabel;
185
196
  };
186
197
  ctx.stepIndex = i;
187
- if (panel.enabled) {
188
- const onLine = (line) => {
189
- ctx.log.push(line);
190
- ctx.lastLine = line;
191
- };
198
+ const stepStart = Date.now();
199
+ const origin = { item: ctx, logLevel: pkgLogLevel, label: step.label };
200
+ /** Every step is bracketed - `start` before, `end` after whatever happened - and every line
201
+ * it produces is an `output` event, whichever reporters render them. */
202
+ emit({ event: 'start', package: pkgLabel, step: step.name, command: stepLabel }, origin);
203
+ const onLine = (line, stream = 'stdout', level) => emit({ event: 'output', package: pkgLabel, stream, line, ...(level && { level }) }, origin);
204
+ let stepError;
205
+ try {
206
+ /**
207
+ * **Piped, never inherited - the child never sees a terminal**, whether or not a panel is
208
+ * drawing. Every line comes back as an event and the reporters print or store it. A child
209
+ * that finds a TTY draws its own live output, and a build is mostly other CLIs: the
210
+ * shared preset's `run.build` is `rman check`, `rman lint`, `rman clean` and `tsc`.
211
+ * Reported as `rman build --no-progress` printing progress and losing its logs: a nested
212
+ * `rman check` drew its own panel inside a run asked not to draw one.
213
+ *
214
+ * **No TTY is the convention every well-behaved CLI already honours**, which is why it
215
+ * beats telling children about `--no-progress`: an environment variable would reach
216
+ * nested rman and nothing else. The cost, stated: a child cannot prompt. Nothing in a
217
+ * run step should.
218
+ */
192
219
  if (step.run)
193
220
  await runFunctionStep(step.run, pkg, cwd, onLine, onCommand);
194
- else
195
- await exec(step.command, { cwd, stdio: 'pipe', onLine, app: pkg.repository.app });
196
- }
197
- else {
198
- /** Match the classic rman output: raw command output streams straight through
199
- * (unbuffered, unprefixed), followed by our own one-line-per-step summary. */
200
- printLegacyExecutingLine(commandName, pkgLabel, step, pkgLogLevel);
201
- const stepStart = Date.now();
202
- let stepError;
203
- try {
204
- /** No capture with the panel off: the step owns the terminal, exactly as a shell
205
- * step's `stdio: 'inherit'` does. */
206
- if (step.run)
207
- await runFunctionStep(step.run, pkg, cwd, undefined, onCommand);
208
- else
209
- await exec(step.command, { cwd, stdio: 'inherit', app: pkg.repository.app });
210
- }
211
- catch (e) {
212
- /**
213
- * **Normalized to an `Error`, because a *falsy* throw was indistinguishable from no
214
- * failure at all.** This was `let stepError: any` with `if (stepError) throw
215
- * stepError` below, so a step doing `throw undefined` - legal JavaScript, and what a
216
- * rejected promise carrying nothing gives you - left `stepError` falsy: the step
217
- * line printed **success**, nothing was rethrown, and the run exited 0. Found by the
218
- * spec written for the message-reporting fix above, which is the only reason it is
219
- * not still there. A step that fails while reporting success is the one outcome this
220
- * slot exists to rule out.
221
- *
222
- * Only the panel-off path had it: with the panel on there is no local catch, and the
223
- * outer one runs whatever was thrown.
224
- */
225
- stepError = e instanceof Error ? e : new Error(messageOf(e));
221
+ else {
222
+ await exec(step.command, {
223
+ cwd,
224
+ stdio: 'pipe',
225
+ onLine,
226
+ env: Object.keys(childEnv).length ? childEnv : undefined,
227
+ app: pkg.repository.app,
228
+ });
226
229
  }
227
- printLegacyStepLine(commandName, pkgLabel, step, Date.now() - stepStart, pkgLogLevel, stepError);
228
- if (stepError)
229
- throw stepError;
230
230
  }
231
+ catch (e) {
232
+ /**
233
+ * **Normalized to an `Error`, because a *falsy* throw was indistinguishable from no
234
+ * failure at all.** This was `let stepError: any` with `if (stepError) throw
235
+ * stepError` below, so a step doing `throw undefined` - legal JavaScript, and what a
236
+ * rejected promise carrying nothing gives you - left `stepError` falsy: the step
237
+ * line printed **success**, nothing was rethrown, and the run exited 0. A step that
238
+ * fails while reporting success is the one outcome this slot exists to rule out.
239
+ */
240
+ stepError = e instanceof Error ? e : new Error(messageOf(e));
241
+ }
242
+ emit({
243
+ event: 'end',
244
+ package: pkgLabel,
245
+ step: step.name,
246
+ status: stepError ? 'failed' : 'success',
247
+ ms: Date.now() - stepStart,
248
+ error: stepError?.message,
249
+ }, origin);
250
+ if (stepError)
251
+ throw stepError;
231
252
  }
232
253
  /** Another part is coming, so the package is not done - and it is about to sit at the
233
254
  * barrier, which the row says out loud rather than leaving the last step's command on
@@ -272,16 +293,17 @@ export class RunService extends Service {
272
293
  /**
273
294
  * **Where this package starts waiting for its dependencies**, or `-1` for not at all.
274
295
  *
275
- * A script whose steps mention `topo` at all is taken as the whole statement: the first step
276
- * saying `true` is where the package blocks, and a script where every step says `false` does
277
- * not wait. Only where **no** step mentions it does `run.<script>.topo` decide, and then the
278
- * wait is before the first step - where it has always been.
296
+ * The first step marked `true`. With none, the first step that says nothing - an unmarked
297
+ * step takes `run.<script>.topo`, which got here only by being on. Only a script whose every
298
+ * step says `false` does not wait.
279
299
  *
280
- * The two halves cannot be collapsed into one `findIndex`: "no step marked true" and "no step
281
- * marked at all" are different answers (no wait against the old wait at step 0), and reading
282
- * `-1` as either one alone gets the other wrong.
300
+ * A step marked `false` frees itself and nothing else. It used to free the whole script: any
301
+ * mention of `topo` was read as the complete statement, so one `after: { topo: false }` from
302
+ * a shared preset left every unmarked step beside it - a repository's own `before`, a
303
+ * package's own `build` script - running with no wait at all. Measured on opra, where
304
+ * `@opra/angular`'s `ng build` started while `@opra/client` was still cleaning.
283
305
  */
284
- const barrier = !plan.topo ? -1 : steps.some(s => s.topo !== undefined) ? steps.findIndex(s => s.topo) : 0;
306
+ const barrier = !plan.topo ? -1 : this.barrierOf(steps);
285
307
  /**
286
308
  * **Split only when the wait is in the middle of the package and there is something to wait
287
309
  * for.** A barrier at step 0 is one task, as it always was; a package with no in-run
@@ -320,6 +342,22 @@ export class RunService extends Service {
320
342
  }
321
343
  panel.detail = this.repository.name;
322
344
  panel.start(this.repository.app.statusRegion);
345
+ /**
346
+ * **With no panel, the status line is silenced for the run as well** - not handed over, silenced:
347
+ * the children's lines are printed straight to the terminal (see the panel-off path in
348
+ * `runSteps`), and a spinner redrawing in place moves the cursor up over whatever was printed
349
+ * since. This is the case `--no-progress` does not cover, since the CLI never draws the line under
350
+ * that flag: a panel turned off by `.rmanrc run.<script>.progress: false`, or by stdout being
351
+ * redirected while stderr is still a terminal.
352
+ *
353
+ * It used to be left drawing on the grounds that it was "the one thing such a run still shows".
354
+ * That held while the children inherited the terminal and nothing was printed past it; once the
355
+ * run prints their lines itself, the two cannot share the screen.
356
+ */
357
+ const statusRegion = this.repository.app.statusRegion;
358
+ const muted = !panel.enabled && !!statusRegion?.live;
359
+ if (muted)
360
+ statusRegion.suspend({ passThrough: text => process.stderr.write(text) });
323
361
  try {
324
362
  /** bail:false here - each package's own resolved bail setting decides whether to call
325
363
  * `rootTask.abort()` itself (see `runSteps`), since power-tasks' own `bail` can't vary per package. */
@@ -338,13 +376,19 @@ export class RunService extends Service {
338
376
  * packages went on to succeed. */
339
377
  await Promise.allSettled(children.map(child => child.toPromise()));
340
378
  panel.stop();
379
+ if (muted)
380
+ statusRegion.resume();
341
381
  }
342
- const summary = panel.printSummary();
343
- /** The tallies, never `rootTask.toPromise()`'s own outcome: with a sibling still in flight at
344
- * the moment one package failed, that promise *resolves*, and this command used to exit 0 on a
345
- * run it had just reported as failed - non-deterministically, since it came down to which
346
- * packages happened to still be running (measured: `1 0 1 1 0` across five identical runs).
347
- * A single failed package must fail the command, every time. */
382
+ /** Counted once, then reported - a screen reporter prints the recap from the same tally, so the
383
+ * prose and the `summary` event cannot disagree. */
384
+ const summary = panel.tally();
385
+ emit({
386
+ event: 'summary',
387
+ succeeded: summary.successCount,
388
+ failed: summary.failedCount,
389
+ skipped: summary.skippedCount,
390
+ ms: Date.now() - runStarted,
391
+ });
348
392
  /** The tallies, never `rootTask.toPromise()`'s own outcome: with a sibling still in flight at
349
393
  * the moment one package failed, that promise *resolves*, and this command used to exit 0 on a
350
394
  * run it had just reported as failed - non-deterministically, since it came down to which
@@ -380,7 +424,7 @@ export class RunService extends Service {
380
424
  const label = options.label ?? 'each';
381
425
  const logLevel = options.logLevel ?? resolveRootLogLevel(repository);
382
426
  const concurrency = resolveConcurrency(options.parallel, os.cpus().length);
383
- const panel = new ProgressPanel(label.toUpperCase(), !!process.stdout.isTTY && options.progress !== false);
427
+ const panel = new ProgressPanel(label.toUpperCase(), !!process.stdout.isTTY && options.progress !== false && !repository.app.logSink?.json);
384
428
  /** Nothing to do is not a failure, and the caller is the one that can tell the two endings
385
429
  * apart - it knows why its list is empty. `check` says so itself and returns. */
386
430
  if (!packages.length)
@@ -455,6 +499,43 @@ export class RunService extends Service {
455
499
  throw failure;
456
500
  return results;
457
501
  }
502
+ /**
503
+ * The index of the step a package waits for its dependencies before, or `-1` when every step is
504
+ * marked `topo: false`: the first step marked `true`, else the first unmarked one.
505
+ */
506
+ barrierOf(steps) {
507
+ /* No steps still waits, as it always did: dependents wait on this package's task, so one that
508
+ * skipped the wait would let them past its own dependencies. */
509
+ if (!steps.length)
510
+ return 0;
511
+ const marked = steps.findIndex(s => s.topo === true);
512
+ if (marked >= 0)
513
+ return marked;
514
+ return steps.findIndex(s => s.topo === undefined);
515
+ }
516
+ /**
517
+ * A line of rman's own about the run - "nothing to run", "no package defines it" - on the console
518
+ * as prose, or as a `message` event under `--json`, where stdout belongs to the events. Also in the
519
+ * log file when there is one.
520
+ */
521
+ say(level, message) {
522
+ for (const reporter of this.reportersFor('run'))
523
+ reporter.report({ event: 'message', level, message });
524
+ }
525
+ /**
526
+ * Who renders a run: the panel when it draws, plain lines when it does not, neither under `--json`
527
+ * (stdout belongs to the events) - plus the run's log, when the CLI asked for one.
528
+ */
529
+ reportersFor(commandName, panel) {
530
+ const sink = this.repository.app.logSink;
531
+ const reporters = [];
532
+ if (!sink?.json) {
533
+ reporters.push(panel?.enabled ? new PanelReporter(commandName, panel) : new PlainReporter(commandName, panel));
534
+ }
535
+ if (sink?.active)
536
+ reporters.push(sink);
537
+ return reporters;
538
+ }
458
539
  }
459
540
  (function (RunService) {
460
541
  /**
@@ -601,15 +682,28 @@ export class RunService extends Service {
601
682
  return pristine[method](...args);
602
683
  /** Split, because one `console.log` may carry several lines and the panel's log is a list
603
684
  * of them - a multi-line entry would render as one unreadable row. */
685
+ const stream = method === 'error' || method === 'warn' ? 'stderr' : 'stdout';
604
686
  for (const line of format(args).split('\n'))
605
- sink(line);
687
+ sink(line, stream);
606
688
  };
607
689
  }
608
690
  pristineConsole = pristine;
609
691
  }
610
692
  /** `Promise.resolve`, because a step may be a plain function - `RunStepFn` returns
611
- * `void | Promise<void>` and only the promise form can be awaited. */
612
- return Promise.resolve(consoleSink.run(onLine, fn)).finally(() => {
693
+ * `void | Promise<void>` and only the promise form can be awaited.
694
+ *
695
+ * **A synchronous throw is caught into a rejection**, or it leaves before `finally` is attached
696
+ * and the depth never comes back down - every later step then finds the console "already
697
+ * patched" and captures nothing. Hidden while only the panel path captured; it surfaced the
698
+ * day a plain run did, as two specs in another describe block failing only after this one. */
699
+ let result;
700
+ try {
701
+ result = consoleSink.run(onLine, fn);
702
+ }
703
+ catch (e) {
704
+ result = Promise.reject(e);
705
+ }
706
+ return Promise.resolve(result).finally(() => {
613
707
  if (--consoleDepth > 0 || !pristineConsole)
614
708
  return;
615
709
  for (const method of CAPTURED_CONSOLE)
@@ -724,36 +818,6 @@ export class RunService extends Service {
724
818
  }
725
819
  RunService.evaluateIf = evaluateIf;
726
820
  })(RunService || (RunService = {}));
727
- /**
728
- * Classic one-line-per-step log ("info build pkg ┆ step success ┆ command (123 ms)"),
729
- * matching rman's original npmlog-based output. Used when the live panel is off.
730
- *
731
- * Respects `logLevel`: 'verbose' also prints an "executing" line before the step runs
732
- * (rman's old behavior, hidden by default); 'error' suppresses success lines; 'silent'
733
- * suppresses everything but a real failure.
734
- */
735
- function printLegacyExecutingLine(commandName, pkgLabel, step, level) {
736
- if (LOG_LEVELS.indexOf(level) < LOG_LEVELS.indexOf('verbose'))
737
- return;
738
- const sep = colors.gray('┆');
739
- console.log(colors.magenta('verbose'), commandName, colors.cyan(pkgLabel), sep, colors.cyanBright.bold(step.name), colors.cyanBright.bold('executing'), ...describing(step));
740
- }
741
- function printLegacyStepLine(commandName, pkgLabel, step, durationMs, level, error) {
742
- if (!error && LOG_LEVELS.indexOf(level) < LOG_LEVELS.indexOf('info'))
743
- return;
744
- if (error && level === 'silent')
745
- return;
746
- const sep = colors.gray('┆');
747
- const levelLabel = error ? colors.red('error') : colors.green('info');
748
- const status = error ? colors.red.bold('failed') : colors.green.bold('success');
749
- console.log(levelLabel, commandName, colors.cyan(pkgLabel), sep, colors.cyanBright.bold(step.name), status, ...describing(step), colors.yellow(` (${durationMs} ms)`));
750
- }
751
- /** The `┆ <what it runs>` tail of a legacy log line, **or nothing at all** when the step has no
752
- * label of its own - an anonymous function handed to `forEachPackage`, where the slot name already
753
- * said everything there is to say and a separator with nothing after it is just noise. */
754
- function describing(step) {
755
- return step.label ? [colors.gray('┆'), step.label] : [];
756
- }
757
821
  function describeValue(value) {
758
822
  if (Array.isArray(value))
759
823
  return 'a nested array';
@@ -814,50 +878,30 @@ function assertStepObject(item, where, allowTopo) {
814
878
  /** What a step object may hold - the step itself, and the marker beside it. */
815
879
  const STEP_OBJECT_KEYS = ['command', 'topo'];
816
880
  /**
817
- * Runs a function step.
818
- *
819
- * **`console` is redirected while it runs, but only when the panel is on** - and that is the same
820
- * split a shell step already makes. `exec` hands the panel its output through `stdio: 'pipe'` and
821
- * `onLine`, so a function writing straight to the real stdout would print *over* the panel it is
822
- * being rendered inside. With the panel off, `exec` uses `stdio: 'inherit'` and the step owns the
823
- * terminal; a function gets the same, untouched.
881
+ * Runs a function step, with its `console` captured into `onLine` - the step's own lines, routed to
882
+ * whichever reporters the run has, beside those of the children it spawns.
824
883
  *
825
- * Restored in a `finally`, because a step that throws must not leave the rest of the run writing
826
- * into a log nobody reads.
884
+ * **Always captured, panel or not.** With no panel a function's `console.log` used to go straight to
885
+ * the terminal, so it was the one kind of step output a plain run could not lead with its package -
886
+ * and it reached `--json` stdout and the log file not at all.
827
887
  *
828
888
  * **Its failure message is reported here, because nothing else does it.** A shell step's reason
829
- * reaches the user on its own - the output streams through `onLine` or straight to the terminal,
830
- * and `exec` names the command and its exit code. A function step has neither: it fails by
831
- * throwing, and the throw goes to the outer `catch` that marks the package failed and rethrows an
832
- * error the CLI treats as already-logged. Measured on a real config whose build step threw a
833
- * carefully worded message about a missing `tsconfig.json`: the run printed
889
+ * reaches the user on its own - its output streams and `exec` names the command and its exit code.
890
+ * A function step fails by throwing, and the throw goes to the catch that marks the package failed
891
+ * and rethrows an error the CLI treats as already-logged. Measured on a real config whose build step
892
+ * threw a carefully worded message about a missing `tsconfig.json`: the run printed
834
893
  * `error build pkg-forgot ┆ exec failed ┆ buildWithTsc` and exited 1, and the message appeared
835
- * nowhere at all - so the one thing that said what to do was the one thing dropped.
836
- *
837
- * Written where a shell step's output goes, so it needs no second channel: through `onLine` with
838
- * the panel on (the step's own log, which is what the panel shows for a failed item), and to
839
- * stderr with it off, ahead of the `failed` line - the order a shell step already produces.
894
+ * nowhere at all. Written to `onLine` on stderr, ahead of the step's `end` - the order a shell step
895
+ * already produces.
840
896
  */
841
897
  async function runFunctionStep(run, pkg, cwd, onLine, onCommand) {
842
898
  const context = RunService.createStepContext(pkg, cwd, onCommand, onLine);
843
- if (!onLine) {
844
- try {
845
- await run(context);
846
- }
847
- catch (e) {
848
- console.error(colors.red(messageOf(e)));
849
- throw e;
850
- }
851
- return;
852
- }
853
899
  try {
854
900
  await RunService.withCapturedConsole(onLine, () => run(context));
855
901
  }
856
902
  catch (e) {
857
- /** Straight to `onLine`, so the message lands in this step's log rather than over the panel it
858
- * is drawn inside. */
859
903
  for (const line of messageOf(e).split('\n'))
860
- onLine(line);
904
+ onLine(colors.red(line), 'stderr', 'error');
861
905
  throw e;
862
906
  }
863
907
  }
@@ -959,9 +1003,30 @@ function slotValues(slot, own, configured, override) {
959
1003
  if (override)
960
1004
  return configured.length ? configured : own;
961
1005
  if (slot === 'exec')
962
- return own.length ? own : configured;
1006
+ return own.length ? withTopoOf(configured, own) : configured;
963
1007
  return slot === 'before' ? [...configured, ...own] : [...own, ...configured];
964
1008
  }
1009
+ /**
1010
+ * The package's own `exec` steps, carrying the `topo` mark of the configured `exec` they replace.
1011
+ */
1012
+ /* **The command is the package's; the ordering is the config's.** A `package.json` script cannot
1013
+ * say `topo` at all, so replacing the configured `exec: { topo: true, command }` with it dropped the
1014
+ * mark - and a shared preset marks `exec` precisely so the build waits for its dependencies.
1015
+ * Measured on opra: `@opra/angular`'s own `"build": "ng build"` started while `@opra/client`, a
1016
+ * peer it builds against, was still cleaning. Every package with its own build script had the same
1017
+ * hole; angular is where a missing dependency output fails loudly. A `true` anywhere in the
1018
+ * replaced slot wins, since a missed wait is the failure that does not look like one. */
1019
+ function withTopoOf(configured, own) {
1020
+ const marks = configured.map(v => (typeof v === 'object' ? v.topo : undefined)).filter(t => t !== undefined);
1021
+ if (!marks.length)
1022
+ return own;
1023
+ const topo = marks.includes(true);
1024
+ return own.map(v => {
1025
+ if (typeof v === 'object')
1026
+ return v.topo === undefined ? { ...v, topo } : v;
1027
+ return { command: v, topo };
1028
+ });
1029
+ }
965
1030
  /**
966
1031
  * What this package's own technology says it declares for `script`.
967
1032
  *
@@ -4,7 +4,7 @@ import { assertOneScheme, semverScheme, VersionScheme } from '../core/classes/ve
4
4
  import { GitHelper } from '../utils/git.js';
5
5
  import { filterPackages } from '../utils/package-filter.js';
6
6
  import { findLastReleaseVersion, formatCalendarVersion, usesCalendarVersion } from '../utils/release-version.js';
7
- import { groupKeyOf } from '../utils/version-group.js';
7
+ import { groupKeyOf, ROOT_GROUP_KEY } from '../utils/version-group.js';
8
8
  import { ConventionalCommitsService } from './conventional-commits.service.js';
9
9
  /**
10
10
  * Question A, computed and nothing else: **which packages have changed since their last release,
@@ -479,7 +479,7 @@ export class VersionPlanService {
479
479
  const root = repository.rootPackage;
480
480
  const anyBumped = memberEntries.some(e => e.status === 'bump');
481
481
  if (!anyBumped) {
482
- return { package: root, groupKey: '__root__', group: 'root', status: 'no-change', from: root.version };
482
+ return { package: root, groupKey: ROOT_GROUP_KEY, group: 'root', status: 'no-change', from: root.version };
483
483
  }
484
484
  const calendar = usesCalendarVersion({
485
485
  groupCount: context.groupCount,
@@ -492,7 +492,7 @@ export class VersionPlanService {
492
492
  : (root.versionScheme.highestVersion(finalVersions) ?? root.version);
493
493
  return {
494
494
  package: root,
495
- groupKey: '__root__',
495
+ groupKey: ROOT_GROUP_KEY,
496
496
  group: 'root',
497
497
  status: 'bump',
498
498
  from: root.version,
@@ -90,5 +90,11 @@ export declare class ProgressPanel implements TerminalRegion {
90
90
  * per-item recap lines are only printed when the live panel was actually on - with it off, the
91
91
  * caller's own plain logging already showed each item's outcome as it happened.
92
92
  */
93
+ /**
94
+ * The tally `printSummary` reports, without printing anything - for a run whose output is not
95
+ * prose (`--json`). Settles every still-pending item as skipped, exactly as `printSummary` does, so
96
+ * the two can never count differently.
97
+ */
98
+ tally(): ProgressSummary;
93
99
  printSummary(): ProgressSummary;
94
100
  }
@@ -226,21 +226,38 @@ export class ProgressPanel {
226
226
  * per-item recap lines are only printed when the live panel was actually on - with it off, the
227
227
  * caller's own plain logging already showed each item's outcome as it happened.
228
228
  */
229
- printSummary() {
229
+ /**
230
+ * The tally `printSummary` reports, without printing anything - for a run whose output is not
231
+ * prose (`--json`). Settles every still-pending item as skipped, exactly as `printSummary` does, so
232
+ * the two can never count differently.
233
+ */
234
+ tally() {
230
235
  let successCount = 0;
231
236
  let failedCount = 0;
232
237
  let skippedCount = 0;
233
238
  for (const item of this.items.values()) {
234
239
  if (item.status === 'pending')
235
240
  item.status = 'skipped';
241
+ if (item.status === 'success')
242
+ successCount++;
243
+ else if (item.status === 'failed')
244
+ failedCount++;
245
+ else
246
+ skippedCount++;
247
+ }
248
+ return { successCount, failedCount, skippedCount };
249
+ }
250
+ printSummary() {
251
+ /** Counted by `tally`, which also settles the pending items - so a `--json` run, which asks
252
+ * `tally` alone, can never count differently from one that prints. */
253
+ const { successCount, failedCount, skippedCount } = this.tally();
254
+ for (const item of this.items.values()) {
236
255
  const duration = item.startedAt && item.finishedAt ? colors.gray(formatDuration(item.finishedAt - item.startedAt)) : '';
237
256
  if (item.status === 'success') {
238
- successCount++;
239
257
  if (this.live.enabled)
240
258
  console.log(colors.green('✓'), item.name, duration);
241
259
  }
242
260
  else if (item.status === 'failed') {
243
- failedCount++;
244
261
  if (this.live.enabled) {
245
262
  console.log(colors.red.bold('X'), item.name, duration);
246
263
  /* **The replayed log is printed as it was captured, never painted.** It holds the output
@@ -261,10 +278,8 @@ export class ProgressPanel {
261
278
  console.log(item.log.join('\n'));
262
279
  }
263
280
  }
264
- else {
265
- skippedCount++;
266
- if (this.live.enabled)
267
- console.log(colors.gray('○'), item.name, colors.gray('skipped'));
281
+ else if (this.live.enabled) {
282
+ console.log(colors.gray('○'), item.name, colors.gray('skipped'));
268
283
  }
269
284
  }
270
285
  const totalElapsed = formatDuration(Date.now() - this.startedAt);
@@ -1,5 +1,5 @@
1
1
  import type { RmanApplication } from '../core/application.js';
2
- import { type LogLevel } from './logger.js';
2
+ import { type LogLevel } from '../core/classes/logger.js';
3
3
  export interface RunBinOptions {
4
4
  /**
5
5
  * The application whose technologies put a repository's locally installed binaries on PATH -
@@ -30,7 +30,8 @@ export interface RunBinOptions {
30
30
  /**
31
31
  * Called once per line of the child's output **instead of writing it to the terminal**.
32
32
  */
33
- onLine?: (line: string) => void;
33
+ /** The stream each line came from, so a caller printing them can keep stdout and stderr apart. */
34
+ onLine?: (line: string, stream: 'stdout' | 'stderr') => void;
34
35
  env?: Record<string, string | undefined>;
35
36
  }
36
37
  export interface RunBinResult {
@@ -58,3 +59,10 @@ export interface RunBinResult {
58
59
  * nothing is the outcome worth ruling out, and that is what silently discarding a code produces.
59
60
  */
60
61
  export declare function runBin(bin: string, argv: string[], options?: RunBinOptions): Promise<RunBinResult>;
62
+ /**
63
+ * Whether a child whose lines rman prints itself should be told to colour them: our own stdout is a
64
+ * terminal, and `NO_COLOR` is not set. The one rule for both kinds of step - `runBin`'s, above, and a
65
+ * shell step `exec` runs with its lines printed by `RunService` - so the two cannot disagree about
66
+ * when a piped child gets its colour back.
67
+ */
68
+ export declare function colorsPrintedOutput(): boolean;