htmlship 0.1.5 → 0.2.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 +13 -1
- package/dist/cli.js +428 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,9 +16,21 @@ npx htmlship delete <slug>
|
|
|
16
16
|
npx htmlship list-mine
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
## Deploy built apps
|
|
20
|
+
|
|
21
|
+
Build a modern frontend project (React/Vite/etc.) and publish the compiled app as one self-contained page:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx htmlship deploy ./my-app # detect build script, build, inline, publish
|
|
25
|
+
npx htmlship deploy --dry-run # build + inline without publishing
|
|
26
|
+
npx htmlship deploy --build-cmd "vite build" --out dist
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The build runs **locally on your machine** (npm/pnpm/yarn/bun, auto-detected) — never on the server. The output is inlined into a single HTML file (≤ 10 MB) and published with relaxed sandboxing, so the app's JavaScript runs in an isolated, opaque origin (no cookies, no same-origin access, no network egress, no `eval`). The same flow is available to agents through the `deploy_project` MCP tool.
|
|
30
|
+
|
|
19
31
|
## MCP server
|
|
20
32
|
|
|
21
|
-
`htmlship mcp` starts a stdio MCP server with
|
|
33
|
+
`htmlship mcp` starts a stdio MCP server with four tools: `publish_html` (with optional `password`), `deploy_project` (build a local project and publish the compiled app), `fetch_html`, `update_html`.
|
|
22
34
|
|
|
23
35
|
### Claude Desktop / Claude Code / Cursor
|
|
24
36
|
|
package/dist/cli.js
CHANGED
|
@@ -51,22 +51,260 @@ var init_errors = __esm({
|
|
|
51
51
|
}
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
// src/build.ts
|
|
55
|
+
import { spawnSync } from "child_process";
|
|
56
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
57
|
+
import { dirname, extname, isAbsolute, join, relative, resolve } from "path";
|
|
58
|
+
function detectPackageManager(dir) {
|
|
59
|
+
for (const [file, pm] of LOCKFILES) {
|
|
60
|
+
if (existsSync(join(dir, file))) return pm;
|
|
61
|
+
}
|
|
62
|
+
return "npm";
|
|
63
|
+
}
|
|
64
|
+
function detectProject(dir, buildCmdOverride) {
|
|
65
|
+
const pkgPath = join(dir, "package.json");
|
|
66
|
+
if (!existsSync(pkgPath)) {
|
|
67
|
+
throw new HTMLShipError(`no package.json found in ${dir}`);
|
|
68
|
+
}
|
|
69
|
+
let scripts = {};
|
|
70
|
+
try {
|
|
71
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
72
|
+
if (pkg && typeof pkg === "object" && "scripts" in pkg) {
|
|
73
|
+
const s = pkg.scripts;
|
|
74
|
+
if (s && typeof s === "object") scripts = s;
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
throw new HTMLShipError(`failed to read package.json: ${err.message}`);
|
|
78
|
+
}
|
|
79
|
+
const pm = detectPackageManager(dir);
|
|
80
|
+
if (buildCmdOverride) {
|
|
81
|
+
return { dir, buildScript: "", packageManager: pm };
|
|
82
|
+
}
|
|
83
|
+
const buildScript = scripts["build"] ? "build" : scripts["build:prod"] ? "build:prod" : null;
|
|
84
|
+
if (!buildScript) {
|
|
85
|
+
throw new HTMLShipError(
|
|
86
|
+
'no "build" or "build:prod" script in package.json (use --build-cmd to specify one)'
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return { dir, buildScript, packageManager: pm };
|
|
90
|
+
}
|
|
91
|
+
function buildCommand(pm, script) {
|
|
92
|
+
return pm === "yarn" ? `yarn ${script}` : `${pm} run ${script}`;
|
|
93
|
+
}
|
|
94
|
+
function installCommand(pm) {
|
|
95
|
+
return `${pm} install`;
|
|
96
|
+
}
|
|
97
|
+
function runBuild(project, opts = {}) {
|
|
98
|
+
const log = opts.log ?? (() => {
|
|
99
|
+
});
|
|
100
|
+
const timeout = opts.timeoutMs ?? DEFAULT_BUILD_TIMEOUT_MS;
|
|
101
|
+
if (opts.install) {
|
|
102
|
+
exec(installCommand(project.packageManager), project.dir, timeout, log);
|
|
103
|
+
}
|
|
104
|
+
const cmd = opts.buildCmd ?? buildCommand(project.packageManager, project.buildScript);
|
|
105
|
+
exec(cmd, project.dir, timeout, log);
|
|
106
|
+
}
|
|
107
|
+
function exec(command, cwd, timeout, log) {
|
|
108
|
+
log(`$ ${command}`);
|
|
109
|
+
const r = spawnSync(command, { cwd, stdio: "inherit", timeout, shell: true });
|
|
110
|
+
if (r.error) {
|
|
111
|
+
const code = r.error.code;
|
|
112
|
+
if (code === "ENOENT") {
|
|
113
|
+
throw new HTMLShipError(`build failed: \`${command}\` \u2014 command not found`);
|
|
114
|
+
}
|
|
115
|
+
throw new HTMLShipError(`build failed: ${r.error.message}`);
|
|
116
|
+
}
|
|
117
|
+
if (r.signal === "SIGTERM") {
|
|
118
|
+
throw new HTMLShipError(`build timed out after ${Math.round(timeout / 1e3)}s: \`${command}\``);
|
|
119
|
+
}
|
|
120
|
+
if (typeof r.status === "number" && r.status !== 0) {
|
|
121
|
+
throw new HTMLShipError(`build exited with code ${r.status}: \`${command}\``);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function resolveOutputDir(dir, override) {
|
|
125
|
+
const candidates = override ? [override] : ["dist", "build"];
|
|
126
|
+
for (const c of candidates) {
|
|
127
|
+
const p = isAbsolute(c) ? c : join(dir, c);
|
|
128
|
+
if (existsSync(p) && statSync(p).isDirectory()) return p;
|
|
129
|
+
}
|
|
130
|
+
throw new HTMLShipError(
|
|
131
|
+
override ? `build output dir not found: ${override}` : "no build output found (looked for dist/ and build/; use --out to specify)"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
function mimeFor(p) {
|
|
135
|
+
return MIME[extname(p).toLowerCase()] ?? "application/octet-stream";
|
|
136
|
+
}
|
|
137
|
+
function isLocalRef(ref) {
|
|
138
|
+
return ref.length > 0 && !/^(https?:)?\/\//i.test(ref) && !ref.startsWith("data:") && !ref.startsWith("#") && !ref.startsWith("mailto:") && !ref.startsWith("blob:");
|
|
139
|
+
}
|
|
140
|
+
function attr(tag, name) {
|
|
141
|
+
const m = tag.match(new RegExp(`\\b${name}\\s*=\\s*("([^"]*)"|'([^']*)'|([^\\s>]+))`, "i"));
|
|
142
|
+
if (!m) return null;
|
|
143
|
+
return m[2] ?? m[3] ?? m[4] ?? null;
|
|
144
|
+
}
|
|
145
|
+
function resolveLocal(root, baseDir, ref) {
|
|
146
|
+
let clean = ref.split("#")[0].split("?")[0];
|
|
147
|
+
if (!clean) return null;
|
|
148
|
+
let base = baseDir;
|
|
149
|
+
if (clean.startsWith("/")) {
|
|
150
|
+
base = root;
|
|
151
|
+
clean = clean.replace(/^\/+/, "");
|
|
152
|
+
}
|
|
153
|
+
let abs;
|
|
154
|
+
try {
|
|
155
|
+
abs = resolve(base, decodeURIComponent(clean));
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const rel = relative(root, abs);
|
|
160
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return null;
|
|
161
|
+
if (!existsSync(abs) || !statSync(abs).isFile()) return null;
|
|
162
|
+
return abs;
|
|
163
|
+
}
|
|
164
|
+
function dataUri(file) {
|
|
165
|
+
return `data:${mimeFor(file)};base64,${readFileSync(file).toString("base64")}`;
|
|
166
|
+
}
|
|
167
|
+
function inlineCssUrls(root, cssDir, css, warnings) {
|
|
168
|
+
return css.replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/gi, (whole, _q, ref) => {
|
|
169
|
+
if (!isLocalRef(ref)) return whole;
|
|
170
|
+
const file = resolveLocal(root, cssDir, ref);
|
|
171
|
+
if (!file) {
|
|
172
|
+
warnings.push(`unresolved css asset: ${ref}`);
|
|
173
|
+
return whole;
|
|
174
|
+
}
|
|
175
|
+
return `url(${dataUri(file)})`;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function inlineHtml(root, htmlFile) {
|
|
179
|
+
if (!existsSync(htmlFile)) {
|
|
180
|
+
throw new HTMLShipError(`entry HTML not found: ${htmlFile}`);
|
|
181
|
+
}
|
|
182
|
+
const htmlDir = dirname(htmlFile);
|
|
183
|
+
const warnings = [];
|
|
184
|
+
let html = readFileSync(htmlFile, "utf8");
|
|
185
|
+
const localFile = (ref) => {
|
|
186
|
+
if (!isLocalRef(ref)) return null;
|
|
187
|
+
const file = resolveLocal(root, htmlDir, ref);
|
|
188
|
+
if (!file) {
|
|
189
|
+
warnings.push(`unresolved asset: ${ref}`);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
return file;
|
|
193
|
+
};
|
|
194
|
+
html = html.replace(/<link\b[^>]*>/gi, (tag) => {
|
|
195
|
+
const rel = attr(tag, "rel")?.toLowerCase() ?? "";
|
|
196
|
+
const href = attr(tag, "href");
|
|
197
|
+
if (!href) return tag;
|
|
198
|
+
if (rel === "stylesheet") {
|
|
199
|
+
const file = localFile(href);
|
|
200
|
+
if (!file) return tag;
|
|
201
|
+
const css = inlineCssUrls(root, dirname(file), readFileSync(file, "utf8"), warnings);
|
|
202
|
+
return `<style>${css}</style>`;
|
|
203
|
+
}
|
|
204
|
+
if (rel === "modulepreload" || rel === "preload" || rel === "prefetch") {
|
|
205
|
+
return isLocalRef(href) ? "" : tag;
|
|
206
|
+
}
|
|
207
|
+
if (rel === "icon" || rel === "shortcut icon" || rel === "apple-touch-icon") {
|
|
208
|
+
const file = localFile(href);
|
|
209
|
+
return file ? tag.replace(href, dataUri(file)) : tag;
|
|
210
|
+
}
|
|
211
|
+
return tag;
|
|
212
|
+
});
|
|
213
|
+
html = html.replace(/<script\b([^>]*)>\s*<\/script>/gi, (tag, attrs) => {
|
|
214
|
+
const src = attr(tag, "src");
|
|
215
|
+
if (!src) return tag;
|
|
216
|
+
const file = localFile(src);
|
|
217
|
+
if (!file) return tag;
|
|
218
|
+
const js = readFileSync(file, "utf8").replace(/<\/script/gi, "<\\/script");
|
|
219
|
+
const typeAttr = /\btype\s*=\s*["']module["']/i.test(attrs) ? ' type="module"' : "";
|
|
220
|
+
return `<script${typeAttr}>${js}</script>`;
|
|
221
|
+
});
|
|
222
|
+
html = html.replace(/<img\b[^>]*>/gi, (tag) => {
|
|
223
|
+
const src = attr(tag, "src");
|
|
224
|
+
if (!src || !isLocalRef(src)) return tag;
|
|
225
|
+
const file = localFile(src);
|
|
226
|
+
return file ? tag.replace(src, dataUri(file)) : tag;
|
|
227
|
+
});
|
|
228
|
+
return { html, warnings, bytes: Buffer.byteLength(html, "utf8") };
|
|
229
|
+
}
|
|
230
|
+
function formatBytes(n) {
|
|
231
|
+
if (n < 1024) return `${n} B`;
|
|
232
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
233
|
+
return `${(n / (1024 * 1024)).toFixed(2)} MB`;
|
|
234
|
+
}
|
|
235
|
+
function buildAndInline(dir, opts = {}) {
|
|
236
|
+
const log = opts.log ?? (() => {
|
|
237
|
+
});
|
|
238
|
+
const project = detectProject(dir, opts.buildCmd);
|
|
239
|
+
log(`project: ${dir}`);
|
|
240
|
+
log(`pkg mgr: ${project.packageManager}`);
|
|
241
|
+
log(`build: ${opts.buildCmd ?? buildCommand(project.packageManager, project.buildScript)}`);
|
|
242
|
+
runBuild(project, {
|
|
243
|
+
install: opts.install,
|
|
244
|
+
buildCmd: opts.buildCmd,
|
|
245
|
+
timeoutMs: opts.timeoutMs,
|
|
246
|
+
log
|
|
247
|
+
});
|
|
248
|
+
const outDir = resolveOutputDir(dir, opts.out);
|
|
249
|
+
const entry = join(outDir, opts.entry ?? "index.html");
|
|
250
|
+
const result = inlineHtml(outDir, entry);
|
|
251
|
+
log(`output: ${outDir}`);
|
|
252
|
+
log(`inlined: ${formatBytes(result.bytes)} single HTML`);
|
|
253
|
+
for (const w of result.warnings) log(`warning: ${w}`);
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
var LOCKFILES, DEFAULT_BUILD_TIMEOUT_MS, MIME;
|
|
257
|
+
var init_build = __esm({
|
|
258
|
+
"src/build.ts"() {
|
|
259
|
+
"use strict";
|
|
260
|
+
init_errors();
|
|
261
|
+
LOCKFILES = [
|
|
262
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
263
|
+
["yarn.lock", "yarn"],
|
|
264
|
+
["bun.lockb", "bun"],
|
|
265
|
+
["bun.lock", "bun"],
|
|
266
|
+
["package-lock.json", "npm"]
|
|
267
|
+
];
|
|
268
|
+
DEFAULT_BUILD_TIMEOUT_MS = 5 * 6e4;
|
|
269
|
+
MIME = {
|
|
270
|
+
".png": "image/png",
|
|
271
|
+
".jpg": "image/jpeg",
|
|
272
|
+
".jpeg": "image/jpeg",
|
|
273
|
+
".gif": "image/gif",
|
|
274
|
+
".svg": "image/svg+xml",
|
|
275
|
+
".webp": "image/webp",
|
|
276
|
+
".avif": "image/avif",
|
|
277
|
+
".ico": "image/x-icon",
|
|
278
|
+
".woff": "font/woff",
|
|
279
|
+
".woff2": "font/woff2",
|
|
280
|
+
".ttf": "font/ttf",
|
|
281
|
+
".otf": "font/otf",
|
|
282
|
+
".css": "text/css",
|
|
283
|
+
".js": "text/javascript",
|
|
284
|
+
".mjs": "text/javascript",
|
|
285
|
+
".json": "application/json"
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
54
290
|
// src/version.ts
|
|
55
291
|
var VERSION;
|
|
56
292
|
var init_version = __esm({
|
|
57
293
|
"src/version.ts"() {
|
|
58
294
|
"use strict";
|
|
59
|
-
VERSION = "0.
|
|
295
|
+
VERSION = "0.2.0";
|
|
60
296
|
}
|
|
61
297
|
});
|
|
62
298
|
|
|
63
299
|
// src/client.ts
|
|
64
|
-
var DEFAULT_API_URL, HTMLShipClient;
|
|
300
|
+
var MAX_PAYLOAD_BYTES, DEFAULT_API_URL, HTMLShipClient;
|
|
65
301
|
var init_client = __esm({
|
|
66
302
|
"src/client.ts"() {
|
|
67
303
|
"use strict";
|
|
304
|
+
init_build();
|
|
68
305
|
init_errors();
|
|
69
306
|
init_version();
|
|
307
|
+
MAX_PAYLOAD_BYTES = 10 * 1024 * 1024;
|
|
70
308
|
DEFAULT_API_URL = "https://api.htmlship.com";
|
|
71
309
|
HTMLShipClient = class {
|
|
72
310
|
baseUrl;
|
|
@@ -92,6 +330,28 @@ var init_client = __esm({
|
|
|
92
330
|
if (options.parentSlug != null) body["parent_slug"] = options.parentSlug;
|
|
93
331
|
return await this.request("POST", "/api/v1/pages", { body });
|
|
94
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* Build a local frontend project, inline its output into one self-contained
|
|
335
|
+
* HTML document, and publish it as a relaxed (sandboxed-script) page. The
|
|
336
|
+
* build runs on this machine — never on the server.
|
|
337
|
+
*/
|
|
338
|
+
async deploy(projectDir, options = {}) {
|
|
339
|
+
const { html, bytes } = buildAndInline(projectDir, {
|
|
340
|
+
buildCmd: options.buildCmd,
|
|
341
|
+
out: options.out,
|
|
342
|
+
entry: options.entry,
|
|
343
|
+
install: options.install
|
|
344
|
+
});
|
|
345
|
+
if (bytes > MAX_PAYLOAD_BYTES) {
|
|
346
|
+
throw new HTMLShipError(`inlined page is ${bytes} bytes, exceeds the 10 MB limit`);
|
|
347
|
+
}
|
|
348
|
+
return await this.publish(html, {
|
|
349
|
+
title: options.title ?? null,
|
|
350
|
+
password: options.password ?? null,
|
|
351
|
+
expiresIn: options.expiresIn ?? null,
|
|
352
|
+
sandboxMode: "relaxed"
|
|
353
|
+
});
|
|
354
|
+
}
|
|
95
355
|
async get(slug) {
|
|
96
356
|
return await this.request("GET", `/api/v1/pages/${encodeURIComponent(slug)}`);
|
|
97
357
|
}
|
|
@@ -240,6 +500,52 @@ function buildMcpServer(client) {
|
|
|
240
500
|
}
|
|
241
501
|
}
|
|
242
502
|
);
|
|
503
|
+
server.registerTool(
|
|
504
|
+
"deploy_project",
|
|
505
|
+
{
|
|
506
|
+
description: "Build a local frontend project (npm/pnpm/yarn/bun) and publish the compiled app as one self-contained, script-enabled page. Runs the project's build script ON THIS MACHINE, inlines the output (dist/ or build/) into a single HTML file, and publishes it with relaxed sandboxing so its JavaScript runs in an isolated origin. The owner_key returned is the only credential to update or delete this page later \u2014 save it.",
|
|
507
|
+
inputSchema: {
|
|
508
|
+
dir: z.string().describe("Path to the project directory (must contain package.json)."),
|
|
509
|
+
build_cmd: z.string().optional().describe("Override the build command (default: detected build/build:prod script)."),
|
|
510
|
+
out: z.string().optional().describe("Build output directory (default: auto-detect dist/ or build/)."),
|
|
511
|
+
install: z.boolean().optional().describe("Run dependency install before building."),
|
|
512
|
+
title: z.string().optional().describe("Optional human-readable title."),
|
|
513
|
+
password: z.string().optional().describe("Optional password required before viewing."),
|
|
514
|
+
expires_in: z.number().int().min(1).max(60 * 24 * 7).optional().describe("Optional TTL in minutes (1\u201310080, i.e. up to 7 days).")
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
async ({ dir, build_cmd, out, install, title, password, expires_in }) => {
|
|
518
|
+
try {
|
|
519
|
+
const page = await c.deploy(dir, {
|
|
520
|
+
buildCmd: build_cmd ?? void 0,
|
|
521
|
+
out: out ?? void 0,
|
|
522
|
+
install: install ?? void 0,
|
|
523
|
+
title: title ?? null,
|
|
524
|
+
password: password ?? null,
|
|
525
|
+
expiresIn: expires_in ?? null
|
|
526
|
+
});
|
|
527
|
+
return {
|
|
528
|
+
content: [
|
|
529
|
+
{
|
|
530
|
+
type: "text",
|
|
531
|
+
text: JSON.stringify(
|
|
532
|
+
{
|
|
533
|
+
url: page.url,
|
|
534
|
+
slug: page.slug,
|
|
535
|
+
owner_key: page.owner_key,
|
|
536
|
+
expires_at: page.expires_at
|
|
537
|
+
},
|
|
538
|
+
null,
|
|
539
|
+
2
|
|
540
|
+
)
|
|
541
|
+
}
|
|
542
|
+
]
|
|
543
|
+
};
|
|
544
|
+
} catch (err) {
|
|
545
|
+
return errorResult("deploy_project", err);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
);
|
|
243
549
|
server.registerTool(
|
|
244
550
|
"fetch_html",
|
|
245
551
|
{
|
|
@@ -328,16 +634,16 @@ init_errors();
|
|
|
328
634
|
import { createInterface } from "readline/promises";
|
|
329
635
|
|
|
330
636
|
// src/keystore.ts
|
|
331
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs";
|
|
637
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync, chmodSync } from "fs";
|
|
332
638
|
import { homedir } from "os";
|
|
333
|
-
import { join } from "path";
|
|
639
|
+
import { join as join2 } from "path";
|
|
334
640
|
function createKeyStore(overrideDir) {
|
|
335
|
-
const dir = overrideDir ?? process.env["HTMLSHIP_KEYS_DIR"] ??
|
|
336
|
-
const file =
|
|
641
|
+
const dir = overrideDir ?? process.env["HTMLSHIP_KEYS_DIR"] ?? join2(homedir(), ".htmlship");
|
|
642
|
+
const file = join2(dir, "keys.json");
|
|
337
643
|
function load() {
|
|
338
|
-
if (!
|
|
644
|
+
if (!existsSync2(file)) return {};
|
|
339
645
|
try {
|
|
340
|
-
const text =
|
|
646
|
+
const text = readFileSync2(file, "utf8");
|
|
341
647
|
const parsed = JSON.parse(text);
|
|
342
648
|
if (parsed && typeof parsed === "object") return parsed;
|
|
343
649
|
return {};
|
|
@@ -346,7 +652,7 @@ function createKeyStore(overrideDir) {
|
|
|
346
652
|
}
|
|
347
653
|
}
|
|
348
654
|
function save(data) {
|
|
349
|
-
if (!
|
|
655
|
+
if (!existsSync2(dir)) mkdirSync(dir, { recursive: true });
|
|
350
656
|
const sorted = {};
|
|
351
657
|
for (const key of Object.keys(data).sort()) {
|
|
352
658
|
sorted[key] = data[key];
|
|
@@ -420,6 +726,117 @@ function registerDelete(program) {
|
|
|
420
726
|
});
|
|
421
727
|
}
|
|
422
728
|
|
|
729
|
+
// src/commands/deploy.ts
|
|
730
|
+
init_build();
|
|
731
|
+
init_client();
|
|
732
|
+
init_errors();
|
|
733
|
+
import { resolve as resolve2 } from "path";
|
|
734
|
+
|
|
735
|
+
// src/io.ts
|
|
736
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
737
|
+
function readHtmlFromSource(source, fileFlag) {
|
|
738
|
+
if (fileFlag) {
|
|
739
|
+
return readFileSync3(fileFlag, "utf8");
|
|
740
|
+
}
|
|
741
|
+
if (source === "-" || source === void 0) {
|
|
742
|
+
if (source === void 0 && process.stdin.isTTY) {
|
|
743
|
+
throw new CliUsageError("Provide a file path or pipe HTML on stdin.");
|
|
744
|
+
}
|
|
745
|
+
return readStdinSync();
|
|
746
|
+
}
|
|
747
|
+
return readFileSync3(source, "utf8");
|
|
748
|
+
}
|
|
749
|
+
var CliUsageError = class extends Error {
|
|
750
|
+
constructor(message) {
|
|
751
|
+
super(message);
|
|
752
|
+
this.name = "CliUsageError";
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
function readStdinSync() {
|
|
756
|
+
try {
|
|
757
|
+
return readFileSync3(0, "utf8");
|
|
758
|
+
} catch (err) {
|
|
759
|
+
throw new CliUsageError(`failed to read stdin: ${err.message}`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
async function tryClipboardCopy(text) {
|
|
763
|
+
try {
|
|
764
|
+
const mod = await import("clipboardy");
|
|
765
|
+
await mod.default.write(text);
|
|
766
|
+
return true;
|
|
767
|
+
} catch {
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// src/commands/deploy.ts
|
|
773
|
+
var MAX_BYTES = 10 * 1024 * 1024;
|
|
774
|
+
function registerDeploy(program) {
|
|
775
|
+
program.command("deploy").description(
|
|
776
|
+
"Build a frontend project and publish the compiled app as one self-contained page."
|
|
777
|
+
).argument("[dir]", "project directory (default: current directory)", ".").option("--build-cmd <cmd>", "Override the build command (default: detected from package.json)").option("--out <dir>", "Build output directory (default: auto-detect dist/ or build/)").option("--entry <file>", "Entry HTML within the output dir (default: index.html)").option("--install", "Run dependency install before building").option("--dry-run", "Build and inline, but report a summary instead of publishing").option("--title <title>", "Optional title").option("--password <password>", "Password-protect the page").option("--expires-in <minutes>", "Minutes until expiry (1\u201310080, i.e. up to 7 days)").option("--no-clipboard", "Don't copy URL to clipboard").option("-q, --quiet", "Print only the URL").action(async function(dir, opts) {
|
|
778
|
+
const projectDir = resolve2(dir);
|
|
779
|
+
const log = opts.quiet ? () => {
|
|
780
|
+
} : (m) => process.stderr.write(`${m}
|
|
781
|
+
`);
|
|
782
|
+
const { html, bytes } = buildAndInline(projectDir, {
|
|
783
|
+
buildCmd: opts.buildCmd,
|
|
784
|
+
out: opts.out,
|
|
785
|
+
entry: opts.entry,
|
|
786
|
+
install: opts.install,
|
|
787
|
+
log
|
|
788
|
+
});
|
|
789
|
+
if (bytes > MAX_BYTES) {
|
|
790
|
+
throw new HTMLShipError(
|
|
791
|
+
`inlined page is ${formatBytes(bytes)}, exceeds the ${formatBytes(MAX_BYTES)} limit`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
if (opts.dryRun) {
|
|
795
|
+
log("dry-run: built and inlined OK; not published");
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const apiUrl = this.parent?.opts()?.apiUrl;
|
|
799
|
+
const client = new HTMLShipClient({ baseUrl: apiUrl });
|
|
800
|
+
let page;
|
|
801
|
+
try {
|
|
802
|
+
page = await client.publish(html, {
|
|
803
|
+
title: opts.title ?? null,
|
|
804
|
+
password: opts.password ?? null,
|
|
805
|
+
expiresIn: opts.expiresIn ? Number.parseInt(opts.expiresIn, 10) : null,
|
|
806
|
+
sandboxMode: "relaxed"
|
|
807
|
+
});
|
|
808
|
+
} catch (err) {
|
|
809
|
+
throw new HTMLShipError(`deploy failed: ${err.message}`);
|
|
810
|
+
}
|
|
811
|
+
const keys = createKeyStore();
|
|
812
|
+
keys.remember(page.slug, {
|
|
813
|
+
owner_key: page.owner_key,
|
|
814
|
+
url: page.url,
|
|
815
|
+
title: opts.title ?? null
|
|
816
|
+
});
|
|
817
|
+
if (opts.quiet) {
|
|
818
|
+
process.stdout.write(`${page.url}
|
|
819
|
+
`);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
process.stdout.write(`${page.url}
|
|
823
|
+
`);
|
|
824
|
+
process.stderr.write(`slug: ${page.slug}
|
|
825
|
+
`);
|
|
826
|
+
process.stderr.write(`owner_key: ${page.owner_key} (saved to ${keys.file})
|
|
827
|
+
`);
|
|
828
|
+
process.stderr.write("sandbox: relaxed (scripts run in an isolated origin)\n");
|
|
829
|
+
if (page.expires_at) {
|
|
830
|
+
process.stderr.write(`expires: ${page.expires_at}
|
|
831
|
+
`);
|
|
832
|
+
}
|
|
833
|
+
if (opts.clipboard !== false) {
|
|
834
|
+
const copied = await tryClipboardCopy(page.url);
|
|
835
|
+
if (copied) process.stderr.write("(URL copied to clipboard)\n");
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
423
840
|
// src/commands/get.ts
|
|
424
841
|
init_client();
|
|
425
842
|
init_errors();
|
|
@@ -498,45 +915,6 @@ function registerListMine(program) {
|
|
|
498
915
|
// src/commands/publish.ts
|
|
499
916
|
init_client();
|
|
500
917
|
init_errors();
|
|
501
|
-
|
|
502
|
-
// src/io.ts
|
|
503
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
504
|
-
function readHtmlFromSource(source, fileFlag) {
|
|
505
|
-
if (fileFlag) {
|
|
506
|
-
return readFileSync2(fileFlag, "utf8");
|
|
507
|
-
}
|
|
508
|
-
if (source === "-" || source === void 0) {
|
|
509
|
-
if (source === void 0 && process.stdin.isTTY) {
|
|
510
|
-
throw new CliUsageError("Provide a file path or pipe HTML on stdin.");
|
|
511
|
-
}
|
|
512
|
-
return readStdinSync();
|
|
513
|
-
}
|
|
514
|
-
return readFileSync2(source, "utf8");
|
|
515
|
-
}
|
|
516
|
-
var CliUsageError = class extends Error {
|
|
517
|
-
constructor(message) {
|
|
518
|
-
super(message);
|
|
519
|
-
this.name = "CliUsageError";
|
|
520
|
-
}
|
|
521
|
-
};
|
|
522
|
-
function readStdinSync() {
|
|
523
|
-
try {
|
|
524
|
-
return readFileSync2(0, "utf8");
|
|
525
|
-
} catch (err) {
|
|
526
|
-
throw new CliUsageError(`failed to read stdin: ${err.message}`);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
async function tryClipboardCopy(text) {
|
|
530
|
-
try {
|
|
531
|
-
const mod = await import("clipboardy");
|
|
532
|
-
await mod.default.write(text);
|
|
533
|
-
return true;
|
|
534
|
-
} catch {
|
|
535
|
-
return false;
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// src/commands/publish.ts
|
|
540
918
|
function registerPublish(program) {
|
|
541
919
|
program.command("publish").description("Publish an HTML document and get a URL.").argument("[source]", "file path, '-' for stdin (default: stdin if piped)").option("-f, --file <path>", "HTML file path").option("--title <title>", "Optional title").option("--password <password>", "Password-protect the page").option("--expires-in <minutes>", "Minutes until expiry (1\u201310080, i.e. up to 7 days)").option("--no-clipboard", "Don't copy URL to clipboard").option("-q, --quiet", "Print only the URL").action(async function(source, opts) {
|
|
542
920
|
const html = readHtmlFromSource(source, opts.file);
|
|
@@ -573,7 +951,7 @@ function registerPublish(program) {
|
|
|
573
951
|
process.stderr.write(`expires: ${page.expires_at}
|
|
574
952
|
`);
|
|
575
953
|
}
|
|
576
|
-
if (opts.
|
|
954
|
+
if (opts.clipboard !== false) {
|
|
577
955
|
const copied = await tryClipboardCopy(page.url);
|
|
578
956
|
if (copied) process.stderr.write("(URL copied to clipboard)\n");
|
|
579
957
|
}
|
|
@@ -618,6 +996,7 @@ function buildProgram() {
|
|
|
618
996
|
"API base URL (default: https://api.htmlship.com or $HTMLSHIP_API_URL)"
|
|
619
997
|
);
|
|
620
998
|
registerPublish(program);
|
|
999
|
+
registerDeploy(program);
|
|
621
1000
|
registerGet(program);
|
|
622
1001
|
registerUpdate(program);
|
|
623
1002
|
registerDelete(program);
|