@infomiho/buzz-cli 0.1.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.ts +2 -0
- package/dist/cli.js +239 -0
- package/package.json +39 -0
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
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
|
|
63
|
+
const cnamePath = join(directory, "CNAME");
|
|
64
|
+
if (!subdomain && existsSync(cnamePath)) {
|
|
65
|
+
subdomain = readFileSync(cnamePath, "utf-8").trim();
|
|
66
|
+
}
|
|
67
|
+
console.log(`Zipping ${directory}...`);
|
|
68
|
+
const zipBuffer = await createZipBuffer(directory);
|
|
69
|
+
const boundary = "----BuzzFormBoundary" + Math.random().toString(36).slice(2);
|
|
70
|
+
const header = `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="site.zip"\r\nContent-Type: application/zip\r\n\r\n`;
|
|
71
|
+
const footer = `\r\n--${boundary}--\r\n`;
|
|
72
|
+
const body = Buffer.concat([
|
|
73
|
+
Buffer.from(header),
|
|
74
|
+
zipBuffer,
|
|
75
|
+
Buffer.from(footer),
|
|
76
|
+
]);
|
|
77
|
+
const headers = {
|
|
78
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
79
|
+
...authHeaders(options.token),
|
|
80
|
+
};
|
|
81
|
+
if (subdomain) {
|
|
82
|
+
headers["x-subdomain"] = subdomain;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetch(`${options.server}/deploy`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers,
|
|
88
|
+
body,
|
|
89
|
+
});
|
|
90
|
+
const data = await response.json();
|
|
91
|
+
if (response.ok) {
|
|
92
|
+
console.log(`Deployed to ${data.url}`);
|
|
93
|
+
// Save subdomain to CNAME file
|
|
94
|
+
const deployedSubdomain = new URL(data.url).hostname.split(".")[0];
|
|
95
|
+
writeFileSync(cnamePath, deployedSubdomain + "\n");
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.error(`Error: ${data.error || "Unknown error"}`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
console.error(`Error: Could not connect to server - ${error instanceof Error ? error.message : error}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function list() {
|
|
108
|
+
const options = getOptions();
|
|
109
|
+
try {
|
|
110
|
+
const response = await fetch(`${options.server}/sites`, {
|
|
111
|
+
headers: authHeaders(options.token),
|
|
112
|
+
});
|
|
113
|
+
if (response.status === 401) {
|
|
114
|
+
console.error("Error: Unauthorized - check your token");
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
const sites = await response.json();
|
|
118
|
+
if (sites.length === 0) {
|
|
119
|
+
console.log("No sites deployed");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
console.log(`${"NAME".padEnd(24)} ${"CREATED".padEnd(20)} ${"SIZE".padEnd(10)}`);
|
|
123
|
+
for (const site of sites) {
|
|
124
|
+
const created = site.created.slice(0, 19).replace("T", " ");
|
|
125
|
+
console.log(`${site.name.padEnd(24)} ${created.padEnd(20)} ${formatSize(site.size_bytes).padEnd(10)}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.error(`Error: Could not connect to server - ${error instanceof Error ? error.message : error}`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function deleteSite(subdomain) {
|
|
134
|
+
const options = getOptions();
|
|
135
|
+
try {
|
|
136
|
+
const response = await fetch(`${options.server}/sites/${subdomain}`, {
|
|
137
|
+
method: "DELETE",
|
|
138
|
+
headers: authHeaders(options.token),
|
|
139
|
+
});
|
|
140
|
+
if (response.status === 204) {
|
|
141
|
+
console.log(`Deleted ${subdomain}`);
|
|
142
|
+
}
|
|
143
|
+
else if (response.status === 401) {
|
|
144
|
+
console.error("Error: Unauthorized - check your token");
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
else if (response.status === 404) {
|
|
148
|
+
console.error(`Error: Site '${subdomain}' not found`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const data = await response.json();
|
|
153
|
+
console.error(`Error: ${data.error || "Unknown error"}`);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
console.error(`Error: Could not connect to server - ${error instanceof Error ? error.message : error}`);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function configCommand(key, value) {
|
|
163
|
+
const config = loadConfig();
|
|
164
|
+
if (!key) {
|
|
165
|
+
// Show current config
|
|
166
|
+
if (Object.keys(config).length === 0) {
|
|
167
|
+
console.log("No configuration set");
|
|
168
|
+
console.log(`\nConfig file: ${CONFIG_PATH}`);
|
|
169
|
+
console.log("\nUsage:");
|
|
170
|
+
console.log(" buzz config server <url> Set server URL");
|
|
171
|
+
console.log(" buzz config token <token> Set auth token");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
console.log("Current configuration:");
|
|
175
|
+
if (config.server)
|
|
176
|
+
console.log(` server: ${config.server}`);
|
|
177
|
+
if (config.token)
|
|
178
|
+
console.log(` token: ${config.token.slice(0, 8)}...`);
|
|
179
|
+
console.log(`\nConfig file: ${CONFIG_PATH}`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (key === "server" && value) {
|
|
183
|
+
config.server = value;
|
|
184
|
+
saveConfig(config);
|
|
185
|
+
console.log(`Server set to ${value}`);
|
|
186
|
+
}
|
|
187
|
+
else if (key === "token" && value) {
|
|
188
|
+
config.token = value;
|
|
189
|
+
saveConfig(config);
|
|
190
|
+
console.log("Token saved");
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
console.error("Usage: buzz config <server|token> <value>");
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
program
|
|
198
|
+
.name("buzz")
|
|
199
|
+
.description("CLI for deploying static sites to Buzz hosting")
|
|
200
|
+
.version("1.0.0")
|
|
201
|
+
.option("-s, --server <url>", "Server URL (overrides config)")
|
|
202
|
+
.option("-t, --token <token>", "Auth token (overrides config)");
|
|
203
|
+
program
|
|
204
|
+
.command("deploy <directory> [subdomain]")
|
|
205
|
+
.description("Deploy a directory to the server")
|
|
206
|
+
.action(deploy);
|
|
207
|
+
program
|
|
208
|
+
.command("list")
|
|
209
|
+
.description("List all deployed sites")
|
|
210
|
+
.action(list);
|
|
211
|
+
program
|
|
212
|
+
.command("delete <subdomain>")
|
|
213
|
+
.description("Delete a deployed site")
|
|
214
|
+
.action(deleteSite);
|
|
215
|
+
program
|
|
216
|
+
.command("config [key] [value]")
|
|
217
|
+
.description("View or set configuration (server, token)")
|
|
218
|
+
.action(configCommand);
|
|
219
|
+
program
|
|
220
|
+
.command("url")
|
|
221
|
+
.description("Show the URL for the current directory")
|
|
222
|
+
.action(() => {
|
|
223
|
+
const cnamePath = join(process.cwd(), "CNAME");
|
|
224
|
+
if (!existsSync(cnamePath)) {
|
|
225
|
+
console.error("No CNAME file found. Deploy first with: buzz deploy .");
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
const subdomain = readFileSync(cnamePath, "utf-8").trim();
|
|
229
|
+
const config = loadConfig();
|
|
230
|
+
const server = config.server || DEFAULT_SERVER;
|
|
231
|
+
try {
|
|
232
|
+
const host = new URL(server).hostname;
|
|
233
|
+
console.log(`https://${subdomain}.${host}`);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
console.log(`http://${subdomain}.localhost:8080`);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@infomiho/buzz-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for deploying static sites to Buzz hosting",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"buzz": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsc --watch",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/infomiho/buzz-static-hosting.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/infomiho/buzz-static-hosting#readme",
|
|
22
|
+
"keywords": [
|
|
23
|
+
"static-site",
|
|
24
|
+
"hosting",
|
|
25
|
+
"deploy",
|
|
26
|
+
"cli"
|
|
27
|
+
],
|
|
28
|
+
"author": "",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/archiver": "^7.0.0",
|
|
32
|
+
"@types/node": "^22.0.0",
|
|
33
|
+
"typescript": "^5.7.0"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"archiver": "^7.0.0",
|
|
37
|
+
"commander": "^13.0.0"
|
|
38
|
+
}
|
|
39
|
+
}
|