rman 2.7.0 → 2.9.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.
@@ -101,6 +101,8 @@ export declare namespace VersionService {
101
101
  * never run. (The doc comment also still named `.script`/`.preScript`/`.postScript`, three keys
102
102
  * that have been `before`/`exec`/`after` for a long time.)
103
103
  */
104
+ /** `topo` is refused by default here, which is the one way this differs from `RunService`'s: a
105
+ * version hook has no package ordering to join. */
104
106
  function normalizeScriptValue(value: unknown, at: string): RunStepValue[];
105
107
  /**
106
108
  * Keeps a package's Dockerfile `org.opencontainers.image.version` label in step with the version
@@ -86,7 +86,10 @@ export class VersionService extends Service {
86
86
  const hook = (slot) => RunService.runLifecycleSlot(pkg, VERSION_LIFECYCLE, slot, RunService.normalizeScriptValue(
87
87
  /** `at`: the path is what tells a step function from a value one, and this is a fragment -
88
88
  * without it a function here was called while the hook was being prepared. */
89
- interpolator.interpolate({ config: pkg.config?.version?.[slot], scope, at: ['version', slot] }), `version.${slot}`));
89
+ interpolator.interpolate({ config: pkg.config?.version?.[slot], scope, at: ['version', slot] }), `version.${slot}`,
90
+ /** A version hook runs for one package around its own version write - there is no
91
+ * package graph here to wait on, so `topo` is refused rather than silently ignored. */
92
+ { topo: false }));
90
93
  await hook('before');
91
94
  /** Through the manifest, not through a `package.json` field: where a version is written is
92
95
  * the provider's business (see `Plugin`'s manifest members), and this is the one place rman changes
@@ -286,8 +289,10 @@ export class VersionService extends Service {
286
289
  * never run. (The doc comment also still named `.script`/`.preScript`/`.postScript`, three keys
287
290
  * that have been `before`/`exec`/`after` for a long time.)
288
291
  */
292
+ /** `topo` is refused by default here, which is the one way this differs from `RunService`'s: a
293
+ * version hook has no package ordering to join. */
289
294
  function normalizeScriptValue(value, at) {
290
- return RunService.normalizeScriptValue(value, at);
295
+ return RunService.normalizeScriptValue(value, at, { topo: false });
291
296
  }
292
297
  VersionService.normalizeScriptValue = normalizeScriptValue;
293
298
  /**
@@ -45,7 +45,7 @@ export class ProgressPanel {
45
45
  /** Registers an item and returns it - callers mutate the returned object directly (`status`,
46
46
  * `currentStep`, `lastLine`, ...) to drive the next render tick. */
47
47
  addItem(name, stepsTotal) {
48
- const item = { name, status: 'pending', stepsTotal, log: [] };
48
+ const item = new PanelItem(name, stepsTotal);
49
49
  this.items.set(name, item);
50
50
  return item;
51
51
  }
@@ -134,28 +134,46 @@ export class ProgressPanel {
134
134
  `${header} ${bar} ${colors.bold(`${done}/${total}`)} ${colors.cyan(`${running} running`)} ${failedText} ${colors.yellow(totalElapsed)}${detail}`,
135
135
  ];
136
136
  const runningList = [...this.items.values()].filter(i => i.status === 'running');
137
- /** Leave room for the header, a safety margin, and a possible "N more running" line - a
138
- * block taller than the terminal breaks cursor-up math (the terminal scrolls instead of
139
- * the cursor moving, so redraws land in the wrong place and pile up). */
137
+ /**
138
+ * **A package that failed stays on the list, under the ones still working.**
139
+ * It used to vanish the moment it failed, so on a long run the only sign that anything had gone
140
+ * wrong was a count in the header - and whatever it printed was not read until the recap, by
141
+ * which time the run had been going for another half-minute.
142
+ *
143
+ * **Below the running rows, and only with the space they leave.** Work in progress is what the
144
+ * panel is for; a repository that fails early would otherwise fill the block with corpses and
145
+ * push the live rows off the screen. One line each rather than two, so more of them fit - a
146
+ * failed row's value is that it is *named*, and its output is replayed in full at the end.
147
+ */
148
+ const failedList = [...this.items.values()].filter(i => i.status === 'failed');
149
+ /** Leave room for the header, a safety margin, and a possible "N more" line - a block taller
150
+ * than the terminal breaks cursor-up math (the terminal scrolls instead of the cursor moving,
151
+ * so redraws land in the wrong place and pile up). */
140
152
  const budget = Math.max(0, (process.stdout.rows || 24) - 3);
153
+ const width = process.stdout.columns || 80;
141
154
  let used = 0;
142
- let shown = 0;
143
- for (const item of runningList) {
144
- const elapsed = item.startedAt ? formatDuration(Date.now() - item.startedAt) : '';
155
+ let shownRunning = 0;
156
+ let shownFailed = 0;
157
+ /** The fixed-width half of a row, plus the command truncated into whatever is left. */
158
+ const describe = (item, elapsed) => {
145
159
  const step = item.stepsTotal && item.stepsTotal > 1 && item.stepIndex != null
146
160
  ? `${item.currentStep} (${item.stepIndex + 1}/${item.stepsTotal})`
147
161
  : item.currentStep || '';
148
- /** Measured against the *plain* text: every piece below is wrapped in escape sequences, and
162
+ /** Measured against the *plain* text: every piece is wrapped in escape sequences, and
149
163
  * `String.length` counts those, so budgeting on the rendered string wraps a row that fits. */
150
164
  const fixed = ` ${item.name} ${step} ${elapsed} | `.length;
151
- const command = truncate(item.currentCommand ?? '', (process.stdout.columns || 80) - fixed - 2);
165
+ return { step, command: truncate(item.currentCommand ?? '', width - fixed - 2) };
166
+ };
167
+ for (const item of runningList) {
168
+ const elapsed = item.startedAt ? formatDuration(Date.now() - item.startedAt) : '';
169
+ const { step, command } = describe(item, elapsed);
152
170
  /**
153
171
  * **The command last, after a `|`, and that is not only layout.** It is the one field with no
154
172
  * bound on its length - a `tsc -b` line carries a path, a `run` step carries whatever the
155
173
  * author wrote - so between the step and the clock it pushed the elapsed time to a different
156
- * column on every row, and off the end entirely once a command was long. Everything fixed-width
157
- * now reads down a straight edge and the variable part runs off to the right, where `truncate`
158
- * cuts it.
174
+ * column on every row, and off the end entirely once a command was long. Everything
175
+ * fixed-width now reads down a straight edge and the variable part runs off to the right,
176
+ * where `truncate` cuts it.
159
177
  */
160
178
  const group = [
161
179
  `${spinner} ${colors.bold(item.name)} ${colors.gray(step)} ${colors.yellow(elapsed)}` +
@@ -167,11 +185,27 @@ export class ProgressPanel {
167
185
  break;
168
186
  lines.push(...group);
169
187
  used += group.length;
170
- shown++;
188
+ shownRunning++;
189
+ }
190
+ for (const item of failedList) {
191
+ if (used + 1 > budget)
192
+ break;
193
+ const elapsed = item.startedAt && item.finishedAt ? formatDuration(item.finishedAt - item.startedAt) : '';
194
+ const { step, command } = describe(item, elapsed);
195
+ lines.push(`${colors.red.bold('✖')} ${colors.bold(item.name)} ${colors.gray(step)} ${colors.yellow(elapsed)}` +
196
+ (command ? ` ${colors.gray('|')} ${colors.red(command)}` : ''));
197
+ used++;
198
+ shownFailed++;
171
199
  }
172
- const remaining = runningList.length - shown;
173
- if (remaining > 0)
174
- lines.push(colors.gray(`… and ${remaining} more running`));
200
+ /** One line for both, or a run with rows cut from each would need two and the block is already
201
+ * budgeted to the row. */
202
+ const hidden = [];
203
+ if (runningList.length > shownRunning)
204
+ hidden.push(`${runningList.length - shownRunning} more running`);
205
+ if (failedList.length > shownFailed)
206
+ hidden.push(`${failedList.length - shownFailed} more failed`);
207
+ if (hidden.length)
208
+ lines.push(colors.gray(`… and ${hidden.join(', ')}`));
175
209
  this.live.render(lines);
176
210
  }
177
211
  /** Stops the redraw loop and erases the panel, leaving the cursor where it started. */
@@ -267,3 +301,51 @@ function renderProgressBar(done, total, width = 24) {
267
301
  const filled = Math.max(0, Math.min(width, cells));
268
302
  return colors.green('█'.repeat(filled)) + colors.gray('░'.repeat(width - filled));
269
303
  }
304
+ /**
305
+ * What `addItem` hands back - a `ProgressItem` whose `currentStep` and `currentCommand` clear
306
+ * `lastLine` when they change.
307
+ */
308
+ /* **Accessors rather than a line at each call site**, because the call sites are four services and
309
+ * a command and the one that forgets is invisible: stale output under a new command does not look
310
+ * like a bug, it looks like output. Reported from `ci`, where the row read
311
+ * `install (2/2) | npm install` over `removed node_modules, package-lock.json` - the wipe's line,
312
+ * sitting under the install's command as though it belonged to it.
313
+ *
314
+ * **Cleared where it changes, not at render time.** The panel redraws every 100ms, so comparing
315
+ * there would race a line that arrived between the change and the next frame and throw away real
316
+ * output. This is synchronous with the assignment.
317
+ */
318
+ class PanelItem {
319
+ name;
320
+ status = 'pending';
321
+ stepIndex;
322
+ stepsTotal;
323
+ lastLine;
324
+ startedAt;
325
+ finishedAt;
326
+ log = [];
327
+ _currentStep;
328
+ _currentCommand;
329
+ constructor(name, stepsTotal) {
330
+ this.name = name;
331
+ this.stepsTotal = stepsTotal;
332
+ }
333
+ get currentStep() {
334
+ return this._currentStep;
335
+ }
336
+ set currentStep(value) {
337
+ if (value === this._currentStep)
338
+ return;
339
+ this._currentStep = value;
340
+ this.lastLine = undefined;
341
+ }
342
+ get currentCommand() {
343
+ return this._currentCommand;
344
+ }
345
+ set currentCommand(value) {
346
+ if (value === this._currentCommand)
347
+ return;
348
+ this._currentCommand = value;
349
+ this.lastLine = undefined;
350
+ }
351
+ }
@@ -27,6 +27,10 @@ export interface RunBinOptions {
27
27
  * even then. An explicit `stdio` still wins over all of this.
28
28
  */
29
29
  logLevel?: LogLevel;
30
+ /**
31
+ * Called once per line of the child's output **instead of writing it to the terminal**.
32
+ */
33
+ onLine?: (line: string) => void;
30
34
  env?: Record<string, string | undefined>;
31
35
  }
32
36
  export interface RunBinResult {
package/utils/run-bin.js CHANGED
@@ -39,7 +39,11 @@ export async function runBin(bin, argv, options = {}) {
39
39
  const region = options.app?.statusRegion?.live ? options.app.statusRegion : undefined;
40
40
  /** Below 'info' the output is captured rather than streamed, so a quiet run stays quiet and a
41
41
  * failing one can still say what went wrong. */
42
- const stdio = options.stdio ?? (region ? 'pipe' : atLeast('info') ? 'inherit' : 'pipe');
42
+ /** **`onLine` forces `pipe` the same way a live region does.** Left to the default, a caller at
43
+ * `info` would get `inherit` - the child writes straight to the terminal, `child.stdout` is
44
+ * null, and the callback is never called at all. An option that silently does nothing is worse
45
+ * than one that does not exist. */
46
+ const stdio = options.stdio ?? (options.onLine || region ? 'pipe' : atLeast('info') ? 'inherit' : 'pipe');
43
47
  if (atLeast('verbose'))
44
48
  console.log(colors.magenta('verbose'), colors.gray('$'), bin, argv.join(' '));
45
49
  const child = spawn(process.platform === 'win32' ? `${bin}.cmd` : bin, argv, {
@@ -74,6 +78,14 @@ export async function runBin(bin, argv, options = {}) {
74
78
  const collect = (d) => {
75
79
  const text = d.toString();
76
80
  output += text;
81
+ /** A caller taking the lines owns them - it is showing them somewhere of its own, and writing
82
+ * them here as well would both double them and scroll whatever it is drawing. */
83
+ if (options.onLine) {
84
+ for (const line of text.split(/\r?\n/))
85
+ if (line)
86
+ options.onLine(line);
87
+ return;
88
+ }
77
89
  if (region && atLeast('info'))
78
90
  region.passThrough(text);
79
91
  };
@@ -89,7 +101,7 @@ export async function runBin(bin, argv, options = {}) {
89
101
  /** Captured output has to be surfaced here or it is lost with the process - the one thing
90
102
  * worse than a noisy failure is a silent one. 'silent' is the caller saying otherwise. */
91
103
  /** Already streamed through the region, so printing it again would double it. */
92
- if (stdio === 'pipe' && output && logLevel !== 'silent' && !(region && atLeast('info'))) {
104
+ if (stdio === 'pipe' && output && logLevel !== 'silent' && !options.onLine && !(region && atLeast('info'))) {
93
105
  process.stderr.write(output);
94
106
  }
95
107
  const err = new Error(`"${bin} ${argv.join(' ')}" exited with code ${code}`);
@@ -1,4 +1,48 @@
1
1
  import type { RunService } from '../services/run.service.js';
2
+ /**
3
+ * The three scheduling flags any command that sweeps packages can take - `--parallel`, `--bail`,
4
+ * `--progress`.
5
+ *
6
+ * **Exported as a group for the reason `packageFilterOptions` is**: a command spreading it gets the
7
+ * flags spelled and behaving exactly as `run`'s, where restating them means `--parallel` quietly
8
+ * accepting a number on one command and not on another. `--parallel`'s `coerce` is the part nobody
9
+ * would copy correctly - it takes a boolean *or* a number, and the type of the parsed value is read
10
+ * off that function.
11
+ *
12
+ * **`--topo` is deliberately not in it.** `run`'s default is ordering on; a command reaching for
13
+ * this is sweeping packages with an independent tool, where a wait nobody asked for costs the whole
14
+ * point. A command that does want it declares it, and `forEachPackage` takes `topo` as an option.
15
+ *
16
+ * The `describe` texts here name no `.rmanrc run.<script>` key, because a command using them owns
17
+ * its own (`check.concurrency`) - which is also why `runOptions` keeps its own wording rather than
18
+ * spreading this.
19
+ */
20
+ export declare const parallelOptions: {
21
+ parallel: {
22
+ target: "cli";
23
+ describe: string;
24
+ /** No `type`: the flag takes a boolean *or* a number, and `coerce` is what says so - which is
25
+ * also where `OptionValue` reads this option's type from. */
26
+ coerce: (v: unknown) => boolean | number | undefined;
27
+ };
28
+ bail: {
29
+ target: "cli";
30
+ describe: string;
31
+ type: "boolean";
32
+ };
33
+ progress: {
34
+ target: "cli";
35
+ describe: string;
36
+ type: "boolean";
37
+ };
38
+ };
39
+ /** The scheduling half of `forEachPackage`'s options, read off argv - the mirror of
40
+ * `readRunOptions` for a command that declared `parallelOptions`. */
41
+ export declare function readParallelOptions(args: any): {
42
+ parallel?: boolean | number;
43
+ bail?: boolean;
44
+ progress?: boolean;
45
+ };
2
46
  /**
3
47
  * Everything `run`, `build` and `test` accept - the package filter, the branch guard, `--from-root`, and
4
48
  * the scheduling flags on top.
@@ -1,5 +1,60 @@
1
1
  import { branchGuardOptions } from './branch-guard.js';
2
2
  import { fromRootOption, packageFilterOptions, readPackageFilterOptions } from './package-filter.js';
3
+ /**
4
+ * The three scheduling flags any command that sweeps packages can take - `--parallel`, `--bail`,
5
+ * `--progress`.
6
+ *
7
+ * **Exported as a group for the reason `packageFilterOptions` is**: a command spreading it gets the
8
+ * flags spelled and behaving exactly as `run`'s, where restating them means `--parallel` quietly
9
+ * accepting a number on one command and not on another. `--parallel`'s `coerce` is the part nobody
10
+ * would copy correctly - it takes a boolean *or* a number, and the type of the parsed value is read
11
+ * off that function.
12
+ *
13
+ * **`--topo` is deliberately not in it.** `run`'s default is ordering on; a command reaching for
14
+ * this is sweeping packages with an independent tool, where a wait nobody asked for costs the whole
15
+ * point. A command that does want it declares it, and `forEachPackage` takes `topo` as an option.
16
+ *
17
+ * The `describe` texts here name no `.rmanrc run.<script>` key, because a command using them owns
18
+ * its own (`check.concurrency`) - which is also why `runOptions` keeps its own wording rather than
19
+ * spreading this.
20
+ */
21
+ export const parallelOptions = {
22
+ parallel: {
23
+ target: 'cli',
24
+ describe: 'Max packages at once: omit/true for CPU count, a number for that many, false to run serially',
25
+ /** No `type`: the flag takes a boolean *or* a number, and `coerce` is what says so - which is
26
+ * also where `OptionValue` reads this option's type from. */
27
+ coerce: (v) => {
28
+ if (v === undefined)
29
+ return undefined;
30
+ if (v === 'false' || v === false)
31
+ return false;
32
+ if (v === 'true' || v === true)
33
+ return true;
34
+ const n = Number(v);
35
+ return Number.isFinite(n) ? n : true;
36
+ },
37
+ },
38
+ bail: {
39
+ target: 'cli',
40
+ describe: 'Stop at the first failure. Default true.',
41
+ type: 'boolean',
42
+ },
43
+ progress: {
44
+ target: 'cli',
45
+ describe: 'Show a live progress panel (default: true; auto-disabled when not a TTY)',
46
+ type: 'boolean',
47
+ },
48
+ };
49
+ /** The scheduling half of `forEachPackage`'s options, read off argv - the mirror of
50
+ * `readRunOptions` for a command that declared `parallelOptions`. */
51
+ export function readParallelOptions(args) {
52
+ return {
53
+ parallel: args.parallel,
54
+ bail: args.bail,
55
+ progress: args.progress,
56
+ };
57
+ }
3
58
  /**
4
59
  * Everything `run`, `build` and `test` accept - the package filter, the branch guard, `--from-root`, and
5
60
  * the scheduling flags on top.