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.
Files changed (3) hide show
  1. package/README.md +13 -1
  2. package/dist/cli.js +428 -49
  3. 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 three tools: `publish_html` (with optional `password`), `fetch_html`, `update_html`.
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.1.5";
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"] ?? join(homedir(), ".htmlship");
336
- const file = join(dir, "keys.json");
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 (!existsSync(file)) return {};
644
+ if (!existsSync2(file)) return {};
339
645
  try {
340
- const text = readFileSync(file, "utf8");
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 (!existsSync(dir)) mkdirSync(dir, { recursive: true });
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.noClipboard !== true) {
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "htmlship",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "Host and share HTML pages from LLMs and coding agents in one line. CLI + MCP server.",
5
5
  "keywords": [
6
6
  "html",