1import type { PinnedRuntimeProgress } from "../cloud/pinnedRuntime.ts";
2
3/** A single status line below the download bar; redirected output remains plain. */
4export function createUpdateProgress(
5 output: Pick<NodeJS.WriteStream, "write" | "isTTY" | "columns"> = process.stderr,
6) {
7 const interactive = output.isTTY && process.env.TERM !== "dumb";
8 const color = interactive && !process.env.NO_COLOR;
9 const style = (code: number, text: string) => (color ? `\x1b[${code}m${text}\x1b[0m` : text);
10 let stage: PinnedRuntimeProgress["stage"] | undefined;
11 let lastDraw = -Infinity;
12 let lineOpen = false;
13 let downloadLine = false;
14 const finish = () => {
15 if (lineOpen) output.write(downloadLine ? "\n" : "\r\x1b[2K");
16 lineOpen = false;
17 };
18 const status = (message: string) => {
19 finish();
20 let line = ` ${message}`;
21 if (interactive) line = line.slice(0, Math.max(0, (output.columns || 80) - 1));
22 output.write(interactive ? `\r\x1b[2K${style(2, line)}` : `${line}\n`);
23 lineOpen = Boolean(interactive);
24 downloadLine = false;
25 };
26 return {
27 finish,
28 status,
29 heading(message: string, detail = "") {
30 finish();
31 output.write(` ${style(2, message)}${detail ? ` ${style(1, detail)}` : ""}\n\n`);
32 },
33 success(message: string) {
34 finish();
35 output.write(` ${style(32, message)}\n\n`);
36 },
37 report(progress: PinnedRuntimeProgress) {
38 if (progress.stage !== stage) {
39 stage = progress.stage;
40 const labels = {
41 download: "Downloading...",
42 verify: "Verifying the download...",
43 extract: "Extracting T3 Code...",
44 validate: "Checking the new executable...",
45 cached: "Using the downloaded release...",
46 };
47 status(labels[stage]);
48 }
49 if (progress.stage !== "download" || !interactive) return;
50 const now = performance.now();
51 if (now - lastDraw < 100 && progress.received !== progress.total) return;
52 lastDraw = now;
53 const mb = (bytes: number) => (bytes / 1024 / 1024).toFixed(1);
54 const { received, total } = progress;
55 const columns = output.columns || 80;
56 let line =
57 columns >= 32
58 ? ` ${style(2, "Downloading")} ${mb(received)} MB`
59 : ` ${mb(received)} MB`.slice(0, Math.max(0, columns - 1));
60 if (total !== undefined && total > 0 && columns >= 9) {
61 const percent = Math.min(100, Math.floor((received / total) * 100));
62 const width = Math.max(1, Math.min(32, columns - 10));
63 const filled = Math.floor((percent * width) / 100);
64 line = ` ${style(94, "■".repeat(filled))}${style(2, "·".repeat(width - filled))} ${String(percent).padStart(3)}%`;
65 if (columns >= 68) line += ` ${style(2, `${mb(received)} / ${mb(total)} MB`)}`;
66 }
67 output.write(`\r\x1b[2K${line}`);
68 lineOpen = true;
69 downloadLine = true;
70 },
71 };
72}