emailr-cli 1.5.0 → 1.5.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 +217 -46
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1008,12 +1008,16 @@ Template: ${template.name}`);
1008
1008
  });
1009
1009
  cmd.command("edit <id>").description(`Start a live editing session for a template.
1010
1010
 
1011
- AGENTIC WORKFLOW:
1012
- 1. Run: emailr templates edit <id> --file ./template.html
1011
+ AGENTIC WORKFLOW (for AI agents, use --background):
1012
+ 1. Run: emailr templates edit <id> --file ./template.html --background
1013
1013
  2. Edit the file at ./template.html - browser auto-refreshes on save
1014
1014
  3. When done: emailr templates update <id> --html-file ./template.html
1015
+ 4. Stop server: emailr templates stop-preview
1015
1016
 
1016
- This command fetches the template HTML, saves it locally, and starts a live preview server with hot-reload. Any changes to the file are instantly reflected in the browser.`).option("--file <path>", "Path to save the HTML file for editing", "./template.html").option("--no-open", "Do not automatically open browser").action(async (id, options) => {
1017
+ INTERACTIVE MODE (default):
1018
+ 1. Run: emailr templates edit <id> --file ./template.html
1019
+ 2. Edit the file - browser auto-refreshes on save
1020
+ 3. Press Ctrl+C when done, then run update command`).option("--file <path>", "Path to save the HTML file for editing", "./template.html").option("--no-open", "Do not automatically open browser").option("--background", "Start preview server in background and exit (for AI agents)").action(async (id, options) => {
1017
1021
  try {
1018
1022
  const config = loadConfig();
1019
1023
  const client = new Emailr3({
@@ -1026,6 +1030,95 @@ This command fetches the template HTML, saves it locally, and starts a live prev
1026
1030
  const htmlContent = template.html_content ?? "";
1027
1031
  fs4.writeFileSync(filePath, htmlContent, "utf-8");
1028
1032
  console.log(`Template saved to: ${filePath}`);
1033
+ if (options.background) {
1034
+ const { spawn: spawn2 } = await import("child_process");
1035
+ const os4 = await import("os");
1036
+ const pidFile = path4.join(os4.tmpdir(), "emailr-preview.pid");
1037
+ try {
1038
+ const existingPid = fs4.readFileSync(pidFile, "utf-8").trim();
1039
+ process.kill(parseInt(existingPid, 10), "SIGTERM");
1040
+ } catch {
1041
+ }
1042
+ const serverCode = `
1043
+ const http = require('http');
1044
+ const fs = require('fs');
1045
+ const path = require('path');
1046
+ const os = require('os');
1047
+ const filePath = ${JSON.stringify(filePath)};
1048
+ const pidFile = path.join(os.tmpdir(), 'emailr-preview.pid');
1049
+ const portFile = path.join(os.tmpdir(), 'emailr-preview.port');
1050
+ let clients = [];
1051
+ const script = '<script>(function(){const e=new EventSource("/__live-reload");e.onmessage=function(d){if(d.data==="reload")location.reload()};})();</script>';
1052
+ const server = http.createServer((req, res) => {
1053
+ if (req.url === '/__live-reload') {
1054
+ res.writeHead(200, {'Content-Type':'text/event-stream','Cache-Control':'no-cache','Connection':'keep-alive'});
1055
+ res.write('data: connected\\n\\n');
1056
+ clients.push(res);
1057
+ req.on('close', () => { clients = clients.filter(c => c !== res); });
1058
+ return;
1059
+ }
1060
+ try {
1061
+ let html = fs.readFileSync(filePath, 'utf-8');
1062
+ html = html.includes('</body>') ? html.replace('</body>', script + '</body>') : html + script;
1063
+ res.writeHead(200, {'Content-Type':'text/html; charset=utf-8'});
1064
+ res.end(html);
1065
+ } catch (e) {
1066
+ res.writeHead(500);
1067
+ res.end('Error: ' + e.message);
1068
+ }
1069
+ });
1070
+ server.listen(0, '127.0.0.1', () => {
1071
+ const port = server.address().port;
1072
+ fs.writeFileSync(pidFile, String(process.pid));
1073
+ fs.writeFileSync(portFile, String(port));
1074
+ console.log('PORT:' + port);
1075
+ let timer;
1076
+ fs.watch(filePath, () => {
1077
+ clearTimeout(timer);
1078
+ timer = setTimeout(() => clients.forEach(c => { try { c.write('data: reload\\n\\n'); } catch {} }), 100);
1079
+ });
1080
+ });
1081
+ process.on('SIGTERM', () => { try { fs.unlinkSync(pidFile); fs.unlinkSync(portFile); } catch {} process.exit(0); });
1082
+ `;
1083
+ const child = spawn2("node", ["-e", serverCode], {
1084
+ detached: true,
1085
+ stdio: ["ignore", "pipe", "ignore"]
1086
+ });
1087
+ let port2 = "";
1088
+ await new Promise((resolve) => {
1089
+ child.stdout?.on("data", (data) => {
1090
+ const output2 = data.toString();
1091
+ const match = output2.match(/PORT:(\d+)/);
1092
+ if (match) {
1093
+ port2 = match[1];
1094
+ resolve();
1095
+ }
1096
+ });
1097
+ setTimeout(resolve, 3e3);
1098
+ });
1099
+ child.unref();
1100
+ const previewUrl2 = `http://127.0.0.1:${port2}/`;
1101
+ console.log(`
1102
+ Template: ${template.name}`);
1103
+ console.log(`Template ID: ${template.id}`);
1104
+ console.log(`Live Preview: ${previewUrl2}`);
1105
+ console.log(`File: ${filePath}`);
1106
+ console.log(`
1107
+ Preview server running in background (PID: ${child.pid}).`);
1108
+ console.log(`Edit the file and see live updates in the browser.`);
1109
+ console.log(`
1110
+ When done:`);
1111
+ console.log(` emailr templates update ${id} --html-file ${options.file}`);
1112
+ console.log(` emailr templates stop-preview`);
1113
+ if (options.open !== false) {
1114
+ try {
1115
+ const open = await import("open");
1116
+ await open.default(previewUrl2);
1117
+ } catch {
1118
+ }
1119
+ }
1120
+ process.exit(0);
1121
+ }
1029
1122
  const port = await startLivePreviewServer(filePath, () => {
1030
1123
  console.log("File changed - browser refreshed");
1031
1124
  });
@@ -1034,6 +1127,7 @@ This command fetches the template HTML, saves it locally, and starts a live prev
1034
1127
  Template: ${template.name}`);
1035
1128
  console.log(`Template ID: ${template.id}`);
1036
1129
  console.log(`Live Preview: ${previewUrl}`);
1130
+ console.log(`File: ${filePath}`);
1037
1131
  console.log(`
1038
1132
  Watching for changes... Edit the file and see live updates.`);
1039
1133
  console.log(`When done, run: emailr templates update ${id} --html-file ${options.file}`);
@@ -1061,12 +1155,16 @@ To save changes: emailr templates update ${id} --html-file ${options.file}`);
1061
1155
  });
1062
1156
  cmd.command("draft").description(`Start a live drafting session for a new template.
1063
1157
 
1064
- AGENTIC WORKFLOW:
1065
- 1. Run: emailr templates draft --file ./new-template.html
1158
+ AGENTIC WORKFLOW (for AI agents, use --background):
1159
+ 1. Run: emailr templates draft --file ./new-template.html --background
1066
1160
  2. Edit the file at ./new-template.html - browser auto-refreshes on save
1067
1161
  3. When done: emailr templates create --name "My Template" --subject "Subject" --html-file ./new-template.html
1162
+ 4. Stop server: emailr templates stop-preview
1068
1163
 
1069
- This command creates a starter HTML file and starts a live preview server with hot-reload. Perfect for building new templates from scratch with instant visual feedback.`).option("--file <path>", "Path to save the HTML file for drafting", "./new-template.html").option("--no-open", "Do not automatically open browser").option("--blank", "Start with a blank file instead of starter template").action(async (options) => {
1164
+ INTERACTIVE MODE (default):
1165
+ 1. Run: emailr templates draft --file ./new-template.html
1166
+ 2. Edit the file - browser auto-refreshes on save
1167
+ 3. Press Ctrl+C when done, then run create command`).option("--file <path>", "Path to save the HTML file for drafting", "./new-template.html").option("--no-open", "Do not automatically open browser").option("--blank", "Start with a blank file instead of starter template").option("--background", "Start preview server in background and exit (for AI agents)").action(async (options) => {
1070
1168
  try {
1071
1169
  const filePath = path4.resolve(options.file);
1072
1170
  let htmlContent;
@@ -1088,51 +1186,21 @@ This command creates a starter HTML file and starts a live preview server with h
1088
1186
  margin: 0 auto;
1089
1187
  padding: 20px;
1090
1188
  }
1091
- .header {
1092
- text-align: center;
1093
- padding: 20px 0;
1094
- border-bottom: 1px solid #eee;
1095
- }
1096
- .content {
1097
- padding: 30px 0;
1098
- }
1099
- .button {
1100
- display: inline-block;
1101
- padding: 12px 24px;
1102
- background-color: #5e5de7;
1103
- color: white;
1104
- text-decoration: none;
1105
- border-radius: 5px;
1106
- font-weight: bold;
1107
- }
1108
- .footer {
1109
- text-align: center;
1110
- padding: 20px 0;
1111
- border-top: 1px solid #eee;
1112
- color: #888;
1113
- font-size: 12px;
1114
- }
1189
+ .header { text-align: center; padding: 20px 0; border-bottom: 1px solid #eee; }
1190
+ .content { padding: 30px 0; }
1191
+ .button { display: inline-block; padding: 12px 24px; background-color: #5e5de7; color: white; text-decoration: none; border-radius: 5px; font-weight: bold; }
1192
+ .footer { text-align: center; padding: 20px 0; border-top: 1px solid #eee; color: #888; font-size: 12px; }
1115
1193
  </style>
1116
1194
  </head>
1117
1195
  <body>
1118
- <div class="header">
1119
- <h1>Your Company</h1>
1120
- </div>
1121
-
1196
+ <div class="header"><h1>Your Company</h1></div>
1122
1197
  <div class="content">
1123
1198
  <h2>Hello {{name}},</h2>
1124
-
1125
1199
  <p>This is a starter template. Edit this file and see your changes live in the browser!</p>
1126
-
1127
1200
  <p>You can use variables like <code>{{name}}</code> that will be replaced when sending emails.</p>
1128
-
1129
- <p style="text-align: center; margin: 30px 0;">
1130
- <a href="https://example.com" class="button">Call to Action</a>
1131
- </p>
1132
-
1201
+ <p style="text-align: center; margin: 30px 0;"><a href="https://example.com" class="button">Call to Action</a></p>
1133
1202
  <p>Best regards,<br>Your Team</p>
1134
1203
  </div>
1135
-
1136
1204
  <div class="footer">
1137
1205
  <p>\xA9 2024 Your Company. All rights reserved.</p>
1138
1206
  <p><a href="{{unsubscribe_link}}">Unsubscribe</a></p>
@@ -1142,12 +1210,99 @@ This command creates a starter HTML file and starts a live preview server with h
1142
1210
  }
1143
1211
  fs4.writeFileSync(filePath, htmlContent, "utf-8");
1144
1212
  console.log(`Template draft created: ${filePath}`);
1213
+ if (options.background) {
1214
+ const { spawn: spawn2 } = await import("child_process");
1215
+ const os4 = await import("os");
1216
+ const pidFile = path4.join(os4.tmpdir(), "emailr-preview.pid");
1217
+ try {
1218
+ const existingPid = fs4.readFileSync(pidFile, "utf-8").trim();
1219
+ process.kill(parseInt(existingPid, 10), "SIGTERM");
1220
+ } catch {
1221
+ }
1222
+ const serverCode = `
1223
+ const http = require('http');
1224
+ const fs = require('fs');
1225
+ const path = require('path');
1226
+ const os = require('os');
1227
+ const filePath = ${JSON.stringify(filePath)};
1228
+ const pidFile = path.join(os.tmpdir(), 'emailr-preview.pid');
1229
+ const portFile = path.join(os.tmpdir(), 'emailr-preview.port');
1230
+ let clients = [];
1231
+ const script = '<script>(function(){const e=new EventSource("/__live-reload");e.onmessage=function(d){if(d.data==="reload")location.reload()};})();</script>';
1232
+ const server = http.createServer((req, res) => {
1233
+ if (req.url === '/__live-reload') {
1234
+ res.writeHead(200, {'Content-Type':'text/event-stream','Cache-Control':'no-cache','Connection':'keep-alive'});
1235
+ res.write('data: connected\\n\\n');
1236
+ clients.push(res);
1237
+ req.on('close', () => { clients = clients.filter(c => c !== res); });
1238
+ return;
1239
+ }
1240
+ try {
1241
+ let html = fs.readFileSync(filePath, 'utf-8');
1242
+ html = html.includes('</body>') ? html.replace('</body>', script + '</body>') : html + script;
1243
+ res.writeHead(200, {'Content-Type':'text/html; charset=utf-8'});
1244
+ res.end(html);
1245
+ } catch (e) {
1246
+ res.writeHead(500);
1247
+ res.end('Error: ' + e.message);
1248
+ }
1249
+ });
1250
+ server.listen(0, '127.0.0.1', () => {
1251
+ const port = server.address().port;
1252
+ fs.writeFileSync(pidFile, String(process.pid));
1253
+ fs.writeFileSync(portFile, String(port));
1254
+ console.log('PORT:' + port);
1255
+ let timer;
1256
+ fs.watch(filePath, () => {
1257
+ clearTimeout(timer);
1258
+ timer = setTimeout(() => clients.forEach(c => { try { c.write('data: reload\\n\\n'); } catch {} }), 100);
1259
+ });
1260
+ });
1261
+ process.on('SIGTERM', () => { try { fs.unlinkSync(pidFile); fs.unlinkSync(portFile); } catch {} process.exit(0); });
1262
+ `;
1263
+ const child = spawn2("node", ["-e", serverCode], {
1264
+ detached: true,
1265
+ stdio: ["ignore", "pipe", "ignore"]
1266
+ });
1267
+ let port2 = "";
1268
+ await new Promise((resolve) => {
1269
+ child.stdout?.on("data", (data) => {
1270
+ const match = data.toString().match(/PORT:(\d+)/);
1271
+ if (match) {
1272
+ port2 = match[1];
1273
+ resolve();
1274
+ }
1275
+ });
1276
+ setTimeout(resolve, 3e3);
1277
+ });
1278
+ child.unref();
1279
+ const previewUrl2 = `http://127.0.0.1:${port2}/`;
1280
+ console.log(`
1281
+ Live Preview: ${previewUrl2}`);
1282
+ console.log(`File: ${filePath}`);
1283
+ console.log(`
1284
+ Preview server running in background (PID: ${child.pid}).`);
1285
+ console.log(`Edit the file and see live updates in the browser.`);
1286
+ console.log(`
1287
+ When done:`);
1288
+ console.log(` emailr templates create --name "Template Name" --subject "Email Subject" --html-file ${options.file}`);
1289
+ console.log(` emailr templates stop-preview`);
1290
+ if (options.open !== false) {
1291
+ try {
1292
+ const open = await import("open");
1293
+ await open.default(previewUrl2);
1294
+ } catch {
1295
+ }
1296
+ }
1297
+ process.exit(0);
1298
+ }
1145
1299
  const port = await startLivePreviewServer(filePath, () => {
1146
1300
  console.log("File changed - browser refreshed");
1147
1301
  });
1148
1302
  const previewUrl = `http://127.0.0.1:${port}/`;
1149
1303
  console.log(`
1150
1304
  Live Preview: ${previewUrl}`);
1305
+ console.log(`File: ${filePath}`);
1151
1306
  console.log(`
1152
1307
  Watching for changes... Edit the file and see live updates.`);
1153
1308
  console.log(`
@@ -1175,6 +1330,18 @@ To create template: emailr templates create --name "Template Name" --subject "Em
1175
1330
  process.exit(1);
1176
1331
  }
1177
1332
  });
1333
+ cmd.command("stop-preview").description("Stop any running background preview server").action(async () => {
1334
+ const os4 = await import("os");
1335
+ const pidFile = path4.join(os4.tmpdir(), "emailr-preview.pid");
1336
+ try {
1337
+ const pid = fs4.readFileSync(pidFile, "utf-8").trim();
1338
+ process.kill(parseInt(pid, 10), "SIGTERM");
1339
+ fs4.unlinkSync(pidFile);
1340
+ success("Preview server stopped");
1341
+ } catch {
1342
+ success("No preview server running");
1343
+ }
1344
+ });
1178
1345
  return cmd;
1179
1346
  }
1180
1347
 
@@ -2341,15 +2508,19 @@ description: Operate the Emailr CLI to send emails, manage contacts, templates,
2341
2508
 
2342
2509
  ## LIVE TEMPLATE EDITING (Agentic Workflow)
2343
2510
 
2511
+ IMPORTANT: Always use \`--background\` flag so the command returns immediately and you can continue editing.
2512
+
2344
2513
  ### Edit existing template with live preview
2345
- 1. Start live editing: \`emailr templates edit <template_id> --file ./template.html\`
2346
- 2. Edit the file - browser auto-refreshes on every save
2514
+ 1. Start live editing: \`emailr templates edit <template_id> --file ./template.html --background\`
2515
+ 2. Edit the file at ./template.html - browser auto-refreshes on every save
2347
2516
  3. When satisfied: \`emailr templates update <template_id> --html-file ./template.html\`
2517
+ 4. Stop server: \`emailr templates stop-preview\`
2348
2518
 
2349
2519
  ### Create new template with live preview
2350
- 1. Start drafting: \`emailr templates draft --file ./new-template.html\`
2351
- 2. Edit the file - browser auto-refreshes on every save
2520
+ 1. Start drafting: \`emailr templates draft --file ./new-template.html --background\`
2521
+ 2. Edit the file at ./new-template.html - browser auto-refreshes on every save
2352
2522
  3. When satisfied: \`emailr templates create --name "Template Name" --subject "Subject" --html-file ./new-template.html\`
2523
+ 4. Stop server: \`emailr templates stop-preview\`
2353
2524
 
2354
2525
  ## Common commands
2355
2526
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emailr-cli",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Command-line interface for the Emailr email API",
5
5
  "type": "module",
6
6
  "bin": {