@tapi-dev/sdk 0.1.15 → 0.1.20
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/README.md +393 -275
- package/dist/cli.d.ts +17 -2
- package/dist/cli.js +861 -211
- package/dist/index.d.ts +4 -0
- package/dist/index.js +6 -1
- package/dist/late-input.d.ts +24 -0
- package/dist/late-input.js +112 -0
- package/dist/runs.d.ts +1 -0
- package/dist/runs.js +3 -0
- package/dist/sessions.d.ts +10 -0
- package/dist/sessions.js +14 -0
- package/dist/types.d.ts +45 -2
- package/dist/website-apis.d.ts +3 -1
- package/dist/website-apis.js +16 -3
- package/package.json +1 -1
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";
|
|
@@ -23,6 +24,9 @@ const DEFAULT_INSTALL_AUTH_TIMEOUT_MS = 120_000;
|
|
|
23
24
|
const PORTABLE_STUDIO_SERVER_READY_TIMEOUT_MS = 30_000;
|
|
24
25
|
const PORTABLE_STUDIO_SERVER_READY_INTERVAL_MS = 250;
|
|
25
26
|
const MAX_WIDE_EVENT_PROCESS_ROOTS = 5;
|
|
27
|
+
const MAX_NONCURRENT_MANAGED_DOWNLOADS = 0;
|
|
28
|
+
const MAX_NONCURRENT_MANAGED_RELEASES = 0;
|
|
29
|
+
const MANAGED_RELEASE_MARKER = ".tapi-managed-release";
|
|
26
30
|
class StudioInstallApprovalError extends Error {
|
|
27
31
|
code;
|
|
28
32
|
status;
|
|
@@ -171,6 +175,23 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
171
175
|
printTriggersHelp();
|
|
172
176
|
return 1;
|
|
173
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
|
+
}
|
|
174
195
|
if (command !== "studio") {
|
|
175
196
|
console.error(`Unknown command: ${command}`);
|
|
176
197
|
printHelp();
|
|
@@ -282,6 +303,14 @@ export function parseStudioOptions(args) {
|
|
|
282
303
|
raw.cacheDir = resolve(arg.slice("--cache-dir=".length));
|
|
283
304
|
continue;
|
|
284
305
|
}
|
|
306
|
+
if (arg === "--install-dir") {
|
|
307
|
+
raw.installDir = resolve(requireOptionValue(args, ++index, "--install-dir"));
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (arg.startsWith("--install-dir=")) {
|
|
311
|
+
raw.installDir = resolve(arg.slice("--install-dir=".length));
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
285
314
|
if (arg === "--exe") {
|
|
286
315
|
raw.exePath = resolve(requireOptionValue(args, ++index, "--exe"));
|
|
287
316
|
continue;
|
|
@@ -324,6 +353,7 @@ export function parseStudioOptions(args) {
|
|
|
324
353
|
manifestUrl,
|
|
325
354
|
manifestUrlOverride: explicitManifestUrl ? normalizeHttpUrl(explicitManifestUrl, "Studio manifest URL") : undefined,
|
|
326
355
|
cacheDir: raw.cacheDir ?? getDefaultStudioCacheDir(),
|
|
356
|
+
installDir: raw.installDir ?? resolve(envString("TAPI_STUDIO_INSTALL_DIR") ?? getDefaultPortableStudioServerReleasesDir()),
|
|
327
357
|
downloadOnly: raw.downloadOnly ?? false,
|
|
328
358
|
silent: raw.silent ?? false,
|
|
329
359
|
exePath: raw.exePath ?? envString("TAPI_STUDIO_EXE"),
|
|
@@ -603,6 +633,350 @@ async function syncApiTriggers(args) {
|
|
|
603
633
|
return 1;
|
|
604
634
|
}
|
|
605
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
|
+
}
|
|
606
980
|
function defaultTriggerConfigPath(root) {
|
|
607
981
|
for (const name of ["tapi.config.ts", "tapi.config.mjs", "tapi.config.js", "tapi.config.cjs", "tapi.config.json"]) {
|
|
608
982
|
const path = join(root, name);
|
|
@@ -844,23 +1218,23 @@ function renderGeneratedApiClient(catalog) {
|
|
|
844
1218
|
const lines = operations.map(({ key, operationName }) => ` ${operationName}: (inputs: Record<string, unknown> = {}, options: GeneratedRunOptions = {}) => client.websiteApis.run(${JSON.stringify(key)}, { ...options, inputs }),`);
|
|
845
1219
|
return ` ${namespace}: {\n${lines.join("\n")}\n },`;
|
|
846
1220
|
});
|
|
847
|
-
return `/* Generated by Tapi. Do not edit by hand. */
|
|
848
|
-
import { TapiClient, type TapiClientOptions, type RuntimeRunOptions } from "@tapi-dev/sdk";
|
|
849
|
-
|
|
850
|
-
export interface GeneratedRunOptions {
|
|
851
|
-
runtime?: RuntimeRunOptions;
|
|
852
|
-
priority?: number;
|
|
853
|
-
runnerId?: string;
|
|
854
|
-
idempotencyKey?: string;
|
|
855
|
-
site?: string;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
export function createTapiGeneratedClient(options: TapiClientOptions) {
|
|
859
|
-
const client = new TapiClient(options);
|
|
860
|
-
return {
|
|
861
|
-
${namespaceBlocks.join("\n")}
|
|
862
|
-
};
|
|
863
|
-
}
|
|
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
|
+
}
|
|
864
1238
|
`;
|
|
865
1239
|
}
|
|
866
1240
|
function safeIdentifier(value) {
|
|
@@ -1074,18 +1448,7 @@ async function fetchServiceManifestForEnsure(options) {
|
|
|
1074
1448
|
options: installEventOptions(options),
|
|
1075
1449
|
});
|
|
1076
1450
|
try {
|
|
1077
|
-
|
|
1078
|
-
if (!installToken) {
|
|
1079
|
-
await event.phase("auth.install_token.request.start", {
|
|
1080
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1081
|
-
channel: options.channel,
|
|
1082
|
-
});
|
|
1083
|
-
installToken = await withCliSpinner("Checking Tapi Service install approval", () => obtainStudioInstallToken(options, event));
|
|
1084
|
-
await event.phase("auth.install_token.request.success", {
|
|
1085
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1086
|
-
channel: options.channel,
|
|
1087
|
-
});
|
|
1088
|
-
}
|
|
1451
|
+
const installToken = await ensureStudioInstallTokenForRun(options, event, "Checking Tapi Service install authorization");
|
|
1089
1452
|
await event.phase("service_manifest.fetch.start", {
|
|
1090
1453
|
apiBaseUrl: options.apiBaseUrl,
|
|
1091
1454
|
channel: options.channel,
|
|
@@ -1148,18 +1511,7 @@ async function installService(options) {
|
|
|
1148
1511
|
arch: process.arch,
|
|
1149
1512
|
});
|
|
1150
1513
|
ensureWindowsHost();
|
|
1151
|
-
|
|
1152
|
-
if (!installToken) {
|
|
1153
|
-
await event.phase("auth.install_token.request.start", {
|
|
1154
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1155
|
-
channel: options.channel,
|
|
1156
|
-
});
|
|
1157
|
-
installToken = await withCliSpinner("Checking Tapi Service install approval", () => obtainStudioInstallToken(options, event));
|
|
1158
|
-
await event.phase("auth.install_token.request.success", {
|
|
1159
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1160
|
-
channel: options.channel,
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1514
|
+
const installToken = await ensureStudioInstallTokenForRun(options, event, "Checking Tapi Service install authorization");
|
|
1163
1515
|
await event.phase("service_manifest.fetch.start", {
|
|
1164
1516
|
apiBaseUrl: options.apiBaseUrl,
|
|
1165
1517
|
channel: options.channel,
|
|
@@ -1202,6 +1554,7 @@ async function installService(options) {
|
|
|
1202
1554
|
}
|
|
1203
1555
|
const releaseDir = join(releasesDir, safePathSegment(manifest.version));
|
|
1204
1556
|
await withCliSpinner(`Extracting Tapi Service ${manifest.version}`, () => extractZip(zipPath, releaseDir));
|
|
1557
|
+
await markManagedReleaseDir(releaseDir, manifest.version);
|
|
1205
1558
|
const installer = join(releaseDir, manifest.installer || "install_tapi_service.ps1");
|
|
1206
1559
|
const hostExe = join(releaseDir, manifest.serviceHostExecutable || "tapi-service-host.exe");
|
|
1207
1560
|
if (!existsSync(installer)) {
|
|
@@ -1243,6 +1596,8 @@ async function installService(options) {
|
|
|
1243
1596
|
releaseDir,
|
|
1244
1597
|
version: manifest.version,
|
|
1245
1598
|
});
|
|
1599
|
+
await pruneManagedFiles(downloadsDir, [zipPath], isTapiServiceDownloadArtifact, MAX_NONCURRENT_MANAGED_DOWNLOADS, "old Tapi Service download");
|
|
1600
|
+
await pruneManagedDirs(releasesDir, [releaseDir], isSafeManagedReleaseDirName, MAX_NONCURRENT_MANAGED_RELEASES, "old Tapi Service release");
|
|
1246
1601
|
console.log("Tapi Service is installed and running.");
|
|
1247
1602
|
return 0;
|
|
1248
1603
|
}
|
|
@@ -1395,6 +1750,10 @@ async function requestJson(method, url, body, apiKey, projectId) {
|
|
|
1395
1750
|
}
|
|
1396
1751
|
}
|
|
1397
1752
|
export function getDefaultStudioCacheDir() {
|
|
1753
|
+
const homeOverride = envString("TAPI_STUDIO_HOME");
|
|
1754
|
+
if (homeOverride) {
|
|
1755
|
+
return join(resolve(homeOverride), "downloads");
|
|
1756
|
+
}
|
|
1398
1757
|
if (process.platform === "win32") {
|
|
1399
1758
|
const localAppData = process.env.LOCALAPPDATA ??
|
|
1400
1759
|
(process.env.USERPROFILE ? join(process.env.USERPROFILE, "AppData", "Local") : join(homedir(), "AppData", "Local"));
|
|
@@ -1716,24 +2075,7 @@ async function installStudio(options) {
|
|
|
1716
2075
|
throw new Error("Direct Studio manifest overrides are no longer supported for install. "
|
|
1717
2076
|
+ "Use --channel with the protected install flow instead.");
|
|
1718
2077
|
}
|
|
1719
|
-
|
|
1720
|
-
if (installToken) {
|
|
1721
|
-
await event.phase("auth.install_token.provided", {
|
|
1722
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1723
|
-
channel: options.channel,
|
|
1724
|
-
});
|
|
1725
|
-
}
|
|
1726
|
-
else {
|
|
1727
|
-
await event.phase("auth.install_token.request.start", {
|
|
1728
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1729
|
-
channel: options.channel,
|
|
1730
|
-
});
|
|
1731
|
-
installToken = await withCliSpinner("Checking Studio install approval", () => obtainStudioInstallToken(options, event));
|
|
1732
|
-
await event.phase("auth.install_token.request.success", {
|
|
1733
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1734
|
-
channel: options.channel,
|
|
1735
|
-
});
|
|
1736
|
-
}
|
|
2078
|
+
const installToken = await ensureStudioInstallTokenForRun(options, event, "Checking Studio install authorization");
|
|
1737
2079
|
await event.phase("manifest.fetch.start", {
|
|
1738
2080
|
apiBaseUrl: options.apiBaseUrl,
|
|
1739
2081
|
channel: options.channel,
|
|
@@ -1852,6 +2194,39 @@ async function obtainStudioInstallToken(options, event) {
|
|
|
1852
2194
|
const issued = await requestStudioInstallToken(options.apiBaseUrl, options.channel, firebaseCreds.idToken);
|
|
1853
2195
|
return issued.installToken;
|
|
1854
2196
|
}
|
|
2197
|
+
export function memoizeStudioInstallToken(options, installToken) {
|
|
2198
|
+
const existing = options.installToken?.trim();
|
|
2199
|
+
if (existing) {
|
|
2200
|
+
options.installToken = existing;
|
|
2201
|
+
return existing;
|
|
2202
|
+
}
|
|
2203
|
+
const normalized = installToken?.trim() ?? "";
|
|
2204
|
+
if (normalized) {
|
|
2205
|
+
options.installToken = normalized;
|
|
2206
|
+
}
|
|
2207
|
+
return normalized;
|
|
2208
|
+
}
|
|
2209
|
+
async function ensureStudioInstallTokenForRun(options, event, message) {
|
|
2210
|
+
const existing = memoizeStudioInstallToken(options);
|
|
2211
|
+
if (existing) {
|
|
2212
|
+
await event.phase("auth.install_token.reused", {
|
|
2213
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
2214
|
+
channel: options.channel,
|
|
2215
|
+
});
|
|
2216
|
+
return existing;
|
|
2217
|
+
}
|
|
2218
|
+
await event.phase("auth.install_token.request.start", {
|
|
2219
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
2220
|
+
channel: options.channel,
|
|
2221
|
+
});
|
|
2222
|
+
const installToken = memoizeStudioInstallToken(options, await withCliSpinner(message, () => obtainStudioInstallToken(options, event)));
|
|
2223
|
+
await event.phase("auth.install_token.request.success", {
|
|
2224
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
2225
|
+
channel: options.channel,
|
|
2226
|
+
cachedForRun: true,
|
|
2227
|
+
});
|
|
2228
|
+
return installToken;
|
|
2229
|
+
}
|
|
1855
2230
|
async function openStudio(options) {
|
|
1856
2231
|
ensureWindowsHost();
|
|
1857
2232
|
if (options.downloadOnly) {
|
|
@@ -1876,7 +2251,7 @@ async function openStudio(options) {
|
|
|
1876
2251
|
}
|
|
1877
2252
|
if (!exePath || !existsSync(exePath)) {
|
|
1878
2253
|
console.error("Tapi Studio executable was not found.");
|
|
1879
|
-
console.error("Run `
|
|
2254
|
+
console.error("Run `tapi studio install --channel pilot`, or set TAPI_STUDIO_EXE to the installed executable path.");
|
|
1880
2255
|
return 1;
|
|
1881
2256
|
}
|
|
1882
2257
|
const child = spawn(exePath, [], {
|
|
@@ -1901,18 +2276,7 @@ async function ensurePortableStudioServer(options) {
|
|
|
1901
2276
|
options: installEventOptions(options),
|
|
1902
2277
|
});
|
|
1903
2278
|
try {
|
|
1904
|
-
|
|
1905
|
-
if (!installToken) {
|
|
1906
|
-
await event.phase("auth.install_token.request.start", {
|
|
1907
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1908
|
-
channel: options.channel,
|
|
1909
|
-
});
|
|
1910
|
-
installToken = await withCliSpinner("Checking Studio install approval", () => obtainStudioInstallToken(options, event));
|
|
1911
|
-
await event.phase("auth.install_token.request.success", {
|
|
1912
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1913
|
-
channel: options.channel,
|
|
1914
|
-
});
|
|
1915
|
-
}
|
|
2279
|
+
const installToken = await ensureStudioInstallTokenForRun(options, event, "Checking Studio install authorization");
|
|
1916
2280
|
await event.phase("manifest.fetch.start", {
|
|
1917
2281
|
apiBaseUrl: options.apiBaseUrl,
|
|
1918
2282
|
channel: options.channel,
|
|
@@ -1928,7 +2292,7 @@ async function ensurePortableStudioServer(options) {
|
|
|
1928
2292
|
});
|
|
1929
2293
|
return null;
|
|
1930
2294
|
}
|
|
1931
|
-
const existing = findPortableStudioServerExe(portableStudioServerReleaseDir(manifest), manifest);
|
|
2295
|
+
const existing = findPortableStudioServerExe(portableStudioServerReleaseDir(manifest, options.installDir), manifest);
|
|
1932
2296
|
if (existing) {
|
|
1933
2297
|
await event.finish(true, {
|
|
1934
2298
|
serverExe: existing,
|
|
@@ -1972,14 +2336,13 @@ async function installPortableStudioServer(options, manifest, event) {
|
|
|
1972
2336
|
}
|
|
1973
2337
|
if (options.downloadOnly) {
|
|
1974
2338
|
console.log(`Downloaded Tapi Studio server artifact: ${artifactPath}`);
|
|
2339
|
+
await pruneManagedFiles(options.cacheDir, [artifactPath], isTapiStudioDownloadArtifact, MAX_NONCURRENT_MANAGED_DOWNLOADS, "old Tapi Studio download");
|
|
1975
2340
|
return artifactPath;
|
|
1976
2341
|
}
|
|
1977
|
-
const releaseDir = portableStudioServerReleaseDir(manifest);
|
|
1978
|
-
await withCliSpinner(`Extracting Tapi Studio server ${manifest.version}`, () =>
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
throw new Error(`Tapi Studio server executable was not found after extraction: ${releaseDir}`);
|
|
1982
|
-
}
|
|
2342
|
+
const releaseDir = portableStudioServerReleaseDir(manifest, options.installDir);
|
|
2343
|
+
const exePath = await withCliSpinner(`Extracting Tapi Studio server ${manifest.version}`, () => extractPortableStudioServerZip(artifactPath, releaseDir, manifest));
|
|
2344
|
+
await pruneManagedFiles(options.cacheDir, [artifactPath], isTapiStudioDownloadArtifact, MAX_NONCURRENT_MANAGED_DOWNLOADS, "old Tapi Studio download");
|
|
2345
|
+
await pruneManagedDirs(dirname(releaseDir), [releaseDir], isSafeManagedReleaseDirName, MAX_NONCURRENT_MANAGED_RELEASES, "old Tapi Studio server release");
|
|
1983
2346
|
console.log(`Installed Tapi Studio server ${manifest.version}: ${releaseDir}`);
|
|
1984
2347
|
return exePath;
|
|
1985
2348
|
}
|
|
@@ -2003,6 +2366,9 @@ async function launchPortableStudioServer(exePath, options, manifest) {
|
|
|
2003
2366
|
env.PLAYWRIGHT_BROWSERS_PATH = playwrightBrowsersPath;
|
|
2004
2367
|
}
|
|
2005
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;
|
|
2006
2372
|
await withCliSpinner("Starting Tapi Studio", async () => {
|
|
2007
2373
|
const child = spawn(exePath, [], {
|
|
2008
2374
|
detached: true,
|
|
@@ -2010,11 +2376,25 @@ async function launchPortableStudioServer(exePath, options, manifest) {
|
|
|
2010
2376
|
stdio: "ignore",
|
|
2011
2377
|
windowsHide: true,
|
|
2012
2378
|
});
|
|
2379
|
+
childPid = child.pid ?? 0;
|
|
2013
2380
|
child.unref();
|
|
2014
2381
|
await waitForHttpOk(`${url}/healthz`, PORTABLE_STUDIO_SERVER_READY_TIMEOUT_MS, PORTABLE_STUDIO_SERVER_READY_INTERVAL_MS);
|
|
2015
2382
|
});
|
|
2016
|
-
|
|
2017
|
-
|
|
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}`);
|
|
2018
2398
|
}
|
|
2019
2399
|
export function findPortablePlaywrightBrowsersDir(exePath) {
|
|
2020
2400
|
const exeDir = dirname(exePath);
|
|
@@ -2060,6 +2440,19 @@ export async function waitForHttpOk(url, timeoutMs = PORTABLE_STUDIO_SERVER_READ
|
|
|
2060
2440
|
const suffix = lastError ? ` Last error: ${lastError}.` : "";
|
|
2061
2441
|
throw new Error(`Tapi Studio server did not become ready at ${url} within ${Math.ceil(timeoutMs / 1000)}s.${suffix}`);
|
|
2062
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
|
+
}
|
|
2063
2456
|
function delay(ms) {
|
|
2064
2457
|
return new Promise((resolvePromise) => {
|
|
2065
2458
|
setTimeout(resolvePromise, Math.max(0, ms));
|
|
@@ -2183,7 +2576,7 @@ async function requestStudioInstallToken(apiBaseUrl, channel, firebaseIdToken, f
|
|
|
2183
2576
|
if (!response.ok) {
|
|
2184
2577
|
const detail = typeof responseBody?.detail === "string" ? responseBody.detail : "";
|
|
2185
2578
|
if (detail === "pending_approval" || detail === "access_pending") {
|
|
2186
|
-
throw new StudioInstallApprovalError("Access request submitted. Check your email for approval, then rerun `
|
|
2579
|
+
throw new StudioInstallApprovalError("Access request submitted. Check your email for approval, then rerun `tapi studio install`.", detail, response.status);
|
|
2187
2580
|
}
|
|
2188
2581
|
if (detail === "access_rejected") {
|
|
2189
2582
|
throw new StudioInstallApprovalError("Studio install access was rejected. Contact the Tapi admin if this is unexpected.", detail, response.status);
|
|
@@ -2370,6 +2763,81 @@ async function writeStudioAuthCache(credentials) {
|
|
|
2370
2763
|
function getStudioAuthCachePath() {
|
|
2371
2764
|
return join(getDefaultTapiDataDir(), "auth.json");
|
|
2372
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
|
+
}
|
|
2373
2841
|
function getDefaultTapiDataDir() {
|
|
2374
2842
|
if (process.platform === "win32") {
|
|
2375
2843
|
const localAppData = process.env.LOCALAPPDATA
|
|
@@ -2493,10 +2961,14 @@ function isPortableStudioServerManifest(manifest) {
|
|
|
2493
2961
|
const kind = String(manifest.installerKind || "").toLowerCase();
|
|
2494
2962
|
return kind === "portable-server" || manifest.artifactName.toLowerCase().endsWith(".zip");
|
|
2495
2963
|
}
|
|
2496
|
-
function portableStudioServerReleaseDir(manifest) {
|
|
2497
|
-
return join(
|
|
2964
|
+
function portableStudioServerReleaseDir(manifest, releasesDir = getDefaultPortableStudioServerReleasesDir()) {
|
|
2965
|
+
return join(releasesDir, safePathSegment(manifest.version));
|
|
2498
2966
|
}
|
|
2499
2967
|
function getDefaultPortableStudioServerReleasesDir() {
|
|
2968
|
+
const homeOverride = envString("TAPI_STUDIO_HOME");
|
|
2969
|
+
if (homeOverride) {
|
|
2970
|
+
return join(resolve(homeOverride), "server", "releases");
|
|
2971
|
+
}
|
|
2500
2972
|
if (process.platform === "win32") {
|
|
2501
2973
|
const localAppData = process.env.LOCALAPPDATA ??
|
|
2502
2974
|
(process.env.USERPROFILE ? join(process.env.USERPROFILE, "AppData", "Local") : join(homedir(), "AppData", "Local"));
|
|
@@ -2519,6 +2991,34 @@ export function findPortableStudioServerExe(releaseDir, manifest) {
|
|
|
2519
2991
|
];
|
|
2520
2992
|
return candidates.find((candidate) => existsSync(candidate));
|
|
2521
2993
|
}
|
|
2994
|
+
async function extractPortableStudioServerZip(zipPath, releaseDir, manifest) {
|
|
2995
|
+
const stagingDir = `${releaseDir}.extracting-${process.pid}-${Date.now()}`;
|
|
2996
|
+
const existingExe = findPortableStudioServerExe(releaseDir, manifest);
|
|
2997
|
+
if (!existingExe) {
|
|
2998
|
+
await rm(releaseDir, { recursive: true, force: true });
|
|
2999
|
+
}
|
|
3000
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
3001
|
+
try {
|
|
3002
|
+
await extractZip(zipPath, stagingDir);
|
|
3003
|
+
const stagedExe = findPortableStudioServerExe(stagingDir, manifest);
|
|
3004
|
+
if (!stagedExe) {
|
|
3005
|
+
throw new Error(`Tapi Studio server executable was not found after extraction. Expected ${manifest.serverExecutable || "tapi-studio-server.exe"} under ${stagingDir}.`);
|
|
3006
|
+
}
|
|
3007
|
+
await rm(releaseDir, { recursive: true, force: true });
|
|
3008
|
+
await mkdir(dirname(releaseDir), { recursive: true });
|
|
3009
|
+
await rename(stagingDir, releaseDir);
|
|
3010
|
+
await markManagedReleaseDir(releaseDir, manifest.version);
|
|
3011
|
+
const finalExe = findPortableStudioServerExe(releaseDir, manifest);
|
|
3012
|
+
if (!finalExe) {
|
|
3013
|
+
throw new Error(`Tapi Studio server executable disappeared while finalizing extraction: ${releaseDir}`);
|
|
3014
|
+
}
|
|
3015
|
+
return finalExe;
|
|
3016
|
+
}
|
|
3017
|
+
catch (error) {
|
|
3018
|
+
await rm(stagingDir, { recursive: true, force: true }).catch(() => undefined);
|
|
3019
|
+
throw error;
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
2522
3022
|
function cachedServiceArtifactName(manifest) {
|
|
2523
3023
|
const rawName = manifest.artifactName || basename(new URL(manifest.url).pathname) || `TapiService-${manifest.version}.zip`;
|
|
2524
3024
|
return rawName.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
@@ -2526,13 +3026,141 @@ function cachedServiceArtifactName(manifest) {
|
|
|
2526
3026
|
async function extractZip(zipPath, destination) {
|
|
2527
3027
|
await rm(destination, { recursive: true, force: true });
|
|
2528
3028
|
await mkdir(destination, { recursive: true });
|
|
2529
|
-
|
|
2530
|
-
"
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
3029
|
+
try {
|
|
3030
|
+
await runProcessCapture("powershell.exe", [
|
|
3031
|
+
"-NoProfile",
|
|
3032
|
+
"-ExecutionPolicy",
|
|
3033
|
+
"Bypass",
|
|
3034
|
+
"-Command",
|
|
3035
|
+
buildExpandArchiveCommand(zipPath, destination),
|
|
3036
|
+
]);
|
|
3037
|
+
}
|
|
3038
|
+
catch (error) {
|
|
3039
|
+
throw new Error(formatZipExtractionFailure(zipPath, destination, error), { cause: error });
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
export function buildExpandArchiveCommand(zipPath, destination) {
|
|
3043
|
+
return `$ErrorActionPreference = 'Stop'; try { Expand-Archive -LiteralPath ${powerShellSingleQuoted(zipPath)} -DestinationPath ${powerShellSingleQuoted(destination)} -Force -ErrorAction Stop } catch { Write-Error $_; exit 1 }`;
|
|
3044
|
+
}
|
|
3045
|
+
export function formatZipExtractionFailure(zipPath, destination, error) {
|
|
3046
|
+
const detail = formatError(error);
|
|
3047
|
+
const diskHint = /not enough space|no space left|disk full/i.test(detail)
|
|
3048
|
+
? " Free disk space on that drive, or set TAPI_STUDIO_HOME / --cache-dir / --install-dir to paths on a drive with more room."
|
|
3049
|
+
: "";
|
|
3050
|
+
return `Failed to extract archive ${zipPath} to ${destination}: ${detail}${diskHint}`;
|
|
3051
|
+
}
|
|
3052
|
+
export async function pruneManagedFiles(directory, keepPaths, isManagedFileName, maxNoncurrentEntries = 0, label = "old Tapi download") {
|
|
3053
|
+
const keep = normalizedKeepPathSet(keepPaths);
|
|
3054
|
+
let entries;
|
|
3055
|
+
try {
|
|
3056
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
3057
|
+
}
|
|
3058
|
+
catch {
|
|
3059
|
+
return 0;
|
|
3060
|
+
}
|
|
3061
|
+
const candidates = [];
|
|
3062
|
+
for (const entry of entries) {
|
|
3063
|
+
if (!entry.isFile() || !isManagedFileName(entry.name)) {
|
|
3064
|
+
continue;
|
|
3065
|
+
}
|
|
3066
|
+
const candidate = join(directory, entry.name);
|
|
3067
|
+
if (keep.has(normalizeFilesystemPath(candidate))) {
|
|
3068
|
+
continue;
|
|
3069
|
+
}
|
|
3070
|
+
const info = await stat(candidate).catch(() => undefined);
|
|
3071
|
+
if (!info?.isFile()) {
|
|
3072
|
+
continue;
|
|
3073
|
+
}
|
|
3074
|
+
candidates.push({ path: candidate, mtimeMs: info.mtimeMs });
|
|
3075
|
+
}
|
|
3076
|
+
candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
3077
|
+
const removable = candidates.slice(Math.max(0, maxNoncurrentEntries));
|
|
3078
|
+
let removed = 0;
|
|
3079
|
+
for (const candidate of removable) {
|
|
3080
|
+
try {
|
|
3081
|
+
await rm(candidate.path, { force: true });
|
|
3082
|
+
removed += 1;
|
|
3083
|
+
}
|
|
3084
|
+
catch {
|
|
3085
|
+
// Cache pruning is best effort; install success should not depend on cleanup.
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
if (removed > 0) {
|
|
3089
|
+
console.log(`Pruned ${removed} ${label}${removed === 1 ? "" : "s"}.`);
|
|
3090
|
+
}
|
|
3091
|
+
return removed;
|
|
3092
|
+
}
|
|
3093
|
+
async function pruneManagedDirs(directory, keepPaths, isManagedDirName, maxNoncurrentEntries = 0, label = "old Tapi release") {
|
|
3094
|
+
const keep = normalizedKeepPathSet(keepPaths);
|
|
3095
|
+
let entries;
|
|
3096
|
+
try {
|
|
3097
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
3098
|
+
}
|
|
3099
|
+
catch {
|
|
3100
|
+
return 0;
|
|
3101
|
+
}
|
|
3102
|
+
const candidates = [];
|
|
3103
|
+
for (const entry of entries) {
|
|
3104
|
+
if (!entry.isDirectory() || !isManagedDirName(entry.name)) {
|
|
3105
|
+
continue;
|
|
3106
|
+
}
|
|
3107
|
+
const candidate = join(directory, entry.name);
|
|
3108
|
+
if (keep.has(normalizeFilesystemPath(candidate))) {
|
|
3109
|
+
continue;
|
|
3110
|
+
}
|
|
3111
|
+
if (!existsSync(join(candidate, MANAGED_RELEASE_MARKER))) {
|
|
3112
|
+
continue;
|
|
3113
|
+
}
|
|
3114
|
+
const info = await stat(candidate).catch(() => undefined);
|
|
3115
|
+
if (!info?.isDirectory()) {
|
|
3116
|
+
continue;
|
|
3117
|
+
}
|
|
3118
|
+
candidates.push({ path: candidate, mtimeMs: info.mtimeMs });
|
|
3119
|
+
}
|
|
3120
|
+
candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
3121
|
+
const removable = candidates.slice(Math.max(0, maxNoncurrentEntries));
|
|
3122
|
+
let removed = 0;
|
|
3123
|
+
for (const candidate of removable) {
|
|
3124
|
+
try {
|
|
3125
|
+
await rm(candidate.path, { recursive: true, force: true });
|
|
3126
|
+
removed += 1;
|
|
3127
|
+
}
|
|
3128
|
+
catch {
|
|
3129
|
+
// Cache pruning is best effort; install success should not depend on cleanup.
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
if (removed > 0) {
|
|
3133
|
+
console.log(`Pruned ${removed} ${label}${removed === 1 ? "" : "s"}.`);
|
|
3134
|
+
}
|
|
3135
|
+
return removed;
|
|
3136
|
+
}
|
|
3137
|
+
export function isTapiStudioDownloadArtifact(fileName) {
|
|
3138
|
+
const normalized = fileName.toLowerCase();
|
|
3139
|
+
return (normalized.endsWith(".zip")
|
|
3140
|
+
&& (normalized.startsWith("tapi-studio")
|
|
3141
|
+
|| normalized.startsWith("tapistudio")
|
|
3142
|
+
|| normalized.includes("tapi_studio")));
|
|
3143
|
+
}
|
|
3144
|
+
export function isTapiServiceDownloadArtifact(fileName) {
|
|
3145
|
+
const normalized = fileName.toLowerCase();
|
|
3146
|
+
return normalized.endsWith(".zip") && (normalized.startsWith("tapiservice-") || normalized.startsWith("tapi-service"));
|
|
3147
|
+
}
|
|
3148
|
+
function isSafeManagedReleaseDirName(name) {
|
|
3149
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name);
|
|
3150
|
+
}
|
|
3151
|
+
async function markManagedReleaseDir(directory, version) {
|
|
3152
|
+
const payload = JSON.stringify({
|
|
3153
|
+
product: "tapi",
|
|
3154
|
+
version,
|
|
3155
|
+
managedBy: "@tapi-dev/sdk",
|
|
3156
|
+
});
|
|
3157
|
+
await writeFile(join(directory, MANAGED_RELEASE_MARKER), `${payload}\n`).catch(() => undefined);
|
|
3158
|
+
}
|
|
3159
|
+
function normalizedKeepPathSet(paths) {
|
|
3160
|
+
return new Set(paths.map((path) => normalizeFilesystemPath(path)));
|
|
3161
|
+
}
|
|
3162
|
+
function normalizeFilesystemPath(path) {
|
|
3163
|
+
return resolve(path).replace(/\\/g, "/").toLowerCase();
|
|
2536
3164
|
}
|
|
2537
3165
|
function installEventOptions(options) {
|
|
2538
3166
|
return {
|
|
@@ -2541,6 +3169,7 @@ function installEventOptions(options) {
|
|
|
2541
3169
|
manifestUrl: options.manifestUrl,
|
|
2542
3170
|
manifestUrlOverride: options.manifestUrlOverride,
|
|
2543
3171
|
cacheDir: options.cacheDir,
|
|
3172
|
+
installDir: options.installDir,
|
|
2544
3173
|
downloadOnly: options.downloadOnly,
|
|
2545
3174
|
silent: options.silent,
|
|
2546
3175
|
exePath: options.exePath,
|
|
@@ -2669,145 +3298,166 @@ function readSdkVersion() {
|
|
|
2669
3298
|
function printHelp() {
|
|
2670
3299
|
console.log(`Tapi CLI
|
|
2671
3300
|
|
|
2672
|
-
Usage:
|
|
2673
|
-
tapi init --project PROJECT
|
|
2674
|
-
tapi link --project PROJECT
|
|
2675
|
-
tapi studio install [--channel pilot] [--api-base-url URL]
|
|
2676
|
-
tapi studio
|
|
2677
|
-
tapi studio open
|
|
2678
|
-
tapi studio doctor
|
|
2679
|
-
tapi apis describe <namespace.operation>
|
|
2680
|
-
tapi apis sync
|
|
2681
|
-
tapi apis generate
|
|
2682
|
-
tapi triggers sync
|
|
2683
|
-
tapi
|
|
2684
|
-
tapi
|
|
2685
|
-
tapi
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
studio
|
|
2692
|
-
studio
|
|
2693
|
-
studio
|
|
2694
|
-
|
|
2695
|
-
apis
|
|
2696
|
-
apis
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
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
|
|
2701
3350
|
`);
|
|
2702
3351
|
}
|
|
2703
3352
|
function printApisHelp() {
|
|
2704
|
-
console.log(`Tapi generated website API commands
|
|
2705
|
-
|
|
2706
|
-
Usage:
|
|
2707
|
-
tapi apis describe <namespace.operation> [--api-base-url URL] [--api-key KEY] [--project PROJECT]
|
|
2708
|
-
tapi apis sync [--api-base-url URL] [--api-key KEY] [--project PROJECT]
|
|
2709
|
-
tapi apis generate [--api-base-url URL] [--api-key KEY] [--project PROJECT] [--out FILE]
|
|
2710
|
-
|
|
2711
|
-
Options:
|
|
2712
|
-
--api-base-url <url> Tapi API base URL
|
|
2713
|
-
--server <url> Alias for --api-base-url
|
|
2714
|
-
--api-key <key> Tapi API key; defaults to TAPI_API_KEY
|
|
2715
|
-
--project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
|
|
2716
|
-
--catalog <path> Catalog JSON output path
|
|
2717
|
-
--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
|
|
2718
3367
|
`);
|
|
2719
3368
|
}
|
|
2720
3369
|
function printTriggersHelp() {
|
|
2721
|
-
console.log(`Tapi API trigger commands
|
|
2722
|
-
|
|
2723
|
-
Usage:
|
|
2724
|
-
tapi triggers sync [--config FILE] [--api-base-url URL] [--api-key KEY] [--project PROJECT]
|
|
2725
|
-
|
|
2726
|
-
Options:
|
|
2727
|
-
--config <path> Trigger config path; defaults to tapi.config.ts/js/json in the workspace root
|
|
2728
|
-
--api-base-url <url> Tapi API base URL
|
|
2729
|
-
--server <url> Alias for --api-base-url
|
|
2730
|
-
--api-key <key> Tapi API key; defaults to TAPI_API_KEY
|
|
2731
|
-
--project <id> Tapi project id; defaults to TAPI_PROJECT_ID or .tapi/project.json
|
|
2732
|
-
|
|
2733
|
-
Config:
|
|
2734
|
-
export default {
|
|
2735
|
-
triggers: {
|
|
2736
|
-
nightlyBalance: {
|
|
2737
|
-
apiRequest: "schwab.get_balance",
|
|
2738
|
-
interval: "1h",
|
|
2739
|
-
inputs: { accountId: "main" },
|
|
2740
|
-
runtime: { profileRef: "perm_default" },
|
|
2741
|
-
},
|
|
2742
|
-
},
|
|
2743
|
-
};
|
|
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
|
+
};
|
|
2744
3393
|
`);
|
|
2745
3394
|
}
|
|
2746
3395
|
function printStudioHelp() {
|
|
2747
|
-
console.log(`Tapi Studio commands
|
|
2748
|
-
|
|
2749
|
-
Usage:
|
|
2750
|
-
tapi studio [options]
|
|
2751
|
-
tapi studio install [options]
|
|
2752
|
-
tapi studio open [options]
|
|
2753
|
-
tapi studio doctor [options]
|
|
2754
|
-
|
|
2755
|
-
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:
|
|
2756
3405
|
--channel <name> Release channel: pilot, stable, or nightly
|
|
2757
|
-
--api-base-url <url> Tapi API base URL for
|
|
2758
|
-
--server <url> Alias for --api-base-url
|
|
2759
|
-
--workspace <path> Search this directory for .tapi/project.json
|
|
2760
|
-
--no-workspace Do not load .tapi/project.json
|
|
2761
|
-
--project <id> Tapi project id for workspace-bound Studio
|
|
2762
|
-
--project-slug <slug> Optional display slug for the bound project
|
|
2763
|
-
--install-token <tok> Preissued Studio install token (skips browser sign-in)
|
|
2764
|
-
--manifest <url> Exact release manifest URL for doctor only
|
|
2765
|
-
--cache-dir <path> Installer download cache directory
|
|
2766
|
-
--
|
|
2767
|
-
--
|
|
2768
|
-
--
|
|
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
|
|
2769
3419
|
`);
|
|
2770
3420
|
}
|
|
2771
3421
|
function printWorkspaceHelp(command) {
|
|
2772
|
-
console.log(`Tapi workspace ${command}
|
|
2773
|
-
|
|
2774
|
-
Usage:
|
|
2775
|
-
tapi ${command} --project PROJECT [--api-base-url URL] [--root PATH] [--force]
|
|
2776
|
-
|
|
2777
|
-
Options:
|
|
2778
|
-
--project <id> Tapi project id/name to bind this repo to
|
|
2779
|
-
--project-slug <slug> Optional display slug
|
|
2780
|
-
--api-base-url <url> Tapi API base URL stored in .tapi/project.json
|
|
2781
|
-
--server <url> Alias for --api-base-url
|
|
2782
|
-
--root <path> Directory where .tapi/project.json should be written
|
|
2783
|
-
--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
|
|
2784
3434
|
`);
|
|
2785
3435
|
}
|
|
2786
3436
|
function printServiceHelp() {
|
|
2787
|
-
console.log(`Tapi service commands
|
|
2788
|
-
|
|
2789
|
-
Usage:
|
|
2790
|
-
tapi service status
|
|
2791
|
-
tapi service start
|
|
2792
|
-
tapi service stop
|
|
2793
|
-
tapi service restart
|
|
2794
|
-
tapi service repair
|
|
2795
|
-
|
|
2796
|
-
Commands:
|
|
2797
|
-
status Print installed/running status for tapi-service
|
|
2798
|
-
start Start tapi-service
|
|
2799
|
-
stop Stop tapi-service
|
|
2800
|
-
restart Restart tapi-service
|
|
2801
|
-
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
|
|
2802
3452
|
`);
|
|
2803
3453
|
}
|
|
2804
3454
|
function printPublishHelp() {
|
|
2805
|
-
console.log(`Tapi publish
|
|
2806
|
-
|
|
2807
|
-
Usage:
|
|
2808
|
-
tapi publish [--api-base-url URL] [--api-key KEY] [--project PROJECT]
|
|
2809
|
-
|
|
2810
|
-
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.
|
|
2811
3461
|
`);
|
|
2812
3462
|
}
|
|
2813
3463
|
function formatError(error) {
|