htmlhost-cli 2.12.0 → 2.13.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 +11 -0
- package/package.json +3 -3
- package/src/api.mjs +11 -10
- package/src/cli.mjs +9 -1
- package/src/commands/clone.mjs +5 -1
- package/src/commands/deploy.mjs +15 -11
- package/src/commands/list.mjs +4 -3
- package/src/commands/pull.mjs +5 -1
- package/src/commands/unpublish.mjs +15 -0
- package/src/commands/versions.mjs +2 -1
- package/src/publication.mjs +21 -0
package/README.md
CHANGED
|
@@ -223,3 +223,14 @@ MIT
|
|
|
223
223
|
### Free publishing allowance
|
|
224
224
|
|
|
225
225
|
Free publishing is limited. Subscribe to Pro to continue creating unlimited sites at https://htmlhost.co/upgrade. See Pro benefits at https://htmlhost.co/pricing. Before your first free CLI deployment, sign in at https://htmlhost.co/new to complete browser verification. If verification is unavailable, contact hey@htmlhost.co for review.
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
## Unpublished sites and drafts
|
|
229
|
+
|
|
230
|
+
Unpublished websites remain private until publishing is explicitly confirmed. Collaborators can publish with confirmation; only owners can unpublish. The server also cancels deployments still running when someone unpublishes.
|
|
231
|
+
|
|
232
|
+
- `htmlhost deploy --draft` saves files without changing publication.
|
|
233
|
+
- `htmlhost deploy --publish` explicitly makes an unpublished website public. The same flag applies to `rollback`.
|
|
234
|
+
- Interactive deploys ask a separate, default-No question when the website is unpublished. `--force` and `--json` never grant this permission. Agents must obtain the user's consent before using `--publish`.
|
|
235
|
+
- `htmlhost unpublish <slug>` takes the website offline while retaining files and history. Non-interactive use requires `--yes` after owner confirmation.
|
|
236
|
+
- `list` labels owned and shared drafts. `pull` / `clone` retrieve working files, including saved drafts, by default; `--published` selects the last deployed files and `--draft` requires a draft.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "htmlhost-cli",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Deploy HTML files from the terminal
|
|
3
|
+
"version": "2.13.0",
|
|
4
|
+
"description": "Deploy HTML files from the terminal \u2014 htmlhost.co CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"htmlhost": "bin/htmlhost.mjs"
|
|
@@ -28,6 +28,6 @@
|
|
|
28
28
|
"url": "git+https://github.com/htmlhost/cli.git"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"test": "node --test test/"
|
|
31
|
+
"test": "node --experimental-test-module-mocks --test test/"
|
|
32
32
|
}
|
|
33
33
|
}
|
package/src/api.mjs
CHANGED
|
@@ -5,9 +5,10 @@ import { getToken, getApi } from "./config.mjs";
|
|
|
5
5
|
import { dim, yellow } from "./ui.mjs";
|
|
6
6
|
|
|
7
7
|
export class ApiError extends Error {
|
|
8
|
-
constructor(message, status) {
|
|
8
|
+
constructor(message, status, code) {
|
|
9
9
|
super(message);
|
|
10
10
|
this.status = status;
|
|
11
|
+
this.code = code;
|
|
11
12
|
}
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -19,9 +20,9 @@ const BASE_DELAY_MS = 500;
|
|
|
19
20
|
* Retries on network errors ("fetch failed", ECONNRESET, etc.)
|
|
20
21
|
* and 5xx server errors. Does NOT retry on 4xx client errors.
|
|
21
22
|
*/
|
|
22
|
-
async function withRetry(fn, label = "request") {
|
|
23
|
+
async function withRetry(fn, label = "request", attempts = MAX_RETRIES) {
|
|
23
24
|
let lastError;
|
|
24
|
-
for (let attempt = 1; attempt <=
|
|
25
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
25
26
|
try {
|
|
26
27
|
return await fn();
|
|
27
28
|
} catch (e) {
|
|
@@ -30,13 +31,13 @@ async function withRetry(fn, label = "request") {
|
|
|
30
31
|
!(e instanceof ApiError) ||
|
|
31
32
|
(e.status >= 500 && e.status < 600);
|
|
32
33
|
|
|
33
|
-
if (!isNetworkError || attempt ===
|
|
34
|
+
if (!isNetworkError || attempt === attempts) {
|
|
34
35
|
throw e;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
38
39
|
console.log(
|
|
39
|
-
` ${yellow("↻")} ${label} failed (attempt ${attempt}/${
|
|
40
|
+
` ${yellow("↻")} ${label} failed (attempt ${attempt}/${attempts}), retrying in ${delay}ms… ${dim(e.message)}`
|
|
40
41
|
);
|
|
41
42
|
await new Promise((r) => setTimeout(r, delay));
|
|
42
43
|
}
|
|
@@ -60,7 +61,7 @@ export async function get(path) {
|
|
|
60
61
|
headers: headers(),
|
|
61
62
|
});
|
|
62
63
|
const data = await res.json();
|
|
63
|
-
if (!res.ok) throw new ApiError(data.error || `HTTP ${res.status}`, res.status);
|
|
64
|
+
if (!res.ok) throw new ApiError(data.error || `HTTP ${res.status}`, res.status, data.code);
|
|
64
65
|
return data;
|
|
65
66
|
}, `GET ${path}`);
|
|
66
67
|
}
|
|
@@ -73,9 +74,9 @@ export async function post(path, body) {
|
|
|
73
74
|
body: JSON.stringify(body),
|
|
74
75
|
});
|
|
75
76
|
const data = await res.json();
|
|
76
|
-
if (!res.ok) throw new ApiError(data.error || `HTTP ${res.status}`, res.status);
|
|
77
|
+
if (!res.ok) throw new ApiError(data.error || `HTTP ${res.status}`, res.status, data.code);
|
|
77
78
|
return data;
|
|
78
|
-
}, `POST ${path}
|
|
79
|
+
}, `POST ${path}`, body?.confirmPublish === true ? 1 : MAX_RETRIES);
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
export async function del(path) {
|
|
@@ -85,7 +86,7 @@ export async function del(path) {
|
|
|
85
86
|
headers: headers(),
|
|
86
87
|
});
|
|
87
88
|
const data = await res.json();
|
|
88
|
-
if (!res.ok) throw new ApiError(data.error || `HTTP ${res.status}`, res.status);
|
|
89
|
+
if (!res.ok) throw new ApiError(data.error || `HTTP ${res.status}`, res.status, data.code);
|
|
89
90
|
return data;
|
|
90
91
|
}, `DELETE ${path}`);
|
|
91
92
|
}
|
|
@@ -108,7 +109,7 @@ export async function uploadFile(path, filePath, fileName, mimeType) {
|
|
|
108
109
|
body: form,
|
|
109
110
|
});
|
|
110
111
|
const data = await res.json();
|
|
111
|
-
if (!res.ok) throw new ApiError(data.error || `HTTP ${res.status}`, res.status);
|
|
112
|
+
if (!res.ok) throw new ApiError(data.error || `HTTP ${res.status}`, res.status, data.code);
|
|
112
113
|
return data;
|
|
113
114
|
}, `upload ${fileName}`);
|
|
114
115
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { bold, dim, cyan, err } from "./ui.mjs";
|
|
|
5
5
|
import { ApiError } from "./api.mjs";
|
|
6
6
|
import { startUpdateCheck, applyUpdate } from "./update.mjs";
|
|
7
7
|
|
|
8
|
-
const VERSION = "2.
|
|
8
|
+
const VERSION = "2.13.0";
|
|
9
9
|
|
|
10
10
|
const HELP = `
|
|
11
11
|
${bold("htmlhost")} ${dim(`v${VERSION}`)} — deploy HTML from the terminal
|
|
@@ -20,6 +20,7 @@ const HELP = `
|
|
|
20
20
|
${cyan("htmlhost recipients")} [slug] [cmd] Manage form notification recipients (owner)
|
|
21
21
|
${cyan("htmlhost list")} List your sites and ones shared with you
|
|
22
22
|
${cyan("htmlhost versions")} [slug] List a site's published versions
|
|
23
|
+
${cyan("htmlhost unpublish")} [slug] Take a site offline (owner only)
|
|
23
24
|
${cyan("htmlhost rollback")} [slug] [ver] Make an earlier version live again (Pro)
|
|
24
25
|
${cyan("htmlhost delete")} <slug> Delete a site (owner)
|
|
25
26
|
${cyan("htmlhost login")} Sign in (approve in your browser)
|
|
@@ -33,6 +34,8 @@ const HELP = `
|
|
|
33
34
|
--title <title> Set the site title
|
|
34
35
|
--new Force a new site (ignore .htmlhost link)
|
|
35
36
|
--no-assets Skip auto-uploading local assets (single-file only)
|
|
37
|
+
--draft Save as a draft; leave publication unchanged
|
|
38
|
+
--publish Explicitly make an unpublished site public
|
|
36
39
|
--no-pull-check Skip remote change detection before deploy
|
|
37
40
|
|
|
38
41
|
${bold("Upload options:")}
|
|
@@ -139,6 +142,11 @@ export async function run(argv) {
|
|
|
139
142
|
await whoami();
|
|
140
143
|
break;
|
|
141
144
|
}
|
|
145
|
+
case "unpublish": {
|
|
146
|
+
const { unpublish } = await import("./commands/unpublish.mjs");
|
|
147
|
+
await unpublish(args, { json: jsonMode });
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
142
150
|
case "deploy": {
|
|
143
151
|
const { deploy } = await import("./commands/deploy.mjs");
|
|
144
152
|
await deploy(args, { json: jsonMode });
|
package/src/commands/clone.mjs
CHANGED
|
@@ -44,6 +44,8 @@ export async function clone(args) {
|
|
|
44
44
|
const positionals = args.filter((a) => !a.startsWith("--"));
|
|
45
45
|
const slug = positionals[0];
|
|
46
46
|
const targetDir = positionals[1]; // optional — defaults to slug name
|
|
47
|
+
const source = args.includes("--published") ? "published" : args.includes("--draft") ? "draft" : "working";
|
|
48
|
+
if (args.includes("--published") && args.includes("--draft")) throw new Error("Choose --published or --draft, not both.");
|
|
47
49
|
const force = args.includes("--force") || args.includes("-f");
|
|
48
50
|
|
|
49
51
|
if (!slug) {
|
|
@@ -86,12 +88,14 @@ export async function clone(args) {
|
|
|
86
88
|
// Fetch remote content
|
|
87
89
|
let data;
|
|
88
90
|
try {
|
|
89
|
-
data = await get(`/api/sites/pull?slug=${encodeURIComponent(cleanSlug)}`);
|
|
91
|
+
data = await get(`/api/sites/pull?slug=${encodeURIComponent(cleanSlug)}&source=${source}`);
|
|
90
92
|
} catch (e) {
|
|
91
93
|
err(`Failed to fetch site: ${e.message}`);
|
|
92
94
|
process.exit(1);
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
info(`${data.source === "draft" ? "Saved draft" : "Last published files"} · ${data.isPublished === false ? "website is unpublished" : "website is published"}`);
|
|
98
|
+
|
|
95
99
|
const pageCount = (data.pages || []).length;
|
|
96
100
|
const assetCount = (data.assets || []).length;
|
|
97
101
|
const totalFiles = pageCount + assetCount;
|
package/src/commands/deploy.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync, statSync, readdirSync, existsSync } from "node:fs";
|
|
|
2
2
|
import { basename, resolve, dirname, join, sep } from "node:path";
|
|
3
3
|
import { createInterface } from "node:readline";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
|
+
import { postPublication } from "../publication.mjs";
|
|
5
6
|
import { post } from "../api.mjs";
|
|
6
7
|
import { ok, err, info, warn, cyan, dim, bold, yellow, green, formatBytes, mimeFromExt } from "../ui.mjs";
|
|
7
8
|
import { checkRemoteChanges } from "./pull.mjs";
|
|
@@ -145,6 +146,9 @@ export async function deploy(args, { json = false } = {}) {
|
|
|
145
146
|
const ttl = values["--ttl"] ?? null;
|
|
146
147
|
const title = values["--title"] ?? null;
|
|
147
148
|
const slug = values["--slug"] ?? null;
|
|
149
|
+
const confirmPublish = args.includes("--publish");
|
|
150
|
+
const isDraft = args.includes("--draft");
|
|
151
|
+
if (confirmPublish && isDraft) throw new Error("Choose --draft or --publish, not both.");
|
|
148
152
|
const forceNew = args.includes("--new");
|
|
149
153
|
const skipAssets = args.includes("--no-assets");
|
|
150
154
|
const skipPullCheck = args.includes("--no-pull-check");
|
|
@@ -162,17 +166,17 @@ export async function deploy(args, { json = false } = {}) {
|
|
|
162
166
|
|
|
163
167
|
// --- Directory deploy (default) ---
|
|
164
168
|
if (stat.isDirectory()) {
|
|
165
|
-
return deployDirectory(targetPath, { ttl, title, slug, forceNew, json, skipPullCheck });
|
|
169
|
+
return deployDirectory(targetPath, { ttl, title, slug, forceNew, json, skipPullCheck, confirmPublish, isDraft });
|
|
166
170
|
}
|
|
167
171
|
|
|
168
172
|
// --- Single-file deploy ---
|
|
169
|
-
await deploySingleFile(target || "index.html", { ttl, title, forceNew, skipAssets, slug, json, skipPullCheck });
|
|
173
|
+
await deploySingleFile(target || "index.html", { ttl, title, forceNew, skipAssets, slug, json, skipPullCheck, confirmPublish, isDraft });
|
|
170
174
|
}
|
|
171
175
|
|
|
172
176
|
/**
|
|
173
177
|
* Deploy a single HTML file (existing behavior preserved).
|
|
174
178
|
*/
|
|
175
|
-
async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug, json, skipPullCheck }) {
|
|
179
|
+
async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug, json, skipPullCheck, confirmPublish, isDraft }) {
|
|
176
180
|
const filePath = resolve(file);
|
|
177
181
|
const projectDir = dirname(filePath);
|
|
178
182
|
const fileName = basename(filePath);
|
|
@@ -268,12 +272,12 @@ async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug,
|
|
|
268
272
|
if (aborted) return;
|
|
269
273
|
}
|
|
270
274
|
|
|
271
|
-
const body = { html };
|
|
275
|
+
const body = { html, confirmPublish, isDraft };
|
|
272
276
|
if (ttl) body.ttl = ttl;
|
|
273
277
|
if (slug) body.slug = slug;
|
|
274
278
|
if (title) body.title = title;
|
|
275
279
|
|
|
276
|
-
const data = await
|
|
280
|
+
const data = await postPublication("/api/sites", body, { json });
|
|
277
281
|
|
|
278
282
|
// Save link
|
|
279
283
|
const existingLink = readLink(projectDir) || {};
|
|
@@ -299,7 +303,7 @@ async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug,
|
|
|
299
303
|
}
|
|
300
304
|
|
|
301
305
|
console.log("");
|
|
302
|
-
ok(`${bold("Live")} at ${cyan(`https://${data.url}`)}`);
|
|
306
|
+
ok(isDraft ? `${bold("Draft saved")} · ${data.isPublished === false ? "not public" : "live site unchanged"}` : `${bold("Live")} at ${cyan(`https://${data.url}`)}`);
|
|
303
307
|
if (data.version > 1) {
|
|
304
308
|
console.log(` ${dim(`Version ${data.version} · ${data.ttl} TTL`)}`);
|
|
305
309
|
} else {
|
|
@@ -321,7 +325,7 @@ async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug,
|
|
|
321
325
|
* Non-HTML files (CSS, JS, images, fonts) are uploaded via the media endpoint
|
|
322
326
|
* and included as pages with their hosted URLs.
|
|
323
327
|
*/
|
|
324
|
-
async function deployDirectory(dirPath, { ttl, title, slug: explicitSlug, forceNew, json, skipPullCheck }) {
|
|
328
|
+
async function deployDirectory(dirPath, { ttl, title, slug: explicitSlug, forceNew, json, skipPullCheck, confirmPublish, isDraft }) {
|
|
325
329
|
// Find all files recursively
|
|
326
330
|
const allFiles = findAllFiles(dirPath, dirPath);
|
|
327
331
|
|
|
@@ -519,7 +523,7 @@ async function deployDirectory(dirPath, { ttl, title, slug: explicitSlug, forceN
|
|
|
519
523
|
console.log("");
|
|
520
524
|
info(existingSlug ? `Deploying to ${cyan(existingSlug)}…` : "Deploying new site…");
|
|
521
525
|
|
|
522
|
-
const body = { pages: pagePayloads, assets: assetPayloads };
|
|
526
|
+
const body = { pages: pagePayloads, assets: assetPayloads, confirmPublish, isDraft };
|
|
523
527
|
if (redirectsText !== null) body.redirects = redirectsText;
|
|
524
528
|
if (ttl) body.ttl = ttl;
|
|
525
529
|
if (existingSlug) {
|
|
@@ -532,7 +536,7 @@ async function deployDirectory(dirPath, { ttl, title, slug: explicitSlug, forceN
|
|
|
532
536
|
if (title) body.title = title;
|
|
533
537
|
|
|
534
538
|
try {
|
|
535
|
-
const data = await
|
|
539
|
+
const data = await postPublication("/api/sites", body, { json });
|
|
536
540
|
|
|
537
541
|
// Save link
|
|
538
542
|
writeLink(dirPath, {
|
|
@@ -554,11 +558,11 @@ async function deployDirectory(dirPath, { ttl, title, slug: explicitSlug, forceN
|
|
|
554
558
|
const ac = assetPayloads.length;
|
|
555
559
|
console.log("");
|
|
556
560
|
console.log(` ${green("━".repeat(40))}`);
|
|
557
|
-
ok(`${bold("Live")} at ${cyan(`https://${data.url}`)}`);
|
|
561
|
+
ok(isDraft ? `${bold("Draft saved")} · ${data.isPublished === false ? "not public" : "live site unchanged"}` : `${bold("Live")} at ${cyan(`https://${data.url}`)}`);
|
|
558
562
|
console.log(` ${dim(`${pc} page${pc !== 1 ? "s" : ""} · ${ac} asset${ac !== 1 ? "s" : ""} · ${data.ttl} TTL`)}`);
|
|
559
563
|
console.log("");
|
|
560
564
|
|
|
561
|
-
for (const { path: pagePath } of pagePayloads) {
|
|
565
|
+
for (const { path: pagePath } of (isDraft ? [] : pagePayloads)) {
|
|
562
566
|
const urlPath = pagePath === "/" ? "" : pagePath;
|
|
563
567
|
console.log(` ${cyan(`https://${data.url}${urlPath}`)}`);
|
|
564
568
|
}
|
package/src/commands/list.mjs
CHANGED
|
@@ -35,6 +35,7 @@ export async function list({ json = false } = {}) {
|
|
|
35
35
|
pad("TITLE", clampedTitleW),
|
|
36
36
|
pad("SIZE", 10),
|
|
37
37
|
pad("TTL", 8),
|
|
38
|
+
pad("STATUS", 12),
|
|
38
39
|
pad("EXPIRES", 14),
|
|
39
40
|
].join("");
|
|
40
41
|
console.log(` ${dim(hdr)}`);
|
|
@@ -46,7 +47,7 @@ export async function list({ json = false } = {}) {
|
|
|
46
47
|
const multiIndicator = s.isMultiPage ? "📄 " : "";
|
|
47
48
|
const title = truncate(s.title && s.title !== s.slug ? s.title : "", clampedTitleW - 2);
|
|
48
49
|
console.log(
|
|
49
|
-
` ${pad(cyan(multiIndicator + s.slug), slugW + (multiIndicator ? 2 : 0))}${pad(dim(title), clampedTitleW)}${pad(s.size, 10)}${pad(s.ttl, 8)}${expiresColor(expires)}`
|
|
50
|
+
` ${pad(cyan(multiIndicator + s.slug), slugW + (multiIndicator ? 2 : 0))}${pad(dim(title), clampedTitleW)}${pad(s.size, 10)}${pad(s.ttl, 8)}${pad(s.isPublished === false ? "Draft" : "Published", 12)}${expiresColor(expires)}`
|
|
50
51
|
);
|
|
51
52
|
}
|
|
52
53
|
|
|
@@ -54,10 +55,10 @@ export async function list({ json = false } = {}) {
|
|
|
54
55
|
const sharedSlugW = Math.max(14, ...shared.map((s) => s.slug.length)) + 2;
|
|
55
56
|
console.log("");
|
|
56
57
|
console.log(` ${bold("Shared with you")} ${dim("(clone, deploy and restore; only the owner can delete)")}`);
|
|
57
|
-
console.log(` ${dim(pad("SLUG", sharedSlugW) + pad("TITLE", 30) + "OWNER")}`);
|
|
58
|
+
console.log(` ${dim(pad("SLUG", sharedSlugW) + pad("TITLE", 30) + pad("STATUS", 12) + "OWNER")}`);
|
|
58
59
|
for (const s of shared) {
|
|
59
60
|
const title = truncate(s.title && s.title !== s.slug ? s.title : "", 28);
|
|
60
|
-
console.log(` ${pad(cyan(s.slug), sharedSlugW)}${pad(dim(title), 30)}${dim(s.owner || "")}`);
|
|
61
|
+
console.log(` ${pad(cyan(s.slug), sharedSlugW)}${pad(dim(title), 30)}${pad(s.isPublished === false ? "Draft" : "Published", 12)}${dim(s.owner || "")}`);
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
package/src/commands/pull.mjs
CHANGED
|
@@ -58,6 +58,8 @@ function diffFile(localPath, remoteContent) {
|
|
|
58
58
|
*/
|
|
59
59
|
export async function pull(args) {
|
|
60
60
|
const slugArg = args.find((a) => !a.startsWith("--"));
|
|
61
|
+
const source = args.includes("--published") ? "published" : args.includes("--draft") ? "draft" : "working";
|
|
62
|
+
if (args.includes("--published") && args.includes("--draft")) throw new Error("Choose --published or --draft, not both.");
|
|
61
63
|
const force = args.includes("--force") || args.includes("-f");
|
|
62
64
|
const dryRun = args.includes("--dry-run");
|
|
63
65
|
|
|
@@ -90,12 +92,14 @@ export async function pull(args) {
|
|
|
90
92
|
// Fetch remote content
|
|
91
93
|
let data;
|
|
92
94
|
try {
|
|
93
|
-
data = await get(`/api/sites/pull?slug=${encodeURIComponent(slug)}`);
|
|
95
|
+
data = await get(`/api/sites/pull?slug=${encodeURIComponent(slug)}&source=${source}`);
|
|
94
96
|
} catch (e) {
|
|
95
97
|
err(`Failed to fetch site: ${e.message}`);
|
|
96
98
|
process.exit(1);
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
info(`${data.source === "draft" ? "Saved draft" : "Last published files"} · ${data.isPublished === false ? "website is unpublished" : "website is published"}`);
|
|
102
|
+
|
|
99
103
|
console.log(` ${dim(`Version ${data.version} · Updated ${relTime(data.updatedAt)}`)}`);
|
|
100
104
|
console.log("");
|
|
101
105
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { post } from "../api.mjs";
|
|
2
|
+
import { readLink } from "../link.mjs";
|
|
3
|
+
import { confirm, ok } from "../ui.mjs";
|
|
4
|
+
|
|
5
|
+
export async function unpublish(args, { json = false } = {}) {
|
|
6
|
+
const slug = (args.find(a => !a.startsWith("-")) || readLink(process.cwd())?.site?.slug)?.replace(/\.htmlhost\.co$/, "");
|
|
7
|
+
if (!slug) throw new Error("Usage: htmlhost unpublish <slug> [--yes]");
|
|
8
|
+
if (!args.includes("--yes")) {
|
|
9
|
+
if (json || !process.stdin.isTTY) throw new Error("Unpublishing takes the website offline. Re-run with --yes after confirming with the owner.");
|
|
10
|
+
if (!await confirm(`Take ${slug}.htmlhost.co and its connected domains offline? Files will be kept as a draft.`)) return;
|
|
11
|
+
}
|
|
12
|
+
const result = await post(`/api/sites/${encodeURIComponent(slug)}/unpublish`, {});
|
|
13
|
+
if (json) console.log(JSON.stringify(result));
|
|
14
|
+
else ok(`${slug} is unpublished. Files, drafts and history are kept.`);
|
|
15
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { postPublication } from "../publication.mjs";
|
|
1
2
|
import { get, post } from "../api.mjs";
|
|
2
3
|
import { ok, err, info, warn, dim, cyan, bold, confirm, formatBytes } from "../ui.mjs";
|
|
3
4
|
import { readLink } from "../link.mjs";
|
|
@@ -117,7 +118,7 @@ export async function rollback(args, { json = false } = {}) {
|
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
const result = await
|
|
121
|
+
const result = await postPublication(`/api/sites/${encodeURIComponent(data.slug)}/versions/${target.version}/restore`, { confirmPublish: args.includes("--publish") }, { json });
|
|
121
122
|
if (json) {
|
|
122
123
|
console.log(JSON.stringify(result, null, 2));
|
|
123
124
|
return;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { post } from "./api.mjs";
|
|
3
|
+
|
|
4
|
+
/** Never let --force, --json or a general deployment confirmation publish a draft. */
|
|
5
|
+
export async function postPublication(path, body, { json = false } = {}) {
|
|
6
|
+
try {
|
|
7
|
+
return await post(path, body);
|
|
8
|
+
} catch (error) {
|
|
9
|
+
if (error.code !== "PUBLICATION_CONFIRMATION_REQUIRED") throw error;
|
|
10
|
+
if (json || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
11
|
+
error.message += " Ask the user first, then re-run with --publish to confirm making it public; use deploy --draft to keep it private.";
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
15
|
+
let answer;
|
|
16
|
+
try { answer = await prompt.question("This website is unpublished. Make it publicly accessible now? (y/N) "); }
|
|
17
|
+
finally { prompt.close(); }
|
|
18
|
+
if (!/^y(es)?$/i.test(answer.trim())) throw new Error("Publishing cancelled. The website remains unpublished.");
|
|
19
|
+
return post(path, { ...body, confirmPublish: true });
|
|
20
|
+
}
|
|
21
|
+
}
|