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