htmlhost-cli 1.3.0 → 1.4.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 +18 -4
- package/package.json +1 -1
- package/src/assets.mjs +38 -9
- package/src/cli.mjs +5 -3
- package/src/commands/deploy.mjs +180 -22
package/README.md
CHANGED
|
@@ -31,18 +31,32 @@ htmlhost upload logo.png
|
|
|
31
31
|
### `htmlhost login`
|
|
32
32
|
Authenticate with an API token. Generate one at [htmlhost.co/settings#keys](https://htmlhost.co/settings#keys).
|
|
33
33
|
|
|
34
|
-
### `htmlhost deploy
|
|
35
|
-
Deploy an HTML file and get
|
|
34
|
+
### `htmlhost deploy [file|dir] [options]`
|
|
35
|
+
Deploy an HTML file — or an entire directory of HTML files — and get live URLs.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Single file
|
|
39
|
+
htmlhost deploy index.html
|
|
40
|
+
|
|
41
|
+
# All .html files in the current directory
|
|
42
|
+
htmlhost deploy .
|
|
43
|
+
```
|
|
36
44
|
|
|
37
45
|
Local assets (scripts, CSS, images, fonts) referenced in the HTML are
|
|
38
46
|
automatically uploaded to your media library and the references are rewritten
|
|
39
47
|
to point at the hosted URLs. Files over 10 MB are skipped.
|
|
40
48
|
|
|
49
|
+
**Directory deploy:** When you pass a directory (or `.`), every `*.html` file is
|
|
50
|
+
deployed as a separate linked site. The mapping is saved to `.htmlhost` so
|
|
51
|
+
subsequent `htmlhost deploy .` updates all of them in place. Shared assets
|
|
52
|
+
(e.g. a `styles.css` referenced by multiple pages) are uploaded once and reused.
|
|
53
|
+
|
|
41
54
|
| Option | Description |
|
|
42
55
|
|---|---|
|
|
43
56
|
| `--ttl <value>` | Set expiry: `1d`, `7d`, `30d`, `never` |
|
|
44
|
-
| `--slug <slug>` | Re-deploy to an existing site |
|
|
45
|
-
| `--title <title>` | Set the site title |
|
|
57
|
+
| `--slug <slug>` | Re-deploy to an existing site (single file only) |
|
|
58
|
+
| `--title <title>` | Set the site title (single file only) |
|
|
59
|
+
| `--new` | Force new sites (ignore .htmlhost link) |
|
|
46
60
|
| `--no-assets` | Skip automatic asset uploading |
|
|
47
61
|
|
|
48
62
|
### `htmlhost list`
|
package/package.json
CHANGED
package/src/assets.mjs
CHANGED
|
@@ -142,11 +142,12 @@ function extractCssUrls(css, add) {
|
|
|
142
142
|
/**
|
|
143
143
|
* Main entry: process HTML, upload local assets, return rewritten HTML.
|
|
144
144
|
*
|
|
145
|
-
* @param {string} html
|
|
146
|
-
* @param {string} baseDir
|
|
147
|
-
* @
|
|
145
|
+
* @param {string} html The original HTML content
|
|
146
|
+
* @param {string} baseDir Directory the HTML file lives in (for resolving relative paths)
|
|
147
|
+
* @param {Map<string, string>} [sharedCache] Optional cache of filePath → hostedUrl for batch deploys
|
|
148
|
+
* @returns {Promise<string>} Rewritten HTML with hosted asset URLs
|
|
148
149
|
*/
|
|
149
|
-
export async function processAssets(html, baseDir) {
|
|
150
|
+
export async function processAssets(html, baseDir, sharedCache) {
|
|
150
151
|
const entries = findAssetRefs(html);
|
|
151
152
|
|
|
152
153
|
if (entries.length === 0) return html;
|
|
@@ -157,13 +158,37 @@ export async function processAssets(html, baseDir) {
|
|
|
157
158
|
/** @type {Map<string, string>} ref → hosted URL */
|
|
158
159
|
const urlMap = new Map();
|
|
159
160
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
// Count how many actually need uploading (not already cached)
|
|
162
|
+
let needsUpload = 0;
|
|
163
|
+
for (const { ref } of entries) {
|
|
164
|
+
if (urlMap.has(ref)) continue;
|
|
165
|
+
const asset = resolveAsset(baseDir, ref);
|
|
166
|
+
if (asset.skip) continue;
|
|
167
|
+
if (sharedCache && sharedCache.has(asset.filePath)) {
|
|
168
|
+
urlMap.set(ref, sharedCache.get(asset.filePath));
|
|
169
|
+
} else {
|
|
170
|
+
needsUpload++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const cachedCount = urlMap.size;
|
|
175
|
+
const totalLocal = cachedCount + needsUpload;
|
|
176
|
+
|
|
177
|
+
if (totalLocal > 0) {
|
|
178
|
+
console.log("");
|
|
179
|
+
const label = totalLocal === 1 ? "local asset" : "local assets";
|
|
180
|
+
if (cachedCount > 0 && needsUpload > 0) {
|
|
181
|
+
ok(`Found ${cyan(String(totalLocal))} ${label} (${cachedCount} cached, ${needsUpload} to upload)`);
|
|
182
|
+
} else if (cachedCount > 0) {
|
|
183
|
+
ok(`Found ${cyan(String(totalLocal))} ${label} (all cached)`);
|
|
184
|
+
} else {
|
|
185
|
+
ok(`Found ${cyan(String(totalLocal))} ${label} to upload`);
|
|
186
|
+
}
|
|
187
|
+
console.log("");
|
|
188
|
+
}
|
|
164
189
|
|
|
165
190
|
for (const { ref } of entries) {
|
|
166
|
-
if (urlMap.has(ref)) continue; // already
|
|
191
|
+
if (urlMap.has(ref)) continue; // already resolved (from cache or previous ref)
|
|
167
192
|
|
|
168
193
|
const asset = resolveAsset(baseDir, ref);
|
|
169
194
|
if (asset.skip) {
|
|
@@ -178,6 +203,10 @@ export async function processAssets(html, baseDir) {
|
|
|
178
203
|
const data = await uploadFile("/api/media", asset.filePath, fileName, mime);
|
|
179
204
|
const hostedUrl = `${api}${data.url}`;
|
|
180
205
|
urlMap.set(ref, hostedUrl);
|
|
206
|
+
// Store in shared cache by absolute path for cross-file dedup
|
|
207
|
+
if (sharedCache) {
|
|
208
|
+
sharedCache.set(asset.filePath, hostedUrl);
|
|
209
|
+
}
|
|
181
210
|
ok(`${cyan(ref)} ${dim(`(${formatBytes(asset.size)})`)} → ${hostedUrl}`);
|
|
182
211
|
} catch (e) {
|
|
183
212
|
warn(`Failed to upload ${cyan(ref)}: ${e.message}`);
|
package/src/cli.mjs
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
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.4.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 deploy")} [file] [
|
|
13
|
+
${cyan("htmlhost deploy")} [file|dir] [opts] Deploy file or directory
|
|
14
14
|
${cyan("htmlhost list")} List your sites
|
|
15
15
|
${cyan("htmlhost delete")} <slug> Delete a site
|
|
16
16
|
${cyan("htmlhost upload")} <file|dir> Upload media assets
|
|
@@ -31,14 +31,16 @@ const HELP = `
|
|
|
31
31
|
${bold("How deploy works:")}
|
|
32
32
|
1st deploy: creates a site, saves slug to ${dim(".htmlhost")}
|
|
33
33
|
2nd deploy: reads ${dim(".htmlhost")}, updates the same site
|
|
34
|
+
Directory: deploys each .html as a separate linked site
|
|
34
35
|
Local scripts, CSS, and images are auto-uploaded
|
|
35
36
|
and rewritten to hosted URLs (files >10 MB are skipped)
|
|
36
37
|
|
|
37
38
|
${bold("Examples:")}
|
|
38
39
|
${dim("$")} htmlhost deploy ${dim("# deploys ./index.html")}
|
|
39
40
|
${dim("$")} htmlhost deploy page.html ${dim("# deploys a specific file")}
|
|
41
|
+
${dim("$")} htmlhost deploy . ${dim("# deploys all *.html in dir")}
|
|
40
42
|
${dim("$")} htmlhost deploy --ttl 30d ${dim("# deploy with 30-day TTL")}
|
|
41
|
-
${dim("$")} htmlhost deploy --new ${dim("# force
|
|
43
|
+
${dim("$")} htmlhost deploy --new ${dim("# force new sites")}
|
|
42
44
|
${dim("$")} htmlhost upload logo.png
|
|
43
45
|
${dim("$")} htmlhost delete old-project --force
|
|
44
46
|
|
package/src/commands/deploy.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, statSync, existsSync } from "node:fs";
|
|
1
|
+
import { readFileSync, writeFileSync, statSync, existsSync, readdirSync } from "node:fs";
|
|
2
2
|
import { basename, resolve, dirname, join } from "node:path";
|
|
3
3
|
import { createInterface } from "node:readline";
|
|
4
4
|
import { post } from "../api.mjs";
|
|
5
|
-
import { ok, err, info, cyan, dim, bold, yellow, formatBytes } from "../ui.mjs";
|
|
5
|
+
import { ok, err, info, cyan, dim, bold, yellow, green, formatBytes } from "../ui.mjs";
|
|
6
6
|
import { processAssets } from "../assets.mjs";
|
|
7
7
|
|
|
8
8
|
const LINK_FILE = ".htmlhost";
|
|
@@ -50,15 +50,34 @@ function promptChoice(question, options) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* htmlhost deploy [file] [--ttl 7d] [--slug existing-slug] [--title "My Site"] [--new]
|
|
53
|
+
* htmlhost deploy [file|dir] [--ttl 7d] [--slug existing-slug] [--title "My Site"] [--new] [--no-assets]
|
|
54
54
|
*
|
|
55
55
|
* - Defaults to index.html in the current directory
|
|
56
|
-
* -
|
|
57
|
-
* -
|
|
56
|
+
* - If a directory (or ".") is given, deploys all *.html files as separate linked sites
|
|
57
|
+
* - Remembers the site(s) via .htmlhost file (auto re-deploy)
|
|
58
|
+
* - Use --new to force fresh deploys
|
|
58
59
|
*/
|
|
59
60
|
export async function deploy(args) {
|
|
60
61
|
let file = args.find((a) => !a.startsWith("--"));
|
|
61
62
|
|
|
63
|
+
const ttl = getFlag(args, "--ttl");
|
|
64
|
+
const title = getFlag(args, "--title");
|
|
65
|
+
const forceNew = args.includes("--new");
|
|
66
|
+
const skipAssets = args.includes("--no-assets");
|
|
67
|
+
|
|
68
|
+
// Check if the target is a directory
|
|
69
|
+
if (file) {
|
|
70
|
+
const filePath = resolve(file);
|
|
71
|
+
try {
|
|
72
|
+
const stat = statSync(filePath);
|
|
73
|
+
if (stat.isDirectory()) {
|
|
74
|
+
return deployDirectory(filePath, { ttl, forceNew, skipAssets });
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// Will be caught below in single-file flow
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
// Default to index.html in the current directory
|
|
63
82
|
if (!file) {
|
|
64
83
|
const defaultFile = resolve("index.html");
|
|
@@ -67,35 +86,35 @@ export async function deploy(args) {
|
|
|
67
86
|
info(`No file specified, using ${cyan("index.html")}`);
|
|
68
87
|
} else {
|
|
69
88
|
err("No file specified and no index.html found in the current directory.");
|
|
70
|
-
console.log(` ${dim("Usage: htmlhost deploy [file.html] [--ttl 7d]")}`);
|
|
89
|
+
console.log(` ${dim("Usage: htmlhost deploy [file.html|dir] [--ttl 7d]")}`);
|
|
71
90
|
process.exit(1);
|
|
72
91
|
}
|
|
73
92
|
}
|
|
74
93
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
94
|
+
// Single-file deploy
|
|
95
|
+
await deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug: getFlag(args, "--slug") });
|
|
96
|
+
}
|
|
78
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Deploy a single HTML file.
|
|
100
|
+
*/
|
|
101
|
+
async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug }) {
|
|
79
102
|
const filePath = resolve(file);
|
|
80
103
|
const projectDir = dirname(filePath);
|
|
81
104
|
|
|
82
|
-
// Resolve slug: explicit --slug > .htmlhost link > new deploy
|
|
83
|
-
let slug = getFlag(args, "--slug");
|
|
84
105
|
let isLinked = false;
|
|
85
106
|
|
|
86
107
|
if (!slug && !forceNew) {
|
|
87
108
|
const link = readLink(projectDir);
|
|
88
|
-
|
|
89
|
-
|
|
109
|
+
// Only use single-file link format (has slug at top level, not sites map)
|
|
110
|
+
if (link?.slug && !link.sites) {
|
|
90
111
|
if (link.onRedeploy === "overwrite") {
|
|
91
112
|
slug = link.slug;
|
|
92
113
|
isLinked = true;
|
|
93
114
|
info(`Linked to ${cyan(slug)} ${dim("(auto-overwrite)")}`);
|
|
94
115
|
} else if (link.onRedeploy === "new") {
|
|
95
116
|
info(`Creating new site ${dim("(auto-new)")}`);
|
|
96
|
-
// slug stays null → new deploy
|
|
97
117
|
} else {
|
|
98
|
-
// Ask the user
|
|
99
118
|
const choice = await promptChoice(
|
|
100
119
|
`This project is linked to ${cyan(bold(link.slug + ".htmlhost.co"))}`,
|
|
101
120
|
[
|
|
@@ -108,24 +127,24 @@ export async function deploy(args) {
|
|
|
108
127
|
);
|
|
109
128
|
|
|
110
129
|
switch (choice) {
|
|
111
|
-
case 0:
|
|
130
|
+
case 0:
|
|
112
131
|
slug = link.slug;
|
|
113
132
|
isLinked = true;
|
|
114
133
|
break;
|
|
115
|
-
case 1:
|
|
134
|
+
case 1:
|
|
116
135
|
break;
|
|
117
|
-
case 2:
|
|
136
|
+
case 2:
|
|
118
137
|
case -1:
|
|
119
138
|
console.log(" Cancelled.");
|
|
120
139
|
process.exit(0);
|
|
121
140
|
break;
|
|
122
|
-
case 3:
|
|
141
|
+
case 3:
|
|
123
142
|
slug = link.slug;
|
|
124
143
|
isLinked = true;
|
|
125
144
|
writeLink(projectDir, { ...link, onRedeploy: "overwrite" });
|
|
126
145
|
info(`Saved preference: ${dim("always overwrite")}`);
|
|
127
146
|
break;
|
|
128
|
-
case 4:
|
|
147
|
+
case 4:
|
|
129
148
|
writeLink(projectDir, { ...link, onRedeploy: "new" });
|
|
130
149
|
info(`Saved preference: ${dim("always create new")}`);
|
|
131
150
|
break;
|
|
@@ -144,7 +163,7 @@ export async function deploy(args) {
|
|
|
144
163
|
}
|
|
145
164
|
|
|
146
165
|
if (stat.isDirectory()) {
|
|
147
|
-
err("
|
|
166
|
+
err("Expected an HTML file, got a directory.");
|
|
148
167
|
process.exit(1);
|
|
149
168
|
}
|
|
150
169
|
|
|
@@ -154,7 +173,6 @@ export async function deploy(args) {
|
|
|
154
173
|
info(`Reading ${cyan(name)} ${dim(`(${formatBytes(stat.size)})`)}`);
|
|
155
174
|
|
|
156
175
|
// Upload local assets (scripts, css, images) and rewrite references
|
|
157
|
-
const skipAssets = args.includes("--no-assets");
|
|
158
176
|
if (!skipAssets) {
|
|
159
177
|
html = await processAssets(html, projectDir);
|
|
160
178
|
}
|
|
@@ -193,6 +211,146 @@ export async function deploy(args) {
|
|
|
193
211
|
console.log("");
|
|
194
212
|
}
|
|
195
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Deploy all *.html files in a directory as separate linked sites.
|
|
216
|
+
*/
|
|
217
|
+
async function deployDirectory(dirPath, { ttl, forceNew, skipAssets }) {
|
|
218
|
+
// Find all .html files in the directory (non-recursive, skip dotfiles)
|
|
219
|
+
const htmlFiles = readdirSync(dirPath)
|
|
220
|
+
.filter((f) => f.endsWith(".html") && !f.startsWith("."))
|
|
221
|
+
.sort((a, b) => {
|
|
222
|
+
// index.html first, then alphabetical
|
|
223
|
+
if (a === "index.html") return -1;
|
|
224
|
+
if (b === "index.html") return 1;
|
|
225
|
+
return a.localeCompare(b);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (htmlFiles.length === 0) {
|
|
229
|
+
err("No .html files found in this directory.");
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Read existing link data
|
|
234
|
+
const link = readLink(dirPath);
|
|
235
|
+
const existingSites = link?.sites || {};
|
|
236
|
+
const onRedeploy = link?.onRedeploy || null;
|
|
237
|
+
|
|
238
|
+
const isRelink = Object.keys(existingSites).length > 0 && !forceNew;
|
|
239
|
+
|
|
240
|
+
console.log("");
|
|
241
|
+
info(`Found ${cyan(bold(String(htmlFiles.length)))} HTML file${htmlFiles.length > 1 ? "s" : ""} in ${cyan(basename(dirPath) || ".")}`);
|
|
242
|
+
|
|
243
|
+
// Show what we're about to do
|
|
244
|
+
for (const f of htmlFiles) {
|
|
245
|
+
const linked = existingSites[f];
|
|
246
|
+
if (linked && !forceNew) {
|
|
247
|
+
console.log(` ${dim("↻")} ${f} → ${dim(linked.slug + ".htmlhost.co")}`);
|
|
248
|
+
} else {
|
|
249
|
+
console.log(` ${dim("+")} ${f} → ${dim("new site")}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
console.log("");
|
|
253
|
+
|
|
254
|
+
// If relinking, ask for confirmation (unless auto-overwrite)
|
|
255
|
+
if (isRelink && onRedeploy !== "overwrite") {
|
|
256
|
+
const linkedCount = htmlFiles.filter((f) => existingSites[f]).length;
|
|
257
|
+
const newCount = htmlFiles.length - linkedCount;
|
|
258
|
+
|
|
259
|
+
let desc = `${linkedCount} site${linkedCount > 1 ? "s" : ""} will be overwritten`;
|
|
260
|
+
if (newCount > 0) desc += `, ${newCount} new`;
|
|
261
|
+
|
|
262
|
+
const choice = await promptChoice(
|
|
263
|
+
`${desc}. Continue?`,
|
|
264
|
+
[
|
|
265
|
+
"Yes, deploy all",
|
|
266
|
+
"Cancel",
|
|
267
|
+
`Always overwrite ${dim("(remember for this project)")}`,
|
|
268
|
+
]
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
switch (choice) {
|
|
272
|
+
case 0:
|
|
273
|
+
break;
|
|
274
|
+
case 1:
|
|
275
|
+
case -1:
|
|
276
|
+
console.log(" Cancelled.");
|
|
277
|
+
process.exit(0);
|
|
278
|
+
break;
|
|
279
|
+
case 2:
|
|
280
|
+
// Save preference so it auto-deploys next time
|
|
281
|
+
writeLink(dirPath, { sites: existingSites, onRedeploy: "overwrite" });
|
|
282
|
+
info(`Saved preference: ${dim("always overwrite")}`);
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Deploy each file
|
|
288
|
+
const results = {};
|
|
289
|
+
let successCount = 0;
|
|
290
|
+
|
|
291
|
+
// Shared asset cache across all files — avoids re-uploading the same CSS/JS/image
|
|
292
|
+
/** @type {Map<string, string>} absolute file path → hosted URL */
|
|
293
|
+
const assetCache = new Map();
|
|
294
|
+
|
|
295
|
+
for (const fileName of htmlFiles) {
|
|
296
|
+
const filePath = join(dirPath, fileName);
|
|
297
|
+
const stat = statSync(filePath);
|
|
298
|
+
|
|
299
|
+
console.log("");
|
|
300
|
+
info(`${bold(fileName)} ${dim(`(${formatBytes(stat.size)})`)}`);
|
|
301
|
+
|
|
302
|
+
let html = readFileSync(filePath, "utf8");
|
|
303
|
+
|
|
304
|
+
// Upload local assets with shared cache
|
|
305
|
+
if (!skipAssets) {
|
|
306
|
+
html = await processAssets(html, dirPath, assetCache);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Determine slug
|
|
310
|
+
const slug = (!forceNew && existingSites[fileName]?.slug) || null;
|
|
311
|
+
|
|
312
|
+
info(slug ? `Re-deploying to ${cyan(slug)}…` : "Deploying new site…");
|
|
313
|
+
|
|
314
|
+
const body = { html };
|
|
315
|
+
if (ttl) body.ttl = ttl;
|
|
316
|
+
if (slug) body.slug = slug;
|
|
317
|
+
// No --title for batch; each site auto-titles from <title> tag
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
const data = await post("/api/sites", body);
|
|
321
|
+
results[fileName] = { slug: data.slug, url: data.url };
|
|
322
|
+
successCount++;
|
|
323
|
+
ok(`${cyan(fileName)} → ${cyan(`https://${data.url}`)}`);
|
|
324
|
+
} catch (e) {
|
|
325
|
+
err(`${fileName}: ${e.message}`);
|
|
326
|
+
// Keep the old link if we had one
|
|
327
|
+
if (existingSites[fileName]) {
|
|
328
|
+
results[fileName] = existingSites[fileName];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Save all links to .htmlhost
|
|
334
|
+
writeLink(dirPath, {
|
|
335
|
+
sites: { ...existingSites, ...results },
|
|
336
|
+
...(onRedeploy ? { onRedeploy } : {}),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Summary
|
|
340
|
+
console.log("");
|
|
341
|
+
console.log(` ${green("━".repeat(40))}`);
|
|
342
|
+
ok(`${bold(String(successCount))}/${htmlFiles.length} sites deployed`);
|
|
343
|
+
console.log("");
|
|
344
|
+
for (const [fileName, site] of Object.entries(results)) {
|
|
345
|
+
if (site.url) {
|
|
346
|
+
console.log(` ${dim(fileName)} → ${cyan(`https://${site.url}`)}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
console.log("");
|
|
350
|
+
console.log(` ${dim("Linked → .htmlhost")}`);
|
|
351
|
+
console.log("");
|
|
352
|
+
}
|
|
353
|
+
|
|
196
354
|
function getFlag(args, flag) {
|
|
197
355
|
const idx = args.indexOf(flag);
|
|
198
356
|
if (idx === -1 || idx >= args.length - 1) return null;
|