@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 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/`, `public/` (Vite/Astro first) and runs `npm run build` if nothing is built yet. Needs `index.html` at the publish dir root. SPA fallback included.
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 | npx @kaooffline/quickhost delete --app my-app | npx @kaooffline/quickhost whoami
8
- import { readFileSync, writeFileSync, existsSync, statSync, readdirSync } from "node:fs";
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) { console.error(`Usage: quickhost ${mode} -u <username>`); process.exit(1); }
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
- const r = await fetch(`${apiBase()}/api/${mode}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
58
- const j = await r.json().catch(() => ({}));
59
- if (!r.ok) { console.error("ERR:", j.error || r.status); process.exit(1); }
60
- if (j.verify === "email-sent") { console.log(`ok: verification mail sent to ${j.email}. Click the link, then: quickhost login -u ${u}`); return; }
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
- console.log(`ok: logged in as ${j.username}`);
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
- function detectDir() {
74
- const manual = arg("--dir") || arg("-d");
75
- if (manual) return resolve(manual);
76
- for (const c of ["dist", "build", "out", "public", "."]) {
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
- // try build once
80
- try {
81
- const pkg = JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf8"));
82
- if (pkg.scripts?.build) {
83
- console.log("no dist/ found, running build:", pkg.scripts.build);
84
- execSync("npm run build", { stdio: "inherit" });
85
- for (const c of ["dist", "build", "out"]) {
86
- if (existsSync(join(process.cwd(), c, "index.html"))) return resolve(join(process.cwd(), c));
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
- } catch {}
90
- console.error("ERR: no index.html found. Build your app (npm run build) or pass --dir ./dist");
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) { console.error("ERR: not logged in. Run: quickhost login -u <name>"); process.exit(1); }
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)) { console.error("ERR: bad app name, use --app my-app"); process.exit(1); }
103
- const dir = detectDir();
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) { console.error("ERR: empty dir", dir); process.exit(1); }
106
- if (files.length > 500) { console.error("ERR: >500 files (limit)"); process.exit(1); }
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
- console.log("note: absolute asset paths (src=\"/assets/...\") detected. Works via platform fallback, but for cleanest hosting set base:'./' in vite.config (or Astro base) and rebuild.");
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
- console.log(`deploying ${files.length} files from ${dir} as ${cfg.username}/${app} ...`);
113
- const t0 = Date.now();
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
- if (buf.length > 25 * 1024 * 1024) { console.error("ERR: file >25MB:", f.rel); process.exit(1); }
118
- const r = await fetch(`${apiBase()}/api/apps/${app}/${encodeURIComponent(f.rel).replace(/%2F/g, "/")}`, {
119
- method: "PUT", headers: { ...authH(), "content-type": mime(f.rel) }, body: buf,
120
- });
121
- if (!r.ok) { console.error("ERR upload", f.rel, await r.text()); process.exit(1); }
122
- if (++i % 20 === 0) process.stdout.write(` ${i}/${files.length}\r`);
123
- }
124
- const r = await fetch(`${apiBase()}/api/apps/${app}/finalize`, { method: "POST", headers: { ...authH(), "content-type": "application/json" }, body: "{}" });
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) { console.error("ERR finalize:", j.error || r.status); process.exit(1); }
127
- console.log(`\nDone in ${((Date.now() - t0) / 1000).toFixed(1)}s: ${j.url}`);
128
- console.log(`Expires: ${new Date(j.expires_at * 1000).toLocaleDateString()} (redeploy renews 30d)`);
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 === "register" || cmd === "login") await registerOrLogin(cmd);
133
- else if (cmd === "deploy" || !cmd) await deploy();
400
+ if (!cmd || cmd === "deploy") await deploy();
401
+ else if (cmd === "register" || cmd === "login") await registerOrLogin(cmd);
134
402
  else if (cmd === "list") {
135
- const r = await fetch(`${apiBase()}/api/apps`, { headers: authH() });
136
- const j = await r.json();
137
- if (!r.ok) { console.error("ERR:", j.error); process.exit(1); }
138
- for (const a of j.apps || []) console.log(`${a.username}.kaooffline.top/${a.appname} expires=${new Date(a.expires_at * 1000).toLocaleDateString()} files=${a.file_count}`);
139
- if (!(j.apps || []).length) console.log("(no live apps)");
140
- } else if (cmd === "delete") {
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) { console.error("Usage: quickhost delete --app <name>"); process.exit(1); }
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
- console.log(r.ok ? "deleted" : "ERR " + (await r.text()));
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
- const r = await fetch(`${apiBase()}/api/me`, { headers: authH() });
147
- console.log(await r.text());
148
- } else { console.log("Usage: quickhost [register -u x|login -u x|deploy --app x --dir ./dist|list|delete --app x|whoami]"); }
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.1.1",
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" },