@tapi-dev/sdk 0.1.29 → 0.1.31
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 +5 -2
- package/dist/cli.js +141 -25
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -60,6 +60,7 @@ export interface StudioCliOptions {
|
|
|
60
60
|
silent: boolean;
|
|
61
61
|
exePath?: string;
|
|
62
62
|
installToken?: string;
|
|
63
|
+
installTokenSource?: "option" | "env" | "minted";
|
|
63
64
|
workspace?: TapiWorkspace;
|
|
64
65
|
workspaceRoot?: string;
|
|
65
66
|
projectId?: string;
|
|
@@ -113,7 +114,7 @@ export declare function getStudioExecutableCandidates(): string[];
|
|
|
113
114
|
export declare function validateStudioManifest(input: unknown): StudioReleaseManifest;
|
|
114
115
|
export declare function validateServiceReleaseManifest(input: unknown): ServiceReleaseManifest;
|
|
115
116
|
export declare function compareVersions(left: string, right: string): number;
|
|
116
|
-
export declare function memoizeStudioInstallToken(options: StudioCliOptions, installToken?: string): string;
|
|
117
|
+
export declare function memoizeStudioInstallToken(options: StudioCliOptions, installToken?: string, source?: StudioCliOptions["installTokenSource"]): string;
|
|
117
118
|
export declare function closeExistingPortableStudioServersForFreshLaunch(stopProcesses?: () => Promise<number>): Promise<{
|
|
118
119
|
stopped: number;
|
|
119
120
|
recordsRemoved: number;
|
|
@@ -125,7 +126,9 @@ export declare function chooseStudioPort(preferred?: number, options?: {
|
|
|
125
126
|
portAvailable?: StudioPortAvailable;
|
|
126
127
|
reapBlockedPorts?: StudioPortReaper;
|
|
127
128
|
}): Promise<number>;
|
|
128
|
-
export declare function downloadAndVerify(url: string, destination: string, expectedSha256: string, label?: string
|
|
129
|
+
export declare function downloadAndVerify(url: string, destination: string, expectedSha256: string, label?: string, options?: {
|
|
130
|
+
stallTimeoutMs?: number;
|
|
131
|
+
}): Promise<void>;
|
|
129
132
|
export declare function studioInstanceRecordPathForOptions(options: StudioCliOptions): string;
|
|
130
133
|
export declare function findPortableStudioServerExe(releaseDir: string, manifest: StudioReleaseManifest): string | undefined;
|
|
131
134
|
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;
|
|
@@ -342,6 +344,8 @@ export function parseStudioOptions(args) {
|
|
|
342
344
|
: `${downloadsBaseUrl.replace(/\/+$/, "")}/studio/channels/${channel}/latest.json`;
|
|
343
345
|
const projectId = raw.projectId ?? envString("TAPI_PROJECT_ID") ?? workspace?.projectId;
|
|
344
346
|
const projectSlug = raw.projectSlug ?? envString("TAPI_PROJECT_SLUG") ?? workspace?.projectSlug;
|
|
347
|
+
const envInstallToken = envString("TAPI_STUDIO_INSTALL_TOKEN");
|
|
348
|
+
const installToken = raw.installToken ?? envInstallToken;
|
|
345
349
|
return {
|
|
346
350
|
channel,
|
|
347
351
|
apiBaseUrl,
|
|
@@ -352,7 +356,8 @@ export function parseStudioOptions(args) {
|
|
|
352
356
|
downloadOnly: raw.downloadOnly ?? false,
|
|
353
357
|
silent: raw.silent ?? false,
|
|
354
358
|
exePath: raw.exePath ?? envString("TAPI_STUDIO_EXE"),
|
|
355
|
-
installToken
|
|
359
|
+
installToken,
|
|
360
|
+
installTokenSource: raw.installToken ? "option" : installToken ? "env" : undefined,
|
|
356
361
|
workspace,
|
|
357
362
|
workspaceRoot: workspace?.root,
|
|
358
363
|
projectId,
|
|
@@ -1965,12 +1970,7 @@ async function installStudio(options) {
|
|
|
1965
1970
|
throw new Error("Direct Studio manifest overrides are no longer supported for install. "
|
|
1966
1971
|
+ "Use --channel with the protected install flow instead.");
|
|
1967
1972
|
}
|
|
1968
|
-
const
|
|
1969
|
-
await event.phase("manifest.fetch.start", {
|
|
1970
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1971
|
-
channel: options.channel,
|
|
1972
|
-
});
|
|
1973
|
-
const manifest = await withCliSpinner(`Fetching Tapi Studio ${options.channel} manifest`, () => fetchProtectedStudioManifest(options.apiBaseUrl, options.channel, installToken));
|
|
1973
|
+
const manifest = await fetchStudioManifestForRun(options, event, "Checking Studio install authorization");
|
|
1974
1974
|
await event.phase("manifest.fetch.success", {
|
|
1975
1975
|
manifest: manifestEventSummary(manifest),
|
|
1976
1976
|
});
|
|
@@ -2084,7 +2084,7 @@ async function obtainStudioInstallToken(options, event) {
|
|
|
2084
2084
|
const issued = await requestStudioInstallToken(options.apiBaseUrl, options.channel, firebaseCreds.idToken);
|
|
2085
2085
|
return issued.installToken;
|
|
2086
2086
|
}
|
|
2087
|
-
export function memoizeStudioInstallToken(options, installToken) {
|
|
2087
|
+
export function memoizeStudioInstallToken(options, installToken, source = "minted") {
|
|
2088
2088
|
const existing = options.installToken?.trim();
|
|
2089
2089
|
if (existing) {
|
|
2090
2090
|
options.installToken = existing;
|
|
@@ -2093,9 +2093,14 @@ export function memoizeStudioInstallToken(options, installToken) {
|
|
|
2093
2093
|
const normalized = installToken?.trim() ?? "";
|
|
2094
2094
|
if (normalized) {
|
|
2095
2095
|
options.installToken = normalized;
|
|
2096
|
+
options.installTokenSource = source;
|
|
2096
2097
|
}
|
|
2097
2098
|
return normalized;
|
|
2098
2099
|
}
|
|
2100
|
+
function clearStudioInstallTokenForRun(options) {
|
|
2101
|
+
options.installToken = undefined;
|
|
2102
|
+
options.installTokenSource = undefined;
|
|
2103
|
+
}
|
|
2099
2104
|
async function ensureStudioInstallTokenForRun(options, event, message) {
|
|
2100
2105
|
const existing = memoizeStudioInstallToken(options);
|
|
2101
2106
|
if (existing) {
|
|
@@ -2109,7 +2114,7 @@ async function ensureStudioInstallTokenForRun(options, event, message) {
|
|
|
2109
2114
|
apiBaseUrl: options.apiBaseUrl,
|
|
2110
2115
|
channel: options.channel,
|
|
2111
2116
|
});
|
|
2112
|
-
const installToken = memoizeStudioInstallToken(options, await withCliSpinner(message, () => obtainStudioInstallToken(options, event)));
|
|
2117
|
+
const installToken = memoizeStudioInstallToken(options, await withCliSpinner(message, () => obtainStudioInstallToken(options, event)), "minted");
|
|
2113
2118
|
await event.phase("auth.install_token.request.success", {
|
|
2114
2119
|
apiBaseUrl: options.apiBaseUrl,
|
|
2115
2120
|
channel: options.channel,
|
|
@@ -2117,6 +2122,39 @@ async function ensureStudioInstallTokenForRun(options, event, message) {
|
|
|
2117
2122
|
});
|
|
2118
2123
|
return installToken;
|
|
2119
2124
|
}
|
|
2125
|
+
async function fetchStudioManifestForRun(options, event, authMessage) {
|
|
2126
|
+
const installToken = await ensureStudioInstallTokenForRun(options, event, authMessage);
|
|
2127
|
+
await event.phase("manifest.fetch.start", {
|
|
2128
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
2129
|
+
channel: options.channel,
|
|
2130
|
+
});
|
|
2131
|
+
try {
|
|
2132
|
+
return await withCliSpinner(`Fetching Tapi Studio ${options.channel} manifest`, () => fetchProtectedStudioManifest(options.apiBaseUrl, options.channel, installToken));
|
|
2133
|
+
}
|
|
2134
|
+
catch (error) {
|
|
2135
|
+
if (!isExpiredStudioInstallTokenError(error) || options.installTokenSource === "option") {
|
|
2136
|
+
throw error;
|
|
2137
|
+
}
|
|
2138
|
+
await event.phase("auth.install_token.expired", {
|
|
2139
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
2140
|
+
channel: options.channel,
|
|
2141
|
+
tokenSource: options.installTokenSource ?? "unknown",
|
|
2142
|
+
error: errorDetails(error),
|
|
2143
|
+
});
|
|
2144
|
+
clearStudioInstallTokenForRun(options);
|
|
2145
|
+
const refreshedToken = await ensureStudioInstallTokenForRun(options, event, "Refreshing Studio install authorization");
|
|
2146
|
+
await event.phase("manifest.fetch.retry.start", {
|
|
2147
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
2148
|
+
channel: options.channel,
|
|
2149
|
+
});
|
|
2150
|
+
return await withCliSpinner(`Retrying Tapi Studio ${options.channel} manifest`, () => fetchProtectedStudioManifest(options.apiBaseUrl, options.channel, refreshedToken));
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
function isExpiredStudioInstallTokenError(error) {
|
|
2154
|
+
const message = formatError(error);
|
|
2155
|
+
return (message.includes("Failed to fetch protected Studio manifest: HTTP 401")
|
|
2156
|
+
&& message.includes("Studio install token has expired"));
|
|
2157
|
+
}
|
|
2120
2158
|
async function openStudio(options) {
|
|
2121
2159
|
ensureWindowsHost();
|
|
2122
2160
|
if (options.downloadOnly) {
|
|
@@ -2178,12 +2216,7 @@ async function ensurePortableStudioServer(options) {
|
|
|
2178
2216
|
options: installEventOptions(options),
|
|
2179
2217
|
});
|
|
2180
2218
|
try {
|
|
2181
|
-
const
|
|
2182
|
-
await event.phase("manifest.fetch.start", {
|
|
2183
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
2184
|
-
channel: options.channel,
|
|
2185
|
-
});
|
|
2186
|
-
const manifest = await withCliSpinner(`Fetching Tapi Studio ${options.channel} manifest`, () => fetchProtectedStudioManifest(options.apiBaseUrl, options.channel, installToken));
|
|
2219
|
+
const manifest = await fetchStudioManifestForRun(options, event, "Checking Studio install authorization");
|
|
2187
2220
|
ensureCompatibleManifest(manifest);
|
|
2188
2221
|
await event.phase("manifest.fetch.success", {
|
|
2189
2222
|
manifest: manifestEventSummary(manifest),
|
|
@@ -2617,11 +2650,11 @@ async function requestStudioInstallToken(apiBaseUrl, channel, firebaseIdToken, f
|
|
|
2617
2650
|
expiresAt: typeof payload?.expiresAt === "string" ? payload.expiresAt : undefined,
|
|
2618
2651
|
};
|
|
2619
2652
|
}
|
|
2620
|
-
export async function downloadAndVerify(url, destination, expectedSha256, label = "Studio installer") {
|
|
2653
|
+
export async function downloadAndVerify(url, destination, expectedSha256, label = "Studio installer", options = {}) {
|
|
2621
2654
|
const partialPath = `${destination}.partial`;
|
|
2622
2655
|
await unlinkIfExists(partialPath);
|
|
2623
2656
|
try {
|
|
2624
|
-
await downloadFile(url, partialPath, label);
|
|
2657
|
+
await downloadFile(url, partialPath, label, options);
|
|
2625
2658
|
const actualSha256 = await sha256File(partialPath);
|
|
2626
2659
|
if (actualSha256 !== expectedSha256.toLowerCase()) {
|
|
2627
2660
|
throw new Error(`${label} checksum mismatch. Expected ${expectedSha256}, got ${actualSha256}.`);
|
|
@@ -2634,15 +2667,97 @@ export async function downloadAndVerify(url, destination, expectedSha256, label
|
|
|
2634
2667
|
throw error;
|
|
2635
2668
|
}
|
|
2636
2669
|
}
|
|
2637
|
-
async function downloadFile(url, destination, label = "download") {
|
|
2638
|
-
const
|
|
2639
|
-
|
|
2640
|
-
|
|
2670
|
+
async function downloadFile(url, destination, label = "download", options = {}) {
|
|
2671
|
+
const stallTimeoutMs = normalizeDownloadTimeout(options.stallTimeoutMs);
|
|
2672
|
+
const controller = new AbortController();
|
|
2673
|
+
let stallTimer;
|
|
2674
|
+
let stallError;
|
|
2675
|
+
let source;
|
|
2676
|
+
let progressStream;
|
|
2677
|
+
let destinationStream;
|
|
2678
|
+
let receivedBytes = 0;
|
|
2679
|
+
let nextProgressLogBytes = DOWNLOAD_PROGRESS_LOG_BYTES;
|
|
2680
|
+
const failIfStalled = (phase) => {
|
|
2681
|
+
if (stallTimer) {
|
|
2682
|
+
clearTimeout(stallTimer);
|
|
2683
|
+
}
|
|
2684
|
+
stallTimer = setTimeout(() => {
|
|
2685
|
+
stallError = formatDownloadStallError(label, stallTimeoutMs, phase, receivedBytes);
|
|
2686
|
+
controller.abort(stallError);
|
|
2687
|
+
source?.destroy(stallError);
|
|
2688
|
+
progressStream?.destroy(stallError);
|
|
2689
|
+
destinationStream?.destroy(stallError);
|
|
2690
|
+
}, stallTimeoutMs);
|
|
2691
|
+
};
|
|
2692
|
+
try {
|
|
2693
|
+
console.log(`Downloading ${label}...`);
|
|
2694
|
+
failIfStalled("connecting");
|
|
2695
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
2696
|
+
if (!response.ok) {
|
|
2697
|
+
throw new Error(`Failed to download ${label}: HTTP ${response.status}`);
|
|
2698
|
+
}
|
|
2699
|
+
if (!response.body) {
|
|
2700
|
+
throw new Error(`${label} response did not include a body.`);
|
|
2701
|
+
}
|
|
2702
|
+
failIfStalled("receiving data");
|
|
2703
|
+
source = Readable.fromWeb(response.body);
|
|
2704
|
+
progressStream = new Transform({
|
|
2705
|
+
transform(chunk, _encoding, callback) {
|
|
2706
|
+
receivedBytes += downloadChunkByteLength(chunk);
|
|
2707
|
+
failIfStalled("receiving data");
|
|
2708
|
+
if (receivedBytes >= nextProgressLogBytes) {
|
|
2709
|
+
console.log(`Downloaded ${label}: ${formatBytes(receivedBytes)}...`);
|
|
2710
|
+
while (receivedBytes >= nextProgressLogBytes) {
|
|
2711
|
+
nextProgressLogBytes += DOWNLOAD_PROGRESS_LOG_BYTES;
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
callback(null, chunk);
|
|
2715
|
+
},
|
|
2716
|
+
});
|
|
2717
|
+
destinationStream = createWriteStream(destination);
|
|
2718
|
+
await pipeline(source, progressStream, destinationStream);
|
|
2719
|
+
console.log(`Downloaded ${label}: ${formatBytes(receivedBytes)}.`);
|
|
2720
|
+
}
|
|
2721
|
+
catch (error) {
|
|
2722
|
+
if (stallError) {
|
|
2723
|
+
throw stallError;
|
|
2724
|
+
}
|
|
2725
|
+
throw error;
|
|
2641
2726
|
}
|
|
2642
|
-
|
|
2643
|
-
|
|
2727
|
+
finally {
|
|
2728
|
+
if (stallTimer) {
|
|
2729
|
+
clearTimeout(stallTimer);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
function downloadChunkByteLength(chunk) {
|
|
2734
|
+
if (typeof chunk === "string") {
|
|
2735
|
+
return Buffer.byteLength(chunk);
|
|
2736
|
+
}
|
|
2737
|
+
if (Buffer.isBuffer(chunk)) {
|
|
2738
|
+
return chunk.length;
|
|
2739
|
+
}
|
|
2740
|
+
if (chunk instanceof Uint8Array) {
|
|
2741
|
+
return chunk.byteLength;
|
|
2742
|
+
}
|
|
2743
|
+
return 0;
|
|
2744
|
+
}
|
|
2745
|
+
function normalizeDownloadTimeout(timeoutMs) {
|
|
2746
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) {
|
|
2747
|
+
return DOWNLOAD_STALL_TIMEOUT_MS;
|
|
2748
|
+
}
|
|
2749
|
+
return Math.max(1, Math.floor(timeoutMs));
|
|
2750
|
+
}
|
|
2751
|
+
function formatDownloadStallError(label, timeoutMs, phase, receivedBytes) {
|
|
2752
|
+
const progress = receivedBytes > 0 ? ` after ${formatBytes(receivedBytes)}` : "";
|
|
2753
|
+
return new Error(`${label} download stalled${progress}: no data received for ${formatDuration(timeoutMs)} while ${phase}. ` +
|
|
2754
|
+
"Check your network connection, VPN, or install authorization and retry.");
|
|
2755
|
+
}
|
|
2756
|
+
function formatDuration(ms) {
|
|
2757
|
+
if (ms < 1000) {
|
|
2758
|
+
return `${ms}ms`;
|
|
2644
2759
|
}
|
|
2645
|
-
|
|
2760
|
+
return `${Math.ceil(ms / 1000)}s`;
|
|
2646
2761
|
}
|
|
2647
2762
|
async function browserGoogleSignIn(timeoutMs = DEFAULT_INSTALL_AUTH_TIMEOUT_MS) {
|
|
2648
2763
|
ensureWindowsHost();
|
|
@@ -3332,6 +3447,7 @@ function installEventOptions(options) {
|
|
|
3332
3447
|
silent: options.silent,
|
|
3333
3448
|
exePath: options.exePath,
|
|
3334
3449
|
installTokenProvided: Boolean(options.installToken),
|
|
3450
|
+
installTokenSource: options.installTokenSource,
|
|
3335
3451
|
};
|
|
3336
3452
|
}
|
|
3337
3453
|
function serviceManifestEventSummary(manifest) {
|