spi-agent-gui 0.1.2 → 0.1.3
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/CHANGELOG.md +7 -0
- package/README.md +2 -0
- package/dist/spi.cjs +197 -51
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
- macOS Apple Silicon/Intel candidates remain separate from public Linux releases. Developer ID signing and notarization are not configured.
|
|
6
6
|
|
|
7
|
+
## 0.1.3
|
|
8
|
+
|
|
9
|
+
- Show live runtime download percentage, transferred/total MiB and average speed every 10 seconds, followed by checksum/extraction/completion phases. Interactive npm lifecycle progress uses only its own controlling terminal when npm buffers script output; headless callers use stderr.
|
|
10
|
+
- Replace the 180-second large-archive deadline with a bounded 60-minute overall budget and a separate 60-second network-inactivity timeout. `SPI_DOWNLOAD_TIMEOUT_SECONDS` accepts 60–21600 seconds without changing npm/Pi settings.
|
|
11
|
+
- Resume interrupted archive transfers within the current installation with validated HTTP Range/If-Range; safely restart if the server ignores Range. Always verify the complete manifest size/SHA-256 before extraction or switching versions. No permanent download cache or cross-process resume is implied.
|
|
12
|
+
- Preserve previous installations on download/integrity failure. Add slow/stalled/disconnected-transfer, unsafe-response and real npm-buffered private-PTY progress regression tests; extend the public-install test's download allowance without extending GUI test timeouts.
|
|
13
|
+
|
|
7
14
|
## 0.1.2
|
|
8
15
|
|
|
9
16
|
- Linux x64/arm64 release containing the actual new editable buffer UI (not only Pi's native queue-count status).
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Lightweight Pi package and desktop installer. Desktop binaries are downloaded fr
|
|
|
4
4
|
|
|
5
5
|
The npm package name is `spi-agent-gui`. Install with `pi install npm:spi-agent-gui`, then run `spi`. The initial release supports Linux x64 and arm64; Windows/macOS are deferred. The package registers `/spi` and `/spi-install`; it never launches a GUI just because an extension was loaded. Project/temporary/CI installs and `SPI_SKIP_INSTALL=1` skip automatic system-wide side effects. If lifecycle scripts are disabled, use `/spi-install` or explicitly run `npx --package spi-agent-gui spi install`.
|
|
6
6
|
|
|
7
|
+
Runtime downloads show percentage, received/total MiB and average speed every 10 seconds, including when interactive npm buffers its postinstall output. A healthy slow transfer has a one-hour overall budget, with a separate 60-second inactivity limit and up to three validated Range/If-Range attempts. The complete archive size/SHA-256 is always verified; prior runtime selection is preserved on failure. `SPI_DOWNLOAD_TIMEOUT_SECONDS` accepts 60–21600 seconds. Resume applies within one install, not across processes; no permanent cache is implied. Headless log capture can use `npm_config_foreground_scripts=true` for that command.
|
|
8
|
+
|
|
7
9
|
`spi --help`, `spi doctor`, `spi update`, `spi rollback`, `spi uninstall`. Use `--bin-dir` if Pi's PATH directory is not writable or another spi command exists. No shell profile is edited. User preferences and Pi sessions survive uninstall. `pi remove` removes this package, not the independently installed desktop runtime.
|
|
8
10
|
|
|
9
11
|
The installer requires Node.js 22.19+; the installed command can use its private Electron runtime's Node mode. Supported platforms are only those present in the verified Release manifest. Unsigned Windows/macOS binaries may trigger OS warnings; do not disable OS protections. Linux needs a glibc desktop environment.
|
package/dist/spi.cjs
CHANGED
|
@@ -11,7 +11,7 @@ var __commonJS = (cb, mod) => function __require() {
|
|
|
11
11
|
var define_SPI_RELEASE_default;
|
|
12
12
|
var init_define_SPI_RELEASE = __esm({
|
|
13
13
|
"<define:__SPI_RELEASE__>"() {
|
|
14
|
-
define_SPI_RELEASE_default = { version: "0.1.
|
|
14
|
+
define_SPI_RELEASE_default = { version: "0.1.3", repository: "DiamondGo/spi", packageName: "spi-agent-gui" };
|
|
15
15
|
}
|
|
16
16
|
});
|
|
17
17
|
|
|
@@ -4677,12 +4677,201 @@ var require_desktop = __commonJS({
|
|
|
4677
4677
|
}
|
|
4678
4678
|
});
|
|
4679
4679
|
|
|
4680
|
+
// packages/pi-spi/lib/download.cjs
|
|
4681
|
+
var require_download = __commonJS({
|
|
4682
|
+
"packages/pi-spi/lib/download.cjs"(exports2, module2) {
|
|
4683
|
+
"use strict";
|
|
4684
|
+
init_define_SPI_RELEASE();
|
|
4685
|
+
var fs2 = require("node:fs");
|
|
4686
|
+
var fsp = require("node:fs/promises");
|
|
4687
|
+
var crypto2 = require("node:crypto");
|
|
4688
|
+
var { performance } = require("node:perf_hooks");
|
|
4689
|
+
var DEFAULTS = Object.freeze({ totalMs: 36e5, metadataMs: 18e4, idleMs: 6e4, attempts: 3, delayMs: 1e3, progressMs: 1e4 });
|
|
4690
|
+
function error(message, fatal = false) {
|
|
4691
|
+
return Object.assign(new Error(message), { fatal });
|
|
4692
|
+
}
|
|
4693
|
+
function trustedURL2(value) {
|
|
4694
|
+
const u = new URL(value);
|
|
4695
|
+
if (u.username || u.password) throw error("URL credentials are not supported", true);
|
|
4696
|
+
const loopback = ["127.0.0.1", "localhost", "[::1]"].includes(u.hostname);
|
|
4697
|
+
if (process.env.SPI_ALLOW_TEST_HTTP === "1" && loopback && u.protocol === "http:") return u;
|
|
4698
|
+
if (u.protocol !== "https:" || !(u.hostname === "github.com" || u.hostname === "api.github.com" || u.hostname.endsWith(".githubusercontent.com"))) throw error("Only GitHub HTTPS release URLs are supported", true);
|
|
4699
|
+
return u;
|
|
4700
|
+
}
|
|
4701
|
+
function archiveTimeout() {
|
|
4702
|
+
const raw = process.env.SPI_DOWNLOAD_TIMEOUT_SECONDS;
|
|
4703
|
+
if (raw === void 0) return DEFAULTS.totalMs;
|
|
4704
|
+
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(Number(raw)) || Number(raw) < 60 || Number(raw) > 21600) throw error("SPI_DOWNLOAD_TIMEOUT_SECONDS must be an integer from 60 to 21600", true);
|
|
4705
|
+
return Number(raw) * 1e3;
|
|
4706
|
+
}
|
|
4707
|
+
function progressReporter() {
|
|
4708
|
+
let fd = 2, owned = false;
|
|
4709
|
+
if (process.env.npm_lifecycle_event === "postinstall" && !process.stderr.isTTY && process.platform !== "win32") {
|
|
4710
|
+
try {
|
|
4711
|
+
fd = fs2.openSync("/dev/tty", fs2.constants.O_WRONLY | fs2.constants.O_NOCTTY);
|
|
4712
|
+
owned = true;
|
|
4713
|
+
} catch {
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
return {
|
|
4717
|
+
write(message) {
|
|
4718
|
+
try {
|
|
4719
|
+
fs2.writeSync(fd, "[sPi] " + message + "\n");
|
|
4720
|
+
} catch {
|
|
4721
|
+
}
|
|
4722
|
+
},
|
|
4723
|
+
close() {
|
|
4724
|
+
if (owned) {
|
|
4725
|
+
try {
|
|
4726
|
+
fs2.closeSync(fd);
|
|
4727
|
+
} catch {
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4730
|
+
}
|
|
4731
|
+
};
|
|
4732
|
+
}
|
|
4733
|
+
function status2(message) {
|
|
4734
|
+
const reporter = progressReporter();
|
|
4735
|
+
try {
|
|
4736
|
+
reporter.write(message);
|
|
4737
|
+
} finally {
|
|
4738
|
+
reporter.close();
|
|
4739
|
+
}
|
|
4740
|
+
}
|
|
4741
|
+
async function download2(url, file, max, options2 = {}) {
|
|
4742
|
+
if (!Number.isSafeInteger(max) || max <= 0) throw error("Invalid download size limit", true);
|
|
4743
|
+
const resume = !!options2.resume;
|
|
4744
|
+
const policy = { ...DEFAULTS, totalMs: resume ? archiveTimeout() : DEFAULTS.metadataMs, ...options2.policy };
|
|
4745
|
+
for (const key of ["totalMs", "idleMs", "attempts", "progressMs"]) if (!Number.isSafeInteger(policy[key]) || policy[key] <= 0) throw error("Invalid download policy", true);
|
|
4746
|
+
if (options2.sha256 && !/^[a-f0-9]{64}$/i.test(options2.sha256)) throw error("Invalid download checksum", true);
|
|
4747
|
+
const firstURL = trustedURL2(url);
|
|
4748
|
+
const reporter = options2.progress ? progressReporter() : null;
|
|
4749
|
+
const emit = options2.report || ((message) => reporter?.write(message));
|
|
4750
|
+
const controller = new AbortController();
|
|
4751
|
+
const totalTimer = setTimeout(() => controller.abort(error("Download exceeded its total time limit; increase SPI_DOWNLOAD_TIMEOUT_SECONDS (maximum 21600)", true)), policy.totalMs);
|
|
4752
|
+
const started = performance.now();
|
|
4753
|
+
let out, bytes = 0, etag, success = false, attempt = 0, progressTimer;
|
|
4754
|
+
const report = () => {
|
|
4755
|
+
const seconds = Math.max(1e-3, (performance.now() - started) / 1e3);
|
|
4756
|
+
emit("Downloading runtime: " + (bytes / 1048576).toFixed(1) + "/" + (max / 1048576).toFixed(1) + " MiB (" + (success ? 100 : Math.min(99, Math.floor(bytes / max * 100))) + "%), " + (bytes / 1024 / seconds).toFixed(0) + " KiB/s, attempt " + attempt + "/" + policy.attempts);
|
|
4757
|
+
};
|
|
4758
|
+
try {
|
|
4759
|
+
out = await fsp.open(file, "wx+", 384);
|
|
4760
|
+
if (options2.progress || options2.report) {
|
|
4761
|
+
emit("Downloading runtime; active transfers may take up to " + Math.round(policy.totalMs / 6e4) + " minutes. Interrupted transfers resume when supported.");
|
|
4762
|
+
progressTimer = setInterval(report, policy.progressMs);
|
|
4763
|
+
}
|
|
4764
|
+
for (attempt = 1; attempt <= policy.attempts; attempt++) {
|
|
4765
|
+
controller.signal.throwIfAborted();
|
|
4766
|
+
if (options2.progress || options2.report) report();
|
|
4767
|
+
if (attempt > 1 && !resume) {
|
|
4768
|
+
await out.truncate(0);
|
|
4769
|
+
bytes = 0;
|
|
4770
|
+
etag = void 0;
|
|
4771
|
+
}
|
|
4772
|
+
const idle = new AbortController();
|
|
4773
|
+
let idleTimer, response;
|
|
4774
|
+
const touch = () => {
|
|
4775
|
+
clearTimeout(idleTimer);
|
|
4776
|
+
idleTimer = setTimeout(() => idle.abort(error("Download stalled: no network data for " + Math.round(policy.idleMs / 1e3) + " seconds")), policy.idleMs);
|
|
4777
|
+
};
|
|
4778
|
+
const signal = AbortSignal.any([controller.signal, idle.signal]);
|
|
4779
|
+
try {
|
|
4780
|
+
if (!(resume && bytes === max)) {
|
|
4781
|
+
let current = firstURL, redirects = 0;
|
|
4782
|
+
const offset = bytes;
|
|
4783
|
+
const headers = { "User-Agent": "sPi-installer", Accept: "application/octet-stream", "Accept-Encoding": "identity" };
|
|
4784
|
+
if (resume && offset) {
|
|
4785
|
+
headers.Range = "bytes=" + offset + "-";
|
|
4786
|
+
if (etag) headers["If-Range"] = etag;
|
|
4787
|
+
}
|
|
4788
|
+
while (true) {
|
|
4789
|
+
touch();
|
|
4790
|
+
response = await fetch(current, { redirect: "manual", signal, headers });
|
|
4791
|
+
if (![301, 302, 303, 307, 308].includes(response.status)) break;
|
|
4792
|
+
await response.body?.cancel();
|
|
4793
|
+
if (++redirects > 5) throw error("Too many download redirects", true);
|
|
4794
|
+
const location = response.headers.get("location");
|
|
4795
|
+
if (!location) throw error("Missing download redirect location", true);
|
|
4796
|
+
current = trustedURL2(new URL(location, current));
|
|
4797
|
+
}
|
|
4798
|
+
if (response.status !== 200 && response.status !== 206) throw error("Download failed: HTTP " + response.status, ![408, 429].includes(response.status) && response.status < 500);
|
|
4799
|
+
const encoding = response.headers.get("content-encoding");
|
|
4800
|
+
if (encoding && encoding !== "identity") throw error("Encoded download responses cannot be safely resumed", true);
|
|
4801
|
+
if (response.status === 206) {
|
|
4802
|
+
const range = /^bytes (\d+)-(\d+)\/(\d+)$/.exec(response.headers.get("content-range") || "");
|
|
4803
|
+
if (!resume || !offset || !range || !range.slice(1).every((n) => Number.isSafeInteger(Number(n))) || Number(range[1]) !== offset || Number(range[2]) !== max - 1 || Number(range[3]) !== max) throw error("Invalid resumed download range", true);
|
|
4804
|
+
if (etag && response.headers.get("etag") && response.headers.get("etag") !== etag) throw error("Resumed download validator changed", true);
|
|
4805
|
+
} else if (offset) {
|
|
4806
|
+
await out.truncate(0);
|
|
4807
|
+
bytes = 0;
|
|
4808
|
+
etag = void 0;
|
|
4809
|
+
emit("Server did not honor resume; restarting this transfer safely.");
|
|
4810
|
+
}
|
|
4811
|
+
const length = response.headers.get("content-length");
|
|
4812
|
+
if (length !== null && (!/^\d+$/.test(length) || !Number.isSafeInteger(Number(length)) || Number(length) > max - bytes)) throw error("Download size limit exceeded", true);
|
|
4813
|
+
const validator = response.headers.get("etag");
|
|
4814
|
+
if (validator && /^"[^\r\n]{1,500}"$/.test(validator)) etag = validator;
|
|
4815
|
+
touch();
|
|
4816
|
+
for await (const chunk of response.body) {
|
|
4817
|
+
touch();
|
|
4818
|
+
if (chunk.length > max - bytes) throw error("Download size limit exceeded", true);
|
|
4819
|
+
let written = 0;
|
|
4820
|
+
while (written < chunk.length) {
|
|
4821
|
+
const result = await out.write(chunk, written, chunk.length - written, bytes);
|
|
4822
|
+
if (!result.bytesWritten) throw error("Cannot write downloaded data", true);
|
|
4823
|
+
written += result.bytesWritten;
|
|
4824
|
+
bytes += result.bytesWritten;
|
|
4825
|
+
}
|
|
4826
|
+
}
|
|
4827
|
+
if (resume && bytes !== max) throw error("Download ended before the expected size");
|
|
4828
|
+
}
|
|
4829
|
+
clearTimeout(idleTimer);
|
|
4830
|
+
if (options2.sha256) {
|
|
4831
|
+
const digest = crypto2.createHash("sha256");
|
|
4832
|
+
for await (const chunk of fs2.createReadStream(file)) digest.update(chunk);
|
|
4833
|
+
if (digest.digest("hex") !== options2.sha256.toLowerCase()) throw error("Release checksum mismatch", true);
|
|
4834
|
+
}
|
|
4835
|
+
controller.signal.throwIfAborted();
|
|
4836
|
+
success = true;
|
|
4837
|
+
if (options2.progress || options2.report) {
|
|
4838
|
+
report();
|
|
4839
|
+
emit("Download verified; preparing the runtime.");
|
|
4840
|
+
}
|
|
4841
|
+
return;
|
|
4842
|
+
} catch (caught) {
|
|
4843
|
+
const reason = controller.signal.aborted ? controller.signal.reason : idle.signal.aborted ? idle.signal.reason : caught;
|
|
4844
|
+
if (reason.fatal || attempt === policy.attempts) throw reason;
|
|
4845
|
+
emit("Transfer interrupted; retrying" + (resume && bytes ? " from " + (bytes / 1048576).toFixed(1) + " MiB" : "") + " (" + attempt + "/" + policy.attempts + ").");
|
|
4846
|
+
} finally {
|
|
4847
|
+
clearTimeout(idleTimer);
|
|
4848
|
+
if (response?.body && !response.body.locked) {
|
|
4849
|
+
try {
|
|
4850
|
+
await response.body.cancel();
|
|
4851
|
+
} catch {
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
await new Promise((resolve) => setTimeout(resolve, policy.delayMs * attempt));
|
|
4856
|
+
}
|
|
4857
|
+
} finally {
|
|
4858
|
+
clearTimeout(totalTimer);
|
|
4859
|
+
clearInterval(progressTimer);
|
|
4860
|
+
await out?.close();
|
|
4861
|
+
if (out && !success) await fsp.rm(file, { force: true });
|
|
4862
|
+
reporter?.close();
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4865
|
+
module2.exports = { download: download2, trustedURL: trustedURL2, archiveTimeout, progressReporter, status: status2, DEFAULTS };
|
|
4866
|
+
}
|
|
4867
|
+
});
|
|
4868
|
+
|
|
4680
4869
|
// package.json
|
|
4681
4870
|
var require_package = __commonJS({
|
|
4682
4871
|
"package.json"(exports2, module2) {
|
|
4683
4872
|
module2.exports = {
|
|
4684
4873
|
name: "spi",
|
|
4685
|
-
version: "0.1.
|
|
4874
|
+
version: "0.1.3",
|
|
4686
4875
|
description: "sPi - Simple Pi Agent GUI",
|
|
4687
4876
|
private: true,
|
|
4688
4877
|
license: "UNLICENSED",
|
|
@@ -4706,7 +4895,7 @@ var require_package = __commonJS({
|
|
|
4706
4895
|
"dist:manifest": "node scripts/distribution.cjs manifest",
|
|
4707
4896
|
"dist:appimage": "electron-vite build && electron-builder --linux AppImage",
|
|
4708
4897
|
"check:privacy": "node scripts/privacy.cjs",
|
|
4709
|
-
"test:installer": "node scripts/installer.test.cjs",
|
|
4898
|
+
"test:installer": "node scripts/download.test.cjs && node scripts/installer.test.cjs",
|
|
4710
4899
|
"test:distribution": "node scripts/with-display.cjs scripts/distribution.test.cjs",
|
|
4711
4900
|
typecheck: "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json",
|
|
4712
4901
|
"test:launch": "node scripts/launch.test.cjs",
|
|
@@ -4764,7 +4953,6 @@ var require_release_config = __commonJS({
|
|
|
4764
4953
|
// packages/pi-spi/lib/installer.cjs
|
|
4765
4954
|
init_define_SPI_RELEASE();
|
|
4766
4955
|
var fs = require("node:fs");
|
|
4767
|
-
var fsp = require("node:fs/promises");
|
|
4768
4956
|
var path = require("node:path");
|
|
4769
4957
|
var os = require("node:os");
|
|
4770
4958
|
var crypto = require("node:crypto");
|
|
@@ -4773,6 +4961,7 @@ var { pipeline } = require("node:stream/promises");
|
|
|
4773
4961
|
var tar = require_index_min();
|
|
4774
4962
|
var yauzl = require_yauzl();
|
|
4775
4963
|
var { syncDesktopEntry, assertDesktopOwned, removeDesktopEntry } = require_desktop();
|
|
4964
|
+
var { download, trustedURL, status } = require_download();
|
|
4776
4965
|
var BUILD = typeof define_SPI_RELEASE_default === "undefined" ? { version: require_package().version, repository: require_release_config().repository, packageName: require_release_config().npmPackage } : define_SPI_RELEASE_default;
|
|
4777
4966
|
var OWNER = "sPi-installer-v1";
|
|
4778
4967
|
var MAX_ARCHIVE = 700 * 1024 * 1024;
|
|
@@ -4939,45 +5128,6 @@ async function extractZip(file, dest) {
|
|
|
4939
5128
|
}
|
|
4940
5129
|
checkExtractedLinks(entries, dest);
|
|
4941
5130
|
}
|
|
4942
|
-
function trustedURL(value) {
|
|
4943
|
-
const u = new URL(value);
|
|
4944
|
-
if (u.username || u.password) fail("URL credentials are not supported");
|
|
4945
|
-
const loopback = ["127.0.0.1", "localhost", "[::1]"].includes(u.hostname);
|
|
4946
|
-
if (process.env.SPI_ALLOW_TEST_HTTP === "1" && loopback && u.protocol === "http:") return u;
|
|
4947
|
-
if (u.protocol !== "https:" || !(u.hostname === "github.com" || u.hostname === "api.github.com" || u.hostname.endsWith(".githubusercontent.com"))) fail("Only GitHub HTTPS release URLs are supported");
|
|
4948
|
-
return u;
|
|
4949
|
-
}
|
|
4950
|
-
async function download(url, file, max = MAX_ARCHIVE) {
|
|
4951
|
-
let current = trustedURL(url), response;
|
|
4952
|
-
for (let n = 0; n < 6; n++) {
|
|
4953
|
-
response = await fetch(current, { redirect: "manual", signal: AbortSignal.timeout(18e4), headers: { "User-Agent": "sPi-installer", "Accept": "application/octet-stream" } });
|
|
4954
|
-
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
4955
|
-
current = trustedURL(new URL(response.headers.get("location"), current));
|
|
4956
|
-
await response.body?.cancel();
|
|
4957
|
-
continue;
|
|
4958
|
-
}
|
|
4959
|
-
break;
|
|
4960
|
-
}
|
|
4961
|
-
if (!response?.ok) {
|
|
4962
|
-
await response?.body?.cancel();
|
|
4963
|
-
fail("Download failed: HTTP " + response?.status);
|
|
4964
|
-
}
|
|
4965
|
-
if (Number(response.headers.get("content-length")) > max) {
|
|
4966
|
-
await response.body.cancel();
|
|
4967
|
-
fail("Download size limit exceeded");
|
|
4968
|
-
}
|
|
4969
|
-
const out = await fsp.open(file, "wx", 384);
|
|
4970
|
-
let bytes = 0;
|
|
4971
|
-
try {
|
|
4972
|
-
for await (const chunk of response.body) {
|
|
4973
|
-
bytes += chunk.length;
|
|
4974
|
-
if (bytes > max) fail("Download size limit exceeded");
|
|
4975
|
-
await out.writeFile(chunk);
|
|
4976
|
-
}
|
|
4977
|
-
} finally {
|
|
4978
|
-
await out.close();
|
|
4979
|
-
}
|
|
4980
|
-
}
|
|
4981
5131
|
async function fetchJSON(url) {
|
|
4982
5132
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "spi-metadata-"));
|
|
4983
5133
|
try {
|
|
@@ -5104,6 +5254,7 @@ async function install(options2 = {}) {
|
|
|
5104
5254
|
const stage = fs.mkdtempSync(path.join(root, ".staging-"));
|
|
5105
5255
|
try {
|
|
5106
5256
|
const base = options2.archive ? null : baseURL(v);
|
|
5257
|
+
if (base) status("Preparing sPi " + v + " (" + t + "); retrieving the release manifest.");
|
|
5107
5258
|
const manifest = options2.manifest ? readJSON(options2.manifest) : await fetchJSON(base + "/release-manifest.json");
|
|
5108
5259
|
const item = manifest.targets?.[t];
|
|
5109
5260
|
if (manifest.schemaVersion !== 1 || manifest.version !== v || !item) fail("Release does not support this version/platform");
|
|
@@ -5120,16 +5271,10 @@ async function install(options2 = {}) {
|
|
|
5120
5271
|
if (!fs.statSync(options2.archive).isFile() || fs.statSync(options2.archive).size !== item.size) fail("Archive size mismatch");
|
|
5121
5272
|
fs.copyFileSync(options2.archive, archive);
|
|
5122
5273
|
} else {
|
|
5123
|
-
|
|
5124
|
-
await download(base + "/" + item.file, archive, item.size);
|
|
5125
|
-
break;
|
|
5126
|
-
} catch (error) {
|
|
5127
|
-
fs.rmSync(archive, { force: true });
|
|
5128
|
-
if (attempt === 2) throw error;
|
|
5129
|
-
await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
|
|
5130
|
-
}
|
|
5274
|
+
await download(base + "/" + item.file, archive, item.size, { resume: true, sha256: item.sha256, progress: true });
|
|
5131
5275
|
}
|
|
5132
5276
|
if (fs.statSync(archive).size !== item.size || hash(archive) !== item.sha256.toLowerCase()) fail("Release checksum/size mismatch");
|
|
5277
|
+
status("Checksum verified; extracting the runtime. Please wait.");
|
|
5133
5278
|
if (item.file.endsWith(".tar.gz")) await extractTar(archive, stage);
|
|
5134
5279
|
else await extractZip(archive, stage);
|
|
5135
5280
|
for (const f of [item.entry, item.permissionExtension]) if (!fs.statSync(path.join(stage, f)).isFile()) fail("Release is incomplete");
|
|
@@ -5157,6 +5302,7 @@ async function install(options2 = {}) {
|
|
|
5157
5302
|
}
|
|
5158
5303
|
if (previous && previous.directory !== directory) atomicJSON(path.join(root, "previous.json"), previous);
|
|
5159
5304
|
console.log("Installed sPi " + v + " (" + t + ")");
|
|
5305
|
+
if (process.env.npm_lifecycle_event === "postinstall" && !process.stdout.isTTY) status("Installed sPi " + v + " (" + t + "). Close and reopen any old window to use the new runtime.");
|
|
5160
5306
|
return readCurrent(root);
|
|
5161
5307
|
} finally {
|
|
5162
5308
|
fs.rmSync(stage, { recursive: true, force: true });
|