@tapi-dev/sdk 0.1.33 → 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,22 +148,77 @@ export async function runCli(argv = process.argv.slice(2)) {
144
148
  }
145
149
  return runServiceCommand(subcommand, rest);
146
150
  }
147
- if (command === "apis") {
148
- if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h" || hasHelpFlag(rest)) {
149
- printApisHelp();
151
+ if (command === "services") {
152
+ if (!subcommand || subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
153
+ printServicesHelp();
154
+ return 0;
155
+ }
156
+ if (hasHelpFlag(rest)) {
157
+ printServicesHelp();
150
158
  return 0;
151
159
  }
152
160
  if (subcommand === "describe") {
153
- return describeApiOperation(rest);
161
+ return describeServiceOperation(rest);
154
162
  }
155
163
  if (subcommand === "sync") {
156
- return syncApiCatalog(rest);
164
+ return syncServiceCatalog(rest);
165
+ }
166
+ console.error(`Unknown service-run command: ${subcommand}`);
167
+ printServicesHelp();
168
+ return 1;
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;
157
210
  }
158
- if (subcommand === "generate") {
159
- return generateApiClient(rest);
211
+ if (subcommand === "setup") {
212
+ return runRunnerSetup(rest);
160
213
  }
161
- console.error(`Unknown apis command: ${subcommand}`);
162
- printApisHelp();
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();
163
222
  return 1;
164
223
  }
165
224
  if (command === "triggers") {
@@ -168,7 +227,7 @@ export async function runCli(argv = process.argv.slice(2)) {
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();
@@ -306,73 +365,597 @@ export function parseStudioOptions(args) {
306
365
  raw.installDir = resolve(requireOptionValue(args, ++index, "--install-dir"));
307
366
  continue;
308
367
  }
309
- if (arg.startsWith("--install-dir=")) {
310
- raw.installDir = resolve(arg.slice("--install-dir=".length));
368
+ if (arg.startsWith("--install-dir=")) {
369
+ raw.installDir = resolve(arg.slice("--install-dir=".length));
370
+ continue;
371
+ }
372
+ if (arg === "--exe") {
373
+ raw.exePath = resolve(requireOptionValue(args, ++index, "--exe"));
374
+ continue;
375
+ }
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);
311
908
  continue;
312
909
  }
313
- if (arg === "--exe") {
314
- raw.exePath = resolve(requireOptionValue(args, ++index, "--exe"));
910
+ if (arg.startsWith("--server=")) {
911
+ apiBaseUrl = arg.slice("--server=".length);
315
912
  continue;
316
913
  }
317
- if (arg.startsWith("--exe=")) {
318
- raw.exePath = resolve(arg.slice("--exe=".length));
914
+ if (arg === "--api-key") {
915
+ apiKey = requireOptionValue(args, ++index, "--api-key");
319
916
  continue;
320
917
  }
321
- if (arg === "--download-only") {
322
- raw.downloadOnly = true;
918
+ if (arg.startsWith("--api-key=")) {
919
+ apiKey = arg.slice("--api-key=".length);
323
920
  continue;
324
921
  }
325
- if (arg === "--silent") {
326
- raw.silent = true;
922
+ if (arg.startsWith("--")) {
923
+ throw new Error(`Unknown tapp queue add option: ${arg}`);
924
+ }
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 parseApiOptions(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 parseApiOptions(args) {
406
989
  continue;
407
990
  }
408
991
  if (arg.startsWith("--")) {
409
- throw new Error(`Unknown apis 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 apis argument: ${arg}`);
416
- }
417
- if (!operation) {
418
- throw new Error("apis describe requires an operation 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("apis 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("apis 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 describeApiOperation(args) {
1032
+ async function createQueue(args) {
436
1033
  try {
437
- const { operation, options } = parseApiOptions(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 API description 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 describeApiOperation(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.generated?.catalog || ".tapi/generated/catalog.json";
458
- let typescriptPath = workspace?.config.generated?.typescript || "src/tapi.generated.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();
1171
+ continue;
1172
+ }
1173
+ if (arg === "--port") {
1174
+ port = parseNonNegativeInteger(requireOptionValue(args, ++index, "--port"), "--port");
497
1175
  continue;
498
1176
  }
499
- if (arg === "--out") {
500
- typescriptPath = requireOptionValue(args, ++index, "--out");
1177
+ if (arg.startsWith("--port=")) {
1178
+ port = parseNonNegativeInteger(arg.slice("--port=".length), "--port");
501
1179
  continue;
502
1180
  }
503
- if (arg.startsWith("--out=")) {
504
- typescriptPath = arg.slice("--out=".length);
1181
+ if (arg === "--no-open") {
1182
+ openBrowser = false;
505
1183
  continue;
506
1184
  }
507
- throw new Error(`Unknown apis option: ${arg}`);
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("apis 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("apis 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 syncApiCatalog(args) {
1215
+ async function runRunnerSetup(args) {
525
1216
  try {
526
- const options = parseApiWorkspaceOptions(args);
527
- const catalog = await withCliSpinner("Fetching Tapi API catalog", () => fetchApiCatalog(options));
528
- await writeJsonFile(options.catalogPath, catalog);
529
- console.log(`Synced Tapi API 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 syncApiCatalog(args) {
534
1230
  return 1;
535
1231
  }
536
1232
  }
537
- async function generateApiClient(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 API catalog", () => fetchApiCatalog(options));
541
- await writeJsonFile(options.catalogPath, catalog);
542
- await writeTextFile(options.typescriptPath, renderGeneratedApiClient(catalog));
543
- console.log(`Generated Tapi API 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,24 +1503,25 @@ 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
- throw new Error(`No Tapi API triggers found in ${options.configPath}.`);
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
- await withCliSpinner(`Syncing ${triggers.length} Tapi API trigger(s)`, async () => {
1519
+ await withCliSpinner(`Syncing ${triggers.length} Tapi service trigger(s)`, async () => {
626
1520
  for (const trigger of triggers) {
627
1521
  await client.triggers.create(trigger);
628
1522
  }
629
1523
  });
630
- console.log(`Synced ${triggers.length} Tapi API trigger(s) from ${options.configPath}.`);
1524
+ console.log(`Synced ${triggers.length} Tapi service trigger(s) from ${options.configPath}.`);
631
1525
  return 0;
632
1526
  }
633
1527
  catch (error) {
@@ -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,24 +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 apiRequest = optionalStringField(entry.apiRequest, "apiRequest") ??
1055
- optionalStringField(entry.api, "api") ??
1056
- apiRequestFromParts(entry.apiName, entry.requestKey);
1057
- if (!apiRequest) {
1058
- throw new Error(`Trigger '${name}' requires apiRequest, api, or apiName/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");
1951
+ if (!serviceRun) {
1952
+ throw new Error(`Trigger '${name}' requires serviceRun.`);
1059
1953
  }
1060
- if (!apiRequest.includes(".")) {
1061
- throw new Error(`Trigger '${name}' apiRequest must be formatted as '<apiName>.<requestKey>'.`);
1954
+ if (!serviceRun.includes(".")) {
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.`);
1062
1959
  }
1063
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`);
1064
1964
  const request = {
1065
1965
  name,
1066
- apiRequest,
1966
+ serviceRun,
1967
+ queueId,
1067
1968
  ...(entry.enabled === undefined ? {} : { enabled: booleanField(entry.enabled, `Trigger '${name}' enabled`) }),
1068
1969
  ...(schedule ? { schedule } : {}),
1069
1970
  ...(entry.inputs === undefined ? {} : { inputs: recordField(entry.inputs, `Trigger '${name}' inputs`) }),
1070
- ...(entry.runtime === undefined ? {} : { runtime: recordField(entry.runtime, `Trigger '${name}' runtime`) }),
1071
- ...(entry.runnerId === undefined ? {} : { runnerId: stringField(entry.runnerId, `Trigger '${name}' runnerId`) }),
1971
+ ...(runtime === undefined ? {} : { runtime }),
1072
1972
  ...(entry.priority === undefined ? {} : { priority: numberField(entry.priority, `Trigger '${name}' priority`) }),
1073
1973
  ...(entry.site === undefined ? {} : { site: stringField(entry.site, `Trigger '${name}' site`) }),
1074
1974
  };
@@ -1151,11 +2051,6 @@ function parseDurationSeconds(value, fieldName) {
1151
2051
  }
1152
2052
  return seconds;
1153
2053
  }
1154
- function apiRequestFromParts(apiName, requestKey) {
1155
- const api = optionalStringField(apiName, "apiName");
1156
- const request = optionalStringField(requestKey, "requestKey");
1157
- return api && request ? `${api}.${request}` : undefined;
1158
- }
1159
2054
  function stringField(value, fieldName) {
1160
2055
  if (typeof value !== "string" || !value.trim()) {
1161
2056
  throw new Error(`${fieldName} must be a non-empty string.`);
@@ -1186,10 +2081,30 @@ function recordField(value, fieldName) {
1186
2081
  }
1187
2082
  return value;
1188
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
+ }
1189
2103
  async function fetchApiCatalog(options) {
1190
2104
  const client = new TapiClient({
1191
2105
  baseUrl: options.apiBaseUrl,
1192
2106
  apiKey: options.apiKey,
2107
+ tappId: options.projectId,
1193
2108
  projectId: options.projectId,
1194
2109
  });
1195
2110
  return client.catalog.get();
@@ -1201,50 +2116,6 @@ async function writeTextFile(path, content) {
1201
2116
  await mkdir(dirname(path), { recursive: true });
1202
2117
  await writeFile(path, content, "utf8");
1203
2118
  }
1204
- function renderGeneratedApiClient(catalog) {
1205
- const namespaces = new Map();
1206
- for (const api of catalog.apis || []) {
1207
- const namespace = safeIdentifier(api.name || "api");
1208
- for (const request of api.requests || []) {
1209
- const key = String(request.operation || request.sdkName || request.key || "").trim();
1210
- if (!key)
1211
- continue;
1212
- const operationName = safeIdentifier(key);
1213
- const operation = `${api.name}.${key}`;
1214
- const items = namespaces.get(namespace) ?? [];
1215
- items.push({ key: operation, operationName });
1216
- namespaces.set(namespace, items);
1217
- }
1218
- }
1219
- const namespaceBlocks = [...namespaces.entries()].map(([namespace, operations]) => {
1220
- const lines = operations.map(({ key, operationName }) => ` ${operationName}: (inputs: Record<string, unknown> = {}, options: GeneratedRunOptions = {}) => client.services.run(${JSON.stringify(key)}, { ...options, inputs }),`);
1221
- return ` ${namespace}: {\n${lines.join("\n")}\n },`;
1222
- });
1223
- return `/* Generated by Tapi. Do not edit by hand. */
1224
- import { TapiClient, type TapiClientOptions, type RuntimeRunOptions } from "@tapi-dev/sdk";
1225
-
1226
- export interface GeneratedRunOptions {
1227
- runtime?: RuntimeRunOptions;
1228
- priority?: number;
1229
- runnerId?: string;
1230
- idempotencyKey?: string;
1231
- site?: string;
1232
- }
1233
-
1234
- export function createTapiGeneratedClient(options: TapiClientOptions) {
1235
- const client = new TapiClient(options);
1236
- return {
1237
- ${namespaceBlocks.join("\n")}
1238
- };
1239
- }
1240
- `;
1241
- }
1242
- function safeIdentifier(value) {
1243
- const cleaned = String(value || "api")
1244
- .replace(/[^a-zA-Z0-9_$]+/g, "_")
1245
- .replace(/^([^a-zA-Z_$])/, "_$1");
1246
- return cleaned || "api";
1247
- }
1248
2119
  function isRecord(value) {
1249
2120
  return typeof value === "object" && value !== null && !Array.isArray(value);
1250
2121
  }
@@ -2444,31 +3315,31 @@ async function stopPortableStudioServerProcesses() {
2444
3315
  if (process.platform !== "win32") {
2445
3316
  return 0;
2446
3317
  }
2447
- const script = `
2448
- $ErrorActionPreference = 'SilentlyContinue'
2449
- $currentPid = $PID
2450
- $processes = @(Get-Process -ErrorAction SilentlyContinue | Where-Object {
2451
- $pidValue = [int]$_.Id
2452
- if ($pidValue -le 0 -or $pidValue -eq $currentPid) {
2453
- $false
2454
- } else {
2455
- $path = ''
2456
- try { $path = [string]$_.Path } catch {}
2457
- $name = [string]$_.ProcessName
2458
- (
2459
- $name -ieq 'tapi-studio-server' -or
2460
- $path -match '\\\\Tapi\\\\Studio\\\\server\\\\releases\\\\'
2461
- )
2462
- }
2463
- })
2464
- $stopped = 0
2465
- foreach ($process in $processes) {
2466
- try {
2467
- Stop-Process -Id ([int]$process.Id) -Force -ErrorAction Stop
2468
- $stopped += 1
2469
- } catch {}
2470
- }
2471
- [pscustomobject]@{ stopped = $stopped } | ConvertTo-Json -Compress
3318
+ const script = `
3319
+ $ErrorActionPreference = 'SilentlyContinue'
3320
+ $currentPid = $PID
3321
+ $processes = @(Get-Process -ErrorAction SilentlyContinue | Where-Object {
3322
+ $pidValue = [int]$_.Id
3323
+ if ($pidValue -le 0 -or $pidValue -eq $currentPid) {
3324
+ $false
3325
+ } else {
3326
+ $path = ''
3327
+ try { $path = [string]$_.Path } catch {}
3328
+ $name = [string]$_.ProcessName
3329
+ (
3330
+ $name -ieq 'tapi-studio-server' -or
3331
+ $path -match '\\\\Tapi\\\\Studio\\\\server\\\\releases\\\\'
3332
+ )
3333
+ }
3334
+ })
3335
+ $stopped = 0
3336
+ foreach ($process in $processes) {
3337
+ try {
3338
+ Stop-Process -Id ([int]$process.Id) -Force -ErrorAction Stop
3339
+ $stopped += 1
3340
+ } catch {}
3341
+ }
3342
+ [pscustomobject]@{ stopped = $stopped } | ConvertTo-Json -Compress
2472
3343
  `;
2473
3344
  try {
2474
3345
  const output = await runProcessCapture("powershell.exe", [
@@ -2992,6 +3863,56 @@ function openBrowser(url) {
2992
3863
  });
2993
3864
  child.unref();
2994
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
+ }
2995
3916
  async function readJsonBody(response) {
2996
3917
  const text = await response.text();
2997
3918
  if (!text.trim()) {
@@ -3004,6 +3925,40 @@ async function readJsonBody(response) {
3004
3925
  return null;
3005
3926
  }
3006
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
+ }
3007
3962
  function isApprovalTerminalError(error) {
3008
3963
  return error instanceof StudioInstallApprovalError
3009
3964
  && (error.code === "pending_approval" || error.code === "access_pending" || error.code === "access_rejected");
@@ -3558,11 +4513,16 @@ Usage:
3558
4513
  tapi link --project PROJECT
3559
4514
  tapi studio install [--channel pilot] [--api-base-url URL]
3560
4515
  tapi studio
3561
- tapi studio open
3562
- tapi studio doctor
3563
- tapi apis describe <namespace.operation>
3564
- tapi apis sync
3565
- tapi apis generate
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>
4525
+ tapi services sync
3566
4526
  tapi triggers sync
3567
4527
  tapi sessions
3568
4528
  tapi service status
@@ -3572,18 +4532,75 @@ Commands:
3572
4532
  init Create .tapi/project.json for this repo
3573
4533
  link Rebind this repo to an existing Tapi project
3574
4534
  studio install Download, verify, and run the Tapi Studio installer
3575
- studio Open Tapi Studio for this repo
3576
- studio open Open Tapi Studio for this repo
3577
- studio doctor Check local SDK and Studio release configuration
3578
- apis describe Print a generated website API input/output contract
3579
- apis sync Save the published API catalog to .tapi/generated
3580
- apis generate Generate a TypeScript runtime wrapper from the catalog
3581
- triggers sync Upsert API-call triggers from tapi.config
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
4546
+ Print a ServiceMap service-run input/output contract
4547
+ services sync Save the service-run catalog to .tapi/services
4548
+ triggers sync Upsert service triggers from tapi.config
3582
4549
  sessions List dev-mode API sessions and open takeover sessions
3583
4550
  service Inspect or control the local Tapi Windows service
3584
4551
  doctor Alias for studio doctor
3585
4552
  `);
3586
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
+ }
3587
4604
  function printSessionsHelp() {
3588
4605
  console.log(`Tapi dev session commands
3589
4606
 
@@ -3602,13 +4619,12 @@ Options:
3602
4619
  --no-interactive Print the grouped list without the arrow-key picker
3603
4620
  `);
3604
4621
  }
3605
- function printApisHelp() {
3606
- console.log(`Tapi generated website API commands
4622
+ function printServicesHelp() {
4623
+ console.log(`Tapi ServiceMap service-run commands
3607
4624
 
3608
4625
  Usage:
3609
- tapi apis describe <namespace.operation> [--api-base-url URL] [--api-key KEY] [--project PROJECT]
3610
- tapi apis sync [--api-base-url URL] [--api-key KEY] [--project PROJECT]
3611
- tapi apis generate [--api-base-url URL] [--api-key KEY] [--project PROJECT] [--out FILE]
4626
+ tapi services describe <servicemap.run> [--api-base-url URL] [--api-key KEY] [--project PROJECT]
4627
+ tapi services sync [--api-base-url URL] [--api-key KEY] [--project PROJECT]
3612
4628
 
3613
4629
  Options:
3614
4630
  --api-base-url <url> Tapi API base URL
@@ -3616,11 +4632,10 @@ Options:
3616
4632
  --api-key <key> Tapi API key; defaults to TAPI_API_KEY
3617
4633
  --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
3618
4634
  --catalog <path> Catalog JSON output path
3619
- --out <path> Generated TypeScript output path
3620
4635
  `);
3621
4636
  }
3622
4637
  function printTriggersHelp() {
3623
- console.log(`Tapi API trigger commands
4638
+ console.log(`Tapi service trigger commands
3624
4639
 
3625
4640
  Usage:
3626
4641
  tapi triggers sync [--config FILE] [--api-base-url URL] [--api-key KEY] [--project PROJECT]
@@ -3636,10 +4651,10 @@ Config:
3636
4651
  export default {
3637
4652
  triggers: {
3638
4653
  nightlyBalance: {
3639
- apiRequest: "schwab.get_balance",
4654
+ serviceRun: "schwab.get_balance",
3640
4655
  interval: "1h",
3641
4656
  inputs: { accountId: "main" },
3642
- runtime: { profileRef: "perm_default" },
4657
+ runtime: { owner: "service_run", profileRef: "perm_default" },
3643
4658
  },
3644
4659
  },
3645
4660
  };
@@ -3752,7 +4767,15 @@ function serializeError(error) {
3752
4767
  }
3753
4768
  function isCliEntrypoint() {
3754
4769
  const invokedPath = process.argv[1];
3755
- 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
+ }
3756
4779
  }
3757
4780
  if (isCliEntrypoint()) {
3758
4781
  runCli().then((code) => {