@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.
- package/dist/cli.mjs +341 -287
- 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/
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
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
|
-
|
|
171
|
+
zip,
|
|
74
172
|
Buffer.from(footer)
|
|
75
173
|
]);
|
|
76
174
|
const headers = {
|
|
77
175
|
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
78
|
-
...authHeaders(
|
|
176
|
+
...authHeaders(token)
|
|
79
177
|
};
|
|
80
178
|
if (subdomain) headers["x-subdomain"] = subdomain;
|
|
179
|
+
let response;
|
|
81
180
|
try {
|
|
82
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
136
|
-
|
|
234
|
+
uploadSpinner.stop("✗ Upload failed");
|
|
235
|
+
throw error;
|
|
137
236
|
}
|
|
138
237
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
})
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
258
|
-
|
|
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
|
|
283
|
-
if (
|
|
284
|
-
console.
|
|
285
|
-
|
|
411
|
+
const tokens = await (await apiRequest("/tokens")).json();
|
|
412
|
+
if (tokens.length === 0) {
|
|
413
|
+
console.log("No deployment tokens");
|
|
414
|
+
return;
|
|
286
415
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
|
365
|
-
if (
|
|
366
|
-
console.
|
|
367
|
-
|
|
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
|
-
|
|
395
|
-
program.command("
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
program.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
+
"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": {
|