@tapi-dev/sdk 0.1.13 → 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 +8 -0
- package/dist/cli.js +406 -146
- 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,6 +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;
|
|
107
|
+
export declare function findPortablePlaywrightBrowsersDir(exePath: string): string | undefined;
|
|
105
108
|
export declare function waitForHttpOk(url: string, timeoutMs?: number, intervalMs?: number, fetchImpl?: FetchLike): Promise<void>;
|
|
106
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;
|
|
107
115
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { createServer as createNetServer } from "node:net";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
-
import { createReadStream, createWriteStream, existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { createReadStream, createWriteStream, existsSync, readFileSync, readdirSync } from "node:fs";
|
|
7
7
|
import { mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { basename, dirname, join, resolve } from "node:path";
|
|
@@ -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;
|
|
@@ -34,6 +37,75 @@ class StudioInstallApprovalError extends Error {
|
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
const sdkVersion = readSdkVersion();
|
|
40
|
+
const CLI_SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
|
41
|
+
function cliSpinnerEnabled() {
|
|
42
|
+
const noSpinner = (process.env.TAPI_NO_SPINNER || "").trim().toLowerCase();
|
|
43
|
+
if (["1", "true", "yes", "on"].includes(noSpinner)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const spinner = (process.env.TAPI_CLI_SPINNER || "").trim().toLowerCase();
|
|
47
|
+
if (["0", "false", "no", "off"].includes(spinner)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return Boolean(process.stderr.isTTY);
|
|
51
|
+
}
|
|
52
|
+
class CliSpinner {
|
|
53
|
+
message;
|
|
54
|
+
frameIndex = 0;
|
|
55
|
+
lastLength = 0;
|
|
56
|
+
timer;
|
|
57
|
+
constructor(message) {
|
|
58
|
+
this.message = message;
|
|
59
|
+
}
|
|
60
|
+
start() {
|
|
61
|
+
if (this.timer) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.render();
|
|
65
|
+
this.timer = setInterval(() => this.render(), 90);
|
|
66
|
+
}
|
|
67
|
+
succeed() {
|
|
68
|
+
this.stop("done");
|
|
69
|
+
}
|
|
70
|
+
fail() {
|
|
71
|
+
this.stop("failed");
|
|
72
|
+
}
|
|
73
|
+
render() {
|
|
74
|
+
const frame = CLI_SPINNER_FRAMES[this.frameIndex % CLI_SPINNER_FRAMES.length];
|
|
75
|
+
this.frameIndex += 1;
|
|
76
|
+
this.write(`${frame} ${this.message}`);
|
|
77
|
+
}
|
|
78
|
+
stop(status) {
|
|
79
|
+
if (this.timer) {
|
|
80
|
+
clearInterval(this.timer);
|
|
81
|
+
this.timer = undefined;
|
|
82
|
+
}
|
|
83
|
+
this.write(`${this.message} ${status}`);
|
|
84
|
+
process.stderr.write("\n");
|
|
85
|
+
this.lastLength = 0;
|
|
86
|
+
}
|
|
87
|
+
write(text) {
|
|
88
|
+
const padding = this.lastLength > text.length ? " ".repeat(this.lastLength - text.length) : "";
|
|
89
|
+
process.stderr.write(`\r${text}${padding}`);
|
|
90
|
+
this.lastLength = text.length;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function withCliSpinner(message, work) {
|
|
94
|
+
if (!cliSpinnerEnabled()) {
|
|
95
|
+
return await work();
|
|
96
|
+
}
|
|
97
|
+
const spinner = new CliSpinner(message);
|
|
98
|
+
spinner.start();
|
|
99
|
+
try {
|
|
100
|
+
const result = await work();
|
|
101
|
+
spinner.succeed();
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
spinner.fail();
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
37
109
|
export async function runCli(argv = process.argv.slice(2)) {
|
|
38
110
|
const [command, subcommand, ...rest] = argv;
|
|
39
111
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
@@ -213,6 +285,14 @@ export function parseStudioOptions(args) {
|
|
|
213
285
|
raw.cacheDir = resolve(arg.slice("--cache-dir=".length));
|
|
214
286
|
continue;
|
|
215
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
|
+
}
|
|
216
296
|
if (arg === "--exe") {
|
|
217
297
|
raw.exePath = resolve(requireOptionValue(args, ++index, "--exe"));
|
|
218
298
|
continue;
|
|
@@ -255,6 +335,7 @@ export function parseStudioOptions(args) {
|
|
|
255
335
|
manifestUrl,
|
|
256
336
|
manifestUrlOverride: explicitManifestUrl ? normalizeHttpUrl(explicitManifestUrl, "Studio manifest URL") : undefined,
|
|
257
337
|
cacheDir: raw.cacheDir ?? getDefaultStudioCacheDir(),
|
|
338
|
+
installDir: raw.installDir ?? resolve(envString("TAPI_STUDIO_INSTALL_DIR") ?? getDefaultPortableStudioServerReleasesDir()),
|
|
258
339
|
downloadOnly: raw.downloadOnly ?? false,
|
|
259
340
|
silent: raw.silent ?? false,
|
|
260
341
|
exePath: raw.exePath ?? envString("TAPI_STUDIO_EXE"),
|
|
@@ -339,7 +420,7 @@ async function describeApiOperation(args) {
|
|
|
339
420
|
apiKey: options.apiKey,
|
|
340
421
|
projectId: options.projectId,
|
|
341
422
|
});
|
|
342
|
-
const description = await client.websiteApis.describe(operation);
|
|
423
|
+
const description = await withCliSpinner(`Fetching Tapi API description for ${operation}`, () => client.websiteApis.describe(operation));
|
|
343
424
|
console.log(JSON.stringify(description, null, 2));
|
|
344
425
|
return 0;
|
|
345
426
|
}
|
|
@@ -423,7 +504,7 @@ function parseApiWorkspaceOptions(args) {
|
|
|
423
504
|
async function syncApiCatalog(args) {
|
|
424
505
|
try {
|
|
425
506
|
const options = parseApiWorkspaceOptions(args);
|
|
426
|
-
const catalog = await fetchApiCatalog(options);
|
|
507
|
+
const catalog = await withCliSpinner("Fetching Tapi API catalog", () => fetchApiCatalog(options));
|
|
427
508
|
await writeJsonFile(options.catalogPath, catalog);
|
|
428
509
|
console.log(`Synced Tapi API catalog: ${options.catalogPath}`);
|
|
429
510
|
return 0;
|
|
@@ -436,7 +517,7 @@ async function syncApiCatalog(args) {
|
|
|
436
517
|
async function generateApiClient(args) {
|
|
437
518
|
try {
|
|
438
519
|
const options = parseApiWorkspaceOptions(args);
|
|
439
|
-
const catalog = await fetchApiCatalog(options);
|
|
520
|
+
const catalog = await withCliSpinner("Fetching Tapi API catalog", () => fetchApiCatalog(options));
|
|
440
521
|
await writeJsonFile(options.catalogPath, catalog);
|
|
441
522
|
await writeTextFile(options.typescriptPath, renderGeneratedApiClient(catalog));
|
|
442
523
|
console.log(`Generated Tapi API client: ${options.typescriptPath}`);
|
|
@@ -521,9 +602,11 @@ async function syncApiTriggers(args) {
|
|
|
521
602
|
apiKey: options.apiKey,
|
|
522
603
|
projectId: options.projectId,
|
|
523
604
|
});
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
605
|
+
await withCliSpinner(`Syncing ${triggers.length} Tapi API trigger(s)`, async () => {
|
|
606
|
+
for (const trigger of triggers) {
|
|
607
|
+
await client.triggers.create(trigger);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
527
610
|
console.log(`Synced ${triggers.length} Tapi API trigger(s) from ${options.configPath}.`);
|
|
528
611
|
return 0;
|
|
529
612
|
}
|
|
@@ -906,7 +989,7 @@ async function runServiceCommand(action, args = []) {
|
|
|
906
989
|
return repairService(parseStudioOptions([]));
|
|
907
990
|
}
|
|
908
991
|
const command = servicePowerShell(normalized);
|
|
909
|
-
const output = await runProcessCapture("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command]);
|
|
992
|
+
const output = await withCliSpinner(`Running tapi-service ${normalized}`, () => runProcessCapture("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command]));
|
|
910
993
|
if (output.trim()) {
|
|
911
994
|
console.log(output.trim());
|
|
912
995
|
}
|
|
@@ -918,7 +1001,7 @@ async function runServiceCommand(action, args = []) {
|
|
|
918
1001
|
}
|
|
919
1002
|
}
|
|
920
1003
|
async function repairService(options) {
|
|
921
|
-
const status = await getServiceStatus();
|
|
1004
|
+
const status = await withCliSpinner("Checking Tapi Service status", () => getServiceStatus());
|
|
922
1005
|
if (!status.installed) {
|
|
923
1006
|
return installService(options);
|
|
924
1007
|
}
|
|
@@ -964,7 +1047,7 @@ async function getServiceStatus() {
|
|
|
964
1047
|
}
|
|
965
1048
|
}
|
|
966
1049
|
async function ensureServiceReadyForStudio(options) {
|
|
967
|
-
const status = await getServiceStatus();
|
|
1050
|
+
const status = await withCliSpinner("Checking Tapi Service status", () => getServiceStatus());
|
|
968
1051
|
if (!status.installed) {
|
|
969
1052
|
console.log("Tapi Service is required and is not installed. Installing it now...");
|
|
970
1053
|
const code = await installService(options);
|
|
@@ -985,13 +1068,13 @@ async function ensureServiceReadyForStudio(options) {
|
|
|
985
1068
|
}
|
|
986
1069
|
if (status.status !== "Running") {
|
|
987
1070
|
console.log("Starting Tapi Service...");
|
|
988
|
-
const output = await runProcessCapture("powershell.exe", [
|
|
1071
|
+
const output = await withCliSpinner("Starting Tapi Service", () => runProcessCapture("powershell.exe", [
|
|
989
1072
|
"-NoProfile",
|
|
990
1073
|
"-ExecutionPolicy",
|
|
991
1074
|
"Bypass",
|
|
992
1075
|
"-Command",
|
|
993
1076
|
servicePowerShell("start"),
|
|
994
|
-
]);
|
|
1077
|
+
]));
|
|
995
1078
|
if (output.trim()) {
|
|
996
1079
|
console.log(output.trim());
|
|
997
1080
|
}
|
|
@@ -1003,23 +1086,12 @@ async function fetchServiceManifestForEnsure(options) {
|
|
|
1003
1086
|
options: installEventOptions(options),
|
|
1004
1087
|
});
|
|
1005
1088
|
try {
|
|
1006
|
-
|
|
1007
|
-
if (!installToken) {
|
|
1008
|
-
await event.phase("auth.install_token.request.start", {
|
|
1009
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1010
|
-
channel: options.channel,
|
|
1011
|
-
});
|
|
1012
|
-
installToken = await obtainStudioInstallToken(options, event);
|
|
1013
|
-
await event.phase("auth.install_token.request.success", {
|
|
1014
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1015
|
-
channel: options.channel,
|
|
1016
|
-
});
|
|
1017
|
-
}
|
|
1089
|
+
const installToken = await ensureStudioInstallTokenForRun(options, event, "Checking Tapi Service install authorization");
|
|
1018
1090
|
await event.phase("service_manifest.fetch.start", {
|
|
1019
1091
|
apiBaseUrl: options.apiBaseUrl,
|
|
1020
1092
|
channel: options.channel,
|
|
1021
1093
|
});
|
|
1022
|
-
const manifest = await fetchProtectedServiceManifest(options.apiBaseUrl, options.channel, installToken);
|
|
1094
|
+
const manifest = await withCliSpinner(`Fetching Tapi Service ${options.channel} manifest`, () => fetchProtectedServiceManifest(options.apiBaseUrl, options.channel, installToken));
|
|
1023
1095
|
ensureCompatibleServiceManifest(manifest);
|
|
1024
1096
|
await event.finish(true, {
|
|
1025
1097
|
manifest: serviceManifestEventSummary(manifest),
|
|
@@ -1077,25 +1149,12 @@ async function installService(options) {
|
|
|
1077
1149
|
arch: process.arch,
|
|
1078
1150
|
});
|
|
1079
1151
|
ensureWindowsHost();
|
|
1080
|
-
|
|
1081
|
-
if (!installToken) {
|
|
1082
|
-
console.log("Checking Tapi Service install approval...");
|
|
1083
|
-
await event.phase("auth.install_token.request.start", {
|
|
1084
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1085
|
-
channel: options.channel,
|
|
1086
|
-
});
|
|
1087
|
-
installToken = await obtainStudioInstallToken(options, event);
|
|
1088
|
-
await event.phase("auth.install_token.request.success", {
|
|
1089
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1090
|
-
channel: options.channel,
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
|
-
console.log(`Fetching Tapi Service ${options.channel} manifest...`);
|
|
1152
|
+
const installToken = await ensureStudioInstallTokenForRun(options, event, "Checking Tapi Service install authorization");
|
|
1094
1153
|
await event.phase("service_manifest.fetch.start", {
|
|
1095
1154
|
apiBaseUrl: options.apiBaseUrl,
|
|
1096
1155
|
channel: options.channel,
|
|
1097
1156
|
});
|
|
1098
|
-
const manifest = await fetchProtectedServiceManifest(options.apiBaseUrl, options.channel, installToken);
|
|
1157
|
+
const manifest = await withCliSpinner(`Fetching Tapi Service ${options.channel} manifest`, () => fetchProtectedServiceManifest(options.apiBaseUrl, options.channel, installToken));
|
|
1099
1158
|
await event.phase("service_manifest.fetch.success", {
|
|
1100
1159
|
manifest: serviceManifestEventSummary(manifest),
|
|
1101
1160
|
});
|
|
@@ -1112,13 +1171,12 @@ async function installService(options) {
|
|
|
1112
1171
|
await event.phase("cache.hit", { zipPath });
|
|
1113
1172
|
}
|
|
1114
1173
|
else {
|
|
1115
|
-
console.log(`Downloading Tapi Service ${manifest.version}...`);
|
|
1116
1174
|
await event.phase("download.start", {
|
|
1117
1175
|
url: manifest.url,
|
|
1118
1176
|
destination: zipPath,
|
|
1119
1177
|
expectedSha256: manifest.sha256,
|
|
1120
1178
|
});
|
|
1121
|
-
await downloadAndVerify(manifest.url, zipPath, manifest.sha256, "Tapi Service artifact");
|
|
1179
|
+
await withCliSpinner(`Downloading Tapi Service ${manifest.version}`, () => downloadAndVerify(manifest.url, zipPath, manifest.sha256, "Tapi Service artifact"));
|
|
1122
1180
|
await event.phase("download.verified", {
|
|
1123
1181
|
zipPath,
|
|
1124
1182
|
expectedSha256: manifest.sha256,
|
|
@@ -1133,7 +1191,8 @@ async function installService(options) {
|
|
|
1133
1191
|
return 0;
|
|
1134
1192
|
}
|
|
1135
1193
|
const releaseDir = join(releasesDir, safePathSegment(manifest.version));
|
|
1136
|
-
await extractZip(zipPath, releaseDir);
|
|
1194
|
+
await withCliSpinner(`Extracting Tapi Service ${manifest.version}`, () => extractZip(zipPath, releaseDir));
|
|
1195
|
+
await markManagedReleaseDir(releaseDir, manifest.version);
|
|
1137
1196
|
const installer = join(releaseDir, manifest.installer || "install_tapi_service.ps1");
|
|
1138
1197
|
const hostExe = join(releaseDir, manifest.serviceHostExecutable || "tapi-service-host.exe");
|
|
1139
1198
|
if (!existsSync(installer)) {
|
|
@@ -1144,7 +1203,6 @@ async function installService(options) {
|
|
|
1144
1203
|
}
|
|
1145
1204
|
const installLog = serviceInstallLogPaths(manifest.version);
|
|
1146
1205
|
await mkdir(dirname(installLog.logPath), { recursive: true });
|
|
1147
|
-
console.log(`Installing Tapi Service ${manifest.version}...`);
|
|
1148
1206
|
await event.phase("service_installer.start", {
|
|
1149
1207
|
installer,
|
|
1150
1208
|
hostExe,
|
|
@@ -1153,7 +1211,7 @@ async function installService(options) {
|
|
|
1153
1211
|
resultPath: installLog.resultPath,
|
|
1154
1212
|
});
|
|
1155
1213
|
try {
|
|
1156
|
-
await runProcess("powershell.exe", [
|
|
1214
|
+
await withCliSpinner(`Installing Tapi Service ${manifest.version}`, () => runProcess("powershell.exe", [
|
|
1157
1215
|
"-NoProfile",
|
|
1158
1216
|
"-ExecutionPolicy",
|
|
1159
1217
|
"Bypass",
|
|
@@ -1167,7 +1225,7 @@ async function installService(options) {
|
|
|
1167
1225
|
installLog.logPath,
|
|
1168
1226
|
"-ResultPath",
|
|
1169
1227
|
installLog.resultPath,
|
|
1170
|
-
]);
|
|
1228
|
+
]));
|
|
1171
1229
|
}
|
|
1172
1230
|
catch (error) {
|
|
1173
1231
|
throw new Error(await formatServiceInstallFailure(error, installLog), { cause: error });
|
|
@@ -1176,6 +1234,8 @@ async function installService(options) {
|
|
|
1176
1234
|
releaseDir,
|
|
1177
1235
|
version: manifest.version,
|
|
1178
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");
|
|
1179
1239
|
console.log("Tapi Service is installed and running.");
|
|
1180
1240
|
return 0;
|
|
1181
1241
|
}
|
|
@@ -1196,42 +1256,45 @@ async function publishLocalApis(args) {
|
|
|
1196
1256
|
if (apis.length === 0) {
|
|
1197
1257
|
throw new Error("No local generated APIs found under .tapi/apis.");
|
|
1198
1258
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
const
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
}
|
|
1205
|
-
await putJson(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/v1/sitemaps/${encodeURIComponent(site)}?project=${encodeURIComponent(options.projectId)}`, { site_map: sitemap.siteMap }, options.apiKey, options.projectId);
|
|
1206
|
-
sitemapCount += 1;
|
|
1207
|
-
}
|
|
1208
|
-
let contracts = 0;
|
|
1209
|
-
let requests = 0;
|
|
1210
|
-
for (const api of apis) {
|
|
1211
|
-
const apiId = String(api.id || api.name || "").trim();
|
|
1212
|
-
if (!apiId) {
|
|
1213
|
-
continue;
|
|
1214
|
-
}
|
|
1215
|
-
const site = String(api.site || "").trim();
|
|
1216
|
-
await putJson(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/v1/generated-apis/contracts/${encodeURIComponent(apiId)}?project=${encodeURIComponent(options.projectId)}`, { generated_api: api }, options.apiKey, options.projectId);
|
|
1217
|
-
contracts += 1;
|
|
1218
|
-
const apiRequests = api.requests && typeof api.requests === "object" ? Object.values(api.requests) : [];
|
|
1219
|
-
for (const request of apiRequests) {
|
|
1220
|
-
if (!isRecord(request)) {
|
|
1259
|
+
const { sitemapCount, contracts, requests, releaseVersion } = await withCliSpinner("Publishing local Tapi APIs", async () => {
|
|
1260
|
+
let sitemapCount = 0;
|
|
1261
|
+
for (const sitemap of sitemaps) {
|
|
1262
|
+
const site = String(sitemap.site || "").trim();
|
|
1263
|
+
if (!site || !isRecord(sitemap.siteMap)) {
|
|
1221
1264
|
continue;
|
|
1222
1265
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1266
|
+
await putJson(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/v1/sitemaps/${encodeURIComponent(site)}?project=${encodeURIComponent(options.projectId)}`, { site_map: sitemap.siteMap }, options.apiKey, options.projectId);
|
|
1267
|
+
sitemapCount += 1;
|
|
1268
|
+
}
|
|
1269
|
+
let contracts = 0;
|
|
1270
|
+
let requests = 0;
|
|
1271
|
+
for (const api of apis) {
|
|
1272
|
+
const apiId = String(api.id || api.name || "").trim();
|
|
1273
|
+
if (!apiId) {
|
|
1226
1274
|
continue;
|
|
1227
1275
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1276
|
+
const site = String(api.site || "").trim();
|
|
1277
|
+
await putJson(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/v1/generated-apis/contracts/${encodeURIComponent(apiId)}?project=${encodeURIComponent(options.projectId)}`, { generated_api: api }, options.apiKey, options.projectId);
|
|
1278
|
+
contracts += 1;
|
|
1279
|
+
const apiRequests = api.requests && typeof api.requests === "object" ? Object.values(api.requests) : [];
|
|
1280
|
+
for (const request of apiRequests) {
|
|
1281
|
+
if (!isRecord(request)) {
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
const requestId = String(request.id || request.key || "").trim();
|
|
1285
|
+
const status = String(request.status || "").trim();
|
|
1286
|
+
if (!requestId || !["ready", "published"].includes(status)) {
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
await postJson(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/v1/generated-apis/contracts/${encodeURIComponent(apiId)}/requests/${encodeURIComponent(requestId)}/publish?project=${encodeURIComponent(options.projectId)}`, { site }, options.apiKey, options.projectId);
|
|
1290
|
+
requests += 1;
|
|
1291
|
+
}
|
|
1230
1292
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1293
|
+
const releaseResponse = await postJson(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/v1/generated-apis/releases?project=${encodeURIComponent(options.projectId)}`, { environment: "production" }, options.apiKey, options.projectId);
|
|
1294
|
+
const release = isRecord(releaseResponse) && isRecord(releaseResponse.release) ? releaseResponse.release : {};
|
|
1295
|
+
const releaseVersion = typeof release.version === "string" ? release.version : "unknown";
|
|
1296
|
+
return { sitemapCount, contracts, requests, releaseVersion };
|
|
1297
|
+
});
|
|
1235
1298
|
console.log(`Published ${requests} API request(s) from ${contracts} contract(s), synced ${sitemapCount} sitemap(s), activated release ${releaseVersion}.`);
|
|
1236
1299
|
return 0;
|
|
1237
1300
|
}
|
|
@@ -1325,6 +1388,10 @@ async function requestJson(method, url, body, apiKey, projectId) {
|
|
|
1325
1388
|
}
|
|
1326
1389
|
}
|
|
1327
1390
|
export function getDefaultStudioCacheDir() {
|
|
1391
|
+
const homeOverride = envString("TAPI_STUDIO_HOME");
|
|
1392
|
+
if (homeOverride) {
|
|
1393
|
+
return join(resolve(homeOverride), "downloads");
|
|
1394
|
+
}
|
|
1328
1395
|
if (process.platform === "win32") {
|
|
1329
1396
|
const localAppData = process.env.LOCALAPPDATA ??
|
|
1330
1397
|
(process.env.USERPROFILE ? join(process.env.USERPROFILE, "AppData", "Local") : join(homedir(), "AppData", "Local"));
|
|
@@ -1646,31 +1713,12 @@ async function installStudio(options) {
|
|
|
1646
1713
|
throw new Error("Direct Studio manifest overrides are no longer supported for install. "
|
|
1647
1714
|
+ "Use --channel with the protected install flow instead.");
|
|
1648
1715
|
}
|
|
1649
|
-
|
|
1650
|
-
if (installToken) {
|
|
1651
|
-
await event.phase("auth.install_token.provided", {
|
|
1652
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1653
|
-
channel: options.channel,
|
|
1654
|
-
});
|
|
1655
|
-
}
|
|
1656
|
-
else {
|
|
1657
|
-
console.log("Checking Studio install approval...");
|
|
1658
|
-
await event.phase("auth.install_token.request.start", {
|
|
1659
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1660
|
-
channel: options.channel,
|
|
1661
|
-
});
|
|
1662
|
-
installToken = await obtainStudioInstallToken(options, event);
|
|
1663
|
-
await event.phase("auth.install_token.request.success", {
|
|
1664
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1665
|
-
channel: options.channel,
|
|
1666
|
-
});
|
|
1667
|
-
}
|
|
1668
|
-
console.log(`Fetching Tapi Studio ${options.channel} manifest...`);
|
|
1716
|
+
const installToken = await ensureStudioInstallTokenForRun(options, event, "Checking Studio install authorization");
|
|
1669
1717
|
await event.phase("manifest.fetch.start", {
|
|
1670
1718
|
apiBaseUrl: options.apiBaseUrl,
|
|
1671
1719
|
channel: options.channel,
|
|
1672
1720
|
});
|
|
1673
|
-
const manifest = await fetchProtectedStudioManifest(options.apiBaseUrl, options.channel, installToken);
|
|
1721
|
+
const manifest = await withCliSpinner(`Fetching Tapi Studio ${options.channel} manifest`, () => fetchProtectedStudioManifest(options.apiBaseUrl, options.channel, installToken));
|
|
1674
1722
|
await event.phase("manifest.fetch.success", {
|
|
1675
1723
|
manifest: manifestEventSummary(manifest),
|
|
1676
1724
|
});
|
|
@@ -1702,13 +1750,12 @@ async function installStudio(options) {
|
|
|
1702
1750
|
}
|
|
1703
1751
|
else {
|
|
1704
1752
|
await event.phase("cache.miss", { installerPath });
|
|
1705
|
-
console.log(`Downloading Tapi Studio ${manifest.version}...`);
|
|
1706
1753
|
await event.phase("download.start", {
|
|
1707
1754
|
url: manifest.url,
|
|
1708
1755
|
destination: installerPath,
|
|
1709
1756
|
expectedSha256: manifest.sha256,
|
|
1710
1757
|
});
|
|
1711
|
-
await downloadAndVerify(manifest.url, installerPath, manifest.sha256);
|
|
1758
|
+
await withCliSpinner(`Downloading Tapi Studio ${manifest.version}`, () => downloadAndVerify(manifest.url, installerPath, manifest.sha256));
|
|
1712
1759
|
await event.phase("download.verified", {
|
|
1713
1760
|
installerPath,
|
|
1714
1761
|
expectedSha256: manifest.sha256,
|
|
@@ -1723,12 +1770,11 @@ async function installStudio(options) {
|
|
|
1723
1770
|
return 0;
|
|
1724
1771
|
}
|
|
1725
1772
|
const installerArgs = options.silent ? ["/S"] : [];
|
|
1726
|
-
console.log(`Starting Tapi Studio installer: ${installerPath}`);
|
|
1727
1773
|
await event.phase("installer.start", {
|
|
1728
1774
|
installerPath,
|
|
1729
1775
|
args: installerArgs,
|
|
1730
1776
|
});
|
|
1731
|
-
await runProcess(installerPath, installerArgs);
|
|
1777
|
+
await withCliSpinner("Starting Tapi Studio installer", () => runProcess(installerPath, installerArgs));
|
|
1732
1778
|
console.log("Tapi Studio installer finished.");
|
|
1733
1779
|
await event.finish(true, {
|
|
1734
1780
|
installerPath,
|
|
@@ -1786,6 +1832,39 @@ async function obtainStudioInstallToken(options, event) {
|
|
|
1786
1832
|
const issued = await requestStudioInstallToken(options.apiBaseUrl, options.channel, firebaseCreds.idToken);
|
|
1787
1833
|
return issued.installToken;
|
|
1788
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
|
+
}
|
|
1789
1868
|
async function openStudio(options) {
|
|
1790
1869
|
ensureWindowsHost();
|
|
1791
1870
|
if (options.downloadOnly) {
|
|
@@ -1795,7 +1874,7 @@ async function openStudio(options) {
|
|
|
1795
1874
|
if (!options.exePath) {
|
|
1796
1875
|
const portable = await ensurePortableStudioServer(options);
|
|
1797
1876
|
if (portable) {
|
|
1798
|
-
await launchPortableStudioServer(portable.exePath, options);
|
|
1877
|
+
await launchPortableStudioServer(portable.exePath, options, portable.manifest);
|
|
1799
1878
|
return 0;
|
|
1800
1879
|
}
|
|
1801
1880
|
}
|
|
@@ -1835,23 +1914,12 @@ async function ensurePortableStudioServer(options) {
|
|
|
1835
1914
|
options: installEventOptions(options),
|
|
1836
1915
|
});
|
|
1837
1916
|
try {
|
|
1838
|
-
|
|
1839
|
-
if (!installToken) {
|
|
1840
|
-
await event.phase("auth.install_token.request.start", {
|
|
1841
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1842
|
-
channel: options.channel,
|
|
1843
|
-
});
|
|
1844
|
-
installToken = await obtainStudioInstallToken(options, event);
|
|
1845
|
-
await event.phase("auth.install_token.request.success", {
|
|
1846
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1847
|
-
channel: options.channel,
|
|
1848
|
-
});
|
|
1849
|
-
}
|
|
1917
|
+
const installToken = await ensureStudioInstallTokenForRun(options, event, "Checking Studio install authorization");
|
|
1850
1918
|
await event.phase("manifest.fetch.start", {
|
|
1851
1919
|
apiBaseUrl: options.apiBaseUrl,
|
|
1852
1920
|
channel: options.channel,
|
|
1853
1921
|
});
|
|
1854
|
-
const manifest = await fetchProtectedStudioManifest(options.apiBaseUrl, options.channel, installToken);
|
|
1922
|
+
const manifest = await withCliSpinner(`Fetching Tapi Studio ${options.channel} manifest`, () => fetchProtectedStudioManifest(options.apiBaseUrl, options.channel, installToken));
|
|
1855
1923
|
ensureCompatibleManifest(manifest);
|
|
1856
1924
|
await event.phase("manifest.fetch.success", {
|
|
1857
1925
|
manifest: manifestEventSummary(manifest),
|
|
@@ -1862,7 +1930,7 @@ async function ensurePortableStudioServer(options) {
|
|
|
1862
1930
|
});
|
|
1863
1931
|
return null;
|
|
1864
1932
|
}
|
|
1865
|
-
const existing = findPortableStudioServerExe(portableStudioServerReleaseDir(manifest), manifest);
|
|
1933
|
+
const existing = findPortableStudioServerExe(portableStudioServerReleaseDir(manifest, options.installDir), manifest);
|
|
1866
1934
|
if (existing) {
|
|
1867
1935
|
await event.finish(true, {
|
|
1868
1936
|
serverExe: existing,
|
|
@@ -1893,13 +1961,12 @@ async function installPortableStudioServer(options, manifest, event) {
|
|
|
1893
1961
|
await event.phase("cache.hit", { artifactPath });
|
|
1894
1962
|
}
|
|
1895
1963
|
else {
|
|
1896
|
-
console.log(`Downloading Tapi Studio server ${manifest.version}...`);
|
|
1897
1964
|
await event.phase("download.start", {
|
|
1898
1965
|
url: manifest.url,
|
|
1899
1966
|
destination: artifactPath,
|
|
1900
1967
|
expectedSha256: manifest.sha256,
|
|
1901
1968
|
});
|
|
1902
|
-
await downloadAndVerify(manifest.url, artifactPath, manifest.sha256, "Tapi Studio server artifact");
|
|
1969
|
+
await withCliSpinner(`Downloading Tapi Studio server ${manifest.version}`, () => downloadAndVerify(manifest.url, artifactPath, manifest.sha256, "Tapi Studio server artifact"));
|
|
1903
1970
|
await event.phase("download.verified", {
|
|
1904
1971
|
artifactPath,
|
|
1905
1972
|
expectedSha256: manifest.sha256,
|
|
@@ -1907,18 +1974,17 @@ async function installPortableStudioServer(options, manifest, event) {
|
|
|
1907
1974
|
}
|
|
1908
1975
|
if (options.downloadOnly) {
|
|
1909
1976
|
console.log(`Downloaded Tapi Studio server artifact: ${artifactPath}`);
|
|
1977
|
+
await pruneManagedFiles(options.cacheDir, [artifactPath], isTapiStudioDownloadArtifact, MAX_NONCURRENT_MANAGED_DOWNLOADS, "old Tapi Studio download");
|
|
1910
1978
|
return artifactPath;
|
|
1911
1979
|
}
|
|
1912
|
-
const releaseDir = portableStudioServerReleaseDir(manifest);
|
|
1913
|
-
await
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
throw new Error(`Tapi Studio server executable was not found after extraction: ${releaseDir}`);
|
|
1917
|
-
}
|
|
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");
|
|
1918
1984
|
console.log(`Installed Tapi Studio server ${manifest.version}: ${releaseDir}`);
|
|
1919
1985
|
return exePath;
|
|
1920
1986
|
}
|
|
1921
|
-
async function launchPortableStudioServer(exePath, options) {
|
|
1987
|
+
async function launchPortableStudioServer(exePath, options, manifest) {
|
|
1922
1988
|
const host = "127.0.0.1";
|
|
1923
1989
|
const port = await chooseStudioPort();
|
|
1924
1990
|
const env = buildStudioLaunchEnv(options);
|
|
@@ -1930,17 +1996,49 @@ async function launchPortableStudioServer(exePath, options) {
|
|
|
1930
1996
|
env.TAPI_STUDIO_HOST = host;
|
|
1931
1997
|
env.TAPI_STUDIO_PORT = String(port);
|
|
1932
1998
|
env.TAPI_STUDIO_SERVER_PORT = String(port);
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1999
|
+
env.TAPI_STUDIO_SERVER_VERSION = manifest.version;
|
|
2000
|
+
env.TAPI_STUDIO_SERVER_COMMIT = manifest.commit || "";
|
|
2001
|
+
env.TAPI_STUDIO_SERVER_COMMIT_SHORT = manifest.commitShort || manifest.version;
|
|
2002
|
+
const playwrightBrowsersPath = findPortablePlaywrightBrowsersDir(exePath);
|
|
2003
|
+
if (playwrightBrowsersPath) {
|
|
2004
|
+
env.PLAYWRIGHT_BROWSERS_PATH = playwrightBrowsersPath;
|
|
2005
|
+
}
|
|
1940
2006
|
const url = `http://${host}:${port}`;
|
|
1941
|
-
await
|
|
2007
|
+
await withCliSpinner("Starting Tapi Studio", async () => {
|
|
2008
|
+
const child = spawn(exePath, [], {
|
|
2009
|
+
detached: true,
|
|
2010
|
+
env,
|
|
2011
|
+
stdio: "ignore",
|
|
2012
|
+
windowsHide: true,
|
|
2013
|
+
});
|
|
2014
|
+
child.unref();
|
|
2015
|
+
await waitForHttpOk(`${url}/healthz`, PORTABLE_STUDIO_SERVER_READY_TIMEOUT_MS, PORTABLE_STUDIO_SERVER_READY_INTERVAL_MS);
|
|
2016
|
+
});
|
|
1942
2017
|
openBrowser(url);
|
|
1943
|
-
console.log(`Opened Tapi Studio: ${url}`);
|
|
2018
|
+
console.log(`Opened Tapi Studio ${manifest.version}: ${url}`);
|
|
2019
|
+
}
|
|
2020
|
+
export function findPortablePlaywrightBrowsersDir(exePath) {
|
|
2021
|
+
const exeDir = dirname(exePath);
|
|
2022
|
+
const releaseDir = basename(exeDir).toLowerCase() === "tapi-studio-server"
|
|
2023
|
+
? dirname(exeDir)
|
|
2024
|
+
: exeDir;
|
|
2025
|
+
const candidates = [
|
|
2026
|
+
join(releaseDir, "pw"),
|
|
2027
|
+
join(releaseDir, "playwright-browsers"),
|
|
2028
|
+
join(releaseDir, "sidecars", "pw"),
|
|
2029
|
+
join(exeDir, "pw"),
|
|
2030
|
+
join(exeDir, "playwright-browsers"),
|
|
2031
|
+
];
|
|
2032
|
+
return candidates.find(isPlaywrightBrowsersDir);
|
|
2033
|
+
}
|
|
2034
|
+
function isPlaywrightBrowsersDir(path) {
|
|
2035
|
+
try {
|
|
2036
|
+
return existsSync(path)
|
|
2037
|
+
&& readdirSync(path, { withFileTypes: true }).some((entry) => entry.isDirectory() && entry.name.startsWith("chromium_headless_shell-"));
|
|
2038
|
+
}
|
|
2039
|
+
catch {
|
|
2040
|
+
return false;
|
|
2041
|
+
}
|
|
1944
2042
|
}
|
|
1945
2043
|
export async function waitForHttpOk(url, timeoutMs = PORTABLE_STUDIO_SERVER_READY_TIMEOUT_MS, intervalMs = PORTABLE_STUDIO_SERVER_READY_INTERVAL_MS, fetchImpl = fetch) {
|
|
1946
2044
|
const startedAt = performance.now();
|
|
@@ -1998,7 +2096,7 @@ async function runDoctor(options) {
|
|
|
1998
2096
|
const exePath = options.exePath ?? getStudioExecutableCandidates().find((candidate) => existsSync(candidate));
|
|
1999
2097
|
console.log(`Studio executable: ${exePath && existsSync(exePath) ? exePath : "not found"}`);
|
|
2000
2098
|
try {
|
|
2001
|
-
const manifest = await fetchStudioManifest(options.manifestUrl);
|
|
2099
|
+
const manifest = await withCliSpinner("Fetching latest Tapi Studio manifest", () => fetchStudioManifest(options.manifestUrl));
|
|
2002
2100
|
ensureCompatibleManifest(manifest);
|
|
2003
2101
|
console.log(`Latest Studio: ${manifest.version} (${manifest.platform})`);
|
|
2004
2102
|
}
|
|
@@ -2396,10 +2494,14 @@ function isPortableStudioServerManifest(manifest) {
|
|
|
2396
2494
|
const kind = String(manifest.installerKind || "").toLowerCase();
|
|
2397
2495
|
return kind === "portable-server" || manifest.artifactName.toLowerCase().endsWith(".zip");
|
|
2398
2496
|
}
|
|
2399
|
-
function portableStudioServerReleaseDir(manifest) {
|
|
2400
|
-
return join(
|
|
2497
|
+
function portableStudioServerReleaseDir(manifest, releasesDir = getDefaultPortableStudioServerReleasesDir()) {
|
|
2498
|
+
return join(releasesDir, safePathSegment(manifest.version));
|
|
2401
2499
|
}
|
|
2402
2500
|
function getDefaultPortableStudioServerReleasesDir() {
|
|
2501
|
+
const homeOverride = envString("TAPI_STUDIO_HOME");
|
|
2502
|
+
if (homeOverride) {
|
|
2503
|
+
return join(resolve(homeOverride), "server", "releases");
|
|
2504
|
+
}
|
|
2403
2505
|
if (process.platform === "win32") {
|
|
2404
2506
|
const localAppData = process.env.LOCALAPPDATA ??
|
|
2405
2507
|
(process.env.USERPROFILE ? join(process.env.USERPROFILE, "AppData", "Local") : join(homedir(), "AppData", "Local"));
|
|
@@ -2422,6 +2524,34 @@ export function findPortableStudioServerExe(releaseDir, manifest) {
|
|
|
2422
2524
|
];
|
|
2423
2525
|
return candidates.find((candidate) => existsSync(candidate));
|
|
2424
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
|
+
}
|
|
2425
2555
|
function cachedServiceArtifactName(manifest) {
|
|
2426
2556
|
const rawName = manifest.artifactName || basename(new URL(manifest.url).pathname) || `TapiService-${manifest.version}.zip`;
|
|
2427
2557
|
return rawName.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
@@ -2429,13 +2559,141 @@ function cachedServiceArtifactName(manifest) {
|
|
|
2429
2559
|
async function extractZip(zipPath, destination) {
|
|
2430
2560
|
await rm(destination, { recursive: true, force: true });
|
|
2431
2561
|
await mkdir(destination, { recursive: true });
|
|
2432
|
-
|
|
2433
|
-
"
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
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();
|
|
2439
2697
|
}
|
|
2440
2698
|
function installEventOptions(options) {
|
|
2441
2699
|
return {
|
|
@@ -2444,6 +2702,7 @@ function installEventOptions(options) {
|
|
|
2444
2702
|
manifestUrl: options.manifestUrl,
|
|
2445
2703
|
manifestUrlOverride: options.manifestUrlOverride,
|
|
2446
2704
|
cacheDir: options.cacheDir,
|
|
2705
|
+
installDir: options.installDir,
|
|
2447
2706
|
downloadOnly: options.downloadOnly,
|
|
2448
2707
|
silent: options.silent,
|
|
2449
2708
|
exePath: options.exePath,
|
|
@@ -2657,7 +2916,7 @@ Usage:
|
|
|
2657
2916
|
|
|
2658
2917
|
Options:
|
|
2659
2918
|
--channel <name> Release channel: pilot, stable, or nightly
|
|
2660
|
-
--api-base-url <url> Tapi API base URL for
|
|
2919
|
+
--api-base-url <url> Tapi API base URL for install authorization and protected downloads
|
|
2661
2920
|
--server <url> Alias for --api-base-url
|
|
2662
2921
|
--workspace <path> Search this directory for .tapi/project.json
|
|
2663
2922
|
--no-workspace Do not load .tapi/project.json
|
|
@@ -2666,6 +2925,7 @@ Options:
|
|
|
2666
2925
|
--install-token <tok> Preissued Studio install token (skips browser sign-in)
|
|
2667
2926
|
--manifest <url> Exact release manifest URL for doctor only
|
|
2668
2927
|
--cache-dir <path> Installer download cache directory
|
|
2928
|
+
--install-dir <path> Portable Studio server release directory
|
|
2669
2929
|
--download-only Download and verify without running the installer
|
|
2670
2930
|
--silent Run the NSIS installer with /S
|
|
2671
2931
|
--exe <path> Tapi Studio executable path for open/doctor
|