@tapi-dev/sdk 0.1.38 → 0.1.39
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 +27 -14
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +310 -28
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -13,20 +13,32 @@ This package is ESM-first and works in runtimes with `fetch`, including modern N
|
|
|
13
13
|
The CLI command is `tapi`. If your package manager only installed the SDK
|
|
14
14
|
locally and `tapi` is not on `PATH`, use `npx tapi` as a fallback.
|
|
15
15
|
|
|
16
|
-
## Developer Flow
|
|
17
|
-
|
|
18
|
-
Tapi Studio is launched from the developer
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
tapi
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
## Developer Flow
|
|
17
|
+
|
|
18
|
+
Tapi Studio is launched from the signed-in developer account. Running
|
|
19
|
+
`tapi studio` lists the Tapps available to that account, asks which one to open,
|
|
20
|
+
then starts Studio in that Tapp context. The current directory can still provide
|
|
21
|
+
a default via `.tapi/project.json`, but it is no longer required for Studio to
|
|
22
|
+
know which Tapp to show.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @tapi-dev/sdk
|
|
26
|
+
tapi login
|
|
27
|
+
tapi tapp create brokerage
|
|
28
|
+
tapi studio
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Useful Studio launch forms:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
tapi studio # ask which Tapp to open
|
|
35
|
+
tapi studio --tapp brokerage # open one Tapp directly
|
|
36
|
+
tapi studio --select # force the picker
|
|
37
|
+
tapi studio --last # reuse the last selected Tapp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`tapi studio` checks the required local `tapi-service`, installs it when needed,
|
|
41
|
+
downloads the portable Studio server release when the channel manifest uses
|
|
30
42
|
`installerKind: portable-server`, starts Studio locally, and opens the browser.
|
|
31
43
|
If the channel still publishes a legacy NSIS desktop Studio manifest and no
|
|
32
44
|
Studio executable is installed yet, `tapi studio` runs the installer first, then
|
|
@@ -57,6 +69,7 @@ tapi login
|
|
|
57
69
|
tapi init --project brokerage
|
|
58
70
|
tapi link --project brokerage
|
|
59
71
|
tapi studio
|
|
72
|
+
tapi studio --tapp brokerage
|
|
60
73
|
tapi service install --channel pilot
|
|
61
74
|
tapi service status
|
|
62
75
|
tapi services describe schwab.place_order
|
package/dist/cli.d.ts
CHANGED
|
@@ -66,8 +66,10 @@ export interface StudioCliOptions {
|
|
|
66
66
|
workspaceRoot?: string;
|
|
67
67
|
projectId?: string;
|
|
68
68
|
projectSlug?: string;
|
|
69
|
+
projectIdSource?: "option" | "env" | "workspace" | "selection";
|
|
69
70
|
workspaceMode: boolean;
|
|
70
71
|
launchUrlQuery?: string;
|
|
72
|
+
tappSelectionMode: "auto" | "select" | "last" | "none";
|
|
71
73
|
}
|
|
72
74
|
export interface RunnerSetupCliOptions {
|
|
73
75
|
apiBaseUrl: string;
|
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,9 @@ import { emitKeypressEvents } from "node:readline";
|
|
|
12
12
|
import { Readable, Transform } from "node:stream";
|
|
13
13
|
import { pipeline } from "node:stream/promises";
|
|
14
14
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
|
+
import { select } from "@inquirer/prompts";
|
|
16
|
+
import { Presets, SingleBar } from "cli-progress";
|
|
17
|
+
import pc from "yoctocolors";
|
|
15
18
|
import { TapiClient } from "./index.js";
|
|
16
19
|
import { loadWorkspace, normalizeProjectValue, writeWorkspaceConfig, } from "./workspace.js";
|
|
17
20
|
const DEFAULT_DOWNLOADS_BASE_URL = "https://d4xaf52nfwiok.cloudfront.net";
|
|
@@ -314,6 +317,9 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
314
317
|
if (!subcommand) {
|
|
315
318
|
return openStudio(parseStudioOptions([]));
|
|
316
319
|
}
|
|
320
|
+
if (subcommand.startsWith("-")) {
|
|
321
|
+
return openStudio(parseStudioOptions([subcommand, ...rest]));
|
|
322
|
+
}
|
|
317
323
|
if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
318
324
|
printStudioHelp();
|
|
319
325
|
return 0;
|
|
@@ -340,6 +346,8 @@ export function parseStudioOptions(args) {
|
|
|
340
346
|
const raw = {};
|
|
341
347
|
let workspaceSearchRoot;
|
|
342
348
|
let skipWorkspace = false;
|
|
349
|
+
let projectIdSource;
|
|
350
|
+
let tappSelectionMode = "auto";
|
|
343
351
|
for (let index = 0; index < args.length; index += 1) {
|
|
344
352
|
const arg = args[index];
|
|
345
353
|
if (!arg) {
|
|
@@ -385,12 +393,40 @@ export function parseStudioOptions(args) {
|
|
|
385
393
|
skipWorkspace = true;
|
|
386
394
|
continue;
|
|
387
395
|
}
|
|
396
|
+
if (arg === "--tapp") {
|
|
397
|
+
raw.projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--tapp"), "tapp");
|
|
398
|
+
projectIdSource = "option";
|
|
399
|
+
tappSelectionMode = "none";
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (arg.startsWith("--tapp=")) {
|
|
403
|
+
raw.projectId = normalizeProjectValue(arg.slice("--tapp=".length), "tapp");
|
|
404
|
+
projectIdSource = "option";
|
|
405
|
+
tappSelectionMode = "none";
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (arg === "--select") {
|
|
409
|
+
tappSelectionMode = "select";
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (arg === "--last") {
|
|
413
|
+
tappSelectionMode = "last";
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (arg === "--no-select") {
|
|
417
|
+
tappSelectionMode = "none";
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
388
420
|
if (arg === "--project") {
|
|
389
421
|
raw.projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
|
|
422
|
+
projectIdSource = "option";
|
|
423
|
+
tappSelectionMode = "none";
|
|
390
424
|
continue;
|
|
391
425
|
}
|
|
392
426
|
if (arg.startsWith("--project=")) {
|
|
393
427
|
raw.projectId = normalizeProjectValue(arg.slice("--project=".length), "project");
|
|
428
|
+
projectIdSource = "option";
|
|
429
|
+
tappSelectionMode = "none";
|
|
394
430
|
continue;
|
|
395
431
|
}
|
|
396
432
|
if (arg === "--project-slug") {
|
|
@@ -459,7 +495,11 @@ export function parseStudioOptions(args) {
|
|
|
459
495
|
const manifestUrl = explicitManifestUrl
|
|
460
496
|
? normalizeHttpUrl(explicitManifestUrl, "Studio manifest URL")
|
|
461
497
|
: `${downloadsBaseUrl.replace(/\/+$/, "")}/studio/channels/${channel}/latest.json`;
|
|
462
|
-
const
|
|
498
|
+
const envProjectId = envString("TAPI_PROJECT_ID");
|
|
499
|
+
const projectId = raw.projectId ?? envProjectId ?? workspace?.projectId;
|
|
500
|
+
if (!projectIdSource && projectId) {
|
|
501
|
+
projectIdSource = raw.projectId ? "option" : envProjectId ? "env" : "workspace";
|
|
502
|
+
}
|
|
463
503
|
const projectSlug = raw.projectSlug ?? envString("TAPI_PROJECT_SLUG") ?? workspace?.projectSlug;
|
|
464
504
|
const envInstallToken = envString("TAPI_STUDIO_INSTALL_TOKEN");
|
|
465
505
|
const installToken = raw.installToken ?? envInstallToken;
|
|
@@ -479,7 +519,9 @@ export function parseStudioOptions(args) {
|
|
|
479
519
|
workspaceRoot: workspace?.root,
|
|
480
520
|
projectId,
|
|
481
521
|
projectSlug,
|
|
522
|
+
projectIdSource,
|
|
482
523
|
workspaceMode: Boolean(projectId || workspace),
|
|
524
|
+
tappSelectionMode,
|
|
483
525
|
};
|
|
484
526
|
}
|
|
485
527
|
function parseServiceOptions(args) {
|
|
@@ -2342,7 +2384,7 @@ async function installService(options) {
|
|
|
2342
2384
|
destination: zipPath,
|
|
2343
2385
|
expectedSha256: manifest.sha256,
|
|
2344
2386
|
});
|
|
2345
|
-
await
|
|
2387
|
+
await downloadAndVerify(manifest.url, zipPath, manifest.sha256, "Tapi Service artifact");
|
|
2346
2388
|
await event.phase("download.verified", {
|
|
2347
2389
|
zipPath,
|
|
2348
2390
|
expectedSha256: manifest.sha256,
|
|
@@ -2797,7 +2839,7 @@ async function installStudio(options) {
|
|
|
2797
2839
|
destination: installerPath,
|
|
2798
2840
|
expectedSha256: manifest.sha256,
|
|
2799
2841
|
});
|
|
2800
|
-
await
|
|
2842
|
+
await downloadAndVerify(manifest.url, installerPath, manifest.sha256);
|
|
2801
2843
|
await event.phase("download.verified", {
|
|
2802
2844
|
installerPath,
|
|
2803
2845
|
expectedSha256: manifest.sha256,
|
|
@@ -2950,23 +2992,24 @@ async function openStudio(options) {
|
|
|
2950
2992
|
if (options.downloadOnly) {
|
|
2951
2993
|
throw new Error("--download-only is only supported with `tapi studio install`.");
|
|
2952
2994
|
}
|
|
2953
|
-
await
|
|
2995
|
+
const launchOptions = await resolveStudioTappSelection(options);
|
|
2996
|
+
await ensureServiceReadyForStudio(launchOptions);
|
|
2954
2997
|
await closeExistingPortableStudioServersForFreshLaunch();
|
|
2955
|
-
if (!
|
|
2956
|
-
const portable = await ensurePortableStudioServer(
|
|
2998
|
+
if (!launchOptions.exePath) {
|
|
2999
|
+
const portable = await ensurePortableStudioServer(launchOptions);
|
|
2957
3000
|
if (portable) {
|
|
2958
|
-
await launchPortableStudioServer(portable.exePath,
|
|
3001
|
+
await launchPortableStudioServer(portable.exePath, launchOptions, portable.manifest);
|
|
2959
3002
|
return 0;
|
|
2960
3003
|
}
|
|
2961
3004
|
}
|
|
2962
|
-
let exePath = findStudioExecutable(
|
|
2963
|
-
if (!exePath && !
|
|
3005
|
+
let exePath = findStudioExecutable(launchOptions);
|
|
3006
|
+
if (!exePath && !launchOptions.exePath) {
|
|
2964
3007
|
console.log("Tapi Studio executable was not found. Installing Tapi Studio now...");
|
|
2965
|
-
const installCode = await installStudio(
|
|
3008
|
+
const installCode = await installStudio(launchOptions);
|
|
2966
3009
|
if (installCode !== 0) {
|
|
2967
3010
|
return installCode;
|
|
2968
3011
|
}
|
|
2969
|
-
exePath = findStudioExecutable(
|
|
3012
|
+
exePath = findStudioExecutable(launchOptions);
|
|
2970
3013
|
}
|
|
2971
3014
|
if (!exePath || !existsSync(exePath)) {
|
|
2972
3015
|
console.error("Tapi Studio executable was not found.");
|
|
@@ -2975,7 +3018,7 @@ async function openStudio(options) {
|
|
|
2975
3018
|
}
|
|
2976
3019
|
const child = spawn(exePath, [], {
|
|
2977
3020
|
detached: true,
|
|
2978
|
-
env: buildStudioLaunchEnv(
|
|
3021
|
+
env: buildStudioLaunchEnv(launchOptions),
|
|
2979
3022
|
stdio: "ignore",
|
|
2980
3023
|
windowsHide: false,
|
|
2981
3024
|
});
|
|
@@ -2983,6 +3026,177 @@ async function openStudio(options) {
|
|
|
2983
3026
|
console.log(`Opened Tapi Studio: ${exePath}`);
|
|
2984
3027
|
return 0;
|
|
2985
3028
|
}
|
|
3029
|
+
async function resolveStudioTappSelection(options) {
|
|
3030
|
+
if (options.tappSelectionMode === "none") {
|
|
3031
|
+
return options.projectId ? await rememberSelectedStudioTapp(options, options.projectId) : options;
|
|
3032
|
+
}
|
|
3033
|
+
if (options.tappSelectionMode === "auto" && options.projectIdSource === "env" && options.projectId) {
|
|
3034
|
+
return await rememberSelectedStudioTapp(options, options.projectId);
|
|
3035
|
+
}
|
|
3036
|
+
const lastSelection = await readStudioSelectionRecord();
|
|
3037
|
+
if (options.tappSelectionMode === "last") {
|
|
3038
|
+
if (!lastSelection?.lastTappId) {
|
|
3039
|
+
throw new Error("No previous Studio Tapp selection exists. Run `tapi studio --select` or `tapi studio --tapp <tapp>`.");
|
|
3040
|
+
}
|
|
3041
|
+
return await rememberSelectedStudioTapp(options, lastSelection.lastTappId);
|
|
3042
|
+
}
|
|
3043
|
+
let tapps = [];
|
|
3044
|
+
try {
|
|
3045
|
+
const authenticated = await withSdkAuth({ authToken: undefined });
|
|
3046
|
+
tapps = await fetchAvailableTappsForStudio(options, authenticated.authToken);
|
|
3047
|
+
}
|
|
3048
|
+
catch (error) {
|
|
3049
|
+
if (options.projectId) {
|
|
3050
|
+
console.warn(`Could not load Tapp list (${formatError(error)}). Opening Studio with ${options.projectId}.`);
|
|
3051
|
+
return await rememberSelectedStudioTapp(options, options.projectId);
|
|
3052
|
+
}
|
|
3053
|
+
throw error;
|
|
3054
|
+
}
|
|
3055
|
+
if (!tapps.length) {
|
|
3056
|
+
if (options.projectId) {
|
|
3057
|
+
return await rememberSelectedStudioTapp(options, options.projectId);
|
|
3058
|
+
}
|
|
3059
|
+
throw new Error("No Tapps found for this account. Create one with `tapi tapp create <name>`, then run `tapi studio`.");
|
|
3060
|
+
}
|
|
3061
|
+
const preferred = preferredStudioTappId(options, lastSelection, tapps);
|
|
3062
|
+
if (!studioTappPickerEnabled()) {
|
|
3063
|
+
const selected = options.projectId
|
|
3064
|
+
|| lastSelection?.lastTappId
|
|
3065
|
+
|| (tapps.length === 1 ? tapps[0]?.tappId : "");
|
|
3066
|
+
if (!selected) {
|
|
3067
|
+
throw new Error("Multiple Tapps are available. Use `tapi studio --tapp <tapp>` or run in an interactive terminal.");
|
|
3068
|
+
}
|
|
3069
|
+
return await rememberSelectedStudioTapp(options, selected);
|
|
3070
|
+
}
|
|
3071
|
+
const selected = await select({
|
|
3072
|
+
message: "Select Tapp",
|
|
3073
|
+
choices: tapps.map((tapp) => ({
|
|
3074
|
+
name: formatTappChoice(tapp),
|
|
3075
|
+
value: tapp.tappId,
|
|
3076
|
+
})),
|
|
3077
|
+
default: preferred || tapps[0]?.tappId,
|
|
3078
|
+
});
|
|
3079
|
+
return await rememberSelectedStudioTapp(options, selected);
|
|
3080
|
+
}
|
|
3081
|
+
async function fetchAvailableTappsForStudio(options, authToken, fetchImpl = fetch) {
|
|
3082
|
+
const response = await fetchImpl(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/tapps`, {
|
|
3083
|
+
method: "GET",
|
|
3084
|
+
headers: sdkJsonHeaders(authToken, options.projectId),
|
|
3085
|
+
});
|
|
3086
|
+
const body = await readJsonBody(response);
|
|
3087
|
+
if (!response.ok) {
|
|
3088
|
+
throw new Error(`Failed to list Tapps: HTTP ${response.status}`);
|
|
3089
|
+
}
|
|
3090
|
+
return normalizeTappList(body);
|
|
3091
|
+
}
|
|
3092
|
+
function normalizeTappList(body) {
|
|
3093
|
+
const raw = isRecord(body) && Array.isArray(body.tapps) ? body.tapps : [];
|
|
3094
|
+
const seen = new Set();
|
|
3095
|
+
const tapps = [];
|
|
3096
|
+
for (const item of raw) {
|
|
3097
|
+
if (!isRecord(item)) {
|
|
3098
|
+
continue;
|
|
3099
|
+
}
|
|
3100
|
+
const tappId = typeof item.tappId === "string" ? item.tappId.trim() : "";
|
|
3101
|
+
if (!tappId || seen.has(tappId)) {
|
|
3102
|
+
continue;
|
|
3103
|
+
}
|
|
3104
|
+
seen.add(tappId);
|
|
3105
|
+
tapps.push({
|
|
3106
|
+
tappId,
|
|
3107
|
+
name: typeof item.name === "string" && item.name.trim() ? item.name.trim() : tappId,
|
|
3108
|
+
serviceCount: normalizeNonnegativeInteger(item.serviceCount),
|
|
3109
|
+
queueCount: normalizeNonnegativeInteger(item.queueCount),
|
|
3110
|
+
});
|
|
3111
|
+
}
|
|
3112
|
+
return tapps;
|
|
3113
|
+
}
|
|
3114
|
+
function normalizeNonnegativeInteger(value) {
|
|
3115
|
+
const number = typeof value === "number" ? value : Number(value);
|
|
3116
|
+
return Number.isFinite(number) && number > 0 ? Math.floor(number) : 0;
|
|
3117
|
+
}
|
|
3118
|
+
function preferredStudioTappId(options, lastSelection, tapps) {
|
|
3119
|
+
const candidates = [
|
|
3120
|
+
options.projectId,
|
|
3121
|
+
lastSelection?.lastTappId,
|
|
3122
|
+
tapps[0]?.tappId,
|
|
3123
|
+
].map((value) => String(value || "").trim()).filter(Boolean);
|
|
3124
|
+
const available = new Set(tapps.map((tapp) => tapp.tappId));
|
|
3125
|
+
return candidates.find((candidate) => available.has(candidate)) || "";
|
|
3126
|
+
}
|
|
3127
|
+
function formatTappChoice(tapp) {
|
|
3128
|
+
const details = [
|
|
3129
|
+
`${tapp.serviceCount} service${tapp.serviceCount === 1 ? "" : "s"}`,
|
|
3130
|
+
`${tapp.queueCount} queue${tapp.queueCount === 1 ? "" : "s"}`,
|
|
3131
|
+
].join(", ");
|
|
3132
|
+
return tapp.name === tapp.tappId
|
|
3133
|
+
? `${tapp.tappId} ${pc.dim(`(${details})`)}`
|
|
3134
|
+
: `${tapp.name} ${pc.dim(`${tapp.tappId} (${details})`)}`;
|
|
3135
|
+
}
|
|
3136
|
+
function studioTappPickerEnabled() {
|
|
3137
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3138
|
+
return false;
|
|
3139
|
+
}
|
|
3140
|
+
const raw = String(process.env.TAPI_STUDIO_TAPP_PICKER || "").trim().toLowerCase();
|
|
3141
|
+
if (["0", "false", "no", "off"].includes(raw)) {
|
|
3142
|
+
return false;
|
|
3143
|
+
}
|
|
3144
|
+
return true;
|
|
3145
|
+
}
|
|
3146
|
+
async function rememberSelectedStudioTapp(options, tappId) {
|
|
3147
|
+
const selected = normalizeProjectValue(tappId, "tapp");
|
|
3148
|
+
const projectSlug = options.projectId === selected && options.projectSlug
|
|
3149
|
+
? options.projectSlug
|
|
3150
|
+
: selected;
|
|
3151
|
+
await writeStudioSelectionRecord(selected);
|
|
3152
|
+
return {
|
|
3153
|
+
...options,
|
|
3154
|
+
projectId: selected,
|
|
3155
|
+
projectSlug,
|
|
3156
|
+
projectIdSource: "selection",
|
|
3157
|
+
workspaceMode: true,
|
|
3158
|
+
launchUrlQuery: studioLaunchQueryWithProject(options.launchUrlQuery, selected),
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
3161
|
+
function studioLaunchQueryWithProject(query, projectId) {
|
|
3162
|
+
const params = new URLSearchParams(String(query || "").replace(/^\?+/, ""));
|
|
3163
|
+
params.set("project", projectId);
|
|
3164
|
+
return params.toString();
|
|
3165
|
+
}
|
|
3166
|
+
async function readStudioSelectionRecord() {
|
|
3167
|
+
try {
|
|
3168
|
+
const payload = JSON.parse(await readFile(studioSelectionRecordPath(), "utf8"));
|
|
3169
|
+
if (!isRecord(payload) || payload.version !== 1) {
|
|
3170
|
+
return null;
|
|
3171
|
+
}
|
|
3172
|
+
const lastTappId = typeof payload.lastTappId === "string" ? payload.lastTappId.trim() : "";
|
|
3173
|
+
if (!lastTappId) {
|
|
3174
|
+
return null;
|
|
3175
|
+
}
|
|
3176
|
+
return {
|
|
3177
|
+
version: 1,
|
|
3178
|
+
lastTappId,
|
|
3179
|
+
updatedAt: typeof payload.updatedAt === "string" ? payload.updatedAt : "",
|
|
3180
|
+
};
|
|
3181
|
+
}
|
|
3182
|
+
catch (error) {
|
|
3183
|
+
if (error.code === "ENOENT") {
|
|
3184
|
+
return null;
|
|
3185
|
+
}
|
|
3186
|
+
return null;
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
async function writeStudioSelectionRecord(tappId) {
|
|
3190
|
+
await mkdir(getDefaultTapiDataDir(), { recursive: true });
|
|
3191
|
+
await writeJsonFile(studioSelectionRecordPath(), {
|
|
3192
|
+
version: 1,
|
|
3193
|
+
lastTappId: tappId,
|
|
3194
|
+
updatedAt: new Date().toISOString(),
|
|
3195
|
+
});
|
|
3196
|
+
}
|
|
3197
|
+
function studioSelectionRecordPath() {
|
|
3198
|
+
return join(getDefaultTapiDataDir(), "studio-selection.json");
|
|
3199
|
+
}
|
|
2986
3200
|
function findStudioExecutable(options) {
|
|
2987
3201
|
if (options.exePath) {
|
|
2988
3202
|
return existsSync(options.exePath) ? options.exePath : undefined;
|
|
@@ -3053,7 +3267,7 @@ async function installPortableStudioServer(options, manifest, event) {
|
|
|
3053
3267
|
destination: artifactPath,
|
|
3054
3268
|
expectedSha256: manifest.sha256,
|
|
3055
3269
|
});
|
|
3056
|
-
await
|
|
3270
|
+
await downloadAndVerify(manifest.url, artifactPath, manifest.sha256, "Tapi Studio server artifact");
|
|
3057
3271
|
await event.phase("download.verified", {
|
|
3058
3272
|
artifactPath,
|
|
3059
3273
|
expectedSha256: manifest.sha256,
|
|
@@ -3115,6 +3329,7 @@ async function launchPortableStudioServer(exePath, options, manifest) {
|
|
|
3115
3329
|
pid: childPid || undefined,
|
|
3116
3330
|
apiBaseUrl: options.apiBaseUrl,
|
|
3117
3331
|
projectId: options.projectId,
|
|
3332
|
+
tappId: options.projectId,
|
|
3118
3333
|
workspaceRoot: options.workspaceRoot,
|
|
3119
3334
|
workspaceConfig: options.workspace?.configPath,
|
|
3120
3335
|
manifestVersion: manifest.version,
|
|
@@ -3420,6 +3635,8 @@ async function downloadFile(url, destination, label = "download", options = {})
|
|
|
3420
3635
|
let destinationStream;
|
|
3421
3636
|
let receivedBytes = 0;
|
|
3422
3637
|
let nextProgressLogBytes = DOWNLOAD_PROGRESS_LOG_BYTES;
|
|
3638
|
+
let progressBar;
|
|
3639
|
+
let expectedBytes = 0;
|
|
3423
3640
|
const failIfStalled = (phase) => {
|
|
3424
3641
|
if (stallTimer) {
|
|
3425
3642
|
clearTimeout(stallTimer);
|
|
@@ -3433,7 +3650,6 @@ async function downloadFile(url, destination, label = "download", options = {})
|
|
|
3433
3650
|
}, stallTimeoutMs);
|
|
3434
3651
|
};
|
|
3435
3652
|
try {
|
|
3436
|
-
console.log(`Downloading ${label}...`);
|
|
3437
3653
|
failIfStalled("connecting");
|
|
3438
3654
|
const response = await fetch(url, { signal: controller.signal });
|
|
3439
3655
|
if (!response.ok) {
|
|
@@ -3443,12 +3659,29 @@ async function downloadFile(url, destination, label = "download", options = {})
|
|
|
3443
3659
|
throw new Error(`${label} response did not include a body.`);
|
|
3444
3660
|
}
|
|
3445
3661
|
failIfStalled("receiving data");
|
|
3662
|
+
expectedBytes = parseContentLength(typeof response.headers?.get === "function" ? response.headers.get("content-length") : null);
|
|
3663
|
+
progressBar = createDownloadProgressBar(label, expectedBytes);
|
|
3664
|
+
if (progressBar) {
|
|
3665
|
+
progressBar.start(expectedBytes, 0, {
|
|
3666
|
+
value: formatBytes(0),
|
|
3667
|
+
total: formatBytes(expectedBytes),
|
|
3668
|
+
});
|
|
3669
|
+
}
|
|
3670
|
+
else {
|
|
3671
|
+
console.log(`Downloading ${label}...`);
|
|
3672
|
+
}
|
|
3446
3673
|
source = Readable.fromWeb(response.body);
|
|
3447
3674
|
progressStream = new Transform({
|
|
3448
3675
|
transform(chunk, _encoding, callback) {
|
|
3449
3676
|
receivedBytes += downloadChunkByteLength(chunk);
|
|
3450
3677
|
failIfStalled("receiving data");
|
|
3451
|
-
if (
|
|
3678
|
+
if (progressBar) {
|
|
3679
|
+
progressBar.update(Math.min(receivedBytes, expectedBytes), {
|
|
3680
|
+
value: formatBytes(receivedBytes),
|
|
3681
|
+
total: formatBytes(expectedBytes),
|
|
3682
|
+
});
|
|
3683
|
+
}
|
|
3684
|
+
else if (receivedBytes >= nextProgressLogBytes) {
|
|
3452
3685
|
console.log(`Downloaded ${label}: ${formatBytes(receivedBytes)}...`);
|
|
3453
3686
|
while (receivedBytes >= nextProgressLogBytes) {
|
|
3454
3687
|
nextProgressLogBytes += DOWNLOAD_PROGRESS_LOG_BYTES;
|
|
@@ -3459,9 +3692,23 @@ async function downloadFile(url, destination, label = "download", options = {})
|
|
|
3459
3692
|
});
|
|
3460
3693
|
destinationStream = createWriteStream(destination);
|
|
3461
3694
|
await pipeline(source, progressStream, destinationStream);
|
|
3462
|
-
|
|
3695
|
+
if (progressBar) {
|
|
3696
|
+
progressBar.update(expectedBytes, {
|
|
3697
|
+
value: formatBytes(receivedBytes),
|
|
3698
|
+
total: formatBytes(expectedBytes),
|
|
3699
|
+
});
|
|
3700
|
+
progressBar.stop();
|
|
3701
|
+
progressBar = undefined;
|
|
3702
|
+
}
|
|
3703
|
+
else {
|
|
3704
|
+
console.log(`Downloaded ${label}: ${formatBytes(receivedBytes)}.`);
|
|
3705
|
+
}
|
|
3463
3706
|
}
|
|
3464
3707
|
catch (error) {
|
|
3708
|
+
if (progressBar) {
|
|
3709
|
+
progressBar.stop();
|
|
3710
|
+
progressBar = undefined;
|
|
3711
|
+
}
|
|
3465
3712
|
if (stallError) {
|
|
3466
3713
|
throw stallError;
|
|
3467
3714
|
}
|
|
@@ -3717,6 +3964,7 @@ async function readStudioInstanceRecord(options) {
|
|
|
3717
3964
|
pid: typeof parsed.pid === "number" && Number.isFinite(parsed.pid) ? parsed.pid : undefined,
|
|
3718
3965
|
apiBaseUrl: typeof parsed.apiBaseUrl === "string" ? parsed.apiBaseUrl : "",
|
|
3719
3966
|
projectId: typeof parsed.projectId === "string" ? parsed.projectId : undefined,
|
|
3967
|
+
tappId: typeof parsed.tappId === "string" ? parsed.tappId : undefined,
|
|
3720
3968
|
workspaceRoot: typeof parsed.workspaceRoot === "string" ? parsed.workspaceRoot : undefined,
|
|
3721
3969
|
workspaceConfig: typeof parsed.workspaceConfig === "string" ? parsed.workspaceConfig : undefined,
|
|
3722
3970
|
manifestVersion: typeof parsed.manifestVersion === "string" ? parsed.manifestVersion : undefined,
|
|
@@ -3842,6 +4090,31 @@ async function readJsonBody(response) {
|
|
|
3842
4090
|
return null;
|
|
3843
4091
|
}
|
|
3844
4092
|
}
|
|
4093
|
+
function createDownloadProgressBar(label, totalBytes) {
|
|
4094
|
+
if (!downloadProgressEnabled() || totalBytes <= 0) {
|
|
4095
|
+
return undefined;
|
|
4096
|
+
}
|
|
4097
|
+
return new SingleBar({
|
|
4098
|
+
format: `${label} |{bar}| {percentage}% | {value}/{total}`,
|
|
4099
|
+
stream: process.stderr,
|
|
4100
|
+
clearOnComplete: false,
|
|
4101
|
+
hideCursor: true,
|
|
4102
|
+
barsize: 32,
|
|
4103
|
+
barCompleteChar: "#",
|
|
4104
|
+
barIncompleteChar: "-",
|
|
4105
|
+
}, Presets.shades_classic);
|
|
4106
|
+
}
|
|
4107
|
+
function downloadProgressEnabled() {
|
|
4108
|
+
const raw = String(process.env.TAPI_CLI_PROGRESS || "").trim().toLowerCase();
|
|
4109
|
+
if (["0", "false", "no", "off"].includes(raw)) {
|
|
4110
|
+
return false;
|
|
4111
|
+
}
|
|
4112
|
+
return Boolean(process.stderr.isTTY);
|
|
4113
|
+
}
|
|
4114
|
+
function parseContentLength(value) {
|
|
4115
|
+
const parsed = Number(value || "");
|
|
4116
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;
|
|
4117
|
+
}
|
|
3845
4118
|
async function readRequestJson(request) {
|
|
3846
4119
|
let body = "";
|
|
3847
4120
|
for await (const chunk of request) {
|
|
@@ -4299,6 +4572,9 @@ function installEventOptions(options) {
|
|
|
4299
4572
|
downloadOnly: options.downloadOnly,
|
|
4300
4573
|
silent: options.silent,
|
|
4301
4574
|
exePath: options.exePath,
|
|
4575
|
+
projectId: options.projectId,
|
|
4576
|
+
projectIdSource: options.projectIdSource,
|
|
4577
|
+
tappSelectionMode: options.tappSelectionMode,
|
|
4302
4578
|
installTokenProvided: Boolean(options.installToken),
|
|
4303
4579
|
installTokenSource: options.installTokenSource,
|
|
4304
4580
|
};
|
|
@@ -4429,8 +4705,10 @@ Usage:
|
|
|
4429
4705
|
tapi login
|
|
4430
4706
|
tapi init --project PROJECT
|
|
4431
4707
|
tapi link --project PROJECT
|
|
4432
|
-
tapi studio install [--channel pilot] [--api-base-url URL]
|
|
4433
|
-
tapi studio
|
|
4708
|
+
tapi studio install [--channel pilot] [--api-base-url URL]
|
|
4709
|
+
tapi studio
|
|
4710
|
+
tapi studio --tapp <tapp>
|
|
4711
|
+
tapi studio --select
|
|
4434
4712
|
tapi studio open
|
|
4435
4713
|
tapi studio doctor
|
|
4436
4714
|
tapi tapp create <tapp>
|
|
@@ -4451,8 +4729,8 @@ Commands:
|
|
|
4451
4729
|
init Create .tapi/project.json for this repo
|
|
4452
4730
|
link Rebind this repo to an existing Tapi project
|
|
4453
4731
|
studio install Download, verify, and run the Tapi Studio installer
|
|
4454
|
-
studio Open Tapi Studio for
|
|
4455
|
-
studio open Open Tapi Studio for
|
|
4732
|
+
studio Open Tapi Studio for a selected Tapp
|
|
4733
|
+
studio open Open Tapi Studio for a selected Tapp
|
|
4456
4734
|
studio doctor Check local SDK and Studio release configuration
|
|
4457
4735
|
tapp create Create a Tapp
|
|
4458
4736
|
queue create Create a queue for service-run routing
|
|
@@ -4591,14 +4869,18 @@ Usage:
|
|
|
4591
4869
|
tapi studio open [options]
|
|
4592
4870
|
tapi studio doctor [options]
|
|
4593
4871
|
|
|
4594
|
-
Options:
|
|
4595
|
-
--channel <name> Release channel: pilot, stable, or nightly
|
|
4596
|
-
--api-base-url <url> Tapi API base URL for install authorization and protected downloads
|
|
4597
|
-
--server <url> Alias for --api-base-url
|
|
4598
|
-
--
|
|
4599
|
-
--
|
|
4600
|
-
--
|
|
4601
|
-
--
|
|
4872
|
+
Options:
|
|
4873
|
+
--channel <name> Release channel: pilot, stable, or nightly
|
|
4874
|
+
--api-base-url <url> Tapi API base URL for install authorization and protected downloads
|
|
4875
|
+
--server <url> Alias for --api-base-url
|
|
4876
|
+
--tapp <id> Open this Tapp directly
|
|
4877
|
+
--select Force the account Tapp picker
|
|
4878
|
+
--last Reuse the last selected Tapp
|
|
4879
|
+
--no-select Skip the picker and use env/workspace/default context
|
|
4880
|
+
--workspace <path> Search this directory for .tapi/project.json
|
|
4881
|
+
--no-workspace Do not load .tapi/project.json
|
|
4882
|
+
--project <id> Alias for --tapp
|
|
4883
|
+
--project-slug <slug> Optional display slug for the bound project
|
|
4602
4884
|
--install-token <tok> Preissued Studio install token (skips browser sign-in)
|
|
4603
4885
|
--manifest <url> Exact release manifest URL for doctor only
|
|
4604
4886
|
--cache-dir <path> Installer download cache directory
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tapi-dev/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.39",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -29,9 +29,15 @@
|
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
+
"@types/cli-progress": "^3.11.6",
|
|
32
33
|
"@types/node": "^20.0.0",
|
|
33
34
|
"typescript": "^5.5.0",
|
|
34
35
|
"vite": "6.4.3",
|
|
35
36
|
"vitest": "^4.1.8"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@inquirer/prompts": "^7.10.1",
|
|
40
|
+
"cli-progress": "^3.12.0",
|
|
41
|
+
"yoctocolors": "^2.1.2"
|
|
36
42
|
}
|
|
37
43
|
}
|