@coderook/cli 0.10.1 → 0.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.
package/dist/cli/src/cli.js
CHANGED
|
@@ -22,6 +22,7 @@ const node_process_1 = __importDefault(require("node:process"));
|
|
|
22
22
|
const api_js_1 = require("./api.js");
|
|
23
23
|
const registry_js_1 = require("./registry.js");
|
|
24
24
|
const help_js_1 = require("./help.js");
|
|
25
|
+
const progress_js_1 = require("./progress.js");
|
|
25
26
|
const project_commands_js_1 = require("./project_commands.js");
|
|
26
27
|
const service_commands_js_1 = require("./service_commands.js");
|
|
27
28
|
const worktree_js_1 = require("../../desktop-app/src/main/worktree.js");
|
|
@@ -74,11 +75,19 @@ function progressLine() {
|
|
|
74
75
|
return (text) => {
|
|
75
76
|
if (!node_process_1.default.stdout.isTTY)
|
|
76
77
|
return;
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
/*
|
|
79
|
+
Measured without the colour codes. Padding to the raw length of a string
|
|
80
|
+
carrying escape sequences overshoots by however many invisible
|
|
81
|
+
characters it holds, which drags the cursor past the end of the line.
|
|
82
|
+
*/
|
|
83
|
+
const visible = text.replace(/\[[0-9;]*m/g, "").length;
|
|
84
|
+
const padded = text + " ".repeat(Math.max(0, last - visible));
|
|
85
|
+
last = visible;
|
|
79
86
|
node_process_1.default.stdout.write(`\r${padded}`);
|
|
80
87
|
};
|
|
81
88
|
}
|
|
89
|
+
/** The shared bar, drawn from a percentage and this session's colour setting. */
|
|
90
|
+
const track = (percent) => (0, progress_js_1.bar)(percent / 100, 18, dim);
|
|
82
91
|
const done = (line) => {
|
|
83
92
|
if (node_process_1.default.stdout.isTTY) {
|
|
84
93
|
line("");
|
|
@@ -383,8 +392,9 @@ async function commandSubmit(parsed) {
|
|
|
383
392
|
*/
|
|
384
393
|
...(link ? { known: link.local ?? link.manifest ?? {} } : {}),
|
|
385
394
|
}, (progress) => {
|
|
386
|
-
line(` ${String(progress.percent).padStart(3)}%
|
|
387
|
-
`${progress.files}/${progress.totalFiles}
|
|
395
|
+
line(` ${track(progress.percent)} ${String(progress.percent).padStart(3)}% ` +
|
|
396
|
+
`${progress.stage.padEnd(7)} ${progress.files}/${progress.totalFiles} ` +
|
|
397
|
+
dim(progress.path.slice(-40)));
|
|
388
398
|
});
|
|
389
399
|
}
|
|
390
400
|
catch (error) {
|
|
@@ -660,8 +670,9 @@ held, mode = "replace") {
|
|
|
660
670
|
}
|
|
661
671
|
const line = progressLine();
|
|
662
672
|
const result = await downloader.run(repositoryId, latest.id, destination, (progress) => {
|
|
663
|
-
line(` ${String(progress.percent).padStart(3)}%
|
|
664
|
-
|
|
673
|
+
line(` ${track(progress.percent)} ${String(progress.percent).padStart(3)}% ` +
|
|
674
|
+
`${progress.files}/${progress.totalFiles} ` +
|
|
675
|
+
dim(progress.path.slice(-40)));
|
|
665
676
|
},
|
|
666
677
|
// What this folder received last time, so a file the project has since
|
|
667
678
|
// dropped is removed while everything untracked is left alone.
|
|
@@ -710,7 +721,8 @@ async function commandBundle(parsed) {
|
|
|
710
721
|
const entries = (await (0, worktree_js_1.changedFiles)(folder, rules, null)).map((file) => file.path);
|
|
711
722
|
const line = progressLine();
|
|
712
723
|
const result = await (0, cbx_js_1.packBundle)({ root: folder, target, entries, sourceName: node_path_1.default.basename(folder) }, (progress) => {
|
|
713
|
-
line(` ${String(progress.percent).padStart(3)}%
|
|
724
|
+
line(` ${track(progress.percent)} ${String(progress.percent).padStart(3)}% ` +
|
|
725
|
+
dim(progress.path.slice(-40)));
|
|
714
726
|
});
|
|
715
727
|
done(line);
|
|
716
728
|
const saved = Math.max(0, 100 - Math.ceil((result.archiveBytes / Math.max(result.sourceBytes, 1)) * 100));
|
|
@@ -728,7 +740,8 @@ async function commandUnbundle(parsed) {
|
|
|
728
740
|
const destination = node_path_1.default.resolve(parsed.positional[1] ?? manifest.source_name);
|
|
729
741
|
const line = progressLine();
|
|
730
742
|
const result = await (0, cbx_js_1.unpackBundle)(node_path_1.default.resolve(source), destination, (progress) => {
|
|
731
|
-
line(` ${String(progress.percent).padStart(3)}%
|
|
743
|
+
line(` ${track(progress.percent)} ${String(progress.percent).padStart(3)}% ` +
|
|
744
|
+
dim(progress.path.slice(-40)));
|
|
732
745
|
});
|
|
733
746
|
done(line);
|
|
734
747
|
console.log(`${result.files} files · ${bytes(result.bytes)} into ${destination}`);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Showing a run as it happens.
|
|
4
|
+
*
|
|
5
|
+
* The runner used to print the name of a job, fall silent for however long the
|
|
6
|
+
* work took, and then print the result. For a five-second echo that is merely
|
|
7
|
+
* odd; for a ten-minute build it means watching a blank terminal with no way
|
|
8
|
+
* to tell compiling from hung from waiting on a prompt that will never be
|
|
9
|
+
* answered. Every byte of output was being captured and shipped to the
|
|
10
|
+
* service, and none of it shown to the person sitting in front of it.
|
|
11
|
+
*
|
|
12
|
+
* Two things fix that, and they are different problems.
|
|
13
|
+
*
|
|
14
|
+
* The fetch has a known total — the version says how many files it holds — so
|
|
15
|
+
* that gets a real bar with a real percentage. The command does not: nothing
|
|
16
|
+
* knows how long `npm run build` takes, and a bar that invents a percentage is
|
|
17
|
+
* worse than no bar, because people plan around it. That phase gets a spinner,
|
|
18
|
+
* an elapsed clock, and the command's own output as it arrives, which is the
|
|
19
|
+
* honest answer to "what is it doing".
|
|
20
|
+
*/
|
|
21
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
22
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
23
|
+
};
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.RunProgress = void 0;
|
|
26
|
+
exports.bar = bar;
|
|
27
|
+
const node_process_1 = __importDefault(require("node:process"));
|
|
28
|
+
/** Braille frames: dense, monospaced, and they animate in one column. */
|
|
29
|
+
const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
30
|
+
const dim = (value) => `[2m${value}[0m`;
|
|
31
|
+
const accent = (value) => `[33m${value}[0m`;
|
|
32
|
+
/**
|
|
33
|
+
* Whether to animate at all.
|
|
34
|
+
*
|
|
35
|
+
* A spinner written to a file or a CI log is thousands of lines of escape
|
|
36
|
+
* codes nobody can read. When there is no terminal, phases are announced once
|
|
37
|
+
* as plain lines and nothing rewrites itself.
|
|
38
|
+
*/
|
|
39
|
+
const live = Boolean(node_process_1.default.stdout.isTTY) && !node_process_1.default.env.NO_COLOR;
|
|
40
|
+
function clock(from) {
|
|
41
|
+
const seconds = Math.floor((Date.now() - from) / 1000);
|
|
42
|
+
const minutes = Math.floor(seconds / 60);
|
|
43
|
+
return minutes
|
|
44
|
+
? `${minutes}:${String(seconds % 60).padStart(2, "0")}`
|
|
45
|
+
: `${seconds}s`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A bar drawn from block characters, for a phase that knows its own size.
|
|
49
|
+
*
|
|
50
|
+
* Shared with push, pull and get, which had a percentage and no bar. `fade` is
|
|
51
|
+
* a parameter because the commands honour NO_COLOR through their own helper,
|
|
52
|
+
* and a track drawn with escape codes somebody asked not to receive is worse
|
|
53
|
+
* than an undimmed one.
|
|
54
|
+
*/
|
|
55
|
+
function bar(fraction, width = 18, fade = dim) {
|
|
56
|
+
const safe = Number.isFinite(fraction) ? fraction : 0;
|
|
57
|
+
/*
|
|
58
|
+
Floored, so the bar fills only when the work is actually done. Rounding
|
|
59
|
+
drew a solid bar from 97% onwards, which is the same lie the percentage
|
|
60
|
+
used to tell — just harder to argue with.
|
|
61
|
+
*/
|
|
62
|
+
const filled = Math.max(0, Math.min(width, Math.floor(safe * width)));
|
|
63
|
+
return `${"█".repeat(filled)}${fade("░".repeat(width - filled))}`;
|
|
64
|
+
}
|
|
65
|
+
class RunProgress {
|
|
66
|
+
frame = 0;
|
|
67
|
+
timer;
|
|
68
|
+
phase = "";
|
|
69
|
+
detail = "";
|
|
70
|
+
fraction = null;
|
|
71
|
+
started = Date.now();
|
|
72
|
+
painted = false;
|
|
73
|
+
/**
|
|
74
|
+
* Move to a new phase.
|
|
75
|
+
*
|
|
76
|
+
* `fraction` is only passed where something real is being counted. Left out,
|
|
77
|
+
* the line shows a spinner and an elapsed time instead of a made-up number.
|
|
78
|
+
*/
|
|
79
|
+
step(phase, detail = "", fraction = null) {
|
|
80
|
+
const previous = this.phase;
|
|
81
|
+
this.phase = phase;
|
|
82
|
+
this.detail = detail;
|
|
83
|
+
this.fraction = fraction;
|
|
84
|
+
if (!live) {
|
|
85
|
+
/*
|
|
86
|
+
Without a terminal each phase is announced once, rather than repeating
|
|
87
|
+
the same line every time a file lands.
|
|
88
|
+
*/
|
|
89
|
+
if (phase !== previous)
|
|
90
|
+
console.log(` ${phase}${detail ? `: ${detail}` : ""}`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this.paint();
|
|
94
|
+
if (!this.timer) {
|
|
95
|
+
this.timer = setInterval(() => {
|
|
96
|
+
this.frame = (this.frame + 1) % FRAMES.length;
|
|
97
|
+
this.paint();
|
|
98
|
+
}, 90);
|
|
99
|
+
this.timer.unref?.();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Update the counted part of the current phase without changing it.
|
|
104
|
+
*
|
|
105
|
+
* Records the numbers and leaves the drawing to the spinner's own timer. A
|
|
106
|
+
* version with thirty thousand files calls this thirty thousand times, and
|
|
107
|
+
* repainting on each one is thirty thousand writes to render eleven frames
|
|
108
|
+
* a person can actually read — the download ends up waiting on the
|
|
109
|
+
* terminal.
|
|
110
|
+
*/
|
|
111
|
+
advance(detail, fraction = null) {
|
|
112
|
+
this.detail = detail;
|
|
113
|
+
this.fraction = fraction;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Output the command itself printed.
|
|
117
|
+
*
|
|
118
|
+
* Cleared out of the way first so the text and the status line do not fight
|
|
119
|
+
* over the same row, then the status line is drawn again underneath — which
|
|
120
|
+
* keeps the newest output directly above where the progress is. A whole
|
|
121
|
+
* chunk at a time, because a build that prints a hundred lines in one breath
|
|
122
|
+
* should cost one repaint rather than a hundred.
|
|
123
|
+
*/
|
|
124
|
+
say(chunk) {
|
|
125
|
+
const lines = chunk.split(/\r?\n/).filter((line) => line.trim() !== "");
|
|
126
|
+
if (!lines.length)
|
|
127
|
+
return;
|
|
128
|
+
if (!live) {
|
|
129
|
+
for (const line of lines)
|
|
130
|
+
console.log(` ${line.replace(/\s+$/, "")}`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.clear();
|
|
134
|
+
for (const line of lines)
|
|
135
|
+
console.log(` ${dim(line.replace(/\s+$/, ""))}`);
|
|
136
|
+
this.paint();
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Stop, and take the status line with it.
|
|
140
|
+
*
|
|
141
|
+
* Deliberately silent. The runner already prints the verdict — which is the
|
|
142
|
+
* line somebody scrolls back to — and a progress display that also announced
|
|
143
|
+
* the result would say the same thing twice, differently.
|
|
144
|
+
*/
|
|
145
|
+
done() {
|
|
146
|
+
if (this.timer)
|
|
147
|
+
clearInterval(this.timer);
|
|
148
|
+
this.timer = undefined;
|
|
149
|
+
this.clear();
|
|
150
|
+
}
|
|
151
|
+
clear() {
|
|
152
|
+
if (!live || !this.painted)
|
|
153
|
+
return;
|
|
154
|
+
node_process_1.default.stdout.write("\r[2K");
|
|
155
|
+
this.painted = false;
|
|
156
|
+
}
|
|
157
|
+
paint() {
|
|
158
|
+
if (!live)
|
|
159
|
+
return;
|
|
160
|
+
const spinner = accent(FRAMES[this.frame] ?? "⠋");
|
|
161
|
+
const measured = this.fraction === null
|
|
162
|
+
? ""
|
|
163
|
+
: /*
|
|
164
|
+
Floored to agree with the bar. Rounding printed 100% beside a bar
|
|
165
|
+
with a block still to fill, and two readings of the same number
|
|
166
|
+
disagreeing is worse than either of them being pessimistic.
|
|
167
|
+
*/
|
|
168
|
+
`${bar(this.fraction)} ${String(Math.floor(this.fraction * 100)).padStart(3)}% `;
|
|
169
|
+
const line = `${spinner} ${this.phase} ${measured}${dim(this.detail)} ${dim(clock(this.started))}`;
|
|
170
|
+
/*
|
|
171
|
+
Truncated to the terminal so a long file path does not wrap and leave
|
|
172
|
+
half a status line stranded above the next repaint.
|
|
173
|
+
*/
|
|
174
|
+
const width = node_process_1.default.stdout.columns ?? 80;
|
|
175
|
+
node_process_1.default.stdout.write(`\r[2K${line.slice(0, width + 40)}`);
|
|
176
|
+
this.painted = true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
exports.RunProgress = RunProgress;
|
package/dist/cli/src/runner.js
CHANGED
|
@@ -37,6 +37,7 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
37
37
|
const download_js_1 = require("../../desktop-app/src/main/download.js");
|
|
38
38
|
const identify_js_1 = require("../../desktop-app/src/main/identify.js");
|
|
39
39
|
const config_js_1 = require("./config.js");
|
|
40
|
+
const progress_js_1 = require("./progress.js");
|
|
40
41
|
/** How often to tell the service the machine is still working. */
|
|
41
42
|
const HEARTBEAT_MS = 30_000;
|
|
42
43
|
/** How often log lines are sent up while a command is running. */
|
|
@@ -73,12 +74,19 @@ async function call(route, options = {}) {
|
|
|
73
74
|
class LogShipper {
|
|
74
75
|
repositoryId;
|
|
75
76
|
runId;
|
|
77
|
+
say;
|
|
76
78
|
pending = [];
|
|
77
79
|
sent = 0;
|
|
78
80
|
failed = false;
|
|
79
|
-
constructor(repositoryId, runId
|
|
81
|
+
constructor(repositoryId, runId,
|
|
82
|
+
/*
|
|
83
|
+
Said through the progress display rather than straight to the console:
|
|
84
|
+
a line written while the status line is on screen lands on top of it.
|
|
85
|
+
*/
|
|
86
|
+
say = console.error) {
|
|
80
87
|
this.repositoryId = repositoryId;
|
|
81
88
|
this.runId = runId;
|
|
89
|
+
this.say = say;
|
|
82
90
|
}
|
|
83
91
|
add(chunk, stream) {
|
|
84
92
|
for (const line of chunk.split(/\r?\n/)) {
|
|
@@ -110,7 +118,7 @@ class LogShipper {
|
|
|
110
118
|
that cannot deliver says so once and stops trying.
|
|
111
119
|
*/
|
|
112
120
|
this.failed = true;
|
|
113
|
-
|
|
121
|
+
this.say(` (log upload stopped: ${errorText(error)})`);
|
|
114
122
|
}
|
|
115
123
|
}
|
|
116
124
|
}
|
|
@@ -317,7 +325,8 @@ function patternMatcher(pattern) {
|
|
|
317
325
|
* directory would let one job's leftovers decide the next job's result.
|
|
318
326
|
*/
|
|
319
327
|
async function performRun(repositoryId, run, runner) {
|
|
320
|
-
const
|
|
328
|
+
const progress = new progress_js_1.RunProgress();
|
|
329
|
+
const logs = new LogShipper(repositoryId, run.runId, (line) => progress.say(line));
|
|
321
330
|
const started = Date.now();
|
|
322
331
|
const workspace = await (0, promises_1.mkdtemp)(node_path_1.default.join((0, node_os_1.tmpdir)(), "coderook-run-"));
|
|
323
332
|
let beating;
|
|
@@ -338,8 +347,14 @@ async function performRun(repositoryId, run, runner) {
|
|
|
338
347
|
}, HEARTBEAT_MS);
|
|
339
348
|
if (run.versionId) {
|
|
340
349
|
logs.add(`Fetching v${run.versionSequence ?? "?"}…`, "stdout");
|
|
350
|
+
/*
|
|
351
|
+
The one phase that knows its own size. The version says how many files
|
|
352
|
+
it holds, so this gets a real proportion rather than a spinner
|
|
353
|
+
pretending to measure something.
|
|
354
|
+
*/
|
|
355
|
+
progress.step(`Fetching v${run.versionSequence ?? "?"}`, "", 0);
|
|
341
356
|
const downloader = new download_js_1.Downloader(config_js_1.credentials);
|
|
342
|
-
const got = await downloader.run(repositoryId, run.versionId, workspace, () => {
|
|
357
|
+
const got = await downloader.run(repositoryId, run.versionId, workspace, (state) => progress.advance(`${state.files}/${state.totalFiles} files`, state.totalFiles ? state.files / state.totalFiles : null), null, "replace");
|
|
343
358
|
logs.add(`Fetched ${got.files} files.`, "stdout");
|
|
344
359
|
}
|
|
345
360
|
else {
|
|
@@ -353,6 +368,7 @@ async function performRun(repositoryId, run, runner) {
|
|
|
353
368
|
const directory = run.workingDirectory
|
|
354
369
|
? insideWorkspace(workspace, run.workingDirectory)
|
|
355
370
|
: workspace;
|
|
371
|
+
progress.step("Running", command);
|
|
356
372
|
flushing = setInterval(() => void logs.flush(), FLUSH_MS);
|
|
357
373
|
const code = await new Promise((settle) => {
|
|
358
374
|
/*
|
|
@@ -373,8 +389,18 @@ async function performRun(repositoryId, run, runner) {
|
|
|
373
389
|
: "",
|
|
374
390
|
},
|
|
375
391
|
});
|
|
376
|
-
|
|
377
|
-
|
|
392
|
+
/*
|
|
393
|
+
Shipped and shown, not one or the other. The service needs the log so
|
|
394
|
+
a failed run can be read later; the person watching needs it so they
|
|
395
|
+
can tell a slow build from a stuck one.
|
|
396
|
+
*/
|
|
397
|
+
const tee = (chunk, stream) => {
|
|
398
|
+
const text = chunk.toString("utf8");
|
|
399
|
+
logs.add(text, stream);
|
|
400
|
+
progress.say(text);
|
|
401
|
+
};
|
|
402
|
+
child.stdout?.on("data", (chunk) => tee(chunk, "stdout"));
|
|
403
|
+
child.stderr?.on("data", (chunk) => tee(chunk, "stderr"));
|
|
378
404
|
child.on("error", (error) => {
|
|
379
405
|
logs.add(errorText(error), "stderr");
|
|
380
406
|
settle(1);
|
|
@@ -389,7 +415,11 @@ async function performRun(repositoryId, run, runner) {
|
|
|
389
415
|
that away is exactly when they would have wanted it.
|
|
390
416
|
*/
|
|
391
417
|
if (run.artifactPaths?.length) {
|
|
392
|
-
|
|
418
|
+
progress.step("Keeping what it built");
|
|
419
|
+
const kept = await uploadArtifacts(repositoryId, run.runId, workspace, run.artifactPaths, (line) => {
|
|
420
|
+
logs.add(line, "stdout");
|
|
421
|
+
progress.say(line);
|
|
422
|
+
});
|
|
393
423
|
if (!kept) {
|
|
394
424
|
logs.add("Nothing matched the paths this workflow keeps.", "stdout");
|
|
395
425
|
}
|
|
@@ -406,6 +436,11 @@ async function performRun(repositoryId, run, runner) {
|
|
|
406
436
|
return { status: "failed", summary: errorText(error).slice(0, 200) };
|
|
407
437
|
}
|
|
408
438
|
finally {
|
|
439
|
+
/*
|
|
440
|
+
Before anything else. A spinner still repainting while the last flush
|
|
441
|
+
goes out would overwrite the verdict the caller is about to print.
|
|
442
|
+
*/
|
|
443
|
+
progress.done();
|
|
409
444
|
if (beating)
|
|
410
445
|
clearInterval(beating);
|
|
411
446
|
if (flushing)
|
|
@@ -31,6 +31,7 @@ const node_fs_1 = require("node:fs");
|
|
|
31
31
|
const promises_1 = require("node:fs/promises");
|
|
32
32
|
const node_path_1 = __importDefault(require("node:path"));
|
|
33
33
|
const node_zlib_1 = require("node:zlib");
|
|
34
|
+
const download_js_1 = require("./download.js");
|
|
34
35
|
exports.HEADER_MAGIC = "CBX1";
|
|
35
36
|
exports.CHUNK_MAGIC = "CHNK";
|
|
36
37
|
exports.MANIFEST_MAGIC = "MANF";
|
|
@@ -175,7 +176,7 @@ async function packBundle(request, report) {
|
|
|
175
176
|
bytes: sourceBytes,
|
|
176
177
|
totalBytes,
|
|
177
178
|
path: relative,
|
|
178
|
-
percent:
|
|
179
|
+
percent: (0, download_js_1.underway)(sourceBytes, totalBytes),
|
|
179
180
|
});
|
|
180
181
|
const whole = (0, node_crypto_1.createHash)("sha256");
|
|
181
182
|
const references = [];
|
|
@@ -261,6 +262,19 @@ async function packBundle(request, report) {
|
|
|
261
262
|
await handle.close();
|
|
262
263
|
await (0, promises_1.rm)(request.target, { force: true });
|
|
263
264
|
await (0, promises_1.rename)(partial, request.target);
|
|
265
|
+
/*
|
|
266
|
+
The bundle is written; say so. Every other reporter here ends on a full
|
|
267
|
+
bar, and this one used to stop wherever the last file started — which
|
|
268
|
+
left a bundle that finished looking like one that gave up near the end.
|
|
269
|
+
*/
|
|
270
|
+
report?.({
|
|
271
|
+
files: request.entries.length,
|
|
272
|
+
totalFiles: request.entries.length,
|
|
273
|
+
bytes: sourceBytes,
|
|
274
|
+
totalBytes,
|
|
275
|
+
path: "Done",
|
|
276
|
+
percent: 100,
|
|
277
|
+
});
|
|
264
278
|
return {
|
|
265
279
|
archiveBytes: offset,
|
|
266
280
|
sourceBytes,
|
|
@@ -374,7 +388,7 @@ async function unpackBundle(source, destination, report) {
|
|
|
374
388
|
bytes,
|
|
375
389
|
totalBytes,
|
|
376
390
|
path: record.path,
|
|
377
|
-
percent:
|
|
391
|
+
percent: (0, download_js_1.underway)(bytes, totalBytes),
|
|
378
392
|
});
|
|
379
393
|
const parts = safeParts(record.path);
|
|
380
394
|
if (!parts.length)
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.Downloader = exports.DownloadCancelled = void 0;
|
|
7
|
+
exports.underway = underway;
|
|
7
8
|
/**
|
|
8
9
|
* Bringing a project down: the file list for a version, then its bytes.
|
|
9
10
|
*
|
|
@@ -56,6 +57,20 @@ async function pruneEmpty(root, removed) {
|
|
|
56
57
|
directory = node_path_1.default.dirname(directory);
|
|
57
58
|
}
|
|
58
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* A percentage for work that is still going.
|
|
62
|
+
*
|
|
63
|
+
* Rounding hit 100 while files were still arriving. A project where one asset
|
|
64
|
+
* is most of the bytes — which is most projects with an asset — read "100%,
|
|
65
|
+
* 3 of 11" for the rest of the download, and the number people trust most is
|
|
66
|
+
* the one that had stopped meaning anything. Held at 99 until the caller says
|
|
67
|
+
* it is finished, and floored so it never rounds up into a lie either.
|
|
68
|
+
*/
|
|
69
|
+
function underway(done, total) {
|
|
70
|
+
if (total <= 0)
|
|
71
|
+
return 0;
|
|
72
|
+
return Math.min(99, Math.floor((done / total) * 100));
|
|
73
|
+
}
|
|
59
74
|
class DownloadCancelled extends Error {
|
|
60
75
|
constructor() {
|
|
61
76
|
super("Download cancelled");
|
|
@@ -172,7 +187,7 @@ class Downloader {
|
|
|
172
187
|
bytes,
|
|
173
188
|
totalBytes,
|
|
174
189
|
path: file.path,
|
|
175
|
-
percent:
|
|
190
|
+
percent: underway(bytes, totalBytes),
|
|
176
191
|
});
|
|
177
192
|
// A path that climbs out of the destination is refused outright.
|
|
178
193
|
const parts = file.path.split("/").filter(Boolean);
|