cli/src/api.tsblame
View source
59e66671import { log } from "@clack/prompts";
90d5eb82import { readFileSync } from "node:fs";
90d5eb83import { basename } from "node:path";
e93a9784import { getHub, getToken } from "./config.js";
e93a9785
e93a9786export async function hubRequest<T>(
e93a9787 path: string,
e93a9788 options: RequestInit = {}
e93a9789): Promise<T> {
e93a97810 const hub = await getHub();
e93a97811 const token = await getToken();
e93a97812
e93a97813 const url = `${hub}${path}`;
e93a97814 const res = await fetch(url, {
e93a97815 ...options,
e93a97816 headers: {
e93a97817 "Content-Type": "application/json",
e93a97818 Authorization: `Bearer ${token}`,
e93a97819 ...options.headers,
e93a97820 },
e93a97821 });
e93a97822
e93a97823 if (!res.ok) {
e93a97824 const body = await res.text();
e93a97825 let message: string;
e93a97826 try {
e93a97827 message = JSON.parse(body).error || body;
e93a97828 } catch {
e93a97829 message = body;
e93a97830 }
59e666731 log.error(`Error ${res.status}: ${message}`);
e93a97832 process.exit(1);
e93a97833 }
e93a97834
e93a97835 return res.json() as Promise<T>;
e93a97836}
90d5eb837
90d5eb838/**
90d5eb839 * Upload a file to a hub endpoint and stream SSE events back.
90d5eb840 * Used for import-bundle which streams gitimport progress.
90d5eb841 */
90d5eb842export async function hubUploadStream(
90d5eb843 path: string,
90d5eb844 filePath: string,
90d5eb845 onEvent: (event: string, data: any) => void,
90d5eb846): Promise<void> {
90d5eb847 const hub = await getHub();
90d5eb848 const token = await getToken();
90d5eb849
90d5eb850 const fileData = readFileSync(filePath);
90d5eb851 const blob = new Blob([fileData], { type: "application/gzip" });
90d5eb852
90d5eb853 const form = new FormData();
90d5eb854 form.append("file", blob, basename(filePath));
90d5eb855
90d5eb856 const res = await fetch(`${hub}${path}`, {
90d5eb857 method: "POST",
90d5eb858 headers: {
90d5eb859 Authorization: `Bearer ${token}`,
90d5eb860 },
90d5eb861 body: form,
90d5eb862 });
90d5eb863
90d5eb864 if (!res.ok && !res.headers.get("content-type")?.includes("text/event-stream")) {
90d5eb865 const body = await res.text();
90d5eb866 throw new Error(`Upload failed (${res.status}): ${body}`);
90d5eb867 }
90d5eb868
90d5eb869 // Parse SSE stream
90d5eb870 const reader = res.body!.getReader();
90d5eb871 const decoder = new TextDecoder();
90d5eb872 let buffer = "";
90d5eb873
90d5eb874 while (true) {
90d5eb875 const { done, value } = await reader.read();
90d5eb876 if (done) break;
90d5eb877
90d5eb878 buffer += decoder.decode(value, { stream: true });
90d5eb879 const lines = buffer.split("\n");
90d5eb880 buffer = lines.pop()!; // keep incomplete line in buffer
90d5eb881
90d5eb882 let currentEvent = "";
90d5eb883 for (const line of lines) {
90d5eb884 if (line.startsWith("event: ")) {
90d5eb885 currentEvent = line.slice(7);
90d5eb886 } else if (line.startsWith("data: ")) {
90d5eb887 try {
90d5eb888 const data = JSON.parse(line.slice(6));
90d5eb889 onEvent(currentEvent, data);
90d5eb890 if (currentEvent === "error") {
90d5eb891 throw new Error(data.message ?? "Import failed");
90d5eb892 }
90d5eb893 } catch (e) {
90d5eb894 if (e instanceof SyntaxError) continue; // skip malformed JSON
90d5eb895 throw e;
90d5eb896 }
90d5eb897 }
90d5eb898 }
90d5eb899 }
90d5eb8100}