@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 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
- for (const trigger of triggers) {
525
- await client.triggers.create(trigger);
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
- console.log(`Installing Tapi Service ${manifest.version}...`);
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
- await runProcess("powershell.exe", [
1152
- "-NoProfile",
1153
- "-ExecutionPolicy",
1154
- "Bypass",
1155
- "-File",
1156
- installer,
1157
- "-ExecutablePath",
1158
- hostExe,
1159
- "-WorkingDirectory",
1160
- releaseDir,
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
- let sitemapCount = 0;
1187
- for (const sitemap of sitemaps) {
1188
- const site = String(sitemap.site || "").trim();
1189
- if (!site || !isRecord(sitemap.siteMap)) {
1190
- continue;
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
- const requestId = String(request.id || request.key || "").trim();
1211
- const status = String(request.status || "").trim();
1212
- if (!requestId || !["ready", "published"].includes(status)) {
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
- 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);
1216
- requests += 1;
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
- const releaseResponse = await postJson(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/v1/generated-apis/releases?project=${encodeURIComponent(options.projectId)}`, { environment: "production" }, options.apiKey, options.projectId);
1220
- const release = isRecord(releaseResponse) && isRecord(releaseResponse.release) ? releaseResponse.release : {};
1221
- const releaseVersion = typeof release.version === "string" ? release.version : "unknown";
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
- const child = spawn(exePath, [], {
1851
- detached: true,
1852
- env,
1853
- stdio: "ignore",
1854
- windowsHide: true,
1855
- });
1856
- child.unref();
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 waitForHttpOk(`${url}/healthz`, PORTABLE_STUDIO_SERVER_READY_TIMEOUT_MS, PORTABLE_STUDIO_SERVER_READY_INTERVAL_MS);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tapi-dev/sdk",
3
- "version": "0.1.12",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",