emailr-cli 1.4.2 → 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.
- package/dist/index.js +357 -52
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command11 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/send.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -854,14 +854,14 @@ It creates a local file with hot-reload preview, then use this command to save.`
|
|
|
854
854
|
subject: options.subject
|
|
855
855
|
};
|
|
856
856
|
if (options.htmlFile) {
|
|
857
|
-
const
|
|
858
|
-
request.html_content =
|
|
857
|
+
const fs6 = await import("fs");
|
|
858
|
+
request.html_content = fs6.readFileSync(options.htmlFile, "utf-8");
|
|
859
859
|
} else if (options.html) {
|
|
860
860
|
request.html_content = options.html;
|
|
861
861
|
}
|
|
862
862
|
if (options.textFile) {
|
|
863
|
-
const
|
|
864
|
-
request.text_content =
|
|
863
|
+
const fs6 = await import("fs");
|
|
864
|
+
request.text_content = fs6.readFileSync(options.textFile, "utf-8");
|
|
865
865
|
} else if (options.text) {
|
|
866
866
|
request.text_content = options.text;
|
|
867
867
|
}
|
|
@@ -914,14 +914,14 @@ It downloads the template with hot-reload preview, then use this command to save
|
|
|
914
914
|
if (options.name) request.name = options.name;
|
|
915
915
|
if (options.subject) request.subject = options.subject;
|
|
916
916
|
if (options.htmlFile) {
|
|
917
|
-
const
|
|
918
|
-
request.html_content =
|
|
917
|
+
const fs6 = await import("fs");
|
|
918
|
+
request.html_content = fs6.readFileSync(options.htmlFile, "utf-8");
|
|
919
919
|
} else if (options.html) {
|
|
920
920
|
request.html_content = options.html;
|
|
921
921
|
}
|
|
922
922
|
if (options.textFile) {
|
|
923
|
-
const
|
|
924
|
-
request.text_content =
|
|
923
|
+
const fs6 = await import("fs");
|
|
924
|
+
request.text_content = fs6.readFileSync(options.textFile, "utf-8");
|
|
925
925
|
} else if (options.text) {
|
|
926
926
|
request.text_content = options.text;
|
|
927
927
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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
|
|
|
@@ -2322,8 +2489,145 @@ async function executeLogin(options) {
|
|
|
2322
2489
|
}
|
|
2323
2490
|
}
|
|
2324
2491
|
|
|
2492
|
+
// src/commands/agent.ts
|
|
2493
|
+
import { Command as Command10 } from "commander";
|
|
2494
|
+
import { spawn, execSync } from "child_process";
|
|
2495
|
+
import fs5 from "fs";
|
|
2496
|
+
import path5 from "path";
|
|
2497
|
+
import os3 from "os";
|
|
2498
|
+
var OPENCODE_SKILL_DIR = path5.join(os3.homedir(), ".config", "opencode", "skills", "emailr-cli");
|
|
2499
|
+
var SKILL_MD = `---
|
|
2500
|
+
name: emailr-cli
|
|
2501
|
+
description: Operate the Emailr CLI to send emails, manage contacts, templates, domains, broadcasts, webhooks, and segments. Includes LIVE PREVIEW editing for templates with hot-reload.
|
|
2502
|
+
---
|
|
2503
|
+
# Emailr CLI
|
|
2504
|
+
|
|
2505
|
+
## Quick start
|
|
2506
|
+
- Ensure auth: \`emailr login\` (browser-based) or \`emailr config set api-key <token>\`.
|
|
2507
|
+
- Prefer machine-readable output with \`--format json\` on listing/get commands.
|
|
2508
|
+
|
|
2509
|
+
## LIVE TEMPLATE EDITING (Agentic Workflow)
|
|
2510
|
+
|
|
2511
|
+
IMPORTANT: Always use \`--background\` flag so the command returns immediately and you can continue editing.
|
|
2512
|
+
|
|
2513
|
+
### Edit existing template with live preview
|
|
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
|
|
2516
|
+
3. When satisfied: \`emailr templates update <template_id> --html-file ./template.html\`
|
|
2517
|
+
4. Stop server: \`emailr templates stop-preview\`
|
|
2518
|
+
|
|
2519
|
+
### Create new template with live preview
|
|
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
|
|
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\`
|
|
2524
|
+
|
|
2525
|
+
## Common commands
|
|
2526
|
+
|
|
2527
|
+
### Sending emails
|
|
2528
|
+
- Plain text: \`emailr send --to user@example.com --from no-reply@example.com --subject "Hi" --text "Hello"\`
|
|
2529
|
+
- HTML: \`emailr send --to user@example.com --from no-reply@example.com --subject "Hi" --html "<h1>Hello</h1>"\`
|
|
2530
|
+
- With template: \`emailr send --to user@example.com --from no-reply@example.com --template tmpl_xxx --template-data '{"name":"Ada"}'\`
|
|
2531
|
+
|
|
2532
|
+
### Templates
|
|
2533
|
+
- List: \`emailr templates list --format json\`
|
|
2534
|
+
- Get: \`emailr templates get <id> --format json\`
|
|
2535
|
+
- Preview: \`emailr templates preview <id>\`
|
|
2536
|
+
- Create: \`emailr templates create --name "Name" --subject "Subject" --html-file ./template.html\`
|
|
2537
|
+
- Update: \`emailr templates update <id> --html-file ./template.html\`
|
|
2538
|
+
- Delete: \`emailr templates delete <id>\`
|
|
2539
|
+
`;
|
|
2540
|
+
function isOpenCodeInstalled() {
|
|
2541
|
+
try {
|
|
2542
|
+
execSync("which opencode", { stdio: "ignore" });
|
|
2543
|
+
return true;
|
|
2544
|
+
} catch {
|
|
2545
|
+
return false;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
function installOpenCode() {
|
|
2549
|
+
console.log("Installing OpenCode AI agent...");
|
|
2550
|
+
try {
|
|
2551
|
+
execSync("npm install -g opencode-ai@latest", { stdio: "inherit" });
|
|
2552
|
+
return true;
|
|
2553
|
+
} catch {
|
|
2554
|
+
if (process.platform === "darwin") {
|
|
2555
|
+
try {
|
|
2556
|
+
execSync("brew install anomalyco/tap/opencode", { stdio: "inherit" });
|
|
2557
|
+
return true;
|
|
2558
|
+
} catch {
|
|
2559
|
+
return false;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
return false;
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
function ensureSkillInstalled() {
|
|
2566
|
+
if (!fs5.existsSync(OPENCODE_SKILL_DIR)) {
|
|
2567
|
+
fs5.mkdirSync(OPENCODE_SKILL_DIR, { recursive: true });
|
|
2568
|
+
}
|
|
2569
|
+
const skillPath = path5.join(OPENCODE_SKILL_DIR, "SKILL.md");
|
|
2570
|
+
fs5.writeFileSync(skillPath, SKILL_MD, "utf-8");
|
|
2571
|
+
}
|
|
2572
|
+
function createAgentCommand() {
|
|
2573
|
+
const cmd = new Command10("agent").description(`Launch an AI agent with Emailr CLI expertise
|
|
2574
|
+
|
|
2575
|
+
This opens OpenCode (opencode.ai), an AI coding agent that knows how to use all Emailr CLI commands.
|
|
2576
|
+
The agent can help you:
|
|
2577
|
+
- Send emails and manage templates with live preview
|
|
2578
|
+
- Manage contacts, broadcasts, and segments
|
|
2579
|
+
- Configure domains and webhooks
|
|
2580
|
+
|
|
2581
|
+
The agent runs in your terminal with full access to the emailr CLI.`).option("--install", "Install OpenCode if not already installed").option("--model <model>", "Model to use (e.g., anthropic/claude-sonnet-4)").action(async (options) => {
|
|
2582
|
+
if (!isOpenCodeInstalled()) {
|
|
2583
|
+
if (options.install) {
|
|
2584
|
+
const installed = installOpenCode();
|
|
2585
|
+
if (!installed) {
|
|
2586
|
+
error("Failed to install OpenCode. Please install manually:");
|
|
2587
|
+
console.log(" npm install -g opencode-ai@latest");
|
|
2588
|
+
console.log(" # or");
|
|
2589
|
+
console.log(" brew install anomalyco/tap/opencode");
|
|
2590
|
+
process.exit(1);
|
|
2591
|
+
}
|
|
2592
|
+
} else {
|
|
2593
|
+
error("OpenCode AI agent is not installed.");
|
|
2594
|
+
console.log("\nInstall it with one of:");
|
|
2595
|
+
console.log(" emailr agent --install");
|
|
2596
|
+
console.log(" npm install -g opencode-ai@latest");
|
|
2597
|
+
console.log(" brew install anomalyco/tap/opencode");
|
|
2598
|
+
process.exit(1);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
try {
|
|
2602
|
+
ensureSkillInstalled();
|
|
2603
|
+
success("Emailr CLI skill loaded");
|
|
2604
|
+
} catch (err) {
|
|
2605
|
+
warn(`Could not install skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
2606
|
+
}
|
|
2607
|
+
const args = [];
|
|
2608
|
+
if (options.model) {
|
|
2609
|
+
args.push("--model", options.model);
|
|
2610
|
+
}
|
|
2611
|
+
console.log("\nStarting Emailr AI Agent...");
|
|
2612
|
+
console.log("The agent has the emailr-cli skill loaded.");
|
|
2613
|
+
console.log("Ask it to help with emails, templates, contacts, or broadcasts.\n");
|
|
2614
|
+
const opencode = spawn("opencode", args, {
|
|
2615
|
+
stdio: "inherit",
|
|
2616
|
+
env: process.env
|
|
2617
|
+
});
|
|
2618
|
+
opencode.on("error", (err) => {
|
|
2619
|
+
error(`Failed to start agent: ${err.message}`);
|
|
2620
|
+
process.exit(1);
|
|
2621
|
+
});
|
|
2622
|
+
opencode.on("exit", (code) => {
|
|
2623
|
+
process.exit(code ?? 0);
|
|
2624
|
+
});
|
|
2625
|
+
});
|
|
2626
|
+
return cmd;
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2325
2629
|
// src/index.ts
|
|
2326
|
-
var program = new
|
|
2630
|
+
var program = new Command11();
|
|
2327
2631
|
program.name("emailr").description("Emailr CLI - Send emails and manage your email infrastructure").version("1.0.0");
|
|
2328
2632
|
program.addCommand(createSendCommand());
|
|
2329
2633
|
program.addCommand(createContactsCommand());
|
|
@@ -2334,4 +2638,5 @@ program.addCommand(createWebhooksCommand());
|
|
|
2334
2638
|
program.addCommand(createSegmentsCommand());
|
|
2335
2639
|
program.addCommand(createConfigCommand());
|
|
2336
2640
|
program.addCommand(createLoginCommand());
|
|
2641
|
+
program.addCommand(createAgentCommand());
|
|
2337
2642
|
program.parse();
|