@infomiho/buzz-cli 0.4.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 +80 -42
  2. package/package.json +4 -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");
@@ -147,74 +149,91 @@ function createSpinner(message) {
147
149
  }
148
150
 
149
151
  //#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");
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) {
154
161
  if (!existsSync(directory)) throw new CliError(`'${directory}' does not exist`);
155
162
  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();
163
+ return createZipBuffer(directory, { onProgress });
164
+ }
165
+ async function uploadSite(server, token, zip, subdomain, fetchFn = globalThis.fetch) {
179
166
  const boundary = "----BuzzFormBoundary" + Math.random().toString(36).slice(2);
180
167
  const header = `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="site.zip"\r\nContent-Type: application/zip\r\n\r\n`;
181
168
  const footer = `\r\n--${boundary}--\r\n`;
182
169
  const body = Buffer.concat([
183
170
  Buffer.from(header),
184
- zipBuffer,
171
+ zip,
185
172
  Buffer.from(footer)
186
173
  ]);
187
174
  const headers = {
188
175
  "Content-Type": `multipart/form-data; boundary=${boundary}`,
189
- ...authHeaders(options.token)
176
+ ...authHeaders(token)
190
177
  };
191
178
  if (subdomain) headers["x-subdomain"] = subdomain;
192
179
  let response;
193
180
  try {
194
- response = await fetch(`${options.server}/deploy`, {
181
+ response = await fetchFn(`${server}/deploy`, {
195
182
  method: "POST",
196
183
  headers,
197
184
  body
198
185
  });
199
186
  } catch (error) {
200
- uploadSpinner.stop("✗ Upload failed");
201
187
  throw new CliError(`Could not connect to server - ${error instanceof Error ? error.message : error}`);
202
188
  }
203
189
  const data = await response.json();
204
190
  if (response.ok) {
205
- uploadSpinner.stop("✓ Uploaded");
206
- console.log(`Deployed to ${data.url}`);
207
191
  const deployedSubdomain = new URL(data.url).hostname.split(".")[0];
208
- writeFileSync(cwdCnamePath, deployedSubdomain + "\n");
209
- return;
192
+ return {
193
+ url: data.url,
194
+ subdomain: deployedSubdomain
195
+ };
210
196
  }
211
- uploadSpinner.stop("✗ Upload failed");
212
197
  if (response.status === 401) throw new CliError("Not authenticated", "Run 'buzz login' first");
213
198
  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);
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);
201
+ }
202
+ throw new CliError(data.detail || "Unknown error");
203
+ }
204
+
205
+ //#endregion
206
+ //#region src/commands/deploy.ts
207
+ async function deploy(directory, subdomain) {
208
+ const options = getOptions();
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();
224
+ }
225
+ console.log(`Compressed to ${formatSize(zipBuffer.length)}`);
226
+ const uploadSpinner = createSpinner("Uploading");
227
+ uploadSpinner.start();
228
+ try {
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");
233
+ } catch (error) {
234
+ uploadSpinner.stop("✗ Upload failed");
235
+ throw error;
216
236
  }
217
- throw new CliError(data.error || "Unknown error");
218
237
  }
219
238
  function registerDeployCommand(program$1) {
220
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,7 +259,25 @@ function registerListCommand(program$1) {
240
259
 
241
260
  //#endregion
242
261
  //#region src/commands/delete.ts
243
- async function deleteSite(subdomain) {
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");
271
+ });
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;
279
+ }
280
+ }
244
281
  const response = await apiRequest(`/sites/${subdomain}`, { method: "DELETE" });
245
282
  if (response.status === 204) {
246
283
  console.log(`Deleted ${subdomain}`);
@@ -250,7 +287,7 @@ async function deleteSite(subdomain) {
250
287
  throw new CliError((await response.json()).error || "Unknown error");
251
288
  }
252
289
  function registerDeleteCommand(program$1) {
253
- program$1.command("delete <subdomain>").description("Delete a deployed site").action(deleteSite);
290
+ program$1.command("delete <subdomain>").description("Delete a deployed site").option("-y, --yes", "Skip confirmation prompt").action(deleteSite);
254
291
  }
255
292
 
256
293
  //#endregion
@@ -429,7 +466,8 @@ function registerCommands(program$1) {
429
466
 
430
467
  //#endregion
431
468
  //#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)");
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)");
433
471
  registerCommands(program);
434
472
  async function main() {
435
473
  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.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": {
@@ -33,7 +34,8 @@
33
34
  "@types/node": "^22.0.0",
34
35
  "publint": "^0.3.16",
35
36
  "tsdown": "^0.20.0-beta.4",
36
- "typescript": "^5.7.0"
37
+ "typescript": "^5.7.0",
38
+ "vitest": "^4.1.1"
37
39
  },
38
40
  "dependencies": {
39
41
  "archiver": "^7.0.0",