@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 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 app repo. The repo's `.tapi/project.json`
19
- is the project binding, so one developer can work on multiple Tapi projects from
20
- different directories without a global Studio project picker.
21
-
22
- ```bash
23
- npm install @tapi-dev/sdk
24
- tapi init --project brokerage
25
- tapi studio
26
- ```
27
-
28
- `tapi studio` checks the required local `tapi-service`, installs it when needed,
29
- downloads the portable Studio server release when the channel manifest uses
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 projectId = raw.projectId ?? envString("TAPI_PROJECT_ID") ?? workspace?.projectId;
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 withCliSpinner(`Downloading Tapi Service ${manifest.version}`, () => downloadAndVerify(manifest.url, zipPath, manifest.sha256, "Tapi Service artifact"));
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 withCliSpinner(`Downloading Tapi Studio ${manifest.version}`, () => downloadAndVerify(manifest.url, installerPath, manifest.sha256));
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 ensureServiceReadyForStudio(options);
2995
+ const launchOptions = await resolveStudioTappSelection(options);
2996
+ await ensureServiceReadyForStudio(launchOptions);
2954
2997
  await closeExistingPortableStudioServersForFreshLaunch();
2955
- if (!options.exePath) {
2956
- const portable = await ensurePortableStudioServer(options);
2998
+ if (!launchOptions.exePath) {
2999
+ const portable = await ensurePortableStudioServer(launchOptions);
2957
3000
  if (portable) {
2958
- await launchPortableStudioServer(portable.exePath, options, portable.manifest);
3001
+ await launchPortableStudioServer(portable.exePath, launchOptions, portable.manifest);
2959
3002
  return 0;
2960
3003
  }
2961
3004
  }
2962
- let exePath = findStudioExecutable(options);
2963
- if (!exePath && !options.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(options);
3008
+ const installCode = await installStudio(launchOptions);
2966
3009
  if (installCode !== 0) {
2967
3010
  return installCode;
2968
3011
  }
2969
- exePath = findStudioExecutable(options);
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(options),
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 withCliSpinner(`Downloading Tapi Studio server ${manifest.version}`, () => downloadAndVerify(manifest.url, artifactPath, manifest.sha256, "Tapi Studio server artifact"));
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 (receivedBytes >= nextProgressLogBytes) {
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
- console.log(`Downloaded ${label}: ${formatBytes(receivedBytes)}.`);
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 this repo
4455
- studio open Open Tapi Studio for this repo
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
- --workspace <path> Search this directory for .tapi/project.json
4599
- --no-workspace Do not load .tapi/project.json
4600
- --project <id> Tapi project id for workspace-bound Studio
4601
- --project-slug <slug> Optional display slug for the bound project
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.38",
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
  }