@tapi-dev/sdk 0.1.29 → 0.1.30
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.d.ts +3 -1
- package/dist/cli.js +94 -10
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -125,7 +125,9 @@ export declare function chooseStudioPort(preferred?: number, options?: {
|
|
|
125
125
|
portAvailable?: StudioPortAvailable;
|
|
126
126
|
reapBlockedPorts?: StudioPortReaper;
|
|
127
127
|
}): Promise<number>;
|
|
128
|
-
export declare function downloadAndVerify(url: string, destination: string, expectedSha256: string, label?: string
|
|
128
|
+
export declare function downloadAndVerify(url: string, destination: string, expectedSha256: string, label?: string, options?: {
|
|
129
|
+
stallTimeoutMs?: number;
|
|
130
|
+
}): Promise<void>;
|
|
129
131
|
export declare function studioInstanceRecordPathForOptions(options: StudioCliOptions): string;
|
|
130
132
|
export declare function findPortableStudioServerExe(releaseDir: string, manifest: StudioReleaseManifest): string | undefined;
|
|
131
133
|
export declare function estimateRequiredExtractionBytes(zipBytes: number): number;
|
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import { homedir } from "node:os";
|
|
|
9
9
|
import { basename, dirname, join, resolve } from "node:path";
|
|
10
10
|
import { performance } from "node:perf_hooks";
|
|
11
11
|
import { emitKeypressEvents } from "node:readline";
|
|
12
|
-
import { Readable } from "node:stream";
|
|
12
|
+
import { Readable, Transform } from "node:stream";
|
|
13
13
|
import { pipeline } from "node:stream/promises";
|
|
14
14
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
15
|
import { TapiClient } from "./index.js";
|
|
@@ -23,6 +23,8 @@ const SUPPORTED_STUDIO_PLATFORM = "windows-x86_64";
|
|
|
23
23
|
const DEFAULT_INSTALL_AUTH_TIMEOUT_MS = 120_000;
|
|
24
24
|
const PORTABLE_STUDIO_SERVER_READY_TIMEOUT_MS = 30_000;
|
|
25
25
|
const PORTABLE_STUDIO_SERVER_READY_INTERVAL_MS = 250;
|
|
26
|
+
const DOWNLOAD_STALL_TIMEOUT_MS = 30_000;
|
|
27
|
+
const DOWNLOAD_PROGRESS_LOG_BYTES = 50 * 1024 * 1024;
|
|
26
28
|
const MAX_WIDE_EVENT_PROCESS_ROOTS = 5;
|
|
27
29
|
const MAX_NONCURRENT_MANAGED_DOWNLOADS = 0;
|
|
28
30
|
const MAX_NONCURRENT_MANAGED_RELEASES = 0;
|
|
@@ -2617,11 +2619,11 @@ async function requestStudioInstallToken(apiBaseUrl, channel, firebaseIdToken, f
|
|
|
2617
2619
|
expiresAt: typeof payload?.expiresAt === "string" ? payload.expiresAt : undefined,
|
|
2618
2620
|
};
|
|
2619
2621
|
}
|
|
2620
|
-
export async function downloadAndVerify(url, destination, expectedSha256, label = "Studio installer") {
|
|
2622
|
+
export async function downloadAndVerify(url, destination, expectedSha256, label = "Studio installer", options = {}) {
|
|
2621
2623
|
const partialPath = `${destination}.partial`;
|
|
2622
2624
|
await unlinkIfExists(partialPath);
|
|
2623
2625
|
try {
|
|
2624
|
-
await downloadFile(url, partialPath, label);
|
|
2626
|
+
await downloadFile(url, partialPath, label, options);
|
|
2625
2627
|
const actualSha256 = await sha256File(partialPath);
|
|
2626
2628
|
if (actualSha256 !== expectedSha256.toLowerCase()) {
|
|
2627
2629
|
throw new Error(`${label} checksum mismatch. Expected ${expectedSha256}, got ${actualSha256}.`);
|
|
@@ -2634,15 +2636,97 @@ export async function downloadAndVerify(url, destination, expectedSha256, label
|
|
|
2634
2636
|
throw error;
|
|
2635
2637
|
}
|
|
2636
2638
|
}
|
|
2637
|
-
async function downloadFile(url, destination, label = "download") {
|
|
2638
|
-
const
|
|
2639
|
-
|
|
2640
|
-
|
|
2639
|
+
async function downloadFile(url, destination, label = "download", options = {}) {
|
|
2640
|
+
const stallTimeoutMs = normalizeDownloadTimeout(options.stallTimeoutMs);
|
|
2641
|
+
const controller = new AbortController();
|
|
2642
|
+
let stallTimer;
|
|
2643
|
+
let stallError;
|
|
2644
|
+
let source;
|
|
2645
|
+
let progressStream;
|
|
2646
|
+
let destinationStream;
|
|
2647
|
+
let receivedBytes = 0;
|
|
2648
|
+
let nextProgressLogBytes = DOWNLOAD_PROGRESS_LOG_BYTES;
|
|
2649
|
+
const failIfStalled = (phase) => {
|
|
2650
|
+
if (stallTimer) {
|
|
2651
|
+
clearTimeout(stallTimer);
|
|
2652
|
+
}
|
|
2653
|
+
stallTimer = setTimeout(() => {
|
|
2654
|
+
stallError = formatDownloadStallError(label, stallTimeoutMs, phase, receivedBytes);
|
|
2655
|
+
controller.abort(stallError);
|
|
2656
|
+
source?.destroy(stallError);
|
|
2657
|
+
progressStream?.destroy(stallError);
|
|
2658
|
+
destinationStream?.destroy(stallError);
|
|
2659
|
+
}, stallTimeoutMs);
|
|
2660
|
+
};
|
|
2661
|
+
try {
|
|
2662
|
+
console.log(`Downloading ${label}...`);
|
|
2663
|
+
failIfStalled("connecting");
|
|
2664
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
2665
|
+
if (!response.ok) {
|
|
2666
|
+
throw new Error(`Failed to download ${label}: HTTP ${response.status}`);
|
|
2667
|
+
}
|
|
2668
|
+
if (!response.body) {
|
|
2669
|
+
throw new Error(`${label} response did not include a body.`);
|
|
2670
|
+
}
|
|
2671
|
+
failIfStalled("receiving data");
|
|
2672
|
+
source = Readable.fromWeb(response.body);
|
|
2673
|
+
progressStream = new Transform({
|
|
2674
|
+
transform(chunk, _encoding, callback) {
|
|
2675
|
+
receivedBytes += downloadChunkByteLength(chunk);
|
|
2676
|
+
failIfStalled("receiving data");
|
|
2677
|
+
if (receivedBytes >= nextProgressLogBytes) {
|
|
2678
|
+
console.log(`Downloaded ${label}: ${formatBytes(receivedBytes)}...`);
|
|
2679
|
+
while (receivedBytes >= nextProgressLogBytes) {
|
|
2680
|
+
nextProgressLogBytes += DOWNLOAD_PROGRESS_LOG_BYTES;
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
callback(null, chunk);
|
|
2684
|
+
},
|
|
2685
|
+
});
|
|
2686
|
+
destinationStream = createWriteStream(destination);
|
|
2687
|
+
await pipeline(source, progressStream, destinationStream);
|
|
2688
|
+
console.log(`Downloaded ${label}: ${formatBytes(receivedBytes)}.`);
|
|
2689
|
+
}
|
|
2690
|
+
catch (error) {
|
|
2691
|
+
if (stallError) {
|
|
2692
|
+
throw stallError;
|
|
2693
|
+
}
|
|
2694
|
+
throw error;
|
|
2641
2695
|
}
|
|
2642
|
-
|
|
2643
|
-
|
|
2696
|
+
finally {
|
|
2697
|
+
if (stallTimer) {
|
|
2698
|
+
clearTimeout(stallTimer);
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
function downloadChunkByteLength(chunk) {
|
|
2703
|
+
if (typeof chunk === "string") {
|
|
2704
|
+
return Buffer.byteLength(chunk);
|
|
2705
|
+
}
|
|
2706
|
+
if (Buffer.isBuffer(chunk)) {
|
|
2707
|
+
return chunk.length;
|
|
2708
|
+
}
|
|
2709
|
+
if (chunk instanceof Uint8Array) {
|
|
2710
|
+
return chunk.byteLength;
|
|
2711
|
+
}
|
|
2712
|
+
return 0;
|
|
2713
|
+
}
|
|
2714
|
+
function normalizeDownloadTimeout(timeoutMs) {
|
|
2715
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) {
|
|
2716
|
+
return DOWNLOAD_STALL_TIMEOUT_MS;
|
|
2717
|
+
}
|
|
2718
|
+
return Math.max(1, Math.floor(timeoutMs));
|
|
2719
|
+
}
|
|
2720
|
+
function formatDownloadStallError(label, timeoutMs, phase, receivedBytes) {
|
|
2721
|
+
const progress = receivedBytes > 0 ? ` after ${formatBytes(receivedBytes)}` : "";
|
|
2722
|
+
return new Error(`${label} download stalled${progress}: no data received for ${formatDuration(timeoutMs)} while ${phase}. ` +
|
|
2723
|
+
"Check your network connection, VPN, or install authorization and retry.");
|
|
2724
|
+
}
|
|
2725
|
+
function formatDuration(ms) {
|
|
2726
|
+
if (ms < 1000) {
|
|
2727
|
+
return `${ms}ms`;
|
|
2644
2728
|
}
|
|
2645
|
-
|
|
2729
|
+
return `${Math.ceil(ms / 1000)}s`;
|
|
2646
2730
|
}
|
|
2647
2731
|
async function browserGoogleSignIn(timeoutMs = DEFAULT_INSTALL_AUTH_TIMEOUT_MS) {
|
|
2648
2732
|
ensureWindowsHost();
|