@kaooffline/quickhost 0.1.1 → 0.2.1
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/README.md +1 -1
- package/bin/quickhost.js +343 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ quickhost delete --app x remove an app now
|
|
|
31
31
|
quickhost whoami current login
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Deploy auto-detects `dist/`, `build/`, `out
|
|
34
|
+
Deploy auto-detects `dist/`, `dist/client/` (vinext static export), `build/`, `out/` (Next export), `.output/public/` (Nuxt), `public/` and runs your `build` script once if nothing is built yet (npm/pnpm/yarn/bun aware). Needs `index.html` at the publish dir root. Static only, SPA fallback included. SSR apps (vinext/Next/Nuxt with API routes) are rejected with a clear error — they need a Node server, not R2.
|
|
35
35
|
|
|
36
36
|
## Limits
|
|
37
37
|
|
package/bin/quickhost.js
CHANGED
|
@@ -1,22 +1,70 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// quickhost CLI - zero dependencies, Node 18+.
|
|
3
|
+
// UI kit is hand-rolled ANSI (looks like chalk+ora+boxen, no deps so npx stays fast).
|
|
3
4
|
// Usage:
|
|
4
5
|
// npx @kaooffline/quickhost register -u alice
|
|
5
6
|
// npx @kaooffline/quickhost login -u alice
|
|
6
7
|
// npx @kaooffline/quickhost deploy [--app my-app] [--dir ./dist] [--api https://api.kaooffline.top]
|
|
7
|
-
// npx @kaooffline/quickhost list |
|
|
8
|
-
import { readFileSync, writeFileSync, existsSync,
|
|
8
|
+
// npx @kaooffline/quickhost list | delete --app my-app | whoami
|
|
9
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
|
|
9
10
|
import { join, relative, resolve, basename } from "node:path";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
|
-
import { execSync } from "node:child_process";
|
|
12
|
+
import { execSync, spawn } from "node:child_process";
|
|
12
13
|
|
|
14
|
+
const VERSION = "0.2.1";
|
|
15
|
+
|
|
16
|
+
// ---------- tiny UI kit (no deps) ----------
|
|
17
|
+
const TTY = !!process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM !== "dumb";
|
|
18
|
+
const ANS = (o, c) => (TTY ? `\x1b[${o}m${c}\x1b[0m` : String(c));
|
|
19
|
+
const bold = (s) => ANS(1, s);
|
|
20
|
+
const dim = (s) => ANS(2, s);
|
|
21
|
+
const red = (s) => ANS(31, s);
|
|
22
|
+
const green = (s) => ANS(32, s);
|
|
23
|
+
const yellow = (s) => ANS(33, s);
|
|
24
|
+
const cyan = (s) => ANS(36, s);
|
|
25
|
+
const gray = (s) => ANS(90, s);
|
|
26
|
+
const SYM = TTY
|
|
27
|
+
? { ok: "✓", err: "✗", warn: "!", arrow: "→", dot: "·", q: "◆", step: "◇" }
|
|
28
|
+
: { ok: "ok", err: "ERR", warn: "!", arrow: "->", dot: "-", q: "*", step: "-" };
|
|
29
|
+
const hr = () => console.log(gray("─".repeat(Math.min(56, process.stdout.columns || 56))));
|
|
30
|
+
const header = (cmd) => {
|
|
31
|
+
console.log(`${cyan(bold("quickHOST"))} ${dim("static hosting on kaooffline.top")} ${gray("v" + VERSION)}`);
|
|
32
|
+
if (cmd) console.log(`${SYM.q} ${bold(cmd)}`);
|
|
33
|
+
};
|
|
34
|
+
const step = (n, total, msg) => console.log(`${gray(`[${n}/${total}]`)} ${msg}`);
|
|
35
|
+
const done = (msg) => console.log(`${green(SYM.ok)} ${msg}`);
|
|
36
|
+
const fail = (msg) => console.error(`${red(SYM.err)} ${msg}`);
|
|
37
|
+
const info = (msg) => console.log(`${cyan(SYM.arrow)} ${msg}`);
|
|
38
|
+
const warn = (msg) => console.log(`${yellow(SYM.warn)} ${msg}`);
|
|
39
|
+
const hint = (msg) => console.log(` ${dim(msg)}`);
|
|
40
|
+
function box(lines, accent = green) {
|
|
41
|
+
const w = Math.max(...lines.map((l) => strip(l).length));
|
|
42
|
+
const top = "╭" + "─".repeat(w + 2) + "╮";
|
|
43
|
+
const bot = "╰" + "─".repeat(w + 2) + "╯";
|
|
44
|
+
console.log(TTY ? accent(top) : top);
|
|
45
|
+
for (const l of lines) console.log(`${TTY ? accent("│") : "|"} ${l}${" ".repeat(Math.max(0, w - strip(l).length))} ${TTY ? accent("│") : "|"}`);
|
|
46
|
+
console.log(TTY ? accent(bot) : bot);
|
|
47
|
+
}
|
|
48
|
+
const strip = (s) => String(s).replace(/\x1b\[\d+m/g, "");
|
|
49
|
+
const fmtBytes = (n) => (n < 1024 ? `${n} B` : n < 1024 ** 2 ? `${(n / 1024).toFixed(1)} KB` : `${(n / 1024 ** 2).toFixed(1)} MB`);
|
|
50
|
+
const bar = (i, total, w = 18) => {
|
|
51
|
+
const f = Math.round((i / total) * w);
|
|
52
|
+
return (TTY ? cyan("█".repeat(f)) : "#".repeat(f)) + gray("─".repeat(Math.max(0, w - f)));
|
|
53
|
+
};
|
|
54
|
+
function errBox(title, hints = []) {
|
|
55
|
+
console.log(`${red(SYM.err + " " + title)}`);
|
|
56
|
+
for (const h of hints) hint(h);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------- config / args ----------
|
|
13
60
|
const CFG = join(homedir(), ".quickhost.json");
|
|
14
61
|
const load = () => { try { return JSON.parse(readFileSync(CFG, "utf8")); } catch { return {}; } };
|
|
15
62
|
const save = (c) => writeFileSync(CFG, JSON.stringify(c, null, 2));
|
|
16
63
|
const arg = (k, d = null) => {
|
|
17
64
|
const i = process.argv.indexOf(k);
|
|
18
|
-
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : d;
|
|
65
|
+
return i >= 0 && process.argv[i + 1] && !process.argv[i + 1].startsWith("--") ? process.argv[i + 1] : d;
|
|
19
66
|
};
|
|
67
|
+
const has = (...ks) => ks.some((k) => process.argv.includes(k));
|
|
20
68
|
const apiBase = () => (arg("--api") || process.env.QUICKHOST_API || load().api || "https://api.kaooffline.top").replace(/\/$/, "");
|
|
21
69
|
const authH = () => { const t = load().token; return t ? { authorization: "Bearer " + t } : {}; };
|
|
22
70
|
|
|
@@ -45,8 +93,9 @@ async function prompt(q) {
|
|
|
45
93
|
}
|
|
46
94
|
|
|
47
95
|
async function registerOrLogin(mode) {
|
|
96
|
+
header(mode === "register" ? "Claim your username" : "Log in");
|
|
48
97
|
const u = (arg("-u") || arg("--username") || "").toLowerCase().trim();
|
|
49
|
-
if (!u) {
|
|
98
|
+
if (!u) { errBox(`Usage: quickhost ${mode} -u <username>`); process.exit(1); }
|
|
50
99
|
const p = arg("-p") || arg("--password") || await promptSecret("password: ");
|
|
51
100
|
const body = { username: u, password: p };
|
|
52
101
|
if (mode === "register") {
|
|
@@ -54,12 +103,22 @@ async function registerOrLogin(mode) {
|
|
|
54
103
|
const p2 = (arg("-p") || arg("--password")) ? p : await promptSecret("password again: ");
|
|
55
104
|
body.password2 = p2;
|
|
56
105
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
106
|
+
let r, j;
|
|
107
|
+
try {
|
|
108
|
+
r = await fetch(`${apiBase()}/api/${mode}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
109
|
+
j = await r.json().catch(() => ({}));
|
|
110
|
+
} catch (e) {
|
|
111
|
+
errBox("API unreachable", [`tried ${apiBase()}`, "check network, or pass --api <url>", String(e.cause?.message || e.message || e)]);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
if (!r.ok) { errBox(j.error || ("http " + r.status)); process.exit(1); }
|
|
115
|
+
if (j.verify === "email-sent") {
|
|
116
|
+
done(`verification mail sent to ${bold(j.email)}`);
|
|
117
|
+
hint("click the link, then run:"); hint(` quickhost login -u ${u}`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
61
120
|
save({ ...load(), api: apiBase(), username: j.username, token: j.token });
|
|
62
|
-
|
|
121
|
+
done(`logged in as ${bold(j.username)}`);
|
|
63
122
|
}
|
|
64
123
|
|
|
65
124
|
function walk(dir, out = [], root = dir) {
|
|
@@ -70,79 +129,306 @@ function walk(dir, out = [], root = dir) {
|
|
|
70
129
|
}
|
|
71
130
|
return out;
|
|
72
131
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
132
|
+
|
|
133
|
+
function readPkg() {
|
|
134
|
+
try { return JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf8")); }
|
|
135
|
+
catch { return null; }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Framework detection: which builder produced this project, and can it be static?
|
|
139
|
+
function detectFramework(pkg) {
|
|
140
|
+
const deps = { ...(pkg?.dependencies || {}), ...(pkg?.devDependencies || {}) };
|
|
141
|
+
const hasDep = (...ns) => ns.some((n) => n in deps);
|
|
142
|
+
if (hasDep("vinext")) return { id: "vinext", label: "vinext (Vite + RSC/SSR)", statik: false };
|
|
143
|
+
if (hasDep("next", "nextjs")) return { id: "next", label: "Next.js (SSR by default)", statik: false };
|
|
144
|
+
if (hasDep("nuxt", "nitropack", "nitro")) return { id: "nuxt", label: "Nuxt/Nitro (SSR by default)", statik: false };
|
|
145
|
+
if (hasDep("@sveltejs/kit")) return { id: "sveltekit", label: "SvelteKit (SSR by default)", statik: false };
|
|
146
|
+
if (hasDep("@remix-run/node", "@react-router/node")) return { id: "remix", label: "Remix/React Router (SSR)", statik: false };
|
|
147
|
+
if (hasDep("astro")) return { id: "astro", label: "Astro", statik: true };
|
|
148
|
+
if (hasDep("vite", "@vitejs/plugin-react", "vue", "svelte")) return { id: "vite", label: "Vite", statik: true };
|
|
149
|
+
return { id: "static", label: "static html", statik: true };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Publish-dir candidates in priority order. dist/client = vinext static export,
|
|
153
|
+
// .output/public = Nuxt/Nitro static, out = Next export.
|
|
154
|
+
const CANDIDATES = ["dist", "dist/client", "build", "out", ".output/public", "public"];
|
|
155
|
+
function findBuiltDir() {
|
|
156
|
+
for (const c of CANDIDATES) {
|
|
77
157
|
if (existsSync(join(process.cwd(), c, "index.html"))) return resolve(join(process.cwd(), c));
|
|
78
158
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
159
|
+
if (existsSync(join(process.cwd(), "index.html"))) return resolve(process.cwd());
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function serverHints(pkg) {
|
|
164
|
+
const hits = [];
|
|
165
|
+
const deps = { ...(pkg?.dependencies || {}), ...(pkg?.devDependencies || {}) };
|
|
166
|
+
for (const d of ["app/api", "pages/api", "src/pages/api", "server", "worker", "functions", "api"]) {
|
|
167
|
+
if (existsSync(join(process.cwd(), d))) hits.push(d + "/");
|
|
168
|
+
}
|
|
169
|
+
for (const k of Object.keys(deps)) {
|
|
170
|
+
if (/^(express|fastify|hono|drizzle-orm|prisma|kysely|pg|sqlite|better-sqlite3)/.test(k)) { hits.push(`dep:${k}`); break; }
|
|
171
|
+
}
|
|
172
|
+
return hits;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function pm() {
|
|
176
|
+
if (existsSync(join(process.cwd(), "bun.lock")) || existsSync(join(process.cwd(), "bun.lockb"))) return { run: "bun run build", name: "bun" };
|
|
177
|
+
if (existsSync(join(process.cwd(), "pnpm-lock.yaml"))) return { run: "pnpm run build", name: "pnpm" };
|
|
178
|
+
if (existsSync(join(process.cwd(), "yarn.lock"))) return { run: "yarn build", name: "yarn" };
|
|
179
|
+
return { run: "npm run build", name: "npm" };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function ssrError(fw, hints, checked) {
|
|
183
|
+
errBox("No static output found — this looks like a server app, not a static site.");
|
|
184
|
+
console.log("");
|
|
185
|
+
console.log(` detected ${bold(fw.label)}`);
|
|
186
|
+
if (hints.length) console.log(` server bits ${dim(hints.slice(0, 4).join(" "))}`);
|
|
187
|
+
console.log(` checked ${dim(checked.join(" ") + " (need index.html at dir root)")}`);
|
|
188
|
+
console.log("");
|
|
189
|
+
console.log(` ${bold("quickHOST hosts static files only")} (index.html + js/css/assets).`);
|
|
190
|
+
console.log(` ${dim("API routes, DB calls, headers()/cookies() need a Node server —")}`);
|
|
191
|
+
console.log(` ${dim("they can't run from R2 (same reason your build lists λ API routes).")}`);
|
|
192
|
+
console.log("");
|
|
193
|
+
console.log(` ${bold("Fix A — static export (if your pages allow it):")}`);
|
|
194
|
+
if (fw.id === "vinext") {
|
|
195
|
+
hint("vinext: configure a static export so the build emits dist/client/index.html,");
|
|
196
|
+
hint("then: quickhost deploy --app <name> --dir ./dist/client");
|
|
197
|
+
} else if (fw.id === "next") {
|
|
198
|
+
hint("next.config: set output: 'export', rebuild, then deploy the out/ dir:");
|
|
199
|
+
hint(" quickhost deploy --app <name> --dir ./out");
|
|
200
|
+
} else if (fw.id === "nuxt") {
|
|
201
|
+
hint("run: npx nuxt generate then: quickhost deploy --app <name> --dir ./.output/public");
|
|
202
|
+
} else {
|
|
203
|
+
hint("build a static bundle (vite build / astro build), then:");
|
|
204
|
+
hint(" quickhost deploy --app <name> --dir ./dist");
|
|
205
|
+
}
|
|
206
|
+
console.log("");
|
|
207
|
+
console.log(` ${bold("Fix B — keep the server, host it elsewhere:")}`);
|
|
208
|
+
hint("this app wants wrangler / Vercel / a VPS (it has a worker/, DB, /api/*).");
|
|
209
|
+
hint("quickHOST is the wrong host for it — use `vinext start` / `wrangler deploy`.");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Run the project's build quietly: live one-line status, full log only on failure.
|
|
213
|
+
// Pass --verbose to stream the raw build output (old behavior, for debugging).
|
|
214
|
+
function runBuild(cmd, { verbose }) {
|
|
215
|
+
if (verbose) {
|
|
216
|
+
const t0 = Date.now();
|
|
217
|
+
try {
|
|
218
|
+
execSync(cmd, { stdio: "inherit" });
|
|
219
|
+
return Promise.resolve({ code: 0, log: "", secs: (Date.now() - t0) / 1000 });
|
|
220
|
+
} catch (e) {
|
|
221
|
+
return Promise.resolve({ code: e.status ?? 1, log: "", secs: (Date.now() - t0) / 1000 });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return new Promise((resolve) => {
|
|
225
|
+
const t0 = Date.now();
|
|
226
|
+
const child = spawn(cmd, { shell: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
227
|
+
let log = "";
|
|
228
|
+
let last = "";
|
|
229
|
+
const tick = TTY ? setInterval(() => {
|
|
230
|
+
const s = ((Date.now() - t0) / 1000).toFixed(0);
|
|
231
|
+
const tail = last ? dim(" " + last.slice(0, 72)) : "";
|
|
232
|
+
process.stdout.write(`\r${cyan("…")} building… ${dim(s + "s")}${tail} `);
|
|
233
|
+
}, 250) : null;
|
|
234
|
+
if (!TTY) console.log(dim("building… (quiet — rerun with --verbose for the full log)"));
|
|
235
|
+
const sniff = (d) => {
|
|
236
|
+
log += d;
|
|
237
|
+
const lines = String(d).split("\n").map((l) => l.replace(/\x1b\[\d+m/g, "").trim()).filter(Boolean);
|
|
238
|
+
if (lines.length) last = lines[lines.length - 1];
|
|
239
|
+
};
|
|
240
|
+
child.stdout.on("data", sniff);
|
|
241
|
+
child.stderr.on("data", sniff);
|
|
242
|
+
const finish = (code) => {
|
|
243
|
+
if (tick) { clearInterval(tick); process.stdout.write("\r" + " ".repeat(110) + "\r"); }
|
|
244
|
+
resolve({ code, log, secs: (Date.now() - t0) / 1000 });
|
|
245
|
+
};
|
|
246
|
+
child.on("close", (code) => finish(code ?? 1));
|
|
247
|
+
child.on("error", (e) => { log += String(e?.message || e); finish(1); });
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function detectDir() {
|
|
252
|
+
const manual = arg("--dir") || arg("-d");
|
|
253
|
+
const pkg = readPkg();
|
|
254
|
+
const fw = detectFramework(pkg);
|
|
255
|
+
if (manual) {
|
|
256
|
+
const abs = resolve(manual);
|
|
257
|
+
if (existsSync(join(abs, "index.html"))) return { dir: abs, fw, built: false };
|
|
258
|
+
errBox(`--dir ${manual} has no index.html`, [`resolved: ${abs}`, "point --dir at the folder containing index.html (e.g. --dir ./dist/client)"]);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
const found = findBuiltDir();
|
|
262
|
+
if (found) return { dir: found, fw, built: false };
|
|
263
|
+
// try build once (quiet by default, --verbose streams raw output)
|
|
264
|
+
if (pkg?.scripts?.build) {
|
|
265
|
+
const { run } = pm();
|
|
266
|
+
const verbose = has("--verbose");
|
|
267
|
+
console.log(`${gray("[build]")} ${dim("no built dir yet, running:")} ${bold(run)}${verbose ? "" : dim(" (quiet)")}`);
|
|
268
|
+
const { code, log, secs } = await runBuild(run, { verbose });
|
|
269
|
+
if (code !== 0) {
|
|
270
|
+
console.log("");
|
|
271
|
+
errBox(`Build failed (exit ${code}) — fix the errors, then retry.`, [`ran: ${run} (${pkg.scripts.build})`]);
|
|
272
|
+
const tail = log.split("\n").filter((l) => l.trim()).slice(-25);
|
|
273
|
+
if (tail.length && !verbose) {
|
|
274
|
+
console.log("");
|
|
275
|
+
for (const l of tail) console.log(` ${dim(l.slice(0, 160))}`);
|
|
276
|
+
hint("full log: rerun with --verbose");
|
|
87
277
|
}
|
|
278
|
+
process.exit(1);
|
|
88
279
|
}
|
|
89
|
-
|
|
90
|
-
|
|
280
|
+
done(`build finished in ${secs.toFixed(1)}s`);
|
|
281
|
+
const again = findBuiltDir();
|
|
282
|
+
if (again) return { dir: again, fw, built: true };
|
|
283
|
+
}
|
|
284
|
+
// nothing usable -> explain, framework-aware
|
|
285
|
+
console.log("");
|
|
286
|
+
if (!fw.statik) ssrError(fw, serverHints(pkg), [...CANDIDATES, "."]);
|
|
287
|
+
else {
|
|
288
|
+
errBox("No static output found.", [
|
|
289
|
+
`checked: ${[...CANDIDATES, "."].join(" ")} (need index.html at dir root)`,
|
|
290
|
+
pkg?.scripts?.build ? `build ran but emitted no index.html — check your output dir` : "no build script in package.json",
|
|
291
|
+
"or pass the folder explicitly: quickhost deploy --app <name> --dir ./dist",
|
|
292
|
+
]);
|
|
293
|
+
}
|
|
91
294
|
process.exit(1);
|
|
92
295
|
}
|
|
93
296
|
|
|
94
297
|
async function deploy() {
|
|
298
|
+
const t0 = Date.now();
|
|
299
|
+
header("deploy");
|
|
95
300
|
const cfg = load();
|
|
96
|
-
if (!cfg.token) {
|
|
301
|
+
if (!cfg.token) {
|
|
302
|
+
errBox("Not logged in.", ["run first:", " quickhost login -u <name>"]);
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
97
305
|
let app = (arg("--app") || arg("-a") || "").toLowerCase().trim();
|
|
98
306
|
if (!app) {
|
|
99
307
|
try { app = basename(resolve(process.cwd())).toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 40); }
|
|
100
308
|
catch {}
|
|
101
309
|
}
|
|
102
|
-
if (!/^[a-z0-9-]{1,40}$/.test(app)) {
|
|
103
|
-
|
|
310
|
+
if (!/^[a-z0-9-]{1,40}$/.test(app)) { errBox("Bad app name — use --app my-app (a-z 0-9 -, max 40)"); process.exit(1); }
|
|
311
|
+
|
|
312
|
+
const TOTAL = 3;
|
|
313
|
+
step(1, TOTAL, `locating static output ${dim(`(${cfg.username}/${app})`)}`);
|
|
314
|
+
const { dir, fw, built } = await detectDir();
|
|
315
|
+
info(`${dim("dir")} ${bold(relative(process.cwd(), dir) || ".")} ${dim("·")} ${dim(fw.label)}${built ? dim(" · just built") : ""}`);
|
|
316
|
+
|
|
317
|
+
step(2, TOTAL, "collecting files");
|
|
104
318
|
const files = walk(dir).filter((f) => !f.rel.startsWith("."));
|
|
105
|
-
if (!files.length) {
|
|
106
|
-
if (files.length > 500) {
|
|
319
|
+
if (!files.length) { errBox(`Empty dir: ${dir}`); process.exit(1); }
|
|
320
|
+
if (files.length > 500) { errBox(`Too many files (${files.length} > 500 limit).`, ["split into two apps, or vendor less into dist/"]); process.exit(1); }
|
|
321
|
+
let bytes = 0;
|
|
322
|
+
for (const f of files) {
|
|
323
|
+
const { statSync } = await import("node:fs");
|
|
324
|
+
const s = statSync(f.abs);
|
|
325
|
+
bytes += s.size;
|
|
326
|
+
if (s.size > 25 * 1024 * 1024) { errBox(`File >25MB: ${f.rel} (${fmtBytes(s.size)})`); process.exit(1); }
|
|
327
|
+
}
|
|
328
|
+
done(`${bold(String(files.length))} files ${dim("·")} ${bold(fmtBytes(bytes))}`);
|
|
107
329
|
try {
|
|
108
330
|
const idx = readFileSync(join(dir, "index.html"), "utf8");
|
|
109
|
-
if (/(src|href)="\/(?!\/)/.test(idx))
|
|
110
|
-
|
|
331
|
+
if (/(src|href)="\/(?!\/)/.test(idx)) {
|
|
332
|
+
warn("absolute asset paths detected (src=\"/assets/…\")");
|
|
333
|
+
hint("works via platform fallback, but cleanest is base:'./' in vite.config (or Astro base) + rebuild.");
|
|
334
|
+
}
|
|
111
335
|
} catch {}
|
|
112
|
-
|
|
113
|
-
|
|
336
|
+
|
|
337
|
+
step(3, TOTAL, `uploading ${dim("→ " + apiBase())}`);
|
|
338
|
+
const base = apiBase();
|
|
114
339
|
let i = 0;
|
|
115
340
|
for (const f of files) {
|
|
116
341
|
const buf = readFileSync(f.abs);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
342
|
+
let r;
|
|
343
|
+
try {
|
|
344
|
+
r = await fetch(`${base}/api/apps/${app}/${encodeURIComponent(f.rel).replace(/%2F/g, "/")}`, {
|
|
345
|
+
method: "PUT", headers: { ...authH(), "content-type": mime(f.rel) }, body: buf,
|
|
346
|
+
});
|
|
347
|
+
} catch (e) {
|
|
348
|
+
process.stdout.write("\n");
|
|
349
|
+
errBox(`Network error on ${f.rel}`, [String(e.cause?.message || e.message || e), `api: ${base}`]);
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
if (!r.ok) { process.stdout.write("\n"); errBox(`Upload failed: ${f.rel} (http ${r.status})`, [(await r.text()).slice(0, 300)]); process.exit(1); }
|
|
353
|
+
i++;
|
|
354
|
+
process.stdout.write(`\r ${bar(i, files.length)} ${dim(`${i}/${files.length}`)} ${dim(f.rel.slice(-38))} `);
|
|
355
|
+
}
|
|
356
|
+
process.stdout.write("\n");
|
|
357
|
+
info("finalizing…");
|
|
358
|
+
let r;
|
|
359
|
+
try {
|
|
360
|
+
r = await fetch(`${base}/api/apps/${app}/finalize`, { method: "POST", headers: { ...authH(), "content-type": "application/json" }, body: "{}" });
|
|
361
|
+
} catch (e) {
|
|
362
|
+
errBox("Finalize unreachable", [String(e.cause?.message || e.message || e)]);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
125
365
|
const j = await r.json().catch(() => ({}));
|
|
126
|
-
if (!r.ok) {
|
|
127
|
-
|
|
128
|
-
|
|
366
|
+
if (!r.ok) { errBox(`Finalize failed: ${j.error || ("http " + r.status)}`); process.exit(1); }
|
|
367
|
+
|
|
368
|
+
const secs = ((Date.now() - t0) / 1000).toFixed(1);
|
|
369
|
+
console.log("");
|
|
370
|
+
box([
|
|
371
|
+
`${green(SYM.ok)} ${bold("Deployed in " + secs + "s")}`,
|
|
372
|
+
`${bold(j.url)}`,
|
|
373
|
+
`${dim(`${files.length} files · ${fmtBytes(bytes)} · expires ${new Date(j.expires_at * 1000).toLocaleDateString()} (redeploy renews 30d)`)}`,
|
|
374
|
+
]);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function help() {
|
|
378
|
+
header();
|
|
379
|
+
console.log(`
|
|
380
|
+
${bold("Usage:")} ${cyan("quickhost")} ${dim("<command> [options]")}
|
|
381
|
+
|
|
382
|
+
${bold("Commands")}
|
|
383
|
+
${cyan("deploy")} [--app x] [--dir ./dist] [--api URL] [--verbose] upload a static dir
|
|
384
|
+
${cyan("register")} -u <name> claim a username
|
|
385
|
+
${cyan("login")} -u <name> log in on this machine
|
|
386
|
+
${cyan("list")} live apps + expiry
|
|
387
|
+
${cyan("delete")} --app x remove an app
|
|
388
|
+
${cyan("whoami")} current login
|
|
389
|
+
|
|
390
|
+
${bold("Deploy")}
|
|
391
|
+
auto-finds ${dim("dist dist/client(vinext) build out .output/public public")}
|
|
392
|
+
runs your ${dim("build")} script once if nothing is built yet (quiet — ${dim("--verbose")} streams it).
|
|
393
|
+
needs ${bold("index.html")} at the publish dir root. static only, SPA fallback on.
|
|
394
|
+
|
|
395
|
+
${bold("Limits")} ${dim("50MB/app · 500 files · 25MB/file · 20 apps/user · 30-day rolling expiry")}
|
|
396
|
+
${dim("Docs: https://kaooffline.top/docs")}`);
|
|
129
397
|
}
|
|
130
398
|
|
|
131
399
|
const cmd = process.argv[2];
|
|
132
|
-
if (cmd
|
|
133
|
-
else if (cmd === "
|
|
400
|
+
if (!cmd || cmd === "deploy") await deploy();
|
|
401
|
+
else if (cmd === "register" || cmd === "login") await registerOrLogin(cmd);
|
|
134
402
|
else if (cmd === "list") {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
403
|
+
header("apps");
|
|
404
|
+
let r;
|
|
405
|
+
try { r = await fetch(`${apiBase()}/api/apps`, { headers: authH() }); }
|
|
406
|
+
catch (e) { errBox("API unreachable", [String(e.cause?.message || e.message || e)]); process.exit(1); }
|
|
407
|
+
const j = await r.json().catch(() => ({}));
|
|
408
|
+
if (!r.ok) { errBox(j.error || ("http " + r.status)); process.exit(1); }
|
|
409
|
+
const apps = j.apps || [];
|
|
410
|
+
if (!apps.length) { info("(no live apps — deploy one with: quickhost deploy)"); process.exit(0); }
|
|
411
|
+
const rows = apps.map((a) => [`${a.username}.kaooffline.top/${a.appname}`, `expires ${new Date(a.expires_at * 1000).toLocaleDateString()}`, `${a.file_count} files`]);
|
|
412
|
+
const w0 = Math.max(...rows.map((r) => r[0].length));
|
|
413
|
+
for (const [u, e, f] of rows) console.log(` ${cyan(bold(u))}\n ${dim(e)} ${dim(SYM.dot)} ${dim(f)}${" ".repeat(Math.max(0, w0 - u.length))}`);
|
|
414
|
+
hint(`${apps.length} live app${apps.length === 1 ? "" : "s"}`);
|
|
415
|
+
} else if (cmd === "delete" || cmd === "revoke") {
|
|
416
|
+
header("delete");
|
|
141
417
|
const app = arg("--app") || arg("-a");
|
|
142
|
-
if (!app) {
|
|
418
|
+
if (!app) { errBox("Usage: quickhost delete --app <name>"); process.exit(1); }
|
|
143
419
|
const r = await fetch(`${apiBase()}/api/apps/${app}`, { method: "DELETE", headers: authH() });
|
|
144
|
-
|
|
420
|
+
if (r.ok) done(`deleted ${bold(app)}`);
|
|
421
|
+
else errBox("Delete failed", [(await r.text()).slice(0, 300)]);
|
|
145
422
|
} else if (cmd === "whoami") {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
423
|
+
header("whoami");
|
|
424
|
+
const c = load();
|
|
425
|
+
if (!c.username) { warn("not logged in here."); hint("run: quickhost login -u <name>"); process.exit(0); }
|
|
426
|
+
let extra = "";
|
|
427
|
+
try {
|
|
428
|
+
const r = await fetch(`${apiBase()}/api/me`, { headers: authH() });
|
|
429
|
+
extra = dim(" (" + (await r.text()).slice(0, 120) + ")");
|
|
430
|
+
} catch {}
|
|
431
|
+
done(`${bold(c.username)} ${dim("·")} ${dim(apiBase())}${extra ? "\n" + extra : ""}`);
|
|
432
|
+
} else if (cmd === "--help" || cmd === "-h" || cmd === "help") help();
|
|
433
|
+
else if (cmd === "--version" || cmd === "-v" || cmd === "version") console.log(`quickhost v${VERSION}`);
|
|
434
|
+
else { errBox(`Unknown command: ${cmd}`); help(); process.exit(1); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaooffline/quickhost",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "quickHOST CLI - one-command static hosting (Vite/Astro/HTML) to username.kaooffline.top/app. Free, 30-day rolling expiry.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": { "quickhost": "bin/quickhost.js" },
|