@tapi-dev/sdk 0.1.15 → 0.1.17
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/README.md +17 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +234 -71
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,6 +30,23 @@ Studio executable is installed yet, `tapi studio` runs the installer first, then
|
|
|
30
30
|
opens Studio. The normal developer path is the portable server launched by the
|
|
31
31
|
CLI.
|
|
32
32
|
|
|
33
|
+
By default, Studio downloads and extracted portable server releases live under
|
|
34
|
+
the user's local app data directory. On Windows that is usually
|
|
35
|
+
`%LOCALAPPDATA%\Tapi\Studio`. To keep the whole Studio install on another drive,
|
|
36
|
+
set `TAPI_STUDIO_HOME` before launching:
|
|
37
|
+
|
|
38
|
+
```powershell
|
|
39
|
+
$env:TAPI_STUDIO_HOME="F:\Tapi\Studio"
|
|
40
|
+
npx tapi studio
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For separate paths, use `--cache-dir` for downloaded zip artifacts and
|
|
44
|
+
`--install-dir` for extracted portable Studio server releases.
|
|
45
|
+
|
|
46
|
+
After a successful install, the CLI prunes older Tapi-managed download artifacts
|
|
47
|
+
from the cache and older SDK-marked release folders from the install directory.
|
|
48
|
+
The current artifact and current release are kept.
|
|
49
|
+
|
|
33
50
|
Useful commands:
|
|
34
51
|
|
|
35
52
|
```bash
|
package/dist/cli.d.ts
CHANGED
|
@@ -54,6 +54,7 @@ interface StudioCliOptions {
|
|
|
54
54
|
manifestUrl: string;
|
|
55
55
|
manifestUrlOverride?: string;
|
|
56
56
|
cacheDir: string;
|
|
57
|
+
installDir: string;
|
|
57
58
|
downloadOnly: boolean;
|
|
58
59
|
silent: boolean;
|
|
59
60
|
exePath?: string;
|
|
@@ -102,7 +103,13 @@ export declare function getStudioExecutableCandidates(): string[];
|
|
|
102
103
|
export declare function validateStudioManifest(input: unknown): StudioReleaseManifest;
|
|
103
104
|
export declare function validateServiceReleaseManifest(input: unknown): ServiceReleaseManifest;
|
|
104
105
|
export declare function compareVersions(left: string, right: string): number;
|
|
106
|
+
export declare function memoizeStudioInstallToken(options: StudioCliOptions, installToken?: string): string;
|
|
105
107
|
export declare function findPortablePlaywrightBrowsersDir(exePath: string): string | undefined;
|
|
106
108
|
export declare function waitForHttpOk(url: string, timeoutMs?: number, intervalMs?: number, fetchImpl?: FetchLike): Promise<void>;
|
|
107
109
|
export declare function findPortableStudioServerExe(releaseDir: string, manifest: StudioReleaseManifest): string | undefined;
|
|
110
|
+
export declare function buildExpandArchiveCommand(zipPath: string, destination: string): string;
|
|
111
|
+
export declare function formatZipExtractionFailure(zipPath: string, destination: string, error: unknown): string;
|
|
112
|
+
export declare function pruneManagedFiles(directory: string, keepPaths: string[], isManagedFileName: (name: string) => boolean, maxNoncurrentEntries?: number, label?: string): Promise<number>;
|
|
113
|
+
export declare function isTapiStudioDownloadArtifact(fileName: string): boolean;
|
|
114
|
+
export declare function isTapiServiceDownloadArtifact(fileName: string): boolean;
|
|
108
115
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -23,6 +23,9 @@ const DEFAULT_INSTALL_AUTH_TIMEOUT_MS = 120_000;
|
|
|
23
23
|
const PORTABLE_STUDIO_SERVER_READY_TIMEOUT_MS = 30_000;
|
|
24
24
|
const PORTABLE_STUDIO_SERVER_READY_INTERVAL_MS = 250;
|
|
25
25
|
const MAX_WIDE_EVENT_PROCESS_ROOTS = 5;
|
|
26
|
+
const MAX_NONCURRENT_MANAGED_DOWNLOADS = 0;
|
|
27
|
+
const MAX_NONCURRENT_MANAGED_RELEASES = 0;
|
|
28
|
+
const MANAGED_RELEASE_MARKER = ".tapi-managed-release";
|
|
26
29
|
class StudioInstallApprovalError extends Error {
|
|
27
30
|
code;
|
|
28
31
|
status;
|
|
@@ -282,6 +285,14 @@ export function parseStudioOptions(args) {
|
|
|
282
285
|
raw.cacheDir = resolve(arg.slice("--cache-dir=".length));
|
|
283
286
|
continue;
|
|
284
287
|
}
|
|
288
|
+
if (arg === "--install-dir") {
|
|
289
|
+
raw.installDir = resolve(requireOptionValue(args, ++index, "--install-dir"));
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (arg.startsWith("--install-dir=")) {
|
|
293
|
+
raw.installDir = resolve(arg.slice("--install-dir=".length));
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
285
296
|
if (arg === "--exe") {
|
|
286
297
|
raw.exePath = resolve(requireOptionValue(args, ++index, "--exe"));
|
|
287
298
|
continue;
|
|
@@ -324,6 +335,7 @@ export function parseStudioOptions(args) {
|
|
|
324
335
|
manifestUrl,
|
|
325
336
|
manifestUrlOverride: explicitManifestUrl ? normalizeHttpUrl(explicitManifestUrl, "Studio manifest URL") : undefined,
|
|
326
337
|
cacheDir: raw.cacheDir ?? getDefaultStudioCacheDir(),
|
|
338
|
+
installDir: raw.installDir ?? resolve(envString("TAPI_STUDIO_INSTALL_DIR") ?? getDefaultPortableStudioServerReleasesDir()),
|
|
327
339
|
downloadOnly: raw.downloadOnly ?? false,
|
|
328
340
|
silent: raw.silent ?? false,
|
|
329
341
|
exePath: raw.exePath ?? envString("TAPI_STUDIO_EXE"),
|
|
@@ -1074,18 +1086,7 @@ async function fetchServiceManifestForEnsure(options) {
|
|
|
1074
1086
|
options: installEventOptions(options),
|
|
1075
1087
|
});
|
|
1076
1088
|
try {
|
|
1077
|
-
|
|
1078
|
-
if (!installToken) {
|
|
1079
|
-
await event.phase("auth.install_token.request.start", {
|
|
1080
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1081
|
-
channel: options.channel,
|
|
1082
|
-
});
|
|
1083
|
-
installToken = await withCliSpinner("Checking Tapi Service install approval", () => obtainStudioInstallToken(options, event));
|
|
1084
|
-
await event.phase("auth.install_token.request.success", {
|
|
1085
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1086
|
-
channel: options.channel,
|
|
1087
|
-
});
|
|
1088
|
-
}
|
|
1089
|
+
const installToken = await ensureStudioInstallTokenForRun(options, event, "Checking Tapi Service install authorization");
|
|
1089
1090
|
await event.phase("service_manifest.fetch.start", {
|
|
1090
1091
|
apiBaseUrl: options.apiBaseUrl,
|
|
1091
1092
|
channel: options.channel,
|
|
@@ -1148,18 +1149,7 @@ async function installService(options) {
|
|
|
1148
1149
|
arch: process.arch,
|
|
1149
1150
|
});
|
|
1150
1151
|
ensureWindowsHost();
|
|
1151
|
-
|
|
1152
|
-
if (!installToken) {
|
|
1153
|
-
await event.phase("auth.install_token.request.start", {
|
|
1154
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1155
|
-
channel: options.channel,
|
|
1156
|
-
});
|
|
1157
|
-
installToken = await withCliSpinner("Checking Tapi Service install approval", () => obtainStudioInstallToken(options, event));
|
|
1158
|
-
await event.phase("auth.install_token.request.success", {
|
|
1159
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1160
|
-
channel: options.channel,
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1152
|
+
const installToken = await ensureStudioInstallTokenForRun(options, event, "Checking Tapi Service install authorization");
|
|
1163
1153
|
await event.phase("service_manifest.fetch.start", {
|
|
1164
1154
|
apiBaseUrl: options.apiBaseUrl,
|
|
1165
1155
|
channel: options.channel,
|
|
@@ -1202,6 +1192,7 @@ async function installService(options) {
|
|
|
1202
1192
|
}
|
|
1203
1193
|
const releaseDir = join(releasesDir, safePathSegment(manifest.version));
|
|
1204
1194
|
await withCliSpinner(`Extracting Tapi Service ${manifest.version}`, () => extractZip(zipPath, releaseDir));
|
|
1195
|
+
await markManagedReleaseDir(releaseDir, manifest.version);
|
|
1205
1196
|
const installer = join(releaseDir, manifest.installer || "install_tapi_service.ps1");
|
|
1206
1197
|
const hostExe = join(releaseDir, manifest.serviceHostExecutable || "tapi-service-host.exe");
|
|
1207
1198
|
if (!existsSync(installer)) {
|
|
@@ -1243,6 +1234,8 @@ async function installService(options) {
|
|
|
1243
1234
|
releaseDir,
|
|
1244
1235
|
version: manifest.version,
|
|
1245
1236
|
});
|
|
1237
|
+
await pruneManagedFiles(downloadsDir, [zipPath], isTapiServiceDownloadArtifact, MAX_NONCURRENT_MANAGED_DOWNLOADS, "old Tapi Service download");
|
|
1238
|
+
await pruneManagedDirs(releasesDir, [releaseDir], isSafeManagedReleaseDirName, MAX_NONCURRENT_MANAGED_RELEASES, "old Tapi Service release");
|
|
1246
1239
|
console.log("Tapi Service is installed and running.");
|
|
1247
1240
|
return 0;
|
|
1248
1241
|
}
|
|
@@ -1395,6 +1388,10 @@ async function requestJson(method, url, body, apiKey, projectId) {
|
|
|
1395
1388
|
}
|
|
1396
1389
|
}
|
|
1397
1390
|
export function getDefaultStudioCacheDir() {
|
|
1391
|
+
const homeOverride = envString("TAPI_STUDIO_HOME");
|
|
1392
|
+
if (homeOverride) {
|
|
1393
|
+
return join(resolve(homeOverride), "downloads");
|
|
1394
|
+
}
|
|
1398
1395
|
if (process.platform === "win32") {
|
|
1399
1396
|
const localAppData = process.env.LOCALAPPDATA ??
|
|
1400
1397
|
(process.env.USERPROFILE ? join(process.env.USERPROFILE, "AppData", "Local") : join(homedir(), "AppData", "Local"));
|
|
@@ -1716,24 +1713,7 @@ async function installStudio(options) {
|
|
|
1716
1713
|
throw new Error("Direct Studio manifest overrides are no longer supported for install. "
|
|
1717
1714
|
+ "Use --channel with the protected install flow instead.");
|
|
1718
1715
|
}
|
|
1719
|
-
|
|
1720
|
-
if (installToken) {
|
|
1721
|
-
await event.phase("auth.install_token.provided", {
|
|
1722
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1723
|
-
channel: options.channel,
|
|
1724
|
-
});
|
|
1725
|
-
}
|
|
1726
|
-
else {
|
|
1727
|
-
await event.phase("auth.install_token.request.start", {
|
|
1728
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1729
|
-
channel: options.channel,
|
|
1730
|
-
});
|
|
1731
|
-
installToken = await withCliSpinner("Checking Studio install approval", () => obtainStudioInstallToken(options, event));
|
|
1732
|
-
await event.phase("auth.install_token.request.success", {
|
|
1733
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1734
|
-
channel: options.channel,
|
|
1735
|
-
});
|
|
1736
|
-
}
|
|
1716
|
+
const installToken = await ensureStudioInstallTokenForRun(options, event, "Checking Studio install authorization");
|
|
1737
1717
|
await event.phase("manifest.fetch.start", {
|
|
1738
1718
|
apiBaseUrl: options.apiBaseUrl,
|
|
1739
1719
|
channel: options.channel,
|
|
@@ -1852,6 +1832,39 @@ async function obtainStudioInstallToken(options, event) {
|
|
|
1852
1832
|
const issued = await requestStudioInstallToken(options.apiBaseUrl, options.channel, firebaseCreds.idToken);
|
|
1853
1833
|
return issued.installToken;
|
|
1854
1834
|
}
|
|
1835
|
+
export function memoizeStudioInstallToken(options, installToken) {
|
|
1836
|
+
const existing = options.installToken?.trim();
|
|
1837
|
+
if (existing) {
|
|
1838
|
+
options.installToken = existing;
|
|
1839
|
+
return existing;
|
|
1840
|
+
}
|
|
1841
|
+
const normalized = installToken?.trim() ?? "";
|
|
1842
|
+
if (normalized) {
|
|
1843
|
+
options.installToken = normalized;
|
|
1844
|
+
}
|
|
1845
|
+
return normalized;
|
|
1846
|
+
}
|
|
1847
|
+
async function ensureStudioInstallTokenForRun(options, event, message) {
|
|
1848
|
+
const existing = memoizeStudioInstallToken(options);
|
|
1849
|
+
if (existing) {
|
|
1850
|
+
await event.phase("auth.install_token.reused", {
|
|
1851
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
1852
|
+
channel: options.channel,
|
|
1853
|
+
});
|
|
1854
|
+
return existing;
|
|
1855
|
+
}
|
|
1856
|
+
await event.phase("auth.install_token.request.start", {
|
|
1857
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
1858
|
+
channel: options.channel,
|
|
1859
|
+
});
|
|
1860
|
+
const installToken = memoizeStudioInstallToken(options, await withCliSpinner(message, () => obtainStudioInstallToken(options, event)));
|
|
1861
|
+
await event.phase("auth.install_token.request.success", {
|
|
1862
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
1863
|
+
channel: options.channel,
|
|
1864
|
+
cachedForRun: true,
|
|
1865
|
+
});
|
|
1866
|
+
return installToken;
|
|
1867
|
+
}
|
|
1855
1868
|
async function openStudio(options) {
|
|
1856
1869
|
ensureWindowsHost();
|
|
1857
1870
|
if (options.downloadOnly) {
|
|
@@ -1901,18 +1914,7 @@ async function ensurePortableStudioServer(options) {
|
|
|
1901
1914
|
options: installEventOptions(options),
|
|
1902
1915
|
});
|
|
1903
1916
|
try {
|
|
1904
|
-
|
|
1905
|
-
if (!installToken) {
|
|
1906
|
-
await event.phase("auth.install_token.request.start", {
|
|
1907
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1908
|
-
channel: options.channel,
|
|
1909
|
-
});
|
|
1910
|
-
installToken = await withCliSpinner("Checking Studio install approval", () => obtainStudioInstallToken(options, event));
|
|
1911
|
-
await event.phase("auth.install_token.request.success", {
|
|
1912
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1913
|
-
channel: options.channel,
|
|
1914
|
-
});
|
|
1915
|
-
}
|
|
1917
|
+
const installToken = await ensureStudioInstallTokenForRun(options, event, "Checking Studio install authorization");
|
|
1916
1918
|
await event.phase("manifest.fetch.start", {
|
|
1917
1919
|
apiBaseUrl: options.apiBaseUrl,
|
|
1918
1920
|
channel: options.channel,
|
|
@@ -1928,7 +1930,7 @@ async function ensurePortableStudioServer(options) {
|
|
|
1928
1930
|
});
|
|
1929
1931
|
return null;
|
|
1930
1932
|
}
|
|
1931
|
-
const existing = findPortableStudioServerExe(portableStudioServerReleaseDir(manifest), manifest);
|
|
1933
|
+
const existing = findPortableStudioServerExe(portableStudioServerReleaseDir(manifest, options.installDir), manifest);
|
|
1932
1934
|
if (existing) {
|
|
1933
1935
|
await event.finish(true, {
|
|
1934
1936
|
serverExe: existing,
|
|
@@ -1972,14 +1974,13 @@ async function installPortableStudioServer(options, manifest, event) {
|
|
|
1972
1974
|
}
|
|
1973
1975
|
if (options.downloadOnly) {
|
|
1974
1976
|
console.log(`Downloaded Tapi Studio server artifact: ${artifactPath}`);
|
|
1977
|
+
await pruneManagedFiles(options.cacheDir, [artifactPath], isTapiStudioDownloadArtifact, MAX_NONCURRENT_MANAGED_DOWNLOADS, "old Tapi Studio download");
|
|
1975
1978
|
return artifactPath;
|
|
1976
1979
|
}
|
|
1977
|
-
const releaseDir = portableStudioServerReleaseDir(manifest);
|
|
1978
|
-
await withCliSpinner(`Extracting Tapi Studio server ${manifest.version}`, () =>
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
throw new Error(`Tapi Studio server executable was not found after extraction: ${releaseDir}`);
|
|
1982
|
-
}
|
|
1980
|
+
const releaseDir = portableStudioServerReleaseDir(manifest, options.installDir);
|
|
1981
|
+
const exePath = await withCliSpinner(`Extracting Tapi Studio server ${manifest.version}`, () => extractPortableStudioServerZip(artifactPath, releaseDir, manifest));
|
|
1982
|
+
await pruneManagedFiles(options.cacheDir, [artifactPath], isTapiStudioDownloadArtifact, MAX_NONCURRENT_MANAGED_DOWNLOADS, "old Tapi Studio download");
|
|
1983
|
+
await pruneManagedDirs(dirname(releaseDir), [releaseDir], isSafeManagedReleaseDirName, MAX_NONCURRENT_MANAGED_RELEASES, "old Tapi Studio server release");
|
|
1983
1984
|
console.log(`Installed Tapi Studio server ${manifest.version}: ${releaseDir}`);
|
|
1984
1985
|
return exePath;
|
|
1985
1986
|
}
|
|
@@ -2493,10 +2494,14 @@ function isPortableStudioServerManifest(manifest) {
|
|
|
2493
2494
|
const kind = String(manifest.installerKind || "").toLowerCase();
|
|
2494
2495
|
return kind === "portable-server" || manifest.artifactName.toLowerCase().endsWith(".zip");
|
|
2495
2496
|
}
|
|
2496
|
-
function portableStudioServerReleaseDir(manifest) {
|
|
2497
|
-
return join(
|
|
2497
|
+
function portableStudioServerReleaseDir(manifest, releasesDir = getDefaultPortableStudioServerReleasesDir()) {
|
|
2498
|
+
return join(releasesDir, safePathSegment(manifest.version));
|
|
2498
2499
|
}
|
|
2499
2500
|
function getDefaultPortableStudioServerReleasesDir() {
|
|
2501
|
+
const homeOverride = envString("TAPI_STUDIO_HOME");
|
|
2502
|
+
if (homeOverride) {
|
|
2503
|
+
return join(resolve(homeOverride), "server", "releases");
|
|
2504
|
+
}
|
|
2500
2505
|
if (process.platform === "win32") {
|
|
2501
2506
|
const localAppData = process.env.LOCALAPPDATA ??
|
|
2502
2507
|
(process.env.USERPROFILE ? join(process.env.USERPROFILE, "AppData", "Local") : join(homedir(), "AppData", "Local"));
|
|
@@ -2519,6 +2524,34 @@ export function findPortableStudioServerExe(releaseDir, manifest) {
|
|
|
2519
2524
|
];
|
|
2520
2525
|
return candidates.find((candidate) => existsSync(candidate));
|
|
2521
2526
|
}
|
|
2527
|
+
async function extractPortableStudioServerZip(zipPath, releaseDir, manifest) {
|
|
2528
|
+
const stagingDir = `${releaseDir}.extracting-${process.pid}-${Date.now()}`;
|
|
2529
|
+
const existingExe = findPortableStudioServerExe(releaseDir, manifest);
|
|
2530
|
+
if (!existingExe) {
|
|
2531
|
+
await rm(releaseDir, { recursive: true, force: true });
|
|
2532
|
+
}
|
|
2533
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
2534
|
+
try {
|
|
2535
|
+
await extractZip(zipPath, stagingDir);
|
|
2536
|
+
const stagedExe = findPortableStudioServerExe(stagingDir, manifest);
|
|
2537
|
+
if (!stagedExe) {
|
|
2538
|
+
throw new Error(`Tapi Studio server executable was not found after extraction. Expected ${manifest.serverExecutable || "tapi-studio-server.exe"} under ${stagingDir}.`);
|
|
2539
|
+
}
|
|
2540
|
+
await rm(releaseDir, { recursive: true, force: true });
|
|
2541
|
+
await mkdir(dirname(releaseDir), { recursive: true });
|
|
2542
|
+
await rename(stagingDir, releaseDir);
|
|
2543
|
+
await markManagedReleaseDir(releaseDir, manifest.version);
|
|
2544
|
+
const finalExe = findPortableStudioServerExe(releaseDir, manifest);
|
|
2545
|
+
if (!finalExe) {
|
|
2546
|
+
throw new Error(`Tapi Studio server executable disappeared while finalizing extraction: ${releaseDir}`);
|
|
2547
|
+
}
|
|
2548
|
+
return finalExe;
|
|
2549
|
+
}
|
|
2550
|
+
catch (error) {
|
|
2551
|
+
await rm(stagingDir, { recursive: true, force: true }).catch(() => undefined);
|
|
2552
|
+
throw error;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2522
2555
|
function cachedServiceArtifactName(manifest) {
|
|
2523
2556
|
const rawName = manifest.artifactName || basename(new URL(manifest.url).pathname) || `TapiService-${manifest.version}.zip`;
|
|
2524
2557
|
return rawName.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
@@ -2526,13 +2559,141 @@ function cachedServiceArtifactName(manifest) {
|
|
|
2526
2559
|
async function extractZip(zipPath, destination) {
|
|
2527
2560
|
await rm(destination, { recursive: true, force: true });
|
|
2528
2561
|
await mkdir(destination, { recursive: true });
|
|
2529
|
-
|
|
2530
|
-
"
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2562
|
+
try {
|
|
2563
|
+
await runProcessCapture("powershell.exe", [
|
|
2564
|
+
"-NoProfile",
|
|
2565
|
+
"-ExecutionPolicy",
|
|
2566
|
+
"Bypass",
|
|
2567
|
+
"-Command",
|
|
2568
|
+
buildExpandArchiveCommand(zipPath, destination),
|
|
2569
|
+
]);
|
|
2570
|
+
}
|
|
2571
|
+
catch (error) {
|
|
2572
|
+
throw new Error(formatZipExtractionFailure(zipPath, destination, error), { cause: error });
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
export function buildExpandArchiveCommand(zipPath, destination) {
|
|
2576
|
+
return `$ErrorActionPreference = 'Stop'; try { Expand-Archive -LiteralPath ${powerShellSingleQuoted(zipPath)} -DestinationPath ${powerShellSingleQuoted(destination)} -Force -ErrorAction Stop } catch { Write-Error $_; exit 1 }`;
|
|
2577
|
+
}
|
|
2578
|
+
export function formatZipExtractionFailure(zipPath, destination, error) {
|
|
2579
|
+
const detail = formatError(error);
|
|
2580
|
+
const diskHint = /not enough space|no space left|disk full/i.test(detail)
|
|
2581
|
+
? " Free disk space on that drive, or set TAPI_STUDIO_HOME / --cache-dir / --install-dir to paths on a drive with more room."
|
|
2582
|
+
: "";
|
|
2583
|
+
return `Failed to extract archive ${zipPath} to ${destination}: ${detail}${diskHint}`;
|
|
2584
|
+
}
|
|
2585
|
+
export async function pruneManagedFiles(directory, keepPaths, isManagedFileName, maxNoncurrentEntries = 0, label = "old Tapi download") {
|
|
2586
|
+
const keep = normalizedKeepPathSet(keepPaths);
|
|
2587
|
+
let entries;
|
|
2588
|
+
try {
|
|
2589
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
2590
|
+
}
|
|
2591
|
+
catch {
|
|
2592
|
+
return 0;
|
|
2593
|
+
}
|
|
2594
|
+
const candidates = [];
|
|
2595
|
+
for (const entry of entries) {
|
|
2596
|
+
if (!entry.isFile() || !isManagedFileName(entry.name)) {
|
|
2597
|
+
continue;
|
|
2598
|
+
}
|
|
2599
|
+
const candidate = join(directory, entry.name);
|
|
2600
|
+
if (keep.has(normalizeFilesystemPath(candidate))) {
|
|
2601
|
+
continue;
|
|
2602
|
+
}
|
|
2603
|
+
const info = await stat(candidate).catch(() => undefined);
|
|
2604
|
+
if (!info?.isFile()) {
|
|
2605
|
+
continue;
|
|
2606
|
+
}
|
|
2607
|
+
candidates.push({ path: candidate, mtimeMs: info.mtimeMs });
|
|
2608
|
+
}
|
|
2609
|
+
candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
2610
|
+
const removable = candidates.slice(Math.max(0, maxNoncurrentEntries));
|
|
2611
|
+
let removed = 0;
|
|
2612
|
+
for (const candidate of removable) {
|
|
2613
|
+
try {
|
|
2614
|
+
await rm(candidate.path, { force: true });
|
|
2615
|
+
removed += 1;
|
|
2616
|
+
}
|
|
2617
|
+
catch {
|
|
2618
|
+
// Cache pruning is best effort; install success should not depend on cleanup.
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
if (removed > 0) {
|
|
2622
|
+
console.log(`Pruned ${removed} ${label}${removed === 1 ? "" : "s"}.`);
|
|
2623
|
+
}
|
|
2624
|
+
return removed;
|
|
2625
|
+
}
|
|
2626
|
+
async function pruneManagedDirs(directory, keepPaths, isManagedDirName, maxNoncurrentEntries = 0, label = "old Tapi release") {
|
|
2627
|
+
const keep = normalizedKeepPathSet(keepPaths);
|
|
2628
|
+
let entries;
|
|
2629
|
+
try {
|
|
2630
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
2631
|
+
}
|
|
2632
|
+
catch {
|
|
2633
|
+
return 0;
|
|
2634
|
+
}
|
|
2635
|
+
const candidates = [];
|
|
2636
|
+
for (const entry of entries) {
|
|
2637
|
+
if (!entry.isDirectory() || !isManagedDirName(entry.name)) {
|
|
2638
|
+
continue;
|
|
2639
|
+
}
|
|
2640
|
+
const candidate = join(directory, entry.name);
|
|
2641
|
+
if (keep.has(normalizeFilesystemPath(candidate))) {
|
|
2642
|
+
continue;
|
|
2643
|
+
}
|
|
2644
|
+
if (!existsSync(join(candidate, MANAGED_RELEASE_MARKER))) {
|
|
2645
|
+
continue;
|
|
2646
|
+
}
|
|
2647
|
+
const info = await stat(candidate).catch(() => undefined);
|
|
2648
|
+
if (!info?.isDirectory()) {
|
|
2649
|
+
continue;
|
|
2650
|
+
}
|
|
2651
|
+
candidates.push({ path: candidate, mtimeMs: info.mtimeMs });
|
|
2652
|
+
}
|
|
2653
|
+
candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
2654
|
+
const removable = candidates.slice(Math.max(0, maxNoncurrentEntries));
|
|
2655
|
+
let removed = 0;
|
|
2656
|
+
for (const candidate of removable) {
|
|
2657
|
+
try {
|
|
2658
|
+
await rm(candidate.path, { recursive: true, force: true });
|
|
2659
|
+
removed += 1;
|
|
2660
|
+
}
|
|
2661
|
+
catch {
|
|
2662
|
+
// Cache pruning is best effort; install success should not depend on cleanup.
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
if (removed > 0) {
|
|
2666
|
+
console.log(`Pruned ${removed} ${label}${removed === 1 ? "" : "s"}.`);
|
|
2667
|
+
}
|
|
2668
|
+
return removed;
|
|
2669
|
+
}
|
|
2670
|
+
export function isTapiStudioDownloadArtifact(fileName) {
|
|
2671
|
+
const normalized = fileName.toLowerCase();
|
|
2672
|
+
return (normalized.endsWith(".zip")
|
|
2673
|
+
&& (normalized.startsWith("tapi-studio")
|
|
2674
|
+
|| normalized.startsWith("tapistudio")
|
|
2675
|
+
|| normalized.includes("tapi_studio")));
|
|
2676
|
+
}
|
|
2677
|
+
export function isTapiServiceDownloadArtifact(fileName) {
|
|
2678
|
+
const normalized = fileName.toLowerCase();
|
|
2679
|
+
return normalized.endsWith(".zip") && (normalized.startsWith("tapiservice-") || normalized.startsWith("tapi-service"));
|
|
2680
|
+
}
|
|
2681
|
+
function isSafeManagedReleaseDirName(name) {
|
|
2682
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name);
|
|
2683
|
+
}
|
|
2684
|
+
async function markManagedReleaseDir(directory, version) {
|
|
2685
|
+
const payload = JSON.stringify({
|
|
2686
|
+
product: "tapi",
|
|
2687
|
+
version,
|
|
2688
|
+
managedBy: "@tapi-dev/sdk",
|
|
2689
|
+
});
|
|
2690
|
+
await writeFile(join(directory, MANAGED_RELEASE_MARKER), `${payload}\n`).catch(() => undefined);
|
|
2691
|
+
}
|
|
2692
|
+
function normalizedKeepPathSet(paths) {
|
|
2693
|
+
return new Set(paths.map((path) => normalizeFilesystemPath(path)));
|
|
2694
|
+
}
|
|
2695
|
+
function normalizeFilesystemPath(path) {
|
|
2696
|
+
return resolve(path).replace(/\\/g, "/").toLowerCase();
|
|
2536
2697
|
}
|
|
2537
2698
|
function installEventOptions(options) {
|
|
2538
2699
|
return {
|
|
@@ -2541,6 +2702,7 @@ function installEventOptions(options) {
|
|
|
2541
2702
|
manifestUrl: options.manifestUrl,
|
|
2542
2703
|
manifestUrlOverride: options.manifestUrlOverride,
|
|
2543
2704
|
cacheDir: options.cacheDir,
|
|
2705
|
+
installDir: options.installDir,
|
|
2544
2706
|
downloadOnly: options.downloadOnly,
|
|
2545
2707
|
silent: options.silent,
|
|
2546
2708
|
exePath: options.exePath,
|
|
@@ -2754,7 +2916,7 @@ Usage:
|
|
|
2754
2916
|
|
|
2755
2917
|
Options:
|
|
2756
2918
|
--channel <name> Release channel: pilot, stable, or nightly
|
|
2757
|
-
--api-base-url <url> Tapi API base URL for
|
|
2919
|
+
--api-base-url <url> Tapi API base URL for install authorization and protected downloads
|
|
2758
2920
|
--server <url> Alias for --api-base-url
|
|
2759
2921
|
--workspace <path> Search this directory for .tapi/project.json
|
|
2760
2922
|
--no-workspace Do not load .tapi/project.json
|
|
@@ -2763,6 +2925,7 @@ Options:
|
|
|
2763
2925
|
--install-token <tok> Preissued Studio install token (skips browser sign-in)
|
|
2764
2926
|
--manifest <url> Exact release manifest URL for doctor only
|
|
2765
2927
|
--cache-dir <path> Installer download cache directory
|
|
2928
|
+
--install-dir <path> Portable Studio server release directory
|
|
2766
2929
|
--download-only Download and verify without running the installer
|
|
2767
2930
|
--silent Run the NSIS installer with /S
|
|
2768
2931
|
--exe <path> Tapi Studio executable path for open/doctor
|