htmlhost-cli 1.7.0 → 1.8.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 +7 -3
- package/package.json +1 -1
- package/src/cli.mjs +18 -3
- package/src/commands/deploy.mjs +18 -7
- package/src/commands/list.mjs +24 -6
- package/src/commands/open.mjs +29 -0
package/README.md
CHANGED
|
@@ -70,14 +70,18 @@ Add a `404.html` file and it will be served automatically for missing pages.
|
|
|
70
70
|
| Option | Description |
|
|
71
71
|
|---|---|
|
|
72
72
|
| `--ttl <value>` | Set expiry: `1d`, `7d`, `30d`, `never` |
|
|
73
|
-
| `--slug <slug>` | Re-deploy to an existing site
|
|
74
|
-
| `--title <title>` | Set the site title
|
|
73
|
+
| `--slug <slug>` | Re-deploy to an existing site |
|
|
74
|
+
| `--title <title>` | Set the site title |
|
|
75
75
|
| `--new` | Force new sites (ignore .htmlhost link) |
|
|
76
76
|
| `--no-assets` | Skip automatic asset uploading |
|
|
77
77
|
| `--pages` | Deploy directory as one multi-page site |
|
|
78
|
+
| `--json` | Output result as JSON (for CI/CD) |
|
|
78
79
|
|
|
79
80
|
### `htmlhost list`
|
|
80
|
-
List all your sites with
|
|
81
|
+
List all your sites with titles, sizes, TTL, and expiry. Multi-page sites are marked with 📄.
|
|
82
|
+
|
|
83
|
+
### `htmlhost open <slug>`
|
|
84
|
+
Open a deployed site in the default browser.
|
|
81
85
|
|
|
82
86
|
### `htmlhost delete <slug>`
|
|
83
87
|
Delete a site. Use `--force` to skip confirmation.
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { bold, dim, cyan, err } from "./ui.mjs";
|
|
5
5
|
import { ApiError } from "./api.mjs";
|
|
6
6
|
|
|
7
|
-
const VERSION = "1.
|
|
7
|
+
const VERSION = "1.8.0";
|
|
8
8
|
|
|
9
9
|
const HELP = `
|
|
10
10
|
${bold("htmlhost")} ${dim(`v${VERSION}`)} — deploy HTML from the terminal
|
|
@@ -17,6 +17,7 @@ const HELP = `
|
|
|
17
17
|
${cyan("htmlhost login")} Authenticate with an API token
|
|
18
18
|
${cyan("htmlhost whoami")} Show current user
|
|
19
19
|
${cyan("htmlhost logout")} Remove saved token
|
|
20
|
+
${cyan("htmlhost open")} <slug> Open a site in the browser
|
|
20
21
|
|
|
21
22
|
${bold("Deploy options:")}
|
|
22
23
|
--ttl <value> Set TTL: 1d, 7d, 30d, never
|
|
@@ -44,6 +45,7 @@ const HELP = `
|
|
|
44
45
|
${dim("$")} htmlhost deploy . --pages ${dim("# multi-page site from dir")}
|
|
45
46
|
${dim("$")} htmlhost deploy --ttl 30d ${dim("# deploy with 30-day TTL")}
|
|
46
47
|
${dim("$")} htmlhost deploy --new ${dim("# force new sites")}
|
|
48
|
+
${dim("$")} htmlhost deploy index.html --json ${dim("# JSON output for CI/CD")}
|
|
47
49
|
${dim("$")} htmlhost upload logo.png
|
|
48
50
|
${dim("$")} htmlhost delete old-project --force
|
|
49
51
|
|
|
@@ -54,6 +56,14 @@ export async function run(argv) {
|
|
|
54
56
|
const command = argv[0];
|
|
55
57
|
const args = argv.slice(1);
|
|
56
58
|
|
|
59
|
+
// Global --json flag — suppress human output, print JSON
|
|
60
|
+
const jsonMode = args.includes("--json");
|
|
61
|
+
if (jsonMode) {
|
|
62
|
+
const filtered = args.filter((a) => a !== "--json");
|
|
63
|
+
args.length = 0;
|
|
64
|
+
args.push(...filtered);
|
|
65
|
+
}
|
|
66
|
+
|
|
57
67
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
58
68
|
console.log(HELP);
|
|
59
69
|
return;
|
|
@@ -83,13 +93,13 @@ export async function run(argv) {
|
|
|
83
93
|
}
|
|
84
94
|
case "deploy": {
|
|
85
95
|
const { deploy } = await import("./commands/deploy.mjs");
|
|
86
|
-
await deploy(args);
|
|
96
|
+
await deploy(args, { json: jsonMode });
|
|
87
97
|
break;
|
|
88
98
|
}
|
|
89
99
|
case "list":
|
|
90
100
|
case "ls": {
|
|
91
101
|
const { list } = await import("./commands/list.mjs");
|
|
92
|
-
await list();
|
|
102
|
+
await list({ json: jsonMode });
|
|
93
103
|
break;
|
|
94
104
|
}
|
|
95
105
|
case "delete":
|
|
@@ -103,6 +113,11 @@ export async function run(argv) {
|
|
|
103
113
|
await upload(args);
|
|
104
114
|
break;
|
|
105
115
|
}
|
|
116
|
+
case "open": {
|
|
117
|
+
const { open } = await import("./commands/open.mjs");
|
|
118
|
+
await open(args);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
106
121
|
default:
|
|
107
122
|
err(`Unknown command: ${command}`);
|
|
108
123
|
console.log(` Run ${cyan("htmlhost --help")} for usage.`);
|
package/src/commands/deploy.mjs
CHANGED
|
@@ -58,11 +58,12 @@ function promptChoice(question, options) {
|
|
|
58
58
|
* - Remembers the site(s) via .htmlhost file (auto re-deploy)
|
|
59
59
|
* - Use --new to force fresh deploys
|
|
60
60
|
*/
|
|
61
|
-
export async function deploy(args) {
|
|
61
|
+
export async function deploy(args, { json = false } = {}) {
|
|
62
62
|
let file = args.find((a) => !a.startsWith("--"));
|
|
63
63
|
|
|
64
64
|
const ttl = getFlag(args, "--ttl");
|
|
65
65
|
const title = getFlag(args, "--title");
|
|
66
|
+
const slug = getFlag(args, "--slug");
|
|
66
67
|
const forceNew = args.includes("--new");
|
|
67
68
|
const skipAssets = args.includes("--no-assets");
|
|
68
69
|
const multiPage = args.includes("--pages");
|
|
@@ -74,7 +75,7 @@ export async function deploy(args) {
|
|
|
74
75
|
const stat = statSync(filePath);
|
|
75
76
|
if (stat.isDirectory()) {
|
|
76
77
|
if (multiPage) {
|
|
77
|
-
return deployMultiPage(filePath, { ttl, title, forceNew, skipAssets });
|
|
78
|
+
return deployMultiPage(filePath, { ttl, title, slug, forceNew, skipAssets, json });
|
|
78
79
|
}
|
|
79
80
|
return deployDirectory(filePath, { ttl, forceNew, skipAssets });
|
|
80
81
|
}
|
|
@@ -85,7 +86,7 @@ export async function deploy(args) {
|
|
|
85
86
|
|
|
86
87
|
// --pages without explicit dir defaults to cwd
|
|
87
88
|
if (multiPage && !file) {
|
|
88
|
-
return deployMultiPage(resolve("."), { ttl, title, forceNew, skipAssets });
|
|
89
|
+
return deployMultiPage(resolve("."), { ttl, title, slug, forceNew, skipAssets, json });
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
// Default to index.html in the current directory
|
|
@@ -102,13 +103,13 @@ export async function deploy(args) {
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
// Single-file deploy
|
|
105
|
-
await deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug
|
|
106
|
+
await deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug, json });
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
/**
|
|
109
110
|
* Deploy a single HTML file.
|
|
110
111
|
*/
|
|
111
|
-
async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug }) {
|
|
112
|
+
async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug, json }) {
|
|
112
113
|
const filePath = resolve(file);
|
|
113
114
|
const projectDir = dirname(filePath);
|
|
114
115
|
const fileName = basename(filePath);
|
|
@@ -238,6 +239,11 @@ async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug }
|
|
|
238
239
|
});
|
|
239
240
|
}
|
|
240
241
|
|
|
242
|
+
if (json) {
|
|
243
|
+
console.log(JSON.stringify(data));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
241
247
|
console.log("");
|
|
242
248
|
ok(`${bold("Live")} at ${cyan(`https://${data.url}`)}`);
|
|
243
249
|
if (data.version > 1) {
|
|
@@ -412,7 +418,7 @@ async function deployDirectory(dirPath, { ttl, forceNew, skipAssets }) {
|
|
|
412
418
|
* blog/index.html → /blog
|
|
413
419
|
* blog/post.html → /blog/post
|
|
414
420
|
*/
|
|
415
|
-
async function deployMultiPage(dirPath, { ttl, title, forceNew, skipAssets }) {
|
|
421
|
+
async function deployMultiPage(dirPath, { ttl, title, slug: explicitSlug, forceNew, skipAssets, json }) {
|
|
416
422
|
// Find all .html files (recursive) — skip dotfiles
|
|
417
423
|
const htmlFiles = findHtmlFiles(dirPath, dirPath);
|
|
418
424
|
|
|
@@ -423,7 +429,7 @@ async function deployMultiPage(dirPath, { ttl, title, forceNew, skipAssets }) {
|
|
|
423
429
|
|
|
424
430
|
// Read existing link data
|
|
425
431
|
const link = readLink(dirPath);
|
|
426
|
-
const existingSlug = (!forceNew && link?.multipage?.slug) || null;
|
|
432
|
+
const existingSlug = explicitSlug || (!forceNew && link?.multipage?.slug) || null;
|
|
427
433
|
|
|
428
434
|
console.log("");
|
|
429
435
|
info(`Multi-page deploy: ${cyan(bold(String(htmlFiles.length)))} page${htmlFiles.length > 1 ? "s" : ""} in ${cyan(basename(dirPath) || ".")}`);
|
|
@@ -484,6 +490,11 @@ async function deployMultiPage(dirPath, { ttl, title, forceNew, skipAssets }) {
|
|
|
484
490
|
assets: Object.fromEntries(persistedCache),
|
|
485
491
|
});
|
|
486
492
|
|
|
493
|
+
if (json) {
|
|
494
|
+
console.log(JSON.stringify(data));
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
487
498
|
console.log("");
|
|
488
499
|
console.log(` ${green("━".repeat(40))}`);
|
|
489
500
|
ok(`${bold("Live")} at ${cyan(`https://${data.url}`)}`);
|
package/src/commands/list.mjs
CHANGED
|
@@ -4,8 +4,14 @@ import { dim, cyan, bold, formatBytes, yellow } from "../ui.mjs";
|
|
|
4
4
|
/**
|
|
5
5
|
* htmlhost list — show all sites.
|
|
6
6
|
*/
|
|
7
|
-
export async function list() {
|
|
7
|
+
export async function list({ json = false } = {}) {
|
|
8
8
|
const data = await get("/api/sites");
|
|
9
|
+
|
|
10
|
+
if (json) {
|
|
11
|
+
console.log(JSON.stringify(data));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
const sites = data.sites || [];
|
|
10
16
|
|
|
11
17
|
if (sites.length === 0) {
|
|
@@ -17,11 +23,14 @@ export async function list() {
|
|
|
17
23
|
|
|
18
24
|
console.log("");
|
|
19
25
|
|
|
20
|
-
//
|
|
21
|
-
const slugW = Math.max(
|
|
26
|
+
// Compute column widths dynamically
|
|
27
|
+
const slugW = Math.max(14, ...sites.map((s) => s.slug.length)) + 2;
|
|
28
|
+
const titleW = Math.max(10, ...sites.map((s) => (s.title || "").length).filter((n) => n < 30)) + 2;
|
|
29
|
+
const clampedTitleW = Math.min(titleW, 30);
|
|
30
|
+
|
|
22
31
|
const hdr = [
|
|
23
32
|
pad("SLUG", slugW),
|
|
24
|
-
pad("
|
|
33
|
+
pad("TITLE", clampedTitleW),
|
|
25
34
|
pad("SIZE", 10),
|
|
26
35
|
pad("TTL", 8),
|
|
27
36
|
pad("EXPIRES", 14),
|
|
@@ -32,8 +41,10 @@ export async function list() {
|
|
|
32
41
|
for (const s of sites) {
|
|
33
42
|
const expires = s.expiresAt ? relTime(s.expiresAt) : "never";
|
|
34
43
|
const expiresColor = s.expiresAt && new Date(s.expiresAt) < new Date() ? yellow : dim;
|
|
44
|
+
const multiIndicator = s.isMultiPage ? "📄 " : "";
|
|
45
|
+
const title = truncate(s.title && s.title !== s.slug ? s.title : "", clampedTitleW - 2);
|
|
35
46
|
console.log(
|
|
36
|
-
` ${pad(cyan(s.slug), slugW)}${pad(
|
|
47
|
+
` ${pad(cyan(multiIndicator + s.slug), slugW + (multiIndicator ? 2 : 0))}${pad(dim(title), clampedTitleW)}${pad(s.size, 10)}${pad(s.ttl, 8)}${expiresColor(expires)}`
|
|
37
48
|
);
|
|
38
49
|
}
|
|
39
50
|
|
|
@@ -48,7 +59,14 @@ export async function list() {
|
|
|
48
59
|
|
|
49
60
|
function pad(str, width) {
|
|
50
61
|
const s = String(str);
|
|
51
|
-
|
|
62
|
+
// Account for ANSI escape codes in string length
|
|
63
|
+
const visible = s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
64
|
+
return s + " ".repeat(Math.max(0, width - visible.length));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function truncate(str, max) {
|
|
68
|
+
if (str.length <= max) return str;
|
|
69
|
+
return str.slice(0, max - 1) + "…";
|
|
52
70
|
}
|
|
53
71
|
|
|
54
72
|
function relTime(iso) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { err, cyan, bold, ok } from "../ui.mjs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* htmlhost open <slug> — open a site in the browser.
|
|
6
|
+
*/
|
|
7
|
+
export async function open(args) {
|
|
8
|
+
const slug = args.find((a) => !a.startsWith("--"));
|
|
9
|
+
if (!slug) {
|
|
10
|
+
err("Usage: htmlhost open <slug>");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const url = `https://${slug}.htmlhost.co`;
|
|
15
|
+
ok(`Opening ${cyan(bold(url))}…`);
|
|
16
|
+
|
|
17
|
+
// Cross-platform open command
|
|
18
|
+
const cmd =
|
|
19
|
+
process.platform === "darwin" ? "open" :
|
|
20
|
+
process.platform === "win32" ? "start" :
|
|
21
|
+
"xdg-open";
|
|
22
|
+
|
|
23
|
+
exec(`${cmd} ${url}`, (error) => {
|
|
24
|
+
if (error) {
|
|
25
|
+
err(`Could not open browser: ${error.message}`);
|
|
26
|
+
console.log(` ${url}`);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|