emailr-cli 1.2.1 → 1.3.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 (2) hide show
  1. package/dist/index.js +175 -10
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -343,6 +343,8 @@ Total: ${result.total}`);
343
343
  // src/commands/templates.ts
344
344
  import { Command as Command3 } from "commander";
345
345
  import { Emailr as Emailr3 } from "emailr";
346
+ import fs4 from "fs";
347
+ import path4 from "path";
346
348
 
347
349
  // src/server/template-store.ts
348
350
  import fs2 from "fs";
@@ -625,6 +627,123 @@ function keepServerAlive() {
625
627
  }
626
628
  }
627
629
 
630
+ // src/server/live-preview-server.ts
631
+ import http2 from "http";
632
+ import fs3 from "fs";
633
+ import path3 from "path";
634
+ var serverInstance2 = null;
635
+ var serverPort2 = null;
636
+ var fileWatcher = null;
637
+ var sseClients = [];
638
+ var LIVE_RELOAD_SCRIPT = `
639
+ <script>
640
+ (function() {
641
+ const evtSource = new EventSource('/__live-reload');
642
+ evtSource.onmessage = function(e) {
643
+ if (e.data === 'reload') {
644
+ window.location.reload();
645
+ }
646
+ };
647
+ evtSource.onerror = function() {
648
+ console.log('Live reload disconnected');
649
+ };
650
+ })();
651
+ </script>
652
+ `;
653
+ function injectLiveReload(html) {
654
+ if (html.includes("</body>")) {
655
+ return html.replace("</body>", `${LIVE_RELOAD_SCRIPT}</body>`);
656
+ }
657
+ return html + LIVE_RELOAD_SCRIPT;
658
+ }
659
+ function notifyReload() {
660
+ sseClients.forEach((client) => {
661
+ try {
662
+ client.write("data: reload\n\n");
663
+ } catch {
664
+ }
665
+ });
666
+ }
667
+ async function startLivePreviewServer(filePath, onReload) {
668
+ const absolutePath = path3.resolve(filePath);
669
+ return new Promise((resolve, reject) => {
670
+ serverInstance2 = http2.createServer((req, res) => {
671
+ if (req.url === "/__live-reload") {
672
+ res.writeHead(200, {
673
+ "Content-Type": "text/event-stream",
674
+ "Cache-Control": "no-cache",
675
+ "Connection": "keep-alive",
676
+ "Access-Control-Allow-Origin": "*"
677
+ });
678
+ res.write("data: connected\n\n");
679
+ sseClients.push(res);
680
+ req.on("close", () => {
681
+ sseClients = sseClients.filter((c) => c !== res);
682
+ });
683
+ return;
684
+ }
685
+ if (req.method === "GET") {
686
+ try {
687
+ const html = fs3.readFileSync(absolutePath, "utf-8");
688
+ const injectedHtml = injectLiveReload(html);
689
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
690
+ res.end(injectedHtml);
691
+ } catch (err) {
692
+ res.writeHead(500, { "Content-Type": "text/plain" });
693
+ res.end(`Error reading file: ${err instanceof Error ? err.message : String(err)}`);
694
+ }
695
+ return;
696
+ }
697
+ res.writeHead(405, { "Content-Type": "text/plain" });
698
+ res.end("Method Not Allowed");
699
+ });
700
+ serverInstance2.listen(0, "127.0.0.1", () => {
701
+ const address = serverInstance2.address();
702
+ if (address && typeof address === "object") {
703
+ serverPort2 = address.port;
704
+ let debounceTimer = null;
705
+ fileWatcher = fs3.watch(absolutePath, (eventType) => {
706
+ if (eventType === "change") {
707
+ if (debounceTimer) clearTimeout(debounceTimer);
708
+ debounceTimer = setTimeout(() => {
709
+ notifyReload();
710
+ onReload?.();
711
+ }, 100);
712
+ }
713
+ });
714
+ resolve(serverPort2);
715
+ } else {
716
+ reject(new Error("Failed to get server address"));
717
+ }
718
+ });
719
+ serverInstance2.on("error", (err) => {
720
+ reject(new Error(`Failed to start server: ${err.message}`));
721
+ });
722
+ });
723
+ }
724
+ async function stopLivePreviewServer() {
725
+ if (fileWatcher) {
726
+ fileWatcher.close();
727
+ fileWatcher = null;
728
+ }
729
+ sseClients.forEach((client) => {
730
+ try {
731
+ client.end();
732
+ } catch {
733
+ }
734
+ });
735
+ sseClients = [];
736
+ if (serverInstance2) {
737
+ return new Promise((resolve) => {
738
+ serverInstance2.close(() => {
739
+ serverInstance2 = null;
740
+ serverPort2 = null;
741
+ resolve();
742
+ });
743
+ });
744
+ }
745
+ }
746
+
628
747
  // src/commands/templates.ts
629
748
  async function handleTemplatePreview(template) {
630
749
  try {
@@ -725,14 +844,14 @@ Total: ${result.length}`);
725
844
  subject: options.subject
726
845
  };
727
846
  if (options.htmlFile) {
728
- const fs3 = await import("fs");
729
- request.html_content = fs3.readFileSync(options.htmlFile, "utf-8");
847
+ const fs5 = await import("fs");
848
+ request.html_content = fs5.readFileSync(options.htmlFile, "utf-8");
730
849
  } else if (options.html) {
731
850
  request.html_content = options.html;
732
851
  }
733
852
  if (options.textFile) {
734
- const fs3 = await import("fs");
735
- request.text_content = fs3.readFileSync(options.textFile, "utf-8");
853
+ const fs5 = await import("fs");
854
+ request.text_content = fs5.readFileSync(options.textFile, "utf-8");
736
855
  } else if (options.text) {
737
856
  request.text_content = options.text;
738
857
  }
@@ -782,14 +901,14 @@ Open the Preview URL in your browser to view the rendered template.`);
782
901
  if (options.name) request.name = options.name;
783
902
  if (options.subject) request.subject = options.subject;
784
903
  if (options.htmlFile) {
785
- const fs3 = await import("fs");
786
- request.html_content = fs3.readFileSync(options.htmlFile, "utf-8");
904
+ const fs5 = await import("fs");
905
+ request.html_content = fs5.readFileSync(options.htmlFile, "utf-8");
787
906
  } else if (options.html) {
788
907
  request.html_content = options.html;
789
908
  }
790
909
  if (options.textFile) {
791
- const fs3 = await import("fs");
792
- request.text_content = fs3.readFileSync(options.textFile, "utf-8");
910
+ const fs5 = await import("fs");
911
+ request.text_content = fs5.readFileSync(options.textFile, "utf-8");
793
912
  } else if (options.text) {
794
913
  request.text_content = options.text;
795
914
  }
@@ -867,6 +986,52 @@ Template: ${template.name}`);
867
986
  process.exit(1);
868
987
  }
869
988
  });
989
+ cmd.command("edit <id>").description("Edit a template with live preview - saves HTML to a local file for editing").option("--file <path>", "Path to save the HTML file", "./template.html").option("--no-open", "Do not automatically open browser").action(async (id, options) => {
990
+ try {
991
+ const config = loadConfig();
992
+ const client = new Emailr3({
993
+ apiKey: config.apiKey,
994
+ baseUrl: config.baseUrl
995
+ });
996
+ const filePath = path4.resolve(options.file);
997
+ console.log(`Fetching template ${id}...`);
998
+ const template = await client.templates.get(id);
999
+ const htmlContent = template.html_content ?? "";
1000
+ fs4.writeFileSync(filePath, htmlContent, "utf-8");
1001
+ console.log(`Template saved to: ${filePath}`);
1002
+ const port = await startLivePreviewServer(filePath, () => {
1003
+ console.log("File changed - browser refreshed");
1004
+ });
1005
+ const previewUrl = `http://127.0.0.1:${port}/`;
1006
+ console.log(`
1007
+ Template: ${template.name}`);
1008
+ console.log(`Template ID: ${template.id}`);
1009
+ console.log(`Live Preview: ${previewUrl}`);
1010
+ console.log(`
1011
+ Watching for changes... Edit the file and see live updates.`);
1012
+ console.log(`When done, run: emailr templates update ${id} --html-file ${options.file}`);
1013
+ console.log(`
1014
+ Press Ctrl+C to stop.`);
1015
+ if (options.open !== false) {
1016
+ try {
1017
+ const open = await import("open");
1018
+ await open.default(previewUrl);
1019
+ } catch {
1020
+ }
1021
+ }
1022
+ process.on("SIGINT", async () => {
1023
+ console.log("\n\nStopping live preview server...");
1024
+ await stopLivePreviewServer();
1025
+ console.log("Done.");
1026
+ console.log(`
1027
+ To save changes: emailr templates update ${id} --html-file ${options.file}`);
1028
+ process.exit(0);
1029
+ });
1030
+ } catch (err) {
1031
+ error(err instanceof Error ? err.message : "Failed to edit template");
1032
+ process.exit(1);
1033
+ }
1034
+ });
870
1035
  return cmd;
871
1036
  }
872
1037
 
@@ -1680,7 +1845,7 @@ function createSegmentsCommand() {
1680
1845
  import { Command as Command9 } from "commander";
1681
1846
 
1682
1847
  // src/server/callback.ts
1683
- import http2 from "http";
1848
+ import http3 from "http";
1684
1849
  import { URL } from "url";
1685
1850
  function parseCallbackParams(url) {
1686
1851
  try {
@@ -1813,7 +1978,7 @@ function createCallbackServer() {
1813
1978
  return {
1814
1979
  async start() {
1815
1980
  return new Promise((resolve, reject) => {
1816
- server = http2.createServer((req, res) => {
1981
+ server = http3.createServer((req, res) => {
1817
1982
  if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
1818
1983
  res.writeHead(404, { "Content-Type": "text/plain" });
1819
1984
  res.end("Not Found");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emailr-cli",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Command-line interface for the Emailr email API",
5
5
  "type": "module",
6
6
  "bin": {