htmlhost-cli 1.5.1 → 1.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "htmlhost-cli",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "Deploy HTML files from the terminal — htmlhost.co CLI",
5
5
  "type": "module",
6
6
  "bin": {
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 = "1.5.0";
7
+ const VERSION = "1.6.0";
8
8
 
9
9
  const HELP = `
10
10
  ${bold("htmlhost")} ${dim(`v${VERSION}`)} — deploy HTML from the terminal
@@ -50,10 +50,11 @@ function promptChoice(question, options) {
50
50
  }
51
51
 
52
52
  /**
53
- * htmlhost deploy [file|dir] [--ttl 7d] [--slug existing-slug] [--title "My Site"] [--new] [--no-assets]
53
+ * htmlhost deploy [file|dir] [--ttl 7d] [--slug existing-slug] [--title "My Site"] [--new] [--no-assets] [--pages]
54
54
  *
55
55
  * - Defaults to index.html in the current directory
56
56
  * - If a directory (or ".") is given, deploys all *.html files as separate linked sites
57
+ * - Use --pages with a directory to deploy all *.html files as pages within ONE multi-page site
57
58
  * - Remembers the site(s) via .htmlhost file (auto re-deploy)
58
59
  * - Use --new to force fresh deploys
59
60
  */
@@ -64,6 +65,7 @@ export async function deploy(args) {
64
65
  const title = getFlag(args, "--title");
65
66
  const forceNew = args.includes("--new");
66
67
  const skipAssets = args.includes("--no-assets");
68
+ const multiPage = args.includes("--pages");
67
69
 
68
70
  // Check if the target is a directory
69
71
  if (file) {
@@ -71,6 +73,9 @@ export async function deploy(args) {
71
73
  try {
72
74
  const stat = statSync(filePath);
73
75
  if (stat.isDirectory()) {
76
+ if (multiPage) {
77
+ return deployMultiPage(filePath, { ttl, title, forceNew, skipAssets });
78
+ }
74
79
  return deployDirectory(filePath, { ttl, forceNew, skipAssets });
75
80
  }
76
81
  } catch {
@@ -78,6 +83,11 @@ export async function deploy(args) {
78
83
  }
79
84
  }
80
85
 
86
+ // --pages without explicit dir defaults to cwd
87
+ if (multiPage && !file) {
88
+ return deployMultiPage(resolve("."), { ttl, title, forceNew, skipAssets });
89
+ }
90
+
81
91
  // Default to index.html in the current directory
82
92
  if (!file) {
83
93
  const defaultFile = resolve("index.html");
@@ -381,6 +391,144 @@ async function deployDirectory(dirPath, { ttl, forceNew, skipAssets }) {
381
391
  console.log("");
382
392
  }
383
393
 
394
+ /**
395
+ * Deploy all *.html files in a directory as pages within a single multi-page site.
396
+ * Uses the --pages flag. File names map to URL paths:
397
+ * index.html → /
398
+ * about.html → /about
399
+ * blog/index.html → /blog
400
+ * blog/post.html → /blog/post
401
+ */
402
+ async function deployMultiPage(dirPath, { ttl, title, forceNew, skipAssets }) {
403
+ // Find all .html files (recursive) — skip dotfiles
404
+ const htmlFiles = findHtmlFiles(dirPath, dirPath);
405
+
406
+ if (htmlFiles.length === 0) {
407
+ err("No .html files found in this directory.");
408
+ process.exit(1);
409
+ }
410
+
411
+ // Read existing link data
412
+ const link = readLink(dirPath);
413
+ const existingSlug = (!forceNew && link?.multipage?.slug) || null;
414
+
415
+ console.log("");
416
+ info(`Multi-page deploy: ${cyan(bold(String(htmlFiles.length)))} page${htmlFiles.length > 1 ? "s" : ""} in ${cyan(basename(dirPath) || ".")}`);
417
+ console.log("");
418
+
419
+ // Show page mapping
420
+ for (const { relativePath, pagePath } of htmlFiles) {
421
+ console.log(` ${dim("📄")} ${relativePath} → ${cyan(pagePath)}`);
422
+ }
423
+ console.log("");
424
+
425
+ // If we have a linked slug, confirm overwrite
426
+ if (existingSlug) {
427
+ info(`Linked to ${cyan(existingSlug + ".htmlhost.co")} ${dim("(multi-page)")}`);
428
+ }
429
+
430
+ // Process assets and build page payloads
431
+ const assetCache = new Map();
432
+ const pagePayloads = [];
433
+
434
+ for (const { filePath, pagePath, relativePath } of htmlFiles) {
435
+ const stat = statSync(filePath);
436
+ info(`${bold(relativePath)} ${dim(`(${formatBytes(stat.size)})`)}`);
437
+
438
+ let html = readFileSync(filePath, "utf8");
439
+
440
+ if (!skipAssets) {
441
+ html = await processAssets(html, dirname(filePath), assetCache);
442
+ }
443
+
444
+ pagePayloads.push({
445
+ path: pagePath,
446
+ html,
447
+ });
448
+ }
449
+
450
+ // Deploy all pages as one multi-page site
451
+ const body = { pages: pagePayloads };
452
+ if (ttl) body.ttl = ttl;
453
+ if (existingSlug) body.slug = existingSlug;
454
+ if (title) body.title = title;
455
+
456
+ info(existingSlug ? `Re-deploying to ${cyan(existingSlug)}…` : "Deploying new multi-page site…");
457
+
458
+ try {
459
+ const data = await post("/api/sites", body);
460
+
461
+ // Save link
462
+ writeLink(dirPath, {
463
+ multipage: {
464
+ slug: data.slug,
465
+ url: data.url,
466
+ pageCount: data.pageCount || pagePayloads.length,
467
+ },
468
+ });
469
+
470
+ console.log("");
471
+ console.log(` ${green("━".repeat(40))}`);
472
+ ok(`${bold("Live")} at ${cyan(`https://${data.url}`)}`);
473
+ console.log(` ${dim(`${data.pageCount || pagePayloads.length} pages · ${data.ttl} TTL`)}`);
474
+ console.log("");
475
+
476
+ for (const { pagePath } of htmlFiles) {
477
+ const urlPath = pagePath === "/" ? "" : pagePath;
478
+ console.log(` ${cyan(`https://${data.url}${urlPath}`)}`);
479
+ }
480
+ console.log("");
481
+ console.log(` ${dim("Linked → .htmlhost (multipage)")}`);
482
+ console.log("");
483
+ } catch (e) {
484
+ err(`Deploy failed: ${e.message}`);
485
+ process.exit(1);
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Recursively find all .html files and derive their page paths.
491
+ * Returns [{ filePath, relativePath, pagePath }]
492
+ */
493
+ function findHtmlFiles(baseDir, currentDir) {
494
+ const entries = readdirSync(currentDir, { withFileTypes: true });
495
+ const results = [];
496
+
497
+ for (const entry of entries) {
498
+ if (entry.name.startsWith(".")) continue;
499
+
500
+ const fullPath = join(currentDir, entry.name);
501
+
502
+ if (entry.isDirectory()) {
503
+ results.push(...findHtmlFiles(baseDir, fullPath));
504
+ } else if (entry.name.endsWith(".html")) {
505
+ const relativePath = fullPath.slice(baseDir.length + 1); // e.g. "about.html" or "blog/post.html"
506
+
507
+ // Derive page path from file path
508
+ let pagePath;
509
+ if (entry.name === "index.html") {
510
+ // index.html → parent directory path (or "/" for root)
511
+ const dirRel = dirname(relativePath);
512
+ pagePath = dirRel === "." ? "/" : `/${dirRel}`;
513
+ } else {
514
+ // about.html → /about, blog/post.html → /blog/post
515
+ pagePath = "/" + relativePath.replace(/\.html$/, "");
516
+ }
517
+
518
+ results.push({ filePath: fullPath, relativePath, pagePath });
519
+ }
520
+ }
521
+
522
+ // Sort: root index first, then alphabetical
523
+ results.sort((a, b) => {
524
+ if (a.pagePath === "/") return -1;
525
+ if (b.pagePath === "/") return 1;
526
+ return a.pagePath.localeCompare(b.pagePath);
527
+ });
528
+
529
+ return results;
530
+ }
531
+
384
532
  function getFlag(args, flag) {
385
533
  const idx = args.indexOf(flag);
386
534
  if (idx === -1 || idx >= args.length - 1) return null;