@tapi-dev/sdk 0.1.30 → 0.1.32

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
@@ -60,6 +60,7 @@ export interface StudioCliOptions {
60
60
  silent: boolean;
61
61
  exePath?: string;
62
62
  installToken?: string;
63
+ installTokenSource?: "option" | "env" | "minted";
63
64
  workspace?: TapiWorkspace;
64
65
  workspaceRoot?: string;
65
66
  projectId?: string;
@@ -84,6 +85,7 @@ export declare function trySelectDevSessionInOpenStudio(session: TapiDevSession,
84
85
  baseUrl?: string;
85
86
  reason?: string;
86
87
  }>;
88
+ export declare function servicePowerShell(action: string): string;
87
89
  export declare function installedServiceVersionFromPathName(pathName: string): string | undefined;
88
90
  export declare function serviceNeedsInstall(status: ServiceStatus, manifest: ServiceReleaseManifest): boolean;
89
91
  export declare function getDefaultStudioCacheDir(): string;
@@ -113,7 +115,7 @@ export declare function getStudioExecutableCandidates(): string[];
113
115
  export declare function validateStudioManifest(input: unknown): StudioReleaseManifest;
114
116
  export declare function validateServiceReleaseManifest(input: unknown): ServiceReleaseManifest;
115
117
  export declare function compareVersions(left: string, right: string): number;
116
- export declare function memoizeStudioInstallToken(options: StudioCliOptions, installToken?: string): string;
118
+ export declare function memoizeStudioInstallToken(options: StudioCliOptions, installToken?: string, source?: StudioCliOptions["installTokenSource"]): string;
117
119
  export declare function closeExistingPortableStudioServersForFreshLaunch(stopProcesses?: () => Promise<number>): Promise<{
118
120
  stopped: number;
119
121
  recordsRemoved: number;
package/dist/cli.js CHANGED
@@ -28,6 +28,8 @@ const DOWNLOAD_PROGRESS_LOG_BYTES = 50 * 1024 * 1024;
28
28
  const MAX_WIDE_EVENT_PROCESS_ROOTS = 5;
29
29
  const MAX_NONCURRENT_MANAGED_DOWNLOADS = 0;
30
30
  const MAX_NONCURRENT_MANAGED_RELEASES = 0;
31
+ const WINDOWS_SERVICE_STATUS_TIMEOUT_MS = 8_000;
32
+ const WINDOWS_SERVICE_COMMAND_TIMEOUT_MS = 30_000;
31
33
  const MANAGED_RELEASE_MARKER = ".tapi-managed-release";
32
34
  const MIN_EXTRACTION_REQUIRED_BYTES = 1024 * 1024 * 1024;
33
35
  const EXTRACTION_REQUIRED_MULTIPLIER = 3;
@@ -344,6 +346,8 @@ export function parseStudioOptions(args) {
344
346
  : `${downloadsBaseUrl.replace(/\/+$/, "")}/studio/channels/${channel}/latest.json`;
345
347
  const projectId = raw.projectId ?? envString("TAPI_PROJECT_ID") ?? workspace?.projectId;
346
348
  const projectSlug = raw.projectSlug ?? envString("TAPI_PROJECT_SLUG") ?? workspace?.projectSlug;
349
+ const envInstallToken = envString("TAPI_STUDIO_INSTALL_TOKEN");
350
+ const installToken = raw.installToken ?? envInstallToken;
347
351
  return {
348
352
  channel,
349
353
  apiBaseUrl,
@@ -354,7 +358,8 @@ export function parseStudioOptions(args) {
354
358
  downloadOnly: raw.downloadOnly ?? false,
355
359
  silent: raw.silent ?? false,
356
360
  exePath: raw.exePath ?? envString("TAPI_STUDIO_EXE"),
357
- installToken: raw.installToken ?? envString("TAPI_STUDIO_INSTALL_TOKEN"),
361
+ installToken,
362
+ installTokenSource: raw.installToken ? "option" : installToken ? "env" : undefined,
358
363
  workspace,
359
364
  workspaceRoot: workspace?.root,
360
365
  projectId,
@@ -1348,7 +1353,7 @@ async function runServiceCommand(action, args = []) {
1348
1353
  return repairService(parseStudioOptions([]));
1349
1354
  }
1350
1355
  const command = servicePowerShell(normalized);
1351
- const output = await withCliSpinner(`Running tapi-service ${normalized}`, () => runProcessCapture("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command]));
1356
+ const output = await withCliSpinner(`Running tapi-service ${normalized}`, () => runProcessCapture("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command], { timeoutMs: WINDOWS_SERVICE_COMMAND_TIMEOUT_MS }));
1352
1357
  if (output.trim()) {
1353
1358
  console.log(output.trim());
1354
1359
  }
@@ -1366,9 +1371,9 @@ async function repairService(options) {
1366
1371
  }
1367
1372
  return runServiceCommand("restart");
1368
1373
  }
1369
- function servicePowerShell(action) {
1374
+ export function servicePowerShell(action) {
1370
1375
  const serviceName = "tapi-service";
1371
- const status = `$svc = Get-Service -Name '${serviceName}' -ErrorAction SilentlyContinue; if ($null -eq $svc) { [pscustomobject]@{ installed = $false; name = '${serviceName}'; status = 'not_installed'; pathName = '' } | ConvertTo-Json -Compress; exit 0 }; $cim = Get-CimInstance Win32_Service -Filter "Name='${serviceName}'" -ErrorAction SilentlyContinue; $pathName = if ($null -ne $cim) { [string]$cim.PathName } else { '' }; [pscustomobject]@{ installed = $true; name = $svc.Name; status = $svc.Status.ToString(); pathName = $pathName } | ConvertTo-Json -Compress`;
1376
+ const status = `$svc = Get-Service -Name '${serviceName}' -ErrorAction SilentlyContinue; if ($null -eq $svc) { [pscustomobject]@{ installed = $false; name = '${serviceName}'; status = 'not_installed'; pathName = '' } | ConvertTo-Json -Compress; exit 0 }; $pathName = ''; try { $imagePath = (Get-ItemProperty -LiteralPath 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\${serviceName}' -Name ImagePath -ErrorAction SilentlyContinue).ImagePath; if ($null -ne $imagePath) { $pathName = [string]$imagePath } } catch {}; [pscustomobject]@{ installed = $true; name = $svc.Name; status = $svc.Status.ToString(); pathName = $pathName } | ConvertTo-Json -Compress`;
1372
1377
  if (action === "status") {
1373
1378
  return status;
1374
1379
  }
@@ -1390,7 +1395,7 @@ async function getServiceStatus() {
1390
1395
  "Bypass",
1391
1396
  "-Command",
1392
1397
  servicePowerShell("status"),
1393
- ]);
1398
+ ], { timeoutMs: WINDOWS_SERVICE_STATUS_TIMEOUT_MS });
1394
1399
  try {
1395
1400
  const payload = JSON.parse(output.trim());
1396
1401
  return {
@@ -1433,7 +1438,7 @@ async function ensureServiceReadyForStudio(options) {
1433
1438
  "Bypass",
1434
1439
  "-Command",
1435
1440
  servicePowerShell("start"),
1436
- ]));
1441
+ ], { timeoutMs: WINDOWS_SERVICE_COMMAND_TIMEOUT_MS }));
1437
1442
  if (output.trim()) {
1438
1443
  console.log(output.trim());
1439
1444
  }
@@ -1967,12 +1972,7 @@ async function installStudio(options) {
1967
1972
  throw new Error("Direct Studio manifest overrides are no longer supported for install. "
1968
1973
  + "Use --channel with the protected install flow instead.");
1969
1974
  }
1970
- const installToken = await ensureStudioInstallTokenForRun(options, event, "Checking Studio install authorization");
1971
- await event.phase("manifest.fetch.start", {
1972
- apiBaseUrl: options.apiBaseUrl,
1973
- channel: options.channel,
1974
- });
1975
- const manifest = await withCliSpinner(`Fetching Tapi Studio ${options.channel} manifest`, () => fetchProtectedStudioManifest(options.apiBaseUrl, options.channel, installToken));
1975
+ const manifest = await fetchStudioManifestForRun(options, event, "Checking Studio install authorization");
1976
1976
  await event.phase("manifest.fetch.success", {
1977
1977
  manifest: manifestEventSummary(manifest),
1978
1978
  });
@@ -2086,7 +2086,7 @@ async function obtainStudioInstallToken(options, event) {
2086
2086
  const issued = await requestStudioInstallToken(options.apiBaseUrl, options.channel, firebaseCreds.idToken);
2087
2087
  return issued.installToken;
2088
2088
  }
2089
- export function memoizeStudioInstallToken(options, installToken) {
2089
+ export function memoizeStudioInstallToken(options, installToken, source = "minted") {
2090
2090
  const existing = options.installToken?.trim();
2091
2091
  if (existing) {
2092
2092
  options.installToken = existing;
@@ -2095,9 +2095,14 @@ export function memoizeStudioInstallToken(options, installToken) {
2095
2095
  const normalized = installToken?.trim() ?? "";
2096
2096
  if (normalized) {
2097
2097
  options.installToken = normalized;
2098
+ options.installTokenSource = source;
2098
2099
  }
2099
2100
  return normalized;
2100
2101
  }
2102
+ function clearStudioInstallTokenForRun(options) {
2103
+ options.installToken = undefined;
2104
+ options.installTokenSource = undefined;
2105
+ }
2101
2106
  async function ensureStudioInstallTokenForRun(options, event, message) {
2102
2107
  const existing = memoizeStudioInstallToken(options);
2103
2108
  if (existing) {
@@ -2111,7 +2116,7 @@ async function ensureStudioInstallTokenForRun(options, event, message) {
2111
2116
  apiBaseUrl: options.apiBaseUrl,
2112
2117
  channel: options.channel,
2113
2118
  });
2114
- const installToken = memoizeStudioInstallToken(options, await withCliSpinner(message, () => obtainStudioInstallToken(options, event)));
2119
+ const installToken = memoizeStudioInstallToken(options, await withCliSpinner(message, () => obtainStudioInstallToken(options, event)), "minted");
2115
2120
  await event.phase("auth.install_token.request.success", {
2116
2121
  apiBaseUrl: options.apiBaseUrl,
2117
2122
  channel: options.channel,
@@ -2119,6 +2124,39 @@ async function ensureStudioInstallTokenForRun(options, event, message) {
2119
2124
  });
2120
2125
  return installToken;
2121
2126
  }
2127
+ async function fetchStudioManifestForRun(options, event, authMessage) {
2128
+ const installToken = await ensureStudioInstallTokenForRun(options, event, authMessage);
2129
+ await event.phase("manifest.fetch.start", {
2130
+ apiBaseUrl: options.apiBaseUrl,
2131
+ channel: options.channel,
2132
+ });
2133
+ try {
2134
+ return await withCliSpinner(`Fetching Tapi Studio ${options.channel} manifest`, () => fetchProtectedStudioManifest(options.apiBaseUrl, options.channel, installToken));
2135
+ }
2136
+ catch (error) {
2137
+ if (!isExpiredStudioInstallTokenError(error) || options.installTokenSource === "option") {
2138
+ throw error;
2139
+ }
2140
+ await event.phase("auth.install_token.expired", {
2141
+ apiBaseUrl: options.apiBaseUrl,
2142
+ channel: options.channel,
2143
+ tokenSource: options.installTokenSource ?? "unknown",
2144
+ error: errorDetails(error),
2145
+ });
2146
+ clearStudioInstallTokenForRun(options);
2147
+ const refreshedToken = await ensureStudioInstallTokenForRun(options, event, "Refreshing Studio install authorization");
2148
+ await event.phase("manifest.fetch.retry.start", {
2149
+ apiBaseUrl: options.apiBaseUrl,
2150
+ channel: options.channel,
2151
+ });
2152
+ return await withCliSpinner(`Retrying Tapi Studio ${options.channel} manifest`, () => fetchProtectedStudioManifest(options.apiBaseUrl, options.channel, refreshedToken));
2153
+ }
2154
+ }
2155
+ function isExpiredStudioInstallTokenError(error) {
2156
+ const message = formatError(error);
2157
+ return (message.includes("Failed to fetch protected Studio manifest: HTTP 401")
2158
+ && message.includes("Studio install token has expired"));
2159
+ }
2122
2160
  async function openStudio(options) {
2123
2161
  ensureWindowsHost();
2124
2162
  if (options.downloadOnly) {
@@ -2180,12 +2218,7 @@ async function ensurePortableStudioServer(options) {
2180
2218
  options: installEventOptions(options),
2181
2219
  });
2182
2220
  try {
2183
- const installToken = await ensureStudioInstallTokenForRun(options, event, "Checking Studio install authorization");
2184
- await event.phase("manifest.fetch.start", {
2185
- apiBaseUrl: options.apiBaseUrl,
2186
- channel: options.channel,
2187
- });
2188
- const manifest = await withCliSpinner(`Fetching Tapi Studio ${options.channel} manifest`, () => fetchProtectedStudioManifest(options.apiBaseUrl, options.channel, installToken));
2221
+ const manifest = await fetchStudioManifestForRun(options, event, "Checking Studio install authorization");
2189
2222
  ensureCompatibleManifest(manifest);
2190
2223
  await event.phase("manifest.fetch.success", {
2191
2224
  manifest: manifestEventSummary(manifest),
@@ -3055,7 +3088,7 @@ async function runProcess(command, args) {
3055
3088
  });
3056
3089
  });
3057
3090
  }
3058
- async function runProcessCapture(command, args) {
3091
+ async function runProcessCapture(command, args, options = {}) {
3059
3092
  return await new Promise((resolvePromise, rejectPromise) => {
3060
3093
  const child = spawn(command, args, {
3061
3094
  stdio: ["ignore", "pipe", "pipe"],
@@ -3063,21 +3096,47 @@ async function runProcessCapture(command, args) {
3063
3096
  });
3064
3097
  let stdout = "";
3065
3098
  let stderr = "";
3099
+ let timedOut = false;
3100
+ let settled = false;
3101
+ const timeoutMs = Math.max(0, options.timeoutMs ?? 0);
3102
+ const timeout = timeoutMs > 0
3103
+ ? setTimeout(() => {
3104
+ timedOut = true;
3105
+ child.kill();
3106
+ }, timeoutMs)
3107
+ : undefined;
3108
+ timeout?.unref?.();
3109
+ const finish = (callback) => {
3110
+ if (settled) {
3111
+ return;
3112
+ }
3113
+ settled = true;
3114
+ if (timeout) {
3115
+ clearTimeout(timeout);
3116
+ }
3117
+ callback();
3118
+ };
3066
3119
  child.stdout.on("data", (chunk) => {
3067
3120
  stdout += String(chunk);
3068
3121
  });
3069
3122
  child.stderr.on("data", (chunk) => {
3070
3123
  stderr += String(chunk);
3071
3124
  });
3072
- child.once("error", rejectPromise);
3125
+ child.once("error", (error) => finish(() => rejectPromise(error)));
3073
3126
  child.once("exit", (code) => {
3074
- const err = stderr.trim();
3075
- if (code === 0) {
3076
- resolvePromise(stdout);
3077
- }
3078
- else {
3079
- rejectPromise(new Error(err || `Process exited with code ${code ?? "unknown"}.`));
3080
- }
3127
+ finish(() => {
3128
+ const err = stderr.trim();
3129
+ if (timedOut) {
3130
+ rejectPromise(new Error(`Process timed out after ${timeoutMs} ms: ${command} ${args.join(" ")}`));
3131
+ return;
3132
+ }
3133
+ if (code === 0) {
3134
+ resolvePromise(stdout);
3135
+ }
3136
+ else {
3137
+ rejectPromise(new Error(err || `Process exited with code ${code ?? "unknown"}.`));
3138
+ }
3139
+ });
3081
3140
  });
3082
3141
  });
3083
3142
  }
@@ -3416,6 +3475,7 @@ function installEventOptions(options) {
3416
3475
  silent: options.silent,
3417
3476
  exePath: options.exePath,
3418
3477
  installTokenProvided: Boolean(options.installToken),
3478
+ installTokenSource: options.installTokenSource,
3419
3479
  };
3420
3480
  }
3421
3481
  function serviceManifestEventSummary(manifest) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tapi-dev/sdk",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",