buildful 0.2.0
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/cli.js +128 -0
- package/package.json +18 -0
package/cli.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import https from "node:https";
|
|
4
|
+
import open from "open";
|
|
5
|
+
|
|
6
|
+
const [command, token, ...args] = process.argv.slice(2);
|
|
7
|
+
const portIndex = args.indexOf("--port");
|
|
8
|
+
const port = Number(portIndex >= 0 ? args[portIndex + 1] : 3000);
|
|
9
|
+
const apiIndex = args.indexOf("--api");
|
|
10
|
+
const api = apiIndex >= 0 ? args[apiIndex + 1] : "https://buildful.ai";
|
|
11
|
+
|
|
12
|
+
if (command !== "preview" || !token || !Number.isInteger(port) || port < 1 || port > 65535) {
|
|
13
|
+
console.error("Usage: buildful preview <token> [--port 3000]");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let server;
|
|
18
|
+
let stopToken;
|
|
19
|
+
let shuttingDown = false;
|
|
20
|
+
|
|
21
|
+
function openBrowser(url) {
|
|
22
|
+
void open(url).catch((error) => {
|
|
23
|
+
console.error(`Could not open the preview in your browser: ${error.message}`);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function stopPreview() {
|
|
28
|
+
server?.close();
|
|
29
|
+
try {
|
|
30
|
+
const stopResponse = await fetch(new URL("/api/preview/stop", api), {
|
|
31
|
+
method: "POST",
|
|
32
|
+
headers: { authorization: `Bearer ${stopToken}` },
|
|
33
|
+
signal: AbortSignal.timeout(15_000),
|
|
34
|
+
});
|
|
35
|
+
if (!stopResponse.ok) throw new Error(`stop request returned ${stopResponse.status}`);
|
|
36
|
+
console.log("Preview sandbox stopped.");
|
|
37
|
+
process.exit(0);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(`Could not stop preview sandbox: ${error.message}`);
|
|
40
|
+
console.error("It will stop automatically when its 45-minute timeout expires.");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function shutdown(signal) {
|
|
46
|
+
if (shuttingDown) return;
|
|
47
|
+
shuttingDown = true;
|
|
48
|
+
console.log(`\n${signal === "SIGINT" ? "Ctrl+C received" : "Shutdown requested"}. Stopping preview sandbox…`);
|
|
49
|
+
if (stopToken) void stopPreview();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
53
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
54
|
+
|
|
55
|
+
console.log("Starting a dedicated preview sandbox. This can take a few minutes…");
|
|
56
|
+
const response = await fetch(new URL("/api/preview/resolve", api), {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { authorization: `Bearer ${token}` },
|
|
59
|
+
});
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const result = await response.json().catch(() => ({}));
|
|
62
|
+
throw new Error(result.error || `Could not resolve preview (${response.status}).`);
|
|
63
|
+
}
|
|
64
|
+
const preview = await response.json();
|
|
65
|
+
stopToken = preview.stopToken;
|
|
66
|
+
if (shuttingDown) {
|
|
67
|
+
await stopPreview();
|
|
68
|
+
}
|
|
69
|
+
const { target, expiresAt } = preview;
|
|
70
|
+
const upstream = new URL(target);
|
|
71
|
+
|
|
72
|
+
server = http.createServer((request, reply) => {
|
|
73
|
+
const localOrigin = `http://localhost:${port}`;
|
|
74
|
+
const headers = {
|
|
75
|
+
...request.headers,
|
|
76
|
+
host: upstream.host,
|
|
77
|
+
"x-forwarded-host": `localhost:${port}`,
|
|
78
|
+
"x-forwarded-proto": "http",
|
|
79
|
+
};
|
|
80
|
+
const proxy = https.request(new URL(request.url || "/", upstream), {
|
|
81
|
+
method: request.method,
|
|
82
|
+
headers,
|
|
83
|
+
}, (upstreamResponse) => {
|
|
84
|
+
const responseHeaders = { ...upstreamResponse.headers };
|
|
85
|
+
if (typeof responseHeaders.location === "string") {
|
|
86
|
+
responseHeaders.location = responseHeaders.location.replace(upstream.origin, localOrigin);
|
|
87
|
+
}
|
|
88
|
+
reply.writeHead(upstreamResponse.statusCode || 502, responseHeaders);
|
|
89
|
+
upstreamResponse.pipe(reply);
|
|
90
|
+
});
|
|
91
|
+
proxy.on("error", (error) => {
|
|
92
|
+
if (!reply.headersSent) reply.writeHead(502);
|
|
93
|
+
reply.end(`Preview unavailable: ${error.message}`);
|
|
94
|
+
});
|
|
95
|
+
request.pipe(proxy);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
server.on("upgrade", (request, socket, head) => {
|
|
99
|
+
const proxy = https.request({
|
|
100
|
+
hostname: upstream.hostname,
|
|
101
|
+
port: 443,
|
|
102
|
+
path: request.url,
|
|
103
|
+
method: "GET",
|
|
104
|
+
headers: {
|
|
105
|
+
...request.headers,
|
|
106
|
+
host: upstream.host,
|
|
107
|
+
"x-forwarded-host": `localhost:${port}`,
|
|
108
|
+
"x-forwarded-proto": "http",
|
|
109
|
+
connection: "Upgrade",
|
|
110
|
+
upgrade: request.headers.upgrade || "websocket",
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
proxy.on("upgrade", (response, upstreamSocket, upstreamHead) => {
|
|
114
|
+
socket.write(`HTTP/1.1 101 Switching Protocols\r\n${Object.entries(response.headers).map(([key, value]) => `${key}: ${value}`).join("\r\n")}\r\n\r\n`);
|
|
115
|
+
if (head.length) upstreamSocket.write(head);
|
|
116
|
+
if (upstreamHead.length) socket.write(upstreamHead);
|
|
117
|
+
upstreamSocket.pipe(socket).pipe(upstreamSocket);
|
|
118
|
+
});
|
|
119
|
+
proxy.on("error", () => socket.destroy());
|
|
120
|
+
proxy.end();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
server.listen(port, "127.0.0.1", () => {
|
|
124
|
+
const url = `http://localhost:${port}`;
|
|
125
|
+
console.log(`Buildful preview: ${url}`);
|
|
126
|
+
console.log(`Available until ${new Date(expiresAt).toLocaleString()}. Press Ctrl+C to stop.`);
|
|
127
|
+
openBrowser(url);
|
|
128
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "buildful",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Connect a local port to a Buildful sandbox preview",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"buildful": "cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"cli.js"
|
|
11
|
+
],
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"open": "^10.2.0"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
}
|
|
18
|
+
}
|