htmlhost-cli 2.6.1 → 2.7.1
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 +4 -1
- package/src/cli.mjs +5 -2
- package/src/commands/clone.mjs +11 -27
- package/src/commands/deploy.mjs +54 -61
- package/src/commands/pull.mjs +2 -28
- package/src/commands/recipients.mjs +3 -15
- package/src/link.mjs +95 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "htmlhost-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.1",
|
|
4
4
|
"description": "Deploy HTML files from the terminal — htmlhost.co CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,5 +26,8 @@
|
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|
|
28
28
|
"url": "git+https://github.com/htmlhost/cli.git"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "node --test test/"
|
|
29
32
|
}
|
|
30
33
|
}
|
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.7.1";
|
|
9
9
|
|
|
10
10
|
const HELP = `
|
|
11
11
|
${bold("htmlhost")} ${dim(`v${VERSION}`)} — deploy HTML from the terminal
|
|
@@ -91,7 +91,10 @@ export async function run(argv) {
|
|
|
91
91
|
args.push(...filtered);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
// `htmlhost <command> --help` must never run the command — for deploy that
|
|
95
|
+
// would scan the folder and prompt to overwrite the linked site.
|
|
96
|
+
if (!command || command === "help" || command === "--help" || command === "-h" ||
|
|
97
|
+
args.includes("--help") || args.includes("-h")) {
|
|
95
98
|
console.log(HELP);
|
|
96
99
|
return;
|
|
97
100
|
}
|
package/src/commands/clone.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from
|
|
|
10
10
|
import { join, dirname, resolve } from "node:path";
|
|
11
11
|
import { get, downloadBuffer } from "../api.mjs";
|
|
12
12
|
import { fetchAgentGuide, writeAgentGuide } from "./agents.mjs";
|
|
13
|
+
import { writeLink, directorySite } from "../link.mjs";
|
|
13
14
|
import {
|
|
14
15
|
ok,
|
|
15
16
|
err,
|
|
@@ -24,7 +25,6 @@ import {
|
|
|
24
25
|
formatBytes,
|
|
25
26
|
} from "../ui.mjs";
|
|
26
27
|
|
|
27
|
-
const LINK_FILE = ".htmlhost";
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Map a page path to a local file path.
|
|
@@ -138,32 +138,16 @@ export async function clone(args) {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
// --- Write .htmlhost link file ---
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
updatedAt: data.updatedAt,
|
|
152
|
-
pulledAt: new Date().toISOString(),
|
|
153
|
-
},
|
|
154
|
-
}
|
|
155
|
-
: {
|
|
156
|
-
slug: cleanSlug,
|
|
157
|
-
url: `${cleanSlug}.htmlhost.co`,
|
|
158
|
-
sync: {
|
|
159
|
-
version: data.version,
|
|
160
|
-
updatedAt: data.updatedAt,
|
|
161
|
-
pulledAt: new Date().toISOString(),
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const linkPath = join(destDir, LINK_FILE);
|
|
166
|
-
writeFileSync(linkPath, JSON.stringify(linkData, null, 2) + "\n", "utf8");
|
|
141
|
+
// A clone is always the whole site as a folder, so it links in directory
|
|
142
|
+
// mode whatever the page count — `deploy` then updates this same site.
|
|
143
|
+
writeLink(destDir, {
|
|
144
|
+
site: directorySite(cleanSlug, pageCount),
|
|
145
|
+
sync: {
|
|
146
|
+
version: data.version,
|
|
147
|
+
updatedAt: data.updatedAt,
|
|
148
|
+
pulledAt: new Date().toISOString(),
|
|
149
|
+
},
|
|
150
|
+
});
|
|
167
151
|
|
|
168
152
|
// Teach the user's AI coding tool how htmlhost works. Best-effort: a
|
|
169
153
|
// failed download mustn't fail the clone.
|
package/src/commands/deploy.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { readFileSync,
|
|
2
|
-
import { basename, resolve, dirname, join
|
|
1
|
+
import { readFileSync, statSync, readdirSync } from "node:fs";
|
|
2
|
+
import { basename, resolve, dirname, join } from "node:path";
|
|
3
3
|
import { createInterface } from "node:readline";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { post } from "../api.mjs";
|
|
6
6
|
import { ok, err, info, warn, cyan, dim, bold, yellow, green, formatBytes, mimeFromExt } from "../ui.mjs";
|
|
7
7
|
import { checkRemoteChanges } from "./pull.mjs";
|
|
8
8
|
import { fetchAgentGuide, hasAgentGuide, writeAgentGuide } from "./agents.mjs";
|
|
9
|
-
|
|
10
|
-
const LINK_FILE = ".htmlhost";
|
|
9
|
+
import { readLink, writeLink, directorySite, directoryDeploySlug, fileDeploySlug } from "../link.mjs";
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* Upload a file to a presigned URL with automatic retry.
|
|
@@ -92,27 +91,6 @@ function shouldIgnore(name) {
|
|
|
92
91
|
return DEFAULT_IGNORE.includes(name);
|
|
93
92
|
}
|
|
94
93
|
|
|
95
|
-
/**
|
|
96
|
-
* Read the project link from .htmlhost
|
|
97
|
-
*/
|
|
98
|
-
function readLink(dir) {
|
|
99
|
-
const linkPath = join(dir, LINK_FILE);
|
|
100
|
-
if (!existsSync(linkPath)) return null;
|
|
101
|
-
try {
|
|
102
|
-
return JSON.parse(readFileSync(linkPath, "utf8"));
|
|
103
|
-
} catch {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Save project link to .htmlhost
|
|
110
|
-
*/
|
|
111
|
-
function writeLink(dir, data) {
|
|
112
|
-
const linkPath = join(dir, LINK_FILE);
|
|
113
|
-
writeFileSync(linkPath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
114
|
-
}
|
|
115
|
-
|
|
116
94
|
/**
|
|
117
95
|
* Simple y/N confirmation prompt.
|
|
118
96
|
*/
|
|
@@ -158,11 +136,12 @@ function promptChoice(question, options) {
|
|
|
158
136
|
* - Use --new to force fresh deploys
|
|
159
137
|
*/
|
|
160
138
|
export async function deploy(args, { json = false } = {}) {
|
|
161
|
-
|
|
139
|
+
const { positionals, values } = parseArgs(args, ["--ttl", "--title", "--slug"]);
|
|
140
|
+
const target = positionals[0];
|
|
162
141
|
|
|
163
|
-
const ttl =
|
|
164
|
-
const title =
|
|
165
|
-
const slug =
|
|
142
|
+
const ttl = values["--ttl"] ?? null;
|
|
143
|
+
const title = values["--title"] ?? null;
|
|
144
|
+
const slug = values["--slug"] ?? null;
|
|
166
145
|
const forceNew = args.includes("--new");
|
|
167
146
|
const skipAssets = args.includes("--no-assets");
|
|
168
147
|
const skipPullCheck = args.includes("--no-pull-check");
|
|
@@ -199,18 +178,19 @@ async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug,
|
|
|
199
178
|
|
|
200
179
|
if (!slug && !forceNew) {
|
|
201
180
|
const link = readLink(projectDir);
|
|
181
|
+
const linkedSlug = fileDeploySlug(link);
|
|
202
182
|
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
slug =
|
|
183
|
+
if (linkedSlug) {
|
|
184
|
+
const onRedeploy = link.site.onRedeploy;
|
|
185
|
+
if (onRedeploy === "overwrite") {
|
|
186
|
+
slug = linkedSlug;
|
|
207
187
|
isLinked = true;
|
|
208
188
|
info(`Linked to ${cyan(slug)} ${dim("(auto-overwrite)")}`);
|
|
209
|
-
} else if (
|
|
189
|
+
} else if (onRedeploy === "new") {
|
|
210
190
|
info(`Creating new site ${dim("(auto-new)")}`);
|
|
211
191
|
} else {
|
|
212
192
|
const choice = await promptChoice(
|
|
213
|
-
`This project is linked to ${cyan(bold(
|
|
193
|
+
`This project is linked to ${cyan(bold(linkedSlug + ".htmlhost.co"))}`,
|
|
214
194
|
[
|
|
215
195
|
"Overwrite existing site",
|
|
216
196
|
"Create a new site instead",
|
|
@@ -222,7 +202,7 @@ async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug,
|
|
|
222
202
|
|
|
223
203
|
switch (choice) {
|
|
224
204
|
case 0:
|
|
225
|
-
slug =
|
|
205
|
+
slug = linkedSlug;
|
|
226
206
|
isLinked = true;
|
|
227
207
|
break;
|
|
228
208
|
case 1:
|
|
@@ -233,13 +213,13 @@ async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug,
|
|
|
233
213
|
process.exit(0);
|
|
234
214
|
break;
|
|
235
215
|
case 3:
|
|
236
|
-
slug =
|
|
216
|
+
slug = linkedSlug;
|
|
237
217
|
isLinked = true;
|
|
238
|
-
writeLink(projectDir, { ...link, onRedeploy: "overwrite" });
|
|
218
|
+
writeLink(projectDir, { ...link, site: { ...link.site, onRedeploy: "overwrite" } });
|
|
239
219
|
info(`Saved preference: ${dim("always overwrite")}`);
|
|
240
220
|
break;
|
|
241
221
|
case 4:
|
|
242
|
-
writeLink(projectDir, { ...link, onRedeploy: "new" });
|
|
222
|
+
writeLink(projectDir, { ...link, site: { ...link.site, onRedeploy: "new" } });
|
|
243
223
|
info(`Saved preference: ${dim("always create new")}`);
|
|
244
224
|
break;
|
|
245
225
|
}
|
|
@@ -293,11 +273,16 @@ async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug,
|
|
|
293
273
|
const data = await post("/api/sites", body);
|
|
294
274
|
|
|
295
275
|
// Save link
|
|
296
|
-
const existingLink = readLink(projectDir);
|
|
276
|
+
const existingLink = readLink(projectDir) || {};
|
|
277
|
+
const onRedeploy = existingLink.site?.mode === "file" ? existingLink.site.onRedeploy : undefined;
|
|
297
278
|
writeLink(projectDir, {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
279
|
+
...existingLink,
|
|
280
|
+
site: {
|
|
281
|
+
slug: data.slug,
|
|
282
|
+
url: data.url,
|
|
283
|
+
mode: "file",
|
|
284
|
+
...(onRedeploy ? { onRedeploy } : {}),
|
|
285
|
+
},
|
|
301
286
|
sync: {
|
|
302
287
|
version: data.version,
|
|
303
288
|
updatedAt: new Date().toISOString(),
|
|
@@ -358,7 +343,8 @@ async function deployDirectory(dirPath, { ttl, title, slug: explicitSlug, forceN
|
|
|
358
343
|
|
|
359
344
|
// Read existing link data
|
|
360
345
|
const link = readLink(dirPath);
|
|
361
|
-
|
|
346
|
+
const linkedSlug = forceNew ? null : directoryDeploySlug(link);
|
|
347
|
+
let existingSlug = explicitSlug || linkedSlug || null;
|
|
362
348
|
|
|
363
349
|
// Calculate total size
|
|
364
350
|
const totalSize = allFiles.reduce((sum, f) => sum + f.size, 0);
|
|
@@ -490,13 +476,7 @@ async function deployDirectory(dirPath, { ttl, title, slug: explicitSlug, forceN
|
|
|
490
476
|
// Write .htmlhost early so a crash mid-upload doesn't orphan the site
|
|
491
477
|
if (generatedSlug && !existingSlug) {
|
|
492
478
|
existingSlug = generatedSlug;
|
|
493
|
-
writeLink(dirPath, {
|
|
494
|
-
multipage: {
|
|
495
|
-
slug: existingSlug,
|
|
496
|
-
url: `${existingSlug}.htmlhost.co`,
|
|
497
|
-
pageCount: pagePayloads.length,
|
|
498
|
-
},
|
|
499
|
-
});
|
|
479
|
+
writeLink(dirPath, { site: directorySite(existingSlug, pagePayloads.length) });
|
|
500
480
|
}
|
|
501
481
|
|
|
502
482
|
for (const u of uploadUrls) {
|
|
@@ -529,7 +509,7 @@ async function deployDirectory(dirPath, { ttl, title, slug: explicitSlug, forceN
|
|
|
529
509
|
if (existingSlug) {
|
|
530
510
|
body.slug = existingSlug;
|
|
531
511
|
// We pass isNewSite only if the generated slug wasn't originally provided
|
|
532
|
-
if (!explicitSlug && !
|
|
512
|
+
if (!explicitSlug && !linkedSlug) {
|
|
533
513
|
body.isNewSite = true;
|
|
534
514
|
}
|
|
535
515
|
}
|
|
@@ -540,11 +520,8 @@ async function deployDirectory(dirPath, { ttl, title, slug: explicitSlug, forceN
|
|
|
540
520
|
|
|
541
521
|
// Save link
|
|
542
522
|
writeLink(dirPath, {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
url: data.url,
|
|
546
|
-
pageCount: data.pageCount || pagePayloads.length,
|
|
547
|
-
},
|
|
523
|
+
...(link || {}),
|
|
524
|
+
site: { ...directorySite(data.slug, data.pageCount || pagePayloads.length), url: data.url },
|
|
548
525
|
sync: {
|
|
549
526
|
version: data.version,
|
|
550
527
|
updatedAt: new Date().toISOString(),
|
|
@@ -690,8 +667,24 @@ function findAllFiles(baseDir, currentDir) {
|
|
|
690
667
|
return results;
|
|
691
668
|
}
|
|
692
669
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
670
|
+
/**
|
|
671
|
+
* Split args into positionals and flag values. Each flag in `valueFlags`
|
|
672
|
+
* consumes the next arg (or takes `--flag=value`), so `deploy --ttl 1d`
|
|
673
|
+
* doesn't mistake "1d" for the deploy target.
|
|
674
|
+
*/
|
|
675
|
+
export function parseArgs(args, valueFlags) {
|
|
676
|
+
const positionals = [];
|
|
677
|
+
const values = {};
|
|
678
|
+
for (let i = 0; i < args.length; i++) {
|
|
679
|
+
const a = args[i];
|
|
680
|
+
const eq = a.indexOf("=");
|
|
681
|
+
const name = eq === -1 ? a : a.slice(0, eq);
|
|
682
|
+
if (valueFlags.includes(name)) {
|
|
683
|
+
if (eq !== -1) values[name] = a.slice(eq + 1);
|
|
684
|
+
else if (i + 1 < args.length) values[name] = args[++i];
|
|
685
|
+
} else if (!a.startsWith("-")) {
|
|
686
|
+
positionals.push(a);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return { positionals, values };
|
|
697
690
|
}
|
package/src/commands/pull.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "no
|
|
|
8
8
|
import { join, dirname, resolve, basename } from "node:path";
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
10
|
import { get, downloadBuffer } from "../api.mjs";
|
|
11
|
+
import { readLink, writeLink } from "../link.mjs";
|
|
11
12
|
import {
|
|
12
13
|
ok,
|
|
13
14
|
err,
|
|
@@ -22,29 +23,6 @@ import {
|
|
|
22
23
|
formatBytes,
|
|
23
24
|
} from "../ui.mjs";
|
|
24
25
|
|
|
25
|
-
const LINK_FILE = ".htmlhost";
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Read the project link from .htmlhost
|
|
29
|
-
*/
|
|
30
|
-
function readLink(dir) {
|
|
31
|
-
const linkPath = join(dir, LINK_FILE);
|
|
32
|
-
if (!existsSync(linkPath)) return null;
|
|
33
|
-
try {
|
|
34
|
-
return JSON.parse(readFileSync(linkPath, "utf8"));
|
|
35
|
-
} catch {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Save project link to .htmlhost
|
|
42
|
-
*/
|
|
43
|
-
function writeLink(dir, data) {
|
|
44
|
-
const linkPath = join(dir, LINK_FILE);
|
|
45
|
-
writeFileSync(linkPath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
26
|
/**
|
|
49
27
|
* Map a page path back to a local file path.
|
|
50
28
|
* "/" → "index.html", "/about" → "about.html", "/blog" → "blog/index.html" or "blog.html"
|
|
@@ -89,11 +67,7 @@ export async function pull(args) {
|
|
|
89
67
|
// Resolve slug
|
|
90
68
|
let slug = slugArg;
|
|
91
69
|
if (!slug) {
|
|
92
|
-
|
|
93
|
-
slug = link.multipage.slug;
|
|
94
|
-
} else if (link?.slug) {
|
|
95
|
-
slug = link.slug;
|
|
96
|
-
}
|
|
70
|
+
slug = link?.site?.slug;
|
|
97
71
|
}
|
|
98
72
|
|
|
99
73
|
if (!slug) {
|
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
import { get, post } from "../api.mjs";
|
|
2
2
|
import { ok, err, info, dim, cyan, bold, yellow, confirm } from "../ui.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { join } from "node:path";
|
|
3
|
+
import { readLink } from "../link.mjs";
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Read the slug from .htmlhost in the given (or current) directory.
|
|
10
|
-
*/
|
|
5
|
+
/** The slug linked by .htmlhost in the given (or current) directory. */
|
|
11
6
|
function readSlugFromLink(dir = process.cwd()) {
|
|
12
|
-
|
|
13
|
-
if (!existsSync(linkPath)) return null;
|
|
14
|
-
try {
|
|
15
|
-
const data = JSON.parse(readFileSync(linkPath, "utf8"));
|
|
16
|
-
return data.multipage?.slug || data.slug || null;
|
|
17
|
-
} catch {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
7
|
+
return readLink(dir)?.site?.slug || null;
|
|
20
8
|
}
|
|
21
9
|
|
|
22
10
|
/**
|
package/src/link.mjs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The .htmlhost link file — ties a local directory to a hosted site.
|
|
3
|
+
*
|
|
4
|
+
* {
|
|
5
|
+
* "site": {
|
|
6
|
+
* "slug": "my-site",
|
|
7
|
+
* "url": "my-site.htmlhost.co",
|
|
8
|
+
* "mode": "directory", // how the site is deployed from here
|
|
9
|
+
* "pageCount": 1, // directory mode only, informational
|
|
10
|
+
* "onRedeploy": "overwrite" // file mode only, remembered prompt choice
|
|
11
|
+
* },
|
|
12
|
+
* "sync": { "version": 3, "updatedAt": "…", "pulledAt": "…", "deployedAt": "…" },
|
|
13
|
+
* "assets": { "img/a.png": "https://…" } // single-file asset upload cache
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* `mode` records the workflow, not the page count: "directory" means the whole
|
|
17
|
+
* folder is the site (any number of pages plus assets — what `clone` and
|
|
18
|
+
* `deploy <dir>` produce); "file" means one HTML file deployed on its own.
|
|
19
|
+
*
|
|
20
|
+
* Older CLIs wrote `{ multipage: {slug, url, pageCount} }` for directory sites
|
|
21
|
+
* and a top-level `{ slug, url, onRedeploy }` otherwise — including for cloned
|
|
22
|
+
* one-page sites, so a legacy top-level link is not proof of a single-file
|
|
23
|
+
* deploy. `readLink` accepts both and always returns the current shape;
|
|
24
|
+
* `writeLink` only writes the current shape.
|
|
25
|
+
*/
|
|
26
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
|
|
29
|
+
export const LINK_FILE = ".htmlhost";
|
|
30
|
+
|
|
31
|
+
/** Normalize parsed link JSON (current or legacy) to the current shape, or null. */
|
|
32
|
+
export function normalizeLink(raw) {
|
|
33
|
+
if (!raw || typeof raw !== "object") return null;
|
|
34
|
+
let site = null;
|
|
35
|
+
if (raw.site?.slug) {
|
|
36
|
+
site = { ...raw.site, mode: raw.site.mode === "file" ? "file" : "directory" };
|
|
37
|
+
} else if (raw.multipage?.slug) {
|
|
38
|
+
site = { ...raw.multipage, mode: "directory" };
|
|
39
|
+
} else if (raw.slug) {
|
|
40
|
+
site = {
|
|
41
|
+
slug: raw.slug,
|
|
42
|
+
url: raw.url || `${raw.slug}.htmlhost.co`,
|
|
43
|
+
mode: "file",
|
|
44
|
+
...(raw.onRedeploy ? { onRedeploy: raw.onRedeploy } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const link = {};
|
|
48
|
+
if (site) link.site = site;
|
|
49
|
+
if (raw.sync) link.sync = raw.sync;
|
|
50
|
+
if (raw.assets) link.assets = raw.assets;
|
|
51
|
+
return link;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Read and normalize `dir/.htmlhost`, or null when absent/unreadable. */
|
|
55
|
+
export function readLink(dir) {
|
|
56
|
+
const linkPath = join(dir, LINK_FILE);
|
|
57
|
+
if (!existsSync(linkPath)) return null;
|
|
58
|
+
try {
|
|
59
|
+
return normalizeLink(JSON.parse(readFileSync(linkPath, "utf8")));
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Write `dir/.htmlhost` in the current shape. */
|
|
66
|
+
export function writeLink(dir, link) {
|
|
67
|
+
const out = {};
|
|
68
|
+
if (link.site) out.site = link.site;
|
|
69
|
+
if (link.sync) out.sync = link.sync;
|
|
70
|
+
if (link.assets) out.assets = link.assets;
|
|
71
|
+
writeFileSync(join(dir, LINK_FILE), JSON.stringify(out, null, 2) + "\n", "utf8");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Site metadata for a directory-mode link. */
|
|
75
|
+
export function directorySite(slug, pageCount) {
|
|
76
|
+
return { slug, url: `${slug}.htmlhost.co`, mode: "directory", pageCount };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The slug a directory deploy should update. Any linked site qualifies: a
|
|
81
|
+
* directory deploy of a folder linked to a single-file site promotes it to a
|
|
82
|
+
* directory site (after the usual overwrite confirmation), matching how the
|
|
83
|
+
* platform grows a one-page project into a multi-file one.
|
|
84
|
+
*/
|
|
85
|
+
export function directoryDeploySlug(link) {
|
|
86
|
+
return link?.site?.slug || null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The slug a single-file deploy may offer to overwrite. Only file-mode links:
|
|
91
|
+
* deploying one file over a directory site would replace all its pages.
|
|
92
|
+
*/
|
|
93
|
+
export function fileDeploySlug(link) {
|
|
94
|
+
return link?.site?.mode === "file" ? link.site.slug : null;
|
|
95
|
+
}
|