@tapi-dev/sdk 0.1.6 → 0.1.8
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/README.md +108 -164
- package/dist/catalog.d.ts +2 -2
- package/dist/cli.d.ts +33 -0
- package/dist/cli.js +1112 -55
- package/dist/client.d.ts +2 -2
- package/dist/client.js +4 -4
- package/dist/cloud-runs.d.ts +2 -2
- package/dist/index.d.ts +10 -9
- package/dist/index.js +10 -9
- package/dist/runners.d.ts +2 -2
- package/dist/runs.d.ts +2 -2
- package/dist/runtime.d.ts +2 -2
- package/dist/types.d.ts +1 -1
- package/dist/website-apis.d.ts +2 -2
- package/dist/website-apis.js +1 -1
- package/dist/workspace.d.ts +35 -0
- package/dist/workspace.js +117 -0
- package/package.json +4 -3
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
|
+
import { createServer as createNetServer } from "node:net";
|
|
4
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
6
|
import { createReadStream, createWriteStream, existsSync, readFileSync } from "node:fs";
|
|
6
7
|
import { mkdir, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
7
8
|
import { homedir } from "node:os";
|
|
8
|
-
import { basename, join, resolve } from "node:path";
|
|
9
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
9
10
|
import { performance } from "node:perf_hooks";
|
|
10
11
|
import { Readable } from "node:stream";
|
|
11
12
|
import { pipeline } from "node:stream/promises";
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
13
|
-
import { TapiClient } from "./index";
|
|
14
|
+
import { TapiClient } from "./index.js";
|
|
15
|
+
import { loadWorkspace, normalizeProjectValue, writeWorkspaceConfig, } from "./workspace.js";
|
|
14
16
|
const DEFAULT_DOWNLOADS_BASE_URL = "https://d4xaf52nfwiok.cloudfront.net";
|
|
15
17
|
const DEFAULT_STUDIO_API_BASE_URL = "https://determined-motivation-production.up.railway.app";
|
|
16
18
|
const DEFAULT_STUDIO_AUTH_HTML_URL = "https://rsarlong-1f92fd.gitlab.io/auth.html";
|
|
@@ -47,6 +49,27 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
47
49
|
}
|
|
48
50
|
return runDoctor(parseStudioOptions([subcommand, ...rest].filter(Boolean)));
|
|
49
51
|
}
|
|
52
|
+
if (command === "init" || command === "link") {
|
|
53
|
+
if (hasHelpFlag([subcommand, ...rest])) {
|
|
54
|
+
printWorkspaceHelp(command);
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
return writeWorkspace(command, [subcommand, ...rest].filter(Boolean));
|
|
58
|
+
}
|
|
59
|
+
if (command === "service") {
|
|
60
|
+
if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h" || hasHelpFlag(rest)) {
|
|
61
|
+
printServiceHelp();
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
return runServiceCommand(subcommand, rest);
|
|
65
|
+
}
|
|
66
|
+
if (command === "publish") {
|
|
67
|
+
if (hasHelpFlag([subcommand, ...rest])) {
|
|
68
|
+
printPublishHelp();
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
return publishLocalApis([subcommand, ...rest].filter(Boolean));
|
|
72
|
+
}
|
|
50
73
|
if (command === "apis") {
|
|
51
74
|
if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h" || hasHelpFlag(rest)) {
|
|
52
75
|
printApisHelp();
|
|
@@ -55,6 +78,12 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
55
78
|
if (subcommand === "describe") {
|
|
56
79
|
return describeApiOperation(rest);
|
|
57
80
|
}
|
|
81
|
+
if (subcommand === "sync") {
|
|
82
|
+
return syncApiCatalog(rest);
|
|
83
|
+
}
|
|
84
|
+
if (subcommand === "generate") {
|
|
85
|
+
return generateApiClient(rest);
|
|
86
|
+
}
|
|
58
87
|
console.error(`Unknown apis command: ${subcommand}`);
|
|
59
88
|
printApisHelp();
|
|
60
89
|
return 1;
|
|
@@ -64,7 +93,10 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
64
93
|
printHelp();
|
|
65
94
|
return 1;
|
|
66
95
|
}
|
|
67
|
-
if (!subcommand
|
|
96
|
+
if (!subcommand) {
|
|
97
|
+
return openStudio(parseStudioOptions([]));
|
|
98
|
+
}
|
|
99
|
+
if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
68
100
|
printStudioHelp();
|
|
69
101
|
return 0;
|
|
70
102
|
}
|
|
@@ -88,6 +120,8 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
88
120
|
}
|
|
89
121
|
export function parseStudioOptions(args) {
|
|
90
122
|
const raw = {};
|
|
123
|
+
let workspaceSearchRoot;
|
|
124
|
+
let skipWorkspace = false;
|
|
91
125
|
for (let index = 0; index < args.length; index += 1) {
|
|
92
126
|
const arg = args[index];
|
|
93
127
|
if (!arg) {
|
|
@@ -121,6 +155,34 @@ export function parseStudioOptions(args) {
|
|
|
121
155
|
raw.apiBaseUrl = arg.slice("--server=".length);
|
|
122
156
|
continue;
|
|
123
157
|
}
|
|
158
|
+
if (arg === "--workspace") {
|
|
159
|
+
workspaceSearchRoot = requireOptionValue(args, ++index, "--workspace");
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (arg.startsWith("--workspace=")) {
|
|
163
|
+
workspaceSearchRoot = arg.slice("--workspace=".length);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (arg === "--no-workspace") {
|
|
167
|
+
skipWorkspace = true;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (arg === "--project") {
|
|
171
|
+
raw.projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (arg.startsWith("--project=")) {
|
|
175
|
+
raw.projectId = normalizeProjectValue(arg.slice("--project=".length), "project");
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (arg === "--project-slug") {
|
|
179
|
+
raw.projectSlug = normalizeProjectValue(requireOptionValue(args, ++index, "--project-slug"), "projectSlug");
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (arg.startsWith("--project-slug=")) {
|
|
183
|
+
raw.projectSlug = normalizeProjectValue(arg.slice("--project-slug=".length), "projectSlug");
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
124
186
|
if (arg === "--install-token") {
|
|
125
187
|
raw.installToken = requireOptionValue(args, ++index, "--install-token");
|
|
126
188
|
continue;
|
|
@@ -158,17 +220,21 @@ export function parseStudioOptions(args) {
|
|
|
158
220
|
}
|
|
159
221
|
throw new Error(`Unknown option: ${arg}`);
|
|
160
222
|
}
|
|
223
|
+
const workspace = skipWorkspace ? undefined : loadWorkspace(workspaceSearchRoot ?? process.cwd());
|
|
161
224
|
const channel = raw.channel ?? parseChannel(envString("TAPI_STUDIO_CHANNEL") ?? DEFAULT_CHANNEL);
|
|
162
225
|
const apiBaseUrl = normalizeHttpUrl(raw.apiBaseUrl
|
|
163
226
|
?? envString("TAPI_STUDIO_API_BASE_URL")
|
|
164
227
|
?? envString("TAPI_BASE_URL")
|
|
165
228
|
?? envString("TAPI_STUDIO_SERVER_URL")
|
|
229
|
+
?? workspace?.apiBaseUrl
|
|
166
230
|
?? DEFAULT_STUDIO_API_BASE_URL, "Studio API base URL");
|
|
167
231
|
const downloadsBaseUrl = normalizeHttpUrl(envString("TAPI_DOWNLOADS_BASE_URL") ?? DEFAULT_DOWNLOADS_BASE_URL, "TAPI_DOWNLOADS_BASE_URL");
|
|
168
232
|
const explicitManifestUrl = raw.manifestUrlOverride ?? envString("TAPI_STUDIO_MANIFEST_URL");
|
|
169
233
|
const manifestUrl = explicitManifestUrl
|
|
170
234
|
? normalizeHttpUrl(explicitManifestUrl, "Studio manifest URL")
|
|
171
235
|
: `${downloadsBaseUrl.replace(/\/+$/, "")}/studio/channels/${channel}/latest.json`;
|
|
236
|
+
const projectId = raw.projectId ?? envString("TAPI_PROJECT_ID") ?? workspace?.projectId;
|
|
237
|
+
const projectSlug = raw.projectSlug ?? envString("TAPI_PROJECT_SLUG") ?? workspace?.projectSlug;
|
|
172
238
|
return {
|
|
173
239
|
channel,
|
|
174
240
|
apiBaseUrl,
|
|
@@ -179,13 +245,19 @@ export function parseStudioOptions(args) {
|
|
|
179
245
|
silent: raw.silent ?? false,
|
|
180
246
|
exePath: raw.exePath ?? envString("TAPI_STUDIO_EXE"),
|
|
181
247
|
installToken: raw.installToken ?? envString("TAPI_STUDIO_INSTALL_TOKEN"),
|
|
248
|
+
workspace,
|
|
249
|
+
workspaceRoot: workspace?.root,
|
|
250
|
+
projectId,
|
|
251
|
+
projectSlug,
|
|
252
|
+
workspaceMode: Boolean(projectId || workspace),
|
|
182
253
|
};
|
|
183
254
|
}
|
|
184
255
|
function parseApiOptions(args) {
|
|
185
256
|
let operation = "";
|
|
186
|
-
|
|
257
|
+
const workspace = loadWorkspace();
|
|
258
|
+
let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
|
|
187
259
|
let apiKey = envString("TAPI_API_KEY") || "";
|
|
188
|
-
let
|
|
260
|
+
let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
|
|
189
261
|
for (let index = 0; index < args.length; index += 1) {
|
|
190
262
|
const arg = args[index];
|
|
191
263
|
if (!arg)
|
|
@@ -210,12 +282,12 @@ function parseApiOptions(args) {
|
|
|
210
282
|
apiKey = arg.slice("--api-key=".length);
|
|
211
283
|
continue;
|
|
212
284
|
}
|
|
213
|
-
if (arg === "--
|
|
214
|
-
|
|
285
|
+
if (arg === "--project") {
|
|
286
|
+
projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
|
|
215
287
|
continue;
|
|
216
288
|
}
|
|
217
|
-
if (arg.startsWith("--
|
|
218
|
-
|
|
289
|
+
if (arg.startsWith("--project=")) {
|
|
290
|
+
projectId = normalizeProjectValue(arg.slice("--project=".length), "project");
|
|
219
291
|
continue;
|
|
220
292
|
}
|
|
221
293
|
if (arg.startsWith("--")) {
|
|
@@ -233,12 +305,15 @@ function parseApiOptions(args) {
|
|
|
233
305
|
if (!apiKey) {
|
|
234
306
|
throw new Error("apis describe requires --api-key or TAPI_API_KEY.");
|
|
235
307
|
}
|
|
308
|
+
if (!projectId) {
|
|
309
|
+
throw new Error("apis describe requires --project, TAPI_PROJECT_ID, or .tapi/project.json.");
|
|
310
|
+
}
|
|
236
311
|
return {
|
|
237
312
|
operation,
|
|
238
313
|
options: {
|
|
239
314
|
apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
|
|
240
315
|
apiKey,
|
|
241
|
-
|
|
316
|
+
projectId,
|
|
242
317
|
},
|
|
243
318
|
};
|
|
244
319
|
}
|
|
@@ -248,7 +323,7 @@ async function describeApiOperation(args) {
|
|
|
248
323
|
const client = new TapiClient({
|
|
249
324
|
baseUrl: options.apiBaseUrl,
|
|
250
325
|
apiKey: options.apiKey,
|
|
251
|
-
|
|
326
|
+
projectId: options.projectId,
|
|
252
327
|
});
|
|
253
328
|
const description = await client.websiteApis.describe(operation);
|
|
254
329
|
console.log(JSON.stringify(description, null, 2));
|
|
@@ -259,6 +334,596 @@ async function describeApiOperation(args) {
|
|
|
259
334
|
return 1;
|
|
260
335
|
}
|
|
261
336
|
}
|
|
337
|
+
function parseApiWorkspaceOptions(args) {
|
|
338
|
+
const workspace = loadWorkspace();
|
|
339
|
+
let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
|
|
340
|
+
let apiKey = envString("TAPI_API_KEY") || "";
|
|
341
|
+
let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
|
|
342
|
+
let catalogPath = workspace?.config.generated?.catalog || ".tapi/generated/catalog.json";
|
|
343
|
+
let typescriptPath = workspace?.config.generated?.typescript || "src/tapi.generated.ts";
|
|
344
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
345
|
+
const arg = args[index];
|
|
346
|
+
if (!arg)
|
|
347
|
+
continue;
|
|
348
|
+
if (arg === "--api-base-url" || arg === "--server") {
|
|
349
|
+
apiBaseUrl = requireOptionValue(args, ++index, arg);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (arg.startsWith("--api-base-url=")) {
|
|
353
|
+
apiBaseUrl = arg.slice("--api-base-url=".length);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (arg.startsWith("--server=")) {
|
|
357
|
+
apiBaseUrl = arg.slice("--server=".length);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (arg === "--api-key") {
|
|
361
|
+
apiKey = requireOptionValue(args, ++index, "--api-key");
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (arg.startsWith("--api-key=")) {
|
|
365
|
+
apiKey = arg.slice("--api-key=".length);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (arg === "--project") {
|
|
369
|
+
projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (arg.startsWith("--project=")) {
|
|
373
|
+
projectId = normalizeProjectValue(arg.slice("--project=".length), "project");
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (arg === "--catalog") {
|
|
377
|
+
catalogPath = requireOptionValue(args, ++index, "--catalog");
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (arg.startsWith("--catalog=")) {
|
|
381
|
+
catalogPath = arg.slice("--catalog=".length);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (arg === "--out") {
|
|
385
|
+
typescriptPath = requireOptionValue(args, ++index, "--out");
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (arg.startsWith("--out=")) {
|
|
389
|
+
typescriptPath = arg.slice("--out=".length);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
throw new Error(`Unknown apis option: ${arg}`);
|
|
393
|
+
}
|
|
394
|
+
if (!apiKey) {
|
|
395
|
+
throw new Error("apis command requires --api-key or TAPI_API_KEY.");
|
|
396
|
+
}
|
|
397
|
+
if (!projectId) {
|
|
398
|
+
throw new Error("apis command requires --project, TAPI_PROJECT_ID, or .tapi/project.json.");
|
|
399
|
+
}
|
|
400
|
+
const root = workspace?.root || process.cwd();
|
|
401
|
+
return {
|
|
402
|
+
apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
|
|
403
|
+
apiKey,
|
|
404
|
+
projectId,
|
|
405
|
+
catalogPath: resolve(root, catalogPath),
|
|
406
|
+
typescriptPath: resolve(root, typescriptPath),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
async function syncApiCatalog(args) {
|
|
410
|
+
try {
|
|
411
|
+
const options = parseApiWorkspaceOptions(args);
|
|
412
|
+
const catalog = await fetchApiCatalog(options);
|
|
413
|
+
await writeJsonFile(options.catalogPath, catalog);
|
|
414
|
+
console.log(`Synced Tapi API catalog: ${options.catalogPath}`);
|
|
415
|
+
return 0;
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
console.error(formatError(error));
|
|
419
|
+
return 1;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async function generateApiClient(args) {
|
|
423
|
+
try {
|
|
424
|
+
const options = parseApiWorkspaceOptions(args);
|
|
425
|
+
const catalog = await fetchApiCatalog(options);
|
|
426
|
+
await writeJsonFile(options.catalogPath, catalog);
|
|
427
|
+
await writeTextFile(options.typescriptPath, renderGeneratedApiClient(catalog));
|
|
428
|
+
console.log(`Generated Tapi API client: ${options.typescriptPath}`);
|
|
429
|
+
return 0;
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
console.error(formatError(error));
|
|
433
|
+
return 1;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function fetchApiCatalog(options) {
|
|
437
|
+
const client = new TapiClient({
|
|
438
|
+
baseUrl: options.apiBaseUrl,
|
|
439
|
+
apiKey: options.apiKey,
|
|
440
|
+
projectId: options.projectId,
|
|
441
|
+
});
|
|
442
|
+
return client.catalog.get();
|
|
443
|
+
}
|
|
444
|
+
async function writeJsonFile(path, payload) {
|
|
445
|
+
await writeTextFile(path, `${JSON.stringify(payload, null, 2)}\n`);
|
|
446
|
+
}
|
|
447
|
+
async function writeTextFile(path, content) {
|
|
448
|
+
await mkdir(dirname(path), { recursive: true });
|
|
449
|
+
await writeFile(path, content, "utf8");
|
|
450
|
+
}
|
|
451
|
+
function renderGeneratedApiClient(catalog) {
|
|
452
|
+
const namespaces = new Map();
|
|
453
|
+
for (const api of catalog.apis || []) {
|
|
454
|
+
const namespace = safeIdentifier(api.name || "api");
|
|
455
|
+
for (const request of api.requests || []) {
|
|
456
|
+
const key = String(request.operation || request.sdkName || request.key || "").trim();
|
|
457
|
+
if (!key)
|
|
458
|
+
continue;
|
|
459
|
+
const operationName = safeIdentifier(key);
|
|
460
|
+
const operation = `${api.name}.${key}`;
|
|
461
|
+
const items = namespaces.get(namespace) ?? [];
|
|
462
|
+
items.push({ key: operation, operationName });
|
|
463
|
+
namespaces.set(namespace, items);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const namespaceBlocks = [...namespaces.entries()].map(([namespace, operations]) => {
|
|
467
|
+
const lines = operations.map(({ key, operationName }) => ` ${operationName}: (inputs: Record<string, unknown> = {}, options: GeneratedRunOptions = {}) => client.websiteApis.run(${JSON.stringify(key)}, { ...options, inputs }),`);
|
|
468
|
+
return ` ${namespace}: {\n${lines.join("\n")}\n },`;
|
|
469
|
+
});
|
|
470
|
+
return `/* Generated by Tapi. Do not edit by hand. */
|
|
471
|
+
import { TapiClient, type TapiClientOptions, type RuntimeRunOptions } from "@tapi-dev/sdk";
|
|
472
|
+
|
|
473
|
+
export interface GeneratedRunOptions {
|
|
474
|
+
runtime?: RuntimeRunOptions;
|
|
475
|
+
priority?: number;
|
|
476
|
+
runnerId?: string;
|
|
477
|
+
idempotencyKey?: string;
|
|
478
|
+
site?: string;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function createTapiGeneratedClient(options: TapiClientOptions) {
|
|
482
|
+
const client = new TapiClient(options);
|
|
483
|
+
return {
|
|
484
|
+
${namespaceBlocks.join("\n")}
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
`;
|
|
488
|
+
}
|
|
489
|
+
function safeIdentifier(value) {
|
|
490
|
+
const cleaned = String(value || "api")
|
|
491
|
+
.replace(/[^a-zA-Z0-9_$]+/g, "_")
|
|
492
|
+
.replace(/^([^a-zA-Z_$])/, "_$1");
|
|
493
|
+
return cleaned || "api";
|
|
494
|
+
}
|
|
495
|
+
function isRecord(value) {
|
|
496
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
497
|
+
}
|
|
498
|
+
function parseWorkspaceOptions(command, args) {
|
|
499
|
+
const raw = {
|
|
500
|
+
force: command === "link",
|
|
501
|
+
};
|
|
502
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
503
|
+
const arg = args[index];
|
|
504
|
+
if (!arg) {
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (arg === "--project") {
|
|
508
|
+
raw.projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (arg.startsWith("--project=")) {
|
|
512
|
+
raw.projectId = normalizeProjectValue(arg.slice("--project=".length), "project");
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (arg === "--project-slug") {
|
|
516
|
+
raw.projectSlug = normalizeProjectValue(requireOptionValue(args, ++index, "--project-slug"), "projectSlug");
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (arg.startsWith("--project-slug=")) {
|
|
520
|
+
raw.projectSlug = normalizeProjectValue(arg.slice("--project-slug=".length), "projectSlug");
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (arg === "--api-base-url" || arg === "--server") {
|
|
524
|
+
raw.apiBaseUrl = requireOptionValue(args, ++index, arg);
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
if (arg.startsWith("--api-base-url=")) {
|
|
528
|
+
raw.apiBaseUrl = arg.slice("--api-base-url=".length);
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (arg.startsWith("--server=")) {
|
|
532
|
+
raw.apiBaseUrl = arg.slice("--server=".length);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (arg === "--root") {
|
|
536
|
+
raw.root = resolve(requireOptionValue(args, ++index, "--root"));
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (arg.startsWith("--root=")) {
|
|
540
|
+
raw.root = resolve(arg.slice("--root=".length));
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (arg === "--force") {
|
|
544
|
+
raw.force = true;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (arg.startsWith("--")) {
|
|
548
|
+
throw new Error(`Unknown ${command} option: ${arg}`);
|
|
549
|
+
}
|
|
550
|
+
if (!raw.projectId) {
|
|
551
|
+
raw.projectId = normalizeProjectValue(arg, "project");
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
throw new Error(`Unexpected ${command} argument: ${arg}`);
|
|
555
|
+
}
|
|
556
|
+
if (!raw.projectId) {
|
|
557
|
+
throw new Error(`tapi ${command} requires --project <id>.`);
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
force: raw.force ?? false,
|
|
561
|
+
projectId: raw.projectId,
|
|
562
|
+
projectSlug: raw.projectSlug,
|
|
563
|
+
apiBaseUrl: raw.apiBaseUrl ? normalizeHttpUrl(raw.apiBaseUrl, "Tapi API base URL") : undefined,
|
|
564
|
+
root: raw.root,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
async function writeWorkspace(command, args) {
|
|
568
|
+
try {
|
|
569
|
+
const options = parseWorkspaceOptions(command, args);
|
|
570
|
+
const workspace = await writeWorkspaceConfig(options);
|
|
571
|
+
console.log(`Tapi workspace ${command === "init" ? "initialized" : "linked"}: ${workspace.configPath}`);
|
|
572
|
+
console.log(`Project: ${workspace.projectId}`);
|
|
573
|
+
if (workspace.apiBaseUrl) {
|
|
574
|
+
console.log(`API: ${workspace.apiBaseUrl}`);
|
|
575
|
+
}
|
|
576
|
+
return 0;
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
console.error(formatError(error));
|
|
580
|
+
return 1;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
async function runServiceCommand(action, args = []) {
|
|
584
|
+
try {
|
|
585
|
+
ensureWindowsHost();
|
|
586
|
+
const normalized = action.toLowerCase();
|
|
587
|
+
if (!["status", "install", "start", "stop", "restart", "repair"].includes(normalized)) {
|
|
588
|
+
throw new Error(`Unknown service command: ${action}`);
|
|
589
|
+
}
|
|
590
|
+
if (normalized === "install") {
|
|
591
|
+
return installService(parseStudioOptions(args));
|
|
592
|
+
}
|
|
593
|
+
if (normalized === "repair" && args.length > 0) {
|
|
594
|
+
return repairService(parseStudioOptions(args));
|
|
595
|
+
}
|
|
596
|
+
if (args.length > 0) {
|
|
597
|
+
throw new Error(`tapi service ${normalized} does not accept options.`);
|
|
598
|
+
}
|
|
599
|
+
if (normalized === "repair") {
|
|
600
|
+
return repairService(parseStudioOptions([]));
|
|
601
|
+
}
|
|
602
|
+
const command = servicePowerShell(normalized);
|
|
603
|
+
const output = await runProcessCapture("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command]);
|
|
604
|
+
if (output.trim()) {
|
|
605
|
+
console.log(output.trim());
|
|
606
|
+
}
|
|
607
|
+
return 0;
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
console.error(formatError(error));
|
|
611
|
+
return 1;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
async function repairService(options) {
|
|
615
|
+
const status = await getServiceStatus();
|
|
616
|
+
if (!status.installed) {
|
|
617
|
+
return installService(options);
|
|
618
|
+
}
|
|
619
|
+
return runServiceCommand("restart");
|
|
620
|
+
}
|
|
621
|
+
function servicePowerShell(action) {
|
|
622
|
+
const serviceName = "tapi-service";
|
|
623
|
+
const status = `$svc = Get-Service -Name '${serviceName}' -ErrorAction SilentlyContinue; if ($null -eq $svc) { [pscustomobject]@{ installed = $false; name = '${serviceName}'; status = 'not_installed' } | ConvertTo-Json -Compress; exit 0 }; [pscustomobject]@{ installed = $true; name = $svc.Name; status = $svc.Status.ToString() } | ConvertTo-Json -Compress`;
|
|
624
|
+
if (action === "status") {
|
|
625
|
+
return status;
|
|
626
|
+
}
|
|
627
|
+
if (action === "start") {
|
|
628
|
+
return `Start-Service -Name '${serviceName}' -ErrorAction Stop; ${status}`;
|
|
629
|
+
}
|
|
630
|
+
if (action === "stop") {
|
|
631
|
+
return `Stop-Service -Name '${serviceName}' -ErrorAction Stop; ${status}`;
|
|
632
|
+
}
|
|
633
|
+
if (action === "restart" || action === "repair") {
|
|
634
|
+
return `Restart-Service -Name '${serviceName}' -Force -ErrorAction Stop; ${status}`;
|
|
635
|
+
}
|
|
636
|
+
return status;
|
|
637
|
+
}
|
|
638
|
+
async function getServiceStatus() {
|
|
639
|
+
const output = await runProcessCapture("powershell.exe", [
|
|
640
|
+
"-NoProfile",
|
|
641
|
+
"-ExecutionPolicy",
|
|
642
|
+
"Bypass",
|
|
643
|
+
"-Command",
|
|
644
|
+
servicePowerShell("status"),
|
|
645
|
+
]);
|
|
646
|
+
try {
|
|
647
|
+
const payload = JSON.parse(output.trim());
|
|
648
|
+
return {
|
|
649
|
+
installed: Boolean(payload.installed),
|
|
650
|
+
name: typeof payload.name === "string" ? payload.name : "tapi-service",
|
|
651
|
+
status: typeof payload.status === "string" ? payload.status : "unknown",
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
return { installed: false, name: "tapi-service", status: "unknown" };
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async function ensureServiceReadyForStudio(options) {
|
|
659
|
+
const status = await getServiceStatus();
|
|
660
|
+
if (!status.installed) {
|
|
661
|
+
console.log("Tapi Service is required and is not installed. Installing it now...");
|
|
662
|
+
const code = await installService(options);
|
|
663
|
+
if (code !== 0) {
|
|
664
|
+
throw new Error("Tapi Service install failed.");
|
|
665
|
+
}
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (status.status !== "Running") {
|
|
669
|
+
console.log("Starting Tapi Service...");
|
|
670
|
+
const output = await runProcessCapture("powershell.exe", [
|
|
671
|
+
"-NoProfile",
|
|
672
|
+
"-ExecutionPolicy",
|
|
673
|
+
"Bypass",
|
|
674
|
+
"-Command",
|
|
675
|
+
servicePowerShell("start"),
|
|
676
|
+
]);
|
|
677
|
+
if (output.trim()) {
|
|
678
|
+
console.log(output.trim());
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
async function installService(options) {
|
|
683
|
+
const event = await createCliWideEvent("tapi_cli.service_install", {
|
|
684
|
+
sdk_version: sdkVersion,
|
|
685
|
+
options: installEventOptions(options),
|
|
686
|
+
});
|
|
687
|
+
try {
|
|
688
|
+
await event.phase("host.check", {
|
|
689
|
+
platform: process.platform,
|
|
690
|
+
arch: process.arch,
|
|
691
|
+
});
|
|
692
|
+
ensureWindowsHost();
|
|
693
|
+
let installToken = options.installToken?.trim();
|
|
694
|
+
if (!installToken) {
|
|
695
|
+
console.log("Checking Tapi Service install approval...");
|
|
696
|
+
await event.phase("auth.install_token.request.start", {
|
|
697
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
698
|
+
channel: options.channel,
|
|
699
|
+
});
|
|
700
|
+
installToken = await obtainStudioInstallToken(options, event);
|
|
701
|
+
await event.phase("auth.install_token.request.success", {
|
|
702
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
703
|
+
channel: options.channel,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
console.log(`Fetching Tapi Service ${options.channel} manifest...`);
|
|
707
|
+
await event.phase("service_manifest.fetch.start", {
|
|
708
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
709
|
+
channel: options.channel,
|
|
710
|
+
});
|
|
711
|
+
const manifest = await fetchProtectedServiceManifest(options.apiBaseUrl, options.channel, installToken);
|
|
712
|
+
await event.phase("service_manifest.fetch.success", {
|
|
713
|
+
manifest: serviceManifestEventSummary(manifest),
|
|
714
|
+
});
|
|
715
|
+
ensureCompatibleServiceManifest(manifest);
|
|
716
|
+
const serviceCacheDir = getDefaultServiceCacheDir();
|
|
717
|
+
const downloadsDir = join(serviceCacheDir, "downloads");
|
|
718
|
+
const releasesDir = join(serviceCacheDir, "releases");
|
|
719
|
+
await mkdir(downloadsDir, { recursive: true });
|
|
720
|
+
await mkdir(releasesDir, { recursive: true });
|
|
721
|
+
const zipPath = join(downloadsDir, cachedServiceArtifactName(manifest));
|
|
722
|
+
const verified = await hasVerifiedCachedInstaller(zipPath, manifest.sha256);
|
|
723
|
+
if (verified) {
|
|
724
|
+
console.log(`Using cached Tapi Service artifact: ${zipPath}`);
|
|
725
|
+
await event.phase("cache.hit", { zipPath });
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
console.log(`Downloading Tapi Service ${manifest.version}...`);
|
|
729
|
+
await event.phase("download.start", {
|
|
730
|
+
url: manifest.url,
|
|
731
|
+
destination: zipPath,
|
|
732
|
+
expectedSha256: manifest.sha256,
|
|
733
|
+
});
|
|
734
|
+
await downloadAndVerify(manifest.url, zipPath, manifest.sha256, "Tapi Service artifact");
|
|
735
|
+
await event.phase("download.verified", {
|
|
736
|
+
zipPath,
|
|
737
|
+
expectedSha256: manifest.sha256,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
if (options.downloadOnly) {
|
|
741
|
+
console.log(`Downloaded Tapi Service artifact: ${zipPath}`);
|
|
742
|
+
await event.finish(true, {
|
|
743
|
+
zipPath,
|
|
744
|
+
downloadOnly: true,
|
|
745
|
+
});
|
|
746
|
+
return 0;
|
|
747
|
+
}
|
|
748
|
+
const releaseDir = join(releasesDir, safePathSegment(manifest.version));
|
|
749
|
+
await extractZip(zipPath, releaseDir);
|
|
750
|
+
const installer = join(releaseDir, manifest.installer || "install_tapi_service.ps1");
|
|
751
|
+
const hostExe = join(releaseDir, manifest.serviceHostExecutable || "tapi-service-host.exe");
|
|
752
|
+
if (!existsSync(installer)) {
|
|
753
|
+
throw new Error(`Tapi Service installer was not found after extraction: ${installer}`);
|
|
754
|
+
}
|
|
755
|
+
if (!existsSync(hostExe)) {
|
|
756
|
+
throw new Error(`Tapi Service host executable was not found after extraction: ${hostExe}`);
|
|
757
|
+
}
|
|
758
|
+
console.log(`Installing Tapi Service ${manifest.version}...`);
|
|
759
|
+
await event.phase("service_installer.start", {
|
|
760
|
+
installer,
|
|
761
|
+
hostExe,
|
|
762
|
+
releaseDir,
|
|
763
|
+
});
|
|
764
|
+
await runProcess("powershell.exe", [
|
|
765
|
+
"-NoProfile",
|
|
766
|
+
"-ExecutionPolicy",
|
|
767
|
+
"Bypass",
|
|
768
|
+
"-File",
|
|
769
|
+
installer,
|
|
770
|
+
"-ExecutablePath",
|
|
771
|
+
hostExe,
|
|
772
|
+
"-WorkingDirectory",
|
|
773
|
+
releaseDir,
|
|
774
|
+
]);
|
|
775
|
+
await event.finish(true, {
|
|
776
|
+
releaseDir,
|
|
777
|
+
version: manifest.version,
|
|
778
|
+
});
|
|
779
|
+
console.log("Tapi Service is installed and running.");
|
|
780
|
+
return 0;
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
await event.finish(false, {
|
|
784
|
+
error: errorDetails(error),
|
|
785
|
+
});
|
|
786
|
+
throw error;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
async function publishLocalApis(args) {
|
|
790
|
+
try {
|
|
791
|
+
const options = parseApiWorkspaceOptions(args);
|
|
792
|
+
const workspace = loadWorkspace();
|
|
793
|
+
const root = workspace?.root || process.cwd();
|
|
794
|
+
const sitemaps = await readLocalSitemaps(join(root, ".tapi", "sitemaps"));
|
|
795
|
+
const apis = await readLocalGeneratedApis(join(root, ".tapi", "apis"));
|
|
796
|
+
if (apis.length === 0) {
|
|
797
|
+
throw new Error("No local generated APIs found under .tapi/apis.");
|
|
798
|
+
}
|
|
799
|
+
let sitemapCount = 0;
|
|
800
|
+
for (const sitemap of sitemaps) {
|
|
801
|
+
const site = String(sitemap.site || "").trim();
|
|
802
|
+
if (!site || !isRecord(sitemap.siteMap)) {
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
await putJson(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/v1/sitemaps/${encodeURIComponent(site)}?project=${encodeURIComponent(options.projectId)}`, { site_map: sitemap.siteMap }, options.apiKey, options.projectId);
|
|
806
|
+
sitemapCount += 1;
|
|
807
|
+
}
|
|
808
|
+
let contracts = 0;
|
|
809
|
+
let requests = 0;
|
|
810
|
+
for (const api of apis) {
|
|
811
|
+
const apiId = String(api.id || api.name || "").trim();
|
|
812
|
+
if (!apiId) {
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
const site = String(api.site || "").trim();
|
|
816
|
+
await putJson(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/v1/generated-apis/contracts/${encodeURIComponent(apiId)}?project=${encodeURIComponent(options.projectId)}`, { generated_api: api }, options.apiKey, options.projectId);
|
|
817
|
+
contracts += 1;
|
|
818
|
+
const apiRequests = api.requests && typeof api.requests === "object" ? Object.values(api.requests) : [];
|
|
819
|
+
for (const request of apiRequests) {
|
|
820
|
+
if (!isRecord(request)) {
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
const requestId = String(request.id || request.key || "").trim();
|
|
824
|
+
const status = String(request.status || "").trim();
|
|
825
|
+
if (!requestId || !["ready", "published"].includes(status)) {
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
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);
|
|
829
|
+
requests += 1;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
const releaseResponse = await postJson(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/v1/generated-apis/releases?project=${encodeURIComponent(options.projectId)}`, { environment: "production" }, options.apiKey, options.projectId);
|
|
833
|
+
const release = isRecord(releaseResponse) && isRecord(releaseResponse.release) ? releaseResponse.release : {};
|
|
834
|
+
const releaseVersion = typeof release.version === "string" ? release.version : "unknown";
|
|
835
|
+
console.log(`Published ${requests} API request(s) from ${contracts} contract(s), synced ${sitemapCount} sitemap(s), activated release ${releaseVersion}.`);
|
|
836
|
+
return 0;
|
|
837
|
+
}
|
|
838
|
+
catch (error) {
|
|
839
|
+
console.error(formatError(error));
|
|
840
|
+
return 1;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
async function readLocalSitemaps(root) {
|
|
844
|
+
const sitemaps = [];
|
|
845
|
+
if (!existsSync(root)) {
|
|
846
|
+
return sitemaps;
|
|
847
|
+
}
|
|
848
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
849
|
+
for (const entry of entries) {
|
|
850
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
const path = join(root, entry.name);
|
|
854
|
+
try {
|
|
855
|
+
const payload = JSON.parse(readFileSync(path, "utf8"));
|
|
856
|
+
if (!isRecord(payload) || !isRecord(payload.siteMap)) {
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
const site = String(payload.site || payload.siteMap.site || "").trim();
|
|
860
|
+
if (site) {
|
|
861
|
+
sitemaps.push({ site, siteMap: payload.siteMap });
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
catch {
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return sitemaps;
|
|
869
|
+
}
|
|
870
|
+
async function readLocalGeneratedApis(root) {
|
|
871
|
+
const apis = [];
|
|
872
|
+
if (!existsSync(root)) {
|
|
873
|
+
return apis;
|
|
874
|
+
}
|
|
875
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
876
|
+
for (const entry of entries) {
|
|
877
|
+
const path = join(root, entry.name);
|
|
878
|
+
if (entry.isDirectory()) {
|
|
879
|
+
apis.push(...await readLocalGeneratedApis(path));
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
try {
|
|
886
|
+
const payload = JSON.parse(readFileSync(path, "utf8"));
|
|
887
|
+
if (isRecord(payload) && isRecord(payload.api)) {
|
|
888
|
+
apis.push(payload.api);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
catch {
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return apis;
|
|
896
|
+
}
|
|
897
|
+
async function putJson(url, body, apiKey, projectId) {
|
|
898
|
+
return requestJson("PUT", url, body, apiKey, projectId);
|
|
899
|
+
}
|
|
900
|
+
async function postJson(url, body, apiKey, projectId) {
|
|
901
|
+
return requestJson("POST", url, body, apiKey, projectId);
|
|
902
|
+
}
|
|
903
|
+
async function requestJson(method, url, body, apiKey, projectId) {
|
|
904
|
+
const response = await fetch(url, {
|
|
905
|
+
method,
|
|
906
|
+
headers: {
|
|
907
|
+
Authorization: `Bearer ${apiKey}`,
|
|
908
|
+
"Content-Type": "application/json",
|
|
909
|
+
"X-Tapi-Project": projectId,
|
|
910
|
+
},
|
|
911
|
+
body: JSON.stringify(body),
|
|
912
|
+
});
|
|
913
|
+
const text = await response.text();
|
|
914
|
+
if (!response.ok) {
|
|
915
|
+
throw new Error(`Tapi publish request failed: HTTP ${response.status} ${text}`);
|
|
916
|
+
}
|
|
917
|
+
if (!text.trim()) {
|
|
918
|
+
return {};
|
|
919
|
+
}
|
|
920
|
+
try {
|
|
921
|
+
return JSON.parse(text);
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
return {};
|
|
925
|
+
}
|
|
926
|
+
}
|
|
262
927
|
export function getDefaultStudioCacheDir() {
|
|
263
928
|
if (process.platform === "win32") {
|
|
264
929
|
const localAppData = process.env.LOCALAPPDATA ??
|
|
@@ -267,6 +932,14 @@ export function getDefaultStudioCacheDir() {
|
|
|
267
932
|
}
|
|
268
933
|
return join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "tapi", "studio");
|
|
269
934
|
}
|
|
935
|
+
export function getDefaultServiceCacheDir() {
|
|
936
|
+
if (process.platform === "win32") {
|
|
937
|
+
const localAppData = process.env.LOCALAPPDATA ??
|
|
938
|
+
(process.env.USERPROFILE ? join(process.env.USERPROFILE, "AppData", "Local") : join(homedir(), "AppData", "Local"));
|
|
939
|
+
return join(localAppData, "Tapi", "Service");
|
|
940
|
+
}
|
|
941
|
+
return join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "tapi", "service");
|
|
942
|
+
}
|
|
270
943
|
export class CliWideEvent {
|
|
271
944
|
filePath;
|
|
272
945
|
startedAt = performance.now();
|
|
@@ -432,12 +1105,46 @@ export function validateStudioManifest(input) {
|
|
|
432
1105
|
commitShort: optionalString(record, "commitShort"),
|
|
433
1106
|
ref: optionalString(record, "ref"),
|
|
434
1107
|
installerKind: optionalString(record, "installerKind"),
|
|
1108
|
+
serverExecutable: optionalString(record, "serverExecutable"),
|
|
435
1109
|
};
|
|
436
1110
|
if (!/^[a-f0-9]{64}$/i.test(manifest.sha256)) {
|
|
437
1111
|
throw new Error("Studio manifest sha256 must be a 64-character hex digest.");
|
|
438
1112
|
}
|
|
439
1113
|
return manifest;
|
|
440
1114
|
}
|
|
1115
|
+
export function validateServiceReleaseManifest(input) {
|
|
1116
|
+
if (!input || typeof input !== "object") {
|
|
1117
|
+
throw new Error("Tapi Service manifest response was not a JSON object.");
|
|
1118
|
+
}
|
|
1119
|
+
const record = input;
|
|
1120
|
+
const contents = Array.isArray(record.contents)
|
|
1121
|
+
? record.contents.filter((item) => typeof item === "string")
|
|
1122
|
+
: undefined;
|
|
1123
|
+
const manifest = {
|
|
1124
|
+
product: optionalString(record, "product"),
|
|
1125
|
+
version: requiredString(record, "version"),
|
|
1126
|
+
channel: requiredString(record, "channel"),
|
|
1127
|
+
platform: requiredString(record, "platform"),
|
|
1128
|
+
artifactName: requiredString(record, "artifactName"),
|
|
1129
|
+
url: normalizeHttpUrl(requiredString(record, "url"), "Tapi Service manifest url"),
|
|
1130
|
+
sha256: requiredString(record, "sha256").toLowerCase(),
|
|
1131
|
+
sizeBytes: optionalNumber(record, "sizeBytes"),
|
|
1132
|
+
serviceHostExecutable: optionalString(record, "serviceHostExecutable"),
|
|
1133
|
+
workerExecutable: optionalString(record, "workerExecutable"),
|
|
1134
|
+
installer: optionalString(record, "installer"),
|
|
1135
|
+
uninstaller: optionalString(record, "uninstaller"),
|
|
1136
|
+
builtAt: optionalString(record, "builtAt"),
|
|
1137
|
+
commit: optionalString(record, "commit"),
|
|
1138
|
+
commitShort: optionalString(record, "commitShort"),
|
|
1139
|
+
ref: optionalString(record, "ref"),
|
|
1140
|
+
mobileChrome: isRecord(record.mobileChrome) ? record.mobileChrome : undefined,
|
|
1141
|
+
contents,
|
|
1142
|
+
};
|
|
1143
|
+
if (!/^[a-f0-9]{64}$/i.test(manifest.sha256)) {
|
|
1144
|
+
throw new Error("Tapi Service manifest sha256 must be a 64-character hex digest.");
|
|
1145
|
+
}
|
|
1146
|
+
return manifest;
|
|
1147
|
+
}
|
|
441
1148
|
export function compareVersions(left, right) {
|
|
442
1149
|
const leftParts = versionCore(left);
|
|
443
1150
|
const rightParts = versionCore(right);
|
|
@@ -503,6 +1210,15 @@ async function installStudio(options) {
|
|
|
503
1210
|
minSdkVersion: manifest.minSdkVersion,
|
|
504
1211
|
platform: manifest.platform,
|
|
505
1212
|
});
|
|
1213
|
+
if (isPortableStudioServerManifest(manifest)) {
|
|
1214
|
+
const serverExe = await installPortableStudioServer(options, manifest, event);
|
|
1215
|
+
await event.finish(true, {
|
|
1216
|
+
serverExe,
|
|
1217
|
+
downloadOnly: options.downloadOnly,
|
|
1218
|
+
installerKind: manifest.installerKind,
|
|
1219
|
+
});
|
|
1220
|
+
return 0;
|
|
1221
|
+
}
|
|
506
1222
|
await mkdir(options.cacheDir, { recursive: true });
|
|
507
1223
|
const installerPath = join(options.cacheDir, cachedInstallerName(manifest));
|
|
508
1224
|
await event.phase("cache.check.start", {
|
|
@@ -602,6 +1318,17 @@ async function obtainStudioInstallToken(options, event) {
|
|
|
602
1318
|
}
|
|
603
1319
|
async function openStudio(options) {
|
|
604
1320
|
ensureWindowsHost();
|
|
1321
|
+
if (options.downloadOnly) {
|
|
1322
|
+
throw new Error("--download-only is only supported with `tapi studio install`.");
|
|
1323
|
+
}
|
|
1324
|
+
await ensureServiceReadyForStudio(options);
|
|
1325
|
+
if (!options.exePath) {
|
|
1326
|
+
const portable = await ensurePortableStudioServer(options);
|
|
1327
|
+
if (portable) {
|
|
1328
|
+
await launchPortableStudioServer(portable.exePath, options);
|
|
1329
|
+
return 0;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
605
1332
|
const exePath = options.exePath ?? getStudioExecutableCandidates().find((candidate) => existsSync(candidate));
|
|
606
1333
|
if (!exePath || !existsSync(exePath)) {
|
|
607
1334
|
console.error("Tapi Studio executable was not found.");
|
|
@@ -610,6 +1337,7 @@ async function openStudio(options) {
|
|
|
610
1337
|
}
|
|
611
1338
|
const child = spawn(exePath, [], {
|
|
612
1339
|
detached: true,
|
|
1340
|
+
env: buildStudioLaunchEnv(options),
|
|
613
1341
|
stdio: "ignore",
|
|
614
1342
|
windowsHide: false,
|
|
615
1343
|
});
|
|
@@ -617,6 +1345,135 @@ async function openStudio(options) {
|
|
|
617
1345
|
console.log(`Opened Tapi Studio: ${exePath}`);
|
|
618
1346
|
return 0;
|
|
619
1347
|
}
|
|
1348
|
+
async function ensurePortableStudioServer(options) {
|
|
1349
|
+
const event = await createCliWideEvent("tapi_cli.studio_server_ensure", {
|
|
1350
|
+
sdk_version: sdkVersion,
|
|
1351
|
+
options: installEventOptions(options),
|
|
1352
|
+
});
|
|
1353
|
+
try {
|
|
1354
|
+
let installToken = options.installToken?.trim();
|
|
1355
|
+
if (!installToken) {
|
|
1356
|
+
await event.phase("auth.install_token.request.start", {
|
|
1357
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
1358
|
+
channel: options.channel,
|
|
1359
|
+
});
|
|
1360
|
+
installToken = await obtainStudioInstallToken(options, event);
|
|
1361
|
+
await event.phase("auth.install_token.request.success", {
|
|
1362
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
1363
|
+
channel: options.channel,
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
await event.phase("manifest.fetch.start", {
|
|
1367
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
1368
|
+
channel: options.channel,
|
|
1369
|
+
});
|
|
1370
|
+
const manifest = await fetchProtectedStudioManifest(options.apiBaseUrl, options.channel, installToken);
|
|
1371
|
+
ensureCompatibleManifest(manifest);
|
|
1372
|
+
await event.phase("manifest.fetch.success", {
|
|
1373
|
+
manifest: manifestEventSummary(manifest),
|
|
1374
|
+
});
|
|
1375
|
+
if (!isPortableStudioServerManifest(manifest)) {
|
|
1376
|
+
await event.finish(true, {
|
|
1377
|
+
installerKind: manifest.installerKind || "legacy",
|
|
1378
|
+
});
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
const existing = findPortableStudioServerExe(portableStudioServerReleaseDir(manifest), manifest);
|
|
1382
|
+
if (existing) {
|
|
1383
|
+
await event.finish(true, {
|
|
1384
|
+
serverExe: existing,
|
|
1385
|
+
cacheHit: true,
|
|
1386
|
+
});
|
|
1387
|
+
return { exePath: existing, manifest };
|
|
1388
|
+
}
|
|
1389
|
+
const exePath = await installPortableStudioServer(options, manifest, event);
|
|
1390
|
+
await event.finish(true, {
|
|
1391
|
+
serverExe: exePath,
|
|
1392
|
+
cacheHit: false,
|
|
1393
|
+
});
|
|
1394
|
+
return { exePath, manifest };
|
|
1395
|
+
}
|
|
1396
|
+
catch (error) {
|
|
1397
|
+
await event.finish(false, {
|
|
1398
|
+
error: errorDetails(error),
|
|
1399
|
+
});
|
|
1400
|
+
throw error;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
async function installPortableStudioServer(options, manifest, event) {
|
|
1404
|
+
await mkdir(options.cacheDir, { recursive: true });
|
|
1405
|
+
const artifactPath = join(options.cacheDir, cachedInstallerName(manifest));
|
|
1406
|
+
const verified = await hasVerifiedCachedInstaller(artifactPath, manifest.sha256);
|
|
1407
|
+
if (verified) {
|
|
1408
|
+
console.log(`Using cached Tapi Studio server artifact: ${artifactPath}`);
|
|
1409
|
+
await event.phase("cache.hit", { artifactPath });
|
|
1410
|
+
}
|
|
1411
|
+
else {
|
|
1412
|
+
console.log(`Downloading Tapi Studio server ${manifest.version}...`);
|
|
1413
|
+
await event.phase("download.start", {
|
|
1414
|
+
url: manifest.url,
|
|
1415
|
+
destination: artifactPath,
|
|
1416
|
+
expectedSha256: manifest.sha256,
|
|
1417
|
+
});
|
|
1418
|
+
await downloadAndVerify(manifest.url, artifactPath, manifest.sha256, "Tapi Studio server artifact");
|
|
1419
|
+
await event.phase("download.verified", {
|
|
1420
|
+
artifactPath,
|
|
1421
|
+
expectedSha256: manifest.sha256,
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
if (options.downloadOnly) {
|
|
1425
|
+
console.log(`Downloaded Tapi Studio server artifact: ${artifactPath}`);
|
|
1426
|
+
return artifactPath;
|
|
1427
|
+
}
|
|
1428
|
+
const releaseDir = portableStudioServerReleaseDir(manifest);
|
|
1429
|
+
await extractZip(artifactPath, releaseDir);
|
|
1430
|
+
const exePath = findPortableStudioServerExe(releaseDir, manifest);
|
|
1431
|
+
if (!exePath) {
|
|
1432
|
+
throw new Error(`Tapi Studio server executable was not found after extraction: ${releaseDir}`);
|
|
1433
|
+
}
|
|
1434
|
+
console.log(`Installed Tapi Studio server ${manifest.version}: ${releaseDir}`);
|
|
1435
|
+
return exePath;
|
|
1436
|
+
}
|
|
1437
|
+
async function launchPortableStudioServer(exePath, options) {
|
|
1438
|
+
const host = "127.0.0.1";
|
|
1439
|
+
const port = await chooseStudioPort();
|
|
1440
|
+
const env = buildStudioLaunchEnv(options);
|
|
1441
|
+
env.TAPI_RUNTIME_MODE = "installed";
|
|
1442
|
+
env.TAPI_RUNTIME_NAMESPACE = "installed";
|
|
1443
|
+
env.TAPI_RUNTIME_CONTROL_PORT = env.TAPI_RUNTIME_CONTROL_PORT || "8765";
|
|
1444
|
+
env.TAPI_PROCESS_ROLE = "studio_server";
|
|
1445
|
+
env.TAPI_STUDIO_OPEN_BROWSER = "0";
|
|
1446
|
+
env.TAPI_STUDIO_HOST = host;
|
|
1447
|
+
env.TAPI_STUDIO_PORT = String(port);
|
|
1448
|
+
env.TAPI_STUDIO_SERVER_PORT = String(port);
|
|
1449
|
+
const child = spawn(exePath, [], {
|
|
1450
|
+
detached: true,
|
|
1451
|
+
env,
|
|
1452
|
+
stdio: "ignore",
|
|
1453
|
+
windowsHide: true,
|
|
1454
|
+
});
|
|
1455
|
+
child.unref();
|
|
1456
|
+
const url = `http://${host}:${port}`;
|
|
1457
|
+
openBrowser(url);
|
|
1458
|
+
console.log(`Opened Tapi Studio: ${url}`);
|
|
1459
|
+
}
|
|
1460
|
+
async function chooseStudioPort(preferred = 18766) {
|
|
1461
|
+
for (let port = preferred; port < preferred + 25; port += 1) {
|
|
1462
|
+
if (await isPortAvailable(port)) {
|
|
1463
|
+
return port;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
throw new Error(`No available local Studio port found starting at ${preferred}.`);
|
|
1467
|
+
}
|
|
1468
|
+
async function isPortAvailable(port) {
|
|
1469
|
+
return await new Promise((resolvePromise) => {
|
|
1470
|
+
const server = createNetServer();
|
|
1471
|
+
server.once("error", () => resolvePromise(false));
|
|
1472
|
+
server.listen(port, "127.0.0.1", () => {
|
|
1473
|
+
server.close(() => resolvePromise(true));
|
|
1474
|
+
});
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
620
1477
|
async function runDoctor(options) {
|
|
621
1478
|
console.log(`Tapi SDK: ${sdkVersion}`);
|
|
622
1479
|
console.log(`Node: ${process.version}`);
|
|
@@ -625,6 +1482,8 @@ async function runDoctor(options) {
|
|
|
625
1482
|
console.log(`Studio channel: ${options.channel}`);
|
|
626
1483
|
console.log(`Studio manifest: ${options.manifestUrl}`);
|
|
627
1484
|
console.log(`Studio cache: ${options.cacheDir}`);
|
|
1485
|
+
console.log(`Workspace: ${options.workspaceRoot ?? "not found"}`);
|
|
1486
|
+
console.log(`Project: ${options.projectId ?? "not set"}`);
|
|
628
1487
|
const exePath = options.exePath ?? getStudioExecutableCandidates().find((candidate) => existsSync(candidate));
|
|
629
1488
|
console.log(`Studio executable: ${exePath && existsSync(exePath) ? exePath : "not found"}`);
|
|
630
1489
|
try {
|
|
@@ -638,6 +1497,27 @@ async function runDoctor(options) {
|
|
|
638
1497
|
}
|
|
639
1498
|
return 0;
|
|
640
1499
|
}
|
|
1500
|
+
function buildStudioLaunchEnv(options) {
|
|
1501
|
+
const env = { ...process.env };
|
|
1502
|
+
env.TAPI_STUDIO_API_BASE_URL = options.apiBaseUrl;
|
|
1503
|
+
env.TAPI_BASE_URL = env.TAPI_BASE_URL || options.apiBaseUrl;
|
|
1504
|
+
if (options.workspaceRoot) {
|
|
1505
|
+
env.TAPI_WORKSPACE_ROOT = options.workspaceRoot;
|
|
1506
|
+
}
|
|
1507
|
+
if (options.workspace?.configPath) {
|
|
1508
|
+
env.TAPI_WORKSPACE_CONFIG = options.workspace.configPath;
|
|
1509
|
+
}
|
|
1510
|
+
if (options.projectId) {
|
|
1511
|
+
env.TAPI_PROJECT_ID = options.projectId;
|
|
1512
|
+
}
|
|
1513
|
+
if (options.projectSlug) {
|
|
1514
|
+
env.TAPI_PROJECT_SLUG = options.projectSlug;
|
|
1515
|
+
}
|
|
1516
|
+
if (options.workspaceMode) {
|
|
1517
|
+
env.TAPI_STUDIO_WORKSPACE_MODE = "1";
|
|
1518
|
+
}
|
|
1519
|
+
return env;
|
|
1520
|
+
}
|
|
641
1521
|
async function fetchProtectedStudioManifest(apiBaseUrl, channel, installToken, fetchImpl = fetch) {
|
|
642
1522
|
const response = await fetchImpl(`${apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/studio/releases/${channel}`, {
|
|
643
1523
|
headers: {
|
|
@@ -654,6 +1534,22 @@ async function fetchProtectedStudioManifest(apiBaseUrl, channel, installToken, f
|
|
|
654
1534
|
}
|
|
655
1535
|
return validateStudioManifest(responseBody);
|
|
656
1536
|
}
|
|
1537
|
+
async function fetchProtectedServiceManifest(apiBaseUrl, channel, installToken, fetchImpl = fetch) {
|
|
1538
|
+
const response = await fetchImpl(`${apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/service/releases/${channel}`, {
|
|
1539
|
+
headers: {
|
|
1540
|
+
Accept: "application/json",
|
|
1541
|
+
Authorization: `Bearer ${installToken}`,
|
|
1542
|
+
},
|
|
1543
|
+
});
|
|
1544
|
+
const responseBody = await readJsonBody(response);
|
|
1545
|
+
if (!response.ok) {
|
|
1546
|
+
const detail = typeof responseBody?.detail === "string" ? responseBody.detail : "";
|
|
1547
|
+
throw new Error(detail
|
|
1548
|
+
? `Failed to fetch protected Tapi Service manifest: HTTP ${response.status} (${detail})`
|
|
1549
|
+
: `Failed to fetch protected Tapi Service manifest: HTTP ${response.status}`);
|
|
1550
|
+
}
|
|
1551
|
+
return validateServiceReleaseManifest(responseBody);
|
|
1552
|
+
}
|
|
657
1553
|
async function fetchStudioManifest(manifestUrl, fetchImpl = fetch) {
|
|
658
1554
|
const response = await fetchImpl(manifestUrl, {
|
|
659
1555
|
headers: {
|
|
@@ -696,24 +1592,24 @@ async function requestStudioInstallToken(apiBaseUrl, channel, firebaseIdToken, f
|
|
|
696
1592
|
expiresAt: typeof payload?.expiresAt === "string" ? payload.expiresAt : undefined,
|
|
697
1593
|
};
|
|
698
1594
|
}
|
|
699
|
-
async function downloadAndVerify(url, destination, expectedSha256) {
|
|
1595
|
+
async function downloadAndVerify(url, destination, expectedSha256, label = "Studio installer") {
|
|
700
1596
|
const partialPath = `${destination}.partial`;
|
|
701
|
-
await downloadFile(url, partialPath);
|
|
1597
|
+
await downloadFile(url, partialPath, label);
|
|
702
1598
|
const actualSha256 = await sha256File(partialPath);
|
|
703
1599
|
if (actualSha256 !== expectedSha256.toLowerCase()) {
|
|
704
1600
|
await unlinkIfExists(partialPath);
|
|
705
|
-
throw new Error(
|
|
1601
|
+
throw new Error(`${label} checksum mismatch. Expected ${expectedSha256}, got ${actualSha256}.`);
|
|
706
1602
|
}
|
|
707
1603
|
await rename(partialPath, destination);
|
|
708
1604
|
console.log(`Verified SHA256: ${actualSha256}`);
|
|
709
1605
|
}
|
|
710
|
-
async function downloadFile(url, destination) {
|
|
1606
|
+
async function downloadFile(url, destination, label = "download") {
|
|
711
1607
|
const response = await fetch(url);
|
|
712
1608
|
if (!response.ok) {
|
|
713
|
-
throw new Error(`Failed to download
|
|
1609
|
+
throw new Error(`Failed to download ${label}: HTTP ${response.status}`);
|
|
714
1610
|
}
|
|
715
1611
|
if (!response.body) {
|
|
716
|
-
throw new Error(
|
|
1612
|
+
throw new Error(`${label} response did not include a body.`);
|
|
717
1613
|
}
|
|
718
1614
|
await pipeline(Readable.fromWeb(response.body), createWriteStream(destination));
|
|
719
1615
|
}
|
|
@@ -937,6 +1833,32 @@ async function runProcess(command, args) {
|
|
|
937
1833
|
});
|
|
938
1834
|
});
|
|
939
1835
|
}
|
|
1836
|
+
async function runProcessCapture(command, args) {
|
|
1837
|
+
return await new Promise((resolvePromise, rejectPromise) => {
|
|
1838
|
+
const child = spawn(command, args, {
|
|
1839
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1840
|
+
windowsHide: true,
|
|
1841
|
+
});
|
|
1842
|
+
let stdout = "";
|
|
1843
|
+
let stderr = "";
|
|
1844
|
+
child.stdout.on("data", (chunk) => {
|
|
1845
|
+
stdout += String(chunk);
|
|
1846
|
+
});
|
|
1847
|
+
child.stderr.on("data", (chunk) => {
|
|
1848
|
+
stderr += String(chunk);
|
|
1849
|
+
});
|
|
1850
|
+
child.once("error", rejectPromise);
|
|
1851
|
+
child.once("exit", (code) => {
|
|
1852
|
+
const err = stderr.trim();
|
|
1853
|
+
if (code === 0) {
|
|
1854
|
+
resolvePromise(stdout);
|
|
1855
|
+
}
|
|
1856
|
+
else {
|
|
1857
|
+
rejectPromise(new Error(err || `Process exited with code ${code ?? "unknown"}.`));
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
940
1862
|
function ensureWindowsHost() {
|
|
941
1863
|
if (process.platform !== "win32" || process.arch !== "x64") {
|
|
942
1864
|
throw new Error("Tapi Studio desktop installer is currently published for Windows x64 only.");
|
|
@@ -950,10 +1872,58 @@ function ensureCompatibleManifest(manifest) {
|
|
|
950
1872
|
throw new Error(`Tapi Studio ${manifest.version} requires @tapi-dev/sdk >= ${manifest.minSdkVersion}; installed SDK is ${sdkVersion}.`);
|
|
951
1873
|
}
|
|
952
1874
|
}
|
|
1875
|
+
function ensureCompatibleServiceManifest(manifest) {
|
|
1876
|
+
if (manifest.platform !== SUPPORTED_STUDIO_PLATFORM) {
|
|
1877
|
+
throw new Error(`This SDK expected ${SUPPORTED_STUDIO_PLATFORM}, but the Tapi Service manifest points to ${manifest.platform}.`);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
953
1880
|
function cachedInstallerName(manifest) {
|
|
954
1881
|
const rawName = manifest.artifactName || basename(new URL(manifest.url).pathname) || `TapiStudioSetup-${manifest.version}.exe`;
|
|
955
1882
|
return rawName.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
956
1883
|
}
|
|
1884
|
+
function isPortableStudioServerManifest(manifest) {
|
|
1885
|
+
const kind = String(manifest.installerKind || "").toLowerCase();
|
|
1886
|
+
return kind === "portable-server" || manifest.artifactName.toLowerCase().endsWith(".zip");
|
|
1887
|
+
}
|
|
1888
|
+
function portableStudioServerReleaseDir(manifest) {
|
|
1889
|
+
return join(getDefaultPortableStudioServerReleasesDir(), safePathSegment(manifest.version));
|
|
1890
|
+
}
|
|
1891
|
+
function getDefaultPortableStudioServerReleasesDir() {
|
|
1892
|
+
if (process.platform === "win32") {
|
|
1893
|
+
const localAppData = process.env.LOCALAPPDATA ??
|
|
1894
|
+
(process.env.USERPROFILE ? join(process.env.USERPROFILE, "AppData", "Local") : join(homedir(), "AppData", "Local"));
|
|
1895
|
+
return join(localAppData, "Tapi", "Studio", "server", "releases");
|
|
1896
|
+
}
|
|
1897
|
+
return join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "tapi", "studio", "server", "releases");
|
|
1898
|
+
}
|
|
1899
|
+
function findPortableStudioServerExe(releaseDir, manifest) {
|
|
1900
|
+
const executable = manifest.serverExecutable || "tapi-studio-server.exe";
|
|
1901
|
+
const baseName = executable.toLowerCase().endsWith(".exe")
|
|
1902
|
+
? executable.slice(0, -4)
|
|
1903
|
+
: executable;
|
|
1904
|
+
const candidates = [
|
|
1905
|
+
join(releaseDir, executable),
|
|
1906
|
+
join(releaseDir, baseName, `${baseName}.exe`),
|
|
1907
|
+
join(releaseDir, "tapi-studio-server.exe"),
|
|
1908
|
+
join(releaseDir, "tapi-studio-server", "tapi-studio-server.exe"),
|
|
1909
|
+
];
|
|
1910
|
+
return candidates.find((candidate) => existsSync(candidate));
|
|
1911
|
+
}
|
|
1912
|
+
function cachedServiceArtifactName(manifest) {
|
|
1913
|
+
const rawName = manifest.artifactName || basename(new URL(manifest.url).pathname) || `TapiService-${manifest.version}.zip`;
|
|
1914
|
+
return rawName.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
1915
|
+
}
|
|
1916
|
+
async function extractZip(zipPath, destination) {
|
|
1917
|
+
await rm(destination, { recursive: true, force: true });
|
|
1918
|
+
await mkdir(destination, { recursive: true });
|
|
1919
|
+
await runProcessCapture("powershell.exe", [
|
|
1920
|
+
"-NoProfile",
|
|
1921
|
+
"-ExecutionPolicy",
|
|
1922
|
+
"Bypass",
|
|
1923
|
+
"-Command",
|
|
1924
|
+
`Expand-Archive -LiteralPath ${powerShellSingleQuoted(zipPath)} -DestinationPath ${powerShellSingleQuoted(destination)} -Force`,
|
|
1925
|
+
]);
|
|
1926
|
+
}
|
|
957
1927
|
function installEventOptions(options) {
|
|
958
1928
|
return {
|
|
959
1929
|
channel: options.channel,
|
|
@@ -967,6 +1937,22 @@ function installEventOptions(options) {
|
|
|
967
1937
|
installTokenProvided: Boolean(options.installToken),
|
|
968
1938
|
};
|
|
969
1939
|
}
|
|
1940
|
+
function serviceManifestEventSummary(manifest) {
|
|
1941
|
+
return {
|
|
1942
|
+
product: manifest.product,
|
|
1943
|
+
version: manifest.version,
|
|
1944
|
+
channel: manifest.channel,
|
|
1945
|
+
platform: manifest.platform,
|
|
1946
|
+
artifactName: manifest.artifactName,
|
|
1947
|
+
url: manifest.url,
|
|
1948
|
+
sha256: manifest.sha256,
|
|
1949
|
+
sizeBytes: manifest.sizeBytes,
|
|
1950
|
+
serviceHostExecutable: manifest.serviceHostExecutable,
|
|
1951
|
+
workerExecutable: manifest.workerExecutable,
|
|
1952
|
+
installer: manifest.installer,
|
|
1953
|
+
commitShort: manifest.commitShort,
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
970
1956
|
function manifestEventSummary(manifest) {
|
|
971
1957
|
return {
|
|
972
1958
|
product: manifest.product,
|
|
@@ -982,6 +1968,12 @@ function manifestEventSummary(manifest) {
|
|
|
982
1968
|
bundledService: manifest.bundledService,
|
|
983
1969
|
};
|
|
984
1970
|
}
|
|
1971
|
+
function safePathSegment(value) {
|
|
1972
|
+
return String(value || "release").replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || "release";
|
|
1973
|
+
}
|
|
1974
|
+
function powerShellSingleQuoted(value) {
|
|
1975
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
1976
|
+
}
|
|
985
1977
|
function parseChannel(value) {
|
|
986
1978
|
if (value === "pilot" || value === "stable" || value === "nightly") {
|
|
987
1979
|
return value;
|
|
@@ -1067,52 +2059,117 @@ function readSdkVersion() {
|
|
|
1067
2059
|
function printHelp() {
|
|
1068
2060
|
console.log(`Tapi CLI
|
|
1069
2061
|
|
|
1070
|
-
Usage:
|
|
1071
|
-
tapi
|
|
1072
|
-
tapi
|
|
1073
|
-
tapi studio
|
|
1074
|
-
tapi
|
|
1075
|
-
tapi
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
2062
|
+
Usage:
|
|
2063
|
+
tapi init --project PROJECT
|
|
2064
|
+
tapi link --project PROJECT
|
|
2065
|
+
tapi studio install [--channel pilot] [--api-base-url URL]
|
|
2066
|
+
tapi studio
|
|
2067
|
+
tapi studio open
|
|
2068
|
+
tapi studio doctor
|
|
2069
|
+
tapi apis describe <namespace.operation>
|
|
2070
|
+
tapi apis sync
|
|
2071
|
+
tapi apis generate
|
|
2072
|
+
tapi publish
|
|
2073
|
+
tapi service status
|
|
2074
|
+
tapi doctor
|
|
2075
|
+
|
|
2076
|
+
Commands:
|
|
2077
|
+
init Create .tapi/project.json for this repo
|
|
2078
|
+
link Rebind this repo to an existing Tapi project
|
|
2079
|
+
studio install Download, verify, and run the Tapi Studio installer
|
|
2080
|
+
studio Open Tapi Studio for this repo
|
|
2081
|
+
studio open Open Tapi Studio for this repo
|
|
2082
|
+
studio doctor Check local SDK and Studio release configuration
|
|
2083
|
+
apis describe Print a generated website API input/output contract
|
|
2084
|
+
apis sync Save the published API catalog to .tapi/generated
|
|
2085
|
+
apis generate Generate a TypeScript runtime wrapper from the catalog
|
|
2086
|
+
publish Upload local .tapi API drafts and publish ready requests
|
|
2087
|
+
service Inspect or control the local Tapi Windows service
|
|
2088
|
+
doctor Alias for studio doctor
|
|
1083
2089
|
`);
|
|
1084
2090
|
}
|
|
1085
2091
|
function printApisHelp() {
|
|
1086
|
-
console.log(`Tapi generated website API commands
|
|
1087
|
-
|
|
1088
|
-
Usage:
|
|
1089
|
-
tapi apis describe <namespace.operation> [--api-base-url URL] [--api-key KEY] [--
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
--api-
|
|
1095
|
-
--
|
|
2092
|
+
console.log(`Tapi generated website API commands
|
|
2093
|
+
|
|
2094
|
+
Usage:
|
|
2095
|
+
tapi apis describe <namespace.operation> [--api-base-url URL] [--api-key KEY] [--project PROJECT]
|
|
2096
|
+
tapi apis sync [--api-base-url URL] [--api-key KEY] [--project PROJECT]
|
|
2097
|
+
tapi apis generate [--api-base-url URL] [--api-key KEY] [--project PROJECT] [--out FILE]
|
|
2098
|
+
|
|
2099
|
+
Options:
|
|
2100
|
+
--api-base-url <url> Tapi API base URL
|
|
2101
|
+
--server <url> Alias for --api-base-url
|
|
2102
|
+
--api-key <key> Tapi API key; defaults to TAPI_API_KEY
|
|
2103
|
+
--project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
|
|
2104
|
+
--catalog <path> Catalog JSON output path
|
|
2105
|
+
--out <path> Generated TypeScript output path
|
|
1096
2106
|
`);
|
|
1097
2107
|
}
|
|
1098
2108
|
function printStudioHelp() {
|
|
1099
|
-
console.log(`Tapi Studio commands
|
|
1100
|
-
|
|
1101
|
-
Usage:
|
|
1102
|
-
tapi studio
|
|
1103
|
-
tapi studio
|
|
1104
|
-
tapi studio
|
|
1105
|
-
|
|
1106
|
-
|
|
2109
|
+
console.log(`Tapi Studio commands
|
|
2110
|
+
|
|
2111
|
+
Usage:
|
|
2112
|
+
tapi studio [options]
|
|
2113
|
+
tapi studio install [options]
|
|
2114
|
+
tapi studio open [options]
|
|
2115
|
+
tapi studio doctor [options]
|
|
2116
|
+
|
|
2117
|
+
Options:
|
|
1107
2118
|
--channel <name> Release channel: pilot, stable, or nightly
|
|
1108
2119
|
--api-base-url <url> Tapi API base URL for approval and protected downloads
|
|
1109
|
-
--server <url> Alias for --api-base-url
|
|
1110
|
-
--
|
|
1111
|
-
--
|
|
1112
|
-
--
|
|
1113
|
-
--
|
|
1114
|
-
--
|
|
1115
|
-
--
|
|
2120
|
+
--server <url> Alias for --api-base-url
|
|
2121
|
+
--workspace <path> Search this directory for .tapi/project.json
|
|
2122
|
+
--no-workspace Do not load .tapi/project.json
|
|
2123
|
+
--project <id> Tapi project id for workspace-bound Studio
|
|
2124
|
+
--project-slug <slug> Optional display slug for the bound project
|
|
2125
|
+
--install-token <tok> Preissued Studio install token (skips browser sign-in)
|
|
2126
|
+
--manifest <url> Exact release manifest URL for doctor only
|
|
2127
|
+
--cache-dir <path> Installer download cache directory
|
|
2128
|
+
--download-only Download and verify without running the installer
|
|
2129
|
+
--silent Run the NSIS installer with /S
|
|
2130
|
+
--exe <path> Tapi Studio executable path for open/doctor
|
|
2131
|
+
`);
|
|
2132
|
+
}
|
|
2133
|
+
function printWorkspaceHelp(command) {
|
|
2134
|
+
console.log(`Tapi workspace ${command}
|
|
2135
|
+
|
|
2136
|
+
Usage:
|
|
2137
|
+
tapi ${command} --project PROJECT [--api-base-url URL] [--root PATH] [--force]
|
|
2138
|
+
|
|
2139
|
+
Options:
|
|
2140
|
+
--project <id> Tapi project id/name to bind this repo to
|
|
2141
|
+
--project-slug <slug> Optional display slug
|
|
2142
|
+
--api-base-url <url> Tapi API base URL stored in .tapi/project.json
|
|
2143
|
+
--server <url> Alias for --api-base-url
|
|
2144
|
+
--root <path> Directory where .tapi/project.json should be written
|
|
2145
|
+
--force Overwrite an existing workspace config
|
|
2146
|
+
`);
|
|
2147
|
+
}
|
|
2148
|
+
function printServiceHelp() {
|
|
2149
|
+
console.log(`Tapi service commands
|
|
2150
|
+
|
|
2151
|
+
Usage:
|
|
2152
|
+
tapi service status
|
|
2153
|
+
tapi service start
|
|
2154
|
+
tapi service stop
|
|
2155
|
+
tapi service restart
|
|
2156
|
+
tapi service repair
|
|
2157
|
+
|
|
2158
|
+
Commands:
|
|
2159
|
+
status Print installed/running status for tapi-service
|
|
2160
|
+
start Start tapi-service
|
|
2161
|
+
stop Stop tapi-service
|
|
2162
|
+
restart Restart tapi-service
|
|
2163
|
+
repair Restart tapi-service using the current installed service
|
|
2164
|
+
`);
|
|
2165
|
+
}
|
|
2166
|
+
function printPublishHelp() {
|
|
2167
|
+
console.log(`Tapi publish
|
|
2168
|
+
|
|
2169
|
+
Usage:
|
|
2170
|
+
tapi publish [--api-base-url URL] [--api-key KEY] [--project PROJECT]
|
|
2171
|
+
|
|
2172
|
+
Publishes ready generated API requests from local .tapi/apis files.
|
|
1116
2173
|
`);
|
|
1117
2174
|
}
|
|
1118
2175
|
function formatError(error) {
|