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