@tapi-dev/sdk 0.1.34 → 0.1.36

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;
@@ -144,8 +148,12 @@ export async function runCli(argv = process.argv.slice(2)) {
144
148
  }
145
149
  return runServiceCommand(subcommand, rest);
146
150
  }
147
- if (command === "services" || command === "apis") {
148
- if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h" || hasHelpFlag(rest)) {
151
+ if (command === "services") {
152
+ if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
153
+ printServicesHelp();
154
+ return 0;
155
+ }
156
+ if (hasHelpFlag(rest)) {
149
157
  printServicesHelp();
150
158
  return 0;
151
159
  }
@@ -155,20 +163,71 @@ export async function runCli(argv = process.argv.slice(2)) {
155
163
  if (subcommand === "sync") {
156
164
  return syncServiceCatalog(rest);
157
165
  }
158
- if (subcommand === "generate") {
159
- return generateServiceClient(rest);
160
- }
161
166
  console.error(`Unknown service-run command: ${subcommand}`);
162
167
  printServicesHelp();
163
168
  return 1;
164
169
  }
170
+ if (command === "tapp") {
171
+ if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h" || hasHelpFlag(rest)) {
172
+ printTappHelp();
173
+ return 0;
174
+ }
175
+ if (subcommand === "create") {
176
+ return createTapp(rest);
177
+ }
178
+ if (subcommand === "service") {
179
+ const serviceCommand = rest[0] || "";
180
+ if (serviceCommand === "add") {
181
+ return addTappService(rest.slice(1));
182
+ }
183
+ }
184
+ if (subcommand === "queue") {
185
+ const queueCommand = rest[0] || "";
186
+ if (queueCommand === "add") {
187
+ return addTappQueue(rest.slice(1));
188
+ }
189
+ }
190
+ console.error(`Unknown Tapp command: ${[subcommand, ...rest].filter(Boolean).join(" ")}`);
191
+ printTappHelp();
192
+ return 1;
193
+ }
194
+ if (command === "queue") {
195
+ if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h" || hasHelpFlag(rest)) {
196
+ printQueueHelp();
197
+ return 0;
198
+ }
199
+ if (subcommand === "create") {
200
+ return createQueue(rest);
201
+ }
202
+ console.error(`Unknown queue command: ${subcommand}`);
203
+ printQueueHelp();
204
+ return 1;
205
+ }
206
+ if (command === "runner") {
207
+ if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h" || hasHelpFlag(rest)) {
208
+ printRunnerHelp();
209
+ return 0;
210
+ }
211
+ if (subcommand === "setup") {
212
+ return runRunnerSetup(rest);
213
+ }
214
+ if (subcommand === "slot") {
215
+ const slotCommand = rest[0] || "";
216
+ if (slotCommand === "set") {
217
+ return setRunnerSlot(rest.slice(1));
218
+ }
219
+ }
220
+ console.error(`Unknown runner command: ${[subcommand, ...rest].filter(Boolean).join(" ")}`);
221
+ printRunnerHelp();
222
+ return 1;
223
+ }
165
224
  if (command === "triggers") {
166
225
  if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h" || hasHelpFlag(rest)) {
167
226
  printTriggersHelp();
168
227
  return 0;
169
228
  }
170
229
  if (subcommand === "sync") {
171
- return syncApiTriggers(rest);
230
+ return syncServiceTriggers(rest);
172
231
  }
173
232
  console.error(`Unknown triggers command: ${subcommand}`);
174
233
  printTriggersHelp();
@@ -314,65 +373,589 @@ export function parseStudioOptions(args) {
314
373
  raw.exePath = resolve(requireOptionValue(args, ++index, "--exe"));
315
374
  continue;
316
375
  }
317
- if (arg.startsWith("--exe=")) {
318
- raw.exePath = resolve(arg.slice("--exe=".length));
376
+ if (arg.startsWith("--exe=")) {
377
+ raw.exePath = resolve(arg.slice("--exe=".length));
378
+ continue;
379
+ }
380
+ if (arg === "--download-only") {
381
+ raw.downloadOnly = true;
382
+ continue;
383
+ }
384
+ if (arg === "--silent") {
385
+ raw.silent = true;
386
+ continue;
387
+ }
388
+ if (arg === "--help" || arg === "-h") {
389
+ continue;
390
+ }
391
+ throw new Error(`Unknown option: ${arg}`);
392
+ }
393
+ const workspace = skipWorkspace ? undefined : loadWorkspace(workspaceSearchRoot ?? process.cwd());
394
+ const channel = raw.channel ?? parseChannel(envString("TAPI_STUDIO_CHANNEL") ?? DEFAULT_CHANNEL);
395
+ const apiBaseUrl = normalizeHttpUrl(raw.apiBaseUrl
396
+ ?? envString("TAPI_STUDIO_API_BASE_URL")
397
+ ?? envString("TAPI_BASE_URL")
398
+ ?? envString("TAPI_STUDIO_SERVER_URL")
399
+ ?? workspace?.apiBaseUrl
400
+ ?? DEFAULT_STUDIO_API_BASE_URL, "Studio API base URL");
401
+ const downloadsBaseUrl = normalizeHttpUrl(envString("TAPI_DOWNLOADS_BASE_URL") ?? DEFAULT_DOWNLOADS_BASE_URL, "TAPI_DOWNLOADS_BASE_URL");
402
+ const explicitManifestUrl = raw.manifestUrlOverride ?? envString("TAPI_STUDIO_MANIFEST_URL");
403
+ const manifestUrl = explicitManifestUrl
404
+ ? normalizeHttpUrl(explicitManifestUrl, "Studio manifest URL")
405
+ : `${downloadsBaseUrl.replace(/\/+$/, "")}/studio/channels/${channel}/latest.json`;
406
+ const projectId = raw.projectId ?? envString("TAPI_PROJECT_ID") ?? workspace?.projectId;
407
+ const projectSlug = raw.projectSlug ?? envString("TAPI_PROJECT_SLUG") ?? workspace?.projectSlug;
408
+ const envInstallToken = envString("TAPI_STUDIO_INSTALL_TOKEN");
409
+ const installToken = raw.installToken ?? envInstallToken;
410
+ return {
411
+ channel,
412
+ apiBaseUrl,
413
+ manifestUrl,
414
+ manifestUrlOverride: explicitManifestUrl ? normalizeHttpUrl(explicitManifestUrl, "Studio manifest URL") : undefined,
415
+ cacheDir: raw.cacheDir ?? getDefaultStudioCacheDir(),
416
+ installDir: raw.installDir ?? resolve(envString("TAPI_STUDIO_INSTALL_DIR") ?? getDefaultPortableStudioServerReleasesDir()),
417
+ downloadOnly: raw.downloadOnly ?? false,
418
+ silent: raw.silent ?? false,
419
+ exePath: raw.exePath ?? envString("TAPI_STUDIO_EXE"),
420
+ installToken,
421
+ installTokenSource: raw.installToken ? "option" : installToken ? "env" : undefined,
422
+ workspace,
423
+ workspaceRoot: workspace?.root,
424
+ projectId,
425
+ projectSlug,
426
+ workspaceMode: Boolean(projectId || workspace),
427
+ };
428
+ }
429
+ function parseServiceOptions(args) {
430
+ let operation = "";
431
+ const workspace = loadWorkspace();
432
+ let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
433
+ let apiKey = envString("TAPI_API_KEY") || "";
434
+ let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
435
+ for (let index = 0; index < args.length; index += 1) {
436
+ const arg = args[index];
437
+ if (!arg)
438
+ continue;
439
+ if (arg === "--api-base-url" || arg === "--server") {
440
+ apiBaseUrl = requireOptionValue(args, ++index, arg);
441
+ continue;
442
+ }
443
+ if (arg.startsWith("--api-base-url=")) {
444
+ apiBaseUrl = arg.slice("--api-base-url=".length);
445
+ continue;
446
+ }
447
+ if (arg.startsWith("--server=")) {
448
+ apiBaseUrl = arg.slice("--server=".length);
449
+ continue;
450
+ }
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
+ if (arg === "--project") {
460
+ projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
461
+ continue;
462
+ }
463
+ if (arg.startsWith("--project=")) {
464
+ projectId = normalizeProjectValue(arg.slice("--project=".length), "project");
465
+ continue;
466
+ }
467
+ if (arg.startsWith("--")) {
468
+ throw new Error(`Unknown services option: ${arg}`);
469
+ }
470
+ if (!operation) {
471
+ operation = arg;
472
+ continue;
473
+ }
474
+ throw new Error(`Unexpected services argument: ${arg}`);
475
+ }
476
+ if (!operation) {
477
+ throw new Error("services describe requires a service run like schwab.place_order.");
478
+ }
479
+ if (!apiKey) {
480
+ throw new Error("services describe requires --api-key or TAPI_API_KEY.");
481
+ }
482
+ if (!projectId) {
483
+ throw new Error("services describe requires --project, TAPI_PROJECT_ID, or .tapi/project.json.");
484
+ }
485
+ return {
486
+ operation,
487
+ options: {
488
+ apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
489
+ apiKey,
490
+ projectId,
491
+ },
492
+ };
493
+ }
494
+ async function describeServiceOperation(args) {
495
+ try {
496
+ const { operation, options } = parseServiceOptions(args);
497
+ const client = new TapiClient({
498
+ baseUrl: options.apiBaseUrl,
499
+ apiKey: options.apiKey,
500
+ tappId: options.projectId,
501
+ projectId: options.projectId,
502
+ });
503
+ const description = await withCliSpinner(`Fetching Tapi service-run contract for ${operation}`, () => client.services.describe(operation));
504
+ console.log(JSON.stringify(description, null, 2));
505
+ return 0;
506
+ }
507
+ catch (error) {
508
+ console.error(formatError(error));
509
+ return 1;
510
+ }
511
+ }
512
+ function parseApiWorkspaceOptions(args) {
513
+ const workspace = loadWorkspace();
514
+ let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
515
+ let apiKey = envString("TAPI_API_KEY") || "";
516
+ let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
517
+ let catalogPath = workspace?.config.services?.catalog || ".tapi/services/catalog.json";
518
+ for (let index = 0; index < args.length; index += 1) {
519
+ const arg = args[index];
520
+ if (!arg)
521
+ continue;
522
+ if (arg === "--api-base-url" || arg === "--server") {
523
+ apiBaseUrl = requireOptionValue(args, ++index, arg);
524
+ continue;
525
+ }
526
+ if (arg.startsWith("--api-base-url=")) {
527
+ apiBaseUrl = arg.slice("--api-base-url=".length);
528
+ continue;
529
+ }
530
+ if (arg.startsWith("--server=")) {
531
+ apiBaseUrl = arg.slice("--server=".length);
532
+ continue;
533
+ }
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
+ if (arg === "--project") {
543
+ projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
544
+ continue;
545
+ }
546
+ if (arg.startsWith("--project=")) {
547
+ projectId = normalizeProjectValue(arg.slice("--project=".length), "project");
548
+ continue;
549
+ }
550
+ if (arg === "--catalog") {
551
+ catalogPath = requireOptionValue(args, ++index, "--catalog");
552
+ continue;
553
+ }
554
+ if (arg.startsWith("--catalog=")) {
555
+ catalogPath = arg.slice("--catalog=".length);
556
+ continue;
557
+ }
558
+ throw new Error(`Unknown services option: ${arg}`);
559
+ }
560
+ if (!apiKey) {
561
+ throw new Error("services command requires --api-key or TAPI_API_KEY.");
562
+ }
563
+ if (!projectId) {
564
+ throw new Error("services command requires --project, TAPI_PROJECT_ID, or .tapi/project.json.");
565
+ }
566
+ const root = workspace?.root || process.cwd();
567
+ return {
568
+ apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
569
+ apiKey,
570
+ projectId,
571
+ catalogPath: resolve(root, catalogPath),
572
+ };
573
+ }
574
+ async function syncServiceCatalog(args) {
575
+ try {
576
+ const options = parseApiWorkspaceOptions(args);
577
+ const catalog = await withCliSpinner("Fetching Tapi service-run catalog", () => fetchApiCatalog(options));
578
+ await writeJsonFile(options.catalogPath, catalog);
579
+ console.log(`Synced Tapi service-run catalog: ${options.catalogPath}`);
580
+ return 0;
581
+ }
582
+ catch (error) {
583
+ console.error(formatError(error));
584
+ return 1;
585
+ }
586
+ }
587
+ function parseTappServiceAddOptions(args) {
588
+ const workspace = loadWorkspace();
589
+ let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
590
+ let apiKey = envString("TAPI_API_KEY") || "";
591
+ let tappId = "";
592
+ let serviceCall = "";
593
+ let serviceMapId = "";
594
+ let entry = "";
595
+ let expectedRevision = "";
596
+ for (let index = 0; index < args.length; index += 1) {
597
+ const arg = args[index];
598
+ if (!arg)
599
+ continue;
600
+ if (arg === "--api-base-url" || arg === "--server") {
601
+ apiBaseUrl = requireOptionValue(args, ++index, arg);
602
+ continue;
603
+ }
604
+ if (arg.startsWith("--api-base-url=")) {
605
+ apiBaseUrl = arg.slice("--api-base-url=".length);
606
+ continue;
607
+ }
608
+ if (arg.startsWith("--server=")) {
609
+ apiBaseUrl = arg.slice("--server=".length);
610
+ continue;
611
+ }
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
+ if (arg === "--service-map") {
621
+ serviceMapId = requireOptionValue(args, ++index, "--service-map");
622
+ continue;
623
+ }
624
+ if (arg.startsWith("--service-map=")) {
625
+ serviceMapId = arg.slice("--service-map=".length);
626
+ continue;
627
+ }
628
+ if (arg === "--entry") {
629
+ entry = requireOptionValue(args, ++index, "--entry");
630
+ continue;
631
+ }
632
+ if (arg.startsWith("--entry=")) {
633
+ entry = arg.slice("--entry=".length);
634
+ continue;
635
+ }
636
+ if (arg === "--expected-revision") {
637
+ expectedRevision = requireOptionValue(args, ++index, "--expected-revision");
638
+ continue;
639
+ }
640
+ if (arg.startsWith("--expected-revision=")) {
641
+ expectedRevision = arg.slice("--expected-revision=".length);
642
+ continue;
643
+ }
644
+ if (arg.startsWith("--")) {
645
+ throw new Error(`Unknown tapp service add option: ${arg}`);
646
+ }
647
+ if (!tappId) {
648
+ tappId = normalizeProjectValue(arg, "tapp");
649
+ continue;
650
+ }
651
+ if (!serviceCall) {
652
+ serviceCall = arg.trim();
653
+ continue;
654
+ }
655
+ throw new Error(`Unexpected tapp service add argument: ${arg}`);
656
+ }
657
+ if (!apiKey) {
658
+ throw new Error("tapp service add requires --api-key or TAPI_API_KEY.");
659
+ }
660
+ if (!tappId) {
661
+ throw new Error("tapp service add requires a Tapp id.");
662
+ }
663
+ if (!serviceCall || !serviceCall.includes(".")) {
664
+ throw new Error("tapp service add requires a service call like schwab.place_order.");
665
+ }
666
+ if (!serviceMapId) {
667
+ throw new Error("tapp service add requires --service-map.");
668
+ }
669
+ if (!entry) {
670
+ throw new Error("tapp service add requires --entry.");
671
+ }
672
+ return {
673
+ apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
674
+ apiKey,
675
+ tappId,
676
+ serviceCall,
677
+ serviceMapId,
678
+ entry,
679
+ expectedRevision,
680
+ };
681
+ }
682
+ function parseTappCreateOptions(args) {
683
+ const workspace = loadWorkspace();
684
+ let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
685
+ let apiKey = envString("TAPI_API_KEY") || "";
686
+ let tappId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
687
+ let name = "";
688
+ for (let index = 0; index < args.length; index += 1) {
689
+ const arg = args[index];
690
+ if (!arg)
691
+ continue;
692
+ if (arg === "--api-base-url" || arg === "--server") {
693
+ apiBaseUrl = requireOptionValue(args, ++index, arg);
694
+ continue;
695
+ }
696
+ if (arg.startsWith("--api-base-url=")) {
697
+ apiBaseUrl = arg.slice("--api-base-url=".length);
698
+ continue;
699
+ }
700
+ if (arg.startsWith("--server=")) {
701
+ apiBaseUrl = arg.slice("--server=".length);
702
+ continue;
703
+ }
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
+ if (arg === "--tapp-id" || arg === "--project") {
713
+ tappId = normalizeProjectValue(requireOptionValue(args, ++index, arg), "tapp");
714
+ continue;
715
+ }
716
+ if (arg.startsWith("--tapp-id=")) {
717
+ tappId = normalizeProjectValue(arg.slice("--tapp-id=".length), "tapp");
718
+ continue;
719
+ }
720
+ if (arg.startsWith("--project=")) {
721
+ tappId = normalizeProjectValue(arg.slice("--project=".length), "tapp");
722
+ continue;
723
+ }
724
+ if (arg === "--name") {
725
+ name = requireOptionValue(args, ++index, "--name").trim();
726
+ continue;
727
+ }
728
+ if (arg.startsWith("--name=")) {
729
+ name = arg.slice("--name=".length).trim();
730
+ continue;
731
+ }
732
+ if (arg.startsWith("--")) {
733
+ throw new Error(`Unknown tapp create option: ${arg}`);
734
+ }
735
+ if (!name) {
736
+ name = arg.trim();
737
+ if (!tappId) {
738
+ tappId = normalizeProjectValue(name, "tapp");
739
+ }
740
+ continue;
741
+ }
742
+ throw new Error(`Unexpected tapp create argument: ${arg}`);
743
+ }
744
+ if (!apiKey) {
745
+ throw new Error("tapp create requires --api-key or TAPI_API_KEY.");
746
+ }
747
+ if (!tappId) {
748
+ throw new Error("tapp create requires a Tapp id or name.");
749
+ }
750
+ if (!name) {
751
+ name = tappId;
752
+ }
753
+ return {
754
+ apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
755
+ apiKey,
756
+ tappId,
757
+ name,
758
+ };
759
+ }
760
+ async function createTapp(args) {
761
+ try {
762
+ const options = parseTappCreateOptions(args);
763
+ const tapp = await withCliSpinner(`Creating Tapi Tapp ${options.tappId}`, () => postTappCreate(options));
764
+ console.log(JSON.stringify(tapp, null, 2));
765
+ return 0;
766
+ }
767
+ catch (error) {
768
+ console.error(formatError(error));
769
+ return 1;
770
+ }
771
+ }
772
+ async function postTappCreate(options, fetchImpl = fetch) {
773
+ const response = await fetchImpl(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/tapps`, {
774
+ method: "POST",
775
+ headers: {
776
+ "Content-Type": "application/json",
777
+ "X-API-Key": options.apiKey,
778
+ "X-Tapi-Project": options.tappId,
779
+ },
780
+ body: JSON.stringify({
781
+ tappId: options.tappId,
782
+ name: options.name,
783
+ }),
784
+ });
785
+ const body = await readJsonBody(response);
786
+ if (!response.ok) {
787
+ const detail = body?.detail ?? body?.error ?? body ?? `HTTP ${response.status}`;
788
+ throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail));
789
+ }
790
+ return body ?? {};
791
+ }
792
+ async function addTappService(args) {
793
+ try {
794
+ const options = parseTappServiceAddOptions(args);
795
+ const operation = await withCliSpinner(`Adding Tapi service ${options.serviceCall} to ${options.tappId}`, () => postTappServiceAdd(options));
796
+ console.log(JSON.stringify(operation, null, 2));
797
+ return 0;
798
+ }
799
+ catch (error) {
800
+ console.error(formatError(error));
801
+ return 1;
802
+ }
803
+ }
804
+ async function postTappServiceAdd(options) {
805
+ const response = await fetch(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/tapps/${encodeURIComponent(options.tappId)}/services/${encodeURIComponent(options.serviceCall)}`, {
806
+ method: "POST",
807
+ headers: {
808
+ "Content-Type": "application/json",
809
+ "X-API-Key": options.apiKey,
810
+ "X-Tapi-Project": options.tappId,
811
+ },
812
+ body: JSON.stringify({
813
+ serviceMapId: options.serviceMapId,
814
+ entry: options.entry,
815
+ ...(options.expectedRevision ? { expectedRevision: options.expectedRevision } : {}),
816
+ }),
817
+ });
818
+ const body = await readJsonBody(response);
819
+ if (!response.ok) {
820
+ const detail = body?.detail ?? body?.error ?? body ?? `HTTP ${response.status}`;
821
+ throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail));
822
+ }
823
+ return body ?? {};
824
+ }
825
+ function parseQueueCreateOptions(args) {
826
+ const workspace = loadWorkspace();
827
+ let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
828
+ let apiKey = envString("TAPI_API_KEY") || "";
829
+ let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
830
+ let displayName = "";
831
+ let maxSlots = 8;
832
+ for (let index = 0; index < args.length; index += 1) {
833
+ const arg = args[index];
834
+ if (!arg)
835
+ continue;
836
+ if (arg === "--api-base-url" || arg === "--server") {
837
+ apiBaseUrl = requireOptionValue(args, ++index, arg);
838
+ continue;
839
+ }
840
+ if (arg.startsWith("--api-base-url=")) {
841
+ apiBaseUrl = arg.slice("--api-base-url=".length);
842
+ continue;
843
+ }
844
+ if (arg.startsWith("--server=")) {
845
+ apiBaseUrl = arg.slice("--server=".length);
846
+ continue;
847
+ }
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
+ if (arg === "--project") {
857
+ projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
858
+ continue;
859
+ }
860
+ if (arg.startsWith("--project=")) {
861
+ projectId = normalizeProjectValue(arg.slice("--project=".length), "project");
862
+ continue;
863
+ }
864
+ if (arg === "--max-slots") {
865
+ maxSlots = parsePositiveInteger(requireOptionValue(args, ++index, "--max-slots"), "--max-slots");
866
+ continue;
867
+ }
868
+ if (arg.startsWith("--max-slots=")) {
869
+ maxSlots = parsePositiveInteger(arg.slice("--max-slots=".length), "--max-slots");
870
+ continue;
871
+ }
872
+ if (arg.startsWith("--")) {
873
+ throw new Error(`Unknown queue create option: ${arg}`);
874
+ }
875
+ if (!displayName) {
876
+ displayName = arg.trim();
877
+ continue;
878
+ }
879
+ throw new Error(`Unexpected queue create argument: ${arg}`);
880
+ }
881
+ if (!apiKey) {
882
+ throw new Error("queue create requires --api-key or TAPI_API_KEY.");
883
+ }
884
+ return {
885
+ apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
886
+ apiKey,
887
+ projectId,
888
+ displayName,
889
+ maxSlots,
890
+ };
891
+ }
892
+ function parseTappQueueAddOptions(args) {
893
+ const workspace = loadWorkspace();
894
+ let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
895
+ let apiKey = envString("TAPI_API_KEY") || "";
896
+ let tappId = "";
897
+ let queueId = "";
898
+ for (let index = 0; index < args.length; index += 1) {
899
+ const arg = args[index];
900
+ if (!arg)
901
+ continue;
902
+ if (arg === "--api-base-url" || arg === "--server") {
903
+ apiBaseUrl = requireOptionValue(args, ++index, arg);
904
+ continue;
905
+ }
906
+ if (arg.startsWith("--api-base-url=")) {
907
+ apiBaseUrl = arg.slice("--api-base-url=".length);
908
+ continue;
909
+ }
910
+ if (arg.startsWith("--server=")) {
911
+ apiBaseUrl = arg.slice("--server=".length);
912
+ continue;
913
+ }
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);
319
920
  continue;
320
921
  }
321
- if (arg === "--download-only") {
322
- raw.downloadOnly = true;
323
- continue;
922
+ if (arg.startsWith("--")) {
923
+ throw new Error(`Unknown tapp queue add option: ${arg}`);
324
924
  }
325
- if (arg === "--silent") {
326
- raw.silent = true;
925
+ if (!tappId) {
926
+ tappId = normalizeProjectValue(arg, "tapp");
327
927
  continue;
328
928
  }
329
- if (arg === "--help" || arg === "-h") {
929
+ if (!queueId) {
930
+ queueId = arg.trim();
330
931
  continue;
331
932
  }
332
- throw new Error(`Unknown option: ${arg}`);
933
+ throw new Error(`Unexpected tapp queue add argument: ${arg}`);
934
+ }
935
+ if (!apiKey) {
936
+ throw new Error("tapp queue add requires --api-key or TAPI_API_KEY.");
937
+ }
938
+ if (!tappId) {
939
+ throw new Error("tapp queue add requires a Tapp id.");
940
+ }
941
+ if (!queueId) {
942
+ throw new Error("tapp queue add requires a queue id.");
333
943
  }
334
- const workspace = skipWorkspace ? undefined : loadWorkspace(workspaceSearchRoot ?? process.cwd());
335
- const channel = raw.channel ?? parseChannel(envString("TAPI_STUDIO_CHANNEL") ?? DEFAULT_CHANNEL);
336
- const apiBaseUrl = normalizeHttpUrl(raw.apiBaseUrl
337
- ?? envString("TAPI_STUDIO_API_BASE_URL")
338
- ?? envString("TAPI_BASE_URL")
339
- ?? envString("TAPI_STUDIO_SERVER_URL")
340
- ?? workspace?.apiBaseUrl
341
- ?? DEFAULT_STUDIO_API_BASE_URL, "Studio API base URL");
342
- const downloadsBaseUrl = normalizeHttpUrl(envString("TAPI_DOWNLOADS_BASE_URL") ?? DEFAULT_DOWNLOADS_BASE_URL, "TAPI_DOWNLOADS_BASE_URL");
343
- const explicitManifestUrl = raw.manifestUrlOverride ?? envString("TAPI_STUDIO_MANIFEST_URL");
344
- const manifestUrl = explicitManifestUrl
345
- ? normalizeHttpUrl(explicitManifestUrl, "Studio manifest URL")
346
- : `${downloadsBaseUrl.replace(/\/+$/, "")}/studio/channels/${channel}/latest.json`;
347
- const projectId = raw.projectId ?? envString("TAPI_PROJECT_ID") ?? workspace?.projectId;
348
- const projectSlug = raw.projectSlug ?? envString("TAPI_PROJECT_SLUG") ?? workspace?.projectSlug;
349
- const envInstallToken = envString("TAPI_STUDIO_INSTALL_TOKEN");
350
- const installToken = raw.installToken ?? envInstallToken;
351
944
  return {
352
- channel,
353
- apiBaseUrl,
354
- manifestUrl,
355
- manifestUrlOverride: explicitManifestUrl ? normalizeHttpUrl(explicitManifestUrl, "Studio manifest URL") : undefined,
356
- cacheDir: raw.cacheDir ?? getDefaultStudioCacheDir(),
357
- installDir: raw.installDir ?? resolve(envString("TAPI_STUDIO_INSTALL_DIR") ?? getDefaultPortableStudioServerReleasesDir()),
358
- downloadOnly: raw.downloadOnly ?? false,
359
- silent: raw.silent ?? false,
360
- exePath: raw.exePath ?? envString("TAPI_STUDIO_EXE"),
361
- installToken,
362
- installTokenSource: raw.installToken ? "option" : installToken ? "env" : undefined,
363
- workspace,
364
- workspaceRoot: workspace?.root,
365
- projectId,
366
- projectSlug,
367
- workspaceMode: Boolean(projectId || workspace),
945
+ apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
946
+ apiKey,
947
+ tappId,
948
+ queueId,
368
949
  };
369
950
  }
370
- function parseServiceOptions(args) {
371
- let operation = "";
951
+ function parseRunnerSlotSetOptions(args) {
372
952
  const workspace = loadWorkspace();
373
953
  let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
374
954
  let apiKey = envString("TAPI_API_KEY") || "";
375
955
  let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
956
+ let runnerId = "";
957
+ let slotIndex = 0;
958
+ let queueId = "";
376
959
  for (let index = 0; index < args.length; index += 1) {
377
960
  const arg = args[index];
378
961
  if (!arg)
@@ -406,42 +989,51 @@ function parseServiceOptions(args) {
406
989
  continue;
407
990
  }
408
991
  if (arg.startsWith("--")) {
409
- throw new Error(`Unknown services option: ${arg}`);
992
+ throw new Error(`Unknown runner slot set option: ${arg}`);
410
993
  }
411
- if (!operation) {
412
- operation = arg;
994
+ if (!runnerId) {
995
+ runnerId = arg.trim();
413
996
  continue;
414
997
  }
415
- throw new Error(`Unexpected services argument: ${arg}`);
416
- }
417
- if (!operation) {
418
- throw new Error("services describe requires a service run like schwab.place_order.");
998
+ if (!slotIndex) {
999
+ slotIndex = parsePositiveInteger(arg.trim(), "slot index");
1000
+ continue;
1001
+ }
1002
+ if (!queueId) {
1003
+ queueId = arg.trim();
1004
+ continue;
1005
+ }
1006
+ throw new Error(`Unexpected runner slot set argument: ${arg}`);
419
1007
  }
420
1008
  if (!apiKey) {
421
- throw new Error("services describe requires --api-key or TAPI_API_KEY.");
1009
+ throw new Error("runner slot set requires --api-key or TAPI_API_KEY.");
422
1010
  }
423
1011
  if (!projectId) {
424
- throw new Error("services describe requires --project, TAPI_PROJECT_ID, or .tapi/project.json.");
1012
+ throw new Error("runner slot set requires --project or TAPI_PROJECT_ID.");
1013
+ }
1014
+ if (!runnerId) {
1015
+ throw new Error("runner slot set requires a runner id.");
1016
+ }
1017
+ if (!slotIndex) {
1018
+ throw new Error("runner slot set requires a slot index.");
1019
+ }
1020
+ if (!queueId) {
1021
+ throw new Error("runner slot set requires a queue id.");
425
1022
  }
426
1023
  return {
427
- operation,
428
- options: {
429
- apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
430
- apiKey,
431
- projectId,
432
- },
1024
+ apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
1025
+ apiKey,
1026
+ projectId,
1027
+ runnerId,
1028
+ slotIndex,
1029
+ queueId,
433
1030
  };
434
1031
  }
435
- async function describeServiceOperation(args) {
1032
+ async function createQueue(args) {
436
1033
  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));
1034
+ const options = parseQueueCreateOptions(args);
1035
+ const queue = await withCliSpinner(`Creating Tapi queue`, () => postQueueCreate(options));
1036
+ console.log(JSON.stringify(queue, null, 2));
445
1037
  return 0;
446
1038
  }
447
1039
  catch (error) {
@@ -449,13 +1041,95 @@ async function describeServiceOperation(args) {
449
1041
  return 1;
450
1042
  }
451
1043
  }
452
- function parseApiWorkspaceOptions(args) {
1044
+ async function addTappQueue(args) {
1045
+ try {
1046
+ const options = parseTappQueueAddOptions(args);
1047
+ const result = await withCliSpinner(`Attaching Tapi queue ${options.queueId} to ${options.tappId}`, () => postTappQueueAdd(options));
1048
+ console.log(JSON.stringify(result, null, 2));
1049
+ return 0;
1050
+ }
1051
+ catch (error) {
1052
+ console.error(formatError(error));
1053
+ return 1;
1054
+ }
1055
+ }
1056
+ async function setRunnerSlot(args) {
1057
+ try {
1058
+ const options = parseRunnerSlotSetOptions(args);
1059
+ const result = await withCliSpinner(`Assigning runner slot ${options.runnerId}/${options.slotIndex} to queue ${options.queueId}`, () => putRunnerSlot(options));
1060
+ console.log(JSON.stringify(result, null, 2));
1061
+ return 0;
1062
+ }
1063
+ catch (error) {
1064
+ console.error(formatError(error));
1065
+ return 1;
1066
+ }
1067
+ }
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
+ }
1076
+ const response = await fetch(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/queues`, {
1077
+ method: "POST",
1078
+ headers,
1079
+ body: JSON.stringify({
1080
+ ...(options.displayName ? { displayName: options.displayName } : {}),
1081
+ maxSlots: options.maxSlots,
1082
+ }),
1083
+ });
1084
+ const body = await readJsonBody(response);
1085
+ if (!response.ok) {
1086
+ const detail = body?.detail ?? body?.error ?? body ?? `HTTP ${response.status}`;
1087
+ throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail));
1088
+ }
1089
+ return body ?? {};
1090
+ }
1091
+ async function postTappQueueAdd(options) {
1092
+ const response = await fetch(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/tapps/${encodeURIComponent(options.tappId)}/queues`, {
1093
+ method: "POST",
1094
+ headers: {
1095
+ "Content-Type": "application/json",
1096
+ "X-API-Key": options.apiKey,
1097
+ "X-Tapi-Project": options.tappId,
1098
+ },
1099
+ body: JSON.stringify({ queueId: options.queueId }),
1100
+ });
1101
+ const body = await readJsonBody(response);
1102
+ if (!response.ok) {
1103
+ const detail = body?.detail ?? body?.error ?? body ?? `HTTP ${response.status}`;
1104
+ throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail));
1105
+ }
1106
+ return body ?? {};
1107
+ }
1108
+ async function putRunnerSlot(options) {
1109
+ const response = await fetch(`${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/runners/${encodeURIComponent(options.runnerId)}/slots/${encodeURIComponent(String(options.slotIndex))}`, {
1110
+ method: "PUT",
1111
+ headers: {
1112
+ "Content-Type": "application/json",
1113
+ "X-API-Key": options.apiKey,
1114
+ "X-Tapi-Project": options.projectId,
1115
+ },
1116
+ body: JSON.stringify({ queueId: options.queueId }),
1117
+ });
1118
+ const body = await readJsonBody(response);
1119
+ if (!response.ok) {
1120
+ const detail = body?.detail ?? body?.error ?? body ?? `HTTP ${response.status}`;
1121
+ throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail));
1122
+ }
1123
+ return body ?? {};
1124
+ }
1125
+ export function parseRunnerSetupOptions(args) {
453
1126
  const workspace = loadWorkspace();
454
1127
  let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
455
1128
  let apiKey = envString("TAPI_API_KEY") || "";
456
1129
  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";
1130
+ let runnerId = envString("TAPI_RUNNER_ID") || "";
1131
+ let port = Number(envString("TAPI_RUNNER_SETUP_PORT") || "17687");
1132
+ let openBrowser = true;
459
1133
  for (let index = 0; index < args.length; index += 1) {
460
1134
  const arg = args[index];
461
1135
  if (!arg)
@@ -488,45 +1162,67 @@ function parseApiWorkspaceOptions(args) {
488
1162
  projectId = normalizeProjectValue(arg.slice("--project=".length), "project");
489
1163
  continue;
490
1164
  }
491
- if (arg === "--catalog") {
492
- catalogPath = requireOptionValue(args, ++index, "--catalog");
1165
+ if (arg === "--runner-id") {
1166
+ runnerId = requireOptionValue(args, ++index, "--runner-id").trim();
493
1167
  continue;
494
1168
  }
495
- if (arg.startsWith("--catalog=")) {
496
- catalogPath = arg.slice("--catalog=".length);
1169
+ if (arg.startsWith("--runner-id=")) {
1170
+ runnerId = arg.slice("--runner-id=".length).trim();
497
1171
  continue;
498
1172
  }
499
- if (arg === "--out") {
500
- typescriptPath = requireOptionValue(args, ++index, "--out");
1173
+ if (arg === "--port") {
1174
+ port = parseNonNegativeInteger(requireOptionValue(args, ++index, "--port"), "--port");
501
1175
  continue;
502
1176
  }
503
- if (arg.startsWith("--out=")) {
504
- typescriptPath = arg.slice("--out=".length);
1177
+ if (arg.startsWith("--port=")) {
1178
+ port = parseNonNegativeInteger(arg.slice("--port=".length), "--port");
505
1179
  continue;
506
1180
  }
507
- throw new Error(`Unknown services option: ${arg}`);
1181
+ if (arg === "--no-open") {
1182
+ openBrowser = false;
1183
+ continue;
1184
+ }
1185
+ if (arg.startsWith("--")) {
1186
+ throw new Error(`Unknown runner setup option: ${arg}`);
1187
+ }
1188
+ if (!runnerId) {
1189
+ runnerId = arg.trim();
1190
+ continue;
1191
+ }
1192
+ throw new Error(`Unexpected runner setup argument: ${arg}`);
508
1193
  }
509
1194
  if (!apiKey) {
510
- throw new Error("services command requires --api-key or TAPI_API_KEY.");
1195
+ throw new Error("runner setup requires --api-key or TAPI_API_KEY.");
511
1196
  }
512
1197
  if (!projectId) {
513
- throw new Error("services command requires --project, TAPI_PROJECT_ID, or .tapi/project.json.");
1198
+ throw new Error("runner setup requires --project or TAPI_PROJECT_ID.");
1199
+ }
1200
+ if (!runnerId) {
1201
+ throw new Error("runner setup requires --runner-id or TAPI_RUNNER_ID.");
1202
+ }
1203
+ if (!Number.isInteger(port) || port < 0) {
1204
+ throw new Error("--port must be a non-negative integer.");
514
1205
  }
515
- const root = workspace?.root || process.cwd();
516
1206
  return {
517
1207
  apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
518
1208
  apiKey,
519
1209
  projectId,
520
- catalogPath: resolve(root, catalogPath),
521
- typescriptPath: resolve(root, typescriptPath),
1210
+ runnerId,
1211
+ port,
1212
+ openBrowser,
522
1213
  };
523
1214
  }
524
- async function syncServiceCatalog(args) {
1215
+ async function runRunnerSetup(args) {
525
1216
  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}`);
1217
+ const options = parseRunnerSetupOptions(args);
1218
+ const handle = await startRunnerSetupServer(options);
1219
+ if (options.openBrowser) {
1220
+ openUrlInBrowser(handle.url);
1221
+ }
1222
+ console.log(`Tapi runner setup: ${handle.url}`);
1223
+ console.log("Press Ctrl+C to stop the setup server.");
1224
+ await waitForProcessSignal();
1225
+ await handle.close();
530
1226
  return 0;
531
1227
  }
532
1228
  catch (error) {
@@ -534,19 +1230,216 @@ async function syncServiceCatalog(args) {
534
1230
  return 1;
535
1231
  }
536
1232
  }
537
- async function generateServiceClient(args) {
1233
+ export async function startRunnerSetupServer(options, fetchImpl = fetch) {
1234
+ const server = createServer((request, response) => {
1235
+ void handleRunnerSetupRequest(request, response, options, fetchImpl);
1236
+ });
1237
+ await listenOnLocalhost(server, options.port);
1238
+ const address = server.address();
1239
+ const port = typeof address === "object" && address ? address.port : options.port;
1240
+ return {
1241
+ url: `http://127.0.0.1:${port}/`,
1242
+ port,
1243
+ close: () => closeServer(server),
1244
+ };
1245
+ }
1246
+ export async function executeRunnerSetupAction(options, action, fetchImpl = fetch) {
1247
+ let queueId = String(action.queueId || "").trim();
1248
+ const maxSlots = coercePositiveInteger(action.maxSlots, 8, "maxSlots");
1249
+ const slotStart = coercePositiveInteger(action.slotStart, 1, "slotStart");
1250
+ const slotCount = coercePositiveInteger(action.slotCount, 1, "slotCount");
1251
+ let queue = {};
1252
+ if (!queueId) {
1253
+ queue = await sdkJsonRequest(fetchImpl, `${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/queues`, {
1254
+ method: "POST",
1255
+ headers: sdkJsonHeaders(options.apiKey, options.projectId),
1256
+ body: JSON.stringify({
1257
+ displayName: String(action.displayName || `${options.projectId} queue`).trim(),
1258
+ maxSlots,
1259
+ }),
1260
+ });
1261
+ queueId = String(queue.queueId || queue.queue_id || "").trim();
1262
+ }
1263
+ if (!queueId) {
1264
+ throw new Error("Runner setup needs an existing queue id or a created queue response with queueId.");
1265
+ }
1266
+ const attachment = await sdkJsonRequest(fetchImpl, `${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/tapps/${encodeURIComponent(options.projectId)}/queues`, {
1267
+ method: "POST",
1268
+ headers: sdkJsonHeaders(options.apiKey, options.projectId),
1269
+ body: JSON.stringify({ queueId }),
1270
+ });
1271
+ const slots = [];
1272
+ for (let offset = 0; offset < slotCount; offset += 1) {
1273
+ const slotIndex = slotStart + offset;
1274
+ slots.push(await sdkJsonRequest(fetchImpl, `${options.apiBaseUrl.replace(/\/+$/, "")}/api/sdk/v1/runners/${encodeURIComponent(options.runnerId)}/slots/${encodeURIComponent(String(slotIndex))}`, {
1275
+ method: "PUT",
1276
+ headers: sdkJsonHeaders(options.apiKey, options.projectId),
1277
+ body: JSON.stringify({ queueId }),
1278
+ }));
1279
+ }
1280
+ return {
1281
+ queueId,
1282
+ queue: queueId && !Object.keys(queue).length ? { queueId } : queue,
1283
+ attachment,
1284
+ slots,
1285
+ };
1286
+ }
1287
+ async function handleRunnerSetupRequest(request, response, options, fetchImpl) {
538
1288
  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;
1289
+ const url = new URL(request.url || "/", "http://127.0.0.1");
1290
+ if (request.method === "GET" && url.pathname === "/") {
1291
+ sendText(response, 200, runnerSetupHtml(options), "text/html; charset=utf-8");
1292
+ return;
1293
+ }
1294
+ if (request.method === "GET" && url.pathname === "/api/status") {
1295
+ sendJson(response, 200, {
1296
+ apiBaseUrl: options.apiBaseUrl,
1297
+ projectId: options.projectId,
1298
+ runnerId: options.runnerId,
1299
+ });
1300
+ return;
1301
+ }
1302
+ if (request.method === "POST" && url.pathname === "/api/setup") {
1303
+ const body = await readRequestJson(request);
1304
+ const result = await executeRunnerSetupAction(options, body, fetchImpl);
1305
+ sendJson(response, 200, result);
1306
+ return;
1307
+ }
1308
+ sendJson(response, 404, { error: "not found" });
545
1309
  }
546
1310
  catch (error) {
547
- console.error(formatError(error));
548
- return 1;
1311
+ sendJson(response, 400, { error: formatError(error) });
1312
+ }
1313
+ }
1314
+ function runnerSetupHtml(options) {
1315
+ const displayName = `${options.projectId} queue`;
1316
+ return `<!doctype html>
1317
+ <html lang="en">
1318
+ <head>
1319
+ <meta charset="utf-8">
1320
+ <meta name="viewport" content="width=device-width,initial-scale=1">
1321
+ <title>Tapi Runner Setup</title>
1322
+ <style>
1323
+ :root { color-scheme: light dark; --bg: #f7f7f5; --panel: #ffffff; --text: #1d2329; --muted: #62707c; --line: #d6dde2; --accent: #176b4d; --accent-2: #0f5ca8; --danger: #ad2e24; }
1324
+ @media (prefers-color-scheme: dark) { :root { --bg: #111418; --panel: #181d22; --text: #eef3f6; --muted: #a6b1bb; --line: #303a43; --accent: #59b88c; --accent-2: #6da8e8; --danger: #ee786e; } }
1325
+ * { box-sizing: border-box; }
1326
+ body { margin: 0; font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--text); background: var(--bg); }
1327
+ main { max-width: 920px; margin: 0 auto; padding: 28px 18px 36px; }
1328
+ h1 { margin: 0 0 18px; font-size: 28px; font-weight: 650; letter-spacing: 0; }
1329
+ h2 { margin: 0 0 12px; font-size: 16px; font-weight: 650; letter-spacing: 0; }
1330
+ .grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(260px, 320px); gap: 18px; align-items: start; }
1331
+ .panel, .summary { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 18px; }
1332
+ .row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
1333
+ label { display: grid; gap: 6px; margin: 0 0 12px; color: var(--muted); font-size: 12px; font-weight: 600; text-transform: uppercase; }
1334
+ input { width: 100%; min-width: 0; border: 1px solid var(--line); border-radius: 6px; padding: 10px 11px; color: var(--text); background: transparent; font: inherit; }
1335
+ button { border: 0; border-radius: 6px; padding: 11px 14px; color: white; background: var(--accent); font: inherit; font-weight: 650; cursor: pointer; }
1336
+ button:disabled { opacity: .55; cursor: progress; }
1337
+ code { word-break: break-all; }
1338
+ .summary dl { display: grid; grid-template-columns: 88px minmax(0, 1fr); gap: 8px 10px; margin: 0; }
1339
+ .summary dt { color: var(--muted); font-weight: 650; }
1340
+ .summary dd { margin: 0; min-width: 0; }
1341
+ .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; }
1342
+ .error { color: var(--danger); }
1343
+ .ok { color: var(--accent-2); }
1344
+ @media (max-width: 720px) { .grid, .row { grid-template-columns: 1fr; } main { padding: 20px 14px 28px; } h1 { font-size: 24px; } }
1345
+ </style>
1346
+ </head>
1347
+ <body>
1348
+ <main>
1349
+ <h1>Tapi Runner Setup</h1>
1350
+ <div class="grid">
1351
+ <section class="panel">
1352
+ <h2>Queue And Slots</h2>
1353
+ <form id="setup-form">
1354
+ <label>Existing Queue ID<input id="queueId" name="queueId" autocomplete="off" placeholder="queue_abc123"></label>
1355
+ <label>New Queue Name<input id="displayName" name="displayName" value="${escapeHtml(displayName)}" autocomplete="off"></label>
1356
+ <div class="row">
1357
+ <label>Queue Slot Limit<input id="maxSlots" name="maxSlots" type="number" min="1" step="1" value="8"></label>
1358
+ <label>First Runner Slot<input id="slotStart" name="slotStart" type="number" min="1" step="1" value="1"></label>
1359
+ </div>
1360
+ <label>Runner Slot Count<input id="slotCount" name="slotCount" type="number" min="1" step="1" value="1"></label>
1361
+ <button id="submit" type="submit">Apply Setup</button>
1362
+ </form>
1363
+ <pre id="result" class="result" aria-live="polite"></pre>
1364
+ </section>
1365
+ <aside class="summary">
1366
+ <h2>Target</h2>
1367
+ <dl>
1368
+ <dt>Server</dt><dd><code>${escapeHtml(options.apiBaseUrl)}</code></dd>
1369
+ <dt>Tapp</dt><dd><code>${escapeHtml(options.projectId)}</code></dd>
1370
+ <dt>Runner</dt><dd><code>${escapeHtml(options.runnerId)}</code></dd>
1371
+ </dl>
1372
+ </aside>
1373
+ </div>
1374
+ </main>
1375
+ <script>
1376
+ const form = document.getElementById("setup-form");
1377
+ const button = document.getElementById("submit");
1378
+ const result = document.getElementById("result");
1379
+ form.addEventListener("submit", async (event) => {
1380
+ event.preventDefault();
1381
+ button.disabled = true;
1382
+ result.className = "result";
1383
+ result.textContent = "Applying setup...";
1384
+ const body = Object.fromEntries(new FormData(form).entries());
1385
+ body.maxSlots = Number(body.maxSlots || 8);
1386
+ body.slotStart = Number(body.slotStart || 1);
1387
+ body.slotCount = Number(body.slotCount || 1);
1388
+ try {
1389
+ const response = await fetch("/api/setup", {
1390
+ method: "POST",
1391
+ headers: { "Content-Type": "application/json" },
1392
+ body: JSON.stringify(body),
1393
+ });
1394
+ const payload = await response.json();
1395
+ if (!response.ok) throw new Error(payload.error || JSON.stringify(payload));
1396
+ result.className = "result ok";
1397
+ result.textContent = JSON.stringify(payload, null, 2);
1398
+ document.getElementById("queueId").value = payload.queueId || body.queueId || "";
1399
+ } catch (error) {
1400
+ result.className = "result error";
1401
+ result.textContent = error instanceof Error ? error.message : String(error);
1402
+ } finally {
1403
+ button.disabled = false;
1404
+ }
1405
+ });
1406
+ </script>
1407
+ </body>
1408
+ </html>`;
1409
+ }
1410
+ async function sdkJsonRequest(fetchImpl, url, init) {
1411
+ const response = await fetchImpl(url, init);
1412
+ const body = await readJsonBody(response);
1413
+ if (!response.ok) {
1414
+ const detail = body?.detail ?? body?.error ?? body ?? `HTTP ${response.status}`;
1415
+ throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail));
1416
+ }
1417
+ return body ?? {};
1418
+ }
1419
+ function sdkJsonHeaders(apiKey, projectId) {
1420
+ return {
1421
+ "Content-Type": "application/json",
1422
+ "X-API-Key": apiKey,
1423
+ "X-Tapi-Project": projectId,
1424
+ };
1425
+ }
1426
+ function coercePositiveInteger(value, fallback, label) {
1427
+ const text = value === undefined || value === null || value === "" ? String(fallback) : String(value);
1428
+ return parsePositiveInteger(text, label);
1429
+ }
1430
+ function parsePositiveInteger(value, label) {
1431
+ const parsed = Number(value);
1432
+ if (!Number.isInteger(parsed) || parsed < 1) {
1433
+ throw new Error(`${label} must be a positive integer.`);
1434
+ }
1435
+ return parsed;
1436
+ }
1437
+ function parseNonNegativeInteger(value, label) {
1438
+ const parsed = Number(value);
1439
+ if (!Number.isInteger(parsed) || parsed < 0) {
1440
+ throw new Error(`${label} must be a non-negative integer.`);
549
1441
  }
1442
+ return parsed;
550
1443
  }
551
1444
  function parseTriggerSyncOptions(args) {
552
1445
  const workspace = loadWorkspace();
@@ -610,16 +1503,17 @@ function parseTriggerSyncOptions(args) {
610
1503
  configPath: configPath ? resolve(root, configPath) : defaultTriggerConfigPath(root),
611
1504
  };
612
1505
  }
613
- async function syncApiTriggers(args) {
1506
+ async function syncServiceTriggers(args) {
614
1507
  try {
615
1508
  const options = parseTriggerSyncOptions(args);
616
- const triggers = await readApiTriggerConfig(options.configPath);
1509
+ const triggers = await readServiceTriggerConfig(options.configPath);
617
1510
  if (triggers.length === 0) {
618
1511
  throw new Error(`No Tapi service triggers found in ${options.configPath}.`);
619
1512
  }
620
1513
  const client = new TapiClient({
621
1514
  baseUrl: options.apiBaseUrl,
622
1515
  apiKey: options.apiKey,
1516
+ tappId: options.projectId,
623
1517
  projectId: options.projectId,
624
1518
  });
625
1519
  await withCliSpinner(`Syncing ${triggers.length} Tapi service trigger(s)`, async () => {
@@ -770,6 +1664,7 @@ async function fetchDevSessions(options) {
770
1664
  const client = new TapiClient({
771
1665
  baseUrl: options.apiBaseUrl,
772
1666
  apiKey: options.apiKey,
1667
+ tappId: options.projectId,
773
1668
  projectId: options.projectId,
774
1669
  });
775
1670
  return await withCliSpinner("Fetching Tapi dev sessions", () => client.sessions.list({ site: options.site }));
@@ -897,7 +1792,6 @@ async function openDevSessionInStudio(session, options) {
897
1792
  function studioSessionLaunchParams(session) {
898
1793
  const params = new URLSearchParams();
899
1794
  params.set("runId", session.id);
900
- params.set("apiRunId", session.id);
901
1795
  if (session.sitemap)
902
1796
  params.set("sitemap", session.sitemap);
903
1797
  if (session.runtimeSessionId)
@@ -908,7 +1802,6 @@ function studioSessionSelectIntent(session) {
908
1802
  return {
909
1803
  sessionId: session.id,
910
1804
  runId: session.id,
911
- apiRunId: session.id,
912
1805
  ...(session.sitemap ? { sitemap: session.sitemap } : {}),
913
1806
  ...(session.runtimeSessionId ? { runtimeSessionId: session.runtimeSessionId } : {}),
914
1807
  source: "cli",
@@ -961,12 +1854,12 @@ function renderDevSessions(sessions, selectedId = "") {
961
1854
  const openable = session.openable ? "*" : " ";
962
1855
  const id = truncateText(session.id || "", 18).padEnd(18, " ");
963
1856
  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";
1857
+ const serviceRun = session.serviceRun || (session.serviceName && session.serviceKey
1858
+ ? `${session.serviceName}.${session.serviceKey}`
1859
+ : session.serviceName || session.serviceKey || "unknown");
967
1860
  const state = session.stateLabel ? ` state=${session.stateLabel}` : "";
968
1861
  const url = session.currentUrl ? ` ${truncateText(session.currentUrl, 60)}` : "";
969
- lines.push(`${selected}${openable} ${id} ${status} ${apiName}${state}${url}`);
1862
+ lines.push(`${selected}${openable} ${id} ${status} ${serviceRun}${state}${url}`);
970
1863
  }
971
1864
  }
972
1865
  lines.push("");
@@ -988,7 +1881,7 @@ function defaultTriggerConfigPath(root) {
988
1881
  }
989
1882
  return join(root, "tapi.config.json");
990
1883
  }
991
- async function readApiTriggerConfig(configPath) {
1884
+ async function readServiceTriggerConfig(configPath) {
992
1885
  if (!existsSync(configPath)) {
993
1886
  throw new Error(`Tapi trigger config not found at ${configPath}. Create tapi.config.json or pass --config.`);
994
1887
  }
@@ -1051,26 +1944,31 @@ function normalizeTriggerEntry(entry, index, configPath) {
1051
1944
  throw new Error(`Trigger #${index + 1} in ${configPath} must be an object.`);
1052
1945
  }
1053
1946
  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);
1947
+ if (RETIRED_TRIGGER_TARGET_FIELD in entry) {
1948
+ throw new Error(`Trigger '${name}' uses a retired trigger target field; use serviceRun.`);
1949
+ }
1950
+ const serviceRun = optionalStringField(entry.serviceRun, "serviceRun");
1059
1951
  if (!serviceRun) {
1060
- throw new Error(`Trigger '${name}' requires serviceRun, service, or serviceName/requestKey.`);
1952
+ throw new Error(`Trigger '${name}' requires serviceRun.`);
1061
1953
  }
1062
1954
  if (!serviceRun.includes(".")) {
1063
- throw new Error(`Trigger '${name}' serviceRun must be formatted as '<serviceName>.<requestKey>'.`);
1955
+ throw new Error(`Trigger '${name}' serviceRun must be formatted as '<serviceName>.<serviceKey>'.`);
1956
+ }
1957
+ if ("runnerId" in entry) {
1958
+ throw new Error(`Trigger '${name}' uses runnerId; use queueId.`);
1064
1959
  }
1065
1960
  const schedule = normalizeTriggerSchedule(entry.schedule, entry.interval, name);
1961
+ const runtime = entry.runtime === undefined ? undefined : recordField(entry.runtime, `Trigger '${name}' runtime`);
1962
+ rejectNonPolicyTriggerRuntimeOwner(runtime, `Trigger '${name}' runtime`);
1963
+ const queueId = stringField(entry.queueId, `Trigger '${name}' queueId`);
1066
1964
  const request = {
1067
1965
  name,
1068
- apiRequest: serviceRun,
1966
+ serviceRun,
1967
+ queueId,
1069
1968
  ...(entry.enabled === undefined ? {} : { enabled: booleanField(entry.enabled, `Trigger '${name}' enabled`) }),
1070
1969
  ...(schedule ? { schedule } : {}),
1071
1970
  ...(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`) }),
1971
+ ...(runtime === undefined ? {} : { runtime }),
1074
1972
  ...(entry.priority === undefined ? {} : { priority: numberField(entry.priority, `Trigger '${name}' priority`) }),
1075
1973
  ...(entry.site === undefined ? {} : { site: stringField(entry.site, `Trigger '${name}' site`) }),
1076
1974
  };
@@ -1153,11 +2051,6 @@ function parseDurationSeconds(value, fieldName) {
1153
2051
  }
1154
2052
  return seconds;
1155
2053
  }
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
2054
  function stringField(value, fieldName) {
1162
2055
  if (typeof value !== "string" || !value.trim()) {
1163
2056
  throw new Error(`${fieldName} must be a non-empty string.`);
@@ -1188,10 +2081,30 @@ function recordField(value, fieldName) {
1188
2081
  }
1189
2082
  return value;
1190
2083
  }
2084
+ function rejectNonPolicyTriggerRuntimeOwner(runtime, fieldName) {
2085
+ if (!runtime) {
2086
+ return;
2087
+ }
2088
+ for (const field of RETIRED_RUNTIME_OWNER_ALIAS_FIELDS) {
2089
+ if (field in runtime) {
2090
+ throw new Error(`unsupported service trigger runtime field: ${field}`);
2091
+ }
2092
+ }
2093
+ for (const field of RUNTIME_OWNER_FIELDS) {
2094
+ if (!(field in runtime)) {
2095
+ continue;
2096
+ }
2097
+ const owner = String(runtime[field] ?? "").trim().toLowerCase();
2098
+ if (owner && owner !== SERVICE_RUN_RUNTIME_OWNER) {
2099
+ throw new Error(`${fieldName} owner must be service_run.`);
2100
+ }
2101
+ }
2102
+ }
1191
2103
  async function fetchApiCatalog(options) {
1192
2104
  const client = new TapiClient({
1193
2105
  baseUrl: options.apiBaseUrl,
1194
2106
  apiKey: options.apiKey,
2107
+ tappId: options.projectId,
1195
2108
  projectId: options.projectId,
1196
2109
  });
1197
2110
  return client.catalog.get();
@@ -1203,50 +2116,6 @@ async function writeTextFile(path, content) {
1203
2116
  await mkdir(dirname(path), { recursive: true });
1204
2117
  await writeFile(path, content, "utf8");
1205
2118
  }
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
2119
  function isRecord(value) {
1251
2120
  return typeof value === "object" && value !== null && !Array.isArray(value);
1252
2121
  }
@@ -2994,6 +3863,56 @@ function openBrowser(url) {
2994
3863
  });
2995
3864
  child.unref();
2996
3865
  }
3866
+ function openUrlInBrowser(url) {
3867
+ if (process.platform === "win32") {
3868
+ openBrowser(url);
3869
+ return;
3870
+ }
3871
+ const command = process.platform === "darwin" ? "open" : "xdg-open";
3872
+ const child = spawn(command, [url], {
3873
+ detached: true,
3874
+ stdio: "ignore",
3875
+ windowsHide: true,
3876
+ });
3877
+ child.unref();
3878
+ }
3879
+ async function listenOnLocalhost(server, port) {
3880
+ await new Promise((resolvePromise, rejectPromise) => {
3881
+ const onError = (error) => {
3882
+ server.off("listening", onListening);
3883
+ rejectPromise(error);
3884
+ };
3885
+ const onListening = () => {
3886
+ server.off("error", onError);
3887
+ resolvePromise();
3888
+ };
3889
+ server.once("error", onError);
3890
+ server.once("listening", onListening);
3891
+ server.listen(port, "127.0.0.1");
3892
+ });
3893
+ }
3894
+ async function closeServer(server) {
3895
+ await new Promise((resolvePromise, rejectPromise) => {
3896
+ server.close((error) => {
3897
+ if (error) {
3898
+ rejectPromise(error);
3899
+ return;
3900
+ }
3901
+ resolvePromise();
3902
+ });
3903
+ });
3904
+ }
3905
+ async function waitForProcessSignal() {
3906
+ await new Promise((resolvePromise) => {
3907
+ const done = () => {
3908
+ process.off("SIGINT", done);
3909
+ process.off("SIGTERM", done);
3910
+ resolvePromise();
3911
+ };
3912
+ process.once("SIGINT", done);
3913
+ process.once("SIGTERM", done);
3914
+ });
3915
+ }
2997
3916
  async function readJsonBody(response) {
2998
3917
  const text = await response.text();
2999
3918
  if (!text.trim()) {
@@ -3006,6 +3925,40 @@ async function readJsonBody(response) {
3006
3925
  return null;
3007
3926
  }
3008
3927
  }
3928
+ async function readRequestJson(request) {
3929
+ let body = "";
3930
+ for await (const chunk of request) {
3931
+ body += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
3932
+ if (Buffer.byteLength(body, "utf8") > 64 * 1024) {
3933
+ throw new Error("request body is too large");
3934
+ }
3935
+ }
3936
+ if (!body.trim()) {
3937
+ return {};
3938
+ }
3939
+ const parsed = JSON.parse(body);
3940
+ if (!isRecord(parsed)) {
3941
+ throw new Error("request body must be a JSON object");
3942
+ }
3943
+ return parsed;
3944
+ }
3945
+ function sendText(response, status, text, contentType) {
3946
+ response.statusCode = status;
3947
+ response.setHeader("Content-Type", contentType);
3948
+ response.setHeader("Cache-Control", "no-store");
3949
+ response.end(text);
3950
+ }
3951
+ function sendJson(response, status, payload) {
3952
+ sendText(response, status, JSON.stringify(payload), "application/json; charset=utf-8");
3953
+ }
3954
+ function escapeHtml(value) {
3955
+ return value
3956
+ .replace(/&/g, "&amp;")
3957
+ .replace(/</g, "&lt;")
3958
+ .replace(/>/g, "&gt;")
3959
+ .replace(/"/g, "&quot;")
3960
+ .replace(/'/g, "&#39;");
3961
+ }
3009
3962
  function isApprovalTerminalError(error) {
3010
3963
  return error instanceof StudioInstallApprovalError
3011
3964
  && (error.code === "pending_approval" || error.code === "access_pending" || error.code === "access_rejected");
@@ -3560,11 +4513,16 @@ Usage:
3560
4513
  tapi link --project PROJECT
3561
4514
  tapi studio install [--channel pilot] [--api-base-url URL]
3562
4515
  tapi studio
3563
- tapi studio open
3564
- tapi studio doctor
3565
- tapi services describe <servicemap.run>
4516
+ tapi studio open
4517
+ tapi studio doctor
4518
+ tapi tapp create <tapp>
4519
+ tapi queue create [display-name]
4520
+ tapi tapp service add <tapp> <service.call> --service-map ID --entry ENTRY
4521
+ tapi tapp queue add <tapp> <queue-id>
4522
+ tapi runner setup --runner-id RUNNER --project TAPP
4523
+ tapi runner slot set <runner-id> <slot-index> <queue-id>
4524
+ tapi services describe <servicemap.run>
3566
4525
  tapi services sync
3567
- tapi services generate
3568
4526
  tapi triggers sync
3569
4527
  tapi sessions
3570
4528
  tapi service status
@@ -3574,20 +4532,75 @@ Commands:
3574
4532
  init Create .tapi/project.json for this repo
3575
4533
  link Rebind this repo to an existing Tapi project
3576
4534
  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
4535
+ studio Open Tapi Studio for this repo
4536
+ studio open Open Tapi Studio for this repo
4537
+ studio doctor Check local SDK and Studio release configuration
4538
+ tapp create Create a Tapp
4539
+ queue create Create a queue for service-run routing
4540
+ tapp service add
4541
+ Add a ServiceMap-backed service call to a Tapp
4542
+ tapp queue add Attach a queue to a Tapp
4543
+ runner setup Open the local runner queue/slot setup UI
4544
+ runner slot set Assign a queue to a runner slot
4545
+ services describe
3581
4546
  Print a ServiceMap service-run input/output contract
3582
4547
  services sync Save the service-run catalog to .tapi/services
3583
- services generate
3584
- Write a TypeScript service-run wrapper from the catalog
3585
4548
  triggers sync Upsert service triggers from tapi.config
3586
4549
  sessions List dev-mode API sessions and open takeover sessions
3587
4550
  service Inspect or control the local Tapi Windows service
3588
4551
  doctor Alias for studio doctor
3589
4552
  `);
3590
4553
  }
4554
+ function printTappHelp() {
4555
+ console.log(`Tapi Tapp commands
4556
+
4557
+ 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]
4561
+
4562
+ Options:
4563
+ --api-base-url <url> Tapi API base URL
4564
+ --server <url> Alias for --api-base-url
4565
+ --api-key <key> Tapi API key; defaults to TAPI_API_KEY
4566
+ --tapp-id <id> Explicit Tapp id for tapp create
4567
+ --name <name> Display name for tapp create
4568
+ --service-map <id> Backing ServiceMap id
4569
+ --entry <entry> ServiceMap entry URL/name to launch
4570
+ --expected-revision <r> Optional ServiceMap revision guard
4571
+ `);
4572
+ }
4573
+ function printQueueHelp() {
4574
+ console.log(`Tapi queue commands
4575
+
4576
+ Usage:
4577
+ tapi queue create [display-name] [--max-slots N] [--project TAPP] [--api-base-url URL] [--api-key KEY]
4578
+
4579
+ Options:
4580
+ --api-base-url <url> Tapi API base URL
4581
+ --server <url> Alias for --api-base-url
4582
+ --api-key <key> Tapi API key; defaults to TAPI_API_KEY
4583
+ --project <id> Optional project scope for the queue request
4584
+ --max-slots <n> Maximum runner slots this queue may use
4585
+ `);
4586
+ }
4587
+ function printRunnerHelp() {
4588
+ console.log(`Tapi runner commands
4589
+
4590
+ 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]
4593
+
4594
+ Options:
4595
+ --api-base-url <url> Tapi API base URL
4596
+ --server <url> Alias for --api-base-url
4597
+ --api-key <key> Tapi API key; defaults to TAPI_API_KEY
4598
+ --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
4599
+ --runner-id <id> Runner id for setup; defaults to TAPI_RUNNER_ID
4600
+ --port <n> Local setup UI port; defaults to 17687
4601
+ --no-open Print the setup URL without opening a browser
4602
+ `);
4603
+ }
3591
4604
  function printSessionsHelp() {
3592
4605
  console.log(`Tapi dev session commands
3593
4606
 
@@ -3612,7 +4625,6 @@ function printServicesHelp() {
3612
4625
  Usage:
3613
4626
  tapi services describe <servicemap.run> [--api-base-url URL] [--api-key KEY] [--project PROJECT]
3614
4627
  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
4628
 
3617
4629
  Options:
3618
4630
  --api-base-url <url> Tapi API base URL
@@ -3620,7 +4632,6 @@ Options:
3620
4632
  --api-key <key> Tapi API key; defaults to TAPI_API_KEY
3621
4633
  --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
3622
4634
  --catalog <path> Catalog JSON output path
3623
- --out <path> TypeScript wrapper output path
3624
4635
  `);
3625
4636
  }
3626
4637
  function printTriggersHelp() {
@@ -3643,7 +4654,7 @@ Config:
3643
4654
  serviceRun: "schwab.get_balance",
3644
4655
  interval: "1h",
3645
4656
  inputs: { accountId: "main" },
3646
- runtime: { profileRef: "perm_default" },
4657
+ runtime: { owner: "service_run", profileRef: "perm_default" },
3647
4658
  },
3648
4659
  },
3649
4660
  };
@@ -3756,7 +4767,15 @@ function serializeError(error) {
3756
4767
  }
3757
4768
  function isCliEntrypoint() {
3758
4769
  const invokedPath = process.argv[1];
3759
- return Boolean(invokedPath && resolve(invokedPath) === fileURLToPath(import.meta.url));
4770
+ return Boolean(invokedPath && realCliPath(invokedPath) === realCliPath(fileURLToPath(import.meta.url)));
4771
+ }
4772
+ function realCliPath(path) {
4773
+ try {
4774
+ return realpathSync(path);
4775
+ }
4776
+ catch {
4777
+ return resolve(path);
4778
+ }
3760
4779
  }
3761
4780
  if (isCliEntrypoint()) {
3762
4781
  runCli().then((code) => {