@infomiho/buzz-cli 0.2.0 → 0.3.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/dist/cli.d.mts +2 -0
- package/dist/cli.mjs +424 -0
- package/package.json +10 -4
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -245
package/dist/cli.d.mts
ADDED
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from "commander";
|
|
3
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
//#region src/cli.ts
|
|
8
|
+
const CONFIG_PATH = join(homedir(), ".buzz.config.json");
|
|
9
|
+
const DEFAULT_SERVER = "http://localhost:8080";
|
|
10
|
+
function loadConfig() {
|
|
11
|
+
if (existsSync(CONFIG_PATH)) try {
|
|
12
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
function saveConfig(config) {
|
|
19
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
20
|
+
}
|
|
21
|
+
function getOptions() {
|
|
22
|
+
const config = loadConfig();
|
|
23
|
+
const opts = program.opts();
|
|
24
|
+
return {
|
|
25
|
+
server: opts.server || config.server || DEFAULT_SERVER,
|
|
26
|
+
token: opts.token || process.env.BUZZ_TOKEN || config.token
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function formatSize(bytes) {
|
|
30
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
31
|
+
if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
32
|
+
return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
|
|
33
|
+
}
|
|
34
|
+
function authHeaders(token) {
|
|
35
|
+
if (token) return { Authorization: `Bearer ${token}` };
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
async function createZipBuffer(directory) {
|
|
39
|
+
const archiver = await import("archiver");
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const archive = archiver.default("zip", { zlib: { level: 9 } });
|
|
42
|
+
const chunks = [];
|
|
43
|
+
archive.on("data", (chunk) => chunks.push(chunk));
|
|
44
|
+
archive.on("end", () => resolve(Buffer.concat(chunks)));
|
|
45
|
+
archive.on("error", reject);
|
|
46
|
+
archive.directory(directory, false);
|
|
47
|
+
archive.finalize();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async function deploy(directory, subdomain) {
|
|
51
|
+
const options = getOptions();
|
|
52
|
+
if (!options.token) {
|
|
53
|
+
console.error("Error: Not authenticated. Run 'buzz login' first");
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
if (!statSync(directory).isDirectory()) {
|
|
57
|
+
console.error(`Error: '${directory}' is not a directory`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
const cwdCnamePath = join(process.cwd(), "CNAME");
|
|
61
|
+
if (!subdomain && existsSync(cwdCnamePath)) subdomain = readFileSync(cwdCnamePath, "utf-8").trim();
|
|
62
|
+
else if (!subdomain) {
|
|
63
|
+
const dirCnamePath = join(directory, "CNAME");
|
|
64
|
+
if (existsSync(dirCnamePath)) subdomain = readFileSync(dirCnamePath, "utf-8").trim();
|
|
65
|
+
}
|
|
66
|
+
console.log(`Zipping ${directory}...`);
|
|
67
|
+
const zipBuffer = await createZipBuffer(directory);
|
|
68
|
+
const boundary = "----BuzzFormBoundary" + Math.random().toString(36).slice(2);
|
|
69
|
+
const header = `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="site.zip"\r\nContent-Type: application/zip\r\n\r\n`;
|
|
70
|
+
const footer = `\r\n--${boundary}--\r\n`;
|
|
71
|
+
const body = Buffer.concat([
|
|
72
|
+
Buffer.from(header),
|
|
73
|
+
zipBuffer,
|
|
74
|
+
Buffer.from(footer)
|
|
75
|
+
]);
|
|
76
|
+
const headers = {
|
|
77
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
78
|
+
...authHeaders(options.token)
|
|
79
|
+
};
|
|
80
|
+
if (subdomain) headers["x-subdomain"] = subdomain;
|
|
81
|
+
try {
|
|
82
|
+
const response = await fetch(`${options.server}/deploy`, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers,
|
|
85
|
+
body
|
|
86
|
+
});
|
|
87
|
+
const data = await response.json();
|
|
88
|
+
if (response.ok) {
|
|
89
|
+
console.log(`Deployed to ${data.url}`);
|
|
90
|
+
const deployedSubdomain = new URL(data.url).hostname.split(".")[0];
|
|
91
|
+
writeFileSync(cwdCnamePath, deployedSubdomain + "\n");
|
|
92
|
+
} else if (response.status === 401) {
|
|
93
|
+
console.error("Error: Not authenticated. Run 'buzz login' first");
|
|
94
|
+
process.exit(1);
|
|
95
|
+
} else if (response.status === 403) {
|
|
96
|
+
console.error(`Error: ${data.error}`);
|
|
97
|
+
if (data.error?.includes("owned by another user")) console.error("Tip: Choose a different subdomain with --subdomain <name>");
|
|
98
|
+
process.exit(1);
|
|
99
|
+
} else {
|
|
100
|
+
console.error(`Error: ${data.error || "Unknown error"}`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error(`Error: Could not connect to server - ${error instanceof Error ? error.message : error}`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function list() {
|
|
109
|
+
const options = getOptions();
|
|
110
|
+
if (!options.token) {
|
|
111
|
+
console.error("Error: Not authenticated. Run 'buzz login' first");
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const response = await fetch(`${options.server}/sites`, { headers: authHeaders(options.token) });
|
|
116
|
+
if (response.status === 401) {
|
|
117
|
+
console.error("Error: Session expired. Run 'buzz login' to re-authenticate");
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
if (response.status === 403) {
|
|
121
|
+
console.error("Error: Deploy tokens cannot list sites. Use 'buzz login' for a session token.");
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
const sites = await response.json();
|
|
125
|
+
if (sites.length === 0) {
|
|
126
|
+
console.log("No sites deployed");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
console.log(`${"NAME".padEnd(24)} ${"CREATED".padEnd(20)} ${"SIZE".padEnd(10)}`);
|
|
130
|
+
for (const site of sites) {
|
|
131
|
+
const created = site.created.slice(0, 19).replace("T", " ");
|
|
132
|
+
console.log(`${site.name.padEnd(24)} ${created.padEnd(20)} ${formatSize(site.size_bytes).padEnd(10)}`);
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error(`Error: Could not connect to server - ${error instanceof Error ? error.message : error}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function deleteSite(subdomain) {
|
|
140
|
+
const options = getOptions();
|
|
141
|
+
if (!options.token) {
|
|
142
|
+
console.error("Error: Not authenticated. Run 'buzz login' first");
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const response = await fetch(`${options.server}/sites/${subdomain}`, {
|
|
147
|
+
method: "DELETE",
|
|
148
|
+
headers: authHeaders(options.token)
|
|
149
|
+
});
|
|
150
|
+
if (response.status === 204) console.log(`Deleted ${subdomain}`);
|
|
151
|
+
else if (response.status === 401) {
|
|
152
|
+
console.error("Error: Session expired. Run 'buzz login' to re-authenticate");
|
|
153
|
+
process.exit(1);
|
|
154
|
+
} else if (response.status === 403) {
|
|
155
|
+
const data = await response.json();
|
|
156
|
+
console.error(`Error: ${data.error || "You don't have permission to delete this site"}`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
} else if (response.status === 404) {
|
|
159
|
+
console.error(`Error: Site '${subdomain}' not found`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
} else {
|
|
162
|
+
const data = await response.json();
|
|
163
|
+
console.error(`Error: ${data.error || "Unknown error"}`);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error(`Error: Could not connect to server - ${error instanceof Error ? error.message : error}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function configCommand(key, value) {
|
|
172
|
+
const config = loadConfig();
|
|
173
|
+
if (!key) {
|
|
174
|
+
if (Object.keys(config).length === 0) {
|
|
175
|
+
console.log("No configuration set");
|
|
176
|
+
console.log(`\nConfig file: ${CONFIG_PATH}`);
|
|
177
|
+
console.log("\nUsage:");
|
|
178
|
+
console.log(" buzz config server <url> Set server URL");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
console.log("Current configuration:");
|
|
182
|
+
if (config.server) console.log(` server: ${config.server}`);
|
|
183
|
+
if (config.token) console.log(` token: ${config.token.slice(0, 16)}...`);
|
|
184
|
+
console.log(`\nConfig file: ${CONFIG_PATH}`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (key === "server" && value) {
|
|
188
|
+
config.server = value;
|
|
189
|
+
saveConfig(config);
|
|
190
|
+
console.log(`Server set to ${value}`);
|
|
191
|
+
} else {
|
|
192
|
+
console.error("Usage: buzz config server <url>");
|
|
193
|
+
console.error("Use 'buzz login' to authenticate");
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function login() {
|
|
198
|
+
const options = getOptions();
|
|
199
|
+
try {
|
|
200
|
+
const deviceResponse = await fetch(`${options.server}/auth/device`, { method: "POST" });
|
|
201
|
+
if (!deviceResponse.ok) {
|
|
202
|
+
const data = await deviceResponse.json();
|
|
203
|
+
console.error(`Error: ${data.error || "Failed to start login"}`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
const deviceData = await deviceResponse.json();
|
|
207
|
+
console.log(`\nVisit: ${deviceData.verification_uri}`);
|
|
208
|
+
console.log(`Enter code: ${deviceData.user_code}\n`);
|
|
209
|
+
console.log("Waiting for authorization...");
|
|
210
|
+
const interval = (deviceData.interval || 5) * 1e3;
|
|
211
|
+
const maxAttempts = Math.ceil((deviceData.expires_in || 900) / (deviceData.interval || 5));
|
|
212
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
213
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
214
|
+
const pollData = await (await fetch(`${options.server}/auth/device/poll`, {
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: { "Content-Type": "application/json" },
|
|
217
|
+
body: JSON.stringify({ device_code: deviceData.device_code })
|
|
218
|
+
})).json();
|
|
219
|
+
if (pollData.status === "pending") continue;
|
|
220
|
+
if (pollData.error) {
|
|
221
|
+
console.error(`\nError: ${pollData.error}`);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
if (pollData.status === "complete") {
|
|
225
|
+
const config = loadConfig();
|
|
226
|
+
config.token = pollData.token;
|
|
227
|
+
saveConfig(config);
|
|
228
|
+
console.log(`\nLogged in as ${pollData.user.login}`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
console.error("\nLogin timed out");
|
|
233
|
+
process.exit(1);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error(`Error: Could not connect to server - ${error instanceof Error ? error.message : error}`);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function logout() {
|
|
240
|
+
const options = getOptions();
|
|
241
|
+
if (!options.token) {
|
|
242
|
+
console.log("Not logged in");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
await fetch(`${options.server}/auth/logout`, {
|
|
247
|
+
method: "POST",
|
|
248
|
+
headers: authHeaders(options.token)
|
|
249
|
+
});
|
|
250
|
+
} catch {}
|
|
251
|
+
const config = loadConfig();
|
|
252
|
+
delete config.token;
|
|
253
|
+
saveConfig(config);
|
|
254
|
+
console.log("Logged out");
|
|
255
|
+
}
|
|
256
|
+
async function whoami() {
|
|
257
|
+
const options = getOptions();
|
|
258
|
+
if (!options.token) {
|
|
259
|
+
console.log("Not logged in");
|
|
260
|
+
console.log("Run 'buzz login' to authenticate");
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
const response = await fetch(`${options.server}/auth/me`, { headers: authHeaders(options.token) });
|
|
265
|
+
if (response.status === 401) {
|
|
266
|
+
console.error("Session expired. Run 'buzz login' to re-authenticate");
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
if (!response.ok) {
|
|
270
|
+
const data = await response.json();
|
|
271
|
+
console.error(`Error: ${data.error || "Unknown error"}`);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
const user = await response.json();
|
|
275
|
+
console.log(`Logged in as ${user.login}${user.name ? ` (${user.name})` : ""}`);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error(`Error: Could not connect to server - ${error instanceof Error ? error.message : error}`);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async function listTokens() {
|
|
282
|
+
const options = getOptions();
|
|
283
|
+
if (!options.token) {
|
|
284
|
+
console.error("Not logged in. Run 'buzz login' first");
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
const response = await fetch(`${options.server}/tokens`, { headers: authHeaders(options.token) });
|
|
289
|
+
if (response.status === 401) {
|
|
290
|
+
console.error("Session expired. Run 'buzz login' to re-authenticate");
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
if (response.status === 403) {
|
|
294
|
+
console.error("Deploy tokens cannot list tokens. Use a session token.");
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
if (!response.ok) {
|
|
298
|
+
const data = await response.json();
|
|
299
|
+
console.error(`Error: ${data.error || "Unknown error"}`);
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
const tokens = await response.json();
|
|
303
|
+
if (tokens.length === 0) {
|
|
304
|
+
console.log("No deployment tokens");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
console.log(`${"ID".padEnd(18)} ${"NAME".padEnd(20)} ${"SITE".padEnd(20)} ${"LAST USED".padEnd(20)}`);
|
|
308
|
+
for (const token of tokens) {
|
|
309
|
+
const lastUsed = token.last_used_at ? token.last_used_at.slice(0, 19).replace("T", " ") : "Never";
|
|
310
|
+
console.log(`${token.id.padEnd(18)} ${token.name.slice(0, 18).padEnd(20)} ${token.site_name.slice(0, 18).padEnd(20)} ${lastUsed.padEnd(20)}`);
|
|
311
|
+
}
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.error(`Error: Could not connect to server - ${error instanceof Error ? error.message : error}`);
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function createToken(siteName, cmdOptions) {
|
|
318
|
+
const options = getOptions();
|
|
319
|
+
if (!options.token) {
|
|
320
|
+
console.error("Not logged in. Run 'buzz login' first");
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const response = await fetch(`${options.server}/tokens`, {
|
|
325
|
+
method: "POST",
|
|
326
|
+
headers: {
|
|
327
|
+
...authHeaders(options.token),
|
|
328
|
+
"Content-Type": "application/json"
|
|
329
|
+
},
|
|
330
|
+
body: JSON.stringify({
|
|
331
|
+
site_name: siteName,
|
|
332
|
+
name: cmdOptions.name || "Deployment token"
|
|
333
|
+
})
|
|
334
|
+
});
|
|
335
|
+
if (response.status === 401) {
|
|
336
|
+
console.error("Session expired. Run 'buzz login' to re-authenticate");
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
if (response.status === 403) {
|
|
340
|
+
const data$1 = await response.json();
|
|
341
|
+
console.error(`Error: ${data$1.error}`);
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
if (response.status === 404) {
|
|
345
|
+
console.error(`Error: Site '${siteName}' not found`);
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
if (!response.ok) {
|
|
349
|
+
const data$1 = await response.json();
|
|
350
|
+
console.error(`Error: ${data$1.error || "Unknown error"}`);
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
const data = await response.json();
|
|
354
|
+
console.log(`Token created for site '${siteName}':\n`);
|
|
355
|
+
console.log(` ${data.token}\n`);
|
|
356
|
+
console.log("Save this token - it won't be shown again!");
|
|
357
|
+
console.log("\nUse in CI by setting BUZZ_TOKEN environment variable.");
|
|
358
|
+
} catch (error) {
|
|
359
|
+
console.error(`Error: Could not connect to server - ${error instanceof Error ? error.message : error}`);
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async function deleteToken(tokenId) {
|
|
364
|
+
const options = getOptions();
|
|
365
|
+
if (!options.token) {
|
|
366
|
+
console.error("Not logged in. Run 'buzz login' first");
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
const response = await fetch(`${options.server}/tokens/${tokenId}`, {
|
|
371
|
+
method: "DELETE",
|
|
372
|
+
headers: authHeaders(options.token)
|
|
373
|
+
});
|
|
374
|
+
if (response.status === 204) {
|
|
375
|
+
console.log("Token deleted");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (response.status === 401) {
|
|
379
|
+
console.error("Session expired. Run 'buzz login' to re-authenticate");
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
if (response.status === 404) {
|
|
383
|
+
console.error("Token not found");
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
const data = await response.json();
|
|
387
|
+
console.error(`Error: ${data.error || "Unknown error"}`);
|
|
388
|
+
process.exit(1);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
console.error(`Error: Could not connect to server - ${error instanceof Error ? error.message : error}`);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
program.name("buzz").description("CLI for deploying static sites to Buzz hosting").version("1.0.0").option("-s, --server <url>", "Server URL (overrides config)").option("-t, --token <token>", "Auth token (overrides config)");
|
|
395
|
+
program.command("deploy <directory>").description("Deploy a directory to the server").option("--subdomain <name>", "Subdomain for the site").action((directory, cmdOptions) => deploy(directory, cmdOptions.subdomain));
|
|
396
|
+
program.command("list").description("List all deployed sites").action(list);
|
|
397
|
+
program.command("delete <subdomain>").description("Delete a deployed site").action(deleteSite);
|
|
398
|
+
program.command("config [key] [value]").description("View or set configuration (server)").action(configCommand);
|
|
399
|
+
program.command("url").description("Show the URL for the current directory").action(() => {
|
|
400
|
+
const cnamePath = join(process.cwd(), "CNAME");
|
|
401
|
+
if (!existsSync(cnamePath)) {
|
|
402
|
+
console.error("No CNAME file found. Deploy first with: buzz deploy .");
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
const subdomain = readFileSync(cnamePath, "utf-8").trim();
|
|
406
|
+
const server = loadConfig().server || DEFAULT_SERVER;
|
|
407
|
+
try {
|
|
408
|
+
const host = new URL(server).hostname;
|
|
409
|
+
console.log(`https://${subdomain}.${host}`);
|
|
410
|
+
} catch {
|
|
411
|
+
console.log(`http://${subdomain}.localhost:8080`);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
program.command("login").description("Login with GitHub OAuth").action(login);
|
|
415
|
+
program.command("logout").description("Logout and clear session").action(logout);
|
|
416
|
+
program.command("whoami").description("Show current logged-in user").action(whoami);
|
|
417
|
+
const tokensCmd = program.command("tokens").description("Manage deployment tokens");
|
|
418
|
+
tokensCmd.command("list").description("List your deployment tokens").action(listTokens);
|
|
419
|
+
tokensCmd.command("create <site>").description("Create a deployment token for a site").option("-n, --name <name>", "Token name (for identification)").action(createToken);
|
|
420
|
+
tokensCmd.command("delete <token-id>").description("Delete a deployment token").action(deleteToken);
|
|
421
|
+
program.parse();
|
|
422
|
+
|
|
423
|
+
//#endregion
|
|
424
|
+
export { };
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@infomiho/buzz-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "CLI for deploying static sites to Buzz hosting",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"buzz": "./dist/cli.
|
|
7
|
+
"buzz": "./dist/cli.mjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "
|
|
14
|
-
"dev": "
|
|
13
|
+
"build": "tsdown",
|
|
14
|
+
"dev": "tsdown --watch",
|
|
15
15
|
"prepublishOnly": "npm run build"
|
|
16
16
|
},
|
|
17
17
|
"repository": {
|
|
@@ -30,10 +30,16 @@
|
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/archiver": "^7.0.0",
|
|
32
32
|
"@types/node": "^22.0.0",
|
|
33
|
+
"publint": "^0.3.16",
|
|
34
|
+
"tsdown": "^0.20.0-beta.4",
|
|
33
35
|
"typescript": "^5.7.0"
|
|
34
36
|
},
|
|
35
37
|
"dependencies": {
|
|
36
38
|
"archiver": "^7.0.0",
|
|
37
39
|
"commander": "^13.0.0"
|
|
40
|
+
},
|
|
41
|
+
"exports": {
|
|
42
|
+
".": "./dist/cli.mjs",
|
|
43
|
+
"./package.json": "./package.json"
|
|
38
44
|
}
|
|
39
45
|
}
|
package/dist/cli.d.ts
DELETED
package/dist/cli.js
DELETED
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { program } from "commander";
|
|
3
|
-
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
const CONFIG_PATH = join(homedir(), ".buzz.config.json");
|
|
7
|
-
const DEFAULT_SERVER = "http://localhost:8080";
|
|
8
|
-
function loadConfig() {
|
|
9
|
-
if (existsSync(CONFIG_PATH)) {
|
|
10
|
-
try {
|
|
11
|
-
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
return {};
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return {};
|
|
18
|
-
}
|
|
19
|
-
function saveConfig(config) {
|
|
20
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
21
|
-
}
|
|
22
|
-
function getOptions() {
|
|
23
|
-
const config = loadConfig();
|
|
24
|
-
const opts = program.opts();
|
|
25
|
-
return {
|
|
26
|
-
server: opts.server || config.server || DEFAULT_SERVER,
|
|
27
|
-
token: opts.token || config.token,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
function formatSize(bytes) {
|
|
31
|
-
if (bytes < 1024)
|
|
32
|
-
return `${bytes} B`;
|
|
33
|
-
if (bytes < 1024 ** 2)
|
|
34
|
-
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
35
|
-
return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
|
|
36
|
-
}
|
|
37
|
-
function authHeaders(token) {
|
|
38
|
-
if (token) {
|
|
39
|
-
return { Authorization: `Bearer ${token}` };
|
|
40
|
-
}
|
|
41
|
-
return {};
|
|
42
|
-
}
|
|
43
|
-
async function createZipBuffer(directory) {
|
|
44
|
-
const archiver = await import("archiver");
|
|
45
|
-
return new Promise((resolve, reject) => {
|
|
46
|
-
const archive = archiver.default("zip", { zlib: { level: 9 } });
|
|
47
|
-
const chunks = [];
|
|
48
|
-
archive.on("data", (chunk) => chunks.push(chunk));
|
|
49
|
-
archive.on("end", () => resolve(Buffer.concat(chunks)));
|
|
50
|
-
archive.on("error", reject);
|
|
51
|
-
archive.directory(directory, false);
|
|
52
|
-
archive.finalize();
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
async function deploy(directory, subdomain) {
|
|
56
|
-
const options = getOptions();
|
|
57
|
-
const stat = statSync(directory);
|
|
58
|
-
if (!stat.isDirectory()) {
|
|
59
|
-
console.error(`Error: '${directory}' is not a directory`);
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
62
|
-
// Check for CNAME file if no subdomain specified (check cwd first, then directory)
|
|
63
|
-
const cwdCnamePath = join(process.cwd(), "CNAME");
|
|
64
|
-
if (!subdomain && existsSync(cwdCnamePath)) {
|
|
65
|
-
subdomain = readFileSync(cwdCnamePath, "utf-8").trim();
|
|
66
|
-
}
|
|
67
|
-
else if (!subdomain) {
|
|
68
|
-
const dirCnamePath = join(directory, "CNAME");
|
|
69
|
-
if (existsSync(dirCnamePath)) {
|
|
70
|
-
subdomain = readFileSync(dirCnamePath, "utf-8").trim();
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
console.log(`Zipping ${directory}...`);
|
|
74
|
-
const zipBuffer = await createZipBuffer(directory);
|
|
75
|
-
const boundary = "----BuzzFormBoundary" + Math.random().toString(36).slice(2);
|
|
76
|
-
const header = `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="site.zip"\r\nContent-Type: application/zip\r\n\r\n`;
|
|
77
|
-
const footer = `\r\n--${boundary}--\r\n`;
|
|
78
|
-
const body = Buffer.concat([
|
|
79
|
-
Buffer.from(header),
|
|
80
|
-
zipBuffer,
|
|
81
|
-
Buffer.from(footer),
|
|
82
|
-
]);
|
|
83
|
-
const headers = {
|
|
84
|
-
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
85
|
-
...authHeaders(options.token),
|
|
86
|
-
};
|
|
87
|
-
if (subdomain) {
|
|
88
|
-
headers["x-subdomain"] = subdomain;
|
|
89
|
-
}
|
|
90
|
-
try {
|
|
91
|
-
const response = await fetch(`${options.server}/deploy`, {
|
|
92
|
-
method: "POST",
|
|
93
|
-
headers,
|
|
94
|
-
body,
|
|
95
|
-
});
|
|
96
|
-
const data = await response.json();
|
|
97
|
-
if (response.ok) {
|
|
98
|
-
console.log(`Deployed to ${data.url}`);
|
|
99
|
-
// Save subdomain to CNAME file in cwd
|
|
100
|
-
const deployedSubdomain = new URL(data.url).hostname.split(".")[0];
|
|
101
|
-
writeFileSync(cwdCnamePath, deployedSubdomain + "\n");
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
console.error(`Error: ${data.error || "Unknown error"}`);
|
|
105
|
-
process.exit(1);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
catch (error) {
|
|
109
|
-
console.error(`Error: Could not connect to server - ${error instanceof Error ? error.message : error}`);
|
|
110
|
-
process.exit(1);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
async function list() {
|
|
114
|
-
const options = getOptions();
|
|
115
|
-
try {
|
|
116
|
-
const response = await fetch(`${options.server}/sites`, {
|
|
117
|
-
headers: authHeaders(options.token),
|
|
118
|
-
});
|
|
119
|
-
if (response.status === 401) {
|
|
120
|
-
console.error("Error: Unauthorized - check your token");
|
|
121
|
-
process.exit(1);
|
|
122
|
-
}
|
|
123
|
-
const sites = await response.json();
|
|
124
|
-
if (sites.length === 0) {
|
|
125
|
-
console.log("No sites deployed");
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
console.log(`${"NAME".padEnd(24)} ${"CREATED".padEnd(20)} ${"SIZE".padEnd(10)}`);
|
|
129
|
-
for (const site of sites) {
|
|
130
|
-
const created = site.created.slice(0, 19).replace("T", " ");
|
|
131
|
-
console.log(`${site.name.padEnd(24)} ${created.padEnd(20)} ${formatSize(site.size_bytes).padEnd(10)}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
console.error(`Error: Could not connect to server - ${error instanceof Error ? error.message : error}`);
|
|
136
|
-
process.exit(1);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
async function deleteSite(subdomain) {
|
|
140
|
-
const options = getOptions();
|
|
141
|
-
try {
|
|
142
|
-
const response = await fetch(`${options.server}/sites/${subdomain}`, {
|
|
143
|
-
method: "DELETE",
|
|
144
|
-
headers: authHeaders(options.token),
|
|
145
|
-
});
|
|
146
|
-
if (response.status === 204) {
|
|
147
|
-
console.log(`Deleted ${subdomain}`);
|
|
148
|
-
}
|
|
149
|
-
else if (response.status === 401) {
|
|
150
|
-
console.error("Error: Unauthorized - check your token");
|
|
151
|
-
process.exit(1);
|
|
152
|
-
}
|
|
153
|
-
else if (response.status === 404) {
|
|
154
|
-
console.error(`Error: Site '${subdomain}' not found`);
|
|
155
|
-
process.exit(1);
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
const data = await response.json();
|
|
159
|
-
console.error(`Error: ${data.error || "Unknown error"}`);
|
|
160
|
-
process.exit(1);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
catch (error) {
|
|
164
|
-
console.error(`Error: Could not connect to server - ${error instanceof Error ? error.message : error}`);
|
|
165
|
-
process.exit(1);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
function configCommand(key, value) {
|
|
169
|
-
const config = loadConfig();
|
|
170
|
-
if (!key) {
|
|
171
|
-
// Show current config
|
|
172
|
-
if (Object.keys(config).length === 0) {
|
|
173
|
-
console.log("No configuration set");
|
|
174
|
-
console.log(`\nConfig file: ${CONFIG_PATH}`);
|
|
175
|
-
console.log("\nUsage:");
|
|
176
|
-
console.log(" buzz config server <url> Set server URL");
|
|
177
|
-
console.log(" buzz config token <token> Set auth token");
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
console.log("Current configuration:");
|
|
181
|
-
if (config.server)
|
|
182
|
-
console.log(` server: ${config.server}`);
|
|
183
|
-
if (config.token)
|
|
184
|
-
console.log(` token: ${config.token.slice(0, 8)}...`);
|
|
185
|
-
console.log(`\nConfig file: ${CONFIG_PATH}`);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
if (key === "server" && value) {
|
|
189
|
-
config.server = value;
|
|
190
|
-
saveConfig(config);
|
|
191
|
-
console.log(`Server set to ${value}`);
|
|
192
|
-
}
|
|
193
|
-
else if (key === "token" && value) {
|
|
194
|
-
config.token = value;
|
|
195
|
-
saveConfig(config);
|
|
196
|
-
console.log("Token saved");
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
console.error("Usage: buzz config <server|token> <value>");
|
|
200
|
-
process.exit(1);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
program
|
|
204
|
-
.name("buzz")
|
|
205
|
-
.description("CLI for deploying static sites to Buzz hosting")
|
|
206
|
-
.version("1.0.0")
|
|
207
|
-
.option("-s, --server <url>", "Server URL (overrides config)")
|
|
208
|
-
.option("-t, --token <token>", "Auth token (overrides config)");
|
|
209
|
-
program
|
|
210
|
-
.command("deploy <directory> [subdomain]")
|
|
211
|
-
.description("Deploy a directory to the server")
|
|
212
|
-
.action(deploy);
|
|
213
|
-
program
|
|
214
|
-
.command("list")
|
|
215
|
-
.description("List all deployed sites")
|
|
216
|
-
.action(list);
|
|
217
|
-
program
|
|
218
|
-
.command("delete <subdomain>")
|
|
219
|
-
.description("Delete a deployed site")
|
|
220
|
-
.action(deleteSite);
|
|
221
|
-
program
|
|
222
|
-
.command("config [key] [value]")
|
|
223
|
-
.description("View or set configuration (server, token)")
|
|
224
|
-
.action(configCommand);
|
|
225
|
-
program
|
|
226
|
-
.command("url")
|
|
227
|
-
.description("Show the URL for the current directory")
|
|
228
|
-
.action(() => {
|
|
229
|
-
const cnamePath = join(process.cwd(), "CNAME");
|
|
230
|
-
if (!existsSync(cnamePath)) {
|
|
231
|
-
console.error("No CNAME file found. Deploy first with: buzz deploy .");
|
|
232
|
-
process.exit(1);
|
|
233
|
-
}
|
|
234
|
-
const subdomain = readFileSync(cnamePath, "utf-8").trim();
|
|
235
|
-
const config = loadConfig();
|
|
236
|
-
const server = config.server || DEFAULT_SERVER;
|
|
237
|
-
try {
|
|
238
|
-
const host = new URL(server).hostname;
|
|
239
|
-
console.log(`https://${subdomain}.${host}`);
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
console.log(`http://${subdomain}.localhost:8080`);
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
program.parse();
|