@kaooffline/quickhost 0.1.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 +40 -0
- package/bin/quickhost.js +130 -0
- package/package.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# quickhost CLI
|
|
2
|
+
|
|
3
|
+
One-command static hosting on [kaooffline.top](https://kaooffline.top). Vite / Astro / plain HTML → `https://you.kaooffline.top/app`. Free, no server. Apps expire 30 days after last deploy (redeploy renews).
|
|
4
|
+
|
|
5
|
+
Works with `npx`, `bunx`, `pnpm dlx`.
|
|
6
|
+
|
|
7
|
+
## Install / use
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @kaooffline/quickhost register -u alice
|
|
11
|
+
cd my-vite-app
|
|
12
|
+
npx @kaooffline/quickhost deploy --app demo
|
|
13
|
+
# -> https://alice.kaooffline.top/demo/
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
With bun:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bunx @kaooffline/quickhost register -u alice
|
|
20
|
+
bunx @kaooffline/quickhost deploy --app demo
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
quickhost register -u <name> claim username (asks password)
|
|
27
|
+
quickhost login -u <name> login on this machine
|
|
28
|
+
quickhost deploy [--app x] [--dir ./dist] [--api URL]
|
|
29
|
+
quickhost list live apps + expiry dates
|
|
30
|
+
quickhost delete --app x remove an app now
|
|
31
|
+
quickhost whoami current login
|
|
32
|
+
```
|
|
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.
|
|
35
|
+
|
|
36
|
+
## Limits
|
|
37
|
+
|
|
38
|
+
50MB/app, 500 files/app, 25MB/file, 20 apps/user, 9GB platform-wide (free-tier hard lock). Static only, no databases. 10 deploys/day/IP.
|
|
39
|
+
|
|
40
|
+
Docs: https://kaooffline.top/docs
|
package/bin/quickhost.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// quickhost CLI - zero dependencies, Node 18+.
|
|
3
|
+
// Usage:
|
|
4
|
+
// npx @kaooffline/quickhost register -u alice
|
|
5
|
+
// npx @kaooffline/quickhost login -u alice
|
|
6
|
+
// 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";
|
|
9
|
+
import { join, relative, resolve, basename } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
12
|
+
|
|
13
|
+
const CFG = join(homedir(), ".quickhost.json");
|
|
14
|
+
const load = () => { try { return JSON.parse(readFileSync(CFG, "utf8")); } catch { return {}; } };
|
|
15
|
+
const save = (c) => writeFileSync(CFG, JSON.stringify(c, null, 2));
|
|
16
|
+
const arg = (k, d = null) => {
|
|
17
|
+
const i = process.argv.indexOf(k);
|
|
18
|
+
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : d;
|
|
19
|
+
};
|
|
20
|
+
const apiBase = () => (arg("--api") || process.env.QUICKHOST_API || load().api || "https://api.kaooffline.top").replace(/\/$/, "");
|
|
21
|
+
const authH = () => { const t = load().token; return t ? { authorization: "Bearer " + t } : {}; };
|
|
22
|
+
|
|
23
|
+
const MIME = { html: "text/html", js: "text/javascript", mjs: "text/javascript", css: "text/css", json: "application/json", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp", gif: "image/gif", svg: "image/svg+xml", ico: "image/x-icon", woff: "font/woff", woff2: "font/woff2", ttf: "font/ttf", txt: "text/plain", xml: "application/xml", webmanifest: "application/manifest+json", map: "application/json" };
|
|
24
|
+
const mime = (p) => MIME[(p.split(".").pop() || "").toLowerCase()] || "application/octet-stream";
|
|
25
|
+
|
|
26
|
+
async function promptSecret(q) {
|
|
27
|
+
process.stdout.write(q);
|
|
28
|
+
return await new Promise((res) => {
|
|
29
|
+
let s = "";
|
|
30
|
+
process.stdin.setRawMode?.(true); process.stdin.resume(); process.stdin.setEncoding("utf8");
|
|
31
|
+
const on = (c) => {
|
|
32
|
+
if (c === "\r" || c === "\n" || c === "\u0004") { process.stdin.setRawMode?.(false); process.stdin.pause(); process.stdin.removeListener("data", on); process.stdout.write("\n"); res(s); }
|
|
33
|
+
else if (c === "\u0003") process.exit(1);
|
|
34
|
+
else if (c === "\b" || c === "\x7f") { s = s.slice(0, -1); }
|
|
35
|
+
else s += c;
|
|
36
|
+
};
|
|
37
|
+
process.stdin.on("data", on);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function registerOrLogin(mode) {
|
|
42
|
+
const u = (arg("-u") || arg("--username") || "").toLowerCase().trim();
|
|
43
|
+
if (!u) { console.error(`Usage: quickhost ${mode} -u <username>`); process.exit(1); }
|
|
44
|
+
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); }
|
|
48
|
+
save({ ...load(), api: apiBase(), username: j.username, token: j.token });
|
|
49
|
+
console.log(`ok: logged in as ${j.username}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function walk(dir, out = [], root = dir) {
|
|
53
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
54
|
+
const p = join(dir, e.name);
|
|
55
|
+
if (e.isDirectory()) { if (e.name === "node_modules" || e.name === ".git") continue; walk(p, out, root); }
|
|
56
|
+
else out.push({ abs: p, rel: relative(root, p).replace(/\\/g, "/") });
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
function detectDir() {
|
|
61
|
+
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));
|
|
65
|
+
}
|
|
66
|
+
// 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
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch {}
|
|
77
|
+
console.error("ERR: no index.html found. Build your app (npm run build) or pass --dir ./dist");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function deploy() {
|
|
82
|
+
const cfg = load();
|
|
83
|
+
if (!cfg.token) { console.error("ERR: not logged in. Run: quickhost login -u <name>"); process.exit(1); }
|
|
84
|
+
let app = (arg("--app") || arg("-a") || "").toLowerCase().trim();
|
|
85
|
+
if (!app) {
|
|
86
|
+
try { app = basename(resolve(process.cwd())).toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 40); }
|
|
87
|
+
catch {}
|
|
88
|
+
}
|
|
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();
|
|
91
|
+
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();
|
|
96
|
+
let i = 0;
|
|
97
|
+
for (const f of files) {
|
|
98
|
+
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: "{}" });
|
|
107
|
+
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)`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const cmd = process.argv[2];
|
|
114
|
+
if (cmd === "register" || cmd === "login") await registerOrLogin(cmd);
|
|
115
|
+
else if (cmd === "deploy" || !cmd) await deploy();
|
|
116
|
+
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") {
|
|
123
|
+
const app = arg("--app") || arg("-a");
|
|
124
|
+
if (!app) { console.error("Usage: quickhost delete --app <name>"); process.exit(1); }
|
|
125
|
+
const r = await fetch(`${apiBase()}/api/apps/${app}`, { method: "DELETE", headers: authH() });
|
|
126
|
+
console.log(r.ok ? "deleted" : "ERR " + (await r.text()));
|
|
127
|
+
} 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]"); }
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kaooffline/quickhost",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "quickHOST CLI - one-command static hosting (Vite/Astro/HTML) to username.kaooffline.top/app. Free, 30-day rolling expiry.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "quickhost": "bin/quickhost.js" },
|
|
7
|
+
"files": ["bin"],
|
|
8
|
+
"keywords": ["hosting", "vite", "astro", "static", "deploy", "kaooffline", "quickhost"],
|
|
9
|
+
"homepage": "https://kaooffline.top/docs",
|
|
10
|
+
"repository": { "type": "git", "url": "https://kaooffline.top" },
|
|
11
|
+
"engines": { "node": ">=18" },
|
|
12
|
+
"license": "MIT"
|
|
13
|
+
}
|