htmlhost-mcp 1.5.0 → 1.7.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 +21 -1
- package/package.json +4 -1
- package/src/api.mjs +9 -9
- package/src/server.mjs +59 -14
package/README.md
CHANGED
|
@@ -4,6 +4,14 @@ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that l
|
|
|
4
4
|
|
|
5
5
|
Works with **Cursor**, **Claude Desktop**, **Windsurf**, **Cline**, **Zed**, and any MCP-compatible tool.
|
|
6
6
|
|
|
7
|
+
**Using Claude with saved htmlhost projects?** Open [htmlhost in Claude’s directory](https://claude.ai/directory/htmlhost), choose **Connect**, then sign in to htmlhost and approve. Enable htmlhost in your chat; the same connection works on web and mobile. No npm install or server URL is needed. The setup below is for coding tools working with local files.
|
|
8
|
+
|
|
9
|
+
## Editing contract
|
|
10
|
+
|
|
11
|
+
The server sends clients instructions to follow **inspect → stage → validate → repair → deploy**, with matching guidance on its pull and deploy tools. Read the [agent guide](https://htmlhost.co/agents.md) before editing. Preserve separate project files and shared header/footer markers, inherit the existing design on new pages, and validate desktop/mobile rendering with the client's own tools. Report checks that could not run.
|
|
12
|
+
|
|
13
|
+
MCP tools manage hosting; they do not execute the browser editor's validation loop. Update an existing project by deploying its complete folder with its slug, rather than sending one generated HTML file.
|
|
14
|
+
|
|
7
15
|
## Setup
|
|
8
16
|
|
|
9
17
|
### 1. Install
|
|
@@ -126,7 +134,7 @@ ttl (optional) Time-to-live: 1d, 7d, 30d, or never
|
|
|
126
134
|
|
|
127
135
|
### `list_sites`
|
|
128
136
|
|
|
129
|
-
Returns all your sites, their URLs, sizes, TTLs, and plan usage, plus `shared`: the sites shared with you
|
|
137
|
+
Returns all your sites, their URLs, sizes, TTLs, and plan usage, plus `shared`: the sites shared with you, with their owner and your `role` (`admin`, `editor`, `contributor` or `viewer`). Admins and Editors pull, deploy to and restore those with your own key; Contributors pull and save drafts but can't publish; Viewers can't change them. Only the owner can delete a site.
|
|
130
138
|
|
|
131
139
|
### `get_site`
|
|
132
140
|
|
|
@@ -206,3 +214,15 @@ echo '{"jsonrpc":"2.0","method":"initialize","params":{"capabilities":{}},"id":1
|
|
|
206
214
|
## License
|
|
207
215
|
|
|
208
216
|
MIT
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
## Unpublished sites and drafts
|
|
220
|
+
|
|
221
|
+
Unpublished websites remain private until publishing is explicitly confirmed. Admins and Editors on a shared site can publish with confirmation and unpublish; Contributors save drafts only. The server also cancels deployments still running when someone unpublishes.
|
|
222
|
+
|
|
223
|
+
- `deploy` / `deploy_html`: use `isDraft: true` to save without changing publication. To publish an unpublished site, obtain the user's explicit agreement to make it public, then pass `confirmPublish: true`.
|
|
224
|
+
- `restore_version` also requires `confirmPublish: true` for an unpublished website; its general `confirm` argument is insufficient.
|
|
225
|
+
- Never automatically retry a publication refusal with confirmation enabled. Older clients are safely blocked by the server.
|
|
226
|
+
- `unpublish_site`: owner only; pass `confirm: true` after agreement to take the site offline.
|
|
227
|
+
- `list_sites` returns `isPublished` and `draftVersion` for owned and shared sites.
|
|
228
|
+
- `pull_site` defaults to `source: "working"` (including the saved draft). Use `source: "published"` only to deliberately retrieve last-deployed files, or `source: "draft"` to require a draft.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "htmlhost-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "MCP server for htmlhost.co — deploy, list, and manage sites from AI coding tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,5 +33,8 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@modelcontextprotocol/server": "^2.1.0",
|
|
35
35
|
"zod": "^4.6.5"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"test": "node --experimental-test-module-mocks --test test/"
|
|
36
39
|
}
|
|
37
40
|
}
|
package/src/api.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { createHash } from "node:crypto";
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
const CONFIG_PATH = join(homedir(), ".htmlhostrc");
|
|
16
16
|
/** Sent with every request, so the API can tell which version is calling. Kept in step with server.mjs. */
|
|
17
|
-
export const CLIENT_VERSION = "1.
|
|
17
|
+
export const CLIENT_VERSION = "1.7.0";
|
|
18
18
|
const DEFAULT_API = "https://htmlhost.co";
|
|
19
19
|
|
|
20
20
|
export function readConfig() {
|
|
@@ -79,7 +79,7 @@ export async function apiGet(path) {
|
|
|
79
79
|
return withRetry(async () => {
|
|
80
80
|
const res = await fetch(`${getApi()}${path}`, { headers: headers() });
|
|
81
81
|
const data = await res.json();
|
|
82
|
-
if (!res.ok) throw new ApiError(data.error || `HTTP ${res.status}`, res.status);
|
|
82
|
+
if (!res.ok) throw new ApiError(data.code === "PUBLICATION_CONFIRMATION_REQUIRED" ? `${data.error} Ask the user for explicit permission before retrying with confirmPublish: true, or use isDraft: true.` : data.error || `HTTP ${res.status}`, res.status);
|
|
83
83
|
return data;
|
|
84
84
|
}, `GET ${path}`);
|
|
85
85
|
}
|
|
@@ -92,9 +92,9 @@ export async function apiPost(path, body) {
|
|
|
92
92
|
body: JSON.stringify(body),
|
|
93
93
|
});
|
|
94
94
|
const data = await res.json();
|
|
95
|
-
if (!res.ok) throw new ApiError(data.error || `HTTP ${res.status}`, res.status);
|
|
95
|
+
if (!res.ok) throw new ApiError(data.code === "PUBLICATION_CONFIRMATION_REQUIRED" ? `${data.error} Ask the user for explicit permission before retrying with confirmPublish: true, or use isDraft: true.` : data.error || `HTTP ${res.status}`, res.status);
|
|
96
96
|
return data;
|
|
97
|
-
}, `POST ${path}
|
|
97
|
+
}, `POST ${path}`, body?.confirmPublish === true ? 1 : 3);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
export async function apiDelete(path) {
|
|
@@ -104,7 +104,7 @@ export async function apiDelete(path) {
|
|
|
104
104
|
headers: headers(),
|
|
105
105
|
});
|
|
106
106
|
const data = await res.json();
|
|
107
|
-
if (!res.ok) throw new ApiError(data.error || `HTTP ${res.status}`, res.status);
|
|
107
|
+
if (!res.ok) throw new ApiError(data.code === "PUBLICATION_CONFIRMATION_REQUIRED" ? `${data.error} Ask the user for explicit permission before retrying with confirmPublish: true, or use isDraft: true.` : data.error || `HTTP ${res.status}`, res.status);
|
|
108
108
|
return data;
|
|
109
109
|
}, `DELETE ${path}`);
|
|
110
110
|
}
|
|
@@ -182,7 +182,7 @@ export function pagePathForFile(relativePath) {
|
|
|
182
182
|
* Deploy a directory to htmlhost. Non-interactive version of CLI deploy.
|
|
183
183
|
* Returns { slug, url, version, pageCount }.
|
|
184
184
|
*/
|
|
185
|
-
export async function deployDirectory(dirPath, { slug = null, title = null, ttl = null } = {}) {
|
|
185
|
+
export async function deployDirectory(dirPath, { slug = null, title = null, ttl = null, confirmPublish = false, isDraft = false } = {}) {
|
|
186
186
|
const resolvedDir = resolve(dirPath);
|
|
187
187
|
const allFiles = findAllFiles(resolvedDir, resolvedDir);
|
|
188
188
|
|
|
@@ -248,7 +248,7 @@ export async function deployDirectory(dirPath, { slug = null, title = null, ttl
|
|
|
248
248
|
|
|
249
249
|
// Final deploy. A root `_redirects` file (Netlify style) replaces the site's
|
|
250
250
|
// redirects from this file; rules added in the dashboard stay. No file, no change.
|
|
251
|
-
const body = { pages: pagePayloads, assets: assetPayloads };
|
|
251
|
+
const body = { pages: pagePayloads, assets: assetPayloads, confirmPublish, isDraft };
|
|
252
252
|
const redirectsPath = join(resolvedDir, "_redirects");
|
|
253
253
|
if (existsSync(redirectsPath)) body.redirects = readFileSync(redirectsPath, "utf8");
|
|
254
254
|
if (ttl) body.ttl = ttl;
|
|
@@ -262,11 +262,11 @@ export async function deployDirectory(dirPath, { slug = null, title = null, ttl
|
|
|
262
262
|
/**
|
|
263
263
|
* Deploy a single HTML file. Non-interactive version of CLI deploy.
|
|
264
264
|
*/
|
|
265
|
-
export async function deploySingleFile(filePath, { slug = null, title = null, ttl = null } = {}) {
|
|
265
|
+
export async function deploySingleFile(filePath, { slug = null, title = null, ttl = null, confirmPublish = false, isDraft = false } = {}) {
|
|
266
266
|
const resolvedPath = resolve(filePath);
|
|
267
267
|
const html = readFileSync(resolvedPath, "utf8");
|
|
268
268
|
|
|
269
|
-
const body = { html };
|
|
269
|
+
const body = { html, confirmPublish, isDraft };
|
|
270
270
|
if (ttl) body.ttl = ttl;
|
|
271
271
|
if (slug) body.slug = slug;
|
|
272
272
|
if (title) body.title = title;
|
package/src/server.mjs
CHANGED
|
@@ -63,6 +63,14 @@ export function createServer() {
|
|
|
63
63
|
const server = new McpServer({
|
|
64
64
|
name: "htmlhost",
|
|
65
65
|
version: VERSION,
|
|
66
|
+
}, {
|
|
67
|
+
instructions: "Read https://htmlhost.co/agents.md before building or editing a site. " +
|
|
68
|
+
"For existing sites: pull current files into a safe working directory, inspect source and the existing design, stage precise edits, validate, repair failures, then deploy. " +
|
|
69
|
+
"For focused edits, inspect the element, parent layout and relevant styles first; expand investigation as needed without skipping validation. " +
|
|
70
|
+
"When resuming staged work, verify the original project is unchanged and retain validation failures; never treat a pause as successful completion. " +
|
|
71
|
+
"Keep HTML/CSS/JS as separate files. New pages inherit the site's design, header, footer and dependencies; preserve shared markers and synchronize shared copies. " +
|
|
72
|
+
"Check references, forms and desktop/mobile rendering with your own tools, and disclose unavailable checks. A frozen static render cannot verify animation playback. These hosting tools do not run the editor's validation loop. " +
|
|
73
|
+
"Deploy the complete project folder with the existing slug when updating; do not replace a multi-file site with deploy_html or a lone HTML file.",
|
|
66
74
|
});
|
|
67
75
|
|
|
68
76
|
// -------------------------------------------------------------------
|
|
@@ -75,6 +83,7 @@ export function createServer() {
|
|
|
75
83
|
description:
|
|
76
84
|
"Deploy a file or directory to htmlhost.co. " +
|
|
77
85
|
"Pass an absolute path to a directory (deploys all files) or a single HTML file. " +
|
|
86
|
+
"Inspect and validate changes before deploying. For an existing multi-file site, deploy its complete folder with slug to preserve its pages and assets. " +
|
|
78
87
|
"Optionally set slug (to update an existing site), title, and ttl. " +
|
|
79
88
|
"A `_redirects` file at the directory's root (one `/old /new [301|302]` per line) sets the site's redirects and isn't published.",
|
|
80
89
|
inputSchema: z.object({
|
|
@@ -82,16 +91,18 @@ export function createServer() {
|
|
|
82
91
|
slug: z.string().optional().describe("Existing site slug to update (omit to create a new site)"),
|
|
83
92
|
title: z.string().optional().describe("Title for the site"),
|
|
84
93
|
ttl: z.string().optional().describe("Time-to-live: 1d, 7d, 30d, or never"),
|
|
94
|
+
isDraft: z.boolean().optional().describe("Save files as a draft without publishing. Does not unpublish a live site."),
|
|
95
|
+
confirmPublish: z.boolean().optional().describe("Set true ONLY after the user explicitly agrees to make an unpublished website public. Ordinary editing or deploy instructions are not confirmation."),
|
|
85
96
|
}),
|
|
86
97
|
},
|
|
87
|
-
wrap(async ({ path, slug, title, ttl }) => {
|
|
98
|
+
wrap(async ({ path, slug, title, ttl, isDraft, confirmPublish }) => {
|
|
88
99
|
if (!isLoggedIn()) return NOT_SIGNED_IN;
|
|
89
100
|
|
|
90
101
|
const resolvedPath = resolve(path);
|
|
91
102
|
if (!existsSync(resolvedPath)) return `Path not found: ${path}`;
|
|
92
103
|
|
|
93
104
|
const stat = statSync(resolvedPath);
|
|
94
|
-
const opts = { slug, title, ttl };
|
|
105
|
+
const opts = { slug, title, ttl, isDraft, confirmPublish };
|
|
95
106
|
|
|
96
107
|
let data;
|
|
97
108
|
if (stat.isDirectory()) {
|
|
@@ -102,7 +113,9 @@ export function createServer() {
|
|
|
102
113
|
|
|
103
114
|
const pc = data.pageCount || 1;
|
|
104
115
|
return {
|
|
105
|
-
message: "Deployed successfully",
|
|
116
|
+
message: data.isPublished === false ? "Saved as an unpublished draft" : isDraft ? "Draft saved; live site unchanged" : "Deployed successfully",
|
|
117
|
+
isPublished: data.isPublished,
|
|
118
|
+
draftVersion: data.draftVersion,
|
|
106
119
|
url: `https://${data.url}`,
|
|
107
120
|
slug: data.slug,
|
|
108
121
|
version: data.version,
|
|
@@ -122,9 +135,11 @@ export function createServer() {
|
|
|
122
135
|
{
|
|
123
136
|
title: "List sites",
|
|
124
137
|
description:
|
|
125
|
-
"List the authenticated user's sites on htmlhost.co, and the sites shared with them
|
|
138
|
+
"List the authenticated user's sites on htmlhost.co, and the sites shared with them (`shared`, with the user's `role`). " +
|
|
126
139
|
"Returns slug, URL, title, size, TTL, and expiration for each site. " +
|
|
127
|
-
"
|
|
140
|
+
"On a shared site the role decides: admin and editor pull, deploy and restore; contributor pulls and saves drafts " +
|
|
141
|
+
"(isDraft: true) but can't publish, so ask the owner, an admin or an editor to publish; viewer changes nothing. " +
|
|
142
|
+
"Only the owner can delete a site.",
|
|
128
143
|
inputSchema: z.object({}),
|
|
129
144
|
},
|
|
130
145
|
wrap(async () => {
|
|
@@ -138,6 +153,8 @@ export function createServer() {
|
|
|
138
153
|
size: s.size,
|
|
139
154
|
ttl: s.ttl,
|
|
140
155
|
isMultiPage: s.isMultiPage,
|
|
156
|
+
isPublished: s.isPublished,
|
|
157
|
+
draftVersion: s.draftVersion,
|
|
141
158
|
expiresAt: s.expiresAt || null,
|
|
142
159
|
}));
|
|
143
160
|
|
|
@@ -146,7 +163,12 @@ export function createServer() {
|
|
|
146
163
|
url: `https://${s.slug}.htmlhost.co`,
|
|
147
164
|
title: s.title || null,
|
|
148
165
|
owner: s.owner || null,
|
|
166
|
+
role: s.role || "editor",
|
|
167
|
+
canEdit: s.canEdit ?? true,
|
|
168
|
+
canPublish: s.canPublish ?? true,
|
|
149
169
|
isMultiPage: s.isMultiPage,
|
|
170
|
+
isPublished: s.isPublished,
|
|
171
|
+
draftVersion: s.draftVersion,
|
|
150
172
|
expiresAt: s.expiresAt || null,
|
|
151
173
|
}));
|
|
152
174
|
|
|
@@ -183,6 +205,9 @@ export function createServer() {
|
|
|
183
205
|
url: `https://${cleanSlug}.htmlhost.co`,
|
|
184
206
|
version: data.version,
|
|
185
207
|
updatedAt: data.updatedAt,
|
|
208
|
+
isPublished: data.isPublished,
|
|
209
|
+
draftVersion: data.draftVersion,
|
|
210
|
+
source: data.source,
|
|
186
211
|
pages: (data.pages || []).map((p) => ({
|
|
187
212
|
path: p.path,
|
|
188
213
|
sizeBytes: p.sizeBytes || (p.html ? Buffer.byteLength(p.html) : 0),
|
|
@@ -205,20 +230,22 @@ export function createServer() {
|
|
|
205
230
|
title: "Pull site to local directory",
|
|
206
231
|
description:
|
|
207
232
|
"Download all pages and assets from a deployed htmlhost site into a local directory. " +
|
|
208
|
-
"Creates the directory if it doesn't exist."
|
|
233
|
+
"Creates the directory if it doesn't exist; existing files may be overwritten, so use a fresh directory when local work must be preserved. " +
|
|
234
|
+
"Read https://htmlhost.co/agents.md, inspect the downloaded source and design, then validate staged edits before deploying the complete folder.",
|
|
209
235
|
inputSchema: z.object({
|
|
210
236
|
slug: z.string().describe("The site slug to pull"),
|
|
211
237
|
directory: z.string().describe("Absolute path to the local directory to write files into"),
|
|
238
|
+
source: z.enum(["working", "draft", "published"]).optional().describe("Default working includes the latest saved draft. published explicitly retrieves the last deployed files."),
|
|
212
239
|
}),
|
|
213
240
|
},
|
|
214
|
-
wrap(async ({ slug, directory }) => {
|
|
241
|
+
wrap(async ({ slug, directory, source = "working" }) => {
|
|
215
242
|
if (!isLoggedIn()) return NOT_SIGNED_IN;
|
|
216
243
|
|
|
217
244
|
const cleanSlug = slug.replace(/\.htmlhost\.co$/, "");
|
|
218
245
|
const destDir = resolve(directory);
|
|
219
246
|
mkdirSync(destDir, { recursive: true });
|
|
220
247
|
|
|
221
|
-
const data = await apiGet(`/api/sites/pull?slug=${encodeURIComponent(cleanSlug)}`);
|
|
248
|
+
const data = await apiGet(`/api/sites/pull?slug=${encodeURIComponent(cleanSlug)}&source=${source}`);
|
|
222
249
|
const written = [];
|
|
223
250
|
|
|
224
251
|
// Write pages
|
|
@@ -256,6 +283,9 @@ export function createServer() {
|
|
|
256
283
|
|
|
257
284
|
return {
|
|
258
285
|
message: `Pulled ${written.length} files from ${cleanSlug}.htmlhost.co`,
|
|
286
|
+
source: data.source,
|
|
287
|
+
isPublished: data.isPublished,
|
|
288
|
+
draftVersion: data.draftVersion,
|
|
259
289
|
directory: destDir,
|
|
260
290
|
version: data.version,
|
|
261
291
|
files: written,
|
|
@@ -333,18 +363,20 @@ export function createServer() {
|
|
|
333
363
|
title: "Deploy HTML content",
|
|
334
364
|
description:
|
|
335
365
|
"Deploy raw HTML content directly (no local files needed). " +
|
|
336
|
-
"
|
|
366
|
+
"For standalone single-page output after validation. Do not use this to edit an existing multi-page or multi-asset project: pull_site, edit and validate locally, then deploy the complete folder with its slug.",
|
|
337
367
|
inputSchema: z.object({
|
|
338
368
|
html: z.string().describe("The HTML content to deploy"),
|
|
339
369
|
slug: z.string().optional().describe("Existing site slug to update (omit to create new)"),
|
|
340
370
|
title: z.string().optional().describe("Title for the site"),
|
|
341
371
|
ttl: z.string().optional().describe("Time-to-live: 1d, 7d, 30d, or never"),
|
|
372
|
+
isDraft: z.boolean().optional().describe("Save files as a draft without publishing. Does not unpublish a live site."),
|
|
373
|
+
confirmPublish: z.boolean().optional().describe("Set true ONLY after the user explicitly agrees to make an unpublished website public. Ordinary editing or deploy instructions are not confirmation."),
|
|
342
374
|
}),
|
|
343
375
|
},
|
|
344
|
-
wrap(async ({ html, slug, title, ttl }) => {
|
|
376
|
+
wrap(async ({ html, slug, title, ttl, isDraft, confirmPublish }) => {
|
|
345
377
|
if (!isLoggedIn()) return NOT_SIGNED_IN;
|
|
346
378
|
|
|
347
|
-
const body = { html };
|
|
379
|
+
const body = { html, isDraft, confirmPublish };
|
|
348
380
|
if (slug) body.slug = slug;
|
|
349
381
|
if (title) body.title = title;
|
|
350
382
|
if (ttl) body.ttl = ttl;
|
|
@@ -352,7 +384,9 @@ export function createServer() {
|
|
|
352
384
|
const data = await apiPost("/api/sites", body);
|
|
353
385
|
|
|
354
386
|
return {
|
|
355
|
-
message: "Deployed successfully",
|
|
387
|
+
message: data.isPublished === false ? "Saved as an unpublished draft" : isDraft ? "Draft saved; live site unchanged" : "Deployed successfully",
|
|
388
|
+
isPublished: data.isPublished,
|
|
389
|
+
draftVersion: data.draftVersion,
|
|
356
390
|
url: `https://${data.url}`,
|
|
357
391
|
slug: data.slug,
|
|
358
392
|
version: data.version,
|
|
@@ -383,6 +417,16 @@ export function createServer() {
|
|
|
383
417
|
})
|
|
384
418
|
);
|
|
385
419
|
|
|
420
|
+
server.registerTool("unpublish_site", {
|
|
421
|
+
title: "Unpublish a website",
|
|
422
|
+
description: "For the owner, an admin or an editor: stop public serving while retaining files, saved drafts and history. Obtain the user's explicit confirmation first.",
|
|
423
|
+
inputSchema: z.object({ slug: z.string(), confirm: z.boolean().describe("True only after the user agrees to take this website offline") }),
|
|
424
|
+
}, wrap(async ({ slug, confirm }) => {
|
|
425
|
+
if (!isLoggedIn()) return NOT_SIGNED_IN;
|
|
426
|
+
if (confirm !== true) return "Unpublish not confirmed.";
|
|
427
|
+
return apiPost(`/api/sites/${encodeURIComponent(slug.replace(/\.htmlhost\.co$/, ""))}/unpublish`, {});
|
|
428
|
+
}));
|
|
429
|
+
|
|
386
430
|
// -------------------------------------------------------------------
|
|
387
431
|
// Tool: restore_version
|
|
388
432
|
// -------------------------------------------------------------------
|
|
@@ -397,13 +441,14 @@ export function createServer() {
|
|
|
397
441
|
slug: z.string().describe("The site slug"),
|
|
398
442
|
version: z.number().int().positive().describe("The version number to restore (from list_versions)"),
|
|
399
443
|
confirm: z.boolean().describe("Must be true to confirm replacing the live site"),
|
|
444
|
+
confirmPublish: z.boolean().optional().describe("For unpublished sites, set true only after the user explicitly agrees to make the website public again."),
|
|
400
445
|
}),
|
|
401
446
|
},
|
|
402
|
-
wrap(async ({ slug, version, confirm }) => {
|
|
447
|
+
wrap(async ({ slug, version, confirm, confirmPublish }) => {
|
|
403
448
|
if (!isLoggedIn()) return NOT_SIGNED_IN;
|
|
404
449
|
if (!confirm) return "Restore not confirmed. Set confirm to true to proceed.";
|
|
405
450
|
const cleanSlug = slug.replace(/\.htmlhost\.co$/, "");
|
|
406
|
-
const data = await apiPost(`/api/sites/${encodeURIComponent(cleanSlug)}/versions/${version}/restore`, {});
|
|
451
|
+
const data = await apiPost(`/api/sites/${encodeURIComponent(cleanSlug)}/versions/${version}/restore`, { confirmPublish });
|
|
407
452
|
return {
|
|
408
453
|
message: `Restored v${version}; it's live as v${data.version}.`,
|
|
409
454
|
url: data.url ? (String(data.url).startsWith("http") ? data.url : `https://${data.url}`) : `https://${cleanSlug}.htmlhost.co`,
|