rman 2.6.1 → 2.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.
@@ -72,6 +72,7 @@ export var CiService;
72
72
  const progress = options.progress ?? true;
73
73
  const panel = new ProgressPanel('CI', !!process.stdout.isTTY && progress);
74
74
  const runStartedAt = Date.now();
75
+ panel.detail = repository.name;
75
76
  panel.start(repository.app.statusRegion);
76
77
  const items = [];
77
78
  let failed = false;
@@ -122,6 +123,9 @@ const LOCK_FILES = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.loc
122
123
  * log uses. `logLine` is expected to go through a `Logger` (so it respects `--log-level`), not a
123
124
  * raw `console.log`. */
124
125
  async function runStep(app, item, panelEnabled, cwd, command, logLine) {
126
+ /** **Set here rather than at each of the three call sites**, which is also what keeps `wipe` out
127
+ * of it: a wipe runs no command, and this is the one place a `ci` step has one. */
128
+ item.currentCommand = command;
125
129
  if (panelEnabled) {
126
130
  await exec(command, {
127
131
  app,
@@ -53,6 +53,7 @@ export var CleanService;
53
53
  const progress = options.progress ?? true;
54
54
  const logger = new Logger(options.logLevel ?? resolveRootLogLevel(repository));
55
55
  const panel = new ProgressPanel(dryRun ? 'CLEAN (dry-run)' : 'CLEAN', !!process.stdout.isTTY && progress);
56
+ panel.detail = repository.name;
56
57
  panel.start(repository.app.statusRegion);
57
58
  let failed = false;
58
59
  try {
@@ -225,6 +225,7 @@ const changelogCommand = registerCommand(app => {
225
225
  progress: panelReporter(panel),
226
226
  };
227
227
  const changelog = app.getService('changelog');
228
+ panel.detail = repository.name;
228
229
  panel.start(app.statusRegion);
229
230
  let entries;
230
231
  try {
package/constants.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.6.1';
1
+ export const version = '2.7.0';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rman",
3
3
  "description": "Repository manager",
4
- "version": "2.6.1",
4
+ "version": "2.7.0",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {
@@ -52,9 +52,14 @@ export class ExecService extends Service {
52
52
  const names = new Set(packages.map(p => p.name));
53
53
  const children = packages.map(pkg => {
54
54
  const ctx = panel.addItem(pkg.name);
55
+ /** One command for every package and it never changes, so it is set once here rather than
56
+ * threaded into `execForPackage` - the row is the only thing that was not saying what `exec`
57
+ * is actually running. */
58
+ ctx.currentCommand = command;
55
59
  const dependencies = topo ? pkg.dependencies.filter(d => names.has(d.name)).map(d => d.name) : [];
56
60
  return new Task(() => execForPackage(this.app, ctx, panel.enabled, pkg.dirname, command, logger, bail ? () => rootTask?.abort() : () => { }), { name: pkg.name, dependencies });
57
61
  });
62
+ panel.detail = repository.name;
58
63
  panel.start(repository.app.statusRegion);
59
64
  let failed = false;
60
65
  try {
@@ -168,7 +168,11 @@ export declare namespace RunService {
168
168
  * differently, which is how `version.<slot>` came to join its array with `' && '`.
169
169
  */
170
170
  export function normalizeScriptValue(value: unknown, at: string): RunStepValue[];
171
- export function createStepContext(pkg: Package, cwd: string): RunStepContext;
171
+ /**
172
+ * `onCommand` is told what the step is spawning, for as long as it runs - the argv while a child
173
+ * is up, `undefined` when it returns.
174
+ */
175
+ export function createStepContext(pkg: Package, cwd: string, onCommand?: (command: string | undefined) => void): RunStepContext;
172
176
  /**
173
177
  * `.rmanrc` conditional execution, GitHub Actions-`if`-flavored but a small closed grammar
174
178
  * instead of a full expression language (less to get wrong, still covers what's asked for) -
@@ -66,7 +66,13 @@ export class RunService extends Service {
66
66
  * does not distinguish a JS step from a shell one. `step.label` is not used directly
67
67
  * because for a function it already folds the slot in (`before (js)`), which the row
68
68
  * prints two characters to its left. */
69
- ctx.currentCommand = step.command ?? (step.run?.name ? `${step.run.name}()` : '(js function)');
69
+ const stepLabel = step.command ?? (step.run?.name ? `${step.run.name}()` : '(js function)');
70
+ ctx.currentCommand = stepLabel;
71
+ /** A function step names itself until it spawns something, and takes its name back when
72
+ * that returns - so the row says `tsc -b tsconfig.json` for as long as tsc is up. */
73
+ const onCommand = (command) => {
74
+ ctx.currentCommand = command ?? stepLabel;
75
+ };
70
76
  ctx.stepIndex = i;
71
77
  if (panel.enabled) {
72
78
  const onLine = (line) => {
@@ -74,7 +80,7 @@ export class RunService extends Service {
74
80
  ctx.lastLine = line;
75
81
  };
76
82
  if (step.run)
77
- await runFunctionStep(step.run, pkg, cwd, onLine);
83
+ await runFunctionStep(step.run, pkg, cwd, onLine, onCommand);
78
84
  else
79
85
  await exec(step.command, { cwd, stdio: 'pipe', onLine, app: pkg.repository.app });
80
86
  }
@@ -88,7 +94,7 @@ export class RunService extends Service {
88
94
  /** No capture with the panel off: the step owns the terminal, exactly as a shell
89
95
  * step's `stdio: 'inherit'` does. */
90
96
  if (step.run)
91
- await runFunctionStep(step.run, pkg, cwd);
97
+ await runFunctionStep(step.run, pkg, cwd, undefined, onCommand);
92
98
  else
93
99
  await exec(step.command, { cwd, stdio: 'inherit', app: pkg.repository.app });
94
100
  }
@@ -228,6 +234,7 @@ export class RunService extends Service {
228
234
  err.logged = true;
229
235
  throw err;
230
236
  }
237
+ panel.detail = repository.name;
231
238
  panel.start(repository.app.statusRegion);
232
239
  try {
233
240
  /** bail:false here - each package's own resolved bail setting decides whether to call
@@ -361,13 +368,31 @@ export class RunService extends Service {
361
368
  return steps;
362
369
  }
363
370
  RunService.normalizeScriptValue = normalizeScriptValue;
364
- function createStepContext(pkg, cwd) {
371
+ /**
372
+ * `onCommand` is told what the step is spawning, for as long as it runs - the argv while a child
373
+ * is up, `undefined` when it returns.
374
+ */
375
+ /* **Without it a function step can only name itself.** The row showed `buildWithTsc()` for the
376
+ * whole of a build, because that is all the slot knows; what a reader wants is the `tsc -b
377
+ * <tsconfig>` it is sitting in. A function step is the shape every shared config uses, so this is
378
+ * the common case rather than an edge one. */
379
+ function createStepContext(pkg, cwd, onCommand) {
365
380
  const logLevel = resolveRootLogLevel(pkg.repository);
366
381
  return {
367
382
  pkg,
368
383
  repository: pkg.repository,
369
384
  cwd,
370
- runBin: (bin, argv, opts) => runBin(bin, argv, { cwd, logLevel, app: pkg.repository.app, ...opts }),
385
+ runBin: async (bin, argv, opts) => {
386
+ onCommand?.([bin, ...argv].join(' '));
387
+ try {
388
+ return await runBin(bin, argv, { cwd, logLevel, app: pkg.repository.app, ...opts });
389
+ }
390
+ finally {
391
+ /** Back to whatever the caller had, or a step that spawns once would wear that command
392
+ * for the rest of its run. */
393
+ onCommand?.(undefined);
394
+ }
395
+ },
371
396
  logger: new Logger(logLevel),
372
397
  };
373
398
  }
@@ -510,8 +535,8 @@ function toStep(slot, value) {
510
535
  * the panel on (the step's own log, which is what the panel shows for a failed item), and to
511
536
  * stderr with it off, ahead of the `failed` line - the order a shell step already produces.
512
537
  */
513
- async function runFunctionStep(run, pkg, cwd, onLine) {
514
- const context = RunService.createStepContext(pkg, cwd);
538
+ async function runFunctionStep(run, pkg, cwd, onLine, onCommand) {
539
+ const context = RunService.createStepContext(pkg, cwd, onCommand);
515
540
  if (!onLine) {
516
541
  try {
517
542
  await run(context);
@@ -49,6 +49,12 @@ export declare class ProgressPanel implements TerminalRegion {
49
49
  private renderLoop?;
50
50
  /** The region this panel took the terminal from, handed back in `stop`. */
51
51
  private statusRegion?;
52
+ /**
53
+ * What the run is about - the repository name, shown beside the title. Assigned rather than
54
+ * constructed with, the way a `ProgressItem`'s fields are: it keeps `stream` from needing an
55
+ * `undefined` placeholder at four of the five call sites.
56
+ */
57
+ detail?: string;
52
58
  private spinnerFrame;
53
59
  private startedAt;
54
60
  /**
@@ -21,6 +21,12 @@ export class ProgressPanel {
21
21
  renderLoop;
22
22
  /** The region this panel took the terminal from, handed back in `stop`. */
23
23
  statusRegion;
24
+ /**
25
+ * What the run is about - the repository name, shown beside the title. Assigned rather than
26
+ * constructed with, the way a `ProgressItem`'s fields are: it keeps `stream` from needing an
27
+ * `undefined` placeholder at four of the five call sites.
28
+ */
29
+ detail;
24
30
  spinnerFrame = 0;
25
31
  startedAt = 0;
26
32
  /**
@@ -111,8 +117,21 @@ export class ProgressPanel {
111
117
  const bar = renderProgressBar(filled, total);
112
118
  const totalElapsed = formatDuration(Date.now() - this.startedAt);
113
119
  const failedText = failedSoFar > 0 ? colors.red.bold(`${failedSoFar} failed`) : colors.gray(`${failedSoFar} failed`);
120
+ /**
121
+ * **The header says what is running, because nothing else does any more.** The status line used
122
+ * to carry `ci opra` beside this panel; it is suspended now so the two stop redrawing over each
123
+ * other, and suppressing it without moving its content here took the command and the repository
124
+ * off the screen entirely. The badge already names the command, so what had to move is the
125
+ * repository.
126
+ *
127
+ * **At the right end, where the line has room.** Between the badge and the bar it would push
128
+ * every column right by the length of a repository name - so the bar, the counts and the clock
129
+ * would sit at a different place in each repository, and the bar would shift the moment a
130
+ * panel's detail were set. The tail is empty space in every terminal this fits in.
131
+ */
132
+ const detail = this.detail ? ` ${colors.gray(this.detail)}` : '';
114
133
  const lines = [
115
- `${header} ${bar} ${colors.bold(`${done}/${total}`)} ${colors.cyan(`${running} running`)} ${failedText} ${colors.yellow(totalElapsed)}`,
134
+ `${header} ${bar} ${colors.bold(`${done}/${total}`)} ${colors.cyan(`${running} running`)} ${failedText} ${colors.yellow(totalElapsed)}${detail}`,
116
135
  ];
117
136
  const runningList = [...this.items.values()].filter(i => i.status === 'running');
118
137
  /** Leave room for the header, a safety margin, and a possible "N more running" line - a
@@ -128,12 +147,19 @@ export class ProgressPanel {
128
147
  : item.currentStep || '';
129
148
  /** Measured against the *plain* text: every piece below is wrapped in escape sequences, and
130
149
  * `String.length` counts those, so budgeting on the rendered string wraps a row that fits. */
131
- const fixed = ` ${item.name} ${step} ${elapsed}`.length;
150
+ const fixed = ` ${item.name} ${step} ${elapsed} | `.length;
132
151
  const command = truncate(item.currentCommand ?? '', (process.stdout.columns || 80) - fixed - 2);
152
+ /**
153
+ * **The command last, after a `|`, and that is not only layout.** It is the one field with no
154
+ * bound on its length - a `tsc -b` line carries a path, a `run` step carries whatever the
155
+ * 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.
159
+ */
133
160
  const group = [
134
- `${spinner} ${colors.bold(item.name)} ${colors.gray(step)}` +
135
- (command ? ` ${colors.cyan(command)}` : '') +
136
- ` ${colors.yellow(elapsed)}`,
161
+ `${spinner} ${colors.bold(item.name)} ${colors.gray(step)} ${colors.yellow(elapsed)}` +
162
+ (command ? ` ${colors.gray('|')} ${colors.cyan(command)}` : ''),
137
163
  ];
138
164
  if (item.lastLine)
139
165
  group.push(` ${colors.dim(item.lastLine)}`);
@@ -55,7 +55,15 @@ export declare class StatusRegion implements TerminalRegion {
55
55
  * screen. The answer to *which* region is the forwarding above.
56
56
  */
57
57
  suspend(takeover?: TerminalRegion): void;
58
- /** Takes the terminal back and starts drawing again - a no-op when it was never suspended. */
58
+ /**
59
+ * Takes the terminal back - a no-op when it was never suspended.
60
+ *
61
+ * **The first frame waits for the timer rather than being drawn now**, which `start` does. A
62
+ * command is usually seconds from finishing when its panel comes down, and drawing immediately
63
+ * put one spinner frame on screen between the panel's recap and the result line that replaces it
64
+ * - a single flash, which is the thing this whole mechanism exists to remove. Anything still
65
+ * working is drawn 80ms later and nobody waits for it.
66
+ */
59
67
  resume(): void;
60
68
  /** Writes `text` without corrupting the block: erase, write, redraw underneath. */
61
69
  passThrough(text: string): void;
@@ -57,12 +57,26 @@ export class StatusRegion {
57
57
  this.timer = undefined;
58
58
  this.region.clear();
59
59
  }
60
- /** Takes the terminal back and starts drawing again - a no-op when it was never suspended. */
60
+ /**
61
+ * Takes the terminal back - a no-op when it was never suspended.
62
+ *
63
+ * **The first frame waits for the timer rather than being drawn now**, which `start` does. A
64
+ * command is usually seconds from finishing when its panel comes down, and drawing immediately
65
+ * put one spinner frame on screen between the panel's recap and the result line that replaces it
66
+ * - a single flash, which is the thing this whole mechanism exists to remove. Anything still
67
+ * working is drawn 80ms later and nobody waits for it.
68
+ */
61
69
  resume() {
62
70
  if (!this.takeover && this.timer)
63
71
  return;
64
72
  this.takeover = undefined;
65
- this.start();
73
+ if (!this.region.enabled || this.timer)
74
+ return;
75
+ this.timer = setInterval(() => {
76
+ this.frame++;
77
+ this.draw();
78
+ }, FRAME_MS);
79
+ this.timer.unref();
66
80
  }
67
81
  /** Writes `text` without corrupting the block: erase, write, redraw underneath. */
68
82
  passThrough(text) {