@kaooffline/quickhost 0.1.0 → 0.2.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/README.md CHANGED
@@ -23,7 +23,7 @@ bunx @kaooffline/quickhost deploy --app demo
23
23
  ## Commands
24
24
 
25
25
  ```
26
- quickhost register -u <name> claim username (asks password)
26
+ quickhost register -u <name> [--email you@mail] claim username (email + password twice, verification link)
27
27
  quickhost login -u <name> login on this machine
28
28
  quickhost deploy [--app x] [--dir ./dist] [--api URL]
29
29
  quickhost list live apps + expiry dates
@@ -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
12
  import { execSync } from "node:child_process";
12
13
 
14
+ const VERSION = "0.2.0";
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
 
@@ -38,15 +86,39 @@ async function promptSecret(q) {
38
86
  });
39
87
  }
40
88
 
89
+ async function prompt(q) {
90
+ const rl = await import("node:readline");
91
+ const r = rl.createInterface({ input: process.stdin, output: process.stdout });
92
+ return await new Promise((res) => r.question(q, (a) => { r.close(); res(a.trim()); }));
93
+ }
94
+
41
95
  async function registerOrLogin(mode) {
96
+ header(mode === "register" ? "Claim your username" : "Log in");
42
97
  const u = (arg("-u") || arg("--username") || "").toLowerCase().trim();
43
- if (!u) { console.error(`Usage: quickhost ${mode} -u <username>`); process.exit(1); }
98
+ if (!u) { errBox(`Usage: quickhost ${mode} -u <username>`); process.exit(1); }
44
99
  const p = arg("-p") || arg("--password") || await promptSecret("password: ");
45
- const r = await fetch(`${apiBase()}/api/${mode}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ username: u, password: p }) });
46
- const j = await r.json().catch(() => ({}));
47
- if (!r.ok) { console.error("ERR:", j.error || r.status); process.exit(1); }
100
+ const body = { username: u, password: p };
101
+ if (mode === "register") {
102
+ body.email = arg("--email") || await prompt("email: ");
103
+ const p2 = (arg("-p") || arg("--password")) ? p : await promptSecret("password again: ");
104
+ body.password2 = p2;
105
+ }
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
+ }
48
120
  save({ ...load(), api: apiBase(), username: j.username, token: j.token });
49
- console.log(`ok: logged in as ${j.username}`);
121
+ done(`logged in as ${bold(j.username)}`);
50
122
  }
51
123
 
52
124
  function walk(dir, out = [], root = dir) {
@@ -57,74 +129,265 @@ function walk(dir, out = [], root = dir) {
57
129
  }
58
130
  return out;
59
131
  }
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) {
157
+ if (existsSync(join(process.cwd(), c, "index.html"))) return resolve(join(process.cwd(), c));
158
+ }
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
+
60
212
  function detectDir() {
61
213
  const manual = arg("--dir") || arg("-d");
62
- if (manual) return resolve(manual);
63
- for (const c of ["dist", "build", "out", "public", "."]) {
64
- if (existsSync(join(process.cwd(), c, "index.html"))) return resolve(join(process.cwd(), c));
214
+ const pkg = readPkg();
215
+ const fw = detectFramework(pkg);
216
+ if (manual) {
217
+ const abs = resolve(manual);
218
+ if (existsSync(join(abs, "index.html"))) return { dir: abs, fw, built: false };
219
+ errBox(`--dir ${manual} has no index.html`, [`resolved: ${abs}`, "point --dir at the folder containing index.html (e.g. --dir ./dist/client)"]);
220
+ process.exit(1);
65
221
  }
222
+ const found = findBuiltDir();
223
+ if (found) return { dir: found, fw, built: false };
66
224
  // try build once
67
- try {
68
- const pkg = JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf8"));
69
- if (pkg.scripts?.build) {
70
- console.log("no dist/ found, running build:", pkg.scripts.build);
71
- execSync("npm run build", { stdio: "inherit" });
72
- for (const c of ["dist", "build", "out"]) {
73
- if (existsSync(join(process.cwd(), c, "index.html"))) return resolve(join(process.cwd(), c));
74
- }
225
+ if (pkg?.scripts?.build) {
226
+ const { run } = pm();
227
+ console.log(`${gray("[build]")} ${dim("no built dir yet, running:")} ${bold(run)}`);
228
+ console.log(`${gray("[build]")} ${dim("script:")} ${dim(pkg.scripts.build)}`);
229
+ hr();
230
+ const t0 = Date.now();
231
+ try {
232
+ execSync(run, { stdio: "inherit" });
233
+ } catch {
234
+ console.log("");
235
+ errBox("Build failed — fix the errors above, then retry.", [`ran: ${run} (${pkg.scripts.build})`]);
236
+ process.exit(1);
75
237
  }
76
- } catch {}
77
- console.error("ERR: no index.html found. Build your app (npm run build) or pass --dir ./dist");
238
+ hr();
239
+ done(`build finished in ${((Date.now() - t0) / 1000).toFixed(1)}s`);
240
+ const again = findBuiltDir();
241
+ if (again) return { dir: again, fw, built: true };
242
+ }
243
+ // nothing usable -> explain, framework-aware
244
+ console.log("");
245
+ if (!fw.statik) ssrError(fw, serverHints(pkg), [...CANDIDATES, "."]);
246
+ else {
247
+ errBox("No static output found.", [
248
+ `checked: ${[...CANDIDATES, "."].join(" ")} (need index.html at dir root)`,
249
+ pkg?.scripts?.build ? `build ran but emitted no index.html — check your output dir` : "no build script in package.json",
250
+ "or pass the folder explicitly: quickhost deploy --app <name> --dir ./dist",
251
+ ]);
252
+ }
78
253
  process.exit(1);
79
254
  }
80
255
 
81
256
  async function deploy() {
257
+ const t0 = Date.now();
258
+ header("deploy");
82
259
  const cfg = load();
83
- if (!cfg.token) { console.error("ERR: not logged in. Run: quickhost login -u <name>"); process.exit(1); }
260
+ if (!cfg.token) {
261
+ errBox("Not logged in.", ["run first:", " quickhost login -u <name>"]);
262
+ process.exit(1);
263
+ }
84
264
  let app = (arg("--app") || arg("-a") || "").toLowerCase().trim();
85
265
  if (!app) {
86
266
  try { app = basename(resolve(process.cwd())).toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 40); }
87
267
  catch {}
88
268
  }
89
- if (!/^[a-z0-9-]{1,40}$/.test(app)) { console.error("ERR: bad app name, use --app my-app"); process.exit(1); }
90
- const dir = detectDir();
269
+ 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); }
270
+
271
+ const TOTAL = 3;
272
+ step(1, TOTAL, `locating static output ${dim(`(${cfg.username}/${app})`)}`);
273
+ const { dir, fw, built } = detectDir();
274
+ info(`${dim("dir")} ${bold(relative(process.cwd(), dir) || ".")} ${dim("·")} ${dim(fw.label)}${built ? dim(" · just built") : ""}`);
275
+
276
+ step(2, TOTAL, "collecting files");
91
277
  const files = walk(dir).filter((f) => !f.rel.startsWith("."));
92
- if (!files.length) { console.error("ERR: empty dir", dir); process.exit(1); }
93
- if (files.length > 500) { console.error("ERR: >500 files (limit)"); process.exit(1); }
94
- console.log(`deploying ${files.length} files from ${dir} as ${cfg.username}/${app} ...`);
95
- const t0 = Date.now();
278
+ if (!files.length) { errBox(`Empty dir: ${dir}`); process.exit(1); }
279
+ if (files.length > 500) { errBox(`Too many files (${files.length} > 500 limit).`, ["split into two apps, or vendor less into dist/"]); process.exit(1); }
280
+ let bytes = 0;
281
+ for (const f of files) {
282
+ const { statSync } = await import("node:fs");
283
+ const s = statSync(f.abs);
284
+ bytes += s.size;
285
+ if (s.size > 25 * 1024 * 1024) { errBox(`File >25MB: ${f.rel} (${fmtBytes(s.size)})`); process.exit(1); }
286
+ }
287
+ done(`${bold(String(files.length))} files ${dim("·")} ${bold(fmtBytes(bytes))}`);
288
+ try {
289
+ const idx = readFileSync(join(dir, "index.html"), "utf8");
290
+ if (/(src|href)="\/(?!\/)/.test(idx)) {
291
+ warn("absolute asset paths detected (src=\"/assets/…\")");
292
+ hint("works via platform fallback, but cleanest is base:'./' in vite.config (or Astro base) + rebuild.");
293
+ }
294
+ } catch {}
295
+
296
+ step(3, TOTAL, `uploading ${dim("→ " + apiBase())}`);
297
+ const base = apiBase();
96
298
  let i = 0;
97
299
  for (const f of files) {
98
300
  const buf = readFileSync(f.abs);
99
- if (buf.length > 25 * 1024 * 1024) { console.error("ERR: file >25MB:", f.rel); process.exit(1); }
100
- const r = await fetch(`${apiBase()}/api/apps/${app}/${encodeURIComponent(f.rel).replace(/%2F/g, "/")}`, {
101
- method: "PUT", headers: { ...authH(), "content-type": mime(f.rel) }, body: buf,
102
- });
103
- if (!r.ok) { console.error("ERR upload", f.rel, await r.text()); process.exit(1); }
104
- if (++i % 20 === 0) process.stdout.write(` ${i}/${files.length}\r`);
105
- }
106
- const r = await fetch(`${apiBase()}/api/apps/${app}/finalize`, { method: "POST", headers: { ...authH(), "content-type": "application/json" }, body: "{}" });
301
+ let r;
302
+ try {
303
+ r = await fetch(`${base}/api/apps/${app}/${encodeURIComponent(f.rel).replace(/%2F/g, "/")}`, {
304
+ method: "PUT", headers: { ...authH(), "content-type": mime(f.rel) }, body: buf,
305
+ });
306
+ } catch (e) {
307
+ process.stdout.write("\n");
308
+ errBox(`Network error on ${f.rel}`, [String(e.cause?.message || e.message || e), `api: ${base}`]);
309
+ process.exit(1);
310
+ }
311
+ if (!r.ok) { process.stdout.write("\n"); errBox(`Upload failed: ${f.rel} (http ${r.status})`, [(await r.text()).slice(0, 300)]); process.exit(1); }
312
+ i++;
313
+ process.stdout.write(`\r ${bar(i, files.length)} ${dim(`${i}/${files.length}`)} ${dim(f.rel.slice(-38))} `);
314
+ }
315
+ process.stdout.write("\n");
316
+ info("finalizing…");
317
+ let r;
318
+ try {
319
+ r = await fetch(`${base}/api/apps/${app}/finalize`, { method: "POST", headers: { ...authH(), "content-type": "application/json" }, body: "{}" });
320
+ } catch (e) {
321
+ errBox("Finalize unreachable", [String(e.cause?.message || e.message || e)]);
322
+ process.exit(1);
323
+ }
107
324
  const j = await r.json().catch(() => ({}));
108
- if (!r.ok) { console.error("ERR finalize:", j.error || r.status); process.exit(1); }
109
- console.log(`\nDone in ${((Date.now() - t0) / 1000).toFixed(1)}s: ${j.url}`);
110
- console.log(`Expires: ${new Date(j.expires_at * 1000).toLocaleDateString()} (redeploy renews 30d)`);
325
+ if (!r.ok) { errBox(`Finalize failed: ${j.error || ("http " + r.status)}`); process.exit(1); }
326
+
327
+ const secs = ((Date.now() - t0) / 1000).toFixed(1);
328
+ console.log("");
329
+ box([
330
+ `${green(SYM.ok)} ${bold("Deployed in " + secs + "s")}`,
331
+ `${bold(j.url)}`,
332
+ `${dim(`${files.length} files · ${fmtBytes(bytes)} · expires ${new Date(j.expires_at * 1000).toLocaleDateString()} (redeploy renews 30d)`)}`,
333
+ ]);
334
+ }
335
+
336
+ function help() {
337
+ header();
338
+ console.log(`
339
+ ${bold("Usage:")} ${cyan("quickhost")} ${dim("<command> [options]")}
340
+
341
+ ${bold("Commands")}
342
+ ${cyan("deploy")} [--app x] [--dir ./dist] [--api URL] upload a static dir
343
+ ${cyan("register")} -u <name> claim a username
344
+ ${cyan("login")} -u <name> log in on this machine
345
+ ${cyan("list")} live apps + expiry
346
+ ${cyan("delete")} --app x remove an app
347
+ ${cyan("whoami")} current login
348
+
349
+ ${bold("Deploy")}
350
+ auto-finds ${dim("dist dist/client(vinext) build out .output/public public")}
351
+ runs your ${dim("build")} script once if nothing is built yet.
352
+ needs ${bold("index.html")} at the publish dir root. static only, SPA fallback on.
353
+
354
+ ${bold("Limits")} ${dim("50MB/app · 500 files · 25MB/file · 20 apps/user · 30-day rolling expiry")}
355
+ ${dim("Docs: https://kaooffline.top/docs")}`);
111
356
  }
112
357
 
113
358
  const cmd = process.argv[2];
114
- if (cmd === "register" || cmd === "login") await registerOrLogin(cmd);
115
- else if (cmd === "deploy" || !cmd) await deploy();
359
+ if (!cmd || cmd === "deploy") await deploy();
360
+ else if (cmd === "register" || cmd === "login") await registerOrLogin(cmd);
116
361
  else if (cmd === "list") {
117
- const r = await fetch(`${apiBase()}/api/apps`, { headers: authH() });
118
- const j = await r.json();
119
- if (!r.ok) { console.error("ERR:", j.error); process.exit(1); }
120
- 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}`);
121
- if (!(j.apps || []).length) console.log("(no live apps)");
122
- } else if (cmd === "delete") {
362
+ header("apps");
363
+ let r;
364
+ try { r = await fetch(`${apiBase()}/api/apps`, { headers: authH() }); }
365
+ catch (e) { errBox("API unreachable", [String(e.cause?.message || e.message || e)]); process.exit(1); }
366
+ const j = await r.json().catch(() => ({}));
367
+ if (!r.ok) { errBox(j.error || ("http " + r.status)); process.exit(1); }
368
+ const apps = j.apps || [];
369
+ if (!apps.length) { info("(no live apps — deploy one with: quickhost deploy)"); process.exit(0); }
370
+ const rows = apps.map((a) => [`${a.username}.kaooffline.top/${a.appname}`, `expires ${new Date(a.expires_at * 1000).toLocaleDateString()}`, `${a.file_count} files`]);
371
+ const w0 = Math.max(...rows.map((r) => r[0].length));
372
+ 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))}`);
373
+ hint(`${apps.length} live app${apps.length === 1 ? "" : "s"}`);
374
+ } else if (cmd === "delete" || cmd === "revoke") {
375
+ header("delete");
123
376
  const app = arg("--app") || arg("-a");
124
- if (!app) { console.error("Usage: quickhost delete --app <name>"); process.exit(1); }
377
+ if (!app) { errBox("Usage: quickhost delete --app <name>"); process.exit(1); }
125
378
  const r = await fetch(`${apiBase()}/api/apps/${app}`, { method: "DELETE", headers: authH() });
126
- console.log(r.ok ? "deleted" : "ERR " + (await r.text()));
379
+ if (r.ok) done(`deleted ${bold(app)}`);
380
+ else errBox("Delete failed", [(await r.text()).slice(0, 300)]);
127
381
  } else if (cmd === "whoami") {
128
- const r = await fetch(`${apiBase()}/api/me`, { headers: authH() });
129
- console.log(await r.text());
130
- } else { console.log("Usage: quickhost [register -u x|login -u x|deploy --app x --dir ./dist|list|delete --app x|whoami]"); }
382
+ header("whoami");
383
+ const c = load();
384
+ if (!c.username) { warn("not logged in here."); hint("run: quickhost login -u <name>"); process.exit(0); }
385
+ let extra = "";
386
+ try {
387
+ const r = await fetch(`${apiBase()}/api/me`, { headers: authH() });
388
+ extra = dim(" (" + (await r.text()).slice(0, 120) + ")");
389
+ } catch {}
390
+ done(`${bold(c.username)} ${dim("·")} ${dim(apiBase())}${extra ? "\n" + extra : ""}`);
391
+ } else if (cmd === "--help" || cmd === "-h" || cmd === "help") help();
392
+ else if (cmd === "--version" || cmd === "-v" || cmd === "version") console.log(`quickhost v${VERSION}`);
393
+ 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.0",
3
+ "version": "0.2.0",
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" },