@kaooffline/quickhost 0.1.1 → 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
@@ -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
 
@@ -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,265 @@ function walk(dir, out = [], root = dir) {
70
129
  }
71
130
  return out;
72
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
+
73
212
  function detectDir() {
74
213
  const manual = arg("--dir") || arg("-d");
75
- if (manual) return resolve(manual);
76
- for (const c of ["dist", "build", "out", "public", "."]) {
77
- 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);
78
221
  }
222
+ const found = findBuiltDir();
223
+ if (found) return { dir: found, fw, built: false };
79
224
  // 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));
87
- }
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);
88
237
  }
89
- } catch {}
90
- 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
+ }
91
253
  process.exit(1);
92
254
  }
93
255
 
94
256
  async function deploy() {
257
+ const t0 = Date.now();
258
+ header("deploy");
95
259
  const cfg = load();
96
- 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
+ }
97
264
  let app = (arg("--app") || arg("-a") || "").toLowerCase().trim();
98
265
  if (!app) {
99
266
  try { app = basename(resolve(process.cwd())).toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 40); }
100
267
  catch {}
101
268
  }
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();
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");
104
277
  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); }
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))}`);
107
288
  try {
108
289
  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.");
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
+ }
111
294
  } catch {}
112
- console.log(`deploying ${files.length} files from ${dir} as ${cfg.username}/${app} ...`);
113
- const t0 = Date.now();
295
+
296
+ step(3, TOTAL, `uploading ${dim("→ " + apiBase())}`);
297
+ const base = apiBase();
114
298
  let i = 0;
115
299
  for (const f of files) {
116
300
  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: "{}" });
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
+ }
125
324
  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)`);
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")}`);
129
356
  }
130
357
 
131
358
  const cmd = process.argv[2];
132
- if (cmd === "register" || cmd === "login") await registerOrLogin(cmd);
133
- else if (cmd === "deploy" || !cmd) await deploy();
359
+ if (!cmd || cmd === "deploy") await deploy();
360
+ else if (cmd === "register" || cmd === "login") await registerOrLogin(cmd);
134
361
  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") {
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");
141
376
  const app = arg("--app") || arg("-a");
142
- if (!app) { console.error("Usage: quickhost delete --app <name>"); process.exit(1); }
377
+ if (!app) { errBox("Usage: quickhost delete --app <name>"); process.exit(1); }
143
378
  const r = await fetch(`${apiBase()}/api/apps/${app}`, { method: "DELETE", headers: authH() });
144
- 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)]);
145
381
  } 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]"); }
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.1",
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" },