@tapi-dev/sdk 0.1.11 → 0.1.13

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
@@ -71,12 +71,20 @@ export interface ServiceStatus {
71
71
  pathName?: string;
72
72
  installedVersion?: string;
73
73
  }
74
+ type FetchLike = typeof fetch;
74
75
  export declare function runCli(argv?: string[]): Promise<number>;
75
76
  export declare function parseStudioOptions(args: string[]): StudioCliOptions;
76
77
  export declare function installedServiceVersionFromPathName(pathName: string): string | undefined;
77
78
  export declare function serviceNeedsInstall(status: ServiceStatus, manifest: ServiceReleaseManifest): boolean;
78
79
  export declare function getDefaultStudioCacheDir(): string;
79
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>;
80
88
  export declare class CliWideEvent {
81
89
  private readonly filePath;
82
90
  private readonly startedAt;
@@ -94,5 +102,6 @@ export declare function getStudioExecutableCandidates(): string[];
94
102
  export declare function validateStudioManifest(input: unknown): StudioReleaseManifest;
95
103
  export declare function validateServiceReleaseManifest(input: unknown): ServiceReleaseManifest;
96
104
  export declare function compareVersions(left: string, right: string): number;
105
+ export declare function waitForHttpOk(url: string, timeoutMs?: number, intervalMs?: number, fetchImpl?: FetchLike): Promise<void>;
97
106
  export declare function findPortableStudioServerExe(releaseDir: string, manifest: StudioReleaseManifest): string | undefined;
98
107
  export {};
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import { createServer } from "node:http";
4
4
  import { createServer as createNetServer } from "node:net";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
6
  import { createReadStream, createWriteStream, existsSync, readFileSync } from "node:fs";
7
- import { mkdir, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
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";
@@ -20,6 +20,8 @@ const DEFAULT_FIREBASE_API_KEY = "AIzaSyCDZR8lWyVQcWYfFdNZa4vuL4IWEC0h6gE";
20
20
  const DEFAULT_CHANNEL = "pilot";
21
21
  const SUPPORTED_STUDIO_PLATFORM = "windows-x86_64";
22
22
  const DEFAULT_INSTALL_AUTH_TIMEOUT_MS = 120_000;
23
+ const PORTABLE_STUDIO_SERVER_READY_TIMEOUT_MS = 30_000;
24
+ const PORTABLE_STUDIO_SERVER_READY_INTERVAL_MS = 250;
23
25
  const MAX_WIDE_EVENT_PROCESS_ROOTS = 5;
24
26
  class StudioInstallApprovalError extends Error {
25
27
  code;
@@ -1028,10 +1030,19 @@ async function fetchServiceManifestForEnsure(options) {
1028
1030
  await event.finish(false, {
1029
1031
  error: errorDetails(error),
1030
1032
  });
1033
+ if (isProtectedServiceManifestAuthError(error)) {
1034
+ throw error;
1035
+ }
1031
1036
  console.warn(`Could not check for Tapi Service updates: ${formatError(error)}`);
1032
1037
  return null;
1033
1038
  }
1034
1039
  }
1040
+ function isProtectedServiceManifestAuthError(error) {
1041
+ const message = formatError(error);
1042
+ return (message.includes("Failed to fetch protected Tapi Service manifest: HTTP 401")
1043
+ || message.includes("Failed to fetch protected Tapi Service manifest: HTTP 403")
1044
+ || message.includes("Worker bootstrap secret required"));
1045
+ }
1035
1046
  export function installedServiceVersionFromPathName(pathName) {
1036
1047
  const normalized = String(pathName || "").replace(/\\/g, "/");
1037
1048
  const parts = normalized
@@ -1131,23 +1142,36 @@ async function installService(options) {
1131
1142
  if (!existsSync(hostExe)) {
1132
1143
  throw new Error(`Tapi Service host executable was not found after extraction: ${hostExe}`);
1133
1144
  }
1145
+ const installLog = serviceInstallLogPaths(manifest.version);
1146
+ await mkdir(dirname(installLog.logPath), { recursive: true });
1134
1147
  console.log(`Installing Tapi Service ${manifest.version}...`);
1135
1148
  await event.phase("service_installer.start", {
1136
1149
  installer,
1137
1150
  hostExe,
1138
1151
  releaseDir,
1152
+ logPath: installLog.logPath,
1153
+ resultPath: installLog.resultPath,
1139
1154
  });
1140
- await runProcess("powershell.exe", [
1141
- "-NoProfile",
1142
- "-ExecutionPolicy",
1143
- "Bypass",
1144
- "-File",
1145
- installer,
1146
- "-ExecutablePath",
1147
- hostExe,
1148
- "-WorkingDirectory",
1149
- releaseDir,
1150
- ]);
1155
+ try {
1156
+ await runProcess("powershell.exe", [
1157
+ "-NoProfile",
1158
+ "-ExecutionPolicy",
1159
+ "Bypass",
1160
+ "-File",
1161
+ installer,
1162
+ "-ExecutablePath",
1163
+ hostExe,
1164
+ "-WorkingDirectory",
1165
+ releaseDir,
1166
+ "-LogPath",
1167
+ installLog.logPath,
1168
+ "-ResultPath",
1169
+ installLog.resultPath,
1170
+ ]);
1171
+ }
1172
+ catch (error) {
1173
+ throw new Error(await formatServiceInstallFailure(error, installLog), { cause: error });
1174
+ }
1151
1175
  await event.finish(true, {
1152
1176
  releaseDir,
1153
1177
  version: manifest.version,
@@ -1316,6 +1340,76 @@ export function getDefaultServiceCacheDir() {
1316
1340
  }
1317
1341
  return join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "tapi", "service");
1318
1342
  }
1343
+ export function getDefaultServiceInstallLogDir() {
1344
+ return join(getDefaultServiceCacheDir(), "logs");
1345
+ }
1346
+ export function serviceInstallLogPaths(version, now = new Date()) {
1347
+ const stamp = now.toISOString().replace(/[:.]/g, "-");
1348
+ const safeVersion = safePathSegment(version);
1349
+ return {
1350
+ logPath: join(getDefaultServiceInstallLogDir(), `install-${safeVersion}-${stamp}.log`),
1351
+ resultPath: join(getDefaultServiceInstallLogDir(), `install-${safeVersion}-${stamp}.json`),
1352
+ };
1353
+ }
1354
+ export async function formatServiceInstallFailure(error, paths) {
1355
+ const lines = [
1356
+ `Tapi Service installer failed: ${formatError(error)}`,
1357
+ `Install log: ${paths.logPath}`,
1358
+ `Install result: ${paths.resultPath}`,
1359
+ ];
1360
+ const result = await readServiceInstallResult(paths.resultPath);
1361
+ if (result) {
1362
+ const phase = installerStringField(result.phase);
1363
+ const installError = installerStringField(result.error);
1364
+ const exitCode = result.exitCode !== undefined ? String(result.exitCode) : "";
1365
+ if (phase || exitCode) {
1366
+ lines.push(`Installer result: phase=${phase || "unknown"} exitCode=${exitCode || "unknown"}`);
1367
+ }
1368
+ if (installError) {
1369
+ lines.push(`Installer error: ${installError}`);
1370
+ }
1371
+ const resultLogPath = installerStringField(result.logPath);
1372
+ if (resultLogPath && resultLogPath !== paths.logPath) {
1373
+ lines.push(`Installer-reported log: ${resultLogPath}`);
1374
+ }
1375
+ }
1376
+ const logText = await readTextFileIfExists(paths.logPath);
1377
+ if (logText?.trim()) {
1378
+ const tail = tailLines(logText, 30);
1379
+ if (tail.length > 0) {
1380
+ lines.push("Last installer log lines:");
1381
+ lines.push(...tail.map((line) => ` ${line}`));
1382
+ }
1383
+ }
1384
+ return lines.join("\n");
1385
+ }
1386
+ async function readServiceInstallResult(path) {
1387
+ const text = await readTextFileIfExists(path);
1388
+ if (!text) {
1389
+ return null;
1390
+ }
1391
+ try {
1392
+ const parsed = JSON.parse(text);
1393
+ return parsed && typeof parsed === "object" ? parsed : null;
1394
+ }
1395
+ catch {
1396
+ return null;
1397
+ }
1398
+ }
1399
+ async function readTextFileIfExists(path) {
1400
+ try {
1401
+ return await readFile(path, "utf8");
1402
+ }
1403
+ catch {
1404
+ return null;
1405
+ }
1406
+ }
1407
+ function installerStringField(value) {
1408
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
1409
+ }
1410
+ function tailLines(text, count) {
1411
+ return text.split(/\r?\n/).filter((line) => line.trim()).slice(-count);
1412
+ }
1319
1413
  export class CliWideEvent {
1320
1414
  filePath;
1321
1415
  startedAt = performance.now();
@@ -1844,9 +1938,36 @@ async function launchPortableStudioServer(exePath, options) {
1844
1938
  });
1845
1939
  child.unref();
1846
1940
  const url = `http://${host}:${port}`;
1941
+ await waitForHttpOk(`${url}/healthz`, PORTABLE_STUDIO_SERVER_READY_TIMEOUT_MS, PORTABLE_STUDIO_SERVER_READY_INTERVAL_MS);
1847
1942
  openBrowser(url);
1848
1943
  console.log(`Opened Tapi Studio: ${url}`);
1849
1944
  }
1945
+ export async function waitForHttpOk(url, timeoutMs = PORTABLE_STUDIO_SERVER_READY_TIMEOUT_MS, intervalMs = PORTABLE_STUDIO_SERVER_READY_INTERVAL_MS, fetchImpl = fetch) {
1946
+ const startedAt = performance.now();
1947
+ let lastError = "";
1948
+ while (performance.now() - startedAt < timeoutMs) {
1949
+ try {
1950
+ const response = await fetchImpl(url, {
1951
+ headers: { Accept: "application/json" },
1952
+ });
1953
+ if (response.ok) {
1954
+ return;
1955
+ }
1956
+ lastError = `HTTP ${response.status}`;
1957
+ }
1958
+ catch (error) {
1959
+ lastError = formatError(error);
1960
+ }
1961
+ await delay(intervalMs);
1962
+ }
1963
+ const suffix = lastError ? ` Last error: ${lastError}.` : "";
1964
+ throw new Error(`Tapi Studio server did not become ready at ${url} within ${Math.ceil(timeoutMs / 1000)}s.${suffix}`);
1965
+ }
1966
+ function delay(ms) {
1967
+ return new Promise((resolvePromise) => {
1968
+ setTimeout(resolvePromise, Math.max(0, ms));
1969
+ });
1970
+ }
1850
1971
  async function chooseStudioPort(preferred = 18766) {
1851
1972
  for (let port = preferred; port < preferred + 25; port += 1) {
1852
1973
  if (await isPortAvailable(port)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tapi-dev/sdk",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",