@tapi-dev/sdk 0.1.36 → 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/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";
@@ -117,6 +120,50 @@ async function withCliSpinner(message, work) {
117
120
  throw error;
118
121
  }
119
122
  }
123
+ async function withSdkAuth(options) {
124
+ if (options.authToken?.trim()) {
125
+ return { ...options, authToken: options.authToken.trim() };
126
+ }
127
+ const envAuthToken = envString("TAPI_FIREBASE_ID_TOKEN");
128
+ if (envAuthToken) {
129
+ return { ...options, authToken: envAuthToken };
130
+ }
131
+ const credentials = await requireSdkFirebaseCredentials();
132
+ return { ...options, authToken: credentials.idToken };
133
+ }
134
+ async function requireSdkFirebaseCredentials() {
135
+ const credentials = await refreshCachedFirebaseCredentials();
136
+ if (!credentials) {
137
+ throw new Error(`Tapi sign-in is required. Run \`tapi login\`, then retry.`);
138
+ }
139
+ await writeStudioAuthCache(credentials);
140
+ return credentials;
141
+ }
142
+ async function runLogin() {
143
+ try {
144
+ const credentials = await withCliSpinner("Signing in to Tapi", () => authenticateSdkUser());
145
+ console.log(`Signed in to Tapi as ${credentials.uid}.`);
146
+ return 0;
147
+ }
148
+ catch (error) {
149
+ console.error(formatError(error));
150
+ return 1;
151
+ }
152
+ }
153
+ async function authenticateSdkUser() {
154
+ const cached = await refreshCachedFirebaseCredentials();
155
+ if (cached) {
156
+ await writeStudioAuthCache(cached);
157
+ return cached;
158
+ }
159
+ const browserAuth = await browserGoogleSignIn();
160
+ if (!browserAuth.idToken) {
161
+ throw new Error(browserAuth.error || browserAuth.detail || "Browser sign-in did not return a Google ID token.");
162
+ }
163
+ const credentials = await exchangeGoogleToFirebase(browserAuth.idToken, browserAuth.accessToken);
164
+ await writeStudioAuthCache(credentials);
165
+ return credentials;
166
+ }
120
167
  export async function runCli(argv = process.argv.slice(2)) {
121
168
  const [command, subcommand, ...rest] = argv;
122
169
  if (!command || command === "help" || command === "--help" || command === "-h") {
@@ -127,6 +174,18 @@ export async function runCli(argv = process.argv.slice(2)) {
127
174
  console.log(sdkVersion);
128
175
  return 0;
129
176
  }
177
+ if (command === "login") {
178
+ if (hasHelpFlag([subcommand, ...rest])) {
179
+ printLoginHelp();
180
+ return 0;
181
+ }
182
+ if (subcommand) {
183
+ console.error(`Unexpected login argument: ${subcommand}`);
184
+ printLoginHelp();
185
+ return 1;
186
+ }
187
+ return runLogin();
188
+ }
130
189
  if (command === "doctor") {
131
190
  if (hasHelpFlag([subcommand, ...rest])) {
132
191
  printStudioHelp();
@@ -258,6 +317,9 @@ export async function runCli(argv = process.argv.slice(2)) {
258
317
  if (!subcommand) {
259
318
  return openStudio(parseStudioOptions([]));
260
319
  }
320
+ if (subcommand.startsWith("-")) {
321
+ return openStudio(parseStudioOptions([subcommand, ...rest]));
322
+ }
261
323
  if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
262
324
  printStudioHelp();
263
325
  return 0;
@@ -284,6 +346,8 @@ export function parseStudioOptions(args) {
284
346
  const raw = {};
285
347
  let workspaceSearchRoot;
286
348
  let skipWorkspace = false;
349
+ let projectIdSource;
350
+ let tappSelectionMode = "auto";
287
351
  for (let index = 0; index < args.length; index += 1) {
288
352
  const arg = args[index];
289
353
  if (!arg) {
@@ -329,12 +393,40 @@ export function parseStudioOptions(args) {
329
393
  skipWorkspace = true;
330
394
  continue;
331
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
+ }
332
420
  if (arg === "--project") {
333
421
  raw.projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
422
+ projectIdSource = "option";
423
+ tappSelectionMode = "none";
334
424
  continue;
335
425
  }
336
426
  if (arg.startsWith("--project=")) {
337
427
  raw.projectId = normalizeProjectValue(arg.slice("--project=".length), "project");
428
+ projectIdSource = "option";
429
+ tappSelectionMode = "none";
338
430
  continue;
339
431
  }
340
432
  if (arg === "--project-slug") {
@@ -403,7 +495,11 @@ export function parseStudioOptions(args) {
403
495
  const manifestUrl = explicitManifestUrl
404
496
  ? normalizeHttpUrl(explicitManifestUrl, "Studio manifest URL")
405
497
  : `${downloadsBaseUrl.replace(/\/+$/, "")}/studio/channels/${channel}/latest.json`;
406
- 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
+ }
407
503
  const projectSlug = raw.projectSlug ?? envString("TAPI_PROJECT_SLUG") ?? workspace?.projectSlug;
408
504
  const envInstallToken = envString("TAPI_STUDIO_INSTALL_TOKEN");
409
505
  const installToken = raw.installToken ?? envInstallToken;
@@ -423,14 +519,15 @@ export function parseStudioOptions(args) {
423
519
  workspaceRoot: workspace?.root,
424
520
  projectId,
425
521
  projectSlug,
522
+ projectIdSource,
426
523
  workspaceMode: Boolean(projectId || workspace),
524
+ tappSelectionMode,
427
525
  };
428
526
  }
429
527
  function parseServiceOptions(args) {
430
528
  let operation = "";
431
529
  const workspace = loadWorkspace();
432
530
  let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
433
- let apiKey = envString("TAPI_API_KEY") || "";
434
531
  let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
435
532
  for (let index = 0; index < args.length; index += 1) {
436
533
  const arg = args[index];
@@ -448,14 +545,6 @@ function parseServiceOptions(args) {
448
545
  apiBaseUrl = arg.slice("--server=".length);
449
546
  continue;
450
547
  }
451
- if (arg === "--api-key") {
452
- apiKey = requireOptionValue(args, ++index, "--api-key");
453
- continue;
454
- }
455
- if (arg.startsWith("--api-key=")) {
456
- apiKey = arg.slice("--api-key=".length);
457
- continue;
458
- }
459
548
  if (arg === "--project") {
460
549
  projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
461
550
  continue;
@@ -476,9 +565,6 @@ function parseServiceOptions(args) {
476
565
  if (!operation) {
477
566
  throw new Error("services describe requires a service run like schwab.place_order.");
478
567
  }
479
- if (!apiKey) {
480
- throw new Error("services describe requires --api-key or TAPI_API_KEY.");
481
- }
482
568
  if (!projectId) {
483
569
  throw new Error("services describe requires --project, TAPI_PROJECT_ID, or .tapi/project.json.");
484
570
  }
@@ -486,7 +572,6 @@ function parseServiceOptions(args) {
486
572
  operation,
487
573
  options: {
488
574
  apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
489
- apiKey,
490
575
  projectId,
491
576
  },
492
577
  };
@@ -494,11 +579,12 @@ function parseServiceOptions(args) {
494
579
  async function describeServiceOperation(args) {
495
580
  try {
496
581
  const { operation, options } = parseServiceOptions(args);
582
+ const authenticated = await withSdkAuth(options);
497
583
  const client = new TapiClient({
498
- baseUrl: options.apiBaseUrl,
499
- apiKey: options.apiKey,
500
- tappId: options.projectId,
501
- projectId: options.projectId,
584
+ baseUrl: authenticated.apiBaseUrl,
585
+ authToken: authenticated.authToken,
586
+ tappId: authenticated.projectId,
587
+ projectId: authenticated.projectId,
502
588
  });
503
589
  const description = await withCliSpinner(`Fetching Tapi service-run contract for ${operation}`, () => client.services.describe(operation));
504
590
  console.log(JSON.stringify(description, null, 2));
@@ -512,7 +598,6 @@ async function describeServiceOperation(args) {
512
598
  function parseApiWorkspaceOptions(args) {
513
599
  const workspace = loadWorkspace();
514
600
  let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
515
- let apiKey = envString("TAPI_API_KEY") || "";
516
601
  let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
517
602
  let catalogPath = workspace?.config.services?.catalog || ".tapi/services/catalog.json";
518
603
  for (let index = 0; index < args.length; index += 1) {
@@ -531,14 +616,6 @@ function parseApiWorkspaceOptions(args) {
531
616
  apiBaseUrl = arg.slice("--server=".length);
532
617
  continue;
533
618
  }
534
- if (arg === "--api-key") {
535
- apiKey = requireOptionValue(args, ++index, "--api-key");
536
- continue;
537
- }
538
- if (arg.startsWith("--api-key=")) {
539
- apiKey = arg.slice("--api-key=".length);
540
- continue;
541
- }
542
619
  if (arg === "--project") {
543
620
  projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
544
621
  continue;
@@ -557,23 +634,19 @@ function parseApiWorkspaceOptions(args) {
557
634
  }
558
635
  throw new Error(`Unknown services option: ${arg}`);
559
636
  }
560
- if (!apiKey) {
561
- throw new Error("services command requires --api-key or TAPI_API_KEY.");
562
- }
563
637
  if (!projectId) {
564
638
  throw new Error("services command requires --project, TAPI_PROJECT_ID, or .tapi/project.json.");
565
639
  }
566
640
  const root = workspace?.root || process.cwd();
567
641
  return {
568
642
  apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
569
- apiKey,
570
643
  projectId,
571
644
  catalogPath: resolve(root, catalogPath),
572
645
  };
573
646
  }
574
647
  async function syncServiceCatalog(args) {
575
648
  try {
576
- const options = parseApiWorkspaceOptions(args);
649
+ const options = await withSdkAuth(parseApiWorkspaceOptions(args));
577
650
  const catalog = await withCliSpinner("Fetching Tapi service-run catalog", () => fetchApiCatalog(options));
578
651
  await writeJsonFile(options.catalogPath, catalog);
579
652
  console.log(`Synced Tapi service-run catalog: ${options.catalogPath}`);
@@ -587,7 +660,6 @@ async function syncServiceCatalog(args) {
587
660
  function parseTappServiceAddOptions(args) {
588
661
  const workspace = loadWorkspace();
589
662
  let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
590
- let apiKey = envString("TAPI_API_KEY") || "";
591
663
  let tappId = "";
592
664
  let serviceCall = "";
593
665
  let serviceMapId = "";
@@ -609,14 +681,6 @@ function parseTappServiceAddOptions(args) {
609
681
  apiBaseUrl = arg.slice("--server=".length);
610
682
  continue;
611
683
  }
612
- if (arg === "--api-key") {
613
- apiKey = requireOptionValue(args, ++index, "--api-key");
614
- continue;
615
- }
616
- if (arg.startsWith("--api-key=")) {
617
- apiKey = arg.slice("--api-key=".length);
618
- continue;
619
- }
620
684
  if (arg === "--service-map") {
621
685
  serviceMapId = requireOptionValue(args, ++index, "--service-map");
622
686
  continue;
@@ -654,9 +718,6 @@ function parseTappServiceAddOptions(args) {
654
718
  }
655
719
  throw new Error(`Unexpected tapp service add argument: ${arg}`);
656
720
  }
657
- if (!apiKey) {
658
- throw new Error("tapp service add requires --api-key or TAPI_API_KEY.");
659
- }
660
721
  if (!tappId) {
661
722
  throw new Error("tapp service add requires a Tapp id.");
662
723
  }
@@ -671,7 +732,6 @@ function parseTappServiceAddOptions(args) {
671
732
  }
672
733
  return {
673
734
  apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
674
- apiKey,
675
735
  tappId,
676
736
  serviceCall,
677
737
  serviceMapId,
@@ -682,7 +742,6 @@ function parseTappServiceAddOptions(args) {
682
742
  function parseTappCreateOptions(args) {
683
743
  const workspace = loadWorkspace();
684
744
  let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
685
- let apiKey = envString("TAPI_API_KEY") || "";
686
745
  let tappId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
687
746
  let name = "";
688
747
  for (let index = 0; index < args.length; index += 1) {
@@ -701,14 +760,6 @@ function parseTappCreateOptions(args) {
701
760
  apiBaseUrl = arg.slice("--server=".length);
702
761
  continue;
703
762
  }
704
- if (arg === "--api-key") {
705
- apiKey = requireOptionValue(args, ++index, "--api-key");
706
- continue;
707
- }
708
- if (arg.startsWith("--api-key=")) {
709
- apiKey = arg.slice("--api-key=".length);
710
- continue;
711
- }
712
763
  if (arg === "--tapp-id" || arg === "--project") {
713
764
  tappId = normalizeProjectValue(requireOptionValue(args, ++index, arg), "tapp");
714
765
  continue;
@@ -741,9 +792,6 @@ function parseTappCreateOptions(args) {
741
792
  }
742
793
  throw new Error(`Unexpected tapp create argument: ${arg}`);
743
794
  }
744
- if (!apiKey) {
745
- throw new Error("tapp create requires --api-key or TAPI_API_KEY.");
746
- }
747
795
  if (!tappId) {
748
796
  throw new Error("tapp create requires a Tapp id or name.");
749
797
  }
@@ -752,14 +800,13 @@ function parseTappCreateOptions(args) {
752
800
  }
753
801
  return {
754
802
  apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
755
- apiKey,
756
803
  tappId,
757
804
  name,
758
805
  };
759
806
  }
760
807
  async function createTapp(args) {
761
808
  try {
762
- const options = parseTappCreateOptions(args);
809
+ const options = await withSdkAuth(parseTappCreateOptions(args));
763
810
  const tapp = await withCliSpinner(`Creating Tapi Tapp ${options.tappId}`, () => postTappCreate(options));
764
811
  console.log(JSON.stringify(tapp, null, 2));
765
812
  return 0;
@@ -772,11 +819,7 @@ async function createTapp(args) {
772
819
  async function postTappCreate(options, fetchImpl = fetch) {
773
820
  const response = await fetchImpl(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/tapps`, {
774
821
  method: "POST",
775
- headers: {
776
- "Content-Type": "application/json",
777
- "X-API-Key": options.apiKey,
778
- "X-Tapi-Project": options.tappId,
779
- },
822
+ headers: sdkJsonHeaders(options.authToken, options.tappId),
780
823
  body: JSON.stringify({
781
824
  tappId: options.tappId,
782
825
  name: options.name,
@@ -791,7 +834,7 @@ async function postTappCreate(options, fetchImpl = fetch) {
791
834
  }
792
835
  async function addTappService(args) {
793
836
  try {
794
- const options = parseTappServiceAddOptions(args);
837
+ const options = await withSdkAuth(parseTappServiceAddOptions(args));
795
838
  const operation = await withCliSpinner(`Adding Tapi service ${options.serviceCall} to ${options.tappId}`, () => postTappServiceAdd(options));
796
839
  console.log(JSON.stringify(operation, null, 2));
797
840
  return 0;
@@ -804,11 +847,7 @@ async function addTappService(args) {
804
847
  async function postTappServiceAdd(options) {
805
848
  const response = await fetch(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/tapps/${encodeURIComponent(options.tappId)}/services/${encodeURIComponent(options.serviceCall)}`, {
806
849
  method: "POST",
807
- headers: {
808
- "Content-Type": "application/json",
809
- "X-API-Key": options.apiKey,
810
- "X-Tapi-Project": options.tappId,
811
- },
850
+ headers: sdkJsonHeaders(options.authToken, options.tappId),
812
851
  body: JSON.stringify({
813
852
  serviceMapId: options.serviceMapId,
814
853
  entry: options.entry,
@@ -825,7 +864,6 @@ async function postTappServiceAdd(options) {
825
864
  function parseQueueCreateOptions(args) {
826
865
  const workspace = loadWorkspace();
827
866
  let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
828
- let apiKey = envString("TAPI_API_KEY") || "";
829
867
  let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
830
868
  let displayName = "";
831
869
  let maxSlots = 8;
@@ -845,14 +883,6 @@ function parseQueueCreateOptions(args) {
845
883
  apiBaseUrl = arg.slice("--server=".length);
846
884
  continue;
847
885
  }
848
- if (arg === "--api-key") {
849
- apiKey = requireOptionValue(args, ++index, "--api-key");
850
- continue;
851
- }
852
- if (arg.startsWith("--api-key=")) {
853
- apiKey = arg.slice("--api-key=".length);
854
- continue;
855
- }
856
886
  if (arg === "--project") {
857
887
  projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
858
888
  continue;
@@ -878,12 +908,8 @@ function parseQueueCreateOptions(args) {
878
908
  }
879
909
  throw new Error(`Unexpected queue create argument: ${arg}`);
880
910
  }
881
- if (!apiKey) {
882
- throw new Error("queue create requires --api-key or TAPI_API_KEY.");
883
- }
884
911
  return {
885
912
  apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
886
- apiKey,
887
913
  projectId,
888
914
  displayName,
889
915
  maxSlots,
@@ -892,7 +918,6 @@ function parseQueueCreateOptions(args) {
892
918
  function parseTappQueueAddOptions(args) {
893
919
  const workspace = loadWorkspace();
894
920
  let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
895
- let apiKey = envString("TAPI_API_KEY") || "";
896
921
  let tappId = "";
897
922
  let queueId = "";
898
923
  for (let index = 0; index < args.length; index += 1) {
@@ -911,14 +936,6 @@ function parseTappQueueAddOptions(args) {
911
936
  apiBaseUrl = arg.slice("--server=".length);
912
937
  continue;
913
938
  }
914
- if (arg === "--api-key") {
915
- apiKey = requireOptionValue(args, ++index, "--api-key");
916
- continue;
917
- }
918
- if (arg.startsWith("--api-key=")) {
919
- apiKey = arg.slice("--api-key=".length);
920
- continue;
921
- }
922
939
  if (arg.startsWith("--")) {
923
940
  throw new Error(`Unknown tapp queue add option: ${arg}`);
924
941
  }
@@ -932,9 +949,6 @@ function parseTappQueueAddOptions(args) {
932
949
  }
933
950
  throw new Error(`Unexpected tapp queue add argument: ${arg}`);
934
951
  }
935
- if (!apiKey) {
936
- throw new Error("tapp queue add requires --api-key or TAPI_API_KEY.");
937
- }
938
952
  if (!tappId) {
939
953
  throw new Error("tapp queue add requires a Tapp id.");
940
954
  }
@@ -943,7 +957,6 @@ function parseTappQueueAddOptions(args) {
943
957
  }
944
958
  return {
945
959
  apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
946
- apiKey,
947
960
  tappId,
948
961
  queueId,
949
962
  };
@@ -951,7 +964,6 @@ function parseTappQueueAddOptions(args) {
951
964
  function parseRunnerSlotSetOptions(args) {
952
965
  const workspace = loadWorkspace();
953
966
  let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
954
- let apiKey = envString("TAPI_API_KEY") || "";
955
967
  let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
956
968
  let runnerId = "";
957
969
  let slotIndex = 0;
@@ -972,14 +984,6 @@ function parseRunnerSlotSetOptions(args) {
972
984
  apiBaseUrl = arg.slice("--server=".length);
973
985
  continue;
974
986
  }
975
- if (arg === "--api-key") {
976
- apiKey = requireOptionValue(args, ++index, "--api-key");
977
- continue;
978
- }
979
- if (arg.startsWith("--api-key=")) {
980
- apiKey = arg.slice("--api-key=".length);
981
- continue;
982
- }
983
987
  if (arg === "--project") {
984
988
  projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
985
989
  continue;
@@ -1005,9 +1009,6 @@ function parseRunnerSlotSetOptions(args) {
1005
1009
  }
1006
1010
  throw new Error(`Unexpected runner slot set argument: ${arg}`);
1007
1011
  }
1008
- if (!apiKey) {
1009
- throw new Error("runner slot set requires --api-key or TAPI_API_KEY.");
1010
- }
1011
1012
  if (!projectId) {
1012
1013
  throw new Error("runner slot set requires --project or TAPI_PROJECT_ID.");
1013
1014
  }
@@ -1022,7 +1023,6 @@ function parseRunnerSlotSetOptions(args) {
1022
1023
  }
1023
1024
  return {
1024
1025
  apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
1025
- apiKey,
1026
1026
  projectId,
1027
1027
  runnerId,
1028
1028
  slotIndex,
@@ -1031,7 +1031,7 @@ function parseRunnerSlotSetOptions(args) {
1031
1031
  }
1032
1032
  async function createQueue(args) {
1033
1033
  try {
1034
- const options = parseQueueCreateOptions(args);
1034
+ const options = await withSdkAuth(parseQueueCreateOptions(args));
1035
1035
  const queue = await withCliSpinner(`Creating Tapi queue`, () => postQueueCreate(options));
1036
1036
  console.log(JSON.stringify(queue, null, 2));
1037
1037
  return 0;
@@ -1043,7 +1043,7 @@ async function createQueue(args) {
1043
1043
  }
1044
1044
  async function addTappQueue(args) {
1045
1045
  try {
1046
- const options = parseTappQueueAddOptions(args);
1046
+ const options = await withSdkAuth(parseTappQueueAddOptions(args));
1047
1047
  const result = await withCliSpinner(`Attaching Tapi queue ${options.queueId} to ${options.tappId}`, () => postTappQueueAdd(options));
1048
1048
  console.log(JSON.stringify(result, null, 2));
1049
1049
  return 0;
@@ -1055,7 +1055,7 @@ async function addTappQueue(args) {
1055
1055
  }
1056
1056
  async function setRunnerSlot(args) {
1057
1057
  try {
1058
- const options = parseRunnerSlotSetOptions(args);
1058
+ const options = await withSdkAuth(parseRunnerSlotSetOptions(args));
1059
1059
  const result = await withCliSpinner(`Assigning runner slot ${options.runnerId}/${options.slotIndex} to queue ${options.queueId}`, () => putRunnerSlot(options));
1060
1060
  console.log(JSON.stringify(result, null, 2));
1061
1061
  return 0;
@@ -1066,13 +1066,7 @@ async function setRunnerSlot(args) {
1066
1066
  }
1067
1067
  }
1068
1068
  async function postQueueCreate(options) {
1069
- const headers = {
1070
- "Content-Type": "application/json",
1071
- "X-API-Key": options.apiKey,
1072
- };
1073
- if (options.projectId) {
1074
- headers["X-Tapi-Project"] = options.projectId;
1075
- }
1069
+ const headers = sdkJsonHeaders(options.authToken, options.projectId);
1076
1070
  const response = await fetch(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/queues`, {
1077
1071
  method: "POST",
1078
1072
  headers,
@@ -1091,11 +1085,7 @@ async function postQueueCreate(options) {
1091
1085
  async function postTappQueueAdd(options) {
1092
1086
  const response = await fetch(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/tapps/${encodeURIComponent(options.tappId)}/queues`, {
1093
1087
  method: "POST",
1094
- headers: {
1095
- "Content-Type": "application/json",
1096
- "X-API-Key": options.apiKey,
1097
- "X-Tapi-Project": options.tappId,
1098
- },
1088
+ headers: sdkJsonHeaders(options.authToken, options.tappId),
1099
1089
  body: JSON.stringify({ queueId: options.queueId }),
1100
1090
  });
1101
1091
  const body = await readJsonBody(response);
@@ -1108,11 +1098,7 @@ async function postTappQueueAdd(options) {
1108
1098
  async function putRunnerSlot(options) {
1109
1099
  const response = await fetch(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/runners/${encodeURIComponent(options.runnerId)}/slots/${encodeURIComponent(String(options.slotIndex))}`, {
1110
1100
  method: "PUT",
1111
- headers: {
1112
- "Content-Type": "application/json",
1113
- "X-API-Key": options.apiKey,
1114
- "X-Tapi-Project": options.projectId,
1115
- },
1101
+ headers: sdkJsonHeaders(options.authToken, options.projectId),
1116
1102
  body: JSON.stringify({ queueId: options.queueId }),
1117
1103
  });
1118
1104
  const body = await readJsonBody(response);
@@ -1125,7 +1111,6 @@ async function putRunnerSlot(options) {
1125
1111
  export function parseRunnerSetupOptions(args) {
1126
1112
  const workspace = loadWorkspace();
1127
1113
  let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
1128
- let apiKey = envString("TAPI_API_KEY") || "";
1129
1114
  let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
1130
1115
  let runnerId = envString("TAPI_RUNNER_ID") || "";
1131
1116
  let port = Number(envString("TAPI_RUNNER_SETUP_PORT") || "17687");
@@ -1146,14 +1131,6 @@ export function parseRunnerSetupOptions(args) {
1146
1131
  apiBaseUrl = arg.slice("--server=".length);
1147
1132
  continue;
1148
1133
  }
1149
- if (arg === "--api-key") {
1150
- apiKey = requireOptionValue(args, ++index, "--api-key");
1151
- continue;
1152
- }
1153
- if (arg.startsWith("--api-key=")) {
1154
- apiKey = arg.slice("--api-key=".length);
1155
- continue;
1156
- }
1157
1134
  if (arg === "--project") {
1158
1135
  projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
1159
1136
  continue;
@@ -1191,9 +1168,6 @@ export function parseRunnerSetupOptions(args) {
1191
1168
  }
1192
1169
  throw new Error(`Unexpected runner setup argument: ${arg}`);
1193
1170
  }
1194
- if (!apiKey) {
1195
- throw new Error("runner setup requires --api-key or TAPI_API_KEY.");
1196
- }
1197
1171
  if (!projectId) {
1198
1172
  throw new Error("runner setup requires --project or TAPI_PROJECT_ID.");
1199
1173
  }
@@ -1205,7 +1179,6 @@ export function parseRunnerSetupOptions(args) {
1205
1179
  }
1206
1180
  return {
1207
1181
  apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
1208
- apiKey,
1209
1182
  projectId,
1210
1183
  runnerId,
1211
1184
  port,
@@ -1214,7 +1187,7 @@ export function parseRunnerSetupOptions(args) {
1214
1187
  }
1215
1188
  async function runRunnerSetup(args) {
1216
1189
  try {
1217
- const options = parseRunnerSetupOptions(args);
1190
+ const options = await withSdkAuth(parseRunnerSetupOptions(args));
1218
1191
  const handle = await startRunnerSetupServer(options);
1219
1192
  if (options.openBrowser) {
1220
1193
  openUrlInBrowser(handle.url);
@@ -1252,7 +1225,7 @@ export async function executeRunnerSetupAction(options, action, fetchImpl = fetc
1252
1225
  if (!queueId) {
1253
1226
  queue = await sdkJsonRequest(fetchImpl, `${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/queues`, {
1254
1227
  method: "POST",
1255
- headers: sdkJsonHeaders(options.apiKey, options.projectId),
1228
+ headers: sdkJsonHeaders(options.authToken, options.projectId),
1256
1229
  body: JSON.stringify({
1257
1230
  displayName: String(action.displayName || `${options.projectId} queue`).trim(),
1258
1231
  maxSlots,
@@ -1265,7 +1238,7 @@ export async function executeRunnerSetupAction(options, action, fetchImpl = fetc
1265
1238
  }
1266
1239
  const attachment = await sdkJsonRequest(fetchImpl, `${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/tapps/${encodeURIComponent(options.projectId)}/queues`, {
1267
1240
  method: "POST",
1268
- headers: sdkJsonHeaders(options.apiKey, options.projectId),
1241
+ headers: sdkJsonHeaders(options.authToken, options.projectId),
1269
1242
  body: JSON.stringify({ queueId }),
1270
1243
  });
1271
1244
  const slots = [];
@@ -1273,7 +1246,7 @@ export async function executeRunnerSetupAction(options, action, fetchImpl = fetc
1273
1246
  const slotIndex = slotStart + offset;
1274
1247
  slots.push(await sdkJsonRequest(fetchImpl, `${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/runners/${encodeURIComponent(options.runnerId)}/slots/${encodeURIComponent(String(slotIndex))}`, {
1275
1248
  method: "PUT",
1276
- headers: sdkJsonHeaders(options.apiKey, options.projectId),
1249
+ headers: sdkJsonHeaders(options.authToken, options.projectId),
1277
1250
  body: JSON.stringify({ queueId }),
1278
1251
  }));
1279
1252
  }
@@ -1416,12 +1389,22 @@ async function sdkJsonRequest(fetchImpl, url, init) {
1416
1389
  }
1417
1390
  return body ?? {};
1418
1391
  }
1419
- function sdkJsonHeaders(apiKey, projectId) {
1420
- return {
1392
+ function sdkJsonHeaders(authToken, projectId) {
1393
+ const headers = {
1421
1394
  "Content-Type": "application/json",
1422
- "X-API-Key": apiKey,
1423
- "X-Tapi-Project": projectId,
1395
+ Authorization: `Bearer ${requireSdkAuthToken(authToken)}`,
1424
1396
  };
1397
+ if (projectId) {
1398
+ headers["X-Tapi-Project"] = projectId;
1399
+ }
1400
+ return headers;
1401
+ }
1402
+ function requireSdkAuthToken(authToken) {
1403
+ const normalized = String(authToken || "").trim();
1404
+ if (!normalized) {
1405
+ throw new Error("Tapi sign-in is required. Run `tapi login`, then retry.");
1406
+ }
1407
+ return normalized;
1425
1408
  }
1426
1409
  function coercePositiveInteger(value, fallback, label) {
1427
1410
  const text = value === undefined || value === null || value === "" ? String(fallback) : String(value);
@@ -1444,7 +1427,6 @@ function parseNonNegativeInteger(value, label) {
1444
1427
  function parseTriggerSyncOptions(args) {
1445
1428
  const workspace = loadWorkspace();
1446
1429
  let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
1447
- let apiKey = envString("TAPI_API_KEY") || "";
1448
1430
  let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
1449
1431
  let configPath = "";
1450
1432
  const root = workspace?.root || process.cwd();
@@ -1464,14 +1446,6 @@ function parseTriggerSyncOptions(args) {
1464
1446
  apiBaseUrl = arg.slice("--server=".length);
1465
1447
  continue;
1466
1448
  }
1467
- if (arg === "--api-key") {
1468
- apiKey = requireOptionValue(args, ++index, "--api-key");
1469
- continue;
1470
- }
1471
- if (arg.startsWith("--api-key=")) {
1472
- apiKey = arg.slice("--api-key=".length);
1473
- continue;
1474
- }
1475
1449
  if (arg === "--project") {
1476
1450
  projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
1477
1451
  continue;
@@ -1490,29 +1464,25 @@ function parseTriggerSyncOptions(args) {
1490
1464
  }
1491
1465
  throw new Error(`Unknown triggers option: ${arg}`);
1492
1466
  }
1493
- if (!apiKey) {
1494
- throw new Error("triggers sync requires --api-key or TAPI_API_KEY.");
1495
- }
1496
1467
  if (!projectId) {
1497
1468
  throw new Error("triggers sync requires --project, TAPI_PROJECT_ID, or .tapi/project.json.");
1498
1469
  }
1499
1470
  return {
1500
1471
  apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
1501
- apiKey,
1502
1472
  projectId,
1503
1473
  configPath: configPath ? resolve(root, configPath) : defaultTriggerConfigPath(root),
1504
1474
  };
1505
1475
  }
1506
1476
  async function syncServiceTriggers(args) {
1507
1477
  try {
1508
- const options = parseTriggerSyncOptions(args);
1478
+ const options = await withSdkAuth(parseTriggerSyncOptions(args));
1509
1479
  const triggers = await readServiceTriggerConfig(options.configPath);
1510
1480
  if (triggers.length === 0) {
1511
1481
  throw new Error(`No Tapi service triggers found in ${options.configPath}.`);
1512
1482
  }
1513
1483
  const client = new TapiClient({
1514
1484
  baseUrl: options.apiBaseUrl,
1515
- apiKey: options.apiKey,
1485
+ authToken: options.authToken,
1516
1486
  tappId: options.projectId,
1517
1487
  projectId: options.projectId,
1518
1488
  });
@@ -1532,7 +1502,6 @@ async function syncServiceTriggers(args) {
1532
1502
  function parseSessionsOptions(args) {
1533
1503
  const workspace = loadWorkspace();
1534
1504
  let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
1535
- let apiKey = envString("TAPI_API_KEY") || "";
1536
1505
  let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
1537
1506
  let site = "";
1538
1507
  let sessionId = "";
@@ -1554,14 +1523,6 @@ function parseSessionsOptions(args) {
1554
1523
  apiBaseUrl = arg.slice("--server=".length);
1555
1524
  continue;
1556
1525
  }
1557
- if (arg === "--api-key") {
1558
- apiKey = requireOptionValue(args, ++index, "--api-key");
1559
- continue;
1560
- }
1561
- if (arg.startsWith("--api-key=")) {
1562
- apiKey = arg.slice("--api-key=".length);
1563
- continue;
1564
- }
1565
1526
  if (arg === "--project") {
1566
1527
  projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
1567
1528
  continue;
@@ -1596,9 +1557,6 @@ function parseSessionsOptions(args) {
1596
1557
  }
1597
1558
  throw new Error(`Unexpected sessions argument: ${arg}`);
1598
1559
  }
1599
- if (!apiKey) {
1600
- throw new Error("sessions command requires --api-key or TAPI_API_KEY.");
1601
- }
1602
1560
  if (!projectId) {
1603
1561
  throw new Error("sessions command requires --project, TAPI_PROJECT_ID, or .tapi/project.json.");
1604
1562
  }
@@ -1606,7 +1564,6 @@ function parseSessionsOptions(args) {
1606
1564
  sessionId,
1607
1565
  options: {
1608
1566
  apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
1609
- apiKey,
1610
1567
  projectId,
1611
1568
  site: site || undefined,
1612
1569
  json,
@@ -1617,18 +1574,19 @@ function parseSessionsOptions(args) {
1617
1574
  async function listDevSessions(args) {
1618
1575
  try {
1619
1576
  const { options } = parseSessionsOptions(args);
1620
- const sessions = await fetchDevSessions(options);
1621
- if (options.json) {
1577
+ const authenticated = await withSdkAuth(options);
1578
+ const sessions = await fetchDevSessions(authenticated);
1579
+ if (authenticated.json) {
1622
1580
  console.log(JSON.stringify(sessions, null, 2));
1623
1581
  return 0;
1624
1582
  }
1625
1583
  const rows = flattenDevSessions(sessions);
1626
1584
  if (rows.length === 0) {
1627
- console.log(`No Tapi dev sessions found for project ${sessions.projectId || options.projectId}.`);
1585
+ console.log(`No Tapi dev sessions found for project ${sessions.projectId || authenticated.projectId}.`);
1628
1586
  return 0;
1629
1587
  }
1630
- if (!options.noInteractive && process.stdin.isTTY && process.stdout.isTTY && rows.some((session) => session.openable)) {
1631
- return await runSessionsPicker(sessions, options);
1588
+ if (!authenticated.noInteractive && process.stdin.isTTY && process.stdout.isTTY && rows.some((session) => session.openable)) {
1589
+ return await runSessionsPicker(sessions, authenticated);
1632
1590
  }
1633
1591
  console.log(renderDevSessions(sessions));
1634
1592
  const openable = rows.find((session) => session.openable);
@@ -1645,15 +1603,16 @@ async function listDevSessions(args) {
1645
1603
  async function openDevSession(args) {
1646
1604
  try {
1647
1605
  const { sessionId, options } = parseSessionsOptions(args);
1606
+ const authenticated = await withSdkAuth(options);
1648
1607
  if (!sessionId) {
1649
1608
  throw new Error("sessions open requires a session id.");
1650
1609
  }
1651
- const sessions = await fetchDevSessions(options);
1610
+ const sessions = await fetchDevSessions(authenticated);
1652
1611
  const session = findDevSession(sessions, sessionId);
1653
1612
  if (!session) {
1654
1613
  throw new Error(`Tapi session not found: ${sessionId}`);
1655
1614
  }
1656
- return await openDevSessionInStudio(session, options);
1615
+ return await openDevSessionInStudio(session, authenticated);
1657
1616
  }
1658
1617
  catch (error) {
1659
1618
  console.error(formatError(error));
@@ -1663,7 +1622,7 @@ async function openDevSession(args) {
1663
1622
  async function fetchDevSessions(options) {
1664
1623
  const client = new TapiClient({
1665
1624
  baseUrl: options.apiBaseUrl,
1666
- apiKey: options.apiKey,
1625
+ authToken: options.authToken,
1667
1626
  tappId: options.projectId,
1668
1627
  projectId: options.projectId,
1669
1628
  });
@@ -2103,7 +2062,7 @@ function rejectNonPolicyTriggerRuntimeOwner(runtime, fieldName) {
2103
2062
  async function fetchApiCatalog(options) {
2104
2063
  const client = new TapiClient({
2105
2064
  baseUrl: options.apiBaseUrl,
2106
- apiKey: options.apiKey,
2065
+ authToken: options.authToken,
2107
2066
  tappId: options.projectId,
2108
2067
  projectId: options.projectId,
2109
2068
  });
@@ -2425,7 +2384,7 @@ async function installService(options) {
2425
2384
  destination: zipPath,
2426
2385
  expectedSha256: manifest.sha256,
2427
2386
  });
2428
- 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");
2429
2388
  await event.phase("download.verified", {
2430
2389
  zipPath,
2431
2390
  expectedSha256: manifest.sha256,
@@ -2880,7 +2839,7 @@ async function installStudio(options) {
2880
2839
  destination: installerPath,
2881
2840
  expectedSha256: manifest.sha256,
2882
2841
  });
2883
- await withCliSpinner(`Downloading Tapi Studio ${manifest.version}`, () => downloadAndVerify(manifest.url, installerPath, manifest.sha256));
2842
+ await downloadAndVerify(manifest.url, installerPath, manifest.sha256);
2884
2843
  await event.phase("download.verified", {
2885
2844
  installerPath,
2886
2845
  expectedSha256: manifest.sha256,
@@ -3033,23 +2992,24 @@ async function openStudio(options) {
3033
2992
  if (options.downloadOnly) {
3034
2993
  throw new Error("--download-only is only supported with `tapi studio install`.");
3035
2994
  }
3036
- await ensureServiceReadyForStudio(options);
2995
+ const launchOptions = await resolveStudioTappSelection(options);
2996
+ await ensureServiceReadyForStudio(launchOptions);
3037
2997
  await closeExistingPortableStudioServersForFreshLaunch();
3038
- if (!options.exePath) {
3039
- const portable = await ensurePortableStudioServer(options);
2998
+ if (!launchOptions.exePath) {
2999
+ const portable = await ensurePortableStudioServer(launchOptions);
3040
3000
  if (portable) {
3041
- await launchPortableStudioServer(portable.exePath, options, portable.manifest);
3001
+ await launchPortableStudioServer(portable.exePath, launchOptions, portable.manifest);
3042
3002
  return 0;
3043
3003
  }
3044
3004
  }
3045
- let exePath = findStudioExecutable(options);
3046
- if (!exePath && !options.exePath) {
3005
+ let exePath = findStudioExecutable(launchOptions);
3006
+ if (!exePath && !launchOptions.exePath) {
3047
3007
  console.log("Tapi Studio executable was not found. Installing Tapi Studio now...");
3048
- const installCode = await installStudio(options);
3008
+ const installCode = await installStudio(launchOptions);
3049
3009
  if (installCode !== 0) {
3050
3010
  return installCode;
3051
3011
  }
3052
- exePath = findStudioExecutable(options);
3012
+ exePath = findStudioExecutable(launchOptions);
3053
3013
  }
3054
3014
  if (!exePath || !existsSync(exePath)) {
3055
3015
  console.error("Tapi Studio executable was not found.");
@@ -3058,7 +3018,7 @@ async function openStudio(options) {
3058
3018
  }
3059
3019
  const child = spawn(exePath, [], {
3060
3020
  detached: true,
3061
- env: buildStudioLaunchEnv(options),
3021
+ env: buildStudioLaunchEnv(launchOptions),
3062
3022
  stdio: "ignore",
3063
3023
  windowsHide: false,
3064
3024
  });
@@ -3066,6 +3026,177 @@ async function openStudio(options) {
3066
3026
  console.log(`Opened Tapi Studio: ${exePath}`);
3067
3027
  return 0;
3068
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
+ }
3069
3200
  function findStudioExecutable(options) {
3070
3201
  if (options.exePath) {
3071
3202
  return existsSync(options.exePath) ? options.exePath : undefined;
@@ -3136,7 +3267,7 @@ async function installPortableStudioServer(options, manifest, event) {
3136
3267
  destination: artifactPath,
3137
3268
  expectedSha256: manifest.sha256,
3138
3269
  });
3139
- 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");
3140
3271
  await event.phase("download.verified", {
3141
3272
  artifactPath,
3142
3273
  expectedSha256: manifest.sha256,
@@ -3198,6 +3329,7 @@ async function launchPortableStudioServer(exePath, options, manifest) {
3198
3329
  pid: childPid || undefined,
3199
3330
  apiBaseUrl: options.apiBaseUrl,
3200
3331
  projectId: options.projectId,
3332
+ tappId: options.projectId,
3201
3333
  workspaceRoot: options.workspaceRoot,
3202
3334
  workspaceConfig: options.workspace?.configPath,
3203
3335
  manifestVersion: manifest.version,
@@ -3503,6 +3635,8 @@ async function downloadFile(url, destination, label = "download", options = {})
3503
3635
  let destinationStream;
3504
3636
  let receivedBytes = 0;
3505
3637
  let nextProgressLogBytes = DOWNLOAD_PROGRESS_LOG_BYTES;
3638
+ let progressBar;
3639
+ let expectedBytes = 0;
3506
3640
  const failIfStalled = (phase) => {
3507
3641
  if (stallTimer) {
3508
3642
  clearTimeout(stallTimer);
@@ -3516,7 +3650,6 @@ async function downloadFile(url, destination, label = "download", options = {})
3516
3650
  }, stallTimeoutMs);
3517
3651
  };
3518
3652
  try {
3519
- console.log(`Downloading ${label}...`);
3520
3653
  failIfStalled("connecting");
3521
3654
  const response = await fetch(url, { signal: controller.signal });
3522
3655
  if (!response.ok) {
@@ -3526,12 +3659,29 @@ async function downloadFile(url, destination, label = "download", options = {})
3526
3659
  throw new Error(`${label} response did not include a body.`);
3527
3660
  }
3528
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
+ }
3529
3673
  source = Readable.fromWeb(response.body);
3530
3674
  progressStream = new Transform({
3531
3675
  transform(chunk, _encoding, callback) {
3532
3676
  receivedBytes += downloadChunkByteLength(chunk);
3533
3677
  failIfStalled("receiving data");
3534
- 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) {
3535
3685
  console.log(`Downloaded ${label}: ${formatBytes(receivedBytes)}...`);
3536
3686
  while (receivedBytes >= nextProgressLogBytes) {
3537
3687
  nextProgressLogBytes += DOWNLOAD_PROGRESS_LOG_BYTES;
@@ -3542,9 +3692,23 @@ async function downloadFile(url, destination, label = "download", options = {})
3542
3692
  });
3543
3693
  destinationStream = createWriteStream(destination);
3544
3694
  await pipeline(source, progressStream, destinationStream);
3545
- 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
+ }
3546
3706
  }
3547
3707
  catch (error) {
3708
+ if (progressBar) {
3709
+ progressBar.stop();
3710
+ progressBar = undefined;
3711
+ }
3548
3712
  if (stallError) {
3549
3713
  throw stallError;
3550
3714
  }
@@ -3800,6 +3964,7 @@ async function readStudioInstanceRecord(options) {
3800
3964
  pid: typeof parsed.pid === "number" && Number.isFinite(parsed.pid) ? parsed.pid : undefined,
3801
3965
  apiBaseUrl: typeof parsed.apiBaseUrl === "string" ? parsed.apiBaseUrl : "",
3802
3966
  projectId: typeof parsed.projectId === "string" ? parsed.projectId : undefined,
3967
+ tappId: typeof parsed.tappId === "string" ? parsed.tappId : undefined,
3803
3968
  workspaceRoot: typeof parsed.workspaceRoot === "string" ? parsed.workspaceRoot : undefined,
3804
3969
  workspaceConfig: typeof parsed.workspaceConfig === "string" ? parsed.workspaceConfig : undefined,
3805
3970
  manifestVersion: typeof parsed.manifestVersion === "string" ? parsed.manifestVersion : undefined,
@@ -3925,6 +4090,31 @@ async function readJsonBody(response) {
3925
4090
  return null;
3926
4091
  }
3927
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
+ }
3928
4118
  async function readRequestJson(request) {
3929
4119
  let body = "";
3930
4120
  for await (const chunk of request) {
@@ -4382,6 +4572,9 @@ function installEventOptions(options) {
4382
4572
  downloadOnly: options.downloadOnly,
4383
4573
  silent: options.silent,
4384
4574
  exePath: options.exePath,
4575
+ projectId: options.projectId,
4576
+ projectIdSource: options.projectIdSource,
4577
+ tappSelectionMode: options.tappSelectionMode,
4385
4578
  installTokenProvided: Boolean(options.installToken),
4386
4579
  installTokenSource: options.installTokenSource,
4387
4580
  };
@@ -4508,11 +4701,14 @@ function readSdkVersion() {
4508
4701
  function printHelp() {
4509
4702
  console.log(`Tapi CLI
4510
4703
 
4511
- Usage:
4512
- tapi init --project PROJECT
4704
+ Usage:
4705
+ tapi login
4706
+ tapi init --project PROJECT
4513
4707
  tapi link --project PROJECT
4514
- tapi studio install [--channel pilot] [--api-base-url URL]
4515
- tapi studio
4708
+ tapi studio install [--channel pilot] [--api-base-url URL]
4709
+ tapi studio
4710
+ tapi studio --tapp <tapp>
4711
+ tapi studio --select
4516
4712
  tapi studio open
4517
4713
  tapi studio doctor
4518
4714
  tapi tapp create <tapp>
@@ -4528,12 +4724,13 @@ Usage:
4528
4724
  tapi service status
4529
4725
  tapi doctor
4530
4726
 
4531
- Commands:
4532
- init Create .tapi/project.json for this repo
4727
+ Commands:
4728
+ login Sign in with Firebase credentials for SDK commands
4729
+ init Create .tapi/project.json for this repo
4533
4730
  link Rebind this repo to an existing Tapi project
4534
4731
  studio install Download, verify, and run the Tapi Studio installer
4535
- studio Open Tapi Studio for this repo
4536
- 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
4537
4734
  studio doctor Check local SDK and Studio release configuration
4538
4735
  tapp create Create a Tapp
4539
4736
  queue create Create a queue for service-run routing
@@ -4551,18 +4748,26 @@ Commands:
4551
4748
  doctor Alias for studio doctor
4552
4749
  `);
4553
4750
  }
4751
+ function printLoginHelp() {
4752
+ console.log(`Tapi login
4753
+
4754
+ Usage:
4755
+ tapi login
4756
+
4757
+ Signs in with the browser and caches Firebase credentials for SDK commands.
4758
+ `);
4759
+ }
4554
4760
  function printTappHelp() {
4555
4761
  console.log(`Tapi Tapp commands
4556
4762
 
4557
4763
  Usage:
4558
- tapi tapp create <tapp> [--name NAME] [--api-base-url URL] [--api-key KEY]
4559
- tapi tapp service add <tapp> <service.call> --service-map ID --entry ENTRY [--api-base-url URL] [--api-key KEY]
4560
- tapi tapp queue add <tapp> <queue-id> [--api-base-url URL] [--api-key KEY]
4764
+ tapi tapp create <tapp> [--name NAME] [--api-base-url URL]
4765
+ tapi tapp service add <tapp> <service.call> --service-map ID --entry ENTRY [--api-base-url URL]
4766
+ tapi tapp queue add <tapp> <queue-id> [--api-base-url URL]
4561
4767
 
4562
4768
  Options:
4563
4769
  --api-base-url <url> Tapi API base URL
4564
4770
  --server <url> Alias for --api-base-url
4565
- --api-key <key> Tapi API key; defaults to TAPI_API_KEY
4566
4771
  --tapp-id <id> Explicit Tapp id for tapp create
4567
4772
  --name <name> Display name for tapp create
4568
4773
  --service-map <id> Backing ServiceMap id
@@ -4574,12 +4779,11 @@ function printQueueHelp() {
4574
4779
  console.log(`Tapi queue commands
4575
4780
 
4576
4781
  Usage:
4577
- tapi queue create [display-name] [--max-slots N] [--project TAPP] [--api-base-url URL] [--api-key KEY]
4782
+ tapi queue create [display-name] [--max-slots N] [--project TAPP] [--api-base-url URL]
4578
4783
 
4579
4784
  Options:
4580
4785
  --api-base-url <url> Tapi API base URL
4581
4786
  --server <url> Alias for --api-base-url
4582
- --api-key <key> Tapi API key; defaults to TAPI_API_KEY
4583
4787
  --project <id> Optional project scope for the queue request
4584
4788
  --max-slots <n> Maximum runner slots this queue may use
4585
4789
  `);
@@ -4588,13 +4792,12 @@ function printRunnerHelp() {
4588
4792
  console.log(`Tapi runner commands
4589
4793
 
4590
4794
  Usage:
4591
- tapi runner setup --runner-id RUNNER --project TAPP [--port PORT] [--no-open] [--api-base-url URL] [--api-key KEY]
4592
- tapi runner slot set <runner-id> <slot-index> <queue-id> [--project TAPP] [--api-base-url URL] [--api-key KEY]
4795
+ tapi runner setup --runner-id RUNNER --project TAPP [--port PORT] [--no-open] [--api-base-url URL]
4796
+ tapi runner slot set <runner-id> <slot-index> <queue-id> [--project TAPP] [--api-base-url URL]
4593
4797
 
4594
4798
  Options:
4595
4799
  --api-base-url <url> Tapi API base URL
4596
4800
  --server <url> Alias for --api-base-url
4597
- --api-key <key> Tapi API key; defaults to TAPI_API_KEY
4598
4801
  --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
4599
4802
  --runner-id <id> Runner id for setup; defaults to TAPI_RUNNER_ID
4600
4803
  --port <n> Local setup UI port; defaults to 17687
@@ -4604,16 +4807,15 @@ Options:
4604
4807
  function printSessionsHelp() {
4605
4808
  console.log(`Tapi dev session commands
4606
4809
 
4607
- Usage:
4608
- tapi sessions [--api-base-url URL] [--api-key KEY] [--project PROJECT]
4609
- tapi sessions list [--json] [--site SITE]
4610
- tapi sessions open <session-id> [--api-base-url URL] [--api-key KEY] [--project PROJECT]
4611
-
4612
- Options:
4613
- --api-base-url <url> Tapi API base URL
4614
- --server <url> Alias for --api-base-url
4615
- --api-key <key> Tapi API key; defaults to TAPI_API_KEY
4616
- --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
4810
+ Usage:
4811
+ tapi sessions [--api-base-url URL] [--project PROJECT]
4812
+ tapi sessions list [--json] [--site SITE]
4813
+ tapi sessions open <session-id> [--api-base-url URL] [--project PROJECT]
4814
+
4815
+ Options:
4816
+ --api-base-url <url> Tapi API base URL
4817
+ --server <url> Alias for --api-base-url
4818
+ --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
4617
4819
  --site <name> Limit sessions to one sitemap/site
4618
4820
  --json Print raw grouped session JSON
4619
4821
  --no-interactive Print the grouped list without the arrow-key picker
@@ -4622,30 +4824,28 @@ Options:
4622
4824
  function printServicesHelp() {
4623
4825
  console.log(`Tapi ServiceMap service-run commands
4624
4826
 
4625
- Usage:
4626
- tapi services describe <servicemap.run> [--api-base-url URL] [--api-key KEY] [--project PROJECT]
4627
- tapi services sync [--api-base-url URL] [--api-key KEY] [--project PROJECT]
4628
-
4629
- Options:
4630
- --api-base-url <url> Tapi API base URL
4631
- --server <url> Alias for --api-base-url
4632
- --api-key <key> Tapi API key; defaults to TAPI_API_KEY
4633
- --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
4827
+ Usage:
4828
+ tapi services describe <servicemap.run> [--api-base-url URL] [--project PROJECT]
4829
+ tapi services sync [--api-base-url URL] [--project PROJECT]
4830
+
4831
+ Options:
4832
+ --api-base-url <url> Tapi API base URL
4833
+ --server <url> Alias for --api-base-url
4834
+ --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
4634
4835
  --catalog <path> Catalog JSON output path
4635
4836
  `);
4636
4837
  }
4637
4838
  function printTriggersHelp() {
4638
4839
  console.log(`Tapi service trigger commands
4639
4840
 
4640
- Usage:
4641
- tapi triggers sync [--config FILE] [--api-base-url URL] [--api-key KEY] [--project PROJECT]
4642
-
4643
- Options:
4644
- --config <path> Trigger config path; defaults to tapi.config.ts/js/json in the workspace root
4645
- --api-base-url <url> Tapi API base URL
4646
- --server <url> Alias for --api-base-url
4647
- --api-key <key> Tapi API key; defaults to TAPI_API_KEY
4648
- --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
4841
+ Usage:
4842
+ tapi triggers sync [--config FILE] [--api-base-url URL] [--project PROJECT]
4843
+
4844
+ Options:
4845
+ --config <path> Trigger config path; defaults to tapi.config.ts/js/json in the workspace root
4846
+ --api-base-url <url> Tapi API base URL
4847
+ --server <url> Alias for --api-base-url
4848
+ --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
4649
4849
 
4650
4850
  Config:
4651
4851
  export default {
@@ -4669,14 +4869,18 @@ Usage:
4669
4869
  tapi studio open [options]
4670
4870
  tapi studio doctor [options]
4671
4871
 
4672
- Options:
4673
- --channel <name> Release channel: pilot, stable, or nightly
4674
- --api-base-url <url> Tapi API base URL for install authorization and protected downloads
4675
- --server <url> Alias for --api-base-url
4676
- --workspace <path> Search this directory for .tapi/project.json
4677
- --no-workspace Do not load .tapi/project.json
4678
- --project <id> Tapi project id for workspace-bound Studio
4679
- --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
4680
4884
  --install-token <tok> Preissued Studio install token (skips browser sign-in)
4681
4885
  --manifest <url> Exact release manifest URL for doctor only
4682
4886
  --cache-dir <path> Installer download cache directory