htmlhost-cli 1.8.0 → 2.0.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 +31 -30
- package/package.json +1 -1
- package/src/cli.mjs +11 -22
- package/src/commands/deploy.mjs +239 -275
- package/src/commands/upload.mjs +0 -68
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# htmlhost
|
|
2
2
|
|
|
3
|
-
Deploy HTML
|
|
3
|
+
Deploy HTML projects from the terminal. [htmlhost.co](https://htmlhost.co)
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -14,16 +14,16 @@ npm i -g htmlhost-cli
|
|
|
14
14
|
# Authenticate (get a token at htmlhost.co/settings#keys)
|
|
15
15
|
htmlhost login
|
|
16
16
|
|
|
17
|
-
# Deploy
|
|
18
|
-
htmlhost deploy
|
|
17
|
+
# Deploy the current directory
|
|
18
|
+
htmlhost deploy
|
|
19
19
|
# → https://bold-fern-x3k.htmlhost.co
|
|
20
20
|
|
|
21
|
-
# Re-deploy
|
|
22
|
-
htmlhost deploy
|
|
21
|
+
# Re-deploy (auto-detects from .htmlhost)
|
|
22
|
+
htmlhost deploy
|
|
23
|
+
# → updated https://bold-fern-x3k.htmlhost.co
|
|
23
24
|
|
|
24
|
-
#
|
|
25
|
-
htmlhost
|
|
26
|
-
# → https://htmlhost.co/m/a3f91c2b
|
|
25
|
+
# Deploy a single file
|
|
26
|
+
htmlhost deploy page.html
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
## Commands
|
|
@@ -32,30 +32,31 @@ htmlhost upload logo.png
|
|
|
32
32
|
Authenticate with an API token. Generate one at [htmlhost.co/settings#keys](https://htmlhost.co/settings#keys).
|
|
33
33
|
|
|
34
34
|
### `htmlhost deploy [file|dir] [options]`
|
|
35
|
-
Deploy an HTML
|
|
35
|
+
Deploy an HTML project and get a live URL.
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
#
|
|
39
|
-
htmlhost deploy
|
|
38
|
+
# Deploy current directory (default)
|
|
39
|
+
htmlhost deploy
|
|
40
40
|
|
|
41
|
-
#
|
|
42
|
-
htmlhost deploy
|
|
41
|
+
# Deploy a specific directory
|
|
42
|
+
htmlhost deploy ./my-project
|
|
43
43
|
|
|
44
|
-
#
|
|
45
|
-
htmlhost deploy .
|
|
44
|
+
# Deploy a single HTML file
|
|
45
|
+
htmlhost deploy page.html
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
**Directory deploy (default):** When you run `htmlhost deploy` with no arguments
|
|
49
|
+
(or pass a directory), the entire folder is deployed as a multi-page site. All
|
|
50
|
+
files — HTML, CSS, JS, images, fonts — are uploaded preserving their original
|
|
51
|
+
folder structure. The site is saved to `.htmlhost` so subsequent deploys update
|
|
52
|
+
the same site.
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
The following files are automatically excluded:
|
|
55
|
+
- `node_modules/`, `.git/`, `.DS_Store`, `.env*`
|
|
56
|
+
- `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`
|
|
57
|
+
- `.vscode/`, `.idea/`
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
multi-page site. File names map to URL paths:
|
|
59
|
+
File names map to URL paths:
|
|
59
60
|
|
|
60
61
|
```
|
|
61
62
|
index.html → /
|
|
@@ -67,14 +68,17 @@ blog/post-1.html → /blog/post-1
|
|
|
67
68
|
|
|
68
69
|
Add a `404.html` file and it will be served automatically for missing pages.
|
|
69
70
|
|
|
71
|
+
**Single-file deploy:** When you pass a specific file (`htmlhost deploy page.html`),
|
|
72
|
+
only that file is deployed. Local CSS, JS, and images referenced in the HTML are
|
|
73
|
+
automatically inlined or uploaded and rewritten to hosted URLs.
|
|
74
|
+
|
|
70
75
|
| Option | Description |
|
|
71
76
|
|---|---|
|
|
72
77
|
| `--ttl <value>` | Set expiry: `1d`, `7d`, `30d`, `never` |
|
|
73
78
|
| `--slug <slug>` | Re-deploy to an existing site |
|
|
74
79
|
| `--title <title>` | Set the site title |
|
|
75
|
-
| `--new` | Force new
|
|
76
|
-
| `--no-assets` | Skip automatic asset
|
|
77
|
-
| `--pages` | Deploy directory as one multi-page site |
|
|
80
|
+
| `--new` | Force new site (ignore .htmlhost link) |
|
|
81
|
+
| `--no-assets` | Skip automatic asset processing (single-file only) |
|
|
78
82
|
| `--json` | Output result as JSON (for CI/CD) |
|
|
79
83
|
|
|
80
84
|
### `htmlhost list`
|
|
@@ -86,9 +90,6 @@ Open a deployed site in the default browser.
|
|
|
86
90
|
### `htmlhost delete <slug>`
|
|
87
91
|
Delete a site. Use `--force` to skip confirmation.
|
|
88
92
|
|
|
89
|
-
### `htmlhost upload <file|dir>`
|
|
90
|
-
Upload media assets to your library. Returns URLs you can use in your HTML.
|
|
91
|
-
|
|
92
93
|
### `htmlhost whoami`
|
|
93
94
|
Show current authenticated user and plan.
|
|
94
95
|
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { bold, dim, cyan, err } from "./ui.mjs";
|
|
5
5
|
import { ApiError } from "./api.mjs";
|
|
6
6
|
|
|
7
|
-
const VERSION = "
|
|
7
|
+
const VERSION = "2.0.0";
|
|
8
8
|
|
|
9
9
|
const HELP = `
|
|
10
10
|
${bold("htmlhost")} ${dim(`v${VERSION}`)} — deploy HTML from the terminal
|
|
@@ -13,7 +13,6 @@ const HELP = `
|
|
|
13
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
|
-
${cyan("htmlhost upload")} <file|dir> Upload media assets
|
|
17
16
|
${cyan("htmlhost login")} Authenticate with an API token
|
|
18
17
|
${cyan("htmlhost whoami")} Show current user
|
|
19
18
|
${cyan("htmlhost logout")} Remove saved token
|
|
@@ -24,29 +23,24 @@ const HELP = `
|
|
|
24
23
|
--slug <slug> Re-deploy to a specific site
|
|
25
24
|
--title <title> Set the site title
|
|
26
25
|
--new Force a new site (ignore .htmlhost link)
|
|
27
|
-
--no-assets Skip auto-uploading local assets
|
|
28
|
-
--pages Deploy directory as one multi-page site
|
|
26
|
+
--no-assets Skip auto-uploading local assets (single-file only)
|
|
29
27
|
|
|
30
28
|
${bold("Delete options:")}
|
|
31
29
|
--force, -f Skip confirmation prompt
|
|
32
30
|
|
|
33
31
|
${bold("How deploy works:")}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Local scripts, CSS, and images are auto-uploaded
|
|
39
|
-
and rewritten to hosted URLs (files >10 MB are skipped)
|
|
32
|
+
No args or dir: deploys entire folder as a multi-page site
|
|
33
|
+
Single file: deploys just that file (assets auto-inlined)
|
|
34
|
+
Linked: reads ${dim(".htmlhost")}, updates the same site
|
|
35
|
+
Local CSS, JS, images, and fonts are auto-uploaded
|
|
40
36
|
|
|
41
37
|
${bold("Examples:")}
|
|
42
|
-
${dim("$")} htmlhost deploy ${dim("# deploys
|
|
43
|
-
${dim("$")} htmlhost deploy
|
|
44
|
-
${dim("$")} htmlhost deploy .
|
|
45
|
-
${dim("$")} htmlhost deploy . --pages ${dim("# multi-page site from dir")}
|
|
38
|
+
${dim("$")} htmlhost deploy ${dim("# deploys cwd as a site")}
|
|
39
|
+
${dim("$")} htmlhost deploy ./my-project ${dim("# deploys a specific folder")}
|
|
40
|
+
${dim("$")} htmlhost deploy page.html ${dim("# deploys a single file")}
|
|
46
41
|
${dim("$")} htmlhost deploy --ttl 30d ${dim("# deploy with 30-day TTL")}
|
|
47
|
-
${dim("$")} htmlhost deploy --new ${dim("# force new
|
|
48
|
-
${dim("$")} htmlhost deploy
|
|
49
|
-
${dim("$")} htmlhost upload logo.png
|
|
42
|
+
${dim("$")} htmlhost deploy --new ${dim("# force new site")}
|
|
43
|
+
${dim("$")} htmlhost deploy --json ${dim("# JSON output for CI/CD")}
|
|
50
44
|
${dim("$")} htmlhost delete old-project --force
|
|
51
45
|
|
|
52
46
|
${dim(`Docs: https://htmlhost.co/docs`)}
|
|
@@ -108,11 +102,6 @@ export async function run(argv) {
|
|
|
108
102
|
await deleteSite(args);
|
|
109
103
|
break;
|
|
110
104
|
}
|
|
111
|
-
case "upload": {
|
|
112
|
-
const { upload } = await import("./commands/upload.mjs");
|
|
113
|
-
await upload(args);
|
|
114
|
-
break;
|
|
115
|
-
}
|
|
116
105
|
case "open": {
|
|
117
106
|
const { open } = await import("./commands/open.mjs");
|
|
118
107
|
await open(args);
|
package/src/commands/deploy.mjs
CHANGED
|
@@ -1,12 +1,50 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, statSync, existsSync, readdirSync } from "node:fs";
|
|
2
|
-
import { basename, resolve, dirname, join } from "node:path";
|
|
2
|
+
import { basename, resolve, dirname, join, extname, relative } from "node:path";
|
|
3
3
|
import { createInterface } from "node:readline";
|
|
4
4
|
import { post } from "../api.mjs";
|
|
5
5
|
import { ok, err, info, cyan, dim, bold, yellow, green, formatBytes } from "../ui.mjs";
|
|
6
6
|
import { processAssets } from "../assets.mjs";
|
|
7
|
+
import { uploadFile } from "../api.mjs";
|
|
8
|
+
import { getApi } from "../config.mjs";
|
|
9
|
+
import { mimeFromExt } from "../ui.mjs";
|
|
7
10
|
|
|
8
11
|
const LINK_FILE = ".htmlhost";
|
|
9
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Default ignore patterns for directory deploys.
|
|
15
|
+
* Matches directory names and file names/patterns.
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_IGNORE = [
|
|
18
|
+
"node_modules",
|
|
19
|
+
".git",
|
|
20
|
+
".DS_Store",
|
|
21
|
+
".htmlhost",
|
|
22
|
+
".env",
|
|
23
|
+
".env.local",
|
|
24
|
+
".env.production",
|
|
25
|
+
".env.development",
|
|
26
|
+
".vscode",
|
|
27
|
+
".idea",
|
|
28
|
+
"Thumbs.db",
|
|
29
|
+
".gitignore",
|
|
30
|
+
".npmrc",
|
|
31
|
+
"package-lock.json",
|
|
32
|
+
"yarn.lock",
|
|
33
|
+
"pnpm-lock.yaml",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if a file/directory name should be ignored.
|
|
38
|
+
*/
|
|
39
|
+
function shouldIgnore(name) {
|
|
40
|
+
if (name.startsWith(".")) {
|
|
41
|
+
// Allow dotfiles that aren't in the ignore list explicitly
|
|
42
|
+
// but block common ones
|
|
43
|
+
if (DEFAULT_IGNORE.includes(name)) return true;
|
|
44
|
+
}
|
|
45
|
+
return DEFAULT_IGNORE.includes(name);
|
|
46
|
+
}
|
|
47
|
+
|
|
10
48
|
/**
|
|
11
49
|
* Read the project link from .htmlhost
|
|
12
50
|
*/
|
|
@@ -28,6 +66,20 @@ function writeLink(dir, data) {
|
|
|
28
66
|
writeFileSync(linkPath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
29
67
|
}
|
|
30
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Simple y/N confirmation prompt.
|
|
71
|
+
*/
|
|
72
|
+
function promptConfirm(question) {
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
75
|
+
rl.question(` ${question} ${dim("(Y/n)")} `, (answer) => {
|
|
76
|
+
rl.close();
|
|
77
|
+
const a = answer.trim().toLowerCase();
|
|
78
|
+
resolve(a === "" || a === "y" || a === "yes");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
31
83
|
/**
|
|
32
84
|
* Interactive menu prompt — returns the index of the selected option.
|
|
33
85
|
*/
|
|
@@ -50,64 +102,45 @@ function promptChoice(question, options) {
|
|
|
50
102
|
}
|
|
51
103
|
|
|
52
104
|
/**
|
|
53
|
-
* htmlhost deploy [file|dir] [--ttl 7d] [--slug existing-slug] [--title "My Site"] [--new] [--no-assets]
|
|
105
|
+
* htmlhost deploy [file|dir] [--ttl 7d] [--slug existing-slug] [--title "My Site"] [--new] [--no-assets]
|
|
54
106
|
*
|
|
55
|
-
* -
|
|
56
|
-
* -
|
|
57
|
-
* -
|
|
58
|
-
* - Remembers the site
|
|
107
|
+
* - No args or "." → deploys entire cwd as a multi-page site (with confirmation)
|
|
108
|
+
* - A directory → deploys that directory as a multi-page site
|
|
109
|
+
* - A single file → deploys just that file (with asset inlining)
|
|
110
|
+
* - Remembers the site via .htmlhost file (auto re-deploy)
|
|
59
111
|
* - Use --new to force fresh deploys
|
|
60
112
|
*/
|
|
61
113
|
export async function deploy(args, { json = false } = {}) {
|
|
62
|
-
let
|
|
114
|
+
let target = args.find((a) => !a.startsWith("--"));
|
|
63
115
|
|
|
64
116
|
const ttl = getFlag(args, "--ttl");
|
|
65
117
|
const title = getFlag(args, "--title");
|
|
66
118
|
const slug = getFlag(args, "--slug");
|
|
67
119
|
const forceNew = args.includes("--new");
|
|
68
120
|
const skipAssets = args.includes("--no-assets");
|
|
69
|
-
const multiPage = args.includes("--pages");
|
|
70
|
-
|
|
71
|
-
// Check if the target is a directory
|
|
72
|
-
if (file) {
|
|
73
|
-
const filePath = resolve(file);
|
|
74
|
-
try {
|
|
75
|
-
const stat = statSync(filePath);
|
|
76
|
-
if (stat.isDirectory()) {
|
|
77
|
-
if (multiPage) {
|
|
78
|
-
return deployMultiPage(filePath, { ttl, title, slug, forceNew, skipAssets, json });
|
|
79
|
-
}
|
|
80
|
-
return deployDirectory(filePath, { ttl, forceNew, skipAssets });
|
|
81
|
-
}
|
|
82
|
-
} catch {
|
|
83
|
-
// Will be caught below in single-file flow
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
121
|
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
122
|
+
// Resolve target
|
|
123
|
+
const targetPath = resolve(target || ".");
|
|
124
|
+
|
|
125
|
+
let stat;
|
|
126
|
+
try {
|
|
127
|
+
stat = statSync(targetPath);
|
|
128
|
+
} catch {
|
|
129
|
+
err(`Not found: ${target || "."}`);
|
|
130
|
+
process.exit(1);
|
|
90
131
|
}
|
|
91
132
|
|
|
92
|
-
//
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
if (existsSync(defaultFile)) {
|
|
96
|
-
file = "index.html";
|
|
97
|
-
info(`No file specified, using ${cyan("index.html")}`);
|
|
98
|
-
} else {
|
|
99
|
-
err("No file specified and no index.html found in the current directory.");
|
|
100
|
-
console.log(` ${dim("Usage: htmlhost deploy [file.html|dir] [--ttl 7d]")}`);
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
133
|
+
// --- Directory deploy (default) ---
|
|
134
|
+
if (stat.isDirectory()) {
|
|
135
|
+
return deployDirectory(targetPath, { ttl, title, slug, forceNew, json });
|
|
103
136
|
}
|
|
104
137
|
|
|
105
|
-
// Single-file deploy
|
|
106
|
-
await deploySingleFile(
|
|
138
|
+
// --- Single-file deploy ---
|
|
139
|
+
await deploySingleFile(target || "index.html", { ttl, title, forceNew, skipAssets, slug, json });
|
|
107
140
|
}
|
|
108
141
|
|
|
109
142
|
/**
|
|
110
|
-
* Deploy a single HTML file.
|
|
143
|
+
* Deploy a single HTML file (existing behavior preserved).
|
|
111
144
|
*/
|
|
112
145
|
async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug, json }) {
|
|
113
146
|
const filePath = resolve(file);
|
|
@@ -119,24 +152,8 @@ async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug,
|
|
|
119
152
|
if (!slug && !forceNew) {
|
|
120
153
|
const link = readLink(projectDir);
|
|
121
154
|
|
|
122
|
-
if (link?.
|
|
123
|
-
//
|
|
124
|
-
const siteCount = Object.keys(link.sites).length;
|
|
125
|
-
if (siteCount > 1) {
|
|
126
|
-
console.log("");
|
|
127
|
-
console.log(` ${yellow("⚠")} This project has ${bold(String(siteCount))} linked sites but you're deploying only ${cyan(fileName)}.`);
|
|
128
|
-
console.log(` ${dim(`To deploy all files, run: ${bold("htmlhost deploy .")}`)}`);
|
|
129
|
-
console.log("");
|
|
130
|
-
}
|
|
131
|
-
// Look up this file's entry in the sites map
|
|
132
|
-
const entry = link.sites[fileName];
|
|
133
|
-
if (entry?.slug) {
|
|
134
|
-
slug = entry.slug;
|
|
135
|
-
isLinked = true;
|
|
136
|
-
info(`Linked to ${cyan(slug)} ${dim(`(via .htmlhost sites map)`)}`);
|
|
137
|
-
}
|
|
138
|
-
} else if (link?.slug) {
|
|
139
|
-
// Single-file format (has slug at top level, no sites map)
|
|
155
|
+
if (link?.slug && !link?.multipage) {
|
|
156
|
+
// Single-file format
|
|
140
157
|
if (link.onRedeploy === "overwrite") {
|
|
141
158
|
slug = link.slug;
|
|
142
159
|
isLinked = true;
|
|
@@ -203,12 +220,10 @@ async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug,
|
|
|
203
220
|
|
|
204
221
|
// Upload local assets (scripts, css, images) and rewrite references
|
|
205
222
|
if (!skipAssets) {
|
|
206
|
-
// Load persisted asset cache from .htmlhost
|
|
207
223
|
const existingLink = readLink(projectDir);
|
|
208
224
|
const persistedCache = new Map(Object.entries(existingLink?.assets || {}));
|
|
209
225
|
const result = await processAssets(html, projectDir, undefined, persistedCache);
|
|
210
226
|
html = result.html;
|
|
211
|
-
// Persist the asset cache for next deploy
|
|
212
227
|
const linkData = readLink(projectDir) || {};
|
|
213
228
|
linkData.assets = Object.fromEntries(result.resolvedCache);
|
|
214
229
|
writeLink(projectDir, linkData);
|
|
@@ -223,21 +238,13 @@ async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug,
|
|
|
223
238
|
|
|
224
239
|
const data = await post("/api/sites", body);
|
|
225
240
|
|
|
226
|
-
// Save
|
|
241
|
+
// Save link
|
|
227
242
|
const existingLink = readLink(projectDir);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
existingLink
|
|
232
|
-
|
|
233
|
-
} else {
|
|
234
|
-
// Single-file format: write the flat slug/url (preserve onRedeploy preference)
|
|
235
|
-
writeLink(projectDir, {
|
|
236
|
-
slug: data.slug,
|
|
237
|
-
url: data.url,
|
|
238
|
-
...(existingLink?.onRedeploy ? { onRedeploy: existingLink.onRedeploy } : {}),
|
|
239
|
-
});
|
|
240
|
-
}
|
|
243
|
+
writeLink(projectDir, {
|
|
244
|
+
slug: data.slug,
|
|
245
|
+
url: data.url,
|
|
246
|
+
...(existingLink?.onRedeploy ? { onRedeploy: existingLink.onRedeploy } : {}),
|
|
247
|
+
});
|
|
241
248
|
|
|
242
249
|
if (json) {
|
|
243
250
|
console.log(JSON.stringify(data));
|
|
@@ -262,221 +269,169 @@ async function deploySingleFile(file, { ttl, title, forceNew, skipAssets, slug,
|
|
|
262
269
|
}
|
|
263
270
|
|
|
264
271
|
/**
|
|
265
|
-
* Deploy
|
|
272
|
+
* Deploy a directory as a multi-page site.
|
|
273
|
+
* All files are uploaded preserving their original folder structure.
|
|
274
|
+
* Non-HTML files (CSS, JS, images, fonts) are uploaded via the media endpoint
|
|
275
|
+
* and included as pages with their hosted URLs.
|
|
266
276
|
*/
|
|
267
|
-
async function deployDirectory(dirPath, { ttl, forceNew,
|
|
268
|
-
// Find all
|
|
269
|
-
const
|
|
270
|
-
.filter((f) => f.endsWith(".html") && !f.startsWith("."))
|
|
271
|
-
.sort((a, b) => {
|
|
272
|
-
// index.html first, then alphabetical
|
|
273
|
-
if (a === "index.html") return -1;
|
|
274
|
-
if (b === "index.html") return 1;
|
|
275
|
-
return a.localeCompare(b);
|
|
276
|
-
});
|
|
277
|
+
async function deployDirectory(dirPath, { ttl, title, slug: explicitSlug, forceNew, json }) {
|
|
278
|
+
// Find all files recursively
|
|
279
|
+
const allFiles = findAllFiles(dirPath, dirPath);
|
|
277
280
|
|
|
278
|
-
if (
|
|
279
|
-
err("No
|
|
281
|
+
if (allFiles.length === 0) {
|
|
282
|
+
err("No files found in this directory.");
|
|
280
283
|
process.exit(1);
|
|
281
284
|
}
|
|
282
285
|
|
|
286
|
+
// Check for index.html
|
|
287
|
+
const hasIndex = allFiles.some((f) => f.relativePath === "index.html");
|
|
288
|
+
if (!hasIndex) {
|
|
289
|
+
const htmlFiles = allFiles.filter((f) => f.relativePath.endsWith(".html"));
|
|
290
|
+
if (htmlFiles.length === 0) {
|
|
291
|
+
err("No index.html or HTML files found. Nothing to deploy.");
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
283
296
|
// Read existing link data
|
|
284
297
|
const link = readLink(dirPath);
|
|
285
|
-
const
|
|
286
|
-
const onRedeploy = link?.onRedeploy || null;
|
|
298
|
+
const existingSlug = explicitSlug || (!forceNew && link?.multipage?.slug) || null;
|
|
287
299
|
|
|
288
|
-
|
|
300
|
+
// Calculate total size
|
|
301
|
+
const totalSize = allFiles.reduce((sum, f) => sum + f.size, 0);
|
|
302
|
+
const htmlFiles = allFiles.filter((f) => f.relativePath.endsWith(".html"));
|
|
303
|
+
const assetFiles = allFiles.filter((f) => !f.relativePath.endsWith(".html"));
|
|
289
304
|
|
|
305
|
+
// Show deploy summary
|
|
290
306
|
console.log("");
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const linked = existingSites[f];
|
|
296
|
-
if (linked && !forceNew) {
|
|
297
|
-
console.log(` ${dim("↻")} ${f} → ${dim(linked.slug + ".htmlhost.co")}`);
|
|
298
|
-
} else {
|
|
299
|
-
console.log(` ${dim("+")} ${f} → ${dim("new site")}`);
|
|
300
|
-
}
|
|
307
|
+
if (existingSlug) {
|
|
308
|
+
info(`Re-deploying to ${cyan(bold(existingSlug + ".htmlhost.co"))}`);
|
|
309
|
+
} else {
|
|
310
|
+
info(`Deploying ${cyan(bold(basename(dirPath) || "."))} as a new site`);
|
|
301
311
|
}
|
|
302
312
|
console.log("");
|
|
303
313
|
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const newCount = htmlFiles.length - linkedCount;
|
|
308
|
-
|
|
309
|
-
let desc = `${linkedCount} site${linkedCount > 1 ? "s" : ""} will be overwritten`;
|
|
310
|
-
if (newCount > 0) desc += `, ${newCount} new`;
|
|
311
|
-
|
|
312
|
-
const choice = await promptChoice(
|
|
313
|
-
`${desc}. Continue?`,
|
|
314
|
-
[
|
|
315
|
-
"Yes, deploy all",
|
|
316
|
-
"Cancel",
|
|
317
|
-
`Always overwrite ${dim("(remember for this project)")}`,
|
|
318
|
-
]
|
|
319
|
-
);
|
|
320
|
-
|
|
321
|
-
switch (choice) {
|
|
322
|
-
case 0:
|
|
323
|
-
break;
|
|
324
|
-
case 1:
|
|
325
|
-
case -1:
|
|
326
|
-
console.log(" Cancelled.");
|
|
327
|
-
process.exit(0);
|
|
328
|
-
break;
|
|
329
|
-
case 2:
|
|
330
|
-
// Save preference so it auto-deploys next time
|
|
331
|
-
writeLink(dirPath, { sites: existingSites, onRedeploy: "overwrite" });
|
|
332
|
-
info(`Saved preference: ${dim("always overwrite")}`);
|
|
333
|
-
break;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Deploy each file
|
|
338
|
-
const results = {};
|
|
339
|
-
let successCount = 0;
|
|
340
|
-
|
|
341
|
-
// Shared asset cache across all files — avoids re-uploading the same CSS/JS/image
|
|
342
|
-
/** @type {Map<string, string>} absolute file path → hosted URL */
|
|
343
|
-
const assetCache = new Map();
|
|
344
|
-
// Load persisted asset cache from .htmlhost
|
|
345
|
-
const existingAssets = link?.assets || {};
|
|
346
|
-
const persistedCache = new Map(Object.entries(existingAssets));
|
|
314
|
+
// Show file tree
|
|
315
|
+
console.log(` ${bold("Files to deploy:")} ${dim(`(${allFiles.length} files, ${formatBytes(totalSize)})`)}`);
|
|
316
|
+
console.log("");
|
|
347
317
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
318
|
+
// Group by directory for cleaner display
|
|
319
|
+
const maxShow = 20;
|
|
320
|
+
const shown = allFiles.slice(0, maxShow);
|
|
321
|
+
for (const f of shown) {
|
|
322
|
+
const icon = f.relativePath.endsWith(".html") ? "📄" : "📦";
|
|
323
|
+
console.log(` ${dim(icon)} ${f.relativePath} ${dim(`(${formatBytes(f.size)})`)}`);
|
|
324
|
+
}
|
|
325
|
+
if (allFiles.length > maxShow) {
|
|
326
|
+
console.log(` ${dim(`… and ${allFiles.length - maxShow} more files`)}`);
|
|
327
|
+
}
|
|
328
|
+
console.log("");
|
|
351
329
|
|
|
330
|
+
// Summary line
|
|
331
|
+
if (htmlFiles.length > 0 && assetFiles.length > 0) {
|
|
332
|
+
console.log(` ${dim(`${htmlFiles.length} page${htmlFiles.length > 1 ? "s" : ""} · ${assetFiles.length} asset${assetFiles.length > 1 ? "s" : ""} · ${formatBytes(totalSize)} total`)}`);
|
|
352
333
|
console.log("");
|
|
353
|
-
info(`${bold(fileName)} ${dim(`(${formatBytes(stat.size)})`)}`);
|
|
354
|
-
|
|
355
|
-
let html = readFileSync(filePath, "utf8");
|
|
356
|
-
|
|
357
|
-
// Upload local assets with shared + persisted cache
|
|
358
|
-
if (!skipAssets) {
|
|
359
|
-
const result = await processAssets(html, dirPath, assetCache, persistedCache);
|
|
360
|
-
html = result.html;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Determine slug
|
|
364
|
-
const slug = (!forceNew && existingSites[fileName]?.slug) || null;
|
|
365
|
-
|
|
366
|
-
info(slug ? `Re-deploying to ${cyan(slug)}…` : "Deploying new site…");
|
|
367
|
-
|
|
368
|
-
const body = { html };
|
|
369
|
-
if (ttl) body.ttl = ttl;
|
|
370
|
-
if (slug) body.slug = slug;
|
|
371
|
-
// No --title for batch; each site auto-titles from <title> tag
|
|
372
|
-
|
|
373
|
-
try {
|
|
374
|
-
const data = await post("/api/sites", body);
|
|
375
|
-
results[fileName] = { slug: data.slug, url: data.url };
|
|
376
|
-
successCount++;
|
|
377
|
-
ok(`${cyan(fileName)} → ${cyan(`https://${data.url}`)}`);
|
|
378
|
-
} catch (e) {
|
|
379
|
-
err(`${fileName}: ${e.message}`);
|
|
380
|
-
// Keep the old link if we had one
|
|
381
|
-
if (existingSites[fileName]) {
|
|
382
|
-
results[fileName] = existingSites[fileName];
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
334
|
}
|
|
386
335
|
|
|
387
|
-
//
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
sites: { ...existingSites, ...results },
|
|
394
|
-
assets: Object.fromEntries(persistedCache),
|
|
395
|
-
...(finalOnRedeploy ? { onRedeploy: finalOnRedeploy } : {}),
|
|
396
|
-
});
|
|
336
|
+
// Confirmation prompt
|
|
337
|
+
const confirmed = await promptConfirm(
|
|
338
|
+
existingSlug
|
|
339
|
+
? `Overwrite ${cyan(existingSlug + ".htmlhost.co")}?`
|
|
340
|
+
: `Deploy ${bold(String(allFiles.length))} files to htmlhost?`
|
|
341
|
+
);
|
|
397
342
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
ok(`${bold(String(successCount))}/${htmlFiles.length} sites deployed`);
|
|
402
|
-
console.log("");
|
|
403
|
-
for (const [fileName, site] of Object.entries(results)) {
|
|
404
|
-
if (site.url) {
|
|
405
|
-
console.log(` ${dim(fileName)} → ${cyan(`https://${site.url}`)}`);
|
|
406
|
-
}
|
|
343
|
+
if (!confirmed) {
|
|
344
|
+
console.log(" Cancelled.");
|
|
345
|
+
process.exit(0);
|
|
407
346
|
}
|
|
408
|
-
console.log("");
|
|
409
|
-
console.log(` ${dim("Linked → .htmlhost")}`);
|
|
410
|
-
console.log("");
|
|
411
|
-
}
|
|
412
347
|
|
|
413
|
-
|
|
414
|
-
* Deploy all *.html files in a directory as pages within a single multi-page site.
|
|
415
|
-
* Uses the --pages flag. File names map to URL paths:
|
|
416
|
-
* index.html → /
|
|
417
|
-
* about.html → /about
|
|
418
|
-
* blog/index.html → /blog
|
|
419
|
-
* blog/post.html → /blog/post
|
|
420
|
-
*/
|
|
421
|
-
async function deployMultiPage(dirPath, { ttl, title, slug: explicitSlug, forceNew, skipAssets, json }) {
|
|
422
|
-
// Find all .html files (recursive) — skip dotfiles
|
|
423
|
-
const htmlFiles = findHtmlFiles(dirPath, dirPath);
|
|
348
|
+
console.log("");
|
|
424
349
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
350
|
+
// --- Upload non-HTML assets first ---
|
|
351
|
+
const api = getApi();
|
|
352
|
+
/** @type {Map<string, string>} relativePath → hosted URL */
|
|
353
|
+
const assetUrlMap = new Map();
|
|
429
354
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
355
|
+
if (assetFiles.length > 0) {
|
|
356
|
+
info(`Uploading ${cyan(bold(String(assetFiles.length)))} asset${assetFiles.length > 1 ? "s" : ""}…`);
|
|
357
|
+
console.log("");
|
|
433
358
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
359
|
+
for (const file of assetFiles) {
|
|
360
|
+
const fileName = basename(file.filePath);
|
|
361
|
+
const mime = mimeFromExt(fileName);
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const data = await uploadFile("/api/media", file.filePath, fileName, mime);
|
|
365
|
+
const hostedUrl = `${api}${data.url}`;
|
|
366
|
+
assetUrlMap.set(file.relativePath, hostedUrl);
|
|
367
|
+
ok(`${cyan(file.relativePath)} ${dim(`(${formatBytes(file.size)})`)} → ${dim("uploaded")}`);
|
|
368
|
+
} catch (e) {
|
|
369
|
+
err(`${file.relativePath}: ${e.message}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
437
372
|
|
|
438
|
-
|
|
439
|
-
for (const { relativePath, pagePath } of htmlFiles) {
|
|
440
|
-
console.log(` ${dim("📄")} ${relativePath} → ${cyan(pagePath)}`);
|
|
373
|
+
console.log("");
|
|
441
374
|
}
|
|
442
|
-
console.log("");
|
|
443
375
|
|
|
444
|
-
//
|
|
445
|
-
|
|
446
|
-
info(`Linked to ${cyan(existingSlug + ".htmlhost.co")} ${dim("(multi-page)")}`);
|
|
447
|
-
}
|
|
376
|
+
// --- Build page payloads (HTML files with asset URLs rewritten) ---
|
|
377
|
+
info(`Processing ${cyan(bold(String(htmlFiles.length)))} page${htmlFiles.length > 1 ? "s" : ""}…`);
|
|
448
378
|
|
|
449
|
-
// Process assets and build page payloads
|
|
450
|
-
const assetCache = new Map();
|
|
451
|
-
const existingAssets = link?.assets || {};
|
|
452
|
-
const persistedCache = new Map(Object.entries(existingAssets));
|
|
453
379
|
const pagePayloads = [];
|
|
454
380
|
|
|
455
|
-
for (const
|
|
456
|
-
|
|
457
|
-
|
|
381
|
+
for (const file of htmlFiles) {
|
|
382
|
+
let html = readFileSync(file.filePath, "utf8");
|
|
383
|
+
|
|
384
|
+
// Rewrite local asset references to hosted URLs
|
|
385
|
+
for (const [relPath, hostedUrl] of assetUrlMap) {
|
|
386
|
+
// Replace both quoted forms: "path" and 'path'
|
|
387
|
+
// Handle relative paths from the HTML file's perspective
|
|
388
|
+
const htmlDir = dirname(file.relativePath);
|
|
389
|
+
const assetFromHtml = htmlDir === "." ? relPath : relative(htmlDir, relPath);
|
|
390
|
+
|
|
391
|
+
// Replace the original reference and the relative-from-html reference
|
|
392
|
+
html = replaceAll(html, `"${relPath}"`, `"${hostedUrl}"`);
|
|
393
|
+
html = replaceAll(html, `'${relPath}'`, `'${hostedUrl}'`);
|
|
394
|
+
html = replaceAll(html, `"${assetFromHtml}"`, `"${hostedUrl}"`);
|
|
395
|
+
html = replaceAll(html, `'${assetFromHtml}'`, `'${hostedUrl}'`);
|
|
396
|
+
|
|
397
|
+
// Also handle ./ prefix
|
|
398
|
+
html = replaceAll(html, `"./${relPath}"`, `"${hostedUrl}"`);
|
|
399
|
+
html = replaceAll(html, `'./${relPath}'`, `'${hostedUrl}'`);
|
|
400
|
+
if (assetFromHtml !== relPath) {
|
|
401
|
+
html = replaceAll(html, `"./${assetFromHtml}"`, `"${hostedUrl}"`);
|
|
402
|
+
html = replaceAll(html, `'./${assetFromHtml}'`, `'${hostedUrl}'`);
|
|
403
|
+
}
|
|
458
404
|
|
|
459
|
-
|
|
405
|
+
// Handle url() in CSS
|
|
406
|
+
html = replaceAll(html, `url(${relPath})`, `url(${hostedUrl})`);
|
|
407
|
+
html = replaceAll(html, `url(${assetFromHtml})`, `url(${hostedUrl})`);
|
|
408
|
+
html = replaceAll(html, `url(./${relPath})`, `url(${hostedUrl})`);
|
|
409
|
+
}
|
|
460
410
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
411
|
+
// Derive page path
|
|
412
|
+
let pagePath;
|
|
413
|
+
if (file.relativePath === "index.html") {
|
|
414
|
+
pagePath = "/";
|
|
415
|
+
} else if (basename(file.relativePath) === "index.html") {
|
|
416
|
+
const dirRel = dirname(file.relativePath);
|
|
417
|
+
pagePath = `/${dirRel}`;
|
|
418
|
+
} else {
|
|
419
|
+
pagePath = "/" + file.relativePath.replace(/\.html$/, "");
|
|
464
420
|
}
|
|
465
421
|
|
|
466
|
-
pagePayloads.push({
|
|
467
|
-
|
|
468
|
-
html,
|
|
469
|
-
});
|
|
422
|
+
pagePayloads.push({ path: pagePath, html });
|
|
423
|
+
ok(`${cyan(file.relativePath)} → ${cyan(pagePath)}`);
|
|
470
424
|
}
|
|
471
425
|
|
|
472
|
-
// Deploy
|
|
426
|
+
// --- Deploy ---
|
|
427
|
+
console.log("");
|
|
428
|
+
info(existingSlug ? `Deploying to ${cyan(existingSlug)}…` : "Deploying new site…");
|
|
429
|
+
|
|
473
430
|
const body = { pages: pagePayloads };
|
|
474
431
|
if (ttl) body.ttl = ttl;
|
|
475
432
|
if (existingSlug) body.slug = existingSlug;
|
|
476
433
|
if (title) body.title = title;
|
|
477
434
|
|
|
478
|
-
info(existingSlug ? `Re-deploying to ${cyan(existingSlug)}…` : "Deploying new multi-page site…");
|
|
479
|
-
|
|
480
435
|
try {
|
|
481
436
|
const data = await post("/api/sites", body);
|
|
482
437
|
|
|
@@ -487,7 +442,6 @@ async function deployMultiPage(dirPath, { ttl, title, slug: explicitSlug, forceN
|
|
|
487
442
|
url: data.url,
|
|
488
443
|
pageCount: data.pageCount || pagePayloads.length,
|
|
489
444
|
},
|
|
490
|
-
assets: Object.fromEntries(persistedCache),
|
|
491
445
|
});
|
|
492
446
|
|
|
493
447
|
if (json) {
|
|
@@ -501,12 +455,12 @@ async function deployMultiPage(dirPath, { ttl, title, slug: explicitSlug, forceN
|
|
|
501
455
|
console.log(` ${dim(`${data.pageCount || pagePayloads.length} pages · ${data.ttl} TTL`)}`);
|
|
502
456
|
console.log("");
|
|
503
457
|
|
|
504
|
-
for (const { pagePath } of
|
|
458
|
+
for (const { path: pagePath } of pagePayloads) {
|
|
505
459
|
const urlPath = pagePath === "/" ? "" : pagePath;
|
|
506
460
|
console.log(` ${cyan(`https://${data.url}${urlPath}`)}`);
|
|
507
461
|
}
|
|
508
462
|
console.log("");
|
|
509
|
-
console.log(` ${dim("Linked → .htmlhost
|
|
463
|
+
console.log(` ${dim("Linked → .htmlhost")}`);
|
|
510
464
|
console.log("");
|
|
511
465
|
} catch (e) {
|
|
512
466
|
err(`Deploy failed: ${e.message}`);
|
|
@@ -515,48 +469,58 @@ async function deployMultiPage(dirPath, { ttl, title, slug: explicitSlug, forceN
|
|
|
515
469
|
}
|
|
516
470
|
|
|
517
471
|
/**
|
|
518
|
-
* Recursively find all
|
|
519
|
-
*
|
|
472
|
+
* Recursively find all files in a directory.
|
|
473
|
+
* Skips ignored files/directories.
|
|
474
|
+
* Returns [{ filePath, relativePath, size }]
|
|
520
475
|
*/
|
|
521
|
-
function
|
|
476
|
+
function findAllFiles(baseDir, currentDir) {
|
|
522
477
|
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
523
478
|
const results = [];
|
|
524
479
|
|
|
525
480
|
for (const entry of entries) {
|
|
526
|
-
if (entry.name
|
|
481
|
+
if (shouldIgnore(entry.name)) continue;
|
|
527
482
|
|
|
528
483
|
const fullPath = join(currentDir, entry.name);
|
|
529
484
|
|
|
530
485
|
if (entry.isDirectory()) {
|
|
531
|
-
results.push(...
|
|
532
|
-
} else if (entry.
|
|
533
|
-
const relativePath = fullPath.slice(baseDir.length + 1);
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const dirRel = dirname(relativePath);
|
|
540
|
-
pagePath = dirRel === "." ? "/" : `/${dirRel}`;
|
|
541
|
-
} else {
|
|
542
|
-
// about.html → /about, blog/post.html → /blog/post
|
|
543
|
-
pagePath = "/" + relativePath.replace(/\.html$/, "");
|
|
486
|
+
results.push(...findAllFiles(baseDir, fullPath));
|
|
487
|
+
} else if (entry.isFile()) {
|
|
488
|
+
const relativePath = fullPath.slice(baseDir.length + 1);
|
|
489
|
+
let size;
|
|
490
|
+
try {
|
|
491
|
+
size = statSync(fullPath).size;
|
|
492
|
+
} catch {
|
|
493
|
+
continue;
|
|
544
494
|
}
|
|
545
|
-
|
|
546
|
-
results.push({ filePath: fullPath, relativePath, pagePath });
|
|
495
|
+
results.push({ filePath: fullPath, relativePath, size });
|
|
547
496
|
}
|
|
548
497
|
}
|
|
549
498
|
|
|
550
|
-
// Sort:
|
|
499
|
+
// Sort: index.html first, then HTML files, then others alphabetically
|
|
551
500
|
results.sort((a, b) => {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
501
|
+
const aIsIndex = a.relativePath === "index.html";
|
|
502
|
+
const bIsIndex = b.relativePath === "index.html";
|
|
503
|
+
if (aIsIndex) return -1;
|
|
504
|
+
if (bIsIndex) return 1;
|
|
505
|
+
|
|
506
|
+
const aIsHtml = a.relativePath.endsWith(".html");
|
|
507
|
+
const bIsHtml = b.relativePath.endsWith(".html");
|
|
508
|
+
if (aIsHtml && !bIsHtml) return -1;
|
|
509
|
+
if (!aIsHtml && bIsHtml) return 1;
|
|
510
|
+
|
|
511
|
+
return a.relativePath.localeCompare(b.relativePath);
|
|
555
512
|
});
|
|
556
513
|
|
|
557
514
|
return results;
|
|
558
515
|
}
|
|
559
516
|
|
|
517
|
+
/**
|
|
518
|
+
* Replace all occurrences of `search` in `str` with `replacement`.
|
|
519
|
+
*/
|
|
520
|
+
function replaceAll(str, search, replacement) {
|
|
521
|
+
return str.split(search).join(replacement);
|
|
522
|
+
}
|
|
523
|
+
|
|
560
524
|
function getFlag(args, flag) {
|
|
561
525
|
const idx = args.indexOf(flag);
|
|
562
526
|
if (idx === -1 || idx >= args.length - 1) return null;
|
package/src/commands/upload.mjs
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { statSync, readdirSync } from "node:fs";
|
|
2
|
-
import { resolve, basename, join } from "node:path";
|
|
3
|
-
import { uploadFile } from "../api.mjs";
|
|
4
|
-
import { ok, err, info, cyan, dim, bold, formatBytes, mimeFromExt } from "../ui.mjs";
|
|
5
|
-
import { getApi } from "../config.mjs";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* htmlhost upload <file|dir> — upload media assets and get URLs.
|
|
9
|
-
*/
|
|
10
|
-
export async function upload(args) {
|
|
11
|
-
const target = args.find((a) => !a.startsWith("--"));
|
|
12
|
-
if (!target) {
|
|
13
|
-
err("Usage: htmlhost upload <file|directory>");
|
|
14
|
-
process.exit(1);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const targetPath = resolve(target);
|
|
18
|
-
let stat;
|
|
19
|
-
try {
|
|
20
|
-
stat = statSync(targetPath);
|
|
21
|
-
} catch {
|
|
22
|
-
err(`Not found: ${target}`);
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const files = stat.isDirectory()
|
|
27
|
-
? readdirSync(targetPath)
|
|
28
|
-
.filter((f) => !f.startsWith("."))
|
|
29
|
-
.map((f) => ({ path: join(targetPath, f), name: f }))
|
|
30
|
-
.filter((f) => statSync(f.path).isFile())
|
|
31
|
-
: [{ path: targetPath, name: basename(targetPath) }];
|
|
32
|
-
|
|
33
|
-
if (files.length === 0) {
|
|
34
|
-
err("No files found.");
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
console.log("");
|
|
39
|
-
info(`Uploading ${bold(String(files.length))} file${files.length > 1 ? "s" : ""}…`);
|
|
40
|
-
console.log("");
|
|
41
|
-
|
|
42
|
-
const api = getApi();
|
|
43
|
-
const results = [];
|
|
44
|
-
|
|
45
|
-
for (const file of files) {
|
|
46
|
-
const size = statSync(file.path).size;
|
|
47
|
-
const mime = mimeFromExt(file.name);
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const data = await uploadFile("/api/media", file.path, file.name, mime);
|
|
51
|
-
const url = `${api}${data.url}`;
|
|
52
|
-
results.push({ name: file.name, url, size });
|
|
53
|
-
ok(`${cyan(file.name)} ${dim(`(${formatBytes(size)})`)} → ${url}`);
|
|
54
|
-
} catch (e) {
|
|
55
|
-
err(`${file.name}: ${e.message}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (results.length > 0) {
|
|
60
|
-
console.log("");
|
|
61
|
-
console.log(` ${dim("Use these URLs in your HTML:")}`);
|
|
62
|
-
console.log("");
|
|
63
|
-
for (const r of results) {
|
|
64
|
-
console.log(` ${dim(`<img src="${r.url}" />`)}`)
|
|
65
|
-
}
|
|
66
|
-
console.log("");
|
|
67
|
-
}
|
|
68
|
-
}
|