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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "htmlhost-cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Deploy HTML files from the terminal — htmlhost.co CLI",
5
5
  "type": "module",
6
6
  "bin": {
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.1.0";
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 login")} Authenticate with an API token
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 an existing site
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 index.html --ttl 30d --title "My Portfolio"
32
- ${dim("$")} htmlhost deploy build/index.html --slug my-project
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
  }
@@ -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
- * htmlhost deploy [file] [--ttl 7d] [--slug existing-slug] [--title "My Site"]
8
- * Defaults to index.html in the current directory.
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] [--slug my-site]")}`);
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);