@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 +1 -1
- package/bin/quickhost.js +300 -55
- 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
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) {
|
|
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,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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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) {
|
|
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)) {
|
|
103
|
-
|
|
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) {
|
|
106
|
-
if (files.length > 500) {
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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) {
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
133
|
-
else if (cmd === "
|
|
359
|
+
if (!cmd || cmd === "deploy") await deploy();
|
|
360
|
+
else if (cmd === "register" || cmd === "login") await registerOrLogin(cmd);
|
|
134
361
|
else if (cmd === "list") {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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) {
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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.
|
|
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" },
|