@infomiho/buzz-cli 0.4.0 → 0.6.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 +97 -43
  2. package/package.json +5 -2
package/dist/cli.mjs CHANGED
@@ -1,9 +1,11 @@
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
5
  import { join } from "node:path";
5
6
  import { homedir } from "node:os";
6
7
  import cliProgress from "cli-progress";
8
+ import { createInterface } from "readline";
7
9
 
8
10
  //#region src/lib.ts
9
11
  const CONFIG_PATH = join(homedir(), ".buzz.config.json");
@@ -78,6 +80,17 @@ async function apiRequest(path, options = {}, { requireAuth = true } = {}) {
78
80
  if (response.status === 403) throw new ApiError((await response.json()).error || "Permission denied", 403);
79
81
  return response;
80
82
  }
83
+ const IGNORED_DIRS = [
84
+ ".git",
85
+ "node_modules",
86
+ ".vscode",
87
+ ".idea"
88
+ ];
89
+ const IGNORED_FILES = [
90
+ "**/.DS_Store",
91
+ "**/.env",
92
+ "**/.env.*"
93
+ ];
81
94
  async function createZipBuffer(directory, callbacks) {
82
95
  const archiver = await import("archiver");
83
96
  return new Promise((resolve, reject) => {
@@ -89,7 +102,12 @@ async function createZipBuffer(directory, callbacks) {
89
102
  archive.on("progress", (progress) => {
90
103
  callbacks?.onProgress?.(progress.entries.processed, progress.entries.total);
91
104
  });
92
- archive.directory(directory, false);
105
+ archive.glob("**/*", {
106
+ cwd: directory,
107
+ dot: true,
108
+ skip: IGNORED_DIRS,
109
+ ignore: IGNORED_FILES
110
+ });
93
111
  archive.finalize();
94
112
  });
95
113
  }
@@ -147,74 +165,91 @@ function createSpinner(message) {
147
165
  }
148
166
 
149
167
  //#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");
168
+ //#region src/deploy.ts
169
+ function resolveSubdomain(cwd, directory, explicit) {
170
+ if (explicit) return explicit;
171
+ const cwdCname = join(cwd, "CNAME");
172
+ if (existsSync(cwdCname)) return readFileSync(cwdCname, "utf-8").trim();
173
+ const dirCname = join(directory, "CNAME");
174
+ if (existsSync(dirCname)) return readFileSync(dirCname, "utf-8").trim();
175
+ }
176
+ async function packSite(directory, onProgress) {
154
177
  if (!existsSync(directory)) throw new CliError(`'${directory}' does not exist`);
155
178
  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
+ return createZipBuffer(directory, { onProgress });
180
+ }
181
+ async function uploadSite(server, token, zip, subdomain, fetchFn = globalThis.fetch) {
179
182
  const boundary = "----BuzzFormBoundary" + Math.random().toString(36).slice(2);
180
183
  const header = `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="site.zip"\r\nContent-Type: application/zip\r\n\r\n`;
181
184
  const footer = `\r\n--${boundary}--\r\n`;
182
185
  const body = Buffer.concat([
183
186
  Buffer.from(header),
184
- zipBuffer,
187
+ zip,
185
188
  Buffer.from(footer)
186
189
  ]);
187
190
  const headers = {
188
191
  "Content-Type": `multipart/form-data; boundary=${boundary}`,
189
- ...authHeaders(options.token)
192
+ ...authHeaders(token)
190
193
  };
191
194
  if (subdomain) headers["x-subdomain"] = subdomain;
192
195
  let response;
193
196
  try {
194
- response = await fetch(`${options.server}/deploy`, {
197
+ response = await fetchFn(`${server}/deploy`, {
195
198
  method: "POST",
196
199
  headers,
197
200
  body
198
201
  });
199
202
  } catch (error) {
200
- uploadSpinner.stop("✗ Upload failed");
201
203
  throw new CliError(`Could not connect to server - ${error instanceof Error ? error.message : error}`);
202
204
  }
203
205
  const data = await response.json();
204
206
  if (response.ok) {
205
- uploadSpinner.stop("✓ Uploaded");
206
- console.log(`Deployed to ${data.url}`);
207
207
  const deployedSubdomain = new URL(data.url).hostname.split(".")[0];
208
- writeFileSync(cwdCnamePath, deployedSubdomain + "\n");
209
- return;
208
+ return {
209
+ url: data.url,
210
+ subdomain: deployedSubdomain
211
+ };
210
212
  }
211
- uploadSpinner.stop("✗ Upload failed");
212
213
  if (response.status === 401) throw new CliError("Not authenticated", "Run 'buzz login' first");
213
214
  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);
215
+ const tip = data.detail?.includes("owned by another user") ? "Choose a different subdomain with --subdomain <name>" : void 0;
216
+ throw new CliError(data.detail || "Permission denied", tip);
217
+ }
218
+ throw new CliError(data.detail || "Unknown error");
219
+ }
220
+
221
+ //#endregion
222
+ //#region src/commands/deploy.ts
223
+ async function deploy(directory, subdomain) {
224
+ const options = getOptions();
225
+ if (!options.token) throw new CliError("Not authenticated", "Run 'buzz login' first");
226
+ subdomain = resolveSubdomain(process.cwd(), directory, subdomain);
227
+ const progressBar = createProgressBar("Zipping");
228
+ let progressStarted = false;
229
+ let zipBuffer;
230
+ try {
231
+ zipBuffer = await packSite(directory, (processed, total) => {
232
+ if (!progressStarted && total > 0) {
233
+ progressBar.start(total, 0);
234
+ progressStarted = true;
235
+ }
236
+ if (progressStarted) progressBar.update(processed);
237
+ });
238
+ } finally {
239
+ if (progressStarted) progressBar.stop();
240
+ }
241
+ console.log(`Compressed to ${formatSize(zipBuffer.length)}`);
242
+ const uploadSpinner = createSpinner("Uploading");
243
+ uploadSpinner.start();
244
+ try {
245
+ const result = await uploadSite(options.server, options.token, zipBuffer, subdomain);
246
+ uploadSpinner.stop("✓ Uploaded");
247
+ console.log(`Deployed to ${result.url}`);
248
+ writeFileSync(join(process.cwd(), "CNAME"), result.subdomain + "\n");
249
+ } catch (error) {
250
+ uploadSpinner.stop("✗ Upload failed");
251
+ throw error;
216
252
  }
217
- throw new CliError(data.error || "Unknown error");
218
253
  }
219
254
  function registerDeployCommand(program$1) {
220
255
  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,7 +275,25 @@ function registerListCommand(program$1) {
240
275
 
241
276
  //#endregion
242
277
  //#region src/commands/delete.ts
243
- async function deleteSite(subdomain) {
278
+ function confirm(message) {
279
+ const rl = createInterface({
280
+ input: process.stdin,
281
+ output: process.stdout
282
+ });
283
+ return new Promise((resolve) => {
284
+ rl.question(`${message} [y/N] `, (answer) => {
285
+ rl.close();
286
+ resolve(answer.toLowerCase() === "y");
287
+ });
288
+ });
289
+ }
290
+ async function deleteSite(subdomain, options) {
291
+ if (!options.yes) {
292
+ if (!await confirm(`Delete site '${subdomain}'?`)) {
293
+ console.log("Aborted.");
294
+ return;
295
+ }
296
+ }
244
297
  const response = await apiRequest(`/sites/${subdomain}`, { method: "DELETE" });
245
298
  if (response.status === 204) {
246
299
  console.log(`Deleted ${subdomain}`);
@@ -250,7 +303,7 @@ async function deleteSite(subdomain) {
250
303
  throw new CliError((await response.json()).error || "Unknown error");
251
304
  }
252
305
  function registerDeleteCommand(program$1) {
253
- program$1.command("delete <subdomain>").description("Delete a deployed site").action(deleteSite);
306
+ program$1.command("delete <subdomain>").description("Delete a deployed site").option("-y, --yes", "Skip confirmation prompt").action(deleteSite);
254
307
  }
255
308
 
256
309
  //#endregion
@@ -429,7 +482,8 @@ function registerCommands(program$1) {
429
482
 
430
483
  //#endregion
431
484
  //#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)");
485
+ const { version } = createRequire(import.meta.url)("../package.json");
486
+ 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)");
433
487
  registerCommands(program);
434
488
  async function main() {
435
489
  await program.parseAsync();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infomiho/buzz-cli",
3
- "version": "0.4.0",
3
+ "version": "0.6.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": {
@@ -31,9 +32,11 @@
31
32
  "@types/archiver": "^7.0.0",
32
33
  "@types/cli-progress": "^3.11.6",
33
34
  "@types/node": "^22.0.0",
35
+ "jszip": "^3.10.1",
34
36
  "publint": "^0.3.16",
35
37
  "tsdown": "^0.20.0-beta.4",
36
- "typescript": "^5.7.0"
38
+ "typescript": "^5.7.0",
39
+ "vitest": "^4.1.1"
37
40
  },
38
41
  "dependencies": {
39
42
  "archiver": "^7.0.0",