emailr-cli 1.2.1 → 1.3.1

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 +183 -11
  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
  }
@@ -801,7 +920,14 @@ Open the Preview URL in your browser to view the rendered template.`);
801
920
  output(template, "json");
802
921
  } else {
803
922
  success(`Template updated: ${template.id}`);
804
- output(template, "table");
923
+ const tableData = {
924
+ ID: template.id,
925
+ Name: template.name,
926
+ Subject: template.subject,
927
+ Variables: template.variables?.join(", ") || "-",
928
+ Updated: template.updated_at
929
+ };
930
+ output(tableData, "table");
805
931
  }
806
932
  } catch (err) {
807
933
  error(err instanceof Error ? err.message : "Failed to update template");
@@ -867,6 +993,52 @@ Template: ${template.name}`);
867
993
  process.exit(1);
868
994
  }
869
995
  });
996
+ 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) => {
997
+ try {
998
+ const config = loadConfig();
999
+ const client = new Emailr3({
1000
+ apiKey: config.apiKey,
1001
+ baseUrl: config.baseUrl
1002
+ });
1003
+ const filePath = path4.resolve(options.file);
1004
+ console.log(`Fetching template ${id}...`);
1005
+ const template = await client.templates.get(id);
1006
+ const htmlContent = template.html_content ?? "";
1007
+ fs4.writeFileSync(filePath, htmlContent, "utf-8");
1008
+ console.log(`Template saved to: ${filePath}`);
1009
+ const port = await startLivePreviewServer(filePath, () => {
1010
+ console.log("File changed - browser refreshed");
1011
+ });
1012
+ const previewUrl = `http://127.0.0.1:${port}/`;
1013
+ console.log(`
1014
+ Template: ${template.name}`);
1015
+ console.log(`Template ID: ${template.id}`);
1016
+ console.log(`Live Preview: ${previewUrl}`);
1017
+ console.log(`
1018
+ Watching for changes... Edit the file and see live updates.`);
1019
+ console.log(`When done, run: emailr templates update ${id} --html-file ${options.file}`);
1020
+ console.log(`
1021
+ Press Ctrl+C to stop.`);
1022
+ if (options.open !== false) {
1023
+ try {
1024
+ const open = await import("open");
1025
+ await open.default(previewUrl);
1026
+ } catch {
1027
+ }
1028
+ }
1029
+ process.on("SIGINT", async () => {
1030
+ console.log("\n\nStopping live preview server...");
1031
+ await stopLivePreviewServer();
1032
+ console.log("Done.");
1033
+ console.log(`
1034
+ To save changes: emailr templates update ${id} --html-file ${options.file}`);
1035
+ process.exit(0);
1036
+ });
1037
+ } catch (err) {
1038
+ error(err instanceof Error ? err.message : "Failed to edit template");
1039
+ process.exit(1);
1040
+ }
1041
+ });
870
1042
  return cmd;
871
1043
  }
872
1044
 
@@ -1680,7 +1852,7 @@ function createSegmentsCommand() {
1680
1852
  import { Command as Command9 } from "commander";
1681
1853
 
1682
1854
  // src/server/callback.ts
1683
- import http2 from "http";
1855
+ import http3 from "http";
1684
1856
  import { URL } from "url";
1685
1857
  function parseCallbackParams(url) {
1686
1858
  try {
@@ -1813,7 +1985,7 @@ function createCallbackServer() {
1813
1985
  return {
1814
1986
  async start() {
1815
1987
  return new Promise((resolve, reject) => {
1816
- server = http2.createServer((req, res) => {
1988
+ server = http3.createServer((req, res) => {
1817
1989
  if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
1818
1990
  res.writeHead(404, { "Content-Type": "text/plain" });
1819
1991
  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.1",
4
4
  "description": "Command-line interface for the Emailr email API",
5
5
  "type": "module",
6
6
  "bin": {