termarium 0.1.7 → 0.1.8
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/npm/scripts/install.js +147 -6
- package/package.json +1 -1
package/npm/scripts/install.js
CHANGED
|
@@ -11,6 +11,8 @@ const packageRoot = path.resolve(__dirname, "..", "..");
|
|
|
11
11
|
const packageJson = require(path.join(packageRoot, "package.json"));
|
|
12
12
|
const repo = "https://github.com/7b7b7b/termarium";
|
|
13
13
|
const version = packageJson.version;
|
|
14
|
+
const downloadRetries = readPositiveIntegerEnv("TERMARIUM_NPM_DOWNLOAD_RETRIES", 3);
|
|
15
|
+
const downloadStallTimeoutMs = readPositiveIntegerEnv("TERMARIUM_NPM_DOWNLOAD_STALL_TIMEOUT_MS", 60_000);
|
|
14
16
|
|
|
15
17
|
const targets = {
|
|
16
18
|
"darwin-arm64": {
|
|
@@ -63,11 +65,11 @@ async function main() {
|
|
|
63
65
|
|
|
64
66
|
try {
|
|
65
67
|
console.log(`termarium: downloading ${asset}`);
|
|
66
|
-
const archive = await download(archiveUrl);
|
|
68
|
+
const archive = await download(archiveUrl, { label: asset, progress: true });
|
|
67
69
|
fs.writeFileSync(archivePath, archive);
|
|
68
70
|
|
|
69
71
|
if (process.env.TERMARIUM_NPM_SKIP_CHECKSUM !== "1") {
|
|
70
|
-
const checksums = (await download(checksumUrl)).toString("utf8");
|
|
72
|
+
const checksums = (await download(checksumUrl, { label: "SHA256SUMS" })).toString("utf8");
|
|
71
73
|
verifyChecksum(asset, archive, checksums);
|
|
72
74
|
}
|
|
73
75
|
|
|
@@ -129,7 +131,25 @@ function run(command, args) {
|
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
function download(url,
|
|
134
|
+
async function download(url, options = {}) {
|
|
135
|
+
let lastError;
|
|
136
|
+
for (let attempt = 1; attempt <= downloadRetries; attempt += 1) {
|
|
137
|
+
try {
|
|
138
|
+
return await downloadOnce(url, options);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
lastError = error;
|
|
141
|
+
if (attempt >= downloadRetries) {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
console.error(
|
|
145
|
+
`termarium: download failed (${error.message}); retrying ${attempt}/${downloadRetries - 1}`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
throw lastError;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function downloadOnce(url, options = {}, redirects = 0) {
|
|
133
153
|
return new Promise((resolve, reject) => {
|
|
134
154
|
const request = https.get(
|
|
135
155
|
url,
|
|
@@ -150,7 +170,9 @@ function download(url, redirects = 0) {
|
|
|
150
170
|
reject(new Error(`too many redirects while downloading ${url}`));
|
|
151
171
|
return;
|
|
152
172
|
}
|
|
153
|
-
resolve(
|
|
173
|
+
resolve(
|
|
174
|
+
downloadOnce(new URL(response.headers.location, url).toString(), options, redirects + 1)
|
|
175
|
+
);
|
|
154
176
|
return;
|
|
155
177
|
}
|
|
156
178
|
|
|
@@ -161,14 +183,133 @@ function download(url, redirects = 0) {
|
|
|
161
183
|
}
|
|
162
184
|
|
|
163
185
|
const chunks = [];
|
|
164
|
-
response.
|
|
165
|
-
response.on("
|
|
186
|
+
const progress = createDownloadProgress(options, response.headers["content-length"]);
|
|
187
|
+
response.on("data", (chunk) => {
|
|
188
|
+
chunks.push(chunk);
|
|
189
|
+
progress.update(chunk.length);
|
|
190
|
+
});
|
|
191
|
+
response.on("end", () => {
|
|
192
|
+
progress.finish();
|
|
193
|
+
resolve(Buffer.concat(chunks));
|
|
194
|
+
});
|
|
166
195
|
}
|
|
167
196
|
);
|
|
197
|
+
request.setTimeout(downloadStallTimeoutMs, () => {
|
|
198
|
+
request.destroy(new Error(`download stalled for more than ${downloadStallTimeoutMs / 1000}s`));
|
|
199
|
+
});
|
|
168
200
|
request.on("error", reject);
|
|
169
201
|
});
|
|
170
202
|
}
|
|
171
203
|
|
|
204
|
+
function createDownloadProgress(options, contentLength) {
|
|
205
|
+
if (!options.progress) {
|
|
206
|
+
return { update() {}, finish() {} };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const label = options.label ?? "download";
|
|
210
|
+
const total = Number(contentLength);
|
|
211
|
+
const hasTotal = Number.isFinite(total) && total > 0;
|
|
212
|
+
const startedAt = Date.now();
|
|
213
|
+
const useSingleLine = process.stderr.isTTY;
|
|
214
|
+
const intervalMs = useSingleLine ? 100 : 5_000;
|
|
215
|
+
let received = 0;
|
|
216
|
+
let lastRenderAt = 0;
|
|
217
|
+
let finished = false;
|
|
218
|
+
|
|
219
|
+
const render = (force = false) => {
|
|
220
|
+
if (finished) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
if (!force && now - lastRenderAt < intervalMs) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
lastRenderAt = now;
|
|
228
|
+
|
|
229
|
+
const elapsedSeconds = Math.max((now - startedAt) / 1000, 0.001);
|
|
230
|
+
const speed = received / elapsedSeconds;
|
|
231
|
+
const receivedText = formatBytes(received);
|
|
232
|
+
let message;
|
|
233
|
+
|
|
234
|
+
if (hasTotal) {
|
|
235
|
+
const percent = Math.min(received / total, 1);
|
|
236
|
+
const eta = speed > 0 ? (total - received) / speed : 0;
|
|
237
|
+
message = [
|
|
238
|
+
`termarium: ${label}`,
|
|
239
|
+
formatPercent(percent),
|
|
240
|
+
progressBar(percent),
|
|
241
|
+
`${receivedText}/${formatBytes(total)}`,
|
|
242
|
+
`${formatBytes(speed)}/s`,
|
|
243
|
+
`ETA ${formatDuration(eta)}`,
|
|
244
|
+
].join(" ");
|
|
245
|
+
} else {
|
|
246
|
+
message = `termarium: ${label} ${receivedText} ${formatBytes(speed)}/s`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (useSingleLine) {
|
|
250
|
+
process.stderr.write(`\r${message}`);
|
|
251
|
+
} else {
|
|
252
|
+
console.error(message);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
update(chunkLength) {
|
|
258
|
+
received += chunkLength;
|
|
259
|
+
render(false);
|
|
260
|
+
},
|
|
261
|
+
finish() {
|
|
262
|
+
render(true);
|
|
263
|
+
finished = true;
|
|
264
|
+
if (useSingleLine) {
|
|
265
|
+
process.stderr.write("\n");
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function progressBar(percent) {
|
|
272
|
+
const width = 20;
|
|
273
|
+
const filled = Math.max(0, Math.min(width, Math.round(percent * width)));
|
|
274
|
+
return `[${"#".repeat(filled)}${"-".repeat(width - filled)}]`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function formatPercent(percent) {
|
|
278
|
+
return `${Math.round(percent * 100).toString().padStart(3, " ")}%`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function formatBytes(bytes) {
|
|
282
|
+
const units = ["B", "KiB", "MiB", "GiB"];
|
|
283
|
+
let value = bytes;
|
|
284
|
+
let unitIndex = 0;
|
|
285
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
286
|
+
value /= 1024;
|
|
287
|
+
unitIndex += 1;
|
|
288
|
+
}
|
|
289
|
+
const precision = value >= 100 || unitIndex === 0 ? 0 : 1;
|
|
290
|
+
return `${value.toFixed(precision)} ${units[unitIndex]}`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function formatDuration(seconds) {
|
|
294
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
295
|
+
return "--";
|
|
296
|
+
}
|
|
297
|
+
if (seconds < 60) {
|
|
298
|
+
return `${Math.ceil(seconds)}s`;
|
|
299
|
+
}
|
|
300
|
+
const minutes = Math.floor(seconds / 60);
|
|
301
|
+
const remainingSeconds = Math.ceil(seconds % 60);
|
|
302
|
+
return `${minutes}m${remainingSeconds.toString().padStart(2, "0")}s`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function readPositiveIntegerEnv(name, fallback) {
|
|
306
|
+
const value = Number(process.env[name]);
|
|
307
|
+
if (Number.isInteger(value) && value > 0) {
|
|
308
|
+
return value;
|
|
309
|
+
}
|
|
310
|
+
return fallback;
|
|
311
|
+
}
|
|
312
|
+
|
|
172
313
|
function escapePowerShell(value) {
|
|
173
314
|
return value.replace(/'/g, "''");
|
|
174
315
|
}
|
package/package.json
CHANGED