@tapi-dev/sdk 0.1.12 → 0.1.15
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 +8 -0
- package/dist/cli.js +270 -90
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -78,6 +78,13 @@ export declare function installedServiceVersionFromPathName(pathName: string): s
|
|
|
78
78
|
export declare function serviceNeedsInstall(status: ServiceStatus, manifest: ServiceReleaseManifest): boolean;
|
|
79
79
|
export declare function getDefaultStudioCacheDir(): string;
|
|
80
80
|
export declare function getDefaultServiceCacheDir(): string;
|
|
81
|
+
interface ServiceInstallLogPaths {
|
|
82
|
+
logPath: string;
|
|
83
|
+
resultPath: string;
|
|
84
|
+
}
|
|
85
|
+
export declare function getDefaultServiceInstallLogDir(): string;
|
|
86
|
+
export declare function serviceInstallLogPaths(version: string, now?: Date): ServiceInstallLogPaths;
|
|
87
|
+
export declare function formatServiceInstallFailure(error: unknown, paths: ServiceInstallLogPaths): Promise<string>;
|
|
81
88
|
export declare class CliWideEvent {
|
|
82
89
|
private readonly filePath;
|
|
83
90
|
private readonly startedAt;
|
|
@@ -95,6 +102,7 @@ export declare function getStudioExecutableCandidates(): string[];
|
|
|
95
102
|
export declare function validateStudioManifest(input: unknown): StudioReleaseManifest;
|
|
96
103
|
export declare function validateServiceReleaseManifest(input: unknown): ServiceReleaseManifest;
|
|
97
104
|
export declare function compareVersions(left: string, right: string): number;
|
|
105
|
+
export declare function findPortablePlaywrightBrowsersDir(exePath: string): string | undefined;
|
|
98
106
|
export declare function waitForHttpOk(url: string, timeoutMs?: number, intervalMs?: number, fetchImpl?: FetchLike): Promise<void>;
|
|
99
107
|
export declare function findPortableStudioServerExe(releaseDir: string, manifest: StudioReleaseManifest): string | undefined;
|
|
100
108
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -3,8 +3,8 @@ 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";
|
|
7
|
-
import { mkdir, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
6
|
+
import { createReadStream, createWriteStream, existsSync, readFileSync, readdirSync } from "node:fs";
|
|
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";
|
|
10
10
|
import { performance } from "node:perf_hooks";
|
|
@@ -34,6 +34,75 @@ class StudioInstallApprovalError extends Error {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
const sdkVersion = readSdkVersion();
|
|
37
|
+
const CLI_SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
|
38
|
+
function cliSpinnerEnabled() {
|
|
39
|
+
const noSpinner = (process.env.TAPI_NO_SPINNER || "").trim().toLowerCase();
|
|
40
|
+
if (["1", "true", "yes", "on"].includes(noSpinner)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const spinner = (process.env.TAPI_CLI_SPINNER || "").trim().toLowerCase();
|
|
44
|
+
if (["0", "false", "no", "off"].includes(spinner)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return Boolean(process.stderr.isTTY);
|
|
48
|
+
}
|
|
49
|
+
class CliSpinner {
|
|
50
|
+
message;
|
|
51
|
+
frameIndex = 0;
|
|
52
|
+
lastLength = 0;
|
|
53
|
+
timer;
|
|
54
|
+
constructor(message) {
|
|
55
|
+
this.message = message;
|
|
56
|
+
}
|
|
57
|
+
start() {
|
|
58
|
+
if (this.timer) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
this.render();
|
|
62
|
+
this.timer = setInterval(() => this.render(), 90);
|
|
63
|
+
}
|
|
64
|
+
succeed() {
|
|
65
|
+
this.stop("done");
|
|
66
|
+
}
|
|
67
|
+
fail() {
|
|
68
|
+
this.stop("failed");
|
|
69
|
+
}
|
|
70
|
+
render() {
|
|
71
|
+
const frame = CLI_SPINNER_FRAMES[this.frameIndex % CLI_SPINNER_FRAMES.length];
|
|
72
|
+
this.frameIndex += 1;
|
|
73
|
+
this.write(`${frame} ${this.message}`);
|
|
74
|
+
}
|
|
75
|
+
stop(status) {
|
|
76
|
+
if (this.timer) {
|
|
77
|
+
clearInterval(this.timer);
|
|
78
|
+
this.timer = undefined;
|
|
79
|
+
}
|
|
80
|
+
this.write(`${this.message} ${status}`);
|
|
81
|
+
process.stderr.write("\n");
|
|
82
|
+
this.lastLength = 0;
|
|
83
|
+
}
|
|
84
|
+
write(text) {
|
|
85
|
+
const padding = this.lastLength > text.length ? " ".repeat(this.lastLength - text.length) : "";
|
|
86
|
+
process.stderr.write(`\r${text}${padding}`);
|
|
87
|
+
this.lastLength = text.length;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function withCliSpinner(message, work) {
|
|
91
|
+
if (!cliSpinnerEnabled()) {
|
|
92
|
+
return await work();
|
|
93
|
+
}
|
|
94
|
+
const spinner = new CliSpinner(message);
|
|
95
|
+
spinner.start();
|
|
96
|
+
try {
|
|
97
|
+
const result = await work();
|
|
98
|
+
spinner.succeed();
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
spinner.fail();
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
37
106
|
export async function runCli(argv = process.argv.slice(2)) {
|
|
38
107
|
const [command, subcommand, ...rest] = argv;
|
|
39
108
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
@@ -339,7 +408,7 @@ async function describeApiOperation(args) {
|
|
|
339
408
|
apiKey: options.apiKey,
|
|
340
409
|
projectId: options.projectId,
|
|
341
410
|
});
|
|
342
|
-
const description = await client.websiteApis.describe(operation);
|
|
411
|
+
const description = await withCliSpinner(`Fetching Tapi API description for ${operation}`, () => client.websiteApis.describe(operation));
|
|
343
412
|
console.log(JSON.stringify(description, null, 2));
|
|
344
413
|
return 0;
|
|
345
414
|
}
|
|
@@ -423,7 +492,7 @@ function parseApiWorkspaceOptions(args) {
|
|
|
423
492
|
async function syncApiCatalog(args) {
|
|
424
493
|
try {
|
|
425
494
|
const options = parseApiWorkspaceOptions(args);
|
|
426
|
-
const catalog = await fetchApiCatalog(options);
|
|
495
|
+
const catalog = await withCliSpinner("Fetching Tapi API catalog", () => fetchApiCatalog(options));
|
|
427
496
|
await writeJsonFile(options.catalogPath, catalog);
|
|
428
497
|
console.log(`Synced Tapi API catalog: ${options.catalogPath}`);
|
|
429
498
|
return 0;
|
|
@@ -436,7 +505,7 @@ async function syncApiCatalog(args) {
|
|
|
436
505
|
async function generateApiClient(args) {
|
|
437
506
|
try {
|
|
438
507
|
const options = parseApiWorkspaceOptions(args);
|
|
439
|
-
const catalog = await fetchApiCatalog(options);
|
|
508
|
+
const catalog = await withCliSpinner("Fetching Tapi API catalog", () => fetchApiCatalog(options));
|
|
440
509
|
await writeJsonFile(options.catalogPath, catalog);
|
|
441
510
|
await writeTextFile(options.typescriptPath, renderGeneratedApiClient(catalog));
|
|
442
511
|
console.log(`Generated Tapi API client: ${options.typescriptPath}`);
|
|
@@ -521,9 +590,11 @@ async function syncApiTriggers(args) {
|
|
|
521
590
|
apiKey: options.apiKey,
|
|
522
591
|
projectId: options.projectId,
|
|
523
592
|
});
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
593
|
+
await withCliSpinner(`Syncing ${triggers.length} Tapi API trigger(s)`, async () => {
|
|
594
|
+
for (const trigger of triggers) {
|
|
595
|
+
await client.triggers.create(trigger);
|
|
596
|
+
}
|
|
597
|
+
});
|
|
527
598
|
console.log(`Synced ${triggers.length} Tapi API trigger(s) from ${options.configPath}.`);
|
|
528
599
|
return 0;
|
|
529
600
|
}
|
|
@@ -906,7 +977,7 @@ async function runServiceCommand(action, args = []) {
|
|
|
906
977
|
return repairService(parseStudioOptions([]));
|
|
907
978
|
}
|
|
908
979
|
const command = servicePowerShell(normalized);
|
|
909
|
-
const output = await runProcessCapture("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command]);
|
|
980
|
+
const output = await withCliSpinner(`Running tapi-service ${normalized}`, () => runProcessCapture("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command]));
|
|
910
981
|
if (output.trim()) {
|
|
911
982
|
console.log(output.trim());
|
|
912
983
|
}
|
|
@@ -918,7 +989,7 @@ async function runServiceCommand(action, args = []) {
|
|
|
918
989
|
}
|
|
919
990
|
}
|
|
920
991
|
async function repairService(options) {
|
|
921
|
-
const status = await getServiceStatus();
|
|
992
|
+
const status = await withCliSpinner("Checking Tapi Service status", () => getServiceStatus());
|
|
922
993
|
if (!status.installed) {
|
|
923
994
|
return installService(options);
|
|
924
995
|
}
|
|
@@ -964,7 +1035,7 @@ async function getServiceStatus() {
|
|
|
964
1035
|
}
|
|
965
1036
|
}
|
|
966
1037
|
async function ensureServiceReadyForStudio(options) {
|
|
967
|
-
const status = await getServiceStatus();
|
|
1038
|
+
const status = await withCliSpinner("Checking Tapi Service status", () => getServiceStatus());
|
|
968
1039
|
if (!status.installed) {
|
|
969
1040
|
console.log("Tapi Service is required and is not installed. Installing it now...");
|
|
970
1041
|
const code = await installService(options);
|
|
@@ -985,13 +1056,13 @@ async function ensureServiceReadyForStudio(options) {
|
|
|
985
1056
|
}
|
|
986
1057
|
if (status.status !== "Running") {
|
|
987
1058
|
console.log("Starting Tapi Service...");
|
|
988
|
-
const output = await runProcessCapture("powershell.exe", [
|
|
1059
|
+
const output = await withCliSpinner("Starting Tapi Service", () => runProcessCapture("powershell.exe", [
|
|
989
1060
|
"-NoProfile",
|
|
990
1061
|
"-ExecutionPolicy",
|
|
991
1062
|
"Bypass",
|
|
992
1063
|
"-Command",
|
|
993
1064
|
servicePowerShell("start"),
|
|
994
|
-
]);
|
|
1065
|
+
]));
|
|
995
1066
|
if (output.trim()) {
|
|
996
1067
|
console.log(output.trim());
|
|
997
1068
|
}
|
|
@@ -1009,7 +1080,7 @@ async function fetchServiceManifestForEnsure(options) {
|
|
|
1009
1080
|
apiBaseUrl: options.apiBaseUrl,
|
|
1010
1081
|
channel: options.channel,
|
|
1011
1082
|
});
|
|
1012
|
-
installToken = await obtainStudioInstallToken(options, event);
|
|
1083
|
+
installToken = await withCliSpinner("Checking Tapi Service install approval", () => obtainStudioInstallToken(options, event));
|
|
1013
1084
|
await event.phase("auth.install_token.request.success", {
|
|
1014
1085
|
apiBaseUrl: options.apiBaseUrl,
|
|
1015
1086
|
channel: options.channel,
|
|
@@ -1019,7 +1090,7 @@ async function fetchServiceManifestForEnsure(options) {
|
|
|
1019
1090
|
apiBaseUrl: options.apiBaseUrl,
|
|
1020
1091
|
channel: options.channel,
|
|
1021
1092
|
});
|
|
1022
|
-
const manifest = await fetchProtectedServiceManifest(options.apiBaseUrl, options.channel, installToken);
|
|
1093
|
+
const manifest = await withCliSpinner(`Fetching Tapi Service ${options.channel} manifest`, () => fetchProtectedServiceManifest(options.apiBaseUrl, options.channel, installToken));
|
|
1023
1094
|
ensureCompatibleServiceManifest(manifest);
|
|
1024
1095
|
await event.finish(true, {
|
|
1025
1096
|
manifest: serviceManifestEventSummary(manifest),
|
|
@@ -1079,23 +1150,21 @@ async function installService(options) {
|
|
|
1079
1150
|
ensureWindowsHost();
|
|
1080
1151
|
let installToken = options.installToken?.trim();
|
|
1081
1152
|
if (!installToken) {
|
|
1082
|
-
console.log("Checking Tapi Service install approval...");
|
|
1083
1153
|
await event.phase("auth.install_token.request.start", {
|
|
1084
1154
|
apiBaseUrl: options.apiBaseUrl,
|
|
1085
1155
|
channel: options.channel,
|
|
1086
1156
|
});
|
|
1087
|
-
installToken = await obtainStudioInstallToken(options, event);
|
|
1157
|
+
installToken = await withCliSpinner("Checking Tapi Service install approval", () => obtainStudioInstallToken(options, event));
|
|
1088
1158
|
await event.phase("auth.install_token.request.success", {
|
|
1089
1159
|
apiBaseUrl: options.apiBaseUrl,
|
|
1090
1160
|
channel: options.channel,
|
|
1091
1161
|
});
|
|
1092
1162
|
}
|
|
1093
|
-
console.log(`Fetching Tapi Service ${options.channel} manifest...`);
|
|
1094
1163
|
await event.phase("service_manifest.fetch.start", {
|
|
1095
1164
|
apiBaseUrl: options.apiBaseUrl,
|
|
1096
1165
|
channel: options.channel,
|
|
1097
1166
|
});
|
|
1098
|
-
const manifest = await fetchProtectedServiceManifest(options.apiBaseUrl, options.channel, installToken);
|
|
1167
|
+
const manifest = await withCliSpinner(`Fetching Tapi Service ${options.channel} manifest`, () => fetchProtectedServiceManifest(options.apiBaseUrl, options.channel, installToken));
|
|
1099
1168
|
await event.phase("service_manifest.fetch.success", {
|
|
1100
1169
|
manifest: serviceManifestEventSummary(manifest),
|
|
1101
1170
|
});
|
|
@@ -1112,13 +1181,12 @@ async function installService(options) {
|
|
|
1112
1181
|
await event.phase("cache.hit", { zipPath });
|
|
1113
1182
|
}
|
|
1114
1183
|
else {
|
|
1115
|
-
console.log(`Downloading Tapi Service ${manifest.version}...`);
|
|
1116
1184
|
await event.phase("download.start", {
|
|
1117
1185
|
url: manifest.url,
|
|
1118
1186
|
destination: zipPath,
|
|
1119
1187
|
expectedSha256: manifest.sha256,
|
|
1120
1188
|
});
|
|
1121
|
-
await downloadAndVerify(manifest.url, zipPath, manifest.sha256, "Tapi Service artifact");
|
|
1189
|
+
await withCliSpinner(`Downloading Tapi Service ${manifest.version}`, () => downloadAndVerify(manifest.url, zipPath, manifest.sha256, "Tapi Service artifact"));
|
|
1122
1190
|
await event.phase("download.verified", {
|
|
1123
1191
|
zipPath,
|
|
1124
1192
|
expectedSha256: manifest.sha256,
|
|
@@ -1133,7 +1201,7 @@ async function installService(options) {
|
|
|
1133
1201
|
return 0;
|
|
1134
1202
|
}
|
|
1135
1203
|
const releaseDir = join(releasesDir, safePathSegment(manifest.version));
|
|
1136
|
-
await extractZip(zipPath, releaseDir);
|
|
1204
|
+
await withCliSpinner(`Extracting Tapi Service ${manifest.version}`, () => extractZip(zipPath, releaseDir));
|
|
1137
1205
|
const installer = join(releaseDir, manifest.installer || "install_tapi_service.ps1");
|
|
1138
1206
|
const hostExe = join(releaseDir, manifest.serviceHostExecutable || "tapi-service-host.exe");
|
|
1139
1207
|
if (!existsSync(installer)) {
|
|
@@ -1142,23 +1210,35 @@ async function installService(options) {
|
|
|
1142
1210
|
if (!existsSync(hostExe)) {
|
|
1143
1211
|
throw new Error(`Tapi Service host executable was not found after extraction: ${hostExe}`);
|
|
1144
1212
|
}
|
|
1145
|
-
|
|
1213
|
+
const installLog = serviceInstallLogPaths(manifest.version);
|
|
1214
|
+
await mkdir(dirname(installLog.logPath), { recursive: true });
|
|
1146
1215
|
await event.phase("service_installer.start", {
|
|
1147
1216
|
installer,
|
|
1148
1217
|
hostExe,
|
|
1149
1218
|
releaseDir,
|
|
1219
|
+
logPath: installLog.logPath,
|
|
1220
|
+
resultPath: installLog.resultPath,
|
|
1150
1221
|
});
|
|
1151
|
-
|
|
1152
|
-
"
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1222
|
+
try {
|
|
1223
|
+
await withCliSpinner(`Installing Tapi Service ${manifest.version}`, () => runProcess("powershell.exe", [
|
|
1224
|
+
"-NoProfile",
|
|
1225
|
+
"-ExecutionPolicy",
|
|
1226
|
+
"Bypass",
|
|
1227
|
+
"-File",
|
|
1228
|
+
installer,
|
|
1229
|
+
"-ExecutablePath",
|
|
1230
|
+
hostExe,
|
|
1231
|
+
"-WorkingDirectory",
|
|
1232
|
+
releaseDir,
|
|
1233
|
+
"-LogPath",
|
|
1234
|
+
installLog.logPath,
|
|
1235
|
+
"-ResultPath",
|
|
1236
|
+
installLog.resultPath,
|
|
1237
|
+
]));
|
|
1238
|
+
}
|
|
1239
|
+
catch (error) {
|
|
1240
|
+
throw new Error(await formatServiceInstallFailure(error, installLog), { cause: error });
|
|
1241
|
+
}
|
|
1162
1242
|
await event.finish(true, {
|
|
1163
1243
|
releaseDir,
|
|
1164
1244
|
version: manifest.version,
|
|
@@ -1183,42 +1263,45 @@ async function publishLocalApis(args) {
|
|
|
1183
1263
|
if (apis.length === 0) {
|
|
1184
1264
|
throw new Error("No local generated APIs found under .tapi/apis.");
|
|
1185
1265
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
}
|
|
1192
|
-
await putJson(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/v1/sitemaps/${encodeURIComponent(site)}?project=${encodeURIComponent(options.projectId)}`, { site_map: sitemap.siteMap }, options.apiKey, options.projectId);
|
|
1193
|
-
sitemapCount += 1;
|
|
1194
|
-
}
|
|
1195
|
-
let contracts = 0;
|
|
1196
|
-
let requests = 0;
|
|
1197
|
-
for (const api of apis) {
|
|
1198
|
-
const apiId = String(api.id || api.name || "").trim();
|
|
1199
|
-
if (!apiId) {
|
|
1200
|
-
continue;
|
|
1201
|
-
}
|
|
1202
|
-
const site = String(api.site || "").trim();
|
|
1203
|
-
await putJson(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/v1/generated-apis/contracts/${encodeURIComponent(apiId)}?project=${encodeURIComponent(options.projectId)}`, { generated_api: api }, options.apiKey, options.projectId);
|
|
1204
|
-
contracts += 1;
|
|
1205
|
-
const apiRequests = api.requests && typeof api.requests === "object" ? Object.values(api.requests) : [];
|
|
1206
|
-
for (const request of apiRequests) {
|
|
1207
|
-
if (!isRecord(request)) {
|
|
1266
|
+
const { sitemapCount, contracts, requests, releaseVersion } = await withCliSpinner("Publishing local Tapi APIs", async () => {
|
|
1267
|
+
let sitemapCount = 0;
|
|
1268
|
+
for (const sitemap of sitemaps) {
|
|
1269
|
+
const site = String(sitemap.site || "").trim();
|
|
1270
|
+
if (!site || !isRecord(sitemap.siteMap)) {
|
|
1208
1271
|
continue;
|
|
1209
1272
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1273
|
+
await putJson(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/v1/sitemaps/${encodeURIComponent(site)}?project=${encodeURIComponent(options.projectId)}`, { site_map: sitemap.siteMap }, options.apiKey, options.projectId);
|
|
1274
|
+
sitemapCount += 1;
|
|
1275
|
+
}
|
|
1276
|
+
let contracts = 0;
|
|
1277
|
+
let requests = 0;
|
|
1278
|
+
for (const api of apis) {
|
|
1279
|
+
const apiId = String(api.id || api.name || "").trim();
|
|
1280
|
+
if (!apiId) {
|
|
1213
1281
|
continue;
|
|
1214
1282
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1283
|
+
const site = String(api.site || "").trim();
|
|
1284
|
+
await putJson(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/v1/generated-apis/contracts/${encodeURIComponent(apiId)}?project=${encodeURIComponent(options.projectId)}`, { generated_api: api }, options.apiKey, options.projectId);
|
|
1285
|
+
contracts += 1;
|
|
1286
|
+
const apiRequests = api.requests && typeof api.requests === "object" ? Object.values(api.requests) : [];
|
|
1287
|
+
for (const request of apiRequests) {
|
|
1288
|
+
if (!isRecord(request)) {
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
const requestId = String(request.id || request.key || "").trim();
|
|
1292
|
+
const status = String(request.status || "").trim();
|
|
1293
|
+
if (!requestId || !["ready", "published"].includes(status)) {
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
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);
|
|
1297
|
+
requests += 1;
|
|
1298
|
+
}
|
|
1217
1299
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1300
|
+
const releaseResponse = await postJson(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/v1/generated-apis/releases?project=${encodeURIComponent(options.projectId)}`, { environment: "production" }, options.apiKey, options.projectId);
|
|
1301
|
+
const release = isRecord(releaseResponse) && isRecord(releaseResponse.release) ? releaseResponse.release : {};
|
|
1302
|
+
const releaseVersion = typeof release.version === "string" ? release.version : "unknown";
|
|
1303
|
+
return { sitemapCount, contracts, requests, releaseVersion };
|
|
1304
|
+
});
|
|
1222
1305
|
console.log(`Published ${requests} API request(s) from ${contracts} contract(s), synced ${sitemapCount} sitemap(s), activated release ${releaseVersion}.`);
|
|
1223
1306
|
return 0;
|
|
1224
1307
|
}
|
|
@@ -1327,6 +1410,76 @@ export function getDefaultServiceCacheDir() {
|
|
|
1327
1410
|
}
|
|
1328
1411
|
return join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "tapi", "service");
|
|
1329
1412
|
}
|
|
1413
|
+
export function getDefaultServiceInstallLogDir() {
|
|
1414
|
+
return join(getDefaultServiceCacheDir(), "logs");
|
|
1415
|
+
}
|
|
1416
|
+
export function serviceInstallLogPaths(version, now = new Date()) {
|
|
1417
|
+
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
|
1418
|
+
const safeVersion = safePathSegment(version);
|
|
1419
|
+
return {
|
|
1420
|
+
logPath: join(getDefaultServiceInstallLogDir(), `install-${safeVersion}-${stamp}.log`),
|
|
1421
|
+
resultPath: join(getDefaultServiceInstallLogDir(), `install-${safeVersion}-${stamp}.json`),
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
export async function formatServiceInstallFailure(error, paths) {
|
|
1425
|
+
const lines = [
|
|
1426
|
+
`Tapi Service installer failed: ${formatError(error)}`,
|
|
1427
|
+
`Install log: ${paths.logPath}`,
|
|
1428
|
+
`Install result: ${paths.resultPath}`,
|
|
1429
|
+
];
|
|
1430
|
+
const result = await readServiceInstallResult(paths.resultPath);
|
|
1431
|
+
if (result) {
|
|
1432
|
+
const phase = installerStringField(result.phase);
|
|
1433
|
+
const installError = installerStringField(result.error);
|
|
1434
|
+
const exitCode = result.exitCode !== undefined ? String(result.exitCode) : "";
|
|
1435
|
+
if (phase || exitCode) {
|
|
1436
|
+
lines.push(`Installer result: phase=${phase || "unknown"} exitCode=${exitCode || "unknown"}`);
|
|
1437
|
+
}
|
|
1438
|
+
if (installError) {
|
|
1439
|
+
lines.push(`Installer error: ${installError}`);
|
|
1440
|
+
}
|
|
1441
|
+
const resultLogPath = installerStringField(result.logPath);
|
|
1442
|
+
if (resultLogPath && resultLogPath !== paths.logPath) {
|
|
1443
|
+
lines.push(`Installer-reported log: ${resultLogPath}`);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
const logText = await readTextFileIfExists(paths.logPath);
|
|
1447
|
+
if (logText?.trim()) {
|
|
1448
|
+
const tail = tailLines(logText, 30);
|
|
1449
|
+
if (tail.length > 0) {
|
|
1450
|
+
lines.push("Last installer log lines:");
|
|
1451
|
+
lines.push(...tail.map((line) => ` ${line}`));
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return lines.join("\n");
|
|
1455
|
+
}
|
|
1456
|
+
async function readServiceInstallResult(path) {
|
|
1457
|
+
const text = await readTextFileIfExists(path);
|
|
1458
|
+
if (!text) {
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
try {
|
|
1462
|
+
const parsed = JSON.parse(text);
|
|
1463
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
1464
|
+
}
|
|
1465
|
+
catch {
|
|
1466
|
+
return null;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
async function readTextFileIfExists(path) {
|
|
1470
|
+
try {
|
|
1471
|
+
return await readFile(path, "utf8");
|
|
1472
|
+
}
|
|
1473
|
+
catch {
|
|
1474
|
+
return null;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
function installerStringField(value) {
|
|
1478
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
1479
|
+
}
|
|
1480
|
+
function tailLines(text, count) {
|
|
1481
|
+
return text.split(/\r?\n/).filter((line) => line.trim()).slice(-count);
|
|
1482
|
+
}
|
|
1330
1483
|
export class CliWideEvent {
|
|
1331
1484
|
filePath;
|
|
1332
1485
|
startedAt = performance.now();
|
|
@@ -1571,23 +1724,21 @@ async function installStudio(options) {
|
|
|
1571
1724
|
});
|
|
1572
1725
|
}
|
|
1573
1726
|
else {
|
|
1574
|
-
console.log("Checking Studio install approval...");
|
|
1575
1727
|
await event.phase("auth.install_token.request.start", {
|
|
1576
1728
|
apiBaseUrl: options.apiBaseUrl,
|
|
1577
1729
|
channel: options.channel,
|
|
1578
1730
|
});
|
|
1579
|
-
installToken = await obtainStudioInstallToken(options, event);
|
|
1731
|
+
installToken = await withCliSpinner("Checking Studio install approval", () => obtainStudioInstallToken(options, event));
|
|
1580
1732
|
await event.phase("auth.install_token.request.success", {
|
|
1581
1733
|
apiBaseUrl: options.apiBaseUrl,
|
|
1582
1734
|
channel: options.channel,
|
|
1583
1735
|
});
|
|
1584
1736
|
}
|
|
1585
|
-
console.log(`Fetching Tapi Studio ${options.channel} manifest...`);
|
|
1586
1737
|
await event.phase("manifest.fetch.start", {
|
|
1587
1738
|
apiBaseUrl: options.apiBaseUrl,
|
|
1588
1739
|
channel: options.channel,
|
|
1589
1740
|
});
|
|
1590
|
-
const manifest = await fetchProtectedStudioManifest(options.apiBaseUrl, options.channel, installToken);
|
|
1741
|
+
const manifest = await withCliSpinner(`Fetching Tapi Studio ${options.channel} manifest`, () => fetchProtectedStudioManifest(options.apiBaseUrl, options.channel, installToken));
|
|
1591
1742
|
await event.phase("manifest.fetch.success", {
|
|
1592
1743
|
manifest: manifestEventSummary(manifest),
|
|
1593
1744
|
});
|
|
@@ -1619,13 +1770,12 @@ async function installStudio(options) {
|
|
|
1619
1770
|
}
|
|
1620
1771
|
else {
|
|
1621
1772
|
await event.phase("cache.miss", { installerPath });
|
|
1622
|
-
console.log(`Downloading Tapi Studio ${manifest.version}...`);
|
|
1623
1773
|
await event.phase("download.start", {
|
|
1624
1774
|
url: manifest.url,
|
|
1625
1775
|
destination: installerPath,
|
|
1626
1776
|
expectedSha256: manifest.sha256,
|
|
1627
1777
|
});
|
|
1628
|
-
await downloadAndVerify(manifest.url, installerPath, manifest.sha256);
|
|
1778
|
+
await withCliSpinner(`Downloading Tapi Studio ${manifest.version}`, () => downloadAndVerify(manifest.url, installerPath, manifest.sha256));
|
|
1629
1779
|
await event.phase("download.verified", {
|
|
1630
1780
|
installerPath,
|
|
1631
1781
|
expectedSha256: manifest.sha256,
|
|
@@ -1640,12 +1790,11 @@ async function installStudio(options) {
|
|
|
1640
1790
|
return 0;
|
|
1641
1791
|
}
|
|
1642
1792
|
const installerArgs = options.silent ? ["/S"] : [];
|
|
1643
|
-
console.log(`Starting Tapi Studio installer: ${installerPath}`);
|
|
1644
1793
|
await event.phase("installer.start", {
|
|
1645
1794
|
installerPath,
|
|
1646
1795
|
args: installerArgs,
|
|
1647
1796
|
});
|
|
1648
|
-
await runProcess(installerPath, installerArgs);
|
|
1797
|
+
await withCliSpinner("Starting Tapi Studio installer", () => runProcess(installerPath, installerArgs));
|
|
1649
1798
|
console.log("Tapi Studio installer finished.");
|
|
1650
1799
|
await event.finish(true, {
|
|
1651
1800
|
installerPath,
|
|
@@ -1712,7 +1861,7 @@ async function openStudio(options) {
|
|
|
1712
1861
|
if (!options.exePath) {
|
|
1713
1862
|
const portable = await ensurePortableStudioServer(options);
|
|
1714
1863
|
if (portable) {
|
|
1715
|
-
await launchPortableStudioServer(portable.exePath, options);
|
|
1864
|
+
await launchPortableStudioServer(portable.exePath, options, portable.manifest);
|
|
1716
1865
|
return 0;
|
|
1717
1866
|
}
|
|
1718
1867
|
}
|
|
@@ -1758,7 +1907,7 @@ async function ensurePortableStudioServer(options) {
|
|
|
1758
1907
|
apiBaseUrl: options.apiBaseUrl,
|
|
1759
1908
|
channel: options.channel,
|
|
1760
1909
|
});
|
|
1761
|
-
installToken = await obtainStudioInstallToken(options, event);
|
|
1910
|
+
installToken = await withCliSpinner("Checking Studio install approval", () => obtainStudioInstallToken(options, event));
|
|
1762
1911
|
await event.phase("auth.install_token.request.success", {
|
|
1763
1912
|
apiBaseUrl: options.apiBaseUrl,
|
|
1764
1913
|
channel: options.channel,
|
|
@@ -1768,7 +1917,7 @@ async function ensurePortableStudioServer(options) {
|
|
|
1768
1917
|
apiBaseUrl: options.apiBaseUrl,
|
|
1769
1918
|
channel: options.channel,
|
|
1770
1919
|
});
|
|
1771
|
-
const manifest = await fetchProtectedStudioManifest(options.apiBaseUrl, options.channel, installToken);
|
|
1920
|
+
const manifest = await withCliSpinner(`Fetching Tapi Studio ${options.channel} manifest`, () => fetchProtectedStudioManifest(options.apiBaseUrl, options.channel, installToken));
|
|
1772
1921
|
ensureCompatibleManifest(manifest);
|
|
1773
1922
|
await event.phase("manifest.fetch.success", {
|
|
1774
1923
|
manifest: manifestEventSummary(manifest),
|
|
@@ -1810,13 +1959,12 @@ async function installPortableStudioServer(options, manifest, event) {
|
|
|
1810
1959
|
await event.phase("cache.hit", { artifactPath });
|
|
1811
1960
|
}
|
|
1812
1961
|
else {
|
|
1813
|
-
console.log(`Downloading Tapi Studio server ${manifest.version}...`);
|
|
1814
1962
|
await event.phase("download.start", {
|
|
1815
1963
|
url: manifest.url,
|
|
1816
1964
|
destination: artifactPath,
|
|
1817
1965
|
expectedSha256: manifest.sha256,
|
|
1818
1966
|
});
|
|
1819
|
-
await downloadAndVerify(manifest.url, artifactPath, manifest.sha256, "Tapi Studio server artifact");
|
|
1967
|
+
await withCliSpinner(`Downloading Tapi Studio server ${manifest.version}`, () => downloadAndVerify(manifest.url, artifactPath, manifest.sha256, "Tapi Studio server artifact"));
|
|
1820
1968
|
await event.phase("download.verified", {
|
|
1821
1969
|
artifactPath,
|
|
1822
1970
|
expectedSha256: manifest.sha256,
|
|
@@ -1827,7 +1975,7 @@ async function installPortableStudioServer(options, manifest, event) {
|
|
|
1827
1975
|
return artifactPath;
|
|
1828
1976
|
}
|
|
1829
1977
|
const releaseDir = portableStudioServerReleaseDir(manifest);
|
|
1830
|
-
await extractZip(artifactPath, releaseDir);
|
|
1978
|
+
await withCliSpinner(`Extracting Tapi Studio server ${manifest.version}`, () => extractZip(artifactPath, releaseDir));
|
|
1831
1979
|
const exePath = findPortableStudioServerExe(releaseDir, manifest);
|
|
1832
1980
|
if (!exePath) {
|
|
1833
1981
|
throw new Error(`Tapi Studio server executable was not found after extraction: ${releaseDir}`);
|
|
@@ -1835,7 +1983,7 @@ async function installPortableStudioServer(options, manifest, event) {
|
|
|
1835
1983
|
console.log(`Installed Tapi Studio server ${manifest.version}: ${releaseDir}`);
|
|
1836
1984
|
return exePath;
|
|
1837
1985
|
}
|
|
1838
|
-
async function launchPortableStudioServer(exePath, options) {
|
|
1986
|
+
async function launchPortableStudioServer(exePath, options, manifest) {
|
|
1839
1987
|
const host = "127.0.0.1";
|
|
1840
1988
|
const port = await chooseStudioPort();
|
|
1841
1989
|
const env = buildStudioLaunchEnv(options);
|
|
@@ -1847,17 +1995,49 @@ async function launchPortableStudioServer(exePath, options) {
|
|
|
1847
1995
|
env.TAPI_STUDIO_HOST = host;
|
|
1848
1996
|
env.TAPI_STUDIO_PORT = String(port);
|
|
1849
1997
|
env.TAPI_STUDIO_SERVER_PORT = String(port);
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1998
|
+
env.TAPI_STUDIO_SERVER_VERSION = manifest.version;
|
|
1999
|
+
env.TAPI_STUDIO_SERVER_COMMIT = manifest.commit || "";
|
|
2000
|
+
env.TAPI_STUDIO_SERVER_COMMIT_SHORT = manifest.commitShort || manifest.version;
|
|
2001
|
+
const playwrightBrowsersPath = findPortablePlaywrightBrowsersDir(exePath);
|
|
2002
|
+
if (playwrightBrowsersPath) {
|
|
2003
|
+
env.PLAYWRIGHT_BROWSERS_PATH = playwrightBrowsersPath;
|
|
2004
|
+
}
|
|
1857
2005
|
const url = `http://${host}:${port}`;
|
|
1858
|
-
await
|
|
2006
|
+
await withCliSpinner("Starting Tapi Studio", async () => {
|
|
2007
|
+
const child = spawn(exePath, [], {
|
|
2008
|
+
detached: true,
|
|
2009
|
+
env,
|
|
2010
|
+
stdio: "ignore",
|
|
2011
|
+
windowsHide: true,
|
|
2012
|
+
});
|
|
2013
|
+
child.unref();
|
|
2014
|
+
await waitForHttpOk(`${url}/healthz`, PORTABLE_STUDIO_SERVER_READY_TIMEOUT_MS, PORTABLE_STUDIO_SERVER_READY_INTERVAL_MS);
|
|
2015
|
+
});
|
|
1859
2016
|
openBrowser(url);
|
|
1860
|
-
console.log(`Opened Tapi Studio: ${url}`);
|
|
2017
|
+
console.log(`Opened Tapi Studio ${manifest.version}: ${url}`);
|
|
2018
|
+
}
|
|
2019
|
+
export function findPortablePlaywrightBrowsersDir(exePath) {
|
|
2020
|
+
const exeDir = dirname(exePath);
|
|
2021
|
+
const releaseDir = basename(exeDir).toLowerCase() === "tapi-studio-server"
|
|
2022
|
+
? dirname(exeDir)
|
|
2023
|
+
: exeDir;
|
|
2024
|
+
const candidates = [
|
|
2025
|
+
join(releaseDir, "pw"),
|
|
2026
|
+
join(releaseDir, "playwright-browsers"),
|
|
2027
|
+
join(releaseDir, "sidecars", "pw"),
|
|
2028
|
+
join(exeDir, "pw"),
|
|
2029
|
+
join(exeDir, "playwright-browsers"),
|
|
2030
|
+
];
|
|
2031
|
+
return candidates.find(isPlaywrightBrowsersDir);
|
|
2032
|
+
}
|
|
2033
|
+
function isPlaywrightBrowsersDir(path) {
|
|
2034
|
+
try {
|
|
2035
|
+
return existsSync(path)
|
|
2036
|
+
&& readdirSync(path, { withFileTypes: true }).some((entry) => entry.isDirectory() && entry.name.startsWith("chromium_headless_shell-"));
|
|
2037
|
+
}
|
|
2038
|
+
catch {
|
|
2039
|
+
return false;
|
|
2040
|
+
}
|
|
1861
2041
|
}
|
|
1862
2042
|
export async function waitForHttpOk(url, timeoutMs = PORTABLE_STUDIO_SERVER_READY_TIMEOUT_MS, intervalMs = PORTABLE_STUDIO_SERVER_READY_INTERVAL_MS, fetchImpl = fetch) {
|
|
1863
2043
|
const startedAt = performance.now();
|
|
@@ -1915,7 +2095,7 @@ async function runDoctor(options) {
|
|
|
1915
2095
|
const exePath = options.exePath ?? getStudioExecutableCandidates().find((candidate) => existsSync(candidate));
|
|
1916
2096
|
console.log(`Studio executable: ${exePath && existsSync(exePath) ? exePath : "not found"}`);
|
|
1917
2097
|
try {
|
|
1918
|
-
const manifest = await fetchStudioManifest(options.manifestUrl);
|
|
2098
|
+
const manifest = await withCliSpinner("Fetching latest Tapi Studio manifest", () => fetchStudioManifest(options.manifestUrl));
|
|
1919
2099
|
ensureCompatibleManifest(manifest);
|
|
1920
2100
|
console.log(`Latest Studio: ${manifest.version} (${manifest.platform})`);
|
|
1921
2101
|
}
|