htmlhost-cli 1.1.0 → 1.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/package.json +1 -1
- package/src/cli.mjs +14 -9
- package/src/commands/deploy.mjs +123 -8
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -4,34 +4,39 @@
|
|
|
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.2.0";
|
|
8
8
|
|
|
9
9
|
const HELP = `
|
|
10
10
|
${bold("htmlhost")} ${dim(`v${VERSION}`)} — deploy HTML from the terminal
|
|
11
11
|
|
|
12
12
|
${bold("Usage:")}
|
|
13
|
-
${cyan("htmlhost
|
|
14
|
-
${cyan("htmlhost deploy")} <file> [options] Deploy an HTML file
|
|
13
|
+
${cyan("htmlhost deploy")} [file] [options] Deploy (defaults to index.html)
|
|
15
14
|
${cyan("htmlhost list")} List your sites
|
|
16
15
|
${cyan("htmlhost delete")} <slug> Delete a site
|
|
17
16
|
${cyan("htmlhost upload")} <file|dir> Upload media assets
|
|
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
20
|
|
|
21
21
|
${bold("Deploy options:")}
|
|
22
22
|
--ttl <value> Set TTL: 1d, 7d, 30d, never
|
|
23
|
-
--slug <slug> Re-deploy to
|
|
23
|
+
--slug <slug> Re-deploy to a specific site
|
|
24
24
|
--title <title> Set the site title
|
|
25
|
+
--new Force a new site (ignore .htmlhost link)
|
|
25
26
|
|
|
26
27
|
${bold("Delete options:")}
|
|
27
28
|
--force, -f Skip confirmation prompt
|
|
28
29
|
|
|
30
|
+
${bold("How deploy works:")}
|
|
31
|
+
1st deploy: creates a site, saves slug to ${dim(".htmlhost")}
|
|
32
|
+
2nd deploy: reads ${dim(".htmlhost")}, updates the same site
|
|
33
|
+
|
|
29
34
|
${bold("Examples:")}
|
|
30
|
-
${dim("$")} htmlhost deploy index.html
|
|
31
|
-
${dim("$")} htmlhost deploy
|
|
32
|
-
${dim("$")} htmlhost deploy
|
|
35
|
+
${dim("$")} htmlhost deploy ${dim("# deploys ./index.html")}
|
|
36
|
+
${dim("$")} htmlhost deploy page.html ${dim("# deploys a specific file")}
|
|
37
|
+
${dim("$")} htmlhost deploy --ttl 30d ${dim("# deploy with 30-day TTL")}
|
|
38
|
+
${dim("$")} htmlhost deploy --new ${dim("# force a new site")}
|
|
33
39
|
${dim("$")} htmlhost upload logo.png
|
|
34
|
-
${dim("$")} htmlhost upload ./assets/
|
|
35
40
|
${dim("$")} htmlhost delete old-project --force
|
|
36
41
|
|
|
37
42
|
${dim(`Docs: https://htmlhost.co/docs`)}
|
|
@@ -41,7 +46,7 @@ export async function run(argv) {
|
|
|
41
46
|
const command = argv[0];
|
|
42
47
|
const args = argv.slice(1);
|
|
43
48
|
|
|
44
|
-
if (!command || command === "--help" || command === "-h") {
|
|
49
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
45
50
|
console.log(HELP);
|
|
46
51
|
return;
|
|
47
52
|
}
|
package/src/commands/deploy.mjs
CHANGED
|
@@ -1,11 +1,59 @@
|
|
|
1
|
-
import { readFileSync, statSync, existsSync } from "node:fs";
|
|
2
|
-
import { basename, resolve } from "node:path";
|
|
1
|
+
import { readFileSync, writeFileSync, statSync, existsSync } from "node:fs";
|
|
2
|
+
import { basename, resolve, dirname, join } from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
3
4
|
import { post } from "../api.mjs";
|
|
4
|
-
import { ok, err, info, cyan, dim, bold, formatBytes } from "../ui.mjs";
|
|
5
|
+
import { ok, err, info, cyan, dim, bold, yellow, formatBytes } from "../ui.mjs";
|
|
6
|
+
|
|
7
|
+
const LINK_FILE = ".htmlhost";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read the project link from .htmlhost
|
|
11
|
+
*/
|
|
12
|
+
function readLink(dir) {
|
|
13
|
+
const linkPath = join(dir, LINK_FILE);
|
|
14
|
+
if (!existsSync(linkPath)) return null;
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(linkPath, "utf8"));
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Save project link to .htmlhost
|
|
24
|
+
*/
|
|
25
|
+
function writeLink(dir, data) {
|
|
26
|
+
const linkPath = join(dir, LINK_FILE);
|
|
27
|
+
writeFileSync(linkPath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
28
|
+
}
|
|
5
29
|
|
|
6
30
|
/**
|
|
7
|
-
*
|
|
8
|
-
|
|
31
|
+
* Interactive menu prompt — returns the index of the selected option.
|
|
32
|
+
*/
|
|
33
|
+
function promptChoice(question, options) {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
36
|
+
console.log("");
|
|
37
|
+
console.log(` ${yellow("?")} ${question}`);
|
|
38
|
+
console.log("");
|
|
39
|
+
options.forEach((opt, i) => {
|
|
40
|
+
console.log(` ${bold(String(i + 1))}. ${opt}`);
|
|
41
|
+
});
|
|
42
|
+
console.log("");
|
|
43
|
+
rl.question(` Enter choice (1-${options.length}): `, (answer) => {
|
|
44
|
+
rl.close();
|
|
45
|
+
const idx = parseInt(answer.trim(), 10) - 1;
|
|
46
|
+
resolve(idx >= 0 && idx < options.length ? idx : -1);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* htmlhost deploy [file] [--ttl 7d] [--slug existing-slug] [--title "My Site"] [--new]
|
|
53
|
+
*
|
|
54
|
+
* - Defaults to index.html in the current directory
|
|
55
|
+
* - Remembers the site via .htmlhost file (auto re-deploy)
|
|
56
|
+
* - Use --new to force a fresh deploy
|
|
9
57
|
*/
|
|
10
58
|
export async function deploy(args) {
|
|
11
59
|
let file = args.find((a) => !a.startsWith("--"));
|
|
@@ -18,16 +66,72 @@ export async function deploy(args) {
|
|
|
18
66
|
info(`No file specified, using ${cyan("index.html")}`);
|
|
19
67
|
} else {
|
|
20
68
|
err("No file specified and no index.html found in the current directory.");
|
|
21
|
-
console.log(` ${dim("Usage: htmlhost deploy [file.html] [--ttl 7d]
|
|
69
|
+
console.log(` ${dim("Usage: htmlhost deploy [file.html] [--ttl 7d]")}`);
|
|
22
70
|
process.exit(1);
|
|
23
71
|
}
|
|
24
72
|
}
|
|
25
73
|
|
|
26
74
|
const ttl = getFlag(args, "--ttl");
|
|
27
|
-
const slug = getFlag(args, "--slug");
|
|
28
75
|
const title = getFlag(args, "--title");
|
|
76
|
+
const forceNew = args.includes("--new");
|
|
29
77
|
|
|
30
78
|
const filePath = resolve(file);
|
|
79
|
+
const projectDir = dirname(filePath);
|
|
80
|
+
|
|
81
|
+
// Resolve slug: explicit --slug > .htmlhost link > new deploy
|
|
82
|
+
let slug = getFlag(args, "--slug");
|
|
83
|
+
let isLinked = false;
|
|
84
|
+
|
|
85
|
+
if (!slug && !forceNew) {
|
|
86
|
+
const link = readLink(projectDir);
|
|
87
|
+
if (link?.slug) {
|
|
88
|
+
// Check if user has a saved preference
|
|
89
|
+
if (link.onRedeploy === "overwrite") {
|
|
90
|
+
slug = link.slug;
|
|
91
|
+
isLinked = true;
|
|
92
|
+
info(`Linked to ${cyan(slug)} ${dim("(auto-overwrite)")}`);
|
|
93
|
+
} else if (link.onRedeploy === "new") {
|
|
94
|
+
info(`Creating new site ${dim("(auto-new)")}`);
|
|
95
|
+
// slug stays null → new deploy
|
|
96
|
+
} else {
|
|
97
|
+
// Ask the user
|
|
98
|
+
const choice = await promptChoice(
|
|
99
|
+
`This project is linked to ${cyan(bold(link.slug + ".htmlhost.co"))}`,
|
|
100
|
+
[
|
|
101
|
+
"Overwrite existing site",
|
|
102
|
+
"Create a new site instead",
|
|
103
|
+
"Cancel",
|
|
104
|
+
`Always overwrite ${dim("(remember for this project)")}`,
|
|
105
|
+
`Always create new ${dim("(remember for this project)")}`,
|
|
106
|
+
]
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
switch (choice) {
|
|
110
|
+
case 0: // Overwrite
|
|
111
|
+
slug = link.slug;
|
|
112
|
+
isLinked = true;
|
|
113
|
+
break;
|
|
114
|
+
case 1: // New
|
|
115
|
+
break;
|
|
116
|
+
case 2: // Cancel
|
|
117
|
+
case -1:
|
|
118
|
+
console.log(" Cancelled.");
|
|
119
|
+
process.exit(0);
|
|
120
|
+
break;
|
|
121
|
+
case 3: // Always overwrite
|
|
122
|
+
slug = link.slug;
|
|
123
|
+
isLinked = true;
|
|
124
|
+
writeLink(projectDir, { ...link, onRedeploy: "overwrite" });
|
|
125
|
+
info(`Saved preference: ${dim("always overwrite")}`);
|
|
126
|
+
break;
|
|
127
|
+
case 4: // Always new
|
|
128
|
+
writeLink(projectDir, { ...link, onRedeploy: "new" });
|
|
129
|
+
info(`Saved preference: ${dim("always create new")}`);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
31
135
|
|
|
32
136
|
// Verify file exists
|
|
33
137
|
let stat;
|
|
@@ -47,7 +151,7 @@ export async function deploy(args) {
|
|
|
47
151
|
const name = basename(filePath);
|
|
48
152
|
|
|
49
153
|
info(`Reading ${cyan(name)} ${dim(`(${formatBytes(stat.size)})`)}`);
|
|
50
|
-
info("Deploying…");
|
|
154
|
+
info(slug ? `Re-deploying to ${cyan(slug)}…` : "Deploying new site…");
|
|
51
155
|
|
|
52
156
|
const body = { html };
|
|
53
157
|
if (ttl) body.ttl = ttl;
|
|
@@ -56,12 +160,23 @@ export async function deploy(args) {
|
|
|
56
160
|
|
|
57
161
|
const data = await post("/api/sites", body);
|
|
58
162
|
|
|
163
|
+
// Save/update the link (preserve onRedeploy preference)
|
|
164
|
+
const existingLink = readLink(projectDir);
|
|
165
|
+
writeLink(projectDir, {
|
|
166
|
+
slug: data.slug,
|
|
167
|
+
url: data.url,
|
|
168
|
+
...(existingLink?.onRedeploy ? { onRedeploy: existingLink.onRedeploy } : {}),
|
|
169
|
+
});
|
|
170
|
+
|
|
59
171
|
console.log("");
|
|
60
172
|
ok(`${bold("Live")} at ${cyan(`https://${data.url}`)}`);
|
|
61
173
|
if (data.version > 1) {
|
|
62
174
|
console.log(` ${dim(`Version ${data.version} · ${data.ttl} TTL`)}`);
|
|
63
175
|
} else {
|
|
64
176
|
console.log(` ${dim(`${data.slug} · ${data.ttl} TTL`)}`);
|
|
177
|
+
if (!isLinked) {
|
|
178
|
+
console.log(` ${dim(`Linked → .htmlhost`)}`);
|
|
179
|
+
}
|
|
65
180
|
}
|
|
66
181
|
if (data.expiresAt) {
|
|
67
182
|
const d = new Date(data.expiresAt);
|