@tapi-dev/sdk 0.1.13 → 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
@@ -102,6 +102,7 @@ export declare function getStudioExecutableCandidates(): string[];
102
102
  export declare function validateStudioManifest(input: unknown): StudioReleaseManifest;
103
103
  export declare function validateServiceReleaseManifest(input: unknown): ServiceReleaseManifest;
104
104
  export declare function compareVersions(left: string, right: string): number;
105
+ export declare function findPortablePlaywrightBrowsersDir(exePath: string): string | undefined;
105
106
  export declare function waitForHttpOk(url: string, timeoutMs?: number, intervalMs?: number, fetchImpl?: FetchLike): Promise<void>;
106
107
  export declare function findPortableStudioServerExe(releaseDir: string, manifest: StudioReleaseManifest): string | undefined;
107
108
  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";
@@ -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)) {
@@ -1144,7 +1212,6 @@ async function installService(options) {
1144
1212
  }
1145
1213
  const installLog = serviceInstallLogPaths(manifest.version);
1146
1214
  await mkdir(dirname(installLog.logPath), { recursive: true });
1147
- console.log(`Installing Tapi Service ${manifest.version}...`);
1148
1215
  await event.phase("service_installer.start", {
1149
1216
  installer,
1150
1217
  hostExe,
@@ -1153,7 +1220,7 @@ async function installService(options) {
1153
1220
  resultPath: installLog.resultPath,
1154
1221
  });
1155
1222
  try {
1156
- await runProcess("powershell.exe", [
1223
+ await withCliSpinner(`Installing Tapi Service ${manifest.version}`, () => runProcess("powershell.exe", [
1157
1224
  "-NoProfile",
1158
1225
  "-ExecutionPolicy",
1159
1226
  "Bypass",
@@ -1167,7 +1234,7 @@ async function installService(options) {
1167
1234
  installLog.logPath,
1168
1235
  "-ResultPath",
1169
1236
  installLog.resultPath,
1170
- ]);
1237
+ ]));
1171
1238
  }
1172
1239
  catch (error) {
1173
1240
  throw new Error(await formatServiceInstallFailure(error, installLog), { cause: error });
@@ -1196,42 +1263,45 @@ async function publishLocalApis(args) {
1196
1263
  if (apis.length === 0) {
1197
1264
  throw new Error("No local generated APIs found under .tapi/apis.");
1198
1265
  }
1199
- let sitemapCount = 0;
1200
- for (const sitemap of sitemaps) {
1201
- const site = String(sitemap.site || "").trim();
1202
- if (!site || !isRecord(sitemap.siteMap)) {
1203
- continue;
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)) {
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)) {
1221
1271
  continue;
1222
1272
  }
1223
- const requestId = String(request.id || request.key || "").trim();
1224
- const status = String(request.status || "").trim();
1225
- 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) {
1226
1281
  continue;
1227
1282
  }
1228
- 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);
1229
- 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
+ }
1230
1299
  }
1231
- }
1232
- const releaseResponse = await postJson(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/v1/generated-apis/releases?project=${encodeURIComponent(options.projectId)}`, { environment: "production" }, options.apiKey, options.projectId);
1233
- const release = isRecord(releaseResponse) && isRecord(releaseResponse.release) ? releaseResponse.release : {};
1234
- 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
+ });
1235
1305
  console.log(`Published ${requests} API request(s) from ${contracts} contract(s), synced ${sitemapCount} sitemap(s), activated release ${releaseVersion}.`);
1236
1306
  return 0;
1237
1307
  }
@@ -1654,23 +1724,21 @@ async function installStudio(options) {
1654
1724
  });
1655
1725
  }
1656
1726
  else {
1657
- console.log("Checking Studio install approval...");
1658
1727
  await event.phase("auth.install_token.request.start", {
1659
1728
  apiBaseUrl: options.apiBaseUrl,
1660
1729
  channel: options.channel,
1661
1730
  });
1662
- installToken = await obtainStudioInstallToken(options, event);
1731
+ installToken = await withCliSpinner("Checking Studio install approval", () => obtainStudioInstallToken(options, event));
1663
1732
  await event.phase("auth.install_token.request.success", {
1664
1733
  apiBaseUrl: options.apiBaseUrl,
1665
1734
  channel: options.channel,
1666
1735
  });
1667
1736
  }
1668
- console.log(`Fetching Tapi Studio ${options.channel} manifest...`);
1669
1737
  await event.phase("manifest.fetch.start", {
1670
1738
  apiBaseUrl: options.apiBaseUrl,
1671
1739
  channel: options.channel,
1672
1740
  });
1673
- 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));
1674
1742
  await event.phase("manifest.fetch.success", {
1675
1743
  manifest: manifestEventSummary(manifest),
1676
1744
  });
@@ -1702,13 +1770,12 @@ async function installStudio(options) {
1702
1770
  }
1703
1771
  else {
1704
1772
  await event.phase("cache.miss", { installerPath });
1705
- console.log(`Downloading Tapi Studio ${manifest.version}...`);
1706
1773
  await event.phase("download.start", {
1707
1774
  url: manifest.url,
1708
1775
  destination: installerPath,
1709
1776
  expectedSha256: manifest.sha256,
1710
1777
  });
1711
- await downloadAndVerify(manifest.url, installerPath, manifest.sha256);
1778
+ await withCliSpinner(`Downloading Tapi Studio ${manifest.version}`, () => downloadAndVerify(manifest.url, installerPath, manifest.sha256));
1712
1779
  await event.phase("download.verified", {
1713
1780
  installerPath,
1714
1781
  expectedSha256: manifest.sha256,
@@ -1723,12 +1790,11 @@ async function installStudio(options) {
1723
1790
  return 0;
1724
1791
  }
1725
1792
  const installerArgs = options.silent ? ["/S"] : [];
1726
- console.log(`Starting Tapi Studio installer: ${installerPath}`);
1727
1793
  await event.phase("installer.start", {
1728
1794
  installerPath,
1729
1795
  args: installerArgs,
1730
1796
  });
1731
- await runProcess(installerPath, installerArgs);
1797
+ await withCliSpinner("Starting Tapi Studio installer", () => runProcess(installerPath, installerArgs));
1732
1798
  console.log("Tapi Studio installer finished.");
1733
1799
  await event.finish(true, {
1734
1800
  installerPath,
@@ -1795,7 +1861,7 @@ async function openStudio(options) {
1795
1861
  if (!options.exePath) {
1796
1862
  const portable = await ensurePortableStudioServer(options);
1797
1863
  if (portable) {
1798
- await launchPortableStudioServer(portable.exePath, options);
1864
+ await launchPortableStudioServer(portable.exePath, options, portable.manifest);
1799
1865
  return 0;
1800
1866
  }
1801
1867
  }
@@ -1841,7 +1907,7 @@ async function ensurePortableStudioServer(options) {
1841
1907
  apiBaseUrl: options.apiBaseUrl,
1842
1908
  channel: options.channel,
1843
1909
  });
1844
- installToken = await obtainStudioInstallToken(options, event);
1910
+ installToken = await withCliSpinner("Checking Studio install approval", () => obtainStudioInstallToken(options, event));
1845
1911
  await event.phase("auth.install_token.request.success", {
1846
1912
  apiBaseUrl: options.apiBaseUrl,
1847
1913
  channel: options.channel,
@@ -1851,7 +1917,7 @@ async function ensurePortableStudioServer(options) {
1851
1917
  apiBaseUrl: options.apiBaseUrl,
1852
1918
  channel: options.channel,
1853
1919
  });
1854
- 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));
1855
1921
  ensureCompatibleManifest(manifest);
1856
1922
  await event.phase("manifest.fetch.success", {
1857
1923
  manifest: manifestEventSummary(manifest),
@@ -1893,13 +1959,12 @@ async function installPortableStudioServer(options, manifest, event) {
1893
1959
  await event.phase("cache.hit", { artifactPath });
1894
1960
  }
1895
1961
  else {
1896
- console.log(`Downloading Tapi Studio server ${manifest.version}...`);
1897
1962
  await event.phase("download.start", {
1898
1963
  url: manifest.url,
1899
1964
  destination: artifactPath,
1900
1965
  expectedSha256: manifest.sha256,
1901
1966
  });
1902
- 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"));
1903
1968
  await event.phase("download.verified", {
1904
1969
  artifactPath,
1905
1970
  expectedSha256: manifest.sha256,
@@ -1910,7 +1975,7 @@ async function installPortableStudioServer(options, manifest, event) {
1910
1975
  return artifactPath;
1911
1976
  }
1912
1977
  const releaseDir = portableStudioServerReleaseDir(manifest);
1913
- await extractZip(artifactPath, releaseDir);
1978
+ await withCliSpinner(`Extracting Tapi Studio server ${manifest.version}`, () => extractZip(artifactPath, releaseDir));
1914
1979
  const exePath = findPortableStudioServerExe(releaseDir, manifest);
1915
1980
  if (!exePath) {
1916
1981
  throw new Error(`Tapi Studio server executable was not found after extraction: ${releaseDir}`);
@@ -1918,7 +1983,7 @@ async function installPortableStudioServer(options, manifest, event) {
1918
1983
  console.log(`Installed Tapi Studio server ${manifest.version}: ${releaseDir}`);
1919
1984
  return exePath;
1920
1985
  }
1921
- async function launchPortableStudioServer(exePath, options) {
1986
+ async function launchPortableStudioServer(exePath, options, manifest) {
1922
1987
  const host = "127.0.0.1";
1923
1988
  const port = await chooseStudioPort();
1924
1989
  const env = buildStudioLaunchEnv(options);
@@ -1930,17 +1995,49 @@ async function launchPortableStudioServer(exePath, options) {
1930
1995
  env.TAPI_STUDIO_HOST = host;
1931
1996
  env.TAPI_STUDIO_PORT = String(port);
1932
1997
  env.TAPI_STUDIO_SERVER_PORT = String(port);
1933
- const child = spawn(exePath, [], {
1934
- detached: true,
1935
- env,
1936
- stdio: "ignore",
1937
- windowsHide: true,
1938
- });
1939
- 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
+ }
1940
2005
  const url = `http://${host}:${port}`;
1941
- 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
+ });
1942
2016
  openBrowser(url);
1943
- 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
+ }
1944
2041
  }
1945
2042
  export async function waitForHttpOk(url, timeoutMs = PORTABLE_STUDIO_SERVER_READY_TIMEOUT_MS, intervalMs = PORTABLE_STUDIO_SERVER_READY_INTERVAL_MS, fetchImpl = fetch) {
1946
2043
  const startedAt = performance.now();
@@ -1998,7 +2095,7 @@ async function runDoctor(options) {
1998
2095
  const exePath = options.exePath ?? getStudioExecutableCandidates().find((candidate) => existsSync(candidate));
1999
2096
  console.log(`Studio executable: ${exePath && existsSync(exePath) ? exePath : "not found"}`);
2000
2097
  try {
2001
- const manifest = await fetchStudioManifest(options.manifestUrl);
2098
+ const manifest = await withCliSpinner("Fetching latest Tapi Studio manifest", () => fetchStudioManifest(options.manifestUrl));
2002
2099
  ensureCompatibleManifest(manifest);
2003
2100
  console.log(`Latest Studio: ${manifest.version} (${manifest.platform})`);
2004
2101
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tapi-dev/sdk",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",