@tapi-dev/sdk 0.1.34 → 0.1.38

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
@@ -3,7 +3,7 @@ import { spawn } from "node:child_process";
3
3
  import { createServer } from "node:http";
4
4
  import { createServer as createNetServer } from "node:net";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
- import { createReadStream, createWriteStream, existsSync, readFileSync, readdirSync } from "node:fs";
6
+ import { createReadStream, createWriteStream, existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
7
7
  import { mkdir, readFile, readdir, rename, rm, stat, statfs, unlink, writeFile } from "node:fs/promises";
8
8
  import { homedir } from "node:os";
9
9
  import { basename, dirname, join, resolve } from "node:path";
@@ -21,7 +21,7 @@ const DEFAULT_FIREBASE_API_KEY = "AIzaSyCDZR8lWyVQcWYfFdNZa4vuL4IWEC0h6gE";
21
21
  const DEFAULT_CHANNEL = "pilot";
22
22
  const SUPPORTED_STUDIO_PLATFORM = "windows-x86_64";
23
23
  const DEFAULT_INSTALL_AUTH_TIMEOUT_MS = 120_000;
24
- const PORTABLE_STUDIO_SERVER_READY_TIMEOUT_MS = 30_000;
24
+ export const PORTABLE_STUDIO_SERVER_READY_TIMEOUT_MS = 120_000;
25
25
  const PORTABLE_STUDIO_SERVER_READY_INTERVAL_MS = 250;
26
26
  const DOWNLOAD_STALL_TIMEOUT_MS = 30_000;
27
27
  const DOWNLOAD_PROGRESS_LOG_BYTES = 50 * 1024 * 1024;
@@ -33,6 +33,10 @@ const WINDOWS_SERVICE_COMMAND_TIMEOUT_MS = 30_000;
33
33
  const MANAGED_RELEASE_MARKER = ".tapi-managed-release";
34
34
  const MIN_EXTRACTION_REQUIRED_BYTES = 1024 * 1024 * 1024;
35
35
  const EXTRACTION_REQUIRED_MULTIPLIER = 3;
36
+ const RETIRED_TRIGGER_TARGET_FIELD = "api" + "Request";
37
+ const RUNTIME_OWNER_FIELDS = ["owner"];
38
+ const RETIRED_RUNTIME_OWNER_ALIAS_FIELDS = ["runtimeOwner", "runtime_owner"];
39
+ const SERVICE_RUN_RUNTIME_OWNER = "service_run";
36
40
  class StudioInstallApprovalError extends Error {
37
41
  code;
38
42
  status;
@@ -113,6 +117,50 @@ async function withCliSpinner(message, work) {
113
117
  throw error;
114
118
  }
115
119
  }
120
+ async function withSdkAuth(options) {
121
+ if (options.authToken?.trim()) {
122
+ return { ...options, authToken: options.authToken.trim() };
123
+ }
124
+ const envAuthToken = envString("TAPI_FIREBASE_ID_TOKEN");
125
+ if (envAuthToken) {
126
+ return { ...options, authToken: envAuthToken };
127
+ }
128
+ const credentials = await requireSdkFirebaseCredentials();
129
+ return { ...options, authToken: credentials.idToken };
130
+ }
131
+ async function requireSdkFirebaseCredentials() {
132
+ const credentials = await refreshCachedFirebaseCredentials();
133
+ if (!credentials) {
134
+ throw new Error(`Tapi sign-in is required. Run \`tapi login\`, then retry.`);
135
+ }
136
+ await writeStudioAuthCache(credentials);
137
+ return credentials;
138
+ }
139
+ async function runLogin() {
140
+ try {
141
+ const credentials = await withCliSpinner("Signing in to Tapi", () => authenticateSdkUser());
142
+ console.log(`Signed in to Tapi as ${credentials.uid}.`);
143
+ return 0;
144
+ }
145
+ catch (error) {
146
+ console.error(formatError(error));
147
+ return 1;
148
+ }
149
+ }
150
+ async function authenticateSdkUser() {
151
+ const cached = await refreshCachedFirebaseCredentials();
152
+ if (cached) {
153
+ await writeStudioAuthCache(cached);
154
+ return cached;
155
+ }
156
+ const browserAuth = await browserGoogleSignIn();
157
+ if (!browserAuth.idToken) {
158
+ throw new Error(browserAuth.error || browserAuth.detail || "Browser sign-in did not return a Google ID token.");
159
+ }
160
+ const credentials = await exchangeGoogleToFirebase(browserAuth.idToken, browserAuth.accessToken);
161
+ await writeStudioAuthCache(credentials);
162
+ return credentials;
163
+ }
116
164
  export async function runCli(argv = process.argv.slice(2)) {
117
165
  const [command, subcommand, ...rest] = argv;
118
166
  if (!command || command === "help" || command === "--help" || command === "-h") {
@@ -123,6 +171,18 @@ export async function runCli(argv = process.argv.slice(2)) {
123
171
  console.log(sdkVersion);
124
172
  return 0;
125
173
  }
174
+ if (command === "login") {
175
+ if (hasHelpFlag([subcommand, ...rest])) {
176
+ printLoginHelp();
177
+ return 0;
178
+ }
179
+ if (subcommand) {
180
+ console.error(`Unexpected login argument: ${subcommand}`);
181
+ printLoginHelp();
182
+ return 1;
183
+ }
184
+ return runLogin();
185
+ }
126
186
  if (command === "doctor") {
127
187
  if (hasHelpFlag([subcommand, ...rest])) {
128
188
  printStudioHelp();
@@ -144,8 +204,12 @@ export async function runCli(argv = process.argv.slice(2)) {
144
204
  }
145
205
  return runServiceCommand(subcommand, rest);
146
206
  }
147
- if (command === "services" || command === "apis") {
148
- if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h" || hasHelpFlag(rest)) {
207
+ if (command === "services") {
208
+ if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
209
+ printServicesHelp();
210
+ return 0;
211
+ }
212
+ if (hasHelpFlag(rest)) {
149
213
  printServicesHelp();
150
214
  return 0;
151
215
  }
@@ -155,20 +219,71 @@ export async function runCli(argv = process.argv.slice(2)) {
155
219
  if (subcommand === "sync") {
156
220
  return syncServiceCatalog(rest);
157
221
  }
158
- if (subcommand === "generate") {
159
- return generateServiceClient(rest);
160
- }
161
222
  console.error(`Unknown service-run command: ${subcommand}`);
162
223
  printServicesHelp();
163
224
  return 1;
164
225
  }
226
+ if (command === "tapp") {
227
+ if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h" || hasHelpFlag(rest)) {
228
+ printTappHelp();
229
+ return 0;
230
+ }
231
+ if (subcommand === "create") {
232
+ return createTapp(rest);
233
+ }
234
+ if (subcommand === "service") {
235
+ const serviceCommand = rest[0] || "";
236
+ if (serviceCommand === "add") {
237
+ return addTappService(rest.slice(1));
238
+ }
239
+ }
240
+ if (subcommand === "queue") {
241
+ const queueCommand = rest[0] || "";
242
+ if (queueCommand === "add") {
243
+ return addTappQueue(rest.slice(1));
244
+ }
245
+ }
246
+ console.error(`Unknown Tapp command: ${[subcommand, ...rest].filter(Boolean).join(" ")}`);
247
+ printTappHelp();
248
+ return 1;
249
+ }
250
+ if (command === "queue") {
251
+ if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h" || hasHelpFlag(rest)) {
252
+ printQueueHelp();
253
+ return 0;
254
+ }
255
+ if (subcommand === "create") {
256
+ return createQueue(rest);
257
+ }
258
+ console.error(`Unknown queue command: ${subcommand}`);
259
+ printQueueHelp();
260
+ return 1;
261
+ }
262
+ if (command === "runner") {
263
+ if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h" || hasHelpFlag(rest)) {
264
+ printRunnerHelp();
265
+ return 0;
266
+ }
267
+ if (subcommand === "setup") {
268
+ return runRunnerSetup(rest);
269
+ }
270
+ if (subcommand === "slot") {
271
+ const slotCommand = rest[0] || "";
272
+ if (slotCommand === "set") {
273
+ return setRunnerSlot(rest.slice(1));
274
+ }
275
+ }
276
+ console.error(`Unknown runner command: ${[subcommand, ...rest].filter(Boolean).join(" ")}`);
277
+ printRunnerHelp();
278
+ return 1;
279
+ }
165
280
  if (command === "triggers") {
166
281
  if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h" || hasHelpFlag(rest)) {
167
282
  printTriggersHelp();
168
283
  return 0;
169
284
  }
170
285
  if (subcommand === "sync") {
171
- return syncApiTriggers(rest);
286
+ return syncServiceTriggers(rest);
172
287
  }
173
288
  console.error(`Unknown triggers command: ${subcommand}`);
174
289
  printTriggersHelp();
@@ -363,16 +478,406 @@ export function parseStudioOptions(args) {
363
478
  workspace,
364
479
  workspaceRoot: workspace?.root,
365
480
  projectId,
366
- projectSlug,
367
- workspaceMode: Boolean(projectId || workspace),
481
+ projectSlug,
482
+ workspaceMode: Boolean(projectId || workspace),
483
+ };
484
+ }
485
+ function parseServiceOptions(args) {
486
+ let operation = "";
487
+ const workspace = loadWorkspace();
488
+ let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
489
+ let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
490
+ for (let index = 0; index < args.length; index += 1) {
491
+ const arg = args[index];
492
+ if (!arg)
493
+ continue;
494
+ if (arg === "--api-base-url" || arg === "--server") {
495
+ apiBaseUrl = requireOptionValue(args, ++index, arg);
496
+ continue;
497
+ }
498
+ if (arg.startsWith("--api-base-url=")) {
499
+ apiBaseUrl = arg.slice("--api-base-url=".length);
500
+ continue;
501
+ }
502
+ if (arg.startsWith("--server=")) {
503
+ apiBaseUrl = arg.slice("--server=".length);
504
+ continue;
505
+ }
506
+ if (arg === "--project") {
507
+ projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
508
+ continue;
509
+ }
510
+ if (arg.startsWith("--project=")) {
511
+ projectId = normalizeProjectValue(arg.slice("--project=".length), "project");
512
+ continue;
513
+ }
514
+ if (arg.startsWith("--")) {
515
+ throw new Error(`Unknown services option: ${arg}`);
516
+ }
517
+ if (!operation) {
518
+ operation = arg;
519
+ continue;
520
+ }
521
+ throw new Error(`Unexpected services argument: ${arg}`);
522
+ }
523
+ if (!operation) {
524
+ throw new Error("services describe requires a service run like schwab.place_order.");
525
+ }
526
+ if (!projectId) {
527
+ throw new Error("services describe requires --project, TAPI_PROJECT_ID, or .tapi/project.json.");
528
+ }
529
+ return {
530
+ operation,
531
+ options: {
532
+ apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
533
+ projectId,
534
+ },
535
+ };
536
+ }
537
+ async function describeServiceOperation(args) {
538
+ try {
539
+ const { operation, options } = parseServiceOptions(args);
540
+ const authenticated = await withSdkAuth(options);
541
+ const client = new TapiClient({
542
+ baseUrl: authenticated.apiBaseUrl,
543
+ authToken: authenticated.authToken,
544
+ tappId: authenticated.projectId,
545
+ projectId: authenticated.projectId,
546
+ });
547
+ const description = await withCliSpinner(`Fetching Tapi service-run contract for ${operation}`, () => client.services.describe(operation));
548
+ console.log(JSON.stringify(description, null, 2));
549
+ return 0;
550
+ }
551
+ catch (error) {
552
+ console.error(formatError(error));
553
+ return 1;
554
+ }
555
+ }
556
+ function parseApiWorkspaceOptions(args) {
557
+ const workspace = loadWorkspace();
558
+ let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
559
+ let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
560
+ let catalogPath = workspace?.config.services?.catalog || ".tapi/services/catalog.json";
561
+ for (let index = 0; index < args.length; index += 1) {
562
+ const arg = args[index];
563
+ if (!arg)
564
+ continue;
565
+ if (arg === "--api-base-url" || arg === "--server") {
566
+ apiBaseUrl = requireOptionValue(args, ++index, arg);
567
+ continue;
568
+ }
569
+ if (arg.startsWith("--api-base-url=")) {
570
+ apiBaseUrl = arg.slice("--api-base-url=".length);
571
+ continue;
572
+ }
573
+ if (arg.startsWith("--server=")) {
574
+ apiBaseUrl = arg.slice("--server=".length);
575
+ continue;
576
+ }
577
+ if (arg === "--project") {
578
+ projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
579
+ continue;
580
+ }
581
+ if (arg.startsWith("--project=")) {
582
+ projectId = normalizeProjectValue(arg.slice("--project=".length), "project");
583
+ continue;
584
+ }
585
+ if (arg === "--catalog") {
586
+ catalogPath = requireOptionValue(args, ++index, "--catalog");
587
+ continue;
588
+ }
589
+ if (arg.startsWith("--catalog=")) {
590
+ catalogPath = arg.slice("--catalog=".length);
591
+ continue;
592
+ }
593
+ throw new Error(`Unknown services option: ${arg}`);
594
+ }
595
+ if (!projectId) {
596
+ throw new Error("services command requires --project, TAPI_PROJECT_ID, or .tapi/project.json.");
597
+ }
598
+ const root = workspace?.root || process.cwd();
599
+ return {
600
+ apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
601
+ projectId,
602
+ catalogPath: resolve(root, catalogPath),
603
+ };
604
+ }
605
+ async function syncServiceCatalog(args) {
606
+ try {
607
+ const options = await withSdkAuth(parseApiWorkspaceOptions(args));
608
+ const catalog = await withCliSpinner("Fetching Tapi service-run catalog", () => fetchApiCatalog(options));
609
+ await writeJsonFile(options.catalogPath, catalog);
610
+ console.log(`Synced Tapi service-run catalog: ${options.catalogPath}`);
611
+ return 0;
612
+ }
613
+ catch (error) {
614
+ console.error(formatError(error));
615
+ return 1;
616
+ }
617
+ }
618
+ function parseTappServiceAddOptions(args) {
619
+ const workspace = loadWorkspace();
620
+ let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
621
+ let tappId = "";
622
+ let serviceCall = "";
623
+ let serviceMapId = "";
624
+ let entry = "";
625
+ let expectedRevision = "";
626
+ for (let index = 0; index < args.length; index += 1) {
627
+ const arg = args[index];
628
+ if (!arg)
629
+ continue;
630
+ if (arg === "--api-base-url" || arg === "--server") {
631
+ apiBaseUrl = requireOptionValue(args, ++index, arg);
632
+ continue;
633
+ }
634
+ if (arg.startsWith("--api-base-url=")) {
635
+ apiBaseUrl = arg.slice("--api-base-url=".length);
636
+ continue;
637
+ }
638
+ if (arg.startsWith("--server=")) {
639
+ apiBaseUrl = arg.slice("--server=".length);
640
+ continue;
641
+ }
642
+ if (arg === "--service-map") {
643
+ serviceMapId = requireOptionValue(args, ++index, "--service-map");
644
+ continue;
645
+ }
646
+ if (arg.startsWith("--service-map=")) {
647
+ serviceMapId = arg.slice("--service-map=".length);
648
+ continue;
649
+ }
650
+ if (arg === "--entry") {
651
+ entry = requireOptionValue(args, ++index, "--entry");
652
+ continue;
653
+ }
654
+ if (arg.startsWith("--entry=")) {
655
+ entry = arg.slice("--entry=".length);
656
+ continue;
657
+ }
658
+ if (arg === "--expected-revision") {
659
+ expectedRevision = requireOptionValue(args, ++index, "--expected-revision");
660
+ continue;
661
+ }
662
+ if (arg.startsWith("--expected-revision=")) {
663
+ expectedRevision = arg.slice("--expected-revision=".length);
664
+ continue;
665
+ }
666
+ if (arg.startsWith("--")) {
667
+ throw new Error(`Unknown tapp service add option: ${arg}`);
668
+ }
669
+ if (!tappId) {
670
+ tappId = normalizeProjectValue(arg, "tapp");
671
+ continue;
672
+ }
673
+ if (!serviceCall) {
674
+ serviceCall = arg.trim();
675
+ continue;
676
+ }
677
+ throw new Error(`Unexpected tapp service add argument: ${arg}`);
678
+ }
679
+ if (!tappId) {
680
+ throw new Error("tapp service add requires a Tapp id.");
681
+ }
682
+ if (!serviceCall || !serviceCall.includes(".")) {
683
+ throw new Error("tapp service add requires a service call like schwab.place_order.");
684
+ }
685
+ if (!serviceMapId) {
686
+ throw new Error("tapp service add requires --service-map.");
687
+ }
688
+ if (!entry) {
689
+ throw new Error("tapp service add requires --entry.");
690
+ }
691
+ return {
692
+ apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
693
+ tappId,
694
+ serviceCall,
695
+ serviceMapId,
696
+ entry,
697
+ expectedRevision,
698
+ };
699
+ }
700
+ function parseTappCreateOptions(args) {
701
+ const workspace = loadWorkspace();
702
+ let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
703
+ let tappId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
704
+ let name = "";
705
+ for (let index = 0; index < args.length; index += 1) {
706
+ const arg = args[index];
707
+ if (!arg)
708
+ continue;
709
+ if (arg === "--api-base-url" || arg === "--server") {
710
+ apiBaseUrl = requireOptionValue(args, ++index, arg);
711
+ continue;
712
+ }
713
+ if (arg.startsWith("--api-base-url=")) {
714
+ apiBaseUrl = arg.slice("--api-base-url=".length);
715
+ continue;
716
+ }
717
+ if (arg.startsWith("--server=")) {
718
+ apiBaseUrl = arg.slice("--server=".length);
719
+ continue;
720
+ }
721
+ if (arg === "--tapp-id" || arg === "--project") {
722
+ tappId = normalizeProjectValue(requireOptionValue(args, ++index, arg), "tapp");
723
+ continue;
724
+ }
725
+ if (arg.startsWith("--tapp-id=")) {
726
+ tappId = normalizeProjectValue(arg.slice("--tapp-id=".length), "tapp");
727
+ continue;
728
+ }
729
+ if (arg.startsWith("--project=")) {
730
+ tappId = normalizeProjectValue(arg.slice("--project=".length), "tapp");
731
+ continue;
732
+ }
733
+ if (arg === "--name") {
734
+ name = requireOptionValue(args, ++index, "--name").trim();
735
+ continue;
736
+ }
737
+ if (arg.startsWith("--name=")) {
738
+ name = arg.slice("--name=".length).trim();
739
+ continue;
740
+ }
741
+ if (arg.startsWith("--")) {
742
+ throw new Error(`Unknown tapp create option: ${arg}`);
743
+ }
744
+ if (!name) {
745
+ name = arg.trim();
746
+ if (!tappId) {
747
+ tappId = normalizeProjectValue(name, "tapp");
748
+ }
749
+ continue;
750
+ }
751
+ throw new Error(`Unexpected tapp create argument: ${arg}`);
752
+ }
753
+ if (!tappId) {
754
+ throw new Error("tapp create requires a Tapp id or name.");
755
+ }
756
+ if (!name) {
757
+ name = tappId;
758
+ }
759
+ return {
760
+ apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
761
+ tappId,
762
+ name,
763
+ };
764
+ }
765
+ async function createTapp(args) {
766
+ try {
767
+ const options = await withSdkAuth(parseTappCreateOptions(args));
768
+ const tapp = await withCliSpinner(`Creating Tapi Tapp ${options.tappId}`, () => postTappCreate(options));
769
+ console.log(JSON.stringify(tapp, null, 2));
770
+ return 0;
771
+ }
772
+ catch (error) {
773
+ console.error(formatError(error));
774
+ return 1;
775
+ }
776
+ }
777
+ async function postTappCreate(options, fetchImpl = fetch) {
778
+ const response = await fetchImpl(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/tapps`, {
779
+ method: "POST",
780
+ headers: sdkJsonHeaders(options.authToken, options.tappId),
781
+ body: JSON.stringify({
782
+ tappId: options.tappId,
783
+ name: options.name,
784
+ }),
785
+ });
786
+ const body = await readJsonBody(response);
787
+ if (!response.ok) {
788
+ const detail = body?.detail ?? body?.error ?? body ?? `HTTP ${response.status}`;
789
+ throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail));
790
+ }
791
+ return body ?? {};
792
+ }
793
+ async function addTappService(args) {
794
+ try {
795
+ const options = await withSdkAuth(parseTappServiceAddOptions(args));
796
+ const operation = await withCliSpinner(`Adding Tapi service ${options.serviceCall} to ${options.tappId}`, () => postTappServiceAdd(options));
797
+ console.log(JSON.stringify(operation, null, 2));
798
+ return 0;
799
+ }
800
+ catch (error) {
801
+ console.error(formatError(error));
802
+ return 1;
803
+ }
804
+ }
805
+ async function postTappServiceAdd(options) {
806
+ const response = await fetch(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/tapps/${encodeURIComponent(options.tappId)}/services/${encodeURIComponent(options.serviceCall)}`, {
807
+ method: "POST",
808
+ headers: sdkJsonHeaders(options.authToken, options.tappId),
809
+ body: JSON.stringify({
810
+ serviceMapId: options.serviceMapId,
811
+ entry: options.entry,
812
+ ...(options.expectedRevision ? { expectedRevision: options.expectedRevision } : {}),
813
+ }),
814
+ });
815
+ const body = await readJsonBody(response);
816
+ if (!response.ok) {
817
+ const detail = body?.detail ?? body?.error ?? body ?? `HTTP ${response.status}`;
818
+ throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail));
819
+ }
820
+ return body ?? {};
821
+ }
822
+ function parseQueueCreateOptions(args) {
823
+ const workspace = loadWorkspace();
824
+ let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
825
+ let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
826
+ let displayName = "";
827
+ let maxSlots = 8;
828
+ for (let index = 0; index < args.length; index += 1) {
829
+ const arg = args[index];
830
+ if (!arg)
831
+ continue;
832
+ if (arg === "--api-base-url" || arg === "--server") {
833
+ apiBaseUrl = requireOptionValue(args, ++index, arg);
834
+ continue;
835
+ }
836
+ if (arg.startsWith("--api-base-url=")) {
837
+ apiBaseUrl = arg.slice("--api-base-url=".length);
838
+ continue;
839
+ }
840
+ if (arg.startsWith("--server=")) {
841
+ apiBaseUrl = arg.slice("--server=".length);
842
+ continue;
843
+ }
844
+ if (arg === "--project") {
845
+ projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
846
+ continue;
847
+ }
848
+ if (arg.startsWith("--project=")) {
849
+ projectId = normalizeProjectValue(arg.slice("--project=".length), "project");
850
+ continue;
851
+ }
852
+ if (arg === "--max-slots") {
853
+ maxSlots = parsePositiveInteger(requireOptionValue(args, ++index, "--max-slots"), "--max-slots");
854
+ continue;
855
+ }
856
+ if (arg.startsWith("--max-slots=")) {
857
+ maxSlots = parsePositiveInteger(arg.slice("--max-slots=".length), "--max-slots");
858
+ continue;
859
+ }
860
+ if (arg.startsWith("--")) {
861
+ throw new Error(`Unknown queue create option: ${arg}`);
862
+ }
863
+ if (!displayName) {
864
+ displayName = arg.trim();
865
+ continue;
866
+ }
867
+ throw new Error(`Unexpected queue create argument: ${arg}`);
868
+ }
869
+ return {
870
+ apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
871
+ projectId,
872
+ displayName,
873
+ maxSlots,
368
874
  };
369
875
  }
370
- function parseServiceOptions(args) {
371
- let operation = "";
876
+ function parseTappQueueAddOptions(args) {
372
877
  const workspace = loadWorkspace();
373
878
  let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
374
- let apiKey = envString("TAPI_API_KEY") || "";
375
- let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
879
+ let tappId = "";
880
+ let queueId = "";
376
881
  for (let index = 0; index < args.length; index += 1) {
377
882
  const arg = args[index];
378
883
  if (!arg)
@@ -389,12 +894,52 @@ function parseServiceOptions(args) {
389
894
  apiBaseUrl = arg.slice("--server=".length);
390
895
  continue;
391
896
  }
392
- if (arg === "--api-key") {
393
- apiKey = requireOptionValue(args, ++index, "--api-key");
897
+ if (arg.startsWith("--")) {
898
+ throw new Error(`Unknown tapp queue add option: ${arg}`);
899
+ }
900
+ if (!tappId) {
901
+ tappId = normalizeProjectValue(arg, "tapp");
902
+ continue;
903
+ }
904
+ if (!queueId) {
905
+ queueId = arg.trim();
906
+ continue;
907
+ }
908
+ throw new Error(`Unexpected tapp queue add argument: ${arg}`);
909
+ }
910
+ if (!tappId) {
911
+ throw new Error("tapp queue add requires a Tapp id.");
912
+ }
913
+ if (!queueId) {
914
+ throw new Error("tapp queue add requires a queue id.");
915
+ }
916
+ return {
917
+ apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
918
+ tappId,
919
+ queueId,
920
+ };
921
+ }
922
+ function parseRunnerSlotSetOptions(args) {
923
+ const workspace = loadWorkspace();
924
+ let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
925
+ let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
926
+ let runnerId = "";
927
+ let slotIndex = 0;
928
+ let queueId = "";
929
+ for (let index = 0; index < args.length; index += 1) {
930
+ const arg = args[index];
931
+ if (!arg)
932
+ continue;
933
+ if (arg === "--api-base-url" || arg === "--server") {
934
+ apiBaseUrl = requireOptionValue(args, ++index, arg);
935
+ continue;
936
+ }
937
+ if (arg.startsWith("--api-base-url=")) {
938
+ apiBaseUrl = arg.slice("--api-base-url=".length);
394
939
  continue;
395
940
  }
396
- if (arg.startsWith("--api-key=")) {
397
- apiKey = arg.slice("--api-key=".length);
941
+ if (arg.startsWith("--server=")) {
942
+ apiBaseUrl = arg.slice("--server=".length);
398
943
  continue;
399
944
  }
400
945
  if (arg === "--project") {
@@ -406,42 +951,47 @@ function parseServiceOptions(args) {
406
951
  continue;
407
952
  }
408
953
  if (arg.startsWith("--")) {
409
- throw new Error(`Unknown services option: ${arg}`);
954
+ throw new Error(`Unknown runner slot set option: ${arg}`);
410
955
  }
411
- if (!operation) {
412
- operation = arg;
956
+ if (!runnerId) {
957
+ runnerId = arg.trim();
413
958
  continue;
414
959
  }
415
- throw new Error(`Unexpected services argument: ${arg}`);
960
+ if (!slotIndex) {
961
+ slotIndex = parsePositiveInteger(arg.trim(), "slot index");
962
+ continue;
963
+ }
964
+ if (!queueId) {
965
+ queueId = arg.trim();
966
+ continue;
967
+ }
968
+ throw new Error(`Unexpected runner slot set argument: ${arg}`);
416
969
  }
417
- if (!operation) {
418
- throw new Error("services describe requires a service run like schwab.place_order.");
970
+ if (!projectId) {
971
+ throw new Error("runner slot set requires --project or TAPI_PROJECT_ID.");
419
972
  }
420
- if (!apiKey) {
421
- throw new Error("services describe requires --api-key or TAPI_API_KEY.");
973
+ if (!runnerId) {
974
+ throw new Error("runner slot set requires a runner id.");
422
975
  }
423
- if (!projectId) {
424
- throw new Error("services describe requires --project, TAPI_PROJECT_ID, or .tapi/project.json.");
976
+ if (!slotIndex) {
977
+ throw new Error("runner slot set requires a slot index.");
978
+ }
979
+ if (!queueId) {
980
+ throw new Error("runner slot set requires a queue id.");
425
981
  }
426
982
  return {
427
- operation,
428
- options: {
429
- apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
430
- apiKey,
431
- projectId,
432
- },
983
+ apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
984
+ projectId,
985
+ runnerId,
986
+ slotIndex,
987
+ queueId,
433
988
  };
434
989
  }
435
- async function describeServiceOperation(args) {
990
+ async function createQueue(args) {
436
991
  try {
437
- const { operation, options } = parseServiceOptions(args);
438
- const client = new TapiClient({
439
- baseUrl: options.apiBaseUrl,
440
- apiKey: options.apiKey,
441
- projectId: options.projectId,
442
- });
443
- const description = await withCliSpinner(`Fetching Tapi service-run contract for ${operation}`, () => client.services.describe(operation));
444
- console.log(JSON.stringify(description, null, 2));
992
+ const options = await withSdkAuth(parseQueueCreateOptions(args));
993
+ const queue = await withCliSpinner(`Creating Tapi queue`, () => postQueueCreate(options));
994
+ console.log(JSON.stringify(queue, null, 2));
445
995
  return 0;
446
996
  }
447
997
  catch (error) {
@@ -449,13 +999,80 @@ async function describeServiceOperation(args) {
449
999
  return 1;
450
1000
  }
451
1001
  }
452
- function parseApiWorkspaceOptions(args) {
1002
+ async function addTappQueue(args) {
1003
+ try {
1004
+ const options = await withSdkAuth(parseTappQueueAddOptions(args));
1005
+ const result = await withCliSpinner(`Attaching Tapi queue ${options.queueId} to ${options.tappId}`, () => postTappQueueAdd(options));
1006
+ console.log(JSON.stringify(result, null, 2));
1007
+ return 0;
1008
+ }
1009
+ catch (error) {
1010
+ console.error(formatError(error));
1011
+ return 1;
1012
+ }
1013
+ }
1014
+ async function setRunnerSlot(args) {
1015
+ try {
1016
+ const options = await withSdkAuth(parseRunnerSlotSetOptions(args));
1017
+ const result = await withCliSpinner(`Assigning runner slot ${options.runnerId}/${options.slotIndex} to queue ${options.queueId}`, () => putRunnerSlot(options));
1018
+ console.log(JSON.stringify(result, null, 2));
1019
+ return 0;
1020
+ }
1021
+ catch (error) {
1022
+ console.error(formatError(error));
1023
+ return 1;
1024
+ }
1025
+ }
1026
+ async function postQueueCreate(options) {
1027
+ const headers = sdkJsonHeaders(options.authToken, options.projectId);
1028
+ const response = await fetch(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/queues`, {
1029
+ method: "POST",
1030
+ headers,
1031
+ body: JSON.stringify({
1032
+ ...(options.displayName ? { displayName: options.displayName } : {}),
1033
+ maxSlots: options.maxSlots,
1034
+ }),
1035
+ });
1036
+ const body = await readJsonBody(response);
1037
+ if (!response.ok) {
1038
+ const detail = body?.detail ?? body?.error ?? body ?? `HTTP ${response.status}`;
1039
+ throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail));
1040
+ }
1041
+ return body ?? {};
1042
+ }
1043
+ async function postTappQueueAdd(options) {
1044
+ const response = await fetch(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/tapps/${encodeURIComponent(options.tappId)}/queues`, {
1045
+ method: "POST",
1046
+ headers: sdkJsonHeaders(options.authToken, options.tappId),
1047
+ body: JSON.stringify({ queueId: options.queueId }),
1048
+ });
1049
+ const body = await readJsonBody(response);
1050
+ if (!response.ok) {
1051
+ const detail = body?.detail ?? body?.error ?? body ?? `HTTP ${response.status}`;
1052
+ throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail));
1053
+ }
1054
+ return body ?? {};
1055
+ }
1056
+ async function putRunnerSlot(options) {
1057
+ const response = await fetch(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/runners/${encodeURIComponent(options.runnerId)}/slots/${encodeURIComponent(String(options.slotIndex))}`, {
1058
+ method: "PUT",
1059
+ headers: sdkJsonHeaders(options.authToken, options.projectId),
1060
+ body: JSON.stringify({ queueId: options.queueId }),
1061
+ });
1062
+ const body = await readJsonBody(response);
1063
+ if (!response.ok) {
1064
+ const detail = body?.detail ?? body?.error ?? body ?? `HTTP ${response.status}`;
1065
+ throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail));
1066
+ }
1067
+ return body ?? {};
1068
+ }
1069
+ export function parseRunnerSetupOptions(args) {
453
1070
  const workspace = loadWorkspace();
454
1071
  let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
455
- let apiKey = envString("TAPI_API_KEY") || "";
456
1072
  let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
457
- let catalogPath = workspace?.config.services?.catalog || workspace?.config.generated?.catalog || ".tapi/services/catalog.json";
458
- let typescriptPath = workspace?.config.services?.typescript || workspace?.config.generated?.typescript || "src/tapi.services.ts";
1073
+ let runnerId = envString("TAPI_RUNNER_ID") || "";
1074
+ let port = Number(envString("TAPI_RUNNER_SETUP_PORT") || "17687");
1075
+ let openBrowser = true;
459
1076
  for (let index = 0; index < args.length; index += 1) {
460
1077
  const arg = args[index];
461
1078
  if (!arg)
@@ -472,14 +1089,6 @@ function parseApiWorkspaceOptions(args) {
472
1089
  apiBaseUrl = arg.slice("--server=".length);
473
1090
  continue;
474
1091
  }
475
- if (arg === "--api-key") {
476
- apiKey = requireOptionValue(args, ++index, "--api-key");
477
- continue;
478
- }
479
- if (arg.startsWith("--api-key=")) {
480
- apiKey = arg.slice("--api-key=".length);
481
- continue;
482
- }
483
1092
  if (arg === "--project") {
484
1093
  projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
485
1094
  continue;
@@ -488,45 +1097,63 @@ function parseApiWorkspaceOptions(args) {
488
1097
  projectId = normalizeProjectValue(arg.slice("--project=".length), "project");
489
1098
  continue;
490
1099
  }
491
- if (arg === "--catalog") {
492
- catalogPath = requireOptionValue(args, ++index, "--catalog");
1100
+ if (arg === "--runner-id") {
1101
+ runnerId = requireOptionValue(args, ++index, "--runner-id").trim();
493
1102
  continue;
494
1103
  }
495
- if (arg.startsWith("--catalog=")) {
496
- catalogPath = arg.slice("--catalog=".length);
1104
+ if (arg.startsWith("--runner-id=")) {
1105
+ runnerId = arg.slice("--runner-id=".length).trim();
497
1106
  continue;
498
1107
  }
499
- if (arg === "--out") {
500
- typescriptPath = requireOptionValue(args, ++index, "--out");
1108
+ if (arg === "--port") {
1109
+ port = parseNonNegativeInteger(requireOptionValue(args, ++index, "--port"), "--port");
501
1110
  continue;
502
1111
  }
503
- if (arg.startsWith("--out=")) {
504
- typescriptPath = arg.slice("--out=".length);
1112
+ if (arg.startsWith("--port=")) {
1113
+ port = parseNonNegativeInteger(arg.slice("--port=".length), "--port");
505
1114
  continue;
506
1115
  }
507
- throw new Error(`Unknown services option: ${arg}`);
508
- }
509
- if (!apiKey) {
510
- throw new Error("services command requires --api-key or TAPI_API_KEY.");
1116
+ if (arg === "--no-open") {
1117
+ openBrowser = false;
1118
+ continue;
1119
+ }
1120
+ if (arg.startsWith("--")) {
1121
+ throw new Error(`Unknown runner setup option: ${arg}`);
1122
+ }
1123
+ if (!runnerId) {
1124
+ runnerId = arg.trim();
1125
+ continue;
1126
+ }
1127
+ throw new Error(`Unexpected runner setup argument: ${arg}`);
511
1128
  }
512
1129
  if (!projectId) {
513
- throw new Error("services command requires --project, TAPI_PROJECT_ID, or .tapi/project.json.");
1130
+ throw new Error("runner setup requires --project or TAPI_PROJECT_ID.");
1131
+ }
1132
+ if (!runnerId) {
1133
+ throw new Error("runner setup requires --runner-id or TAPI_RUNNER_ID.");
1134
+ }
1135
+ if (!Number.isInteger(port) || port < 0) {
1136
+ throw new Error("--port must be a non-negative integer.");
514
1137
  }
515
- const root = workspace?.root || process.cwd();
516
1138
  return {
517
1139
  apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
518
- apiKey,
519
1140
  projectId,
520
- catalogPath: resolve(root, catalogPath),
521
- typescriptPath: resolve(root, typescriptPath),
1141
+ runnerId,
1142
+ port,
1143
+ openBrowser,
522
1144
  };
523
1145
  }
524
- async function syncServiceCatalog(args) {
1146
+ async function runRunnerSetup(args) {
525
1147
  try {
526
- const options = parseApiWorkspaceOptions(args);
527
- const catalog = await withCliSpinner("Fetching Tapi service-run catalog", () => fetchApiCatalog(options));
528
- await writeJsonFile(options.catalogPath, catalog);
529
- console.log(`Synced Tapi service-run catalog: ${options.catalogPath}`);
1148
+ const options = await withSdkAuth(parseRunnerSetupOptions(args));
1149
+ const handle = await startRunnerSetupServer(options);
1150
+ if (options.openBrowser) {
1151
+ openUrlInBrowser(handle.url);
1152
+ }
1153
+ console.log(`Tapi runner setup: ${handle.url}`);
1154
+ console.log("Press Ctrl+C to stop the setup server.");
1155
+ await waitForProcessSignal();
1156
+ await handle.close();
530
1157
  return 0;
531
1158
  }
532
1159
  catch (error) {
@@ -534,24 +1161,230 @@ async function syncServiceCatalog(args) {
534
1161
  return 1;
535
1162
  }
536
1163
  }
537
- async function generateServiceClient(args) {
1164
+ export async function startRunnerSetupServer(options, fetchImpl = fetch) {
1165
+ const server = createServer((request, response) => {
1166
+ void handleRunnerSetupRequest(request, response, options, fetchImpl);
1167
+ });
1168
+ await listenOnLocalhost(server, options.port);
1169
+ const address = server.address();
1170
+ const port = typeof address === "object" && address ? address.port : options.port;
1171
+ return {
1172
+ url: `http://127.0.0.1:${port}/`,
1173
+ port,
1174
+ close: () => closeServer(server),
1175
+ };
1176
+ }
1177
+ export async function executeRunnerSetupAction(options, action, fetchImpl = fetch) {
1178
+ let queueId = String(action.queueId || "").trim();
1179
+ const maxSlots = coercePositiveInteger(action.maxSlots, 8, "maxSlots");
1180
+ const slotStart = coercePositiveInteger(action.slotStart, 1, "slotStart");
1181
+ const slotCount = coercePositiveInteger(action.slotCount, 1, "slotCount");
1182
+ let queue = {};
1183
+ if (!queueId) {
1184
+ queue = await sdkJsonRequest(fetchImpl, `${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/queues`, {
1185
+ method: "POST",
1186
+ headers: sdkJsonHeaders(options.authToken, options.projectId),
1187
+ body: JSON.stringify({
1188
+ displayName: String(action.displayName || `${options.projectId} queue`).trim(),
1189
+ maxSlots,
1190
+ }),
1191
+ });
1192
+ queueId = String(queue.queueId || queue.queue_id || "").trim();
1193
+ }
1194
+ if (!queueId) {
1195
+ throw new Error("Runner setup needs an existing queue id or a created queue response with queueId.");
1196
+ }
1197
+ const attachment = await sdkJsonRequest(fetchImpl, `${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/tapps/${encodeURIComponent(options.projectId)}/queues`, {
1198
+ method: "POST",
1199
+ headers: sdkJsonHeaders(options.authToken, options.projectId),
1200
+ body: JSON.stringify({ queueId }),
1201
+ });
1202
+ const slots = [];
1203
+ for (let offset = 0; offset < slotCount; offset += 1) {
1204
+ const slotIndex = slotStart + offset;
1205
+ slots.push(await sdkJsonRequest(fetchImpl, `${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/runners/${encodeURIComponent(options.runnerId)}/slots/${encodeURIComponent(String(slotIndex))}`, {
1206
+ method: "PUT",
1207
+ headers: sdkJsonHeaders(options.authToken, options.projectId),
1208
+ body: JSON.stringify({ queueId }),
1209
+ }));
1210
+ }
1211
+ return {
1212
+ queueId,
1213
+ queue: queueId && !Object.keys(queue).length ? { queueId } : queue,
1214
+ attachment,
1215
+ slots,
1216
+ };
1217
+ }
1218
+ async function handleRunnerSetupRequest(request, response, options, fetchImpl) {
538
1219
  try {
539
- const options = parseApiWorkspaceOptions(args);
540
- const catalog = await withCliSpinner("Fetching Tapi service-run catalog", () => fetchApiCatalog(options));
541
- await writeJsonFile(options.catalogPath, catalog);
542
- await writeTextFile(options.typescriptPath, renderServiceRunClient(catalog));
543
- console.log(`Wrote Tapi service-run client: ${options.typescriptPath}`);
544
- return 0;
1220
+ const url = new URL(request.url || "/", "http://127.0.0.1");
1221
+ if (request.method === "GET" && url.pathname === "/") {
1222
+ sendText(response, 200, runnerSetupHtml(options), "text/html; charset=utf-8");
1223
+ return;
1224
+ }
1225
+ if (request.method === "GET" && url.pathname === "/api/status") {
1226
+ sendJson(response, 200, {
1227
+ apiBaseUrl: options.apiBaseUrl,
1228
+ projectId: options.projectId,
1229
+ runnerId: options.runnerId,
1230
+ });
1231
+ return;
1232
+ }
1233
+ if (request.method === "POST" && url.pathname === "/api/setup") {
1234
+ const body = await readRequestJson(request);
1235
+ const result = await executeRunnerSetupAction(options, body, fetchImpl);
1236
+ sendJson(response, 200, result);
1237
+ return;
1238
+ }
1239
+ sendJson(response, 404, { error: "not found" });
545
1240
  }
546
1241
  catch (error) {
547
- console.error(formatError(error));
548
- return 1;
1242
+ sendJson(response, 400, { error: formatError(error) });
1243
+ }
1244
+ }
1245
+ function runnerSetupHtml(options) {
1246
+ const displayName = `${options.projectId} queue`;
1247
+ return `<!doctype html>
1248
+ <html lang="en">
1249
+ <head>
1250
+ <meta charset="utf-8">
1251
+ <meta name="viewport" content="width=device-width,initial-scale=1">
1252
+ <title>Tapi Runner Setup</title>
1253
+ <style>
1254
+ :root { color-scheme: light dark; --bg: #f7f7f5; --panel: #ffffff; --text: #1d2329; --muted: #62707c; --line: #d6dde2; --accent: #176b4d; --accent-2: #0f5ca8; --danger: #ad2e24; }
1255
+ @media (prefers-color-scheme: dark) { :root { --bg: #111418; --panel: #181d22; --text: #eef3f6; --muted: #a6b1bb; --line: #303a43; --accent: #59b88c; --accent-2: #6da8e8; --danger: #ee786e; } }
1256
+ * { box-sizing: border-box; }
1257
+ body { margin: 0; font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--text); background: var(--bg); }
1258
+ main { max-width: 920px; margin: 0 auto; padding: 28px 18px 36px; }
1259
+ h1 { margin: 0 0 18px; font-size: 28px; font-weight: 650; letter-spacing: 0; }
1260
+ h2 { margin: 0 0 12px; font-size: 16px; font-weight: 650; letter-spacing: 0; }
1261
+ .grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(260px, 320px); gap: 18px; align-items: start; }
1262
+ .panel, .summary { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 18px; }
1263
+ .row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
1264
+ label { display: grid; gap: 6px; margin: 0 0 12px; color: var(--muted); font-size: 12px; font-weight: 600; text-transform: uppercase; }
1265
+ input { width: 100%; min-width: 0; border: 1px solid var(--line); border-radius: 6px; padding: 10px 11px; color: var(--text); background: transparent; font: inherit; }
1266
+ button { border: 0; border-radius: 6px; padding: 11px 14px; color: white; background: var(--accent); font: inherit; font-weight: 650; cursor: pointer; }
1267
+ button:disabled { opacity: .55; cursor: progress; }
1268
+ code { word-break: break-all; }
1269
+ .summary dl { display: grid; grid-template-columns: 88px minmax(0, 1fr); gap: 8px 10px; margin: 0; }
1270
+ .summary dt { color: var(--muted); font-weight: 650; }
1271
+ .summary dd { margin: 0; min-width: 0; }
1272
+ .result { margin-top: 14px; min-height: 120px; border: 1px solid var(--line); border-radius: 8px; padding: 12px; overflow: auto; background: rgba(0,0,0,.035); white-space: pre-wrap; }
1273
+ .error { color: var(--danger); }
1274
+ .ok { color: var(--accent-2); }
1275
+ @media (max-width: 720px) { .grid, .row { grid-template-columns: 1fr; } main { padding: 20px 14px 28px; } h1 { font-size: 24px; } }
1276
+ </style>
1277
+ </head>
1278
+ <body>
1279
+ <main>
1280
+ <h1>Tapi Runner Setup</h1>
1281
+ <div class="grid">
1282
+ <section class="panel">
1283
+ <h2>Queue And Slots</h2>
1284
+ <form id="setup-form">
1285
+ <label>Existing Queue ID<input id="queueId" name="queueId" autocomplete="off" placeholder="queue_abc123"></label>
1286
+ <label>New Queue Name<input id="displayName" name="displayName" value="${escapeHtml(displayName)}" autocomplete="off"></label>
1287
+ <div class="row">
1288
+ <label>Queue Slot Limit<input id="maxSlots" name="maxSlots" type="number" min="1" step="1" value="8"></label>
1289
+ <label>First Runner Slot<input id="slotStart" name="slotStart" type="number" min="1" step="1" value="1"></label>
1290
+ </div>
1291
+ <label>Runner Slot Count<input id="slotCount" name="slotCount" type="number" min="1" step="1" value="1"></label>
1292
+ <button id="submit" type="submit">Apply Setup</button>
1293
+ </form>
1294
+ <pre id="result" class="result" aria-live="polite"></pre>
1295
+ </section>
1296
+ <aside class="summary">
1297
+ <h2>Target</h2>
1298
+ <dl>
1299
+ <dt>Server</dt><dd><code>${escapeHtml(options.apiBaseUrl)}</code></dd>
1300
+ <dt>Tapp</dt><dd><code>${escapeHtml(options.projectId)}</code></dd>
1301
+ <dt>Runner</dt><dd><code>${escapeHtml(options.runnerId)}</code></dd>
1302
+ </dl>
1303
+ </aside>
1304
+ </div>
1305
+ </main>
1306
+ <script>
1307
+ const form = document.getElementById("setup-form");
1308
+ const button = document.getElementById("submit");
1309
+ const result = document.getElementById("result");
1310
+ form.addEventListener("submit", async (event) => {
1311
+ event.preventDefault();
1312
+ button.disabled = true;
1313
+ result.className = "result";
1314
+ result.textContent = "Applying setup...";
1315
+ const body = Object.fromEntries(new FormData(form).entries());
1316
+ body.maxSlots = Number(body.maxSlots || 8);
1317
+ body.slotStart = Number(body.slotStart || 1);
1318
+ body.slotCount = Number(body.slotCount || 1);
1319
+ try {
1320
+ const response = await fetch("/api/setup", {
1321
+ method: "POST",
1322
+ headers: { "Content-Type": "application/json" },
1323
+ body: JSON.stringify(body),
1324
+ });
1325
+ const payload = await response.json();
1326
+ if (!response.ok) throw new Error(payload.error || JSON.stringify(payload));
1327
+ result.className = "result ok";
1328
+ result.textContent = JSON.stringify(payload, null, 2);
1329
+ document.getElementById("queueId").value = payload.queueId || body.queueId || "";
1330
+ } catch (error) {
1331
+ result.className = "result error";
1332
+ result.textContent = error instanceof Error ? error.message : String(error);
1333
+ } finally {
1334
+ button.disabled = false;
1335
+ }
1336
+ });
1337
+ </script>
1338
+ </body>
1339
+ </html>`;
1340
+ }
1341
+ async function sdkJsonRequest(fetchImpl, url, init) {
1342
+ const response = await fetchImpl(url, init);
1343
+ const body = await readJsonBody(response);
1344
+ if (!response.ok) {
1345
+ const detail = body?.detail ?? body?.error ?? body ?? `HTTP ${response.status}`;
1346
+ throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail));
1347
+ }
1348
+ return body ?? {};
1349
+ }
1350
+ function sdkJsonHeaders(authToken, projectId) {
1351
+ const headers = {
1352
+ "Content-Type": "application/json",
1353
+ Authorization: `Bearer ${requireSdkAuthToken(authToken)}`,
1354
+ };
1355
+ if (projectId) {
1356
+ headers["X-Tapi-Project"] = projectId;
1357
+ }
1358
+ return headers;
1359
+ }
1360
+ function requireSdkAuthToken(authToken) {
1361
+ const normalized = String(authToken || "").trim();
1362
+ if (!normalized) {
1363
+ throw new Error("Tapi sign-in is required. Run `tapi login`, then retry.");
1364
+ }
1365
+ return normalized;
1366
+ }
1367
+ function coercePositiveInteger(value, fallback, label) {
1368
+ const text = value === undefined || value === null || value === "" ? String(fallback) : String(value);
1369
+ return parsePositiveInteger(text, label);
1370
+ }
1371
+ function parsePositiveInteger(value, label) {
1372
+ const parsed = Number(value);
1373
+ if (!Number.isInteger(parsed) || parsed < 1) {
1374
+ throw new Error(`${label} must be a positive integer.`);
1375
+ }
1376
+ return parsed;
1377
+ }
1378
+ function parseNonNegativeInteger(value, label) {
1379
+ const parsed = Number(value);
1380
+ if (!Number.isInteger(parsed) || parsed < 0) {
1381
+ throw new Error(`${label} must be a non-negative integer.`);
549
1382
  }
1383
+ return parsed;
550
1384
  }
551
1385
  function parseTriggerSyncOptions(args) {
552
1386
  const workspace = loadWorkspace();
553
1387
  let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
554
- let apiKey = envString("TAPI_API_KEY") || "";
555
1388
  let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
556
1389
  let configPath = "";
557
1390
  const root = workspace?.root || process.cwd();
@@ -571,14 +1404,6 @@ function parseTriggerSyncOptions(args) {
571
1404
  apiBaseUrl = arg.slice("--server=".length);
572
1405
  continue;
573
1406
  }
574
- if (arg === "--api-key") {
575
- apiKey = requireOptionValue(args, ++index, "--api-key");
576
- continue;
577
- }
578
- if (arg.startsWith("--api-key=")) {
579
- apiKey = arg.slice("--api-key=".length);
580
- continue;
581
- }
582
1407
  if (arg === "--project") {
583
1408
  projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
584
1409
  continue;
@@ -597,29 +1422,26 @@ function parseTriggerSyncOptions(args) {
597
1422
  }
598
1423
  throw new Error(`Unknown triggers option: ${arg}`);
599
1424
  }
600
- if (!apiKey) {
601
- throw new Error("triggers sync requires --api-key or TAPI_API_KEY.");
602
- }
603
1425
  if (!projectId) {
604
1426
  throw new Error("triggers sync requires --project, TAPI_PROJECT_ID, or .tapi/project.json.");
605
1427
  }
606
1428
  return {
607
1429
  apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
608
- apiKey,
609
1430
  projectId,
610
1431
  configPath: configPath ? resolve(root, configPath) : defaultTriggerConfigPath(root),
611
1432
  };
612
1433
  }
613
- async function syncApiTriggers(args) {
1434
+ async function syncServiceTriggers(args) {
614
1435
  try {
615
- const options = parseTriggerSyncOptions(args);
616
- const triggers = await readApiTriggerConfig(options.configPath);
1436
+ const options = await withSdkAuth(parseTriggerSyncOptions(args));
1437
+ const triggers = await readServiceTriggerConfig(options.configPath);
617
1438
  if (triggers.length === 0) {
618
1439
  throw new Error(`No Tapi service triggers found in ${options.configPath}.`);
619
1440
  }
620
1441
  const client = new TapiClient({
621
1442
  baseUrl: options.apiBaseUrl,
622
- apiKey: options.apiKey,
1443
+ authToken: options.authToken,
1444
+ tappId: options.projectId,
623
1445
  projectId: options.projectId,
624
1446
  });
625
1447
  await withCliSpinner(`Syncing ${triggers.length} Tapi service trigger(s)`, async () => {
@@ -638,7 +1460,6 @@ async function syncApiTriggers(args) {
638
1460
  function parseSessionsOptions(args) {
639
1461
  const workspace = loadWorkspace();
640
1462
  let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
641
- let apiKey = envString("TAPI_API_KEY") || "";
642
1463
  let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
643
1464
  let site = "";
644
1465
  let sessionId = "";
@@ -660,14 +1481,6 @@ function parseSessionsOptions(args) {
660
1481
  apiBaseUrl = arg.slice("--server=".length);
661
1482
  continue;
662
1483
  }
663
- if (arg === "--api-key") {
664
- apiKey = requireOptionValue(args, ++index, "--api-key");
665
- continue;
666
- }
667
- if (arg.startsWith("--api-key=")) {
668
- apiKey = arg.slice("--api-key=".length);
669
- continue;
670
- }
671
1484
  if (arg === "--project") {
672
1485
  projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
673
1486
  continue;
@@ -702,9 +1515,6 @@ function parseSessionsOptions(args) {
702
1515
  }
703
1516
  throw new Error(`Unexpected sessions argument: ${arg}`);
704
1517
  }
705
- if (!apiKey) {
706
- throw new Error("sessions command requires --api-key or TAPI_API_KEY.");
707
- }
708
1518
  if (!projectId) {
709
1519
  throw new Error("sessions command requires --project, TAPI_PROJECT_ID, or .tapi/project.json.");
710
1520
  }
@@ -712,7 +1522,6 @@ function parseSessionsOptions(args) {
712
1522
  sessionId,
713
1523
  options: {
714
1524
  apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
715
- apiKey,
716
1525
  projectId,
717
1526
  site: site || undefined,
718
1527
  json,
@@ -723,18 +1532,19 @@ function parseSessionsOptions(args) {
723
1532
  async function listDevSessions(args) {
724
1533
  try {
725
1534
  const { options } = parseSessionsOptions(args);
726
- const sessions = await fetchDevSessions(options);
727
- if (options.json) {
1535
+ const authenticated = await withSdkAuth(options);
1536
+ const sessions = await fetchDevSessions(authenticated);
1537
+ if (authenticated.json) {
728
1538
  console.log(JSON.stringify(sessions, null, 2));
729
1539
  return 0;
730
1540
  }
731
1541
  const rows = flattenDevSessions(sessions);
732
1542
  if (rows.length === 0) {
733
- console.log(`No Tapi dev sessions found for project ${sessions.projectId || options.projectId}.`);
1543
+ console.log(`No Tapi dev sessions found for project ${sessions.projectId || authenticated.projectId}.`);
734
1544
  return 0;
735
1545
  }
736
- if (!options.noInteractive && process.stdin.isTTY && process.stdout.isTTY && rows.some((session) => session.openable)) {
737
- return await runSessionsPicker(sessions, options);
1546
+ if (!authenticated.noInteractive && process.stdin.isTTY && process.stdout.isTTY && rows.some((session) => session.openable)) {
1547
+ return await runSessionsPicker(sessions, authenticated);
738
1548
  }
739
1549
  console.log(renderDevSessions(sessions));
740
1550
  const openable = rows.find((session) => session.openable);
@@ -751,15 +1561,16 @@ async function listDevSessions(args) {
751
1561
  async function openDevSession(args) {
752
1562
  try {
753
1563
  const { sessionId, options } = parseSessionsOptions(args);
1564
+ const authenticated = await withSdkAuth(options);
754
1565
  if (!sessionId) {
755
1566
  throw new Error("sessions open requires a session id.");
756
1567
  }
757
- const sessions = await fetchDevSessions(options);
1568
+ const sessions = await fetchDevSessions(authenticated);
758
1569
  const session = findDevSession(sessions, sessionId);
759
1570
  if (!session) {
760
1571
  throw new Error(`Tapi session not found: ${sessionId}`);
761
1572
  }
762
- return await openDevSessionInStudio(session, options);
1573
+ return await openDevSessionInStudio(session, authenticated);
763
1574
  }
764
1575
  catch (error) {
765
1576
  console.error(formatError(error));
@@ -769,7 +1580,8 @@ async function openDevSession(args) {
769
1580
  async function fetchDevSessions(options) {
770
1581
  const client = new TapiClient({
771
1582
  baseUrl: options.apiBaseUrl,
772
- apiKey: options.apiKey,
1583
+ authToken: options.authToken,
1584
+ tappId: options.projectId,
773
1585
  projectId: options.projectId,
774
1586
  });
775
1587
  return await withCliSpinner("Fetching Tapi dev sessions", () => client.sessions.list({ site: options.site }));
@@ -897,7 +1709,6 @@ async function openDevSessionInStudio(session, options) {
897
1709
  function studioSessionLaunchParams(session) {
898
1710
  const params = new URLSearchParams();
899
1711
  params.set("runId", session.id);
900
- params.set("apiRunId", session.id);
901
1712
  if (session.sitemap)
902
1713
  params.set("sitemap", session.sitemap);
903
1714
  if (session.runtimeSessionId)
@@ -908,7 +1719,6 @@ function studioSessionSelectIntent(session) {
908
1719
  return {
909
1720
  sessionId: session.id,
910
1721
  runId: session.id,
911
- apiRunId: session.id,
912
1722
  ...(session.sitemap ? { sitemap: session.sitemap } : {}),
913
1723
  ...(session.runtimeSessionId ? { runtimeSessionId: session.runtimeSessionId } : {}),
914
1724
  source: "cli",
@@ -961,12 +1771,12 @@ function renderDevSessions(sessions, selectedId = "") {
961
1771
  const openable = session.openable ? "*" : " ";
962
1772
  const id = truncateText(session.id || "", 18).padEnd(18, " ");
963
1773
  const status = truncateText(String(session.status || ""), 24).padEnd(24, " ");
964
- const apiName = session.apiName && session.requestKey
965
- ? `${session.apiName}.${session.requestKey}`
966
- : session.apiName || session.requestKey || "unknown";
1774
+ const serviceRun = session.serviceRun || (session.serviceName && session.serviceKey
1775
+ ? `${session.serviceName}.${session.serviceKey}`
1776
+ : session.serviceName || session.serviceKey || "unknown");
967
1777
  const state = session.stateLabel ? ` state=${session.stateLabel}` : "";
968
1778
  const url = session.currentUrl ? ` ${truncateText(session.currentUrl, 60)}` : "";
969
- lines.push(`${selected}${openable} ${id} ${status} ${apiName}${state}${url}`);
1779
+ lines.push(`${selected}${openable} ${id} ${status} ${serviceRun}${state}${url}`);
970
1780
  }
971
1781
  }
972
1782
  lines.push("");
@@ -988,7 +1798,7 @@ function defaultTriggerConfigPath(root) {
988
1798
  }
989
1799
  return join(root, "tapi.config.json");
990
1800
  }
991
- async function readApiTriggerConfig(configPath) {
1801
+ async function readServiceTriggerConfig(configPath) {
992
1802
  if (!existsSync(configPath)) {
993
1803
  throw new Error(`Tapi trigger config not found at ${configPath}. Create tapi.config.json or pass --config.`);
994
1804
  }
@@ -1051,26 +1861,31 @@ function normalizeTriggerEntry(entry, index, configPath) {
1051
1861
  throw new Error(`Trigger #${index + 1} in ${configPath} must be an object.`);
1052
1862
  }
1053
1863
  const name = stringField(entry.name, `Trigger #${index + 1} name`);
1054
- const serviceRun = optionalStringField(entry.serviceRun, "serviceRun") ??
1055
- optionalStringField(entry.service, "service") ??
1056
- optionalStringField(entry.apiRequest, "apiRequest") ??
1057
- optionalStringField(entry.api, "api") ??
1058
- serviceRunFromParts(entry.serviceName ?? entry.apiName, entry.requestKey);
1864
+ if (RETIRED_TRIGGER_TARGET_FIELD in entry) {
1865
+ throw new Error(`Trigger '${name}' uses a retired trigger target field; use serviceRun.`);
1866
+ }
1867
+ const serviceRun = optionalStringField(entry.serviceRun, "serviceRun");
1059
1868
  if (!serviceRun) {
1060
- throw new Error(`Trigger '${name}' requires serviceRun, service, or serviceName/requestKey.`);
1869
+ throw new Error(`Trigger '${name}' requires serviceRun.`);
1061
1870
  }
1062
1871
  if (!serviceRun.includes(".")) {
1063
- throw new Error(`Trigger '${name}' serviceRun must be formatted as '<serviceName>.<requestKey>'.`);
1872
+ throw new Error(`Trigger '${name}' serviceRun must be formatted as '<serviceName>.<serviceKey>'.`);
1873
+ }
1874
+ if ("runnerId" in entry) {
1875
+ throw new Error(`Trigger '${name}' uses runnerId; use queueId.`);
1064
1876
  }
1065
1877
  const schedule = normalizeTriggerSchedule(entry.schedule, entry.interval, name);
1878
+ const runtime = entry.runtime === undefined ? undefined : recordField(entry.runtime, `Trigger '${name}' runtime`);
1879
+ rejectNonPolicyTriggerRuntimeOwner(runtime, `Trigger '${name}' runtime`);
1880
+ const queueId = stringField(entry.queueId, `Trigger '${name}' queueId`);
1066
1881
  const request = {
1067
1882
  name,
1068
- apiRequest: serviceRun,
1883
+ serviceRun,
1884
+ queueId,
1069
1885
  ...(entry.enabled === undefined ? {} : { enabled: booleanField(entry.enabled, `Trigger '${name}' enabled`) }),
1070
1886
  ...(schedule ? { schedule } : {}),
1071
1887
  ...(entry.inputs === undefined ? {} : { inputs: recordField(entry.inputs, `Trigger '${name}' inputs`) }),
1072
- ...(entry.runtime === undefined ? {} : { runtime: recordField(entry.runtime, `Trigger '${name}' runtime`) }),
1073
- ...(entry.runnerId === undefined ? {} : { runnerId: stringField(entry.runnerId, `Trigger '${name}' runnerId`) }),
1888
+ ...(runtime === undefined ? {} : { runtime }),
1074
1889
  ...(entry.priority === undefined ? {} : { priority: numberField(entry.priority, `Trigger '${name}' priority`) }),
1075
1890
  ...(entry.site === undefined ? {} : { site: stringField(entry.site, `Trigger '${name}' site`) }),
1076
1891
  };
@@ -1153,11 +1968,6 @@ function parseDurationSeconds(value, fieldName) {
1153
1968
  }
1154
1969
  return seconds;
1155
1970
  }
1156
- function serviceRunFromParts(serviceName, requestKey) {
1157
- const api = optionalStringField(serviceName, "serviceName");
1158
- const request = optionalStringField(requestKey, "requestKey");
1159
- return api && request ? `${api}.${request}` : undefined;
1160
- }
1161
1971
  function stringField(value, fieldName) {
1162
1972
  if (typeof value !== "string" || !value.trim()) {
1163
1973
  throw new Error(`${fieldName} must be a non-empty string.`);
@@ -1188,10 +1998,30 @@ function recordField(value, fieldName) {
1188
1998
  }
1189
1999
  return value;
1190
2000
  }
2001
+ function rejectNonPolicyTriggerRuntimeOwner(runtime, fieldName) {
2002
+ if (!runtime) {
2003
+ return;
2004
+ }
2005
+ for (const field of RETIRED_RUNTIME_OWNER_ALIAS_FIELDS) {
2006
+ if (field in runtime) {
2007
+ throw new Error(`unsupported service trigger runtime field: ${field}`);
2008
+ }
2009
+ }
2010
+ for (const field of RUNTIME_OWNER_FIELDS) {
2011
+ if (!(field in runtime)) {
2012
+ continue;
2013
+ }
2014
+ const owner = String(runtime[field] ?? "").trim().toLowerCase();
2015
+ if (owner && owner !== SERVICE_RUN_RUNTIME_OWNER) {
2016
+ throw new Error(`${fieldName} owner must be service_run.`);
2017
+ }
2018
+ }
2019
+ }
1191
2020
  async function fetchApiCatalog(options) {
1192
2021
  const client = new TapiClient({
1193
2022
  baseUrl: options.apiBaseUrl,
1194
- apiKey: options.apiKey,
2023
+ authToken: options.authToken,
2024
+ tappId: options.projectId,
1195
2025
  projectId: options.projectId,
1196
2026
  });
1197
2027
  return client.catalog.get();
@@ -1203,50 +2033,6 @@ async function writeTextFile(path, content) {
1203
2033
  await mkdir(dirname(path), { recursive: true });
1204
2034
  await writeFile(path, content, "utf8");
1205
2035
  }
1206
- function renderServiceRunClient(catalog) {
1207
- const namespaces = new Map();
1208
- for (const api of catalog.apis || []) {
1209
- const namespace = safeIdentifier(api.name || "api");
1210
- for (const request of api.requests || []) {
1211
- const key = String(request.operation || request.sdkName || request.key || "").trim();
1212
- if (!key)
1213
- continue;
1214
- const operationName = safeIdentifier(key);
1215
- const operation = `${api.name}.${key}`;
1216
- const items = namespaces.get(namespace) ?? [];
1217
- items.push({ key: operation, operationName });
1218
- namespaces.set(namespace, items);
1219
- }
1220
- }
1221
- const namespaceBlocks = [...namespaces.entries()].map(([namespace, operations]) => {
1222
- const lines = operations.map(({ key, operationName }) => ` ${operationName}: (inputs: Record<string, unknown> = {}, options: ServiceRunOptions = {}) => client.services.run(${JSON.stringify(key)}, { ...options, inputs }),`);
1223
- return ` ${namespace}: {\n${lines.join("\n")}\n },`;
1224
- });
1225
- return `/* Created by Tapi. Do not edit by hand. */
1226
- import { TapiClient, type TapiClientOptions, type RuntimeRunOptions } from "@tapi-dev/sdk";
1227
-
1228
- export interface ServiceRunOptions {
1229
- runtime?: RuntimeRunOptions;
1230
- priority?: number;
1231
- runnerId?: string;
1232
- idempotencyKey?: string;
1233
- site?: string;
1234
- }
1235
-
1236
- export function createTapiServicesClient(options: TapiClientOptions) {
1237
- const client = new TapiClient(options);
1238
- return {
1239
- ${namespaceBlocks.join("\n")}
1240
- };
1241
- }
1242
- `;
1243
- }
1244
- function safeIdentifier(value) {
1245
- const cleaned = String(value || "api")
1246
- .replace(/[^a-zA-Z0-9_$]+/g, "_")
1247
- .replace(/^([^a-zA-Z_$])/, "_$1");
1248
- return cleaned || "api";
1249
- }
1250
2036
  function isRecord(value) {
1251
2037
  return typeof value === "object" && value !== null && !Array.isArray(value);
1252
2038
  }
@@ -2994,6 +3780,56 @@ function openBrowser(url) {
2994
3780
  });
2995
3781
  child.unref();
2996
3782
  }
3783
+ function openUrlInBrowser(url) {
3784
+ if (process.platform === "win32") {
3785
+ openBrowser(url);
3786
+ return;
3787
+ }
3788
+ const command = process.platform === "darwin" ? "open" : "xdg-open";
3789
+ const child = spawn(command, [url], {
3790
+ detached: true,
3791
+ stdio: "ignore",
3792
+ windowsHide: true,
3793
+ });
3794
+ child.unref();
3795
+ }
3796
+ async function listenOnLocalhost(server, port) {
3797
+ await new Promise((resolvePromise, rejectPromise) => {
3798
+ const onError = (error) => {
3799
+ server.off("listening", onListening);
3800
+ rejectPromise(error);
3801
+ };
3802
+ const onListening = () => {
3803
+ server.off("error", onError);
3804
+ resolvePromise();
3805
+ };
3806
+ server.once("error", onError);
3807
+ server.once("listening", onListening);
3808
+ server.listen(port, "127.0.0.1");
3809
+ });
3810
+ }
3811
+ async function closeServer(server) {
3812
+ await new Promise((resolvePromise, rejectPromise) => {
3813
+ server.close((error) => {
3814
+ if (error) {
3815
+ rejectPromise(error);
3816
+ return;
3817
+ }
3818
+ resolvePromise();
3819
+ });
3820
+ });
3821
+ }
3822
+ async function waitForProcessSignal() {
3823
+ await new Promise((resolvePromise) => {
3824
+ const done = () => {
3825
+ process.off("SIGINT", done);
3826
+ process.off("SIGTERM", done);
3827
+ resolvePromise();
3828
+ };
3829
+ process.once("SIGINT", done);
3830
+ process.once("SIGTERM", done);
3831
+ });
3832
+ }
2997
3833
  async function readJsonBody(response) {
2998
3834
  const text = await response.text();
2999
3835
  if (!text.trim()) {
@@ -3006,6 +3842,40 @@ async function readJsonBody(response) {
3006
3842
  return null;
3007
3843
  }
3008
3844
  }
3845
+ async function readRequestJson(request) {
3846
+ let body = "";
3847
+ for await (const chunk of request) {
3848
+ body += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
3849
+ if (Buffer.byteLength(body, "utf8") > 64 * 1024) {
3850
+ throw new Error("request body is too large");
3851
+ }
3852
+ }
3853
+ if (!body.trim()) {
3854
+ return {};
3855
+ }
3856
+ const parsed = JSON.parse(body);
3857
+ if (!isRecord(parsed)) {
3858
+ throw new Error("request body must be a JSON object");
3859
+ }
3860
+ return parsed;
3861
+ }
3862
+ function sendText(response, status, text, contentType) {
3863
+ response.statusCode = status;
3864
+ response.setHeader("Content-Type", contentType);
3865
+ response.setHeader("Cache-Control", "no-store");
3866
+ response.end(text);
3867
+ }
3868
+ function sendJson(response, status, payload) {
3869
+ sendText(response, status, JSON.stringify(payload), "application/json; charset=utf-8");
3870
+ }
3871
+ function escapeHtml(value) {
3872
+ return value
3873
+ .replace(/&/g, "&amp;")
3874
+ .replace(/</g, "&lt;")
3875
+ .replace(/>/g, "&gt;")
3876
+ .replace(/"/g, "&quot;")
3877
+ .replace(/'/g, "&#39;");
3878
+ }
3009
3879
  function isApprovalTerminalError(error) {
3010
3880
  return error instanceof StudioInstallApprovalError
3011
3881
  && (error.code === "pending_approval" || error.code === "access_pending" || error.code === "access_rejected");
@@ -3555,52 +4425,119 @@ function readSdkVersion() {
3555
4425
  function printHelp() {
3556
4426
  console.log(`Tapi CLI
3557
4427
 
3558
- Usage:
3559
- tapi init --project PROJECT
4428
+ Usage:
4429
+ tapi login
4430
+ tapi init --project PROJECT
3560
4431
  tapi link --project PROJECT
3561
4432
  tapi studio install [--channel pilot] [--api-base-url URL]
3562
4433
  tapi studio
3563
- tapi studio open
3564
- tapi studio doctor
3565
- tapi services describe <servicemap.run>
4434
+ tapi studio open
4435
+ tapi studio doctor
4436
+ tapi tapp create <tapp>
4437
+ tapi queue create [display-name]
4438
+ tapi tapp service add <tapp> <service.call> --service-map ID --entry ENTRY
4439
+ tapi tapp queue add <tapp> <queue-id>
4440
+ tapi runner setup --runner-id RUNNER --project TAPP
4441
+ tapi runner slot set <runner-id> <slot-index> <queue-id>
4442
+ tapi services describe <servicemap.run>
3566
4443
  tapi services sync
3567
- tapi services generate
3568
4444
  tapi triggers sync
3569
4445
  tapi sessions
3570
4446
  tapi service status
3571
4447
  tapi doctor
3572
4448
 
3573
- Commands:
3574
- init Create .tapi/project.json for this repo
4449
+ Commands:
4450
+ login Sign in with Firebase credentials for SDK commands
4451
+ init Create .tapi/project.json for this repo
3575
4452
  link Rebind this repo to an existing Tapi project
3576
4453
  studio install Download, verify, and run the Tapi Studio installer
3577
- studio Open Tapi Studio for this repo
3578
- studio open Open Tapi Studio for this repo
3579
- studio doctor Check local SDK and Studio release configuration
3580
- services describe
4454
+ studio Open Tapi Studio for this repo
4455
+ studio open Open Tapi Studio for this repo
4456
+ studio doctor Check local SDK and Studio release configuration
4457
+ tapp create Create a Tapp
4458
+ queue create Create a queue for service-run routing
4459
+ tapp service add
4460
+ Add a ServiceMap-backed service call to a Tapp
4461
+ tapp queue add Attach a queue to a Tapp
4462
+ runner setup Open the local runner queue/slot setup UI
4463
+ runner slot set Assign a queue to a runner slot
4464
+ services describe
3581
4465
  Print a ServiceMap service-run input/output contract
3582
4466
  services sync Save the service-run catalog to .tapi/services
3583
- services generate
3584
- Write a TypeScript service-run wrapper from the catalog
3585
4467
  triggers sync Upsert service triggers from tapi.config
3586
4468
  sessions List dev-mode API sessions and open takeover sessions
3587
4469
  service Inspect or control the local Tapi Windows service
3588
4470
  doctor Alias for studio doctor
3589
4471
  `);
3590
4472
  }
4473
+ function printLoginHelp() {
4474
+ console.log(`Tapi login
4475
+
4476
+ Usage:
4477
+ tapi login
4478
+
4479
+ Signs in with the browser and caches Firebase credentials for SDK commands.
4480
+ `);
4481
+ }
4482
+ function printTappHelp() {
4483
+ console.log(`Tapi Tapp commands
4484
+
4485
+ Usage:
4486
+ tapi tapp create <tapp> [--name NAME] [--api-base-url URL]
4487
+ tapi tapp service add <tapp> <service.call> --service-map ID --entry ENTRY [--api-base-url URL]
4488
+ tapi tapp queue add <tapp> <queue-id> [--api-base-url URL]
4489
+
4490
+ Options:
4491
+ --api-base-url <url> Tapi API base URL
4492
+ --server <url> Alias for --api-base-url
4493
+ --tapp-id <id> Explicit Tapp id for tapp create
4494
+ --name <name> Display name for tapp create
4495
+ --service-map <id> Backing ServiceMap id
4496
+ --entry <entry> ServiceMap entry URL/name to launch
4497
+ --expected-revision <r> Optional ServiceMap revision guard
4498
+ `);
4499
+ }
4500
+ function printQueueHelp() {
4501
+ console.log(`Tapi queue commands
4502
+
4503
+ Usage:
4504
+ tapi queue create [display-name] [--max-slots N] [--project TAPP] [--api-base-url URL]
4505
+
4506
+ Options:
4507
+ --api-base-url <url> Tapi API base URL
4508
+ --server <url> Alias for --api-base-url
4509
+ --project <id> Optional project scope for the queue request
4510
+ --max-slots <n> Maximum runner slots this queue may use
4511
+ `);
4512
+ }
4513
+ function printRunnerHelp() {
4514
+ console.log(`Tapi runner commands
4515
+
4516
+ Usage:
4517
+ tapi runner setup --runner-id RUNNER --project TAPP [--port PORT] [--no-open] [--api-base-url URL]
4518
+ tapi runner slot set <runner-id> <slot-index> <queue-id> [--project TAPP] [--api-base-url URL]
4519
+
4520
+ Options:
4521
+ --api-base-url <url> Tapi API base URL
4522
+ --server <url> Alias for --api-base-url
4523
+ --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
4524
+ --runner-id <id> Runner id for setup; defaults to TAPI_RUNNER_ID
4525
+ --port <n> Local setup UI port; defaults to 17687
4526
+ --no-open Print the setup URL without opening a browser
4527
+ `);
4528
+ }
3591
4529
  function printSessionsHelp() {
3592
4530
  console.log(`Tapi dev session commands
3593
4531
 
3594
- Usage:
3595
- tapi sessions [--api-base-url URL] [--api-key KEY] [--project PROJECT]
3596
- tapi sessions list [--json] [--site SITE]
3597
- tapi sessions open <session-id> [--api-base-url URL] [--api-key KEY] [--project PROJECT]
3598
-
3599
- Options:
3600
- --api-base-url <url> Tapi API base URL
3601
- --server <url> Alias for --api-base-url
3602
- --api-key <key> Tapi API key; defaults to TAPI_API_KEY
3603
- --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
4532
+ Usage:
4533
+ tapi sessions [--api-base-url URL] [--project PROJECT]
4534
+ tapi sessions list [--json] [--site SITE]
4535
+ tapi sessions open <session-id> [--api-base-url URL] [--project PROJECT]
4536
+
4537
+ Options:
4538
+ --api-base-url <url> Tapi API base URL
4539
+ --server <url> Alias for --api-base-url
4540
+ --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
3604
4541
  --site <name> Limit sessions to one sitemap/site
3605
4542
  --json Print raw grouped session JSON
3606
4543
  --no-interactive Print the grouped list without the arrow-key picker
@@ -3609,32 +4546,28 @@ Options:
3609
4546
  function printServicesHelp() {
3610
4547
  console.log(`Tapi ServiceMap service-run commands
3611
4548
 
3612
- Usage:
3613
- tapi services describe <servicemap.run> [--api-base-url URL] [--api-key KEY] [--project PROJECT]
3614
- tapi services sync [--api-base-url URL] [--api-key KEY] [--project PROJECT]
3615
- tapi services generate [--api-base-url URL] [--api-key KEY] [--project PROJECT] [--out FILE]
3616
-
3617
- Options:
3618
- --api-base-url <url> Tapi API base URL
3619
- --server <url> Alias for --api-base-url
3620
- --api-key <key> Tapi API key; defaults to TAPI_API_KEY
3621
- --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
4549
+ Usage:
4550
+ tapi services describe <servicemap.run> [--api-base-url URL] [--project PROJECT]
4551
+ tapi services sync [--api-base-url URL] [--project PROJECT]
4552
+
4553
+ Options:
4554
+ --api-base-url <url> Tapi API base URL
4555
+ --server <url> Alias for --api-base-url
4556
+ --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
3622
4557
  --catalog <path> Catalog JSON output path
3623
- --out <path> TypeScript wrapper output path
3624
4558
  `);
3625
4559
  }
3626
4560
  function printTriggersHelp() {
3627
4561
  console.log(`Tapi service trigger commands
3628
4562
 
3629
- Usage:
3630
- tapi triggers sync [--config FILE] [--api-base-url URL] [--api-key KEY] [--project PROJECT]
3631
-
3632
- Options:
3633
- --config <path> Trigger config path; defaults to tapi.config.ts/js/json in the workspace root
3634
- --api-base-url <url> Tapi API base URL
3635
- --server <url> Alias for --api-base-url
3636
- --api-key <key> Tapi API key; defaults to TAPI_API_KEY
3637
- --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
4563
+ Usage:
4564
+ tapi triggers sync [--config FILE] [--api-base-url URL] [--project PROJECT]
4565
+
4566
+ Options:
4567
+ --config <path> Trigger config path; defaults to tapi.config.ts/js/json in the workspace root
4568
+ --api-base-url <url> Tapi API base URL
4569
+ --server <url> Alias for --api-base-url
4570
+ --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
3638
4571
 
3639
4572
  Config:
3640
4573
  export default {
@@ -3643,7 +4576,7 @@ Config:
3643
4576
  serviceRun: "schwab.get_balance",
3644
4577
  interval: "1h",
3645
4578
  inputs: { accountId: "main" },
3646
- runtime: { profileRef: "perm_default" },
4579
+ runtime: { owner: "service_run", profileRef: "perm_default" },
3647
4580
  },
3648
4581
  },
3649
4582
  };
@@ -3756,7 +4689,15 @@ function serializeError(error) {
3756
4689
  }
3757
4690
  function isCliEntrypoint() {
3758
4691
  const invokedPath = process.argv[1];
3759
- return Boolean(invokedPath && resolve(invokedPath) === fileURLToPath(import.meta.url));
4692
+ return Boolean(invokedPath && realCliPath(invokedPath) === realCliPath(fileURLToPath(import.meta.url)));
4693
+ }
4694
+ function realCliPath(path) {
4695
+ try {
4696
+ return realpathSync(path);
4697
+ }
4698
+ catch {
4699
+ return resolve(path);
4700
+ }
3760
4701
  }
3761
4702
  if (isCliEntrypoint()) {
3762
4703
  runCli().then((code) => {