spi-agent-gui 0.1.1 → 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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - macOS Apple Silicon/Intel candidates remain separate from public Linux releases. Developer ID signing and notarization are not configured.
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
+
14
+ ## 0.1.2
15
+
16
+ - Linux x64/arm64 release containing the actual new editable buffer UI (not only Pi's native queue-count status).
17
+ - User-resizable persisted sidebar split, active directory/recent session highlighting and display-only home-path abbreviation.
18
+ - Main-process editable/cancellable outbox with fixed composer overlay, images, follow-up tail appends / newest-steer head insertion, Stop pause/resume and single-use submission/revision protection.
19
+ - Replace sPi permission modes/rule editor with a session-local YOLO checkbox: on automatically replies Allow once to compatible Pi tool approvals; off delegates to Pi without another sPi prompt. Legacy sPi permission files are preserved but no longer applied. Generic dialogs/direct blocks and Stop/close guards remain effective.
20
+ - Secondary processes exit before readiness; owning-window app quit uses the same confirmation/cleanup controller. Runtime sanitization preserves native helper modes and safe relative framework links.
21
+ - Published 0.1.0/0.1.1 assets remain unchanged. This public release is Linux-only; macOS/Windows public distribution remains deferred.
22
+
3
23
  ## 0.1.1
4
24
 
5
25
  - Original female-spy silhouette icon for the application window, favicon and platform packages.
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.1", repository: "DiamondGo/spi", packageName: "spi-agent-gui" };
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.1",
4874
+ version: "0.1.3",
4686
4875
  description: "sPi - Simple Pi Agent GUI",
4687
4876
  private: true,
4688
4877
  license: "UNLICENSED",
@@ -4697,6 +4886,7 @@ var require_package = __commonJS({
4697
4886
  build: "electron-vite build",
4698
4887
  "build:icons": "node scripts/icons.cjs",
4699
4888
  "test:icons": "node scripts/icons.test.cjs",
4889
+ "test:native-build": "node scripts/native-build.test.cjs && node scripts/macos-integrity.test.cjs",
4700
4890
  start: "electron-vite preview",
4701
4891
  dist: "node scripts/build-portable.cjs",
4702
4892
  "dist:linux": "node scripts/build-portable.cjs",
@@ -4705,7 +4895,7 @@ var require_package = __commonJS({
4705
4895
  "dist:manifest": "node scripts/distribution.cjs manifest",
4706
4896
  "dist:appimage": "electron-vite build && electron-builder --linux AppImage",
4707
4897
  "check:privacy": "node scripts/privacy.cjs",
4708
- "test:installer": "node scripts/installer.test.cjs",
4898
+ "test:installer": "node scripts/download.test.cjs && node scripts/installer.test.cjs",
4709
4899
  "test:distribution": "node scripts/with-display.cjs scripts/distribution.test.cjs",
4710
4900
  typecheck: "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json",
4711
4901
  "test:launch": "node scripts/launch.test.cjs",
@@ -4714,8 +4904,12 @@ var require_package = __commonJS({
4714
4904
  "test:terminal-smoke": "electron-vite build && node scripts/with-display.cjs scripts/smoke.cjs --terminal",
4715
4905
  "test:settings": "node scripts/settings.test.cjs",
4716
4906
  "test:settings-smoke": "electron-vite build && node scripts/with-display.cjs scripts/smoke.cjs --settings",
4717
- "test:permissions": "node scripts/permissions.test.cjs",
4718
- "test:permission-smoke": "electron-vite build && node scripts/with-display.cjs scripts/smoke.cjs --permissions",
4907
+ "test:yolo": "node scripts/yolo.test.cjs && node scripts/session-bridge.test.cjs",
4908
+ "test:extension-ui": "node scripts/extension-ui.test.cjs",
4909
+ "test:workspace-ui": "node scripts/workspace-ui.test.cjs",
4910
+ "test:workspace-ui-smoke": "electron-vite build && node scripts/with-display.cjs scripts/smoke.cjs --workspace-ui",
4911
+ "test:extension-ui-smoke": "electron-vite build && node scripts/with-display.cjs scripts/smoke.cjs --extension-ui",
4912
+ "test:yolo-smoke": "electron-vite build && node scripts/with-display.cjs scripts/smoke.cjs --yolo",
4719
4913
  "test:smoke": "electron-vite build && node scripts/with-display.cjs scripts/smoke.cjs",
4720
4914
  "test:advanced": "electron-vite build && node scripts/with-display.cjs scripts/smoke.cjs --advanced"
4721
4915
  },
@@ -4759,7 +4953,6 @@ var require_release_config = __commonJS({
4759
4953
  // packages/pi-spi/lib/installer.cjs
4760
4954
  init_define_SPI_RELEASE();
4761
4955
  var fs = require("node:fs");
4762
- var fsp = require("node:fs/promises");
4763
4956
  var path = require("node:path");
4764
4957
  var os = require("node:os");
4765
4958
  var crypto = require("node:crypto");
@@ -4768,6 +4961,7 @@ var { pipeline } = require("node:stream/promises");
4768
4961
  var tar = require_index_min();
4769
4962
  var yauzl = require_yauzl();
4770
4963
  var { syncDesktopEntry, assertDesktopOwned, removeDesktopEntry } = require_desktop();
4964
+ var { download, trustedURL, status } = require_download();
4771
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;
4772
4966
  var OWNER = "sPi-installer-v1";
4773
4967
  var MAX_ARCHIVE = 700 * 1024 * 1024;
@@ -4782,7 +4976,7 @@ function version(v) {
4782
4976
  }
4783
4977
  function target() {
4784
4978
  const t = process.platform + "-" + process.arch;
4785
- if (!["linux-x64", "linux-arm64", "win32-x64", "darwin-arm64"].includes(t)) fail("Unsupported platform: " + t);
4979
+ if (!["linux-x64", "linux-arm64", "win32-x64", "darwin-arm64", "darwin-x64"].includes(t)) fail("Unsupported platform: " + t);
4786
4980
  if (process.platform === "linux" && !process.report.getReport().header.glibcVersionRuntime) fail("Linux requires glibc; musl is not supported");
4787
4981
  return t;
4788
4982
  }
@@ -4847,6 +5041,10 @@ function validateEntries(entries) {
4847
5041
  }
4848
5042
  }
4849
5043
  }
5044
+ function checkExtractedLinks(entries, dest) {
5045
+ const base = fs.realpathSync(dest) + path.sep;
5046
+ for (const e of entries) if (e.link && !fs.realpathSync(path.join(dest, e.name)).startsWith(base)) fail("Extracted link escapes destination");
5047
+ }
4850
5048
  async function extractTar(file, dest) {
4851
5049
  const entries = [];
4852
5050
  await tar.t({ file, onReadEntry: (e) => entries.push({ name: e.path, size: e.size, type: { File: "file", Directory: "directory", SymbolicLink: "symlink", Link: "hardlink" }[e.type] || "unsupported", link: e.linkpath }) });
@@ -4856,10 +5054,7 @@ async function extractTar(file, dest) {
4856
5054
  const f = path.join(dest, e.name);
4857
5055
  fs.chmodSync(f, fs.statSync(f).mode & 511);
4858
5056
  }
4859
- for (const e of entries) if (e.link) {
4860
- const resolved = fs.realpathSync(path.join(dest, e.name));
4861
- if (!resolved.startsWith(path.resolve(dest) + path.sep)) fail("Extracted link escapes destination");
4862
- }
5057
+ checkExtractedLinks(entries, dest);
4863
5058
  }
4864
5059
  async function zipEntries(file) {
4865
5060
  return new Promise((resolve, reject) => yauzl.open(file, { lazyEntries: true, strictFileNames: true, validateEntrySizes: true }, (error, zip) => {
@@ -4931,46 +5126,7 @@ async function extractZip(file, dest) {
4931
5126
  fs.mkdirSync(path.dirname(out), { recursive: true });
4932
5127
  fs.symlinkSync(e.link, out);
4933
5128
  }
4934
- for (const e of entries) if (e.type === "symlink" && !fs.realpathSync(path.join(dest, e.name)).startsWith(path.resolve(dest) + path.sep)) fail("Extracted link escapes destination");
4935
- }
4936
- function trustedURL(value) {
4937
- const u = new URL(value);
4938
- if (u.username || u.password) fail("URL credentials are not supported");
4939
- const loopback = ["127.0.0.1", "localhost", "[::1]"].includes(u.hostname);
4940
- if (process.env.SPI_ALLOW_TEST_HTTP === "1" && loopback && u.protocol === "http:") return u;
4941
- 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");
4942
- return u;
4943
- }
4944
- async function download(url, file, max = MAX_ARCHIVE) {
4945
- let current = trustedURL(url), response;
4946
- for (let n = 0; n < 6; n++) {
4947
- response = await fetch(current, { redirect: "manual", signal: AbortSignal.timeout(18e4), headers: { "User-Agent": "sPi-installer", "Accept": "application/octet-stream" } });
4948
- if ([301, 302, 303, 307, 308].includes(response.status)) {
4949
- current = trustedURL(new URL(response.headers.get("location"), current));
4950
- await response.body?.cancel();
4951
- continue;
4952
- }
4953
- break;
4954
- }
4955
- if (!response?.ok) {
4956
- await response?.body?.cancel();
4957
- fail("Download failed: HTTP " + response?.status);
4958
- }
4959
- if (Number(response.headers.get("content-length")) > max) {
4960
- await response.body.cancel();
4961
- fail("Download size limit exceeded");
4962
- }
4963
- const out = await fsp.open(file, "wx", 384);
4964
- let bytes = 0;
4965
- try {
4966
- for await (const chunk of response.body) {
4967
- bytes += chunk.length;
4968
- if (bytes > max) fail("Download size limit exceeded");
4969
- await out.writeFile(chunk);
4970
- }
4971
- } finally {
4972
- await out.close();
4973
- }
5129
+ checkExtractedLinks(entries, dest);
4974
5130
  }
4975
5131
  async function fetchJSON(url) {
4976
5132
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "spi-metadata-"));
@@ -5029,7 +5185,7 @@ function readCurrent(root) {
5029
5185
  if (readJSON(path.join(root, ".spi-owned.json")).owner !== OWNER) fail("Not an sPi install");
5030
5186
  const current = readJSON(path.join(root, "current.json"));
5031
5187
  version(current.version);
5032
- if (!["linux-x64", "linux-arm64", "win32-x64", "darwin-arm64"].includes(current.target)) fail("Invalid install target");
5188
+ if (!["linux-x64", "linux-arm64", "win32-x64", "darwin-arm64", "darwin-x64"].includes(current.target)) fail("Invalid install target");
5033
5189
  if (current.directory !== "releases/" + current.version + "/" + current.target) fail("Invalid install record");
5034
5190
  safeName(current.entry);
5035
5191
  safeName(current.permissionExtension);
@@ -5098,6 +5254,7 @@ async function install(options2 = {}) {
5098
5254
  const stage = fs.mkdtempSync(path.join(root, ".staging-"));
5099
5255
  try {
5100
5256
  const base = options2.archive ? null : baseURL(v);
5257
+ if (base) status("Preparing sPi " + v + " (" + t + "); retrieving the release manifest.");
5101
5258
  const manifest = options2.manifest ? readJSON(options2.manifest) : await fetchJSON(base + "/release-manifest.json");
5102
5259
  const item = manifest.targets?.[t];
5103
5260
  if (manifest.schemaVersion !== 1 || manifest.version !== v || !item) fail("Release does not support this version/platform");
@@ -5114,16 +5271,10 @@ async function install(options2 = {}) {
5114
5271
  if (!fs.statSync(options2.archive).isFile() || fs.statSync(options2.archive).size !== item.size) fail("Archive size mismatch");
5115
5272
  fs.copyFileSync(options2.archive, archive);
5116
5273
  } else {
5117
- for (let attempt = 0; ; attempt++) try {
5118
- await download(base + "/" + item.file, archive, item.size);
5119
- break;
5120
- } catch (error) {
5121
- fs.rmSync(archive, { force: true });
5122
- if (attempt === 2) throw error;
5123
- await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
5124
- }
5274
+ await download(base + "/" + item.file, archive, item.size, { resume: true, sha256: item.sha256, progress: true });
5125
5275
  }
5126
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.");
5127
5278
  if (item.file.endsWith(".tar.gz")) await extractTar(archive, stage);
5128
5279
  else await extractZip(archive, stage);
5129
5280
  for (const f of [item.entry, item.permissionExtension]) if (!fs.statSync(path.join(stage, f)).isFile()) fail("Release is incomplete");
@@ -5151,6 +5302,7 @@ async function install(options2 = {}) {
5151
5302
  }
5152
5303
  if (previous && previous.directory !== directory) atomicJSON(path.join(root, "previous.json"), previous);
5153
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.");
5154
5306
  return readCurrent(root);
5155
5307
  } finally {
5156
5308
  fs.rmSync(stage, { recursive: true, force: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spi-agent-gui",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "sPi - Simple Pi Agent GUI installer and Pi launcher",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",