@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 +9 -0
- package/dist/cli.js +133 -12
- package/package.json +1 -1
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
|
-
|
|
1141
|
-
"
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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)) {
|