rman 2.5.0 → 2.6.1
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 +1 -1
- package/builtins/platforms/node/services/clean.service.js +1 -1
- package/commands/changelog.command.d.ts +84 -0
- package/commands/changelog.command.js +154 -11
- package/constants.js +1 -1
- package/package.json +1 -1
- package/services/changelog.service.d.ts +70 -4
- package/services/changelog.service.js +169 -23
- package/services/exec.service.js +1 -1
- package/services/run.service.js +1 -1
- package/services/version.service.js +88 -8
- package/utils/git.d.ts +9 -1
- package/utils/git.js +12 -4
- package/utils/progress-panel.d.ts +24 -4
- package/utils/progress-panel.js +66 -8
- package/utils/status-region.d.ts +38 -1
- package/utils/status-region.js +39 -27
- package/utils/version-group.js +33 -1
package/utils/progress-panel.js
CHANGED
|
@@ -19,11 +19,19 @@ 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;
|
|
22
24
|
spinnerFrame = 0;
|
|
23
25
|
startedAt = 0;
|
|
24
|
-
|
|
26
|
+
/**
|
|
27
|
+
* `stream` is where the panel is drawn - **stdout by default**, which is what `run` and `exec`
|
|
28
|
+
* have always used. A command whose *answer* goes to stdout passes `process.stderr`, or
|
|
29
|
+
* redirecting that answer into a file captures the panel's cursor-movement codes along with it:
|
|
30
|
+
* `rman changelog > NOTES.md` is the case, and it is the same reason the status region moved.
|
|
31
|
+
*/
|
|
32
|
+
constructor(title, enabled, stream) {
|
|
25
33
|
this.title = title;
|
|
26
|
-
this.live = new LiveRegion(enabled);
|
|
34
|
+
this.live = new LiveRegion(enabled, stream);
|
|
27
35
|
}
|
|
28
36
|
get enabled() {
|
|
29
37
|
return this.live.enabled;
|
|
@@ -35,13 +43,36 @@ export class ProgressPanel {
|
|
|
35
43
|
this.items.set(name, item);
|
|
36
44
|
return item;
|
|
37
45
|
}
|
|
38
|
-
/**
|
|
39
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Starts the redraw loop. No-op (and no timer) when disabled.
|
|
48
|
+
*
|
|
49
|
+
* **Pass `statusRegion` wherever one exists** - `app.statusRegion`, which every command has while
|
|
50
|
+
* it runs. The panel takes the terminal over for as long as it draws and hands it back in `stop`;
|
|
51
|
+
* without that, two regions redraw on top of each other and the bottom lines visibly swap places
|
|
52
|
+
* several times a second. See `StatusRegion.suspend`.
|
|
53
|
+
*/
|
|
54
|
+
start(statusRegion) {
|
|
40
55
|
this.startedAt = Date.now();
|
|
41
56
|
if (!this.live.enabled)
|
|
42
57
|
return;
|
|
58
|
+
/** Only when this panel is actually drawing: a disabled panel owns nothing, and suspending the
|
|
59
|
+
* status line for it would take away the one thing a non-TTY run still shows. */
|
|
60
|
+
this.statusRegion = statusRegion;
|
|
61
|
+
this.statusRegion?.suspend(this);
|
|
43
62
|
this.renderLoop = setInterval(() => this.render(), 100);
|
|
44
63
|
}
|
|
64
|
+
/** Writes `text` above the panel without corrupting it - erase, write, redraw underneath. The
|
|
65
|
+
* `TerminalRegion` half of the takeover, so a `runBin` child's output goes somewhere sane while
|
|
66
|
+
* a panel is up. */
|
|
67
|
+
passThrough(text) {
|
|
68
|
+
if (!this.live.enabled) {
|
|
69
|
+
process.stdout.write(text);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this.live.clear();
|
|
73
|
+
process.stdout.write(text);
|
|
74
|
+
this.render();
|
|
75
|
+
}
|
|
45
76
|
render() {
|
|
46
77
|
this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
47
78
|
const spinner = colors.cyan(SPINNER_FRAMES[this.spinnerFrame]);
|
|
@@ -49,16 +80,35 @@ export class ProgressPanel {
|
|
|
49
80
|
let running = 0;
|
|
50
81
|
let done = 0;
|
|
51
82
|
let failedSoFar = 0;
|
|
83
|
+
/**
|
|
84
|
+
* **What the bar is filled from, and it is not `done`.** A finished item counts as a whole one;
|
|
85
|
+
* a *running* item counts as the fraction of its own steps it has got through, so the bar moves
|
|
86
|
+
* while a single item is working rather than only when one ends.
|
|
87
|
+
*
|
|
88
|
+
* Counting whole items made the bar useless exactly where it was needed most: `rman changelog`
|
|
89
|
+
* writes one file under `changelog.groupBy: 'group'`, so the bar sat empty at `0/1` for the
|
|
90
|
+
* entire run while the row beside it counted to 1825. `run` gains the same thing in the small -
|
|
91
|
+
* a nine-step build now advances within the package instead of jumping at the end of it.
|
|
92
|
+
*/
|
|
93
|
+
let filled = 0;
|
|
52
94
|
for (const item of this.items.values()) {
|
|
53
|
-
if (item.status === 'running')
|
|
95
|
+
if (item.status === 'running') {
|
|
54
96
|
running++;
|
|
55
|
-
|
|
97
|
+
/** `stepIndex` is 0-based, so a step in progress is `index + 1` of `total` begun - and
|
|
98
|
+
* clamped, since a caller may report more steps than it first declared. */
|
|
99
|
+
if (item.stepsTotal && item.stepIndex != null) {
|
|
100
|
+
filled += Math.min(1, (item.stepIndex + 1) / item.stepsTotal);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else if (item.status !== 'pending') {
|
|
56
104
|
done++;
|
|
105
|
+
filled += 1;
|
|
106
|
+
}
|
|
57
107
|
if (item.status === 'failed')
|
|
58
108
|
failedSoFar++;
|
|
59
109
|
}
|
|
60
110
|
const header = colors.bgCyan.black.bold(` ${this.title} `);
|
|
61
|
-
const bar = renderProgressBar(
|
|
111
|
+
const bar = renderProgressBar(filled, total);
|
|
62
112
|
const totalElapsed = formatDuration(Date.now() - this.startedAt);
|
|
63
113
|
const failedText = failedSoFar > 0 ? colors.red.bold(`${failedSoFar} failed`) : colors.gray(`${failedSoFar} failed`);
|
|
64
114
|
const lines = [
|
|
@@ -104,6 +154,11 @@ export class ProgressPanel {
|
|
|
104
154
|
clearInterval(this.renderLoop);
|
|
105
155
|
this.renderLoop = undefined;
|
|
106
156
|
this.live.clear();
|
|
157
|
+
/** **Handed back, not left suspended.** A command keeps working after its panel comes down -
|
|
158
|
+
* `ci` prints its summary, `run` its recap - and those writes have to go through a region
|
|
159
|
+
* again, or they scroll the status line's own block. */
|
|
160
|
+
this.statusRegion?.resume();
|
|
161
|
+
this.statusRegion = undefined;
|
|
107
162
|
}
|
|
108
163
|
/**
|
|
109
164
|
* Prints the final per-item recap (✓/X/○) and the "N succeeded, M failed (Xs)" summary line,
|
|
@@ -179,7 +234,10 @@ function truncate(text, width) {
|
|
|
179
234
|
}
|
|
180
235
|
/** Classic braille "dots" spinner (cli-spinners' default), one frame per render tick. */
|
|
181
236
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
237
|
+
/** `done` is fractional - see the `filled` accumulator in `render`. Clamped both ends, or a
|
|
238
|
+
* rounding overshoot asks `repeat` for a negative count and throws inside a redraw. */
|
|
182
239
|
function renderProgressBar(done, total, width = 24) {
|
|
183
|
-
const
|
|
240
|
+
const cells = total ? Math.round((done / total) * width) : 0;
|
|
241
|
+
const filled = Math.max(0, Math.min(width, cells));
|
|
184
242
|
return colors.green('█'.repeat(filled)) + colors.gray('░'.repeat(width - filled));
|
|
185
243
|
}
|
package/utils/status-region.d.ts
CHANGED
|
@@ -7,19 +7,56 @@
|
|
|
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
|
+
/** Takes the terminal back and starts drawing again - a no-op when it was never suspended. */
|
|
59
|
+
resume(): void;
|
|
23
60
|
/** Writes `text` without corrupting the block: erase, write, redraw underneath. */
|
|
24
61
|
passThrough(text: string): void;
|
|
25
62
|
/** 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,41 @@ 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
|
+
/** Takes the terminal back and starts drawing again - a no-op when it was never suspended. */
|
|
61
|
+
resume() {
|
|
62
|
+
if (!this.takeover && this.timer)
|
|
63
|
+
return;
|
|
64
|
+
this.takeover = undefined;
|
|
65
|
+
this.start();
|
|
66
|
+
}
|
|
60
67
|
/** Writes `text` without corrupting the block: erase, write, redraw underneath. */
|
|
61
68
|
passThrough(text) {
|
|
69
|
+
if (this.takeover) {
|
|
70
|
+
this.takeover.passThrough(text);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
62
73
|
if (!this.region.enabled) {
|
|
63
74
|
process.stderr.write(text);
|
|
64
75
|
return;
|
|
@@ -69,6 +80,7 @@ export class StatusRegion {
|
|
|
69
80
|
}
|
|
70
81
|
/** Erases the spinner and leaves one line saying how it went. */
|
|
71
82
|
stop(outcome) {
|
|
83
|
+
this.takeover = undefined;
|
|
72
84
|
if (this.timer)
|
|
73
85
|
clearInterval(this.timer);
|
|
74
86
|
this.timer = undefined;
|
package/utils/version-group.js
CHANGED
|
@@ -19,7 +19,7 @@ export function groupKeyOf(pkg) {
|
|
|
19
19
|
if (g === false)
|
|
20
20
|
return `solo:${pkg.name}`;
|
|
21
21
|
if (typeof g === 'string' && g)
|
|
22
|
-
return `named:${g}`;
|
|
22
|
+
return `named:${assertGroupName(g, pkg)}`;
|
|
23
23
|
return 'default';
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
@@ -41,3 +41,35 @@ export function versionLineCount(pkg) {
|
|
|
41
41
|
return 1;
|
|
42
42
|
return new Set(packages.map(groupKeyOf)).size;
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* What a group may be called - a letter or digit, then letters, digits, `.`, `-` and `_`, up to
|
|
46
|
+
* `GROUP_NAME_MAX`.
|
|
47
|
+
*
|
|
48
|
+
* **A group name is a file name now**, since `changelog.groupBy: 'group'` writes one
|
|
49
|
+
* `CHANGELOG-<name>.md` per group, so it is no longer a key nothing outside rman ever sees. The
|
|
50
|
+
* alternative was escaping it at the one place it is written, and that is the worse half of the
|
|
51
|
+
* trade: an escape turns `core/api` into some `core-api.md` the repository never asked for and
|
|
52
|
+
* cannot search for, and it would have to be repeated by every future reader of the name. A
|
|
53
|
+
* refusal is one rule, stated once, before anything is written.
|
|
54
|
+
*
|
|
55
|
+
* Leading `.` and `-` are out for the two reasons a file name has: a dot-file is hidden, and a
|
|
56
|
+
* leading dash reads as a flag to everything that takes a path on a command line.
|
|
57
|
+
*/
|
|
58
|
+
const GROUP_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
59
|
+
/**
|
|
60
|
+
* **15, and the limit is about the reader rather than any filesystem.** A group name is repeated in
|
|
61
|
+
* every heading of the file it names and in the file name itself; a sentence-long one stops being a
|
|
62
|
+
* label. Filesystems allow far more, which is exactly why a limit has to be a decision rather than
|
|
63
|
+
* whatever the platform happens to permit.
|
|
64
|
+
*/
|
|
65
|
+
const GROUP_NAME_MAX = 15;
|
|
66
|
+
function assertGroupName(name, pkg) {
|
|
67
|
+
if (name.length > GROUP_NAME_MAX)
|
|
68
|
+
throw new Error(`Invalid group name "${name}" on package "${pkg.name}": at most ${GROUP_NAME_MAX} characters, ` +
|
|
69
|
+
`and it is ${name.length}. A group name is written into a file name (CHANGELOG-${name}.md).`);
|
|
70
|
+
if (!GROUP_NAME.test(name))
|
|
71
|
+
throw new Error(`Invalid group name "${name}" on package "${pkg.name}": start with a letter or digit, then ` +
|
|
72
|
+
`letters, digits, ".", "-" or "_". A group name is written into a file name ` +
|
|
73
|
+
`(CHANGELOG-<name>.md), so it has to be one.`);
|
|
74
|
+
return name;
|
|
75
|
+
}
|