@tapi-dev/sdk 0.1.17 → 0.1.21

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
@@ -8,6 +8,7 @@ import { mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "n
8
8
  import { homedir } from "node:os";
9
9
  import { basename, dirname, join, resolve } from "node:path";
10
10
  import { performance } from "node:perf_hooks";
11
+ import { emitKeypressEvents } from "node:readline";
11
12
  import { Readable } from "node:stream";
12
13
  import { pipeline } from "node:stream/promises";
13
14
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -174,6 +175,23 @@ export async function runCli(argv = process.argv.slice(2)) {
174
175
  printTriggersHelp();
175
176
  return 1;
176
177
  }
178
+ if (command === "sessions") {
179
+ const sessionsCommand = subcommand && !subcommand.startsWith("-") ? subcommand : "list";
180
+ const sessionsArgs = subcommand && !subcommand.startsWith("-") ? rest : [subcommand, ...rest].filter(Boolean);
181
+ if (sessionsCommand === "help" || sessionsCommand === "--help" || sessionsCommand === "-h" || hasHelpFlag(sessionsArgs)) {
182
+ printSessionsHelp();
183
+ return 0;
184
+ }
185
+ if (sessionsCommand === "list") {
186
+ return listDevSessions(sessionsArgs);
187
+ }
188
+ if (sessionsCommand === "open") {
189
+ return openDevSession(sessionsArgs);
190
+ }
191
+ console.error(`Unknown sessions command: ${sessionsCommand}`);
192
+ printSessionsHelp();
193
+ return 1;
194
+ }
177
195
  if (command !== "studio") {
178
196
  console.error(`Unknown command: ${command}`);
179
197
  printHelp();
@@ -615,6 +633,350 @@ async function syncApiTriggers(args) {
615
633
  return 1;
616
634
  }
617
635
  }
636
+ function parseSessionsOptions(args) {
637
+ const workspace = loadWorkspace();
638
+ let apiBaseUrl = envString("TAPI_BASE_URL") || workspace?.apiBaseUrl || DEFAULT_STUDIO_API_BASE_URL;
639
+ let apiKey = envString("TAPI_API_KEY") || "";
640
+ let projectId = envString("TAPI_PROJECT_ID") || workspace?.projectId || "";
641
+ let site = "";
642
+ let sessionId = "";
643
+ let json = false;
644
+ let noInteractive = false;
645
+ for (let index = 0; index < args.length; index += 1) {
646
+ const arg = args[index];
647
+ if (!arg)
648
+ continue;
649
+ if (arg === "--api-base-url" || arg === "--server") {
650
+ apiBaseUrl = requireOptionValue(args, ++index, arg);
651
+ continue;
652
+ }
653
+ if (arg.startsWith("--api-base-url=")) {
654
+ apiBaseUrl = arg.slice("--api-base-url=".length);
655
+ continue;
656
+ }
657
+ if (arg.startsWith("--server=")) {
658
+ apiBaseUrl = arg.slice("--server=".length);
659
+ continue;
660
+ }
661
+ if (arg === "--api-key") {
662
+ apiKey = requireOptionValue(args, ++index, "--api-key");
663
+ continue;
664
+ }
665
+ if (arg.startsWith("--api-key=")) {
666
+ apiKey = arg.slice("--api-key=".length);
667
+ continue;
668
+ }
669
+ if (arg === "--project") {
670
+ projectId = normalizeProjectValue(requireOptionValue(args, ++index, "--project"), "project");
671
+ continue;
672
+ }
673
+ if (arg.startsWith("--project=")) {
674
+ projectId = normalizeProjectValue(arg.slice("--project=".length), "project");
675
+ continue;
676
+ }
677
+ if (arg === "--site") {
678
+ site = requireOptionValue(args, ++index, "--site").trim();
679
+ continue;
680
+ }
681
+ if (arg.startsWith("--site=")) {
682
+ site = arg.slice("--site=".length).trim();
683
+ continue;
684
+ }
685
+ if (arg === "--json") {
686
+ json = true;
687
+ noInteractive = true;
688
+ continue;
689
+ }
690
+ if (arg === "--no-interactive") {
691
+ noInteractive = true;
692
+ continue;
693
+ }
694
+ if (arg.startsWith("--")) {
695
+ throw new Error(`Unknown sessions option: ${arg}`);
696
+ }
697
+ if (!sessionId) {
698
+ sessionId = arg;
699
+ continue;
700
+ }
701
+ throw new Error(`Unexpected sessions argument: ${arg}`);
702
+ }
703
+ if (!apiKey) {
704
+ throw new Error("sessions command requires --api-key or TAPI_API_KEY.");
705
+ }
706
+ if (!projectId) {
707
+ throw new Error("sessions command requires --project, TAPI_PROJECT_ID, or .tapi/project.json.");
708
+ }
709
+ return {
710
+ sessionId,
711
+ options: {
712
+ apiBaseUrl: normalizeHttpUrl(apiBaseUrl, "Tapi API base URL"),
713
+ apiKey,
714
+ projectId,
715
+ site: site || undefined,
716
+ json,
717
+ noInteractive,
718
+ },
719
+ };
720
+ }
721
+ async function listDevSessions(args) {
722
+ try {
723
+ const { options } = parseSessionsOptions(args);
724
+ const sessions = await fetchDevSessions(options);
725
+ if (options.json) {
726
+ console.log(JSON.stringify(sessions, null, 2));
727
+ return 0;
728
+ }
729
+ const rows = flattenDevSessions(sessions);
730
+ if (rows.length === 0) {
731
+ console.log(`No Tapi dev sessions found for project ${sessions.projectId || options.projectId}.`);
732
+ return 0;
733
+ }
734
+ if (!options.noInteractive && process.stdin.isTTY && process.stdout.isTTY && rows.some((session) => session.openable)) {
735
+ return await runSessionsPicker(sessions, options);
736
+ }
737
+ console.log(renderDevSessions(sessions));
738
+ const openable = rows.find((session) => session.openable);
739
+ if (openable) {
740
+ console.log("\nOpen a takeover session with `tapi sessions open <session-id>`.");
741
+ }
742
+ return 0;
743
+ }
744
+ catch (error) {
745
+ console.error(formatError(error));
746
+ return 1;
747
+ }
748
+ }
749
+ async function openDevSession(args) {
750
+ try {
751
+ const { sessionId, options } = parseSessionsOptions(args);
752
+ if (!sessionId) {
753
+ throw new Error("sessions open requires a session id.");
754
+ }
755
+ const sessions = await fetchDevSessions(options);
756
+ const session = findDevSession(sessions, sessionId);
757
+ if (!session) {
758
+ throw new Error(`Tapi session not found: ${sessionId}`);
759
+ }
760
+ return await openDevSessionInStudio(session, options);
761
+ }
762
+ catch (error) {
763
+ console.error(formatError(error));
764
+ return 1;
765
+ }
766
+ }
767
+ async function fetchDevSessions(options) {
768
+ const client = new TapiClient({
769
+ baseUrl: options.apiBaseUrl,
770
+ apiKey: options.apiKey,
771
+ projectId: options.projectId,
772
+ });
773
+ return await withCliSpinner("Fetching Tapi dev sessions", () => client.sessions.list({ site: options.site }));
774
+ }
775
+ function flattenDevSessions(sessions) {
776
+ return (sessions.groups || []).flatMap((group) => group.sessions || []);
777
+ }
778
+ function findDevSession(sessions, sessionId) {
779
+ const normalized = sessionId.trim();
780
+ return flattenDevSessions(sessions).find((session) => session.id === normalized || session.id.startsWith(normalized));
781
+ }
782
+ async function runSessionsPicker(sessions, options) {
783
+ let currentSessions = sessions;
784
+ let rows = flattenDevSessions(currentSessions);
785
+ let openableIndexes = openableSessionIndexes(rows);
786
+ if (openableIndexes.length === 0) {
787
+ console.log(renderDevSessions(currentSessions));
788
+ return 0;
789
+ }
790
+ let selectedIndex = openableIndexes[0];
791
+ const input = process.stdin;
792
+ return await new Promise((resolvePromise) => {
793
+ let onKeypress;
794
+ const cleanup = () => {
795
+ input.off("keypress", onKeypress);
796
+ input.setRawMode?.(false);
797
+ input.pause();
798
+ };
799
+ const move = (direction) => {
800
+ if (openableIndexes.length === 0)
801
+ return;
802
+ const currentOpenableIndex = openableIndexes.indexOf(selectedIndex);
803
+ const nextOpenableIndex = (currentOpenableIndex + direction + openableIndexes.length) % openableIndexes.length;
804
+ selectedIndex = openableIndexes[nextOpenableIndex];
805
+ render();
806
+ };
807
+ const render = () => {
808
+ process.stdout.write("\x1Bc");
809
+ process.stdout.write(renderDevSessions(currentSessions, rows[selectedIndex]?.id));
810
+ process.stdout.write("\n\nUse Up/Down and Enter to open Studio. Press r to refresh, q to exit.\n");
811
+ };
812
+ const finish = (code) => {
813
+ cleanup();
814
+ resolvePromise(code);
815
+ };
816
+ onKeypress = (_input, key = {}) => {
817
+ if (key.ctrl && key.name === "c") {
818
+ finish(130);
819
+ return;
820
+ }
821
+ if (key.name === "q" || key.name === "escape") {
822
+ finish(0);
823
+ return;
824
+ }
825
+ if (key.name === "up") {
826
+ move(-1);
827
+ return;
828
+ }
829
+ if (key.name === "down") {
830
+ move(1);
831
+ return;
832
+ }
833
+ if (key.name === "r") {
834
+ fetchDevSessions(options)
835
+ .then((nextSessions) => {
836
+ currentSessions = nextSessions;
837
+ rows = flattenDevSessions(currentSessions);
838
+ openableIndexes = openableSessionIndexes(rows);
839
+ if (!rows[selectedIndex]?.openable) {
840
+ selectedIndex = openableIndexes[0] ?? 0;
841
+ }
842
+ render();
843
+ })
844
+ .catch((error) => {
845
+ process.stderr.write(`\n${formatError(error)}\n`);
846
+ render();
847
+ });
848
+ return;
849
+ }
850
+ if (key.name === "return" || key.name === "enter") {
851
+ const session = rows[selectedIndex];
852
+ if (!session?.openable) {
853
+ return;
854
+ }
855
+ cleanup();
856
+ openDevSessionInStudio(session, options)
857
+ .then(resolvePromise)
858
+ .catch((error) => {
859
+ console.error(formatError(error));
860
+ resolvePromise(1);
861
+ });
862
+ }
863
+ };
864
+ emitKeypressEvents(input);
865
+ input.setRawMode?.(true);
866
+ input.resume();
867
+ input.on("keypress", onKeypress);
868
+ render();
869
+ });
870
+ }
871
+ function openableSessionIndexes(rows) {
872
+ return rows
873
+ .map((session, index) => (session.openable ? index : -1))
874
+ .filter((index) => index >= 0);
875
+ }
876
+ async function openDevSessionInStudio(session, options) {
877
+ if (!session.openable) {
878
+ throw new Error(`Tapi session ${session.id} is not awaiting developer takeover.`);
879
+ }
880
+ const params = studioSessionLaunchParams(session);
881
+ const studioOptions = parseStudioOptions([
882
+ "--api-base-url",
883
+ options.apiBaseUrl,
884
+ "--project",
885
+ options.projectId,
886
+ ]);
887
+ studioOptions.launchUrlQuery = params.toString();
888
+ const selected = await trySelectDevSessionInOpenStudio(session, studioOptions);
889
+ if (selected.selected) {
890
+ console.log(`Selected Tapi Studio session in existing Studio: ${selected.baseUrl}`);
891
+ return 0;
892
+ }
893
+ return await openStudio(studioOptions);
894
+ }
895
+ function studioSessionLaunchParams(session) {
896
+ const params = new URLSearchParams();
897
+ params.set("runId", session.id);
898
+ params.set("apiRunId", session.id);
899
+ if (session.sitemap)
900
+ params.set("sitemap", session.sitemap);
901
+ if (session.runtimeSessionId)
902
+ params.set("runtimeSessionId", session.runtimeSessionId);
903
+ return params;
904
+ }
905
+ function studioSessionSelectIntent(session) {
906
+ return {
907
+ sessionId: session.id,
908
+ runId: session.id,
909
+ apiRunId: session.id,
910
+ ...(session.sitemap ? { sitemap: session.sitemap } : {}),
911
+ ...(session.runtimeSessionId ? { runtimeSessionId: session.runtimeSessionId } : {}),
912
+ source: "cli",
913
+ selectedAt: new Date().toISOString(),
914
+ };
915
+ }
916
+ export async function trySelectDevSessionInOpenStudio(session, options, fetchImpl = fetch) {
917
+ const record = await readStudioInstanceRecord(options);
918
+ if (!record) {
919
+ return { selected: false, reason: "instance_record_missing" };
920
+ }
921
+ try {
922
+ const identityResponse = await fetchWithTimeout(`${record.baseUrl.replace(/\/+$/, "")}/api/studio/control/identity`, { headers: { Accept: "application/json" } }, 1_000, fetchImpl);
923
+ if (!identityResponse.ok) {
924
+ await unlinkIfExists(studioInstanceRecordPathForOptions(options));
925
+ return { selected: false, reason: `identity_http_${identityResponse.status}` };
926
+ }
927
+ const identity = await readJsonBody(identityResponse);
928
+ if (!studioInstanceIdentityMatches(identity, options)) {
929
+ return { selected: false, reason: "identity_mismatch" };
930
+ }
931
+ const selectResponse = await fetchWithTimeout(`${record.baseUrl.replace(/\/+$/, "")}/api/studio/control/select-session`, {
932
+ method: "POST",
933
+ headers: {
934
+ Accept: "application/json",
935
+ "Content-Type": "application/json",
936
+ },
937
+ body: JSON.stringify(studioSessionSelectIntent(session)),
938
+ }, 1_500, fetchImpl);
939
+ if (!selectResponse.ok) {
940
+ return { selected: false, reason: `select_http_${selectResponse.status}` };
941
+ }
942
+ return { selected: true, baseUrl: record.baseUrl };
943
+ }
944
+ catch (error) {
945
+ return { selected: false, reason: formatError(error) };
946
+ }
947
+ }
948
+ function renderDevSessions(sessions, selectedId = "") {
949
+ const lines = [`Tapi sessions for project ${sessions.projectId || "unknown"}`];
950
+ for (const group of sessions.groups || []) {
951
+ lines.push("");
952
+ lines.push(`[${group.sitemap || "unknown"}]`);
953
+ if (!group.sessions || group.sessions.length === 0) {
954
+ lines.push(" No sessions.");
955
+ continue;
956
+ }
957
+ for (const session of group.sessions) {
958
+ const selected = selectedId && session.id === selectedId ? ">" : " ";
959
+ const openable = session.openable ? "*" : " ";
960
+ const id = truncateText(session.id || "", 18).padEnd(18, " ");
961
+ const status = truncateText(String(session.status || ""), 24).padEnd(24, " ");
962
+ const apiName = session.apiName && session.requestKey
963
+ ? `${session.apiName}.${session.requestKey}`
964
+ : session.apiName || session.requestKey || "unknown";
965
+ const state = session.stateLabel ? ` state=${session.stateLabel}` : "";
966
+ const url = session.currentUrl ? ` ${truncateText(session.currentUrl, 60)}` : "";
967
+ lines.push(`${selected}${openable} ${id} ${status} ${apiName}${state}${url}`);
968
+ }
969
+ }
970
+ lines.push("");
971
+ lines.push("* openable awaiting-takeover session");
972
+ return lines.join("\n");
973
+ }
974
+ function truncateText(value, maxLength) {
975
+ if (value.length <= maxLength) {
976
+ return value;
977
+ }
978
+ return `${value.slice(0, Math.max(0, maxLength - 3))}...`;
979
+ }
618
980
  function defaultTriggerConfigPath(root) {
619
981
  for (const name of ["tapi.config.ts", "tapi.config.mjs", "tapi.config.js", "tapi.config.cjs", "tapi.config.json"]) {
620
982
  const path = join(root, name);
@@ -856,23 +1218,23 @@ function renderGeneratedApiClient(catalog) {
856
1218
  const lines = operations.map(({ key, operationName }) => ` ${operationName}: (inputs: Record<string, unknown> = {}, options: GeneratedRunOptions = {}) => client.websiteApis.run(${JSON.stringify(key)}, { ...options, inputs }),`);
857
1219
  return ` ${namespace}: {\n${lines.join("\n")}\n },`;
858
1220
  });
859
- return `/* Generated by Tapi. Do not edit by hand. */
860
- import { TapiClient, type TapiClientOptions, type RuntimeRunOptions } from "@tapi-dev/sdk";
861
-
862
- export interface GeneratedRunOptions {
863
- runtime?: RuntimeRunOptions;
864
- priority?: number;
865
- runnerId?: string;
866
- idempotencyKey?: string;
867
- site?: string;
868
- }
869
-
870
- export function createTapiGeneratedClient(options: TapiClientOptions) {
871
- const client = new TapiClient(options);
872
- return {
873
- ${namespaceBlocks.join("\n")}
874
- };
875
- }
1221
+ return `/* Generated by Tapi. Do not edit by hand. */
1222
+ import { TapiClient, type TapiClientOptions, type RuntimeRunOptions } from "@tapi-dev/sdk";
1223
+
1224
+ export interface GeneratedRunOptions {
1225
+ runtime?: RuntimeRunOptions;
1226
+ priority?: number;
1227
+ runnerId?: string;
1228
+ idempotencyKey?: string;
1229
+ site?: string;
1230
+ }
1231
+
1232
+ export function createTapiGeneratedClient(options: TapiClientOptions) {
1233
+ const client = new TapiClient(options);
1234
+ return {
1235
+ ${namespaceBlocks.join("\n")}
1236
+ };
1237
+ }
876
1238
  `;
877
1239
  }
878
1240
  function safeIdentifier(value) {
@@ -1889,7 +2251,7 @@ async function openStudio(options) {
1889
2251
  }
1890
2252
  if (!exePath || !existsSync(exePath)) {
1891
2253
  console.error("Tapi Studio executable was not found.");
1892
- console.error("Run `npx tapi studio install --channel pilot`, or set TAPI_STUDIO_EXE to the installed executable path.");
2254
+ console.error("Run `tapi studio install --channel pilot`, or set TAPI_STUDIO_EXE to the installed executable path.");
1893
2255
  return 1;
1894
2256
  }
1895
2257
  const child = spawn(exePath, [], {
@@ -2004,6 +2366,9 @@ async function launchPortableStudioServer(exePath, options, manifest) {
2004
2366
  env.PLAYWRIGHT_BROWSERS_PATH = playwrightBrowsersPath;
2005
2367
  }
2006
2368
  const url = `http://${host}:${port}`;
2369
+ const launchQuery = options.launchUrlQuery ? options.launchUrlQuery.replace(/^\?+/, "") : "";
2370
+ const launchUrl = launchQuery ? `${url}/?${launchQuery}` : url;
2371
+ let childPid = 0;
2007
2372
  await withCliSpinner("Starting Tapi Studio", async () => {
2008
2373
  const child = spawn(exePath, [], {
2009
2374
  detached: true,
@@ -2011,11 +2376,25 @@ async function launchPortableStudioServer(exePath, options, manifest) {
2011
2376
  stdio: "ignore",
2012
2377
  windowsHide: true,
2013
2378
  });
2379
+ childPid = child.pid ?? 0;
2014
2380
  child.unref();
2015
2381
  await waitForHttpOk(`${url}/healthz`, PORTABLE_STUDIO_SERVER_READY_TIMEOUT_MS, PORTABLE_STUDIO_SERVER_READY_INTERVAL_MS);
2016
2382
  });
2017
- openBrowser(url);
2018
- console.log(`Opened Tapi Studio ${manifest.version}: ${url}`);
2383
+ await writeStudioInstanceRecord(options, {
2384
+ version: 1,
2385
+ baseUrl: url,
2386
+ host,
2387
+ port,
2388
+ pid: childPid || undefined,
2389
+ apiBaseUrl: options.apiBaseUrl,
2390
+ projectId: options.projectId,
2391
+ workspaceRoot: options.workspaceRoot,
2392
+ workspaceConfig: options.workspace?.configPath,
2393
+ manifestVersion: manifest.version,
2394
+ updatedAt: new Date().toISOString(),
2395
+ });
2396
+ openBrowser(launchUrl);
2397
+ console.log(`Opened Tapi Studio ${manifest.version}: ${launchUrl}`);
2019
2398
  }
2020
2399
  export function findPortablePlaywrightBrowsersDir(exePath) {
2021
2400
  const exeDir = dirname(exePath);
@@ -2061,6 +2440,19 @@ export async function waitForHttpOk(url, timeoutMs = PORTABLE_STUDIO_SERVER_READ
2061
2440
  const suffix = lastError ? ` Last error: ${lastError}.` : "";
2062
2441
  throw new Error(`Tapi Studio server did not become ready at ${url} within ${Math.ceil(timeoutMs / 1000)}s.${suffix}`);
2063
2442
  }
2443
+ async function fetchWithTimeout(url, init, timeoutMs, fetchImpl = fetch) {
2444
+ const controller = new AbortController();
2445
+ const timeout = setTimeout(() => controller.abort(), Math.max(1, timeoutMs));
2446
+ try {
2447
+ return await fetchImpl(url, {
2448
+ ...init,
2449
+ signal: controller.signal,
2450
+ });
2451
+ }
2452
+ finally {
2453
+ clearTimeout(timeout);
2454
+ }
2455
+ }
2064
2456
  function delay(ms) {
2065
2457
  return new Promise((resolvePromise) => {
2066
2458
  setTimeout(resolvePromise, Math.max(0, ms));
@@ -2184,7 +2576,7 @@ async function requestStudioInstallToken(apiBaseUrl, channel, firebaseIdToken, f
2184
2576
  if (!response.ok) {
2185
2577
  const detail = typeof responseBody?.detail === "string" ? responseBody.detail : "";
2186
2578
  if (detail === "pending_approval" || detail === "access_pending") {
2187
- throw new StudioInstallApprovalError("Access request submitted. Check your email for approval, then rerun `npx tapi studio install`.", detail, response.status);
2579
+ throw new StudioInstallApprovalError("Access request submitted. Check your email for approval, then rerun `tapi studio install`.", detail, response.status);
2188
2580
  }
2189
2581
  if (detail === "access_rejected") {
2190
2582
  throw new StudioInstallApprovalError("Studio install access was rejected. Contact the Tapi admin if this is unexpected.", detail, response.status);
@@ -2371,6 +2763,81 @@ async function writeStudioAuthCache(credentials) {
2371
2763
  function getStudioAuthCachePath() {
2372
2764
  return join(getDefaultTapiDataDir(), "auth.json");
2373
2765
  }
2766
+ export function studioInstanceRecordPathForOptions(options) {
2767
+ const key = createHash("sha256")
2768
+ .update(JSON.stringify({
2769
+ apiBaseUrl: comparableHttpUrl(options.apiBaseUrl),
2770
+ projectId: options.projectId ?? "",
2771
+ workspaceRoot: comparableFsPath(options.workspaceRoot ?? ""),
2772
+ }))
2773
+ .digest("hex")
2774
+ .slice(0, 24);
2775
+ return join(getDefaultTapiDataDir(), "Studio", "instances", `${key}.json`);
2776
+ }
2777
+ async function writeStudioInstanceRecord(options, record) {
2778
+ await writeJsonFile(studioInstanceRecordPathForOptions(options), record);
2779
+ }
2780
+ async function readStudioInstanceRecord(options) {
2781
+ try {
2782
+ const parsed = JSON.parse(await readFile(studioInstanceRecordPathForOptions(options), "utf8"));
2783
+ if (!isRecord(parsed)) {
2784
+ return null;
2785
+ }
2786
+ const baseUrl = typeof parsed.baseUrl === "string" ? parsed.baseUrl.trim() : "";
2787
+ const host = typeof parsed.host === "string" ? parsed.host.trim() : "";
2788
+ const port = typeof parsed.port === "number" && Number.isFinite(parsed.port) ? parsed.port : 0;
2789
+ if (!baseUrl || !host || port <= 0) {
2790
+ return null;
2791
+ }
2792
+ return {
2793
+ version: 1,
2794
+ baseUrl,
2795
+ host,
2796
+ port,
2797
+ pid: typeof parsed.pid === "number" && Number.isFinite(parsed.pid) ? parsed.pid : undefined,
2798
+ apiBaseUrl: typeof parsed.apiBaseUrl === "string" ? parsed.apiBaseUrl : "",
2799
+ projectId: typeof parsed.projectId === "string" ? parsed.projectId : undefined,
2800
+ workspaceRoot: typeof parsed.workspaceRoot === "string" ? parsed.workspaceRoot : undefined,
2801
+ workspaceConfig: typeof parsed.workspaceConfig === "string" ? parsed.workspaceConfig : undefined,
2802
+ manifestVersion: typeof parsed.manifestVersion === "string" ? parsed.manifestVersion : undefined,
2803
+ updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : "",
2804
+ };
2805
+ }
2806
+ catch (error) {
2807
+ if (error.code === "ENOENT") {
2808
+ return null;
2809
+ }
2810
+ return null;
2811
+ }
2812
+ }
2813
+ function studioInstanceIdentityMatches(identity, options) {
2814
+ if (!identity || identity.ok !== true) {
2815
+ return false;
2816
+ }
2817
+ const expectedProject = (options.projectId ?? "").trim();
2818
+ const actualProject = typeof identity.projectId === "string" ? identity.projectId.trim() : "";
2819
+ if (expectedProject && actualProject && actualProject !== expectedProject) {
2820
+ return false;
2821
+ }
2822
+ const expectedWorkspace = comparableFsPath(options.workspaceRoot ?? "");
2823
+ const actualWorkspace = comparableFsPath(typeof identity.workspaceRoot === "string" ? identity.workspaceRoot : "");
2824
+ if (expectedWorkspace && actualWorkspace && actualWorkspace !== expectedWorkspace) {
2825
+ return false;
2826
+ }
2827
+ const expectedApiBaseUrl = comparableHttpUrl(options.apiBaseUrl);
2828
+ const actualApiBaseUrl = comparableHttpUrl(typeof identity.apiBaseUrl === "string" ? identity.apiBaseUrl : "");
2829
+ if (expectedApiBaseUrl && actualApiBaseUrl && actualApiBaseUrl !== expectedApiBaseUrl) {
2830
+ return false;
2831
+ }
2832
+ return true;
2833
+ }
2834
+ function comparableHttpUrl(value) {
2835
+ return String(value || "").trim().replace(/\/+$/, "").toLowerCase();
2836
+ }
2837
+ function comparableFsPath(value) {
2838
+ const trimmed = String(value || "").trim();
2839
+ return trimmed ? resolve(trimmed).toLowerCase() : "";
2840
+ }
2374
2841
  function getDefaultTapiDataDir() {
2375
2842
  if (process.platform === "win32") {
2376
2843
  const localAppData = process.env.LOCALAPPDATA
@@ -2831,146 +3298,166 @@ function readSdkVersion() {
2831
3298
  function printHelp() {
2832
3299
  console.log(`Tapi CLI
2833
3300
 
2834
- Usage:
2835
- tapi init --project PROJECT
2836
- tapi link --project PROJECT
2837
- tapi studio install [--channel pilot] [--api-base-url URL]
2838
- tapi studio
2839
- tapi studio open
2840
- tapi studio doctor
2841
- tapi apis describe <namespace.operation>
2842
- tapi apis sync
2843
- tapi apis generate
2844
- tapi triggers sync
2845
- tapi publish
2846
- tapi service status
2847
- tapi doctor
2848
-
2849
- Commands:
2850
- init Create .tapi/project.json for this repo
2851
- link Rebind this repo to an existing Tapi project
2852
- studio install Download, verify, and run the Tapi Studio installer
2853
- studio Open Tapi Studio for this repo
2854
- studio open Open Tapi Studio for this repo
2855
- studio doctor Check local SDK and Studio release configuration
2856
- apis describe Print a generated website API input/output contract
2857
- apis sync Save the published API catalog to .tapi/generated
2858
- apis generate Generate a TypeScript runtime wrapper from the catalog
2859
- triggers sync Upsert API-call triggers from tapi.config
2860
- publish Upload local .tapi API drafts and publish ready requests
2861
- service Inspect or control the local Tapi Windows service
2862
- doctor Alias for studio doctor
3301
+ Usage:
3302
+ tapi init --project PROJECT
3303
+ tapi link --project PROJECT
3304
+ tapi studio install [--channel pilot] [--api-base-url URL]
3305
+ tapi studio
3306
+ tapi studio open
3307
+ tapi studio doctor
3308
+ tapi apis describe <namespace.operation>
3309
+ tapi apis sync
3310
+ tapi apis generate
3311
+ tapi triggers sync
3312
+ tapi sessions
3313
+ tapi publish
3314
+ tapi service status
3315
+ tapi doctor
3316
+
3317
+ Commands:
3318
+ init Create .tapi/project.json for this repo
3319
+ link Rebind this repo to an existing Tapi project
3320
+ studio install Download, verify, and run the Tapi Studio installer
3321
+ studio Open Tapi Studio for this repo
3322
+ studio open Open Tapi Studio for this repo
3323
+ studio doctor Check local SDK and Studio release configuration
3324
+ apis describe Print a generated website API input/output contract
3325
+ apis sync Save the published API catalog to .tapi/generated
3326
+ apis generate Generate a TypeScript runtime wrapper from the catalog
3327
+ triggers sync Upsert API-call triggers from tapi.config
3328
+ sessions List dev-mode API sessions and open takeover sessions
3329
+ publish Upload local .tapi API drafts and publish ready requests
3330
+ service Inspect or control the local Tapi Windows service
3331
+ doctor Alias for studio doctor
3332
+ `);
3333
+ }
3334
+ function printSessionsHelp() {
3335
+ console.log(`Tapi dev session commands
3336
+
3337
+ Usage:
3338
+ tapi sessions [--api-base-url URL] [--api-key KEY] [--project PROJECT]
3339
+ tapi sessions list [--json] [--site SITE]
3340
+ tapi sessions open <session-id> [--api-base-url URL] [--api-key KEY] [--project PROJECT]
3341
+
3342
+ Options:
3343
+ --api-base-url <url> Tapi API base URL
3344
+ --server <url> Alias for --api-base-url
3345
+ --api-key <key> Tapi API key; defaults to TAPI_API_KEY
3346
+ --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
3347
+ --site <name> Limit sessions to one sitemap/site
3348
+ --json Print raw grouped session JSON
3349
+ --no-interactive Print the grouped list without the arrow-key picker
2863
3350
  `);
2864
3351
  }
2865
3352
  function printApisHelp() {
2866
- console.log(`Tapi generated website API commands
2867
-
2868
- Usage:
2869
- tapi apis describe <namespace.operation> [--api-base-url URL] [--api-key KEY] [--project PROJECT]
2870
- tapi apis sync [--api-base-url URL] [--api-key KEY] [--project PROJECT]
2871
- tapi apis generate [--api-base-url URL] [--api-key KEY] [--project PROJECT] [--out FILE]
2872
-
2873
- Options:
2874
- --api-base-url <url> Tapi API base URL
2875
- --server <url> Alias for --api-base-url
2876
- --api-key <key> Tapi API key; defaults to TAPI_API_KEY
2877
- --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
2878
- --catalog <path> Catalog JSON output path
2879
- --out <path> Generated TypeScript output path
3353
+ console.log(`Tapi generated website API commands
3354
+
3355
+ Usage:
3356
+ tapi apis describe <namespace.operation> [--api-base-url URL] [--api-key KEY] [--project PROJECT]
3357
+ tapi apis sync [--api-base-url URL] [--api-key KEY] [--project PROJECT]
3358
+ tapi apis generate [--api-base-url URL] [--api-key KEY] [--project PROJECT] [--out FILE]
3359
+
3360
+ Options:
3361
+ --api-base-url <url> Tapi API base URL
3362
+ --server <url> Alias for --api-base-url
3363
+ --api-key <key> Tapi API key; defaults to TAPI_API_KEY
3364
+ --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
3365
+ --catalog <path> Catalog JSON output path
3366
+ --out <path> Generated TypeScript output path
2880
3367
  `);
2881
3368
  }
2882
3369
  function printTriggersHelp() {
2883
- console.log(`Tapi API trigger commands
2884
-
2885
- Usage:
2886
- tapi triggers sync [--config FILE] [--api-base-url URL] [--api-key KEY] [--project PROJECT]
2887
-
2888
- Options:
2889
- --config <path> Trigger config path; defaults to tapi.config.ts/js/json in the workspace root
2890
- --api-base-url <url> Tapi API base URL
2891
- --server <url> Alias for --api-base-url
2892
- --api-key <key> Tapi API key; defaults to TAPI_API_KEY
2893
- --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
2894
-
2895
- Config:
2896
- export default {
2897
- triggers: {
2898
- nightlyBalance: {
2899
- apiRequest: "schwab.get_balance",
2900
- interval: "1h",
2901
- inputs: { accountId: "main" },
2902
- runtime: { profileRef: "perm_default" },
2903
- },
2904
- },
2905
- };
3370
+ console.log(`Tapi API trigger commands
3371
+
3372
+ Usage:
3373
+ tapi triggers sync [--config FILE] [--api-base-url URL] [--api-key KEY] [--project PROJECT]
3374
+
3375
+ Options:
3376
+ --config <path> Trigger config path; defaults to tapi.config.ts/js/json in the workspace root
3377
+ --api-base-url <url> Tapi API base URL
3378
+ --server <url> Alias for --api-base-url
3379
+ --api-key <key> Tapi API key; defaults to TAPI_API_KEY
3380
+ --project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
3381
+
3382
+ Config:
3383
+ export default {
3384
+ triggers: {
3385
+ nightlyBalance: {
3386
+ apiRequest: "schwab.get_balance",
3387
+ interval: "1h",
3388
+ inputs: { accountId: "main" },
3389
+ runtime: { profileRef: "perm_default" },
3390
+ },
3391
+ },
3392
+ };
2906
3393
  `);
2907
3394
  }
2908
3395
  function printStudioHelp() {
2909
- console.log(`Tapi Studio commands
2910
-
2911
- Usage:
2912
- tapi studio [options]
2913
- tapi studio install [options]
2914
- tapi studio open [options]
2915
- tapi studio doctor [options]
2916
-
2917
- Options:
3396
+ console.log(`Tapi Studio commands
3397
+
3398
+ Usage:
3399
+ tapi studio [options]
3400
+ tapi studio install [options]
3401
+ tapi studio open [options]
3402
+ tapi studio doctor [options]
3403
+
3404
+ Options:
2918
3405
  --channel <name> Release channel: pilot, stable, or nightly
2919
- --api-base-url <url> Tapi API base URL for install authorization and protected downloads
2920
- --server <url> Alias for --api-base-url
2921
- --workspace <path> Search this directory for .tapi/project.json
2922
- --no-workspace Do not load .tapi/project.json
2923
- --project <id> Tapi project id for workspace-bound Studio
2924
- --project-slug <slug> Optional display slug for the bound project
2925
- --install-token <tok> Preissued Studio install token (skips browser sign-in)
2926
- --manifest <url> Exact release manifest URL for doctor only
2927
- --cache-dir <path> Installer download cache directory
2928
- --install-dir <path> Portable Studio server release directory
2929
- --download-only Download and verify without running the installer
2930
- --silent Run the NSIS installer with /S
2931
- --exe <path> Tapi Studio executable path for open/doctor
3406
+ --api-base-url <url> Tapi API base URL for install authorization and protected downloads
3407
+ --server <url> Alias for --api-base-url
3408
+ --workspace <path> Search this directory for .tapi/project.json
3409
+ --no-workspace Do not load .tapi/project.json
3410
+ --project <id> Tapi project id for workspace-bound Studio
3411
+ --project-slug <slug> Optional display slug for the bound project
3412
+ --install-token <tok> Preissued Studio install token (skips browser sign-in)
3413
+ --manifest <url> Exact release manifest URL for doctor only
3414
+ --cache-dir <path> Installer download cache directory
3415
+ --install-dir <path> Portable Studio server release directory
3416
+ --download-only Download and verify without running the installer
3417
+ --silent Run the NSIS installer with /S
3418
+ --exe <path> Tapi Studio executable path for open/doctor
2932
3419
  `);
2933
3420
  }
2934
3421
  function printWorkspaceHelp(command) {
2935
- console.log(`Tapi workspace ${command}
2936
-
2937
- Usage:
2938
- tapi ${command} --project PROJECT [--api-base-url URL] [--root PATH] [--force]
2939
-
2940
- Options:
2941
- --project <id> Tapi project id/name to bind this repo to
2942
- --project-slug <slug> Optional display slug
2943
- --api-base-url <url> Tapi API base URL stored in .tapi/project.json
2944
- --server <url> Alias for --api-base-url
2945
- --root <path> Directory where .tapi/project.json should be written
2946
- --force Overwrite an existing workspace config
3422
+ console.log(`Tapi workspace ${command}
3423
+
3424
+ Usage:
3425
+ tapi ${command} --project PROJECT [--api-base-url URL] [--root PATH] [--force]
3426
+
3427
+ Options:
3428
+ --project <id> Tapi project id/name to bind this repo to
3429
+ --project-slug <slug> Optional display slug
3430
+ --api-base-url <url> Tapi API base URL stored in .tapi/project.json
3431
+ --server <url> Alias for --api-base-url
3432
+ --root <path> Directory where .tapi/project.json should be written
3433
+ --force Overwrite an existing workspace config
2947
3434
  `);
2948
3435
  }
2949
3436
  function printServiceHelp() {
2950
- console.log(`Tapi service commands
2951
-
2952
- Usage:
2953
- tapi service status
2954
- tapi service start
2955
- tapi service stop
2956
- tapi service restart
2957
- tapi service repair
2958
-
2959
- Commands:
2960
- status Print installed/running status for tapi-service
2961
- start Start tapi-service
2962
- stop Stop tapi-service
2963
- restart Restart tapi-service
2964
- repair Restart tapi-service using the current installed service
3437
+ console.log(`Tapi service commands
3438
+
3439
+ Usage:
3440
+ tapi service status
3441
+ tapi service start
3442
+ tapi service stop
3443
+ tapi service restart
3444
+ tapi service repair
3445
+
3446
+ Commands:
3447
+ status Print installed/running status for tapi-service
3448
+ start Start tapi-service
3449
+ stop Stop tapi-service
3450
+ restart Restart tapi-service
3451
+ repair Restart tapi-service using the current installed service
2965
3452
  `);
2966
3453
  }
2967
3454
  function printPublishHelp() {
2968
- console.log(`Tapi publish
2969
-
2970
- Usage:
2971
- tapi publish [--api-base-url URL] [--api-key KEY] [--project PROJECT]
2972
-
2973
- Publishes ready generated API requests from local .tapi/apis files.
3455
+ console.log(`Tapi publish
3456
+
3457
+ Usage:
3458
+ tapi publish [--api-base-url URL] [--api-key KEY] [--project PROJECT]
3459
+
3460
+ Publishes ready generated API requests from local .tapi/apis files.
2974
3461
  `);
2975
3462
  }
2976
3463
  function formatError(error) {