@tapi-dev/sdk 0.1.7 → 0.1.9
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 +110 -164
- package/dist/cli.d.ts +29 -0
- package/dist/cli.js +1126 -55
- package/dist/client.d.ts +1 -1
- package/dist/client.js +3 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +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
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,7 +1318,26 @@ async function obtainStudioInstallToken(options, event) {
|
|
|
602
1318
|
}
|
|
603
1319
|
async function openStudio(options) {
|
|
604
1320
|
ensureWindowsHost();
|
|
605
|
-
|
|
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
|
+
}
|
|
1332
|
+
let exePath = findStudioExecutable(options);
|
|
1333
|
+
if (!exePath && !options.exePath) {
|
|
1334
|
+
console.log("Tapi Studio executable was not found. Installing Tapi Studio now...");
|
|
1335
|
+
const installCode = await installStudio(options);
|
|
1336
|
+
if (installCode !== 0) {
|
|
1337
|
+
return installCode;
|
|
1338
|
+
}
|
|
1339
|
+
exePath = findStudioExecutable(options);
|
|
1340
|
+
}
|
|
606
1341
|
if (!exePath || !existsSync(exePath)) {
|
|
607
1342
|
console.error("Tapi Studio executable was not found.");
|
|
608
1343
|
console.error("Run `npx tapi studio install --channel pilot`, or set TAPI_STUDIO_EXE to the installed executable path.");
|
|
@@ -610,6 +1345,7 @@ async function openStudio(options) {
|
|
|
610
1345
|
}
|
|
611
1346
|
const child = spawn(exePath, [], {
|
|
612
1347
|
detached: true,
|
|
1348
|
+
env: buildStudioLaunchEnv(options),
|
|
613
1349
|
stdio: "ignore",
|
|
614
1350
|
windowsHide: false,
|
|
615
1351
|
});
|
|
@@ -617,6 +1353,141 @@ async function openStudio(options) {
|
|
|
617
1353
|
console.log(`Opened Tapi Studio: ${exePath}`);
|
|
618
1354
|
return 0;
|
|
619
1355
|
}
|
|
1356
|
+
function findStudioExecutable(options) {
|
|
1357
|
+
if (options.exePath) {
|
|
1358
|
+
return existsSync(options.exePath) ? options.exePath : undefined;
|
|
1359
|
+
}
|
|
1360
|
+
return getStudioExecutableCandidates().find((candidate) => existsSync(candidate));
|
|
1361
|
+
}
|
|
1362
|
+
async function ensurePortableStudioServer(options) {
|
|
1363
|
+
const event = await createCliWideEvent("tapi_cli.studio_server_ensure", {
|
|
1364
|
+
sdk_version: sdkVersion,
|
|
1365
|
+
options: installEventOptions(options),
|
|
1366
|
+
});
|
|
1367
|
+
try {
|
|
1368
|
+
let installToken = options.installToken?.trim();
|
|
1369
|
+
if (!installToken) {
|
|
1370
|
+
await event.phase("auth.install_token.request.start", {
|
|
1371
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
1372
|
+
channel: options.channel,
|
|
1373
|
+
});
|
|
1374
|
+
installToken = await obtainStudioInstallToken(options, event);
|
|
1375
|
+
await event.phase("auth.install_token.request.success", {
|
|
1376
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
1377
|
+
channel: options.channel,
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
await event.phase("manifest.fetch.start", {
|
|
1381
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
1382
|
+
channel: options.channel,
|
|
1383
|
+
});
|
|
1384
|
+
const manifest = await fetchProtectedStudioManifest(options.apiBaseUrl, options.channel, installToken);
|
|
1385
|
+
ensureCompatibleManifest(manifest);
|
|
1386
|
+
await event.phase("manifest.fetch.success", {
|
|
1387
|
+
manifest: manifestEventSummary(manifest),
|
|
1388
|
+
});
|
|
1389
|
+
if (!isPortableStudioServerManifest(manifest)) {
|
|
1390
|
+
await event.finish(true, {
|
|
1391
|
+
installerKind: manifest.installerKind || "legacy",
|
|
1392
|
+
});
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
1395
|
+
const existing = findPortableStudioServerExe(portableStudioServerReleaseDir(manifest), manifest);
|
|
1396
|
+
if (existing) {
|
|
1397
|
+
await event.finish(true, {
|
|
1398
|
+
serverExe: existing,
|
|
1399
|
+
cacheHit: true,
|
|
1400
|
+
});
|
|
1401
|
+
return { exePath: existing, manifest };
|
|
1402
|
+
}
|
|
1403
|
+
const exePath = await installPortableStudioServer(options, manifest, event);
|
|
1404
|
+
await event.finish(true, {
|
|
1405
|
+
serverExe: exePath,
|
|
1406
|
+
cacheHit: false,
|
|
1407
|
+
});
|
|
1408
|
+
return { exePath, manifest };
|
|
1409
|
+
}
|
|
1410
|
+
catch (error) {
|
|
1411
|
+
await event.finish(false, {
|
|
1412
|
+
error: errorDetails(error),
|
|
1413
|
+
});
|
|
1414
|
+
throw error;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
async function installPortableStudioServer(options, manifest, event) {
|
|
1418
|
+
await mkdir(options.cacheDir, { recursive: true });
|
|
1419
|
+
const artifactPath = join(options.cacheDir, cachedInstallerName(manifest));
|
|
1420
|
+
const verified = await hasVerifiedCachedInstaller(artifactPath, manifest.sha256);
|
|
1421
|
+
if (verified) {
|
|
1422
|
+
console.log(`Using cached Tapi Studio server artifact: ${artifactPath}`);
|
|
1423
|
+
await event.phase("cache.hit", { artifactPath });
|
|
1424
|
+
}
|
|
1425
|
+
else {
|
|
1426
|
+
console.log(`Downloading Tapi Studio server ${manifest.version}...`);
|
|
1427
|
+
await event.phase("download.start", {
|
|
1428
|
+
url: manifest.url,
|
|
1429
|
+
destination: artifactPath,
|
|
1430
|
+
expectedSha256: manifest.sha256,
|
|
1431
|
+
});
|
|
1432
|
+
await downloadAndVerify(manifest.url, artifactPath, manifest.sha256, "Tapi Studio server artifact");
|
|
1433
|
+
await event.phase("download.verified", {
|
|
1434
|
+
artifactPath,
|
|
1435
|
+
expectedSha256: manifest.sha256,
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
if (options.downloadOnly) {
|
|
1439
|
+
console.log(`Downloaded Tapi Studio server artifact: ${artifactPath}`);
|
|
1440
|
+
return artifactPath;
|
|
1441
|
+
}
|
|
1442
|
+
const releaseDir = portableStudioServerReleaseDir(manifest);
|
|
1443
|
+
await extractZip(artifactPath, releaseDir);
|
|
1444
|
+
const exePath = findPortableStudioServerExe(releaseDir, manifest);
|
|
1445
|
+
if (!exePath) {
|
|
1446
|
+
throw new Error(`Tapi Studio server executable was not found after extraction: ${releaseDir}`);
|
|
1447
|
+
}
|
|
1448
|
+
console.log(`Installed Tapi Studio server ${manifest.version}: ${releaseDir}`);
|
|
1449
|
+
return exePath;
|
|
1450
|
+
}
|
|
1451
|
+
async function launchPortableStudioServer(exePath, options) {
|
|
1452
|
+
const host = "127.0.0.1";
|
|
1453
|
+
const port = await chooseStudioPort();
|
|
1454
|
+
const env = buildStudioLaunchEnv(options);
|
|
1455
|
+
env.TAPI_RUNTIME_MODE = "installed";
|
|
1456
|
+
env.TAPI_RUNTIME_NAMESPACE = "installed";
|
|
1457
|
+
env.TAPI_RUNTIME_CONTROL_PORT = env.TAPI_RUNTIME_CONTROL_PORT || "8765";
|
|
1458
|
+
env.TAPI_PROCESS_ROLE = "studio_server";
|
|
1459
|
+
env.TAPI_STUDIO_OPEN_BROWSER = "0";
|
|
1460
|
+
env.TAPI_STUDIO_HOST = host;
|
|
1461
|
+
env.TAPI_STUDIO_PORT = String(port);
|
|
1462
|
+
env.TAPI_STUDIO_SERVER_PORT = String(port);
|
|
1463
|
+
const child = spawn(exePath, [], {
|
|
1464
|
+
detached: true,
|
|
1465
|
+
env,
|
|
1466
|
+
stdio: "ignore",
|
|
1467
|
+
windowsHide: true,
|
|
1468
|
+
});
|
|
1469
|
+
child.unref();
|
|
1470
|
+
const url = `http://${host}:${port}`;
|
|
1471
|
+
openBrowser(url);
|
|
1472
|
+
console.log(`Opened Tapi Studio: ${url}`);
|
|
1473
|
+
}
|
|
1474
|
+
async function chooseStudioPort(preferred = 18766) {
|
|
1475
|
+
for (let port = preferred; port < preferred + 25; port += 1) {
|
|
1476
|
+
if (await isPortAvailable(port)) {
|
|
1477
|
+
return port;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
throw new Error(`No available local Studio port found starting at ${preferred}.`);
|
|
1481
|
+
}
|
|
1482
|
+
async function isPortAvailable(port) {
|
|
1483
|
+
return await new Promise((resolvePromise) => {
|
|
1484
|
+
const server = createNetServer();
|
|
1485
|
+
server.once("error", () => resolvePromise(false));
|
|
1486
|
+
server.listen(port, "127.0.0.1", () => {
|
|
1487
|
+
server.close(() => resolvePromise(true));
|
|
1488
|
+
});
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
620
1491
|
async function runDoctor(options) {
|
|
621
1492
|
console.log(`Tapi SDK: ${sdkVersion}`);
|
|
622
1493
|
console.log(`Node: ${process.version}`);
|
|
@@ -625,6 +1496,8 @@ async function runDoctor(options) {
|
|
|
625
1496
|
console.log(`Studio channel: ${options.channel}`);
|
|
626
1497
|
console.log(`Studio manifest: ${options.manifestUrl}`);
|
|
627
1498
|
console.log(`Studio cache: ${options.cacheDir}`);
|
|
1499
|
+
console.log(`Workspace: ${options.workspaceRoot ?? "not found"}`);
|
|
1500
|
+
console.log(`Project: ${options.projectId ?? "not set"}`);
|
|
628
1501
|
const exePath = options.exePath ?? getStudioExecutableCandidates().find((candidate) => existsSync(candidate));
|
|
629
1502
|
console.log(`Studio executable: ${exePath && existsSync(exePath) ? exePath : "not found"}`);
|
|
630
1503
|
try {
|
|
@@ -638,6 +1511,27 @@ async function runDoctor(options) {
|
|
|
638
1511
|
}
|
|
639
1512
|
return 0;
|
|
640
1513
|
}
|
|
1514
|
+
function buildStudioLaunchEnv(options) {
|
|
1515
|
+
const env = { ...process.env };
|
|
1516
|
+
env.TAPI_STUDIO_API_BASE_URL = options.apiBaseUrl;
|
|
1517
|
+
env.TAPI_BASE_URL = env.TAPI_BASE_URL || options.apiBaseUrl;
|
|
1518
|
+
if (options.workspaceRoot) {
|
|
1519
|
+
env.TAPI_WORKSPACE_ROOT = options.workspaceRoot;
|
|
1520
|
+
}
|
|
1521
|
+
if (options.workspace?.configPath) {
|
|
1522
|
+
env.TAPI_WORKSPACE_CONFIG = options.workspace.configPath;
|
|
1523
|
+
}
|
|
1524
|
+
if (options.projectId) {
|
|
1525
|
+
env.TAPI_PROJECT_ID = options.projectId;
|
|
1526
|
+
}
|
|
1527
|
+
if (options.projectSlug) {
|
|
1528
|
+
env.TAPI_PROJECT_SLUG = options.projectSlug;
|
|
1529
|
+
}
|
|
1530
|
+
if (options.workspaceMode) {
|
|
1531
|
+
env.TAPI_STUDIO_WORKSPACE_MODE = "1";
|
|
1532
|
+
}
|
|
1533
|
+
return env;
|
|
1534
|
+
}
|
|
641
1535
|
async function fetchProtectedStudioManifest(apiBaseUrl, channel, installToken, fetchImpl = fetch) {
|
|
642
1536
|
const response = await fetchImpl(`${apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/studio/releases/${channel}`, {
|
|
643
1537
|
headers: {
|
|
@@ -654,6 +1548,22 @@ async function fetchProtectedStudioManifest(apiBaseUrl, channel, installToken, f
|
|
|
654
1548
|
}
|
|
655
1549
|
return validateStudioManifest(responseBody);
|
|
656
1550
|
}
|
|
1551
|
+
async function fetchProtectedServiceManifest(apiBaseUrl, channel, installToken, fetchImpl = fetch) {
|
|
1552
|
+
const response = await fetchImpl(`${apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/service/releases/${channel}`, {
|
|
1553
|
+
headers: {
|
|
1554
|
+
Accept: "application/json",
|
|
1555
|
+
Authorization: `Bearer ${installToken}`,
|
|
1556
|
+
},
|
|
1557
|
+
});
|
|
1558
|
+
const responseBody = await readJsonBody(response);
|
|
1559
|
+
if (!response.ok) {
|
|
1560
|
+
const detail = typeof responseBody?.detail === "string" ? responseBody.detail : "";
|
|
1561
|
+
throw new Error(detail
|
|
1562
|
+
? `Failed to fetch protected Tapi Service manifest: HTTP ${response.status} (${detail})`
|
|
1563
|
+
: `Failed to fetch protected Tapi Service manifest: HTTP ${response.status}`);
|
|
1564
|
+
}
|
|
1565
|
+
return validateServiceReleaseManifest(responseBody);
|
|
1566
|
+
}
|
|
657
1567
|
async function fetchStudioManifest(manifestUrl, fetchImpl = fetch) {
|
|
658
1568
|
const response = await fetchImpl(manifestUrl, {
|
|
659
1569
|
headers: {
|
|
@@ -696,24 +1606,24 @@ async function requestStudioInstallToken(apiBaseUrl, channel, firebaseIdToken, f
|
|
|
696
1606
|
expiresAt: typeof payload?.expiresAt === "string" ? payload.expiresAt : undefined,
|
|
697
1607
|
};
|
|
698
1608
|
}
|
|
699
|
-
async function downloadAndVerify(url, destination, expectedSha256) {
|
|
1609
|
+
async function downloadAndVerify(url, destination, expectedSha256, label = "Studio installer") {
|
|
700
1610
|
const partialPath = `${destination}.partial`;
|
|
701
|
-
await downloadFile(url, partialPath);
|
|
1611
|
+
await downloadFile(url, partialPath, label);
|
|
702
1612
|
const actualSha256 = await sha256File(partialPath);
|
|
703
1613
|
if (actualSha256 !== expectedSha256.toLowerCase()) {
|
|
704
1614
|
await unlinkIfExists(partialPath);
|
|
705
|
-
throw new Error(
|
|
1615
|
+
throw new Error(`${label} checksum mismatch. Expected ${expectedSha256}, got ${actualSha256}.`);
|
|
706
1616
|
}
|
|
707
1617
|
await rename(partialPath, destination);
|
|
708
1618
|
console.log(`Verified SHA256: ${actualSha256}`);
|
|
709
1619
|
}
|
|
710
|
-
async function downloadFile(url, destination) {
|
|
1620
|
+
async function downloadFile(url, destination, label = "download") {
|
|
711
1621
|
const response = await fetch(url);
|
|
712
1622
|
if (!response.ok) {
|
|
713
|
-
throw new Error(`Failed to download
|
|
1623
|
+
throw new Error(`Failed to download ${label}: HTTP ${response.status}`);
|
|
714
1624
|
}
|
|
715
1625
|
if (!response.body) {
|
|
716
|
-
throw new Error(
|
|
1626
|
+
throw new Error(`${label} response did not include a body.`);
|
|
717
1627
|
}
|
|
718
1628
|
await pipeline(Readable.fromWeb(response.body), createWriteStream(destination));
|
|
719
1629
|
}
|
|
@@ -937,6 +1847,32 @@ async function runProcess(command, args) {
|
|
|
937
1847
|
});
|
|
938
1848
|
});
|
|
939
1849
|
}
|
|
1850
|
+
async function runProcessCapture(command, args) {
|
|
1851
|
+
return await new Promise((resolvePromise, rejectPromise) => {
|
|
1852
|
+
const child = spawn(command, args, {
|
|
1853
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1854
|
+
windowsHide: true,
|
|
1855
|
+
});
|
|
1856
|
+
let stdout = "";
|
|
1857
|
+
let stderr = "";
|
|
1858
|
+
child.stdout.on("data", (chunk) => {
|
|
1859
|
+
stdout += String(chunk);
|
|
1860
|
+
});
|
|
1861
|
+
child.stderr.on("data", (chunk) => {
|
|
1862
|
+
stderr += String(chunk);
|
|
1863
|
+
});
|
|
1864
|
+
child.once("error", rejectPromise);
|
|
1865
|
+
child.once("exit", (code) => {
|
|
1866
|
+
const err = stderr.trim();
|
|
1867
|
+
if (code === 0) {
|
|
1868
|
+
resolvePromise(stdout);
|
|
1869
|
+
}
|
|
1870
|
+
else {
|
|
1871
|
+
rejectPromise(new Error(err || `Process exited with code ${code ?? "unknown"}.`));
|
|
1872
|
+
}
|
|
1873
|
+
});
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
940
1876
|
function ensureWindowsHost() {
|
|
941
1877
|
if (process.platform !== "win32" || process.arch !== "x64") {
|
|
942
1878
|
throw new Error("Tapi Studio desktop installer is currently published for Windows x64 only.");
|
|
@@ -950,10 +1886,58 @@ function ensureCompatibleManifest(manifest) {
|
|
|
950
1886
|
throw new Error(`Tapi Studio ${manifest.version} requires @tapi-dev/sdk >= ${manifest.minSdkVersion}; installed SDK is ${sdkVersion}.`);
|
|
951
1887
|
}
|
|
952
1888
|
}
|
|
1889
|
+
function ensureCompatibleServiceManifest(manifest) {
|
|
1890
|
+
if (manifest.platform !== SUPPORTED_STUDIO_PLATFORM) {
|
|
1891
|
+
throw new Error(`This SDK expected ${SUPPORTED_STUDIO_PLATFORM}, but the Tapi Service manifest points to ${manifest.platform}.`);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
953
1894
|
function cachedInstallerName(manifest) {
|
|
954
1895
|
const rawName = manifest.artifactName || basename(new URL(manifest.url).pathname) || `TapiStudioSetup-${manifest.version}.exe`;
|
|
955
1896
|
return rawName.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
956
1897
|
}
|
|
1898
|
+
function isPortableStudioServerManifest(manifest) {
|
|
1899
|
+
const kind = String(manifest.installerKind || "").toLowerCase();
|
|
1900
|
+
return kind === "portable-server" || manifest.artifactName.toLowerCase().endsWith(".zip");
|
|
1901
|
+
}
|
|
1902
|
+
function portableStudioServerReleaseDir(manifest) {
|
|
1903
|
+
return join(getDefaultPortableStudioServerReleasesDir(), safePathSegment(manifest.version));
|
|
1904
|
+
}
|
|
1905
|
+
function getDefaultPortableStudioServerReleasesDir() {
|
|
1906
|
+
if (process.platform === "win32") {
|
|
1907
|
+
const localAppData = process.env.LOCALAPPDATA ??
|
|
1908
|
+
(process.env.USERPROFILE ? join(process.env.USERPROFILE, "AppData", "Local") : join(homedir(), "AppData", "Local"));
|
|
1909
|
+
return join(localAppData, "Tapi", "Studio", "server", "releases");
|
|
1910
|
+
}
|
|
1911
|
+
return join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "tapi", "studio", "server", "releases");
|
|
1912
|
+
}
|
|
1913
|
+
function findPortableStudioServerExe(releaseDir, manifest) {
|
|
1914
|
+
const executable = manifest.serverExecutable || "tapi-studio-server.exe";
|
|
1915
|
+
const baseName = executable.toLowerCase().endsWith(".exe")
|
|
1916
|
+
? executable.slice(0, -4)
|
|
1917
|
+
: executable;
|
|
1918
|
+
const candidates = [
|
|
1919
|
+
join(releaseDir, executable),
|
|
1920
|
+
join(releaseDir, baseName, `${baseName}.exe`),
|
|
1921
|
+
join(releaseDir, "tapi-studio-server.exe"),
|
|
1922
|
+
join(releaseDir, "tapi-studio-server", "tapi-studio-server.exe"),
|
|
1923
|
+
];
|
|
1924
|
+
return candidates.find((candidate) => existsSync(candidate));
|
|
1925
|
+
}
|
|
1926
|
+
function cachedServiceArtifactName(manifest) {
|
|
1927
|
+
const rawName = manifest.artifactName || basename(new URL(manifest.url).pathname) || `TapiService-${manifest.version}.zip`;
|
|
1928
|
+
return rawName.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
1929
|
+
}
|
|
1930
|
+
async function extractZip(zipPath, destination) {
|
|
1931
|
+
await rm(destination, { recursive: true, force: true });
|
|
1932
|
+
await mkdir(destination, { recursive: true });
|
|
1933
|
+
await runProcessCapture("powershell.exe", [
|
|
1934
|
+
"-NoProfile",
|
|
1935
|
+
"-ExecutionPolicy",
|
|
1936
|
+
"Bypass",
|
|
1937
|
+
"-Command",
|
|
1938
|
+
`Expand-Archive -LiteralPath ${powerShellSingleQuoted(zipPath)} -DestinationPath ${powerShellSingleQuoted(destination)} -Force`,
|
|
1939
|
+
]);
|
|
1940
|
+
}
|
|
957
1941
|
function installEventOptions(options) {
|
|
958
1942
|
return {
|
|
959
1943
|
channel: options.channel,
|
|
@@ -967,6 +1951,22 @@ function installEventOptions(options) {
|
|
|
967
1951
|
installTokenProvided: Boolean(options.installToken),
|
|
968
1952
|
};
|
|
969
1953
|
}
|
|
1954
|
+
function serviceManifestEventSummary(manifest) {
|
|
1955
|
+
return {
|
|
1956
|
+
product: manifest.product,
|
|
1957
|
+
version: manifest.version,
|
|
1958
|
+
channel: manifest.channel,
|
|
1959
|
+
platform: manifest.platform,
|
|
1960
|
+
artifactName: manifest.artifactName,
|
|
1961
|
+
url: manifest.url,
|
|
1962
|
+
sha256: manifest.sha256,
|
|
1963
|
+
sizeBytes: manifest.sizeBytes,
|
|
1964
|
+
serviceHostExecutable: manifest.serviceHostExecutable,
|
|
1965
|
+
workerExecutable: manifest.workerExecutable,
|
|
1966
|
+
installer: manifest.installer,
|
|
1967
|
+
commitShort: manifest.commitShort,
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
970
1970
|
function manifestEventSummary(manifest) {
|
|
971
1971
|
return {
|
|
972
1972
|
product: manifest.product,
|
|
@@ -982,6 +1982,12 @@ function manifestEventSummary(manifest) {
|
|
|
982
1982
|
bundledService: manifest.bundledService,
|
|
983
1983
|
};
|
|
984
1984
|
}
|
|
1985
|
+
function safePathSegment(value) {
|
|
1986
|
+
return String(value || "release").replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || "release";
|
|
1987
|
+
}
|
|
1988
|
+
function powerShellSingleQuoted(value) {
|
|
1989
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
1990
|
+
}
|
|
985
1991
|
function parseChannel(value) {
|
|
986
1992
|
if (value === "pilot" || value === "stable" || value === "nightly") {
|
|
987
1993
|
return value;
|
|
@@ -1067,52 +2073,117 @@ function readSdkVersion() {
|
|
|
1067
2073
|
function printHelp() {
|
|
1068
2074
|
console.log(`Tapi CLI
|
|
1069
2075
|
|
|
1070
|
-
Usage:
|
|
1071
|
-
tapi
|
|
1072
|
-
tapi
|
|
1073
|
-
tapi studio
|
|
1074
|
-
tapi
|
|
1075
|
-
tapi
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
2076
|
+
Usage:
|
|
2077
|
+
tapi init --project PROJECT
|
|
2078
|
+
tapi link --project PROJECT
|
|
2079
|
+
tapi studio install [--channel pilot] [--api-base-url URL]
|
|
2080
|
+
tapi studio
|
|
2081
|
+
tapi studio open
|
|
2082
|
+
tapi studio doctor
|
|
2083
|
+
tapi apis describe <namespace.operation>
|
|
2084
|
+
tapi apis sync
|
|
2085
|
+
tapi apis generate
|
|
2086
|
+
tapi publish
|
|
2087
|
+
tapi service status
|
|
2088
|
+
tapi doctor
|
|
2089
|
+
|
|
2090
|
+
Commands:
|
|
2091
|
+
init Create .tapi/project.json for this repo
|
|
2092
|
+
link Rebind this repo to an existing Tapi project
|
|
2093
|
+
studio install Download, verify, and run the Tapi Studio installer
|
|
2094
|
+
studio Open Tapi Studio for this repo
|
|
2095
|
+
studio open Open Tapi Studio for this repo
|
|
2096
|
+
studio doctor Check local SDK and Studio release configuration
|
|
2097
|
+
apis describe Print a generated website API input/output contract
|
|
2098
|
+
apis sync Save the published API catalog to .tapi/generated
|
|
2099
|
+
apis generate Generate a TypeScript runtime wrapper from the catalog
|
|
2100
|
+
publish Upload local .tapi API drafts and publish ready requests
|
|
2101
|
+
service Inspect or control the local Tapi Windows service
|
|
2102
|
+
doctor Alias for studio doctor
|
|
1083
2103
|
`);
|
|
1084
2104
|
}
|
|
1085
2105
|
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
|
-
--
|
|
2106
|
+
console.log(`Tapi generated website API commands
|
|
2107
|
+
|
|
2108
|
+
Usage:
|
|
2109
|
+
tapi apis describe <namespace.operation> [--api-base-url URL] [--api-key KEY] [--project PROJECT]
|
|
2110
|
+
tapi apis sync [--api-base-url URL] [--api-key KEY] [--project PROJECT]
|
|
2111
|
+
tapi apis generate [--api-base-url URL] [--api-key KEY] [--project PROJECT] [--out FILE]
|
|
2112
|
+
|
|
2113
|
+
Options:
|
|
2114
|
+
--api-base-url <url> Tapi API base URL
|
|
2115
|
+
--server <url> Alias for --api-base-url
|
|
2116
|
+
--api-key <key> Tapi API key; defaults to TAPI_API_KEY
|
|
2117
|
+
--project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
|
|
2118
|
+
--catalog <path> Catalog JSON output path
|
|
2119
|
+
--out <path> Generated TypeScript output path
|
|
1096
2120
|
`);
|
|
1097
2121
|
}
|
|
1098
2122
|
function printStudioHelp() {
|
|
1099
|
-
console.log(`Tapi Studio commands
|
|
1100
|
-
|
|
1101
|
-
Usage:
|
|
1102
|
-
tapi studio
|
|
1103
|
-
tapi studio
|
|
1104
|
-
tapi studio
|
|
1105
|
-
|
|
1106
|
-
|
|
2123
|
+
console.log(`Tapi Studio commands
|
|
2124
|
+
|
|
2125
|
+
Usage:
|
|
2126
|
+
tapi studio [options]
|
|
2127
|
+
tapi studio install [options]
|
|
2128
|
+
tapi studio open [options]
|
|
2129
|
+
tapi studio doctor [options]
|
|
2130
|
+
|
|
2131
|
+
Options:
|
|
1107
2132
|
--channel <name> Release channel: pilot, stable, or nightly
|
|
1108
2133
|
--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
|
-
--
|
|
2134
|
+
--server <url> Alias for --api-base-url
|
|
2135
|
+
--workspace <path> Search this directory for .tapi/project.json
|
|
2136
|
+
--no-workspace Do not load .tapi/project.json
|
|
2137
|
+
--project <id> Tapi project id for workspace-bound Studio
|
|
2138
|
+
--project-slug <slug> Optional display slug for the bound project
|
|
2139
|
+
--install-token <tok> Preissued Studio install token (skips browser sign-in)
|
|
2140
|
+
--manifest <url> Exact release manifest URL for doctor only
|
|
2141
|
+
--cache-dir <path> Installer download cache directory
|
|
2142
|
+
--download-only Download and verify without running the installer
|
|
2143
|
+
--silent Run the NSIS installer with /S
|
|
2144
|
+
--exe <path> Tapi Studio executable path for open/doctor
|
|
2145
|
+
`);
|
|
2146
|
+
}
|
|
2147
|
+
function printWorkspaceHelp(command) {
|
|
2148
|
+
console.log(`Tapi workspace ${command}
|
|
2149
|
+
|
|
2150
|
+
Usage:
|
|
2151
|
+
tapi ${command} --project PROJECT [--api-base-url URL] [--root PATH] [--force]
|
|
2152
|
+
|
|
2153
|
+
Options:
|
|
2154
|
+
--project <id> Tapi project id/name to bind this repo to
|
|
2155
|
+
--project-slug <slug> Optional display slug
|
|
2156
|
+
--api-base-url <url> Tapi API base URL stored in .tapi/project.json
|
|
2157
|
+
--server <url> Alias for --api-base-url
|
|
2158
|
+
--root <path> Directory where .tapi/project.json should be written
|
|
2159
|
+
--force Overwrite an existing workspace config
|
|
2160
|
+
`);
|
|
2161
|
+
}
|
|
2162
|
+
function printServiceHelp() {
|
|
2163
|
+
console.log(`Tapi service commands
|
|
2164
|
+
|
|
2165
|
+
Usage:
|
|
2166
|
+
tapi service status
|
|
2167
|
+
tapi service start
|
|
2168
|
+
tapi service stop
|
|
2169
|
+
tapi service restart
|
|
2170
|
+
tapi service repair
|
|
2171
|
+
|
|
2172
|
+
Commands:
|
|
2173
|
+
status Print installed/running status for tapi-service
|
|
2174
|
+
start Start tapi-service
|
|
2175
|
+
stop Stop tapi-service
|
|
2176
|
+
restart Restart tapi-service
|
|
2177
|
+
repair Restart tapi-service using the current installed service
|
|
2178
|
+
`);
|
|
2179
|
+
}
|
|
2180
|
+
function printPublishHelp() {
|
|
2181
|
+
console.log(`Tapi publish
|
|
2182
|
+
|
|
2183
|
+
Usage:
|
|
2184
|
+
tapi publish [--api-base-url URL] [--api-key KEY] [--project PROJECT]
|
|
2185
|
+
|
|
2186
|
+
Publishes ready generated API requests from local .tapi/apis files.
|
|
1116
2187
|
`);
|
|
1117
2188
|
}
|
|
1118
2189
|
function formatError(error) {
|