rman 2.6.0 → 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.
- package/builtins/platforms/node/services/ci.service.js +5 -1
- package/builtins/platforms/node/services/clean.service.js +2 -1
- package/commands/changelog.command.js +2 -1
- package/constants.js +1 -1
- package/package.json +1 -1
- package/services/exec.service.js +6 -1
- package/services/run.service.d.ts +5 -1
- package/services/run.service.js +33 -8
- package/utils/progress-panel.d.ts +23 -3
- package/utils/progress-panel.js +63 -7
- package/utils/status-region.d.ts +46 -1
- package/utils/status-region.js +53 -27
|
@@ -72,7 +72,8 @@ 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.
|
|
75
|
+
panel.detail = repository.name;
|
|
76
|
+
panel.start(repository.app.statusRegion);
|
|
76
77
|
const items = [];
|
|
77
78
|
let failed = false;
|
|
78
79
|
try {
|
|
@@ -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,7 +53,8 @@ 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.
|
|
56
|
+
panel.detail = repository.name;
|
|
57
|
+
panel.start(repository.app.statusRegion);
|
|
57
58
|
let failed = false;
|
|
58
59
|
try {
|
|
59
60
|
const results = await Promise.allSettled(targets.map(pkg => {
|
|
@@ -225,7 +225,8 @@ const changelogCommand = registerCommand(app => {
|
|
|
225
225
|
progress: panelReporter(panel),
|
|
226
226
|
};
|
|
227
227
|
const changelog = app.getService('changelog');
|
|
228
|
-
panel.
|
|
228
|
+
panel.detail = repository.name;
|
|
229
|
+
panel.start(app.statusRegion);
|
|
229
230
|
let entries;
|
|
230
231
|
try {
|
|
231
232
|
entries = write ? await changelog.generateToFile(options) : await changelog.getEntries(options);
|
package/constants.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.
|
|
1
|
+
export const version = '2.7.0';
|
package/package.json
CHANGED
package/services/exec.service.js
CHANGED
|
@@ -52,10 +52,15 @@ 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
|
});
|
|
58
|
-
panel.
|
|
62
|
+
panel.detail = repository.name;
|
|
63
|
+
panel.start(repository.app.statusRegion);
|
|
59
64
|
let failed = false;
|
|
60
65
|
try {
|
|
61
66
|
/** bail:false here - each item's own task decides whether to call `rootTask.abort()`
|
|
@@ -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
|
-
|
|
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) -
|
package/services/run.service.js
CHANGED
|
@@ -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
|
-
|
|
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,7 +234,8 @@ export class RunService extends Service {
|
|
|
228
234
|
err.logged = true;
|
|
229
235
|
throw err;
|
|
230
236
|
}
|
|
231
|
-
panel.
|
|
237
|
+
panel.detail = repository.name;
|
|
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
|
|
234
241
|
* `rootTask.abort()` itself (see `runSteps`), since power-tasks' own `bail` can't vary per package. */
|
|
@@ -361,13 +368,31 @@ export class RunService extends Service {
|
|
|
361
368
|
return steps;
|
|
362
369
|
}
|
|
363
370
|
RunService.normalizeScriptValue = normalizeScriptValue;
|
|
364
|
-
|
|
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) =>
|
|
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);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LiveRegion } from './live-region.js';
|
|
2
|
+
import type { StatusRegion, TerminalRegion } from './status-region.js';
|
|
2
3
|
export type ProgressStatus = 'pending' | 'running' | 'success' | 'failed' | 'skipped';
|
|
3
4
|
export interface ProgressItem {
|
|
4
5
|
readonly name: string;
|
|
@@ -41,11 +42,19 @@ export declare function formatDuration(ms: number): string;
|
|
|
41
42
|
* callers are expected to fall back to their own plain logging in that case, the way `run`
|
|
42
43
|
* falls back to its "classic" per-step log.
|
|
43
44
|
*/
|
|
44
|
-
export declare class ProgressPanel {
|
|
45
|
+
export declare class ProgressPanel implements TerminalRegion {
|
|
45
46
|
private readonly title;
|
|
46
47
|
readonly live: LiveRegion;
|
|
47
48
|
private readonly items;
|
|
48
49
|
private renderLoop?;
|
|
50
|
+
/** The region this panel took the terminal from, handed back in `stop`. */
|
|
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;
|
|
49
58
|
private spinnerFrame;
|
|
50
59
|
private startedAt;
|
|
51
60
|
/**
|
|
@@ -59,8 +68,19 @@ export declare class ProgressPanel {
|
|
|
59
68
|
/** Registers an item and returns it - callers mutate the returned object directly (`status`,
|
|
60
69
|
* `currentStep`, `lastLine`, ...) to drive the next render tick. */
|
|
61
70
|
addItem(name: string, stepsTotal?: number): ProgressItem;
|
|
62
|
-
/**
|
|
63
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Starts the redraw loop. No-op (and no timer) when disabled.
|
|
73
|
+
*
|
|
74
|
+
* **Pass `statusRegion` wherever one exists** - `app.statusRegion`, which every command has while
|
|
75
|
+
* it runs. The panel takes the terminal over for as long as it draws and hands it back in `stop`;
|
|
76
|
+
* without that, two regions redraw on top of each other and the bottom lines visibly swap places
|
|
77
|
+
* several times a second. See `StatusRegion.suspend`.
|
|
78
|
+
*/
|
|
79
|
+
start(statusRegion?: StatusRegion): void;
|
|
80
|
+
/** Writes `text` above the panel without corrupting it - erase, write, redraw underneath. The
|
|
81
|
+
* `TerminalRegion` half of the takeover, so a `runBin` child's output goes somewhere sane while
|
|
82
|
+
* a panel is up. */
|
|
83
|
+
passThrough(text: string): void;
|
|
64
84
|
private render;
|
|
65
85
|
/** Stops the redraw loop and erases the panel, leaving the cursor where it started. */
|
|
66
86
|
stop(): void;
|
package/utils/progress-panel.js
CHANGED
|
@@ -19,6 +19,14 @@ export class ProgressPanel {
|
|
|
19
19
|
live;
|
|
20
20
|
items = new Map();
|
|
21
21
|
renderLoop;
|
|
22
|
+
/** The region this panel took the terminal from, handed back in `stop`. */
|
|
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;
|
|
22
30
|
spinnerFrame = 0;
|
|
23
31
|
startedAt = 0;
|
|
24
32
|
/**
|
|
@@ -41,13 +49,36 @@ export class ProgressPanel {
|
|
|
41
49
|
this.items.set(name, item);
|
|
42
50
|
return item;
|
|
43
51
|
}
|
|
44
|
-
/**
|
|
45
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Starts the redraw loop. No-op (and no timer) when disabled.
|
|
54
|
+
*
|
|
55
|
+
* **Pass `statusRegion` wherever one exists** - `app.statusRegion`, which every command has while
|
|
56
|
+
* it runs. The panel takes the terminal over for as long as it draws and hands it back in `stop`;
|
|
57
|
+
* without that, two regions redraw on top of each other and the bottom lines visibly swap places
|
|
58
|
+
* several times a second. See `StatusRegion.suspend`.
|
|
59
|
+
*/
|
|
60
|
+
start(statusRegion) {
|
|
46
61
|
this.startedAt = Date.now();
|
|
47
62
|
if (!this.live.enabled)
|
|
48
63
|
return;
|
|
64
|
+
/** Only when this panel is actually drawing: a disabled panel owns nothing, and suspending the
|
|
65
|
+
* status line for it would take away the one thing a non-TTY run still shows. */
|
|
66
|
+
this.statusRegion = statusRegion;
|
|
67
|
+
this.statusRegion?.suspend(this);
|
|
49
68
|
this.renderLoop = setInterval(() => this.render(), 100);
|
|
50
69
|
}
|
|
70
|
+
/** Writes `text` above the panel without corrupting it - erase, write, redraw underneath. The
|
|
71
|
+
* `TerminalRegion` half of the takeover, so a `runBin` child's output goes somewhere sane while
|
|
72
|
+
* a panel is up. */
|
|
73
|
+
passThrough(text) {
|
|
74
|
+
if (!this.live.enabled) {
|
|
75
|
+
process.stdout.write(text);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.live.clear();
|
|
79
|
+
process.stdout.write(text);
|
|
80
|
+
this.render();
|
|
81
|
+
}
|
|
51
82
|
render() {
|
|
52
83
|
this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
53
84
|
const spinner = colors.cyan(SPINNER_FRAMES[this.spinnerFrame]);
|
|
@@ -86,8 +117,21 @@ export class ProgressPanel {
|
|
|
86
117
|
const bar = renderProgressBar(filled, total);
|
|
87
118
|
const totalElapsed = formatDuration(Date.now() - this.startedAt);
|
|
88
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)}` : '';
|
|
89
133
|
const lines = [
|
|
90
|
-
`${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}`,
|
|
91
135
|
];
|
|
92
136
|
const runningList = [...this.items.values()].filter(i => i.status === 'running');
|
|
93
137
|
/** Leave room for the header, a safety margin, and a possible "N more running" line - a
|
|
@@ -103,12 +147,19 @@ export class ProgressPanel {
|
|
|
103
147
|
: item.currentStep || '';
|
|
104
148
|
/** Measured against the *plain* text: every piece below is wrapped in escape sequences, and
|
|
105
149
|
* `String.length` counts those, so budgeting on the rendered string wraps a row that fits. */
|
|
106
|
-
const fixed = ` ${item.name} ${step} ${elapsed}`.length;
|
|
150
|
+
const fixed = ` ${item.name} ${step} ${elapsed} | `.length;
|
|
107
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
|
+
*/
|
|
108
160
|
const group = [
|
|
109
|
-
`${spinner} ${colors.bold(item.name)} ${colors.gray(step)}` +
|
|
110
|
-
(command ? ` ${colors.cyan(command)}` : '')
|
|
111
|
-
` ${colors.yellow(elapsed)}`,
|
|
161
|
+
`${spinner} ${colors.bold(item.name)} ${colors.gray(step)} ${colors.yellow(elapsed)}` +
|
|
162
|
+
(command ? ` ${colors.gray('|')} ${colors.cyan(command)}` : ''),
|
|
112
163
|
];
|
|
113
164
|
if (item.lastLine)
|
|
114
165
|
group.push(` ${colors.dim(item.lastLine)}`);
|
|
@@ -129,6 +180,11 @@ export class ProgressPanel {
|
|
|
129
180
|
clearInterval(this.renderLoop);
|
|
130
181
|
this.renderLoop = undefined;
|
|
131
182
|
this.live.clear();
|
|
183
|
+
/** **Handed back, not left suspended.** A command keeps working after its panel comes down -
|
|
184
|
+
* `ci` prints its summary, `run` its recap - and those writes have to go through a region
|
|
185
|
+
* again, or they scroll the status line's own block. */
|
|
186
|
+
this.statusRegion?.resume();
|
|
187
|
+
this.statusRegion = undefined;
|
|
132
188
|
}
|
|
133
189
|
/**
|
|
134
190
|
* Prints the final per-item recap (✓/X/○) and the "N succeeded, M failed (Xs)" summary line,
|
package/utils/status-region.d.ts
CHANGED
|
@@ -7,19 +7,64 @@
|
|
|
7
7
|
* ✔ lint 3.6s <- once it ends
|
|
8
8
|
* ```
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* What a region has to offer to take the terminal over from `StatusRegion` - and `passThrough` is
|
|
12
|
+
* the whole of it.
|
|
13
|
+
*
|
|
14
|
+
* **Deliberately not `live`.** `StatusRegion.live` answers "does something own this terminal", which
|
|
15
|
+
* is what `runBin` reads and which stays true across a takeover; `ProgressPanel` spells the same
|
|
16
|
+
* question `enabled` and uses `live` for the `LiveRegion` itself. Putting it in the contract would
|
|
17
|
+
* force one of the two to be renamed for no gain - the handover needs somewhere to write, nothing
|
|
18
|
+
* more.
|
|
19
|
+
*/
|
|
20
|
+
export interface TerminalRegion {
|
|
21
|
+
/** Write `text` without corrupting the block: erase, write, redraw underneath. */
|
|
22
|
+
passThrough(text: string): void;
|
|
23
|
+
}
|
|
24
|
+
export declare class StatusRegion implements TerminalRegion {
|
|
11
25
|
private readonly label;
|
|
12
26
|
private readonly detail;
|
|
13
27
|
private readonly region;
|
|
14
28
|
private readonly startedAt;
|
|
15
29
|
private timer?;
|
|
16
30
|
private frame;
|
|
31
|
+
/**
|
|
32
|
+
* Who owns the terminal while this region is suspended - see `suspend`. Pass-through is forwarded
|
|
33
|
+
* there, so `runBin` keeps asking one object (`app.statusRegion`) whatever is actually drawing.
|
|
34
|
+
*/
|
|
35
|
+
private takeover?;
|
|
17
36
|
constructor(label: string, detail?: string, enabled?: boolean);
|
|
18
37
|
/** Whether the spinner is actually drawn. `false` leaves `passThrough` a plain write and `stop`
|
|
19
38
|
* its single line, which is what a non-TTY wants. */
|
|
20
39
|
get live(): boolean;
|
|
21
40
|
/** Starts the spinner. Safe to call when not live - it does nothing. */
|
|
22
41
|
start(): void;
|
|
42
|
+
/**
|
|
43
|
+
* **Hands the terminal to another region**, usually a `ProgressPanel` - the spinner stops, its
|
|
44
|
+
* line is erased, and pass-through is forwarded to `takeover` until `resume`.
|
|
45
|
+
*
|
|
46
|
+
* **Two live regions on one terminal cannot both work, and this is what enforces that.** Each
|
|
47
|
+
* redraws by moving the cursor up by *its own* line count; interleaved, every redraw lands on the
|
|
48
|
+
* other's rows. On screen that reads as the last line and the status line swapping places several
|
|
49
|
+
* times a second - reported on `rman ci`, and `rman build` has it too through `runBin`, whose
|
|
50
|
+
* pass-through went to this region while the panel was drawing. `LiveRegion`'s own doc has said
|
|
51
|
+
* "only one region is ever live" since it grew a `stream` parameter; nothing enforced it.
|
|
52
|
+
*
|
|
53
|
+
* `live` deliberately stays `true` while suspended: it answers "does something own this
|
|
54
|
+
* terminal", which is what `runBin` needs to decide to pipe rather than let a child scroll the
|
|
55
|
+
* screen. The answer to *which* region is the forwarding above.
|
|
56
|
+
*/
|
|
57
|
+
suspend(takeover?: TerminalRegion): void;
|
|
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
|
+
*/
|
|
67
|
+
resume(): void;
|
|
23
68
|
/** Writes `text` without corrupting the block: erase, write, redraw underneath. */
|
|
24
69
|
passThrough(text: string): void;
|
|
25
70
|
/** Erases the spinner and leaves one line saying how it went. */
|
package/utils/status-region.js
CHANGED
|
@@ -1,32 +1,5 @@
|
|
|
1
1
|
import colors from 'ansi-colors';
|
|
2
2
|
import { LiveRegion } from './live-region.js';
|
|
3
|
-
/**
|
|
4
|
-
* One line at the bottom of the terminal while a command runs - a spinner, the command's name, the
|
|
5
|
-
* repository, and the elapsed time counting up - replaced by a result line when it ends.
|
|
6
|
-
*
|
|
7
|
-
* ```
|
|
8
|
-
* ⠹ lint postgrejs 2.4s <- while running, redrawn in place
|
|
9
|
-
* ✔ lint 3.6s <- once it ends
|
|
10
|
-
* ```
|
|
11
|
-
*/
|
|
12
|
-
/* **Why a live line rather than one printed at the start**: a command that prints nothing is
|
|
13
|
-
* indistinguishable from one that never ran, and a static start line answers "did it start" without
|
|
14
|
-
* answering "is it still going". `rman lint` on a clean repository is silent for several seconds
|
|
15
|
-
* because eslint says nothing when it has nothing to say.
|
|
16
|
-
*
|
|
17
|
-
* **On stderr, unlike `ProgressPanel`'s region.** A command's *answer* goes to stdout, and
|
|
18
|
-
* `rman changelog > NOTES.md` has to leave the notes alone in the file - cursor-movement codes in
|
|
19
|
-
* there would be worse than noise. The two streams share a terminal, so only one region is ever
|
|
20
|
-
* live.
|
|
21
|
-
*
|
|
22
|
-
* **Anything printed while it is live has to go through `passThrough`**, which erases the block,
|
|
23
|
-
* writes, and redraws below. A write that bypasses it scrolls the screen, and the next redraw's
|
|
24
|
-
* "move up N rows" then lands on the wrong rows and erases what was just printed - the same cursor
|
|
25
|
-
* arithmetic `LiveRegion` documents as its own hard edge. That is why `runBin` pipes a child while
|
|
26
|
-
* a region is live instead of letting it inherit the terminal.
|
|
27
|
-
*
|
|
28
|
-
* **No-op when stderr is not a TTY** (CI, a pipe), where the escape codes mean nothing. There the
|
|
29
|
-
* caller still gets the result line, which is the part that carries information. */
|
|
30
3
|
export class StatusRegion {
|
|
31
4
|
label;
|
|
32
5
|
detail;
|
|
@@ -34,6 +7,11 @@ export class StatusRegion {
|
|
|
34
7
|
startedAt = Date.now();
|
|
35
8
|
timer;
|
|
36
9
|
frame = 0;
|
|
10
|
+
/**
|
|
11
|
+
* Who owns the terminal while this region is suspended - see `suspend`. Pass-through is forwarded
|
|
12
|
+
* there, so `runBin` keeps asking one object (`app.statusRegion`) whatever is actually drawing.
|
|
13
|
+
*/
|
|
14
|
+
takeover;
|
|
37
15
|
constructor(label, detail = '', enabled) {
|
|
38
16
|
this.label = label;
|
|
39
17
|
this.detail = detail;
|
|
@@ -57,8 +35,55 @@ export class StatusRegion {
|
|
|
57
35
|
}, FRAME_MS);
|
|
58
36
|
this.timer.unref();
|
|
59
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* **Hands the terminal to another region**, usually a `ProgressPanel` - the spinner stops, its
|
|
40
|
+
* line is erased, and pass-through is forwarded to `takeover` until `resume`.
|
|
41
|
+
*
|
|
42
|
+
* **Two live regions on one terminal cannot both work, and this is what enforces that.** Each
|
|
43
|
+
* redraws by moving the cursor up by *its own* line count; interleaved, every redraw lands on the
|
|
44
|
+
* other's rows. On screen that reads as the last line and the status line swapping places several
|
|
45
|
+
* times a second - reported on `rman ci`, and `rman build` has it too through `runBin`, whose
|
|
46
|
+
* pass-through went to this region while the panel was drawing. `LiveRegion`'s own doc has said
|
|
47
|
+
* "only one region is ever live" since it grew a `stream` parameter; nothing enforced it.
|
|
48
|
+
*
|
|
49
|
+
* `live` deliberately stays `true` while suspended: it answers "does something own this
|
|
50
|
+
* terminal", which is what `runBin` needs to decide to pipe rather than let a child scroll the
|
|
51
|
+
* screen. The answer to *which* region is the forwarding above.
|
|
52
|
+
*/
|
|
53
|
+
suspend(takeover) {
|
|
54
|
+
this.takeover = takeover;
|
|
55
|
+
if (this.timer)
|
|
56
|
+
clearInterval(this.timer);
|
|
57
|
+
this.timer = undefined;
|
|
58
|
+
this.region.clear();
|
|
59
|
+
}
|
|
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
|
+
*/
|
|
69
|
+
resume() {
|
|
70
|
+
if (!this.takeover && this.timer)
|
|
71
|
+
return;
|
|
72
|
+
this.takeover = undefined;
|
|
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();
|
|
80
|
+
}
|
|
60
81
|
/** Writes `text` without corrupting the block: erase, write, redraw underneath. */
|
|
61
82
|
passThrough(text) {
|
|
83
|
+
if (this.takeover) {
|
|
84
|
+
this.takeover.passThrough(text);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
62
87
|
if (!this.region.enabled) {
|
|
63
88
|
process.stderr.write(text);
|
|
64
89
|
return;
|
|
@@ -69,6 +94,7 @@ export class StatusRegion {
|
|
|
69
94
|
}
|
|
70
95
|
/** Erases the spinner and leaves one line saying how it went. */
|
|
71
96
|
stop(outcome) {
|
|
97
|
+
this.takeover = undefined;
|
|
72
98
|
if (this.timer)
|
|
73
99
|
clearInterval(this.timer);
|
|
74
100
|
this.timer = undefined;
|