2.5 KB101 lines
Blame
1import { log } from "@clack/prompts";
2import { readFileSync } from "node:fs";
3import { basename } from "node:path";
4import { getHub, getToken } from "./config.js";
5
6export async function hubRequest<T>(
7 path: string,
8 options: RequestInit = {}
9): Promise<T> {
10 const hub = await getHub();
11 const token = await getToken();
12
13 const url = `${hub}${path}`;
14 const res = await fetch(url, {
15 ...options,
16 headers: {
17 "Content-Type": "application/json",
18 Authorization: `Bearer ${token}`,
19 ...options.headers,
20 },
21 });
22
23 if (!res.ok) {
24 const body = await res.text();
25 let message: string;
26 try {
27 message = JSON.parse(body).error || body;
28 } catch {
29 message = body;
30 }
31 log.error(`Error ${res.status}: ${message}`);
32 process.exit(1);
33 }
34
35 return res.json() as Promise<T>;
36}
37
38/**
39 * Upload a file to a hub endpoint and stream SSE events back.
40 * Used for import-bundle which streams gitimport progress.
41 */
42export async function hubUploadStream(
43 path: string,
44 filePath: string,
45 onEvent: (event: string, data: any) => void,
46): Promise<void> {
47 const hub = await getHub();
48 const token = await getToken();
49
50 const fileData = readFileSync(filePath);
51 const blob = new Blob([fileData], { type: "application/gzip" });
52
53 const form = new FormData();
54 form.append("file", blob, basename(filePath));
55
56 const res = await fetch(`${hub}${path}`, {
57 method: "POST",
58 headers: {
59 Authorization: `Bearer ${token}`,
60 },
61 body: form,
62 });
63
64 if (!res.ok && !res.headers.get("content-type")?.includes("text/event-stream")) {
65 const body = await res.text();
66 throw new Error(`Upload failed (${res.status}): ${body}`);
67 }
68
69 // Parse SSE stream
70 const reader = res.body!.getReader();
71 const decoder = new TextDecoder();
72 let buffer = "";
73
74 while (true) {
75 const { done, value } = await reader.read();
76 if (done) break;
77
78 buffer += decoder.decode(value, { stream: true });
79 const lines = buffer.split("\n");
80 buffer = lines.pop()!; // keep incomplete line in buffer
81
82 let currentEvent = "";
83 for (const line of lines) {
84 if (line.startsWith("event: ")) {
85 currentEvent = line.slice(7);
86 } else if (line.startsWith("data: ")) {
87 try {
88 const data = JSON.parse(line.slice(6));
89 onEvent(currentEvent, data);
90 if (currentEvent === "error") {
91 throw new Error(data.message ?? "Import failed");
92 }
93 } catch (e) {
94 if (e instanceof SyntaxError) continue; // skip malformed JSON
95 throw e;
96 }
97 }
98 }
99 }
100}
101