emailr-cli 1.5.0 → 1.5.2
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 +164 -2147
- package/package.json +11 -10
package/dist/index.js
CHANGED
|
@@ -1,398 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {Command}from'commander';import {Emailr}from'emailr';import S from'fs';import W from'os';import N from'path';import ie from'cli-table3';import h from'chalk';import Be from'http';import {URL}from'url';import ct from'crypto';import {spawn,execSync,exec}from'child_process';var Le=[N.join(W.homedir(),".emailrrc"),N.join(W.homedir(),".config","emailr","config.json")];function m(){if(process.env.EMAILR_API_KEY)return {apiKey:process.env.EMAILR_API_KEY,baseUrl:process.env.EMAILR_BASE_URL,format:process.env.EMAILR_FORMAT||"table"};for(let n of Le)if(S.existsSync(n))try{let t=S.readFileSync(n,"utf-8"),e=JSON.parse(t);if(e.apiKey)return {apiKey:e.apiKey,baseUrl:e.baseUrl,format:e.format||"table"}}catch{}throw new Error(`No API key configured.
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
Set the EMAILR_API_KEY environment variable:
|
|
5
|
+
export EMAILR_API_KEY=your-api-key
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import { Emailr } from "emailr";
|
|
7
|
+
Or create a config file at ~/.emailrrc:
|
|
8
|
+
{ "apiKey": "your-api-key" }
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import path from "path";
|
|
14
|
-
var CONFIG_PATHS = [
|
|
15
|
-
path.join(os.homedir(), ".emailrrc"),
|
|
16
|
-
path.join(os.homedir(), ".config", "emailr", "config.json")
|
|
17
|
-
];
|
|
18
|
-
function loadConfig() {
|
|
19
|
-
if (process.env.EMAILR_API_KEY) {
|
|
20
|
-
return {
|
|
21
|
-
apiKey: process.env.EMAILR_API_KEY,
|
|
22
|
-
baseUrl: process.env.EMAILR_BASE_URL,
|
|
23
|
-
format: process.env.EMAILR_FORMAT || "table"
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
for (const configPath of CONFIG_PATHS) {
|
|
27
|
-
if (fs.existsSync(configPath)) {
|
|
28
|
-
try {
|
|
29
|
-
const content = fs.readFileSync(configPath, "utf-8");
|
|
30
|
-
const config = JSON.parse(content);
|
|
31
|
-
if (config.apiKey) {
|
|
32
|
-
return {
|
|
33
|
-
apiKey: config.apiKey,
|
|
34
|
-
baseUrl: config.baseUrl,
|
|
35
|
-
format: config.format || "table"
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
} catch {
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
throw new Error(
|
|
43
|
-
'No API key configured.\n\nSet the EMAILR_API_KEY environment variable:\n export EMAILR_API_KEY=your-api-key\n\nOr create a config file at ~/.emailrrc:\n { "apiKey": "your-api-key" }\n\nOr run: emailr config set api-key <your-api-key>'
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
function getConfigPath() {
|
|
47
|
-
const configDir = path.join(os.homedir(), ".config", "emailr");
|
|
48
|
-
return path.join(configDir, "config.json");
|
|
49
|
-
}
|
|
50
|
-
function saveConfig(config) {
|
|
51
|
-
const configPath = getConfigPath();
|
|
52
|
-
const configDir = path.dirname(configPath);
|
|
53
|
-
if (!fs.existsSync(configDir)) {
|
|
54
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
55
|
-
}
|
|
56
|
-
let existingConfig = {};
|
|
57
|
-
if (fs.existsSync(configPath)) {
|
|
58
|
-
try {
|
|
59
|
-
existingConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
60
|
-
} catch {
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
const newConfig = { ...existingConfig, ...config };
|
|
64
|
-
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n");
|
|
65
|
-
}
|
|
66
|
-
function getConfigValue(key) {
|
|
67
|
-
try {
|
|
68
|
-
const config = loadConfig();
|
|
69
|
-
return config[key]?.toString();
|
|
70
|
-
} catch {
|
|
71
|
-
return void 0;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// src/output.ts
|
|
76
|
-
import Table from "cli-table3";
|
|
77
|
-
import chalk from "chalk";
|
|
78
|
-
function output(data, format = "table") {
|
|
79
|
-
if (format === "json") {
|
|
80
|
-
console.log(JSON.stringify(data, null, 2));
|
|
81
|
-
} else {
|
|
82
|
-
printTable(data);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
function printTable(data) {
|
|
86
|
-
if (Array.isArray(data)) {
|
|
87
|
-
printArrayTable(data);
|
|
88
|
-
} else if (typeof data === "object" && data !== null) {
|
|
89
|
-
printObjectTable(data);
|
|
90
|
-
} else {
|
|
91
|
-
console.log(data);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
function printArrayTable(data) {
|
|
95
|
-
if (data.length === 0) {
|
|
96
|
-
console.log(chalk.gray("No results"));
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
const firstItem = data[0];
|
|
100
|
-
if (typeof firstItem !== "object" || firstItem === null) {
|
|
101
|
-
data.forEach((item) => console.log(item));
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
const keys = Object.keys(firstItem);
|
|
105
|
-
const table = new Table({
|
|
106
|
-
head: keys.map((k) => chalk.cyan(k)),
|
|
107
|
-
style: { head: [], border: [] }
|
|
108
|
-
});
|
|
109
|
-
for (const item of data) {
|
|
110
|
-
const row = keys.map((key) => {
|
|
111
|
-
const value = item[key];
|
|
112
|
-
return formatValue(value);
|
|
113
|
-
});
|
|
114
|
-
table.push(row);
|
|
115
|
-
}
|
|
116
|
-
console.log(table.toString());
|
|
117
|
-
}
|
|
118
|
-
function printObjectTable(data) {
|
|
119
|
-
const table = new Table({
|
|
120
|
-
style: { head: [], border: [] }
|
|
121
|
-
});
|
|
122
|
-
for (const [key, value] of Object.entries(data)) {
|
|
123
|
-
table.push([chalk.cyan(key), formatValue(value)]);
|
|
124
|
-
}
|
|
125
|
-
console.log(table.toString());
|
|
126
|
-
}
|
|
127
|
-
function formatValue(value) {
|
|
128
|
-
if (value === null || value === void 0) {
|
|
129
|
-
return chalk.gray("-");
|
|
130
|
-
}
|
|
131
|
-
if (typeof value === "boolean") {
|
|
132
|
-
return value ? chalk.green("\u2713") : chalk.red("\u2717");
|
|
133
|
-
}
|
|
134
|
-
if (typeof value === "object") {
|
|
135
|
-
return JSON.stringify(value);
|
|
136
|
-
}
|
|
137
|
-
return String(value);
|
|
138
|
-
}
|
|
139
|
-
function success(message) {
|
|
140
|
-
console.log(chalk.green("\u2713"), message);
|
|
141
|
-
}
|
|
142
|
-
function error(message) {
|
|
143
|
-
console.error(chalk.red("\u2717"), message);
|
|
144
|
-
}
|
|
145
|
-
function warn(message) {
|
|
146
|
-
console.warn(chalk.yellow("\u26A0"), message);
|
|
147
|
-
}
|
|
148
|
-
function info(message) {
|
|
149
|
-
console.log(chalk.blue("\u2139"), message);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// src/commands/send.ts
|
|
153
|
-
function createSendCommand() {
|
|
154
|
-
const cmd = new Command("send").description("Send an email").requiredOption("--to <email>", "Recipient email address (comma-separated for multiple)").option("--from <email>", "Sender email address").option("--subject <subject>", "Email subject").option("--html <html>", "HTML content").option("--text <text>", "Plain text content").option("--template <id>", "Template ID to use").option("--template-data <json>", "Template data as JSON").option("--cc <emails>", "CC recipients (comma-separated)").option("--bcc <emails>", "BCC recipients (comma-separated)").option("--reply-to <email>", "Reply-to email address").option("--schedule <datetime>", "Schedule send time (ISO 8601)").option("--format <format>", "Output format (json|table)", "table").action(async (options) => {
|
|
155
|
-
try {
|
|
156
|
-
const config = loadConfig();
|
|
157
|
-
const client = new Emailr({
|
|
158
|
-
apiKey: config.apiKey,
|
|
159
|
-
baseUrl: config.baseUrl
|
|
160
|
-
});
|
|
161
|
-
const to = options.to.split(",").map((e) => e.trim());
|
|
162
|
-
const request = {
|
|
163
|
-
to: to.length === 1 ? to[0] : to
|
|
164
|
-
};
|
|
165
|
-
if (options.from) request.from = options.from;
|
|
166
|
-
if (options.subject) request.subject = options.subject;
|
|
167
|
-
if (options.html) request.html = options.html;
|
|
168
|
-
if (options.text) request.text = options.text;
|
|
169
|
-
if (options.template) request.template_id = options.template;
|
|
170
|
-
if (options.templateData) {
|
|
171
|
-
try {
|
|
172
|
-
request.template_data = JSON.parse(options.templateData);
|
|
173
|
-
} catch {
|
|
174
|
-
error("Invalid JSON for --template-data");
|
|
175
|
-
process.exit(1);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (options.cc) {
|
|
179
|
-
const cc = options.cc.split(",").map((e) => e.trim());
|
|
180
|
-
request.cc = cc.length === 1 ? cc[0] : cc;
|
|
181
|
-
}
|
|
182
|
-
if (options.bcc) {
|
|
183
|
-
const bcc = options.bcc.split(",").map((e) => e.trim());
|
|
184
|
-
request.bcc = bcc.length === 1 ? bcc[0] : bcc;
|
|
185
|
-
}
|
|
186
|
-
if (options.replyTo) request.reply_to_email = options.replyTo;
|
|
187
|
-
if (options.schedule) request.scheduled_at = options.schedule;
|
|
188
|
-
const result = await client.emails.send(request);
|
|
189
|
-
if (options.format === "json") {
|
|
190
|
-
output(result, "json");
|
|
191
|
-
} else {
|
|
192
|
-
success(`Email sent successfully!`);
|
|
193
|
-
output({
|
|
194
|
-
"Message ID": result.message_id,
|
|
195
|
-
Recipients: result.recipients,
|
|
196
|
-
Status: result.status,
|
|
197
|
-
...result.scheduled_at && { "Scheduled At": result.scheduled_at }
|
|
198
|
-
}, "table");
|
|
199
|
-
}
|
|
200
|
-
} catch (err) {
|
|
201
|
-
error(err instanceof Error ? err.message : "Failed to send email");
|
|
202
|
-
process.exit(1);
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
return cmd;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// src/commands/contacts.ts
|
|
209
|
-
import { Command as Command2 } from "commander";
|
|
210
|
-
import { Emailr as Emailr2 } from "emailr";
|
|
211
|
-
function createContactsCommand() {
|
|
212
|
-
const cmd = new Command2("contacts").description("Manage contacts");
|
|
213
|
-
cmd.command("list").description("List all contacts").option("--limit <number>", "Number of contacts to return", "20").option("--offset <number>", "Offset for pagination", "0").option("--subscribed", "Only show subscribed contacts").option("--unsubscribed", "Only show unsubscribed contacts").option("--format <format>", "Output format (json|table)", "table").action(async (options) => {
|
|
214
|
-
try {
|
|
215
|
-
const config = loadConfig();
|
|
216
|
-
const client = new Emailr2({
|
|
217
|
-
apiKey: config.apiKey,
|
|
218
|
-
baseUrl: config.baseUrl
|
|
219
|
-
});
|
|
220
|
-
const params = {
|
|
221
|
-
limit: parseInt(options.limit, 10),
|
|
222
|
-
offset: parseInt(options.offset, 10)
|
|
223
|
-
};
|
|
224
|
-
if (options.subscribed) params.subscribed = true;
|
|
225
|
-
if (options.unsubscribed) params.subscribed = false;
|
|
226
|
-
const result = await client.contacts.list(params);
|
|
227
|
-
if (options.format === "json") {
|
|
228
|
-
output(result, "json");
|
|
229
|
-
} else {
|
|
230
|
-
const contacts = result.contacts.map((c) => ({
|
|
231
|
-
ID: c.id,
|
|
232
|
-
Email: c.email,
|
|
233
|
-
Name: [c.first_name, c.last_name].filter(Boolean).join(" ") || "-",
|
|
234
|
-
Subscribed: c.subscribed,
|
|
235
|
-
Created: c.created_at
|
|
236
|
-
}));
|
|
237
|
-
output(contacts, "table");
|
|
238
|
-
console.log(`
|
|
239
|
-
Total: ${result.total}`);
|
|
240
|
-
}
|
|
241
|
-
} catch (err) {
|
|
242
|
-
error(err instanceof Error ? err.message : "Failed to list contacts");
|
|
243
|
-
process.exit(1);
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
cmd.command("get <id>").description("Get a contact by ID").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
247
|
-
try {
|
|
248
|
-
const config = loadConfig();
|
|
249
|
-
const client = new Emailr2({
|
|
250
|
-
apiKey: config.apiKey,
|
|
251
|
-
baseUrl: config.baseUrl
|
|
252
|
-
});
|
|
253
|
-
const contact = await client.contacts.get(id);
|
|
254
|
-
output(contact, options.format);
|
|
255
|
-
} catch (err) {
|
|
256
|
-
error(err instanceof Error ? err.message : "Failed to get contact");
|
|
257
|
-
process.exit(1);
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
cmd.command("create").description("Create a new contact").requiredOption("--email <email>", "Contact email address").option("--first-name <name>", "First name").option("--last-name <name>", "Last name").option("--subscribed", "Mark as subscribed (default: true)").option("--metadata <json>", "Metadata as JSON").option("--format <format>", "Output format (json|table)", "table").action(async (options) => {
|
|
261
|
-
try {
|
|
262
|
-
const config = loadConfig();
|
|
263
|
-
const client = new Emailr2({
|
|
264
|
-
apiKey: config.apiKey,
|
|
265
|
-
baseUrl: config.baseUrl
|
|
266
|
-
});
|
|
267
|
-
const request = {
|
|
268
|
-
email: options.email
|
|
269
|
-
};
|
|
270
|
-
if (options.firstName) request.first_name = options.firstName;
|
|
271
|
-
if (options.lastName) request.last_name = options.lastName;
|
|
272
|
-
if (options.subscribed !== void 0) request.subscribed = options.subscribed;
|
|
273
|
-
if (options.metadata) {
|
|
274
|
-
try {
|
|
275
|
-
request.metadata = JSON.parse(options.metadata);
|
|
276
|
-
} catch {
|
|
277
|
-
error("Invalid JSON for --metadata");
|
|
278
|
-
process.exit(1);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
const contact = await client.contacts.create(request);
|
|
282
|
-
if (options.format === "json") {
|
|
283
|
-
output(contact, "json");
|
|
284
|
-
} else {
|
|
285
|
-
success(`Contact created: ${contact.id}`);
|
|
286
|
-
output(contact, "table");
|
|
287
|
-
}
|
|
288
|
-
} catch (err) {
|
|
289
|
-
error(err instanceof Error ? err.message : "Failed to create contact");
|
|
290
|
-
process.exit(1);
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
cmd.command("update <id>").description("Update a contact").option("--email <email>", "New email address").option("--first-name <name>", "First name").option("--last-name <name>", "Last name").option("--subscribed", "Mark as subscribed").option("--unsubscribed", "Mark as unsubscribed").option("--metadata <json>", "Metadata as JSON").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
294
|
-
try {
|
|
295
|
-
const config = loadConfig();
|
|
296
|
-
const client = new Emailr2({
|
|
297
|
-
apiKey: config.apiKey,
|
|
298
|
-
baseUrl: config.baseUrl
|
|
299
|
-
});
|
|
300
|
-
const request = {};
|
|
301
|
-
if (options.email) request.email = options.email;
|
|
302
|
-
if (options.firstName) request.first_name = options.firstName;
|
|
303
|
-
if (options.lastName) request.last_name = options.lastName;
|
|
304
|
-
if (options.subscribed) request.subscribed = true;
|
|
305
|
-
if (options.unsubscribed) request.subscribed = false;
|
|
306
|
-
if (options.metadata) {
|
|
307
|
-
try {
|
|
308
|
-
request.metadata = JSON.parse(options.metadata);
|
|
309
|
-
} catch {
|
|
310
|
-
error("Invalid JSON for --metadata");
|
|
311
|
-
process.exit(1);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
const contact = await client.contacts.update(id, request);
|
|
315
|
-
if (options.format === "json") {
|
|
316
|
-
output(contact, "json");
|
|
317
|
-
} else {
|
|
318
|
-
success(`Contact updated: ${contact.id}`);
|
|
319
|
-
output(contact, "table");
|
|
320
|
-
}
|
|
321
|
-
} catch (err) {
|
|
322
|
-
error(err instanceof Error ? err.message : "Failed to update contact");
|
|
323
|
-
process.exit(1);
|
|
324
|
-
}
|
|
325
|
-
});
|
|
326
|
-
cmd.command("delete <id>").description("Delete a contact").action(async (id) => {
|
|
327
|
-
try {
|
|
328
|
-
const config = loadConfig();
|
|
329
|
-
const client = new Emailr2({
|
|
330
|
-
apiKey: config.apiKey,
|
|
331
|
-
baseUrl: config.baseUrl
|
|
332
|
-
});
|
|
333
|
-
await client.contacts.delete(id);
|
|
334
|
-
success(`Contact deleted: ${id}`);
|
|
335
|
-
} catch (err) {
|
|
336
|
-
error(err instanceof Error ? err.message : "Failed to delete contact");
|
|
337
|
-
process.exit(1);
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
return cmd;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// src/commands/templates.ts
|
|
344
|
-
import { Command as Command3 } from "commander";
|
|
345
|
-
import { Emailr as Emailr3 } from "emailr";
|
|
346
|
-
import fs4 from "fs";
|
|
347
|
-
import path4 from "path";
|
|
348
|
-
|
|
349
|
-
// src/server/template-store.ts
|
|
350
|
-
import fs2 from "fs";
|
|
351
|
-
import os2 from "os";
|
|
352
|
-
import path2 from "path";
|
|
353
|
-
function getTemplatesDir() {
|
|
354
|
-
return path2.join(os2.homedir(), ".config", "emailr", "templates");
|
|
355
|
-
}
|
|
356
|
-
function ensureTemplatesDir() {
|
|
357
|
-
const templatesDir = getTemplatesDir();
|
|
358
|
-
if (!fs2.existsSync(templatesDir)) {
|
|
359
|
-
fs2.mkdirSync(templatesDir, { recursive: true });
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
function getTemplatePath(templateId) {
|
|
363
|
-
return path2.join(getTemplatesDir(), `${templateId}.html`);
|
|
364
|
-
}
|
|
365
|
-
function saveTemplate(templateId, htmlContent) {
|
|
366
|
-
ensureTemplatesDir();
|
|
367
|
-
const templatePath = getTemplatePath(templateId);
|
|
368
|
-
fs2.writeFileSync(templatePath, htmlContent, "utf-8");
|
|
369
|
-
}
|
|
370
|
-
function readTemplate(templateId) {
|
|
371
|
-
const templatePath = getTemplatePath(templateId);
|
|
372
|
-
if (!fs2.existsSync(templatePath)) {
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
|
-
return fs2.readFileSync(templatePath, "utf-8");
|
|
376
|
-
}
|
|
377
|
-
function templateExists(templateId) {
|
|
378
|
-
const templatePath = getTemplatePath(templateId);
|
|
379
|
-
return fs2.existsSync(templatePath);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// src/server/preview-server.ts
|
|
383
|
-
import http from "http";
|
|
384
|
-
var serverInstance = null;
|
|
385
|
-
var serverPort = null;
|
|
386
|
-
var serverRunning = false;
|
|
387
|
-
function escapeHtml(str) {
|
|
388
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
389
|
-
}
|
|
390
|
-
function isEmptyContent(content) {
|
|
391
|
-
return content === null || content.trim() === "";
|
|
392
|
-
}
|
|
393
|
-
function generateEmptyContentPlaceholder(templateId) {
|
|
394
|
-
const escapedId = escapeHtml(templateId);
|
|
395
|
-
return `<!DOCTYPE html>
|
|
10
|
+
Or run: emailr config set api-key <your-api-key>`)}function w(){let n=N.join(W.homedir(),".config","emailr");return N.join(n,"config.json")}function R(n){let t=w(),e=N.dirname(t);S.existsSync(e)||S.mkdirSync(e,{recursive:true});let o={};if(S.existsSync(t))try{o=JSON.parse(S.readFileSync(t,"utf-8"));}catch{}let i={...o,...n};S.writeFileSync(t,JSON.stringify(i,null,2)+`
|
|
11
|
+
`);}function re(n){try{return m()[n]?.toString()}catch{return}}function l(n,t="table"){t==="json"?console.log(JSON.stringify(n,null,2)):Ne(n);}function Ne(n){Array.isArray(n)?Re(n):typeof n=="object"&&n!==null?De(n):console.log(n);}function Re(n){if(n.length===0){console.log(h.gray("No results"));return}let t=n[0];if(typeof t!="object"||t===null){n.forEach(i=>console.log(i));return}let e=Object.keys(t),o=new ie({head:e.map(i=>h.cyan(i)),style:{head:[],border:[]}});for(let i of n){let a=e.map(r=>{let s=i[r];return se(s)});o.push(a);}console.log(o.toString());}function De(n){let t=new ie({style:{head:[],border:[]}});for(let[e,o]of Object.entries(n))t.push([h.cyan(e),se(o)]);console.log(t.toString());}function se(n){return n==null?h.gray("-"):typeof n=="boolean"?n?h.green("\u2713"):h.red("\u2717"):typeof n=="object"?JSON.stringify(n):String(n)}function d(n){console.log(h.green("\u2713"),n);}function c(n){console.error(h.red("\u2717"),n);}function v(n){console.warn(h.yellow("\u26A0"),n);}function p(n){console.log(h.blue("\u2139"),n);}function le(){return new Command("send").description("Send an email").requiredOption("--to <email>","Recipient email address (comma-separated for multiple)").option("--from <email>","Sender email address").option("--subject <subject>","Email subject").option("--html <html>","HTML content").option("--text <text>","Plain text content").option("--template <id>","Template ID to use").option("--template-data <json>","Template data as JSON").option("--cc <emails>","CC recipients (comma-separated)").option("--bcc <emails>","BCC recipients (comma-separated)").option("--reply-to <email>","Reply-to email address").option("--schedule <datetime>","Schedule send time (ISO 8601)").option("--format <format>","Output format (json|table)","table").action(async t=>{try{let e=m(),o=new Emailr({apiKey:e.apiKey,baseUrl:e.baseUrl}),i=t.to.split(",").map(s=>s.trim()),a={to:i.length===1?i[0]:i};if(t.from&&(a.from=t.from),t.subject&&(a.subject=t.subject),t.html&&(a.html=t.html),t.text&&(a.text=t.text),t.template&&(a.template_id=t.template),t.templateData)try{a.template_data=JSON.parse(t.templateData);}catch{c("Invalid JSON for --template-data"),process.exit(1);}if(t.cc){let s=t.cc.split(",").map(f=>f.trim());a.cc=s.length===1?s[0]:s;}if(t.bcc){let s=t.bcc.split(",").map(f=>f.trim());a.bcc=s.length===1?s[0]:s;}t.replyTo&&(a.reply_to_email=t.replyTo),t.schedule&&(a.scheduled_at=t.schedule);let r=await o.emails.send(a);t.format==="json"?l(r,"json"):(d("Email sent successfully!"),l({"Message ID":r.message_id,Recipients:r.recipients,Status:r.status,...r.scheduled_at&&{"Scheduled At":r.scheduled_at}},"table"));}catch(e){c(e instanceof Error?e.message:"Failed to send email"),process.exit(1);}})}function ce(){let n=new Command("contacts").description("Manage contacts");return n.command("list").description("List all contacts").option("--limit <number>","Number of contacts to return","20").option("--offset <number>","Offset for pagination","0").option("--subscribed","Only show subscribed contacts").option("--unsubscribed","Only show unsubscribed contacts").option("--format <format>","Output format (json|table)","table").action(async t=>{try{let e=m(),o=new Emailr({apiKey:e.apiKey,baseUrl:e.baseUrl}),i={limit:parseInt(t.limit,10),offset:parseInt(t.offset,10)};t.subscribed&&(i.subscribed=!0),t.unsubscribed&&(i.subscribed=!1);let a=await o.contacts.list(i);if(t.format==="json")l(a,"json");else {let r=a.contacts.map(s=>({ID:s.id,Email:s.email,Name:[s.first_name,s.last_name].filter(Boolean).join(" ")||"-",Subscribed:s.subscribed,Created:s.created_at}));l(r,"table"),console.log(`
|
|
12
|
+
Total: ${a.total}`);}}catch(e){c(e instanceof Error?e.message:"Failed to list contacts"),process.exit(1);}}),n.command("get <id>").description("Get a contact by ID").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),a=await new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}).contacts.get(t);l(a,e.format);}catch(o){c(o instanceof Error?o.message:"Failed to get contact"),process.exit(1);}}),n.command("create").description("Create a new contact").requiredOption("--email <email>","Contact email address").option("--first-name <name>","First name").option("--last-name <name>","Last name").option("--subscribed","Mark as subscribed (default: true)").option("--metadata <json>","Metadata as JSON").option("--format <format>","Output format (json|table)","table").action(async t=>{try{let e=m(),o=new Emailr({apiKey:e.apiKey,baseUrl:e.baseUrl}),i={email:t.email};if(t.firstName&&(i.first_name=t.firstName),t.lastName&&(i.last_name=t.lastName),t.subscribed!==void 0&&(i.subscribed=t.subscribed),t.metadata)try{i.metadata=JSON.parse(t.metadata);}catch{c("Invalid JSON for --metadata"),process.exit(1);}let a=await o.contacts.create(i);t.format==="json"?l(a,"json"):(d(`Contact created: ${a.id}`),l(a,"table"));}catch(e){c(e instanceof Error?e.message:"Failed to create contact"),process.exit(1);}}),n.command("update <id>").description("Update a contact").option("--email <email>","New email address").option("--first-name <name>","First name").option("--last-name <name>","Last name").option("--subscribed","Mark as subscribed").option("--unsubscribed","Mark as unsubscribed").option("--metadata <json>","Metadata as JSON").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),i=new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}),a={};if(e.email&&(a.email=e.email),e.firstName&&(a.first_name=e.firstName),e.lastName&&(a.last_name=e.lastName),e.subscribed&&(a.subscribed=!0),e.unsubscribed&&(a.subscribed=!1),e.metadata)try{a.metadata=JSON.parse(e.metadata);}catch{c("Invalid JSON for --metadata"),process.exit(1);}let r=await i.contacts.update(t,a);e.format==="json"?l(r,"json"):(d(`Contact updated: ${r.id}`),l(r,"table"));}catch(o){c(o instanceof Error?o.message:"Failed to update contact"),process.exit(1);}}),n.command("delete <id>").description("Delete a contact").action(async t=>{try{let e=m();await new Emailr({apiKey:e.apiKey,baseUrl:e.baseUrl}).contacts.delete(t),d(`Contact deleted: ${t}`);}catch(e){c(e instanceof Error?e.message:"Failed to delete contact"),process.exit(1);}}),n}function de(){return N.join(W.homedir(),".config","emailr","templates")}function qe(){let n=de();S.existsSync(n)||S.mkdirSync(n,{recursive:true});}function G(n){return N.join(de(),`${n}.html`)}function J(n,t){qe();let e=G(n);S.writeFileSync(e,t,"utf-8");}function pe(n){let t=G(n);return S.existsSync(t)?S.readFileSync(t,"utf-8"):null}function fe(n){let t=G(n);return S.existsSync(t)}var b=null,C=null,$=false;function ge(n){return n.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function We(n){return n===null||n.trim()===""}function Ge(n){return `<!DOCTYPE html>
|
|
396
13
|
<html lang="en">
|
|
397
14
|
<head>
|
|
398
15
|
<meta charset="UTF-8">
|
|
@@ -458,18 +75,14 @@ function generateEmptyContentPlaceholder(templateId) {
|
|
|
458
75
|
<div class="icon">\u{1F4ED}</div>
|
|
459
76
|
<h1>No Content Available</h1>
|
|
460
77
|
<p>This template exists but has no HTML content to display.</p>
|
|
461
|
-
<div class="template-id">${
|
|
78
|
+
<div class="template-id">${ge(n)}</div>
|
|
462
79
|
<div class="hint">
|
|
463
80
|
<p><strong>To add content:</strong></p>
|
|
464
81
|
<p>Update the template using the CLI with the <code>--html</code> option or provide HTML content when creating the template.</p>
|
|
465
82
|
</div>
|
|
466
83
|
</div>
|
|
467
84
|
</body>
|
|
468
|
-
</html
|
|
469
|
-
}
|
|
470
|
-
function generateNotFoundHtml(templateId) {
|
|
471
|
-
const escapedId = escapeHtml(templateId);
|
|
472
|
-
return `<!DOCTYPE html>
|
|
85
|
+
</html>`}function ue(n){return `<!DOCTYPE html>
|
|
473
86
|
<html lang="en">
|
|
474
87
|
<head>
|
|
475
88
|
<meta charset="UTF-8">
|
|
@@ -521,121 +134,11 @@ function generateNotFoundHtml(templateId) {
|
|
|
521
134
|
<div class="icon">404</div>
|
|
522
135
|
<h1>Template Not Found</h1>
|
|
523
136
|
<p>The requested template could not be found in local storage.</p>
|
|
524
|
-
<div class="template-id">${
|
|
137
|
+
<div class="template-id">${ge(n)}</div>
|
|
525
138
|
<p style="margin-top: 1rem;">Try creating or retrieving the template first using the CLI.</p>
|
|
526
139
|
</div>
|
|
527
140
|
</body>
|
|
528
|
-
</html
|
|
529
|
-
}
|
|
530
|
-
function parseTemplateIdFromPath(urlPath) {
|
|
531
|
-
const match = urlPath.match(/^\/preview\/([^/]+)$/);
|
|
532
|
-
return match ? match[1] : null;
|
|
533
|
-
}
|
|
534
|
-
function createRequestHandler() {
|
|
535
|
-
return (req, res) => {
|
|
536
|
-
if (req.method !== "GET") {
|
|
537
|
-
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
538
|
-
res.end("Method Not Allowed");
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
const urlPath = req.url || "/";
|
|
542
|
-
const templateId = parseTemplateIdFromPath(urlPath);
|
|
543
|
-
if (!templateId) {
|
|
544
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
545
|
-
res.end("Not Found");
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
if (!templateExists(templateId)) {
|
|
549
|
-
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
550
|
-
res.end(generateNotFoundHtml(templateId));
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
const htmlContent = readTemplate(templateId);
|
|
554
|
-
if (htmlContent === null) {
|
|
555
|
-
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
556
|
-
res.end(generateNotFoundHtml(templateId));
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
if (isEmptyContent(htmlContent)) {
|
|
560
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
561
|
-
res.end(generateEmptyContentPlaceholder(templateId));
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
565
|
-
res.end(htmlContent);
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
var previewServer = {
|
|
569
|
-
async start() {
|
|
570
|
-
if (serverRunning && serverPort !== null) {
|
|
571
|
-
return serverPort;
|
|
572
|
-
}
|
|
573
|
-
return new Promise((resolve, reject) => {
|
|
574
|
-
serverInstance = http.createServer(createRequestHandler());
|
|
575
|
-
serverInstance.listen(0, "127.0.0.1", () => {
|
|
576
|
-
const address = serverInstance.address();
|
|
577
|
-
if (address && typeof address === "object") {
|
|
578
|
-
serverPort = address.port;
|
|
579
|
-
serverRunning = true;
|
|
580
|
-
serverInstance.unref();
|
|
581
|
-
resolve(serverPort);
|
|
582
|
-
} else {
|
|
583
|
-
reject(new Error("Failed to get server address"));
|
|
584
|
-
}
|
|
585
|
-
});
|
|
586
|
-
serverInstance.on("error", (err) => {
|
|
587
|
-
serverRunning = false;
|
|
588
|
-
serverPort = null;
|
|
589
|
-
serverInstance = null;
|
|
590
|
-
reject(new Error(`Failed to start preview server: ${err.message}`));
|
|
591
|
-
});
|
|
592
|
-
});
|
|
593
|
-
},
|
|
594
|
-
getPort() {
|
|
595
|
-
return serverPort;
|
|
596
|
-
},
|
|
597
|
-
isRunning() {
|
|
598
|
-
return serverRunning;
|
|
599
|
-
},
|
|
600
|
-
async stop() {
|
|
601
|
-
if (serverInstance) {
|
|
602
|
-
return new Promise((resolve) => {
|
|
603
|
-
serverInstance.close(() => {
|
|
604
|
-
serverInstance = null;
|
|
605
|
-
serverPort = null;
|
|
606
|
-
serverRunning = false;
|
|
607
|
-
resolve();
|
|
608
|
-
});
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
};
|
|
613
|
-
function getPreviewServer() {
|
|
614
|
-
return previewServer;
|
|
615
|
-
}
|
|
616
|
-
async function getPreviewUrl(templateId) {
|
|
617
|
-
try {
|
|
618
|
-
const port = await previewServer.start();
|
|
619
|
-
return `http://127.0.0.1:${port}/preview/${templateId}`;
|
|
620
|
-
} catch {
|
|
621
|
-
return null;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
function keepServerAlive() {
|
|
625
|
-
if (serverInstance) {
|
|
626
|
-
serverInstance.ref();
|
|
627
|
-
}
|
|
628
|
-
}
|
|
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 = `
|
|
141
|
+
</html>`}function Je(n){let t=n.match(/^\/preview\/([^/]+)$/);return t?t[1]:null}function ze(){return (n,t)=>{if(n.method!=="GET"){t.writeHead(405,{"Content-Type":"text/plain"}),t.end("Method Not Allowed");return}let e=n.url||"/",o=Je(e);if(!o){t.writeHead(404,{"Content-Type":"text/plain"}),t.end("Not Found");return}if(!fe(o)){t.writeHead(404,{"Content-Type":"text/html; charset=utf-8"}),t.end(ue(o));return}let i=pe(o);if(i===null){t.writeHead(404,{"Content-Type":"text/html; charset=utf-8"}),t.end(ue(o));return}if(We(i)){t.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),t.end(Ge(o));return}t.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),t.end(i);}}var be={async start(){return $&&C!==null?C:new Promise((n,t)=>{b=Be.createServer(ze()),b.listen(0,"127.0.0.1",()=>{let e=b.address();e&&typeof e=="object"?(C=e.port,$=true,b.unref(),n(C)):t(new Error("Failed to get server address"));}),b.on("error",e=>{$=false,C=null,b=null,t(new Error(`Failed to start preview server: ${e.message}`));});})},getPort(){return C},isRunning(){return $},async stop(){if(b)return new Promise(n=>{b.close(()=>{b=null,C=null,$=false,n();});})}};function he(){return be}async function z(n){try{return `http://127.0.0.1:${await be.start()}/preview/${n}`}catch{return null}}function ye(){b&&b.ref();}var k=null,V=null,q=null,I=[],ve=`
|
|
639
142
|
<script>
|
|
640
143
|
(function() {
|
|
641
144
|
const evtSource = new EventSource('/__live-reload');
|
|
@@ -649,431 +152,103 @@ var LIVE_RELOAD_SCRIPT = `
|
|
|
649
152
|
};
|
|
650
153
|
})();
|
|
651
154
|
</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
|
-
}
|
|
155
|
+
`;function Qe(n){return n.includes("</body>")?n.replace("</body>",`${ve}</body>`):n+ve}function Xe(){I.forEach(n=>{try{n.write(`data: reload
|
|
746
156
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const htmlContent = template.html_content ?? "";
|
|
751
|
-
saveTemplate(template.id, htmlContent);
|
|
752
|
-
} catch (err) {
|
|
753
|
-
warn(`Could not save template for preview: ${err instanceof Error ? err.message : String(err)}`);
|
|
754
|
-
return null;
|
|
755
|
-
}
|
|
756
|
-
try {
|
|
757
|
-
const previewUrl = await getPreviewUrl(template.id);
|
|
758
|
-
if (previewUrl === null) {
|
|
759
|
-
warn("Could not start preview server");
|
|
760
|
-
return null;
|
|
761
|
-
}
|
|
762
|
-
return previewUrl;
|
|
763
|
-
} catch (err) {
|
|
764
|
-
warn(`Could not generate preview URL: ${err instanceof Error ? err.message : String(err)}`);
|
|
765
|
-
return null;
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
function createTemplatesCommand() {
|
|
769
|
-
const cmd = new Command3("templates").description(`Manage email templates
|
|
157
|
+
`);}catch{}});}async function Y(n,t){let e=N.resolve(n);return new Promise((o,i)=>{k=Be.createServer((a,r)=>{if(a.url==="/__live-reload"){r.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive","Access-Control-Allow-Origin":"*"}),r.write(`data: connected
|
|
158
|
+
|
|
159
|
+
`),I.push(r),a.on("close",()=>{I=I.filter(s=>s!==r);});return}if(a.method==="GET"){try{let s=S.readFileSync(e,"utf-8"),f=Qe(s);r.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),r.end(f);}catch(s){r.writeHead(500,{"Content-Type":"text/plain"}),r.end(`Error reading file: ${s instanceof Error?s.message:String(s)}`);}return}r.writeHead(405,{"Content-Type":"text/plain"}),r.end("Method Not Allowed");}),k.listen(0,"127.0.0.1",()=>{let a=k.address();if(a&&typeof a=="object"){V=a.port;let r=null;q=S.watch(e,s=>{s==="change"&&(r&&clearTimeout(r),r=setTimeout(()=>{Xe(),t?.();},100));}),o(V);}else i(new Error("Failed to get server address"));}),k.on("error",a=>{i(new Error(`Failed to start server: ${a.message}`));});})}async function Q(){if(q&&(q.close(),q=null),I.forEach(n=>{try{n.end();}catch{}}),I=[],k)return new Promise(n=>{k.close(()=>{k=null,V=null,n();});})}async function xe(n){try{let t=n.html_content??"";J(n.id,t);}catch(t){return v(`Could not save template for preview: ${t instanceof Error?t.message:String(t)}`),null}try{let t=await z(n.id);return t===null?(v("Could not start preview server"),null):t}catch(t){return v(`Could not generate preview URL: ${t instanceof Error?t.message:String(t)}`),null}}function Se(){let n=new Command("templates").description(`Manage email templates
|
|
770
160
|
|
|
771
161
|
AGENTIC LIVE EDITING:
|
|
772
162
|
draft Create new template with live preview
|
|
773
163
|
edit <id> Edit existing template with live preview
|
|
774
164
|
|
|
775
165
|
Both commands save HTML to a local file and start a hot-reload server.
|
|
776
|
-
Edit the file and see changes instantly in the browser.`);
|
|
777
|
-
|
|
778
|
-
try {
|
|
779
|
-
const config = loadConfig();
|
|
780
|
-
const client = new Emailr3({
|
|
781
|
-
apiKey: config.apiKey,
|
|
782
|
-
baseUrl: config.baseUrl
|
|
783
|
-
});
|
|
784
|
-
const result = await client.templates.list({
|
|
785
|
-
limit: parseInt(options.limit, 10),
|
|
786
|
-
page: parseInt(options.page, 10)
|
|
787
|
-
});
|
|
788
|
-
if (options.format === "json") {
|
|
789
|
-
output(result, "json");
|
|
790
|
-
} else {
|
|
791
|
-
const templates = result.map((t) => ({
|
|
792
|
-
ID: t.id,
|
|
793
|
-
Name: t.name,
|
|
794
|
-
Subject: t.subject,
|
|
795
|
-
Variables: t.variables?.join(", ") || "-",
|
|
796
|
-
Created: t.created_at
|
|
797
|
-
}));
|
|
798
|
-
output(templates, "table");
|
|
799
|
-
console.log(`
|
|
800
|
-
Total: ${result.length}`);
|
|
801
|
-
}
|
|
802
|
-
} catch (err) {
|
|
803
|
-
error(err instanceof Error ? err.message : "Failed to list templates");
|
|
804
|
-
process.exit(1);
|
|
805
|
-
}
|
|
806
|
-
});
|
|
807
|
-
cmd.command("get <id>").description("Get a template by ID").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
808
|
-
try {
|
|
809
|
-
const config = loadConfig();
|
|
810
|
-
const client = new Emailr3({
|
|
811
|
-
apiKey: config.apiKey,
|
|
812
|
-
baseUrl: config.baseUrl
|
|
813
|
-
});
|
|
814
|
-
const template = await client.templates.get(id);
|
|
815
|
-
const previewUrl = await handleTemplatePreview({
|
|
816
|
-
id: template.id,
|
|
817
|
-
html_content: template.html_content ?? void 0
|
|
818
|
-
});
|
|
819
|
-
if (options.format === "json") {
|
|
820
|
-
output({
|
|
821
|
-
...template,
|
|
822
|
-
preview_url: previewUrl
|
|
823
|
-
}, "json");
|
|
824
|
-
} else {
|
|
825
|
-
const tableData = {
|
|
826
|
-
ID: template.id,
|
|
827
|
-
Name: template.name,
|
|
828
|
-
Subject: template.subject,
|
|
829
|
-
Variables: template.variables?.join(", ") || "-",
|
|
830
|
-
Created: template.created_at
|
|
831
|
-
};
|
|
832
|
-
if (previewUrl) {
|
|
833
|
-
tableData["Preview URL"] = previewUrl;
|
|
834
|
-
}
|
|
835
|
-
output(tableData, "table");
|
|
836
|
-
}
|
|
837
|
-
} catch (err) {
|
|
838
|
-
error(err instanceof Error ? err.message : "Failed to get template");
|
|
839
|
-
process.exit(1);
|
|
840
|
-
}
|
|
841
|
-
});
|
|
842
|
-
cmd.command("create").description(`Create a new template
|
|
166
|
+
Edit the file and see changes instantly in the browser.`);return n.command("list").description("List all templates").option("--limit <number>","Number of templates to return","20").option("--page <number>","Page number","1").option("--format <format>","Output format (json|table)","table").action(async t=>{try{let e=m(),i=await new Emailr({apiKey:e.apiKey,baseUrl:e.baseUrl}).templates.list({limit:parseInt(t.limit,10),page:parseInt(t.page,10)});if(t.format==="json")l(i,"json");else {let a=i.map(r=>({ID:r.id,Name:r.name,Subject:r.subject,Variables:r.variables?.join(", ")||"-",Created:r.created_at}));l(a,"table"),console.log(`
|
|
167
|
+
Total: ${i.length}`);}}catch(e){c(e instanceof Error?e.message:"Failed to list templates"),process.exit(1);}}),n.command("get <id>").description("Get a template by ID").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),a=await new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}).templates.get(t),r=await xe({id:a.id,html_content:a.html_content??void 0});if(e.format==="json")l({...a,preview_url:r},"json");else {let s={ID:a.id,Name:a.name,Subject:a.subject,Variables:a.variables?.join(", ")||"-",Created:a.created_at};r&&(s["Preview URL"]=r),l(s,"table");}}catch(o){c(o instanceof Error?o.message:"Failed to get template"),process.exit(1);}}),n.command("create").description(`Create a new template
|
|
843
168
|
|
|
844
169
|
TIP: For live preview while building, use "emailr templates draft" first.
|
|
845
|
-
It creates a local file with hot-reload preview, then use this command to save.`).requiredOption("--name <name>",
|
|
846
|
-
|
|
847
|
-
const config = loadConfig();
|
|
848
|
-
const client = new Emailr3({
|
|
849
|
-
apiKey: config.apiKey,
|
|
850
|
-
baseUrl: config.baseUrl
|
|
851
|
-
});
|
|
852
|
-
const request = {
|
|
853
|
-
name: options.name,
|
|
854
|
-
subject: options.subject
|
|
855
|
-
};
|
|
856
|
-
if (options.htmlFile) {
|
|
857
|
-
const fs6 = await import("fs");
|
|
858
|
-
request.html_content = fs6.readFileSync(options.htmlFile, "utf-8");
|
|
859
|
-
} else if (options.html) {
|
|
860
|
-
request.html_content = options.html;
|
|
861
|
-
}
|
|
862
|
-
if (options.textFile) {
|
|
863
|
-
const fs6 = await import("fs");
|
|
864
|
-
request.text_content = fs6.readFileSync(options.textFile, "utf-8");
|
|
865
|
-
} else if (options.text) {
|
|
866
|
-
request.text_content = options.text;
|
|
867
|
-
}
|
|
868
|
-
if (options.from) request.from_email = options.from;
|
|
869
|
-
if (options.replyTo) request.reply_to = options.replyTo;
|
|
870
|
-
if (options.previewText) request.preview_text = options.previewText;
|
|
871
|
-
const template = await client.templates.create(request);
|
|
872
|
-
const previewUrl = await handleTemplatePreview({
|
|
873
|
-
id: template.id,
|
|
874
|
-
html_content: template.html_content ?? void 0
|
|
875
|
-
});
|
|
876
|
-
if (options.format === "json") {
|
|
877
|
-
output({
|
|
878
|
-
...template,
|
|
879
|
-
preview_url: previewUrl
|
|
880
|
-
}, "json");
|
|
881
|
-
} else {
|
|
882
|
-
success(`Template created: ${template.id}`);
|
|
883
|
-
const tableData = {
|
|
884
|
-
ID: template.id,
|
|
885
|
-
Name: template.name,
|
|
886
|
-
Subject: template.subject,
|
|
887
|
-
Variables: template.variables?.join(", ") || "-"
|
|
888
|
-
};
|
|
889
|
-
if (previewUrl) {
|
|
890
|
-
tableData["Preview URL"] = previewUrl;
|
|
891
|
-
}
|
|
892
|
-
output(tableData, "table");
|
|
893
|
-
if (previewUrl) {
|
|
894
|
-
console.log(`
|
|
895
|
-
Open the Preview URL in your browser to view the rendered template.`);
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
} catch (err) {
|
|
899
|
-
error(err instanceof Error ? err.message : "Failed to create template");
|
|
900
|
-
process.exit(1);
|
|
901
|
-
}
|
|
902
|
-
});
|
|
903
|
-
cmd.command("update <id>").description(`Update a template
|
|
170
|
+
It creates a local file with hot-reload preview, then use this command to save.`).requiredOption("--name <name>","Template name").requiredOption("--subject <subject>","Email subject").option("--html <html>","HTML content").option("--text <text>","Plain text content").option("--html-file <path>","Read HTML content from file").option("--text-file <path>","Read text content from file").option("--from <email>","Default from email").option("--reply-to <email>","Default reply-to email").option("--preview-text <text>","Preview text").option("--format <format>","Output format (json|table)","table").action(async t=>{try{let e=m(),o=new Emailr({apiKey:e.apiKey,baseUrl:e.baseUrl}),i={name:t.name,subject:t.subject};if(t.htmlFile){let s=await import('fs');i.html_content=s.readFileSync(t.htmlFile,"utf-8");}else t.html&&(i.html_content=t.html);if(t.textFile){let s=await import('fs');i.text_content=s.readFileSync(t.textFile,"utf-8");}else t.text&&(i.text_content=t.text);t.from&&(i.from_email=t.from),t.replyTo&&(i.reply_to=t.replyTo),t.previewText&&(i.preview_text=t.previewText);let a=await o.templates.create(i),r=await xe({id:a.id,html_content:a.html_content??void 0});if(t.format==="json")l({...a,preview_url:r},"json");else {d(`Template created: ${a.id}`);let s={ID:a.id,Name:a.name,Subject:a.subject,Variables:a.variables?.join(", ")||"-"};r&&(s["Preview URL"]=r),l(s,"table"),r&&console.log(`
|
|
171
|
+
Open the Preview URL in your browser to view the rendered template.`);}}catch(e){c(e instanceof Error?e.message:"Failed to create template"),process.exit(1);}}),n.command("update <id>").description(`Update a template
|
|
904
172
|
|
|
905
173
|
TIP: For live preview while editing, use "emailr templates edit <id>" first.
|
|
906
|
-
It downloads the template with hot-reload preview, then use this command to save.`).option("--name <name>",
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
baseUrl: config.baseUrl
|
|
912
|
-
});
|
|
913
|
-
const request = {};
|
|
914
|
-
if (options.name) request.name = options.name;
|
|
915
|
-
if (options.subject) request.subject = options.subject;
|
|
916
|
-
if (options.htmlFile) {
|
|
917
|
-
const fs6 = await import("fs");
|
|
918
|
-
request.html_content = fs6.readFileSync(options.htmlFile, "utf-8");
|
|
919
|
-
} else if (options.html) {
|
|
920
|
-
request.html_content = options.html;
|
|
921
|
-
}
|
|
922
|
-
if (options.textFile) {
|
|
923
|
-
const fs6 = await import("fs");
|
|
924
|
-
request.text_content = fs6.readFileSync(options.textFile, "utf-8");
|
|
925
|
-
} else if (options.text) {
|
|
926
|
-
request.text_content = options.text;
|
|
927
|
-
}
|
|
928
|
-
if (options.from) request.from_email = options.from;
|
|
929
|
-
if (options.replyTo) request.reply_to = options.replyTo;
|
|
930
|
-
if (options.previewText) request.preview_text = options.previewText;
|
|
931
|
-
const template = await client.templates.update(id, request);
|
|
932
|
-
if (options.format === "json") {
|
|
933
|
-
output(template, "json");
|
|
934
|
-
} else {
|
|
935
|
-
success(`Template updated: ${template.id}`);
|
|
936
|
-
const tableData = {
|
|
937
|
-
ID: template.id,
|
|
938
|
-
Name: template.name,
|
|
939
|
-
Subject: template.subject,
|
|
940
|
-
Variables: template.variables?.join(", ") || "-",
|
|
941
|
-
Updated: template.updated_at
|
|
942
|
-
};
|
|
943
|
-
output(tableData, "table");
|
|
944
|
-
}
|
|
945
|
-
} catch (err) {
|
|
946
|
-
error(err instanceof Error ? err.message : "Failed to update template");
|
|
947
|
-
process.exit(1);
|
|
948
|
-
}
|
|
949
|
-
});
|
|
950
|
-
cmd.command("delete <id>").description("Delete a template").action(async (id) => {
|
|
951
|
-
try {
|
|
952
|
-
const config = loadConfig();
|
|
953
|
-
const client = new Emailr3({
|
|
954
|
-
apiKey: config.apiKey,
|
|
955
|
-
baseUrl: config.baseUrl
|
|
956
|
-
});
|
|
957
|
-
await client.templates.delete(id);
|
|
958
|
-
success(`Template deleted: ${id}`);
|
|
959
|
-
} catch (err) {
|
|
960
|
-
error(err instanceof Error ? err.message : "Failed to delete template");
|
|
961
|
-
process.exit(1);
|
|
962
|
-
}
|
|
963
|
-
});
|
|
964
|
-
cmd.command("preview <id>").description("Preview a template in the browser (keeps server running until Ctrl+C)").option("--no-open", "Do not automatically open browser").action(async (id, options) => {
|
|
965
|
-
try {
|
|
966
|
-
const config = loadConfig();
|
|
967
|
-
const client = new Emailr3({
|
|
968
|
-
apiKey: config.apiKey,
|
|
969
|
-
baseUrl: config.baseUrl
|
|
970
|
-
});
|
|
971
|
-
console.log(`Fetching template ${id}...`);
|
|
972
|
-
const template = await client.templates.get(id);
|
|
973
|
-
const htmlContent = template.html_content ?? "";
|
|
974
|
-
saveTemplate(template.id, htmlContent);
|
|
975
|
-
const previewUrl = await getPreviewUrl(template.id);
|
|
976
|
-
if (!previewUrl) {
|
|
977
|
-
error("Failed to start preview server");
|
|
978
|
-
process.exit(1);
|
|
979
|
-
}
|
|
980
|
-
keepServerAlive();
|
|
981
|
-
console.log(`
|
|
982
|
-
Template: ${template.name}`);
|
|
983
|
-
console.log(`Preview URL: ${previewUrl}`);
|
|
984
|
-
if (options.open !== false) {
|
|
985
|
-
try {
|
|
986
|
-
const open = await import("open");
|
|
987
|
-
await open.default(previewUrl);
|
|
988
|
-
console.log("\nBrowser opened. Press Ctrl+C to stop the preview server.");
|
|
989
|
-
} catch {
|
|
990
|
-
console.log("\nCould not open browser automatically. Open the URL above manually.");
|
|
991
|
-
console.log("Press Ctrl+C to stop the preview server.");
|
|
992
|
-
}
|
|
993
|
-
} else {
|
|
994
|
-
console.log("\nOpen the URL above in your browser.");
|
|
995
|
-
console.log("Press Ctrl+C to stop the preview server.");
|
|
996
|
-
}
|
|
997
|
-
process.on("SIGINT", async () => {
|
|
998
|
-
console.log("\n\nStopping preview server...");
|
|
999
|
-
const server = getPreviewServer();
|
|
1000
|
-
await server.stop();
|
|
1001
|
-
console.log("Done.");
|
|
1002
|
-
process.exit(0);
|
|
1003
|
-
});
|
|
1004
|
-
} catch (err) {
|
|
1005
|
-
error(err instanceof Error ? err.message : "Failed to preview template");
|
|
1006
|
-
process.exit(1);
|
|
1007
|
-
}
|
|
1008
|
-
});
|
|
1009
|
-
cmd.command("edit <id>").description(`Start a live editing session for a template.
|
|
174
|
+
It downloads the template with hot-reload preview, then use this command to save.`).option("--name <name>","Template name").option("--subject <subject>","Email subject").option("--html <html>","HTML content").option("--text <text>","Plain text content").option("--html-file <path>","Read HTML content from file").option("--text-file <path>","Read text content from file").option("--from <email>","Default from email").option("--reply-to <email>","Default reply-to email").option("--preview-text <text>","Preview text").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),i=new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}),a={};if(e.name&&(a.name=e.name),e.subject&&(a.subject=e.subject),e.htmlFile){let s=await import('fs');a.html_content=s.readFileSync(e.htmlFile,"utf-8");}else e.html&&(a.html_content=e.html);if(e.textFile){let s=await import('fs');a.text_content=s.readFileSync(e.textFile,"utf-8");}else e.text&&(a.text_content=e.text);e.from&&(a.from_email=e.from),e.replyTo&&(a.reply_to=e.replyTo),e.previewText&&(a.preview_text=e.previewText);let r=await i.templates.update(t,a);if(e.format==="json")l(r,"json");else {d(`Template updated: ${r.id}`);let s={ID:r.id,Name:r.name,Subject:r.subject,Variables:r.variables?.join(", ")||"-",Updated:r.updated_at};l(s,"table");}}catch(o){c(o instanceof Error?o.message:"Failed to update template"),process.exit(1);}}),n.command("delete <id>").description("Delete a template").action(async t=>{try{let e=m();await new Emailr({apiKey:e.apiKey,baseUrl:e.baseUrl}).templates.delete(t),d(`Template deleted: ${t}`);}catch(e){c(e instanceof Error?e.message:"Failed to delete template"),process.exit(1);}}),n.command("preview <id>").description("Preview a template in the browser (keeps server running until Ctrl+C)").option("--no-open","Do not automatically open browser").action(async(t,e)=>{try{let o=m(),i=new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl});console.log(`Fetching template ${t}...`);let a=await i.templates.get(t),r=a.html_content??"";J(a.id,r);let s=await z(a.id);if(s||(c("Failed to start preview server"),process.exit(1)),ye(),console.log(`
|
|
175
|
+
Template: ${a.name}`),console.log(`Preview URL: ${s}`),e.open!==!1)try{await(await import('open')).default(s),console.log(`
|
|
176
|
+
Browser opened. Press Ctrl+C to stop the preview server.`);}catch{console.log(`
|
|
177
|
+
Could not open browser automatically. Open the URL above manually.`),console.log("Press Ctrl+C to stop the preview server.");}else console.log(`
|
|
178
|
+
Open the URL above in your browser.`),console.log("Press Ctrl+C to stop the preview server.");process.on("SIGINT",async()=>{console.log(`
|
|
1010
179
|
|
|
1011
|
-
|
|
1012
|
-
|
|
180
|
+
Stopping preview server...`),await he().stop(),console.log("Done."),process.exit(0);});}catch(o){c(o instanceof Error?o.message:"Failed to preview template"),process.exit(1);}}),n.command("edit <id>").description(`Start a live editing session for a template.
|
|
181
|
+
|
|
182
|
+
AGENTIC WORKFLOW (for AI agents, use --background):
|
|
183
|
+
1. Run: emailr templates edit <id> --file ./template.html --background
|
|
1013
184
|
2. Edit the file at ./template.html - browser auto-refreshes on save
|
|
1014
185
|
3. When done: emailr templates update <id> --html-file ./template.html
|
|
186
|
+
4. Stop server: emailr templates stop-preview
|
|
1015
187
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
188
|
+
INTERACTIVE MODE (default):
|
|
189
|
+
1. Run: emailr templates edit <id> --file ./template.html
|
|
190
|
+
2. Edit the file - browser auto-refreshes on save
|
|
191
|
+
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(t,e)=>{try{let o=m(),i=new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}),a=N.resolve(e.file);console.log(`Fetching template ${t}...`);let r=await i.templates.get(t),s=r.html_content??"";if(S.writeFileSync(a,s,"utf-8"),console.log(`Template saved to: ${a}`),e.background){let{spawn:y}=await import('child_process'),M=await import('os'),H=N.join(M.tmpdir(),"emailr-preview.pid");try{let L=S.readFileSync(H,"utf-8").trim();process.kill(parseInt(L,10),"SIGTERM");}catch{}let x=`
|
|
192
|
+
const http = require('http');
|
|
193
|
+
const fs = require('fs');
|
|
194
|
+
const path = require('path');
|
|
195
|
+
const os = require('os');
|
|
196
|
+
const filePath = ${JSON.stringify(a)};
|
|
197
|
+
const pidFile = path.join(os.tmpdir(), 'emailr-preview.pid');
|
|
198
|
+
const portFile = path.join(os.tmpdir(), 'emailr-preview.port');
|
|
199
|
+
let clients = [];
|
|
200
|
+
const script = '<script>(function(){const e=new EventSource("/__live-reload");e.onmessage=function(d){if(d.data==="reload")location.reload()};})();</script>';
|
|
201
|
+
const server = http.createServer((req, res) => {
|
|
202
|
+
if (req.url === '/__live-reload') {
|
|
203
|
+
res.writeHead(200, {'Content-Type':'text/event-stream','Cache-Control':'no-cache','Connection':'keep-alive'});
|
|
204
|
+
res.write('data: connected\\n\\n');
|
|
205
|
+
clients.push(res);
|
|
206
|
+
req.on('close', () => { clients = clients.filter(c => c !== res); });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
let html = fs.readFileSync(filePath, 'utf-8');
|
|
211
|
+
html = html.includes('</body>') ? html.replace('</body>', script + '</body>') : html + script;
|
|
212
|
+
res.writeHead(200, {'Content-Type':'text/html; charset=utf-8'});
|
|
213
|
+
res.end(html);
|
|
214
|
+
} catch (e) {
|
|
215
|
+
res.writeHead(500);
|
|
216
|
+
res.end('Error: ' + e.message);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
server.listen(0, '127.0.0.1', () => {
|
|
220
|
+
const port = server.address().port;
|
|
221
|
+
fs.writeFileSync(pidFile, String(process.pid));
|
|
222
|
+
fs.writeFileSync(portFile, String(port));
|
|
223
|
+
console.log('PORT:' + port);
|
|
224
|
+
let timer;
|
|
225
|
+
fs.watch(filePath, () => {
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
timer = setTimeout(() => clients.forEach(c => { try { c.write('data: reload\\n\\n'); } catch {} }), 100);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
process.on('SIGTERM', () => { try { fs.unlinkSync(pidFile); fs.unlinkSync(portFile); } catch {} process.exit(0); });
|
|
231
|
+
`,_=y("node",["-e",x],{detached:!0,stdio:["ignore","pipe","ignore"]}),K="";await new Promise(L=>{_.stdout?.on("data",Ke=>{let ne=Ke.toString().match(/PORT:(\d+)/);ne&&(K=ne[1],L());}),setTimeout(L,3e3);}),_.unref();let ae=`http://127.0.0.1:${K}/`;if(console.log(`
|
|
232
|
+
Template: ${r.name}`),console.log(`Template ID: ${r.id}`),console.log(`Live Preview: ${ae}`),console.log(`File: ${a}`),console.log(`
|
|
233
|
+
Preview server running in background (PID: ${_.pid}).`),console.log("Edit the file and see live updates in the browser."),console.log(`
|
|
234
|
+
When done:`),console.log(` emailr templates update ${t} --html-file ${e.file}`),console.log(" emailr templates stop-preview"),e.open!==!1)try{await(await import('open')).default(ae);}catch{}process.exit(0);}let g=`http://127.0.0.1:${await Y(a,()=>{console.log("File changed - browser refreshed");})}/`;if(console.log(`
|
|
235
|
+
Template: ${r.name}`),console.log(`Template ID: ${r.id}`),console.log(`Live Preview: ${g}`),console.log(`File: ${a}`),console.log(`
|
|
236
|
+
Watching for changes... Edit the file and see live updates.`),console.log(`When done, run: emailr templates update ${t} --html-file ${e.file}`),console.log(`
|
|
237
|
+
Press Ctrl+C to stop.`),e.open!==!1)try{await(await import('open')).default(g);}catch{}process.on("SIGINT",async()=>{console.log(`
|
|
238
|
+
|
|
239
|
+
Stopping live preview server...`),await Q(),console.log("Done."),console.log(`
|
|
240
|
+
To save changes: emailr templates update ${t} --html-file ${e.file}`),process.exit(0);});}catch(o){c(o instanceof Error?o.message:"Failed to edit template"),process.exit(1);}}),n.command("draft").description(`Start a live drafting session for a new template.
|
|
241
|
+
|
|
242
|
+
AGENTIC WORKFLOW (for AI agents, use --background):
|
|
243
|
+
1. Run: emailr templates draft --file ./new-template.html --background
|
|
1066
244
|
2. Edit the file at ./new-template.html - browser auto-refreshes on save
|
|
1067
245
|
3. When done: emailr templates create --name "My Template" --subject "Subject" --html-file ./new-template.html
|
|
246
|
+
4. Stop server: emailr templates stop-preview
|
|
1068
247
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
if (options.blank) {
|
|
1074
|
-
htmlContent = "";
|
|
1075
|
-
} else {
|
|
1076
|
-
htmlContent = `<!DOCTYPE html>
|
|
248
|
+
INTERACTIVE MODE (default):
|
|
249
|
+
1. Run: emailr templates draft --file ./new-template.html
|
|
250
|
+
2. Edit the file - browser auto-refreshes on save
|
|
251
|
+
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 t=>{try{let e=N.resolve(t.file),o;if(t.blank?o="":o=`<!DOCTYPE html>
|
|
1077
252
|
<html>
|
|
1078
253
|
<head>
|
|
1079
254
|
<meta charset="UTF-8">
|
|
@@ -1088,924 +263,77 @@ This command creates a starter HTML file and starts a live preview server with h
|
|
|
1088
263
|
margin: 0 auto;
|
|
1089
264
|
padding: 20px;
|
|
1090
265
|
}
|
|
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
|
-
}
|
|
266
|
+
.header { text-align: center; padding: 20px 0; border-bottom: 1px solid #eee; }
|
|
267
|
+
.content { padding: 30px 0; }
|
|
268
|
+
.button { display: inline-block; padding: 12px 24px; background-color: #5e5de7; color: white; text-decoration: none; border-radius: 5px; font-weight: bold; }
|
|
269
|
+
.footer { text-align: center; padding: 20px 0; border-top: 1px solid #eee; color: #888; font-size: 12px; }
|
|
1115
270
|
</style>
|
|
1116
271
|
</head>
|
|
1117
272
|
<body>
|
|
1118
|
-
<div class="header">
|
|
1119
|
-
<h1>Your Company</h1>
|
|
1120
|
-
</div>
|
|
1121
|
-
|
|
273
|
+
<div class="header"><h1>Your Company</h1></div>
|
|
1122
274
|
<div class="content">
|
|
1123
275
|
<h2>Hello {{name}},</h2>
|
|
1124
|
-
|
|
1125
276
|
<p>This is a starter template. Edit this file and see your changes live in the browser!</p>
|
|
1126
|
-
|
|
1127
277
|
<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
|
-
|
|
278
|
+
<p style="text-align: center; margin: 30px 0;"><a href="https://example.com" class="button">Call to Action</a></p>
|
|
1133
279
|
<p>Best regards,<br>Your Team</p>
|
|
1134
280
|
</div>
|
|
1135
|
-
|
|
1136
281
|
<div class="footer">
|
|
1137
282
|
<p>\xA9 2024 Your Company. All rights reserved.</p>
|
|
1138
283
|
<p><a href="{{unsubscribe_link}}">Unsubscribe</a></p>
|
|
1139
284
|
</div>
|
|
1140
285
|
</body>
|
|
1141
|
-
</html
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
baseUrl: config.baseUrl
|
|
1192
|
-
});
|
|
1193
|
-
const domains = await client.domains.list();
|
|
1194
|
-
if (options.format === "json") {
|
|
1195
|
-
output(domains, "json");
|
|
1196
|
-
} else {
|
|
1197
|
-
const formatted = domains.map((d) => ({
|
|
1198
|
-
ID: d.id,
|
|
1199
|
-
Domain: d.domain,
|
|
1200
|
-
Status: d.status,
|
|
1201
|
-
DKIM: d.dkim_verified,
|
|
1202
|
-
SPF: d.spf_verified,
|
|
1203
|
-
DMARC: d.dmarc_verified,
|
|
1204
|
-
Created: d.created_at
|
|
1205
|
-
}));
|
|
1206
|
-
output(formatted, "table");
|
|
1207
|
-
}
|
|
1208
|
-
} catch (err) {
|
|
1209
|
-
error(err instanceof Error ? err.message : "Failed to list domains");
|
|
1210
|
-
process.exit(1);
|
|
1211
|
-
}
|
|
1212
|
-
});
|
|
1213
|
-
cmd.command("get <id>").description("Get a domain by ID").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
1214
|
-
try {
|
|
1215
|
-
const config = loadConfig();
|
|
1216
|
-
const client = new Emailr4({
|
|
1217
|
-
apiKey: config.apiKey,
|
|
1218
|
-
baseUrl: config.baseUrl
|
|
1219
|
-
});
|
|
1220
|
-
const domain = await client.domains.get(id);
|
|
1221
|
-
output(domain, options.format);
|
|
1222
|
-
} catch (err) {
|
|
1223
|
-
error(err instanceof Error ? err.message : "Failed to get domain");
|
|
1224
|
-
process.exit(1);
|
|
1225
|
-
}
|
|
1226
|
-
});
|
|
1227
|
-
cmd.command("add <domain>").description("Add a new domain").option("--receiving-subdomain <subdomain>", "Subdomain for receiving emails").option("--format <format>", "Output format (json|table)", "table").action(async (domainName, options) => {
|
|
1228
|
-
try {
|
|
1229
|
-
const config = loadConfig();
|
|
1230
|
-
const client = new Emailr4({
|
|
1231
|
-
apiKey: config.apiKey,
|
|
1232
|
-
baseUrl: config.baseUrl
|
|
1233
|
-
});
|
|
1234
|
-
const request = {
|
|
1235
|
-
domain: domainName
|
|
1236
|
-
};
|
|
1237
|
-
if (options.receivingSubdomain) {
|
|
1238
|
-
request.receivingSubdomain = options.receivingSubdomain;
|
|
1239
|
-
}
|
|
1240
|
-
const domain = await client.domains.add(request);
|
|
1241
|
-
if (options.format === "json") {
|
|
1242
|
-
output(domain, "json");
|
|
1243
|
-
} else {
|
|
1244
|
-
success(`Domain added: ${domain.domain}`);
|
|
1245
|
-
info("Add the following DNS records to verify your domain:");
|
|
1246
|
-
console.log("");
|
|
1247
|
-
if (domain.dns_records) {
|
|
1248
|
-
const records = [
|
|
1249
|
-
{ Type: "DKIM", ...formatDnsRecord(domain.dns_records.dkim) },
|
|
1250
|
-
{ Type: "SPF", ...formatDnsRecord(domain.dns_records.spf) },
|
|
1251
|
-
{ Type: "DMARC", ...formatDnsRecord(domain.dns_records.dmarc) }
|
|
1252
|
-
];
|
|
1253
|
-
output(records, "table");
|
|
1254
|
-
}
|
|
1255
|
-
console.log("");
|
|
1256
|
-
info(`Run 'emailr domains verify ${domain.id}' after adding DNS records`);
|
|
1257
|
-
}
|
|
1258
|
-
} catch (err) {
|
|
1259
|
-
error(err instanceof Error ? err.message : "Failed to add domain");
|
|
1260
|
-
process.exit(1);
|
|
1261
|
-
}
|
|
1262
|
-
});
|
|
1263
|
-
cmd.command("verify <id>").description("Verify a domain").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
1264
|
-
try {
|
|
1265
|
-
const config = loadConfig();
|
|
1266
|
-
const client = new Emailr4({
|
|
1267
|
-
apiKey: config.apiKey,
|
|
1268
|
-
baseUrl: config.baseUrl
|
|
1269
|
-
});
|
|
1270
|
-
const result = await client.domains.verify(id);
|
|
1271
|
-
if (options.format === "json") {
|
|
1272
|
-
output(result, "json");
|
|
1273
|
-
} else {
|
|
1274
|
-
if (result.verified) {
|
|
1275
|
-
success("Domain verified successfully!");
|
|
1276
|
-
} else {
|
|
1277
|
-
info(`Domain status: ${result.status}`);
|
|
1278
|
-
if (result.dkim_status) {
|
|
1279
|
-
info(`DKIM status: ${result.dkim_status}`);
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
} catch (err) {
|
|
1284
|
-
error(err instanceof Error ? err.message : "Failed to verify domain");
|
|
1285
|
-
process.exit(1);
|
|
1286
|
-
}
|
|
1287
|
-
});
|
|
1288
|
-
cmd.command("check-dns <id>").description("Check DNS records for a domain").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
1289
|
-
try {
|
|
1290
|
-
const config = loadConfig();
|
|
1291
|
-
const client = new Emailr4({
|
|
1292
|
-
apiKey: config.apiKey,
|
|
1293
|
-
baseUrl: config.baseUrl
|
|
1294
|
-
});
|
|
1295
|
-
const result = await client.domains.checkDns(id);
|
|
1296
|
-
if (options.format === "json") {
|
|
1297
|
-
output(result, "json");
|
|
1298
|
-
} else {
|
|
1299
|
-
const records = Object.entries(result).map(([name, status]) => ({
|
|
1300
|
-
Record: name,
|
|
1301
|
-
Verified: status.verified,
|
|
1302
|
-
Expected: status.expected || "-",
|
|
1303
|
-
Found: status.found?.join(", ") || "-"
|
|
1304
|
-
}));
|
|
1305
|
-
output(records, "table");
|
|
1306
|
-
}
|
|
1307
|
-
} catch (err) {
|
|
1308
|
-
error(err instanceof Error ? err.message : "Failed to check DNS");
|
|
1309
|
-
process.exit(1);
|
|
1310
|
-
}
|
|
1311
|
-
});
|
|
1312
|
-
cmd.command("delete <id>").description("Delete a domain").action(async (id) => {
|
|
1313
|
-
try {
|
|
1314
|
-
const config = loadConfig();
|
|
1315
|
-
const client = new Emailr4({
|
|
1316
|
-
apiKey: config.apiKey,
|
|
1317
|
-
baseUrl: config.baseUrl
|
|
1318
|
-
});
|
|
1319
|
-
await client.domains.delete(id);
|
|
1320
|
-
success(`Domain deleted: ${id}`);
|
|
1321
|
-
} catch (err) {
|
|
1322
|
-
error(err instanceof Error ? err.message : "Failed to delete domain");
|
|
1323
|
-
process.exit(1);
|
|
1324
|
-
}
|
|
1325
|
-
});
|
|
1326
|
-
return cmd;
|
|
1327
|
-
}
|
|
1328
|
-
function formatDnsRecord(record) {
|
|
1329
|
-
return {
|
|
1330
|
-
"Record Type": record.type,
|
|
1331
|
-
Name: record.name,
|
|
1332
|
-
Value: record.value.length > 50 ? record.value.substring(0, 47) + "..." : record.value,
|
|
1333
|
-
...record.priority !== void 0 && { Priority: record.priority }
|
|
1334
|
-
};
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
// src/commands/config.ts
|
|
1338
|
-
import { Command as Command5 } from "commander";
|
|
1339
|
-
function createConfigCommand() {
|
|
1340
|
-
const cmd = new Command5("config").description("Manage CLI configuration");
|
|
1341
|
-
cmd.command("set <key> <value>").description("Set a configuration value").action(async (key, value) => {
|
|
1342
|
-
try {
|
|
1343
|
-
const validKeys = ["api-key", "base-url", "format"];
|
|
1344
|
-
const normalizedKey = key.toLowerCase();
|
|
1345
|
-
if (!validKeys.includes(normalizedKey)) {
|
|
1346
|
-
error(`Invalid config key: ${key}`);
|
|
1347
|
-
info(`Valid keys: ${validKeys.join(", ")}`);
|
|
1348
|
-
process.exit(1);
|
|
1349
|
-
}
|
|
1350
|
-
const keyMap = {
|
|
1351
|
-
"api-key": "apiKey",
|
|
1352
|
-
"base-url": "baseUrl",
|
|
1353
|
-
"format": "format"
|
|
1354
|
-
};
|
|
1355
|
-
const configKey = keyMap[normalizedKey];
|
|
1356
|
-
if (normalizedKey === "format" && !["json", "table"].includes(value)) {
|
|
1357
|
-
error(`Invalid format value: ${value}`);
|
|
1358
|
-
info("Valid formats: json, table");
|
|
1359
|
-
process.exit(1);
|
|
1360
|
-
}
|
|
1361
|
-
saveConfig({ [configKey]: value });
|
|
1362
|
-
success(`Configuration saved: ${key} = ${normalizedKey === "api-key" ? "***" : value}`);
|
|
1363
|
-
info(`Config file: ${getConfigPath()}`);
|
|
1364
|
-
} catch (err) {
|
|
1365
|
-
error(err instanceof Error ? err.message : "Failed to save configuration");
|
|
1366
|
-
process.exit(1);
|
|
1367
|
-
}
|
|
1368
|
-
});
|
|
1369
|
-
cmd.command("get <key>").description("Get a configuration value").action(async (key) => {
|
|
1370
|
-
try {
|
|
1371
|
-
const validKeys = ["api-key", "base-url", "format"];
|
|
1372
|
-
const normalizedKey = key.toLowerCase();
|
|
1373
|
-
if (!validKeys.includes(normalizedKey)) {
|
|
1374
|
-
error(`Invalid config key: ${key}`);
|
|
1375
|
-
info(`Valid keys: ${validKeys.join(", ")}`);
|
|
1376
|
-
process.exit(1);
|
|
1377
|
-
}
|
|
1378
|
-
const keyMap = {
|
|
1379
|
-
"api-key": "apiKey",
|
|
1380
|
-
"base-url": "baseUrl",
|
|
1381
|
-
"format": "format"
|
|
1382
|
-
};
|
|
1383
|
-
const configKey = keyMap[normalizedKey];
|
|
1384
|
-
const value = getConfigValue(configKey);
|
|
1385
|
-
if (value) {
|
|
1386
|
-
if (normalizedKey === "api-key") {
|
|
1387
|
-
console.log(value.substring(0, 8) + "..." + value.substring(value.length - 4));
|
|
1388
|
-
} else {
|
|
1389
|
-
console.log(value);
|
|
1390
|
-
}
|
|
1391
|
-
} else {
|
|
1392
|
-
info(`${key} is not set`);
|
|
1393
|
-
}
|
|
1394
|
-
} catch (err) {
|
|
1395
|
-
error(err instanceof Error ? err.message : "Failed to get configuration");
|
|
1396
|
-
process.exit(1);
|
|
1397
|
-
}
|
|
1398
|
-
});
|
|
1399
|
-
cmd.command("list").description("List all configuration values").option("--format <format>", "Output format (json|table)", "table").action(async (options) => {
|
|
1400
|
-
try {
|
|
1401
|
-
const config = loadConfig();
|
|
1402
|
-
const configData = {
|
|
1403
|
-
"api-key": config.apiKey ? config.apiKey.substring(0, 8) + "..." + config.apiKey.substring(config.apiKey.length - 4) : "(not set)",
|
|
1404
|
-
"base-url": config.baseUrl || "(default)",
|
|
1405
|
-
"format": config.format || "table"
|
|
1406
|
-
};
|
|
1407
|
-
if (options.format === "json") {
|
|
1408
|
-
output(configData, "json");
|
|
1409
|
-
} else {
|
|
1410
|
-
const rows = Object.entries(configData).map(([key, value]) => ({
|
|
1411
|
-
Key: key,
|
|
1412
|
-
Value: value
|
|
1413
|
-
}));
|
|
1414
|
-
output(rows, "table");
|
|
1415
|
-
}
|
|
1416
|
-
console.log("");
|
|
1417
|
-
info(`Config file: ${getConfigPath()}`);
|
|
1418
|
-
} catch (err) {
|
|
1419
|
-
if (err instanceof Error && err.message.includes("No API key configured")) {
|
|
1420
|
-
info("No configuration found.");
|
|
1421
|
-
info(`Run 'emailr config set api-key <your-api-key>' to get started.`);
|
|
1422
|
-
} else {
|
|
1423
|
-
error(err instanceof Error ? err.message : "Failed to list configuration");
|
|
1424
|
-
process.exit(1);
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
});
|
|
1428
|
-
cmd.command("path").description("Show the configuration file path").action(() => {
|
|
1429
|
-
console.log(getConfigPath());
|
|
1430
|
-
});
|
|
1431
|
-
cmd.command("init").description("Initialize configuration interactively").option("--api-key <key>", "API key to use").option("--base-url <url>", "Base URL for API").action(async (options) => {
|
|
1432
|
-
try {
|
|
1433
|
-
if (options.apiKey) {
|
|
1434
|
-
saveConfig({
|
|
1435
|
-
apiKey: options.apiKey,
|
|
1436
|
-
baseUrl: options.baseUrl
|
|
1437
|
-
});
|
|
1438
|
-
success("Configuration initialized!");
|
|
1439
|
-
info(`Config file: ${getConfigPath()}`);
|
|
1440
|
-
} else {
|
|
1441
|
-
info("Initialize your Emailr CLI configuration:");
|
|
1442
|
-
console.log("");
|
|
1443
|
-
info("Run with --api-key flag:");
|
|
1444
|
-
console.log(" emailr config init --api-key <your-api-key>");
|
|
1445
|
-
console.log("");
|
|
1446
|
-
info("Or set environment variable:");
|
|
1447
|
-
console.log(" export EMAILR_API_KEY=<your-api-key>");
|
|
1448
|
-
}
|
|
1449
|
-
} catch (err) {
|
|
1450
|
-
error(err instanceof Error ? err.message : "Failed to initialize configuration");
|
|
1451
|
-
process.exit(1);
|
|
1452
|
-
}
|
|
1453
|
-
});
|
|
1454
|
-
return cmd;
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
// src/commands/broadcasts.ts
|
|
1458
|
-
import { Command as Command6 } from "commander";
|
|
1459
|
-
import { Emailr as Emailr5 } from "emailr";
|
|
1460
|
-
function createBroadcastsCommand() {
|
|
1461
|
-
const cmd = new Command6("broadcasts").description("Manage broadcast campaigns");
|
|
1462
|
-
cmd.command("list").description("List all broadcasts").option("--status <status>", "Filter by status (draft, scheduled, sending, sent)").option("--limit <number>", "Number of broadcasts to return", "20").option("--format <format>", "Output format (json|table)", "table").action(async (options) => {
|
|
1463
|
-
try {
|
|
1464
|
-
const config = loadConfig();
|
|
1465
|
-
const client = new Emailr5({
|
|
1466
|
-
apiKey: config.apiKey,
|
|
1467
|
-
baseUrl: config.baseUrl
|
|
1468
|
-
});
|
|
1469
|
-
const broadcasts = await client.broadcasts.list({
|
|
1470
|
-
status: options.status,
|
|
1471
|
-
limit: parseInt(options.limit)
|
|
1472
|
-
});
|
|
1473
|
-
if (options.format === "json") {
|
|
1474
|
-
output(broadcasts, "json");
|
|
1475
|
-
} else {
|
|
1476
|
-
if (broadcasts.length === 0) {
|
|
1477
|
-
console.log("No broadcasts found.");
|
|
1478
|
-
return;
|
|
1479
|
-
}
|
|
1480
|
-
const tableData = broadcasts.map((b) => ({
|
|
1481
|
-
ID: b.id,
|
|
1482
|
-
Name: b.name,
|
|
1483
|
-
Subject: b.subject.substring(0, 30) + (b.subject.length > 30 ? "..." : ""),
|
|
1484
|
-
Status: b.status,
|
|
1485
|
-
Recipients: b.total_recipients || 0,
|
|
1486
|
-
"Sent": b.sent_count || 0,
|
|
1487
|
-
"Created": new Date(b.created_at).toLocaleDateString()
|
|
1488
|
-
}));
|
|
1489
|
-
output(tableData, "table");
|
|
1490
|
-
}
|
|
1491
|
-
} catch (err) {
|
|
1492
|
-
error(err instanceof Error ? err.message : "Failed to list broadcasts");
|
|
1493
|
-
process.exit(1);
|
|
1494
|
-
}
|
|
1495
|
-
});
|
|
1496
|
-
cmd.command("get <id>").description("Get broadcast details").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
1497
|
-
try {
|
|
1498
|
-
const config = loadConfig();
|
|
1499
|
-
const client = new Emailr5({
|
|
1500
|
-
apiKey: config.apiKey,
|
|
1501
|
-
baseUrl: config.baseUrl
|
|
1502
|
-
});
|
|
1503
|
-
const broadcast = await client.broadcasts.get(id);
|
|
1504
|
-
if (options.format === "json") {
|
|
1505
|
-
output(broadcast, "json");
|
|
1506
|
-
} else {
|
|
1507
|
-
output({
|
|
1508
|
-
ID: broadcast.id,
|
|
1509
|
-
Name: broadcast.name,
|
|
1510
|
-
Subject: broadcast.subject,
|
|
1511
|
-
"From Email": broadcast.from_email,
|
|
1512
|
-
Status: broadcast.status,
|
|
1513
|
-
"Total Recipients": broadcast.total_recipients || 0,
|
|
1514
|
-
"Sent Count": broadcast.sent_count || 0,
|
|
1515
|
-
"Delivered": broadcast.delivered_count || 0,
|
|
1516
|
-
"Opened": broadcast.opened_count || 0,
|
|
1517
|
-
"Clicked": broadcast.clicked_count || 0,
|
|
1518
|
-
"Bounced": broadcast.bounced_count || 0,
|
|
1519
|
-
"Scheduled At": broadcast.scheduled_at || "N/A",
|
|
1520
|
-
"Started At": broadcast.started_at || "N/A",
|
|
1521
|
-
"Completed At": broadcast.completed_at || "N/A",
|
|
1522
|
-
"Created At": broadcast.created_at
|
|
1523
|
-
}, "table");
|
|
1524
|
-
}
|
|
1525
|
-
} catch (err) {
|
|
1526
|
-
error(err instanceof Error ? err.message : "Failed to get broadcast");
|
|
1527
|
-
process.exit(1);
|
|
1528
|
-
}
|
|
1529
|
-
});
|
|
1530
|
-
cmd.command("create").description("Create a new broadcast").requiredOption("--name <name>", "Broadcast name").requiredOption("--subject <subject>", "Email subject").requiredOption("--from <email>", "Sender email address").option("--template <id>", "Template ID to use").option("--segment <id>", "Segment ID to target").option("--html <html>", "HTML content").option("--text <text>", "Plain text content").option("--schedule <datetime>", "Schedule time (ISO 8601)").option("--format <format>", "Output format (json|table)", "table").action(async (options) => {
|
|
1531
|
-
try {
|
|
1532
|
-
const config = loadConfig();
|
|
1533
|
-
const client = new Emailr5({
|
|
1534
|
-
apiKey: config.apiKey,
|
|
1535
|
-
baseUrl: config.baseUrl
|
|
1536
|
-
});
|
|
1537
|
-
const broadcast = await client.broadcasts.create({
|
|
1538
|
-
name: options.name,
|
|
1539
|
-
subject: options.subject,
|
|
1540
|
-
from_email: options.from,
|
|
1541
|
-
template_id: options.template,
|
|
1542
|
-
segment_id: options.segment,
|
|
1543
|
-
html_content: options.html,
|
|
1544
|
-
text_content: options.text,
|
|
1545
|
-
scheduled_at: options.schedule
|
|
1546
|
-
});
|
|
1547
|
-
if (options.format === "json") {
|
|
1548
|
-
output(broadcast, "json");
|
|
1549
|
-
} else {
|
|
1550
|
-
success("Broadcast created successfully!");
|
|
1551
|
-
output({
|
|
1552
|
-
ID: broadcast.id,
|
|
1553
|
-
Name: broadcast.name,
|
|
1554
|
-
Status: broadcast.status,
|
|
1555
|
-
"Scheduled At": broadcast.scheduled_at || "Not scheduled"
|
|
1556
|
-
}, "table");
|
|
1557
|
-
}
|
|
1558
|
-
} catch (err) {
|
|
1559
|
-
error(err instanceof Error ? err.message : "Failed to create broadcast");
|
|
1560
|
-
process.exit(1);
|
|
1561
|
-
}
|
|
1562
|
-
});
|
|
1563
|
-
cmd.command("send <id>").description("Send a broadcast immediately").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
1564
|
-
try {
|
|
1565
|
-
const config = loadConfig();
|
|
1566
|
-
const client = new Emailr5({
|
|
1567
|
-
apiKey: config.apiKey,
|
|
1568
|
-
baseUrl: config.baseUrl
|
|
1569
|
-
});
|
|
1570
|
-
const result = await client.broadcasts.send(id);
|
|
1571
|
-
if (options.format === "json") {
|
|
1572
|
-
output(result, "json");
|
|
1573
|
-
} else {
|
|
1574
|
-
success("Broadcast sent successfully!");
|
|
1575
|
-
output({
|
|
1576
|
-
Success: result.success,
|
|
1577
|
-
Sent: result.sent,
|
|
1578
|
-
Total: result.total
|
|
1579
|
-
}, "table");
|
|
1580
|
-
}
|
|
1581
|
-
} catch (err) {
|
|
1582
|
-
error(err instanceof Error ? err.message : "Failed to send broadcast");
|
|
1583
|
-
process.exit(1);
|
|
1584
|
-
}
|
|
1585
|
-
});
|
|
1586
|
-
cmd.command("schedule <id>").description("Schedule a broadcast").requiredOption("--at <datetime>", "Schedule time (ISO 8601)").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
1587
|
-
try {
|
|
1588
|
-
const config = loadConfig();
|
|
1589
|
-
const client = new Emailr5({
|
|
1590
|
-
apiKey: config.apiKey,
|
|
1591
|
-
baseUrl: config.baseUrl
|
|
1592
|
-
});
|
|
1593
|
-
const broadcast = await client.broadcasts.schedule(id, options.at);
|
|
1594
|
-
if (options.format === "json") {
|
|
1595
|
-
output(broadcast, "json");
|
|
1596
|
-
} else {
|
|
1597
|
-
success("Broadcast scheduled successfully!");
|
|
1598
|
-
output({
|
|
1599
|
-
ID: broadcast.id,
|
|
1600
|
-
Name: broadcast.name,
|
|
1601
|
-
Status: broadcast.status,
|
|
1602
|
-
"Scheduled At": broadcast.scheduled_at
|
|
1603
|
-
}, "table");
|
|
1604
|
-
}
|
|
1605
|
-
} catch (err) {
|
|
1606
|
-
error(err instanceof Error ? err.message : "Failed to schedule broadcast");
|
|
1607
|
-
process.exit(1);
|
|
1608
|
-
}
|
|
1609
|
-
});
|
|
1610
|
-
cmd.command("cancel <id>").description("Cancel a scheduled broadcast").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
1611
|
-
try {
|
|
1612
|
-
const config = loadConfig();
|
|
1613
|
-
const client = new Emailr5({
|
|
1614
|
-
apiKey: config.apiKey,
|
|
1615
|
-
baseUrl: config.baseUrl
|
|
1616
|
-
});
|
|
1617
|
-
const result = await client.broadcasts.cancel(id);
|
|
1618
|
-
if (options.format === "json") {
|
|
1619
|
-
output(result, "json");
|
|
1620
|
-
} else {
|
|
1621
|
-
success("Broadcast cancelled successfully!");
|
|
1622
|
-
}
|
|
1623
|
-
} catch (err) {
|
|
1624
|
-
error(err instanceof Error ? err.message : "Failed to cancel broadcast");
|
|
1625
|
-
process.exit(1);
|
|
1626
|
-
}
|
|
1627
|
-
});
|
|
1628
|
-
cmd.command("delete <id>").description("Delete a broadcast").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
1629
|
-
try {
|
|
1630
|
-
const config = loadConfig();
|
|
1631
|
-
const client = new Emailr5({
|
|
1632
|
-
apiKey: config.apiKey,
|
|
1633
|
-
baseUrl: config.baseUrl
|
|
1634
|
-
});
|
|
1635
|
-
const result = await client.broadcasts.delete(id);
|
|
1636
|
-
if (options.format === "json") {
|
|
1637
|
-
output(result, "json");
|
|
1638
|
-
} else {
|
|
1639
|
-
success("Broadcast deleted successfully!");
|
|
1640
|
-
}
|
|
1641
|
-
} catch (err) {
|
|
1642
|
-
error(err instanceof Error ? err.message : "Failed to delete broadcast");
|
|
1643
|
-
process.exit(1);
|
|
1644
|
-
}
|
|
1645
|
-
});
|
|
1646
|
-
return cmd;
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
// src/commands/webhooks.ts
|
|
1650
|
-
import { Command as Command7 } from "commander";
|
|
1651
|
-
import { Emailr as Emailr6 } from "emailr";
|
|
1652
|
-
function createWebhooksCommand() {
|
|
1653
|
-
const cmd = new Command7("webhooks").description("Manage webhooks");
|
|
1654
|
-
cmd.command("list").description("List all webhooks").option("--format <format>", "Output format (json|table)", "table").action(async (options) => {
|
|
1655
|
-
try {
|
|
1656
|
-
const config = loadConfig();
|
|
1657
|
-
const client = new Emailr6({
|
|
1658
|
-
apiKey: config.apiKey,
|
|
1659
|
-
baseUrl: config.baseUrl
|
|
1660
|
-
});
|
|
1661
|
-
const webhooks = await client.webhooks.list();
|
|
1662
|
-
if (options.format === "json") {
|
|
1663
|
-
output(webhooks, "json");
|
|
1664
|
-
} else {
|
|
1665
|
-
if (webhooks.length === 0) {
|
|
1666
|
-
console.log("No webhooks found.");
|
|
1667
|
-
return;
|
|
1668
|
-
}
|
|
1669
|
-
const tableData = webhooks.map((w) => ({
|
|
1670
|
-
ID: w.id,
|
|
1671
|
-
Name: w.name,
|
|
1672
|
-
URL: w.url.substring(0, 40) + (w.url.length > 40 ? "..." : ""),
|
|
1673
|
-
Events: w.events.join(", "),
|
|
1674
|
-
Active: w.active ? "Yes" : "No"
|
|
1675
|
-
}));
|
|
1676
|
-
output(tableData, "table");
|
|
1677
|
-
}
|
|
1678
|
-
} catch (err) {
|
|
1679
|
-
error(err instanceof Error ? err.message : "Failed to list webhooks");
|
|
1680
|
-
process.exit(1);
|
|
1681
|
-
}
|
|
1682
|
-
});
|
|
1683
|
-
cmd.command("get <id>").description("Get webhook details").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
1684
|
-
try {
|
|
1685
|
-
const config = loadConfig();
|
|
1686
|
-
const client = new Emailr6({
|
|
1687
|
-
apiKey: config.apiKey,
|
|
1688
|
-
baseUrl: config.baseUrl
|
|
1689
|
-
});
|
|
1690
|
-
const webhook = await client.webhooks.get(id);
|
|
1691
|
-
if (options.format === "json") {
|
|
1692
|
-
output(webhook, "json");
|
|
1693
|
-
} else {
|
|
1694
|
-
output({
|
|
1695
|
-
ID: webhook.id,
|
|
1696
|
-
Name: webhook.name,
|
|
1697
|
-
URL: webhook.url,
|
|
1698
|
-
Events: webhook.events.join(", "),
|
|
1699
|
-
Active: webhook.active ? "Yes" : "No",
|
|
1700
|
-
Secret: webhook.secret,
|
|
1701
|
-
"Created At": webhook.created_at
|
|
1702
|
-
}, "table");
|
|
1703
|
-
}
|
|
1704
|
-
} catch (err) {
|
|
1705
|
-
error(err instanceof Error ? err.message : "Failed to get webhook");
|
|
1706
|
-
process.exit(1);
|
|
1707
|
-
}
|
|
1708
|
-
});
|
|
1709
|
-
cmd.command("create").description("Create a new webhook").requiredOption("--name <name>", "Webhook name").requiredOption("--url <url>", "Webhook URL").requiredOption("--events <events>", "Events to subscribe to (comma-separated)").option("--format <format>", "Output format (json|table)", "table").action(async (options) => {
|
|
1710
|
-
try {
|
|
1711
|
-
const config = loadConfig();
|
|
1712
|
-
const client = new Emailr6({
|
|
1713
|
-
apiKey: config.apiKey,
|
|
1714
|
-
baseUrl: config.baseUrl
|
|
1715
|
-
});
|
|
1716
|
-
const events = options.events.split(",").map((e) => e.trim());
|
|
1717
|
-
const webhook = await client.webhooks.create({
|
|
1718
|
-
name: options.name,
|
|
1719
|
-
url: options.url,
|
|
1720
|
-
events
|
|
1721
|
-
});
|
|
1722
|
-
if (options.format === "json") {
|
|
1723
|
-
output(webhook, "json");
|
|
1724
|
-
} else {
|
|
1725
|
-
success("Webhook created successfully!");
|
|
1726
|
-
output({
|
|
1727
|
-
ID: webhook.id,
|
|
1728
|
-
Name: webhook.name,
|
|
1729
|
-
URL: webhook.url,
|
|
1730
|
-
Secret: webhook.secret
|
|
1731
|
-
}, "table");
|
|
1732
|
-
}
|
|
1733
|
-
} catch (err) {
|
|
1734
|
-
error(err instanceof Error ? err.message : "Failed to create webhook");
|
|
1735
|
-
process.exit(1);
|
|
1736
|
-
}
|
|
1737
|
-
});
|
|
1738
|
-
cmd.command("update <id>").description("Update a webhook").option("--name <name>", "New webhook name").option("--url <url>", "New webhook URL").option("--events <events>", "New events (comma-separated)").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
1739
|
-
try {
|
|
1740
|
-
const config = loadConfig();
|
|
1741
|
-
const client = new Emailr6({
|
|
1742
|
-
apiKey: config.apiKey,
|
|
1743
|
-
baseUrl: config.baseUrl
|
|
1744
|
-
});
|
|
1745
|
-
const updateData = {};
|
|
1746
|
-
if (options.name) updateData.name = options.name;
|
|
1747
|
-
if (options.url) updateData.url = options.url;
|
|
1748
|
-
if (options.events) {
|
|
1749
|
-
updateData.events = options.events.split(",").map((e) => e.trim());
|
|
1750
|
-
}
|
|
1751
|
-
const webhook = await client.webhooks.update(id, updateData);
|
|
1752
|
-
if (options.format === "json") {
|
|
1753
|
-
output(webhook, "json");
|
|
1754
|
-
} else {
|
|
1755
|
-
success("Webhook updated successfully!");
|
|
1756
|
-
output({
|
|
1757
|
-
ID: webhook.id,
|
|
1758
|
-
Name: webhook.name,
|
|
1759
|
-
URL: webhook.url
|
|
1760
|
-
}, "table");
|
|
1761
|
-
}
|
|
1762
|
-
} catch (err) {
|
|
1763
|
-
error(err instanceof Error ? err.message : "Failed to update webhook");
|
|
1764
|
-
process.exit(1);
|
|
1765
|
-
}
|
|
1766
|
-
});
|
|
1767
|
-
cmd.command("enable <id>").description("Enable a webhook").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
1768
|
-
try {
|
|
1769
|
-
const config = loadConfig();
|
|
1770
|
-
const client = new Emailr6({
|
|
1771
|
-
apiKey: config.apiKey,
|
|
1772
|
-
baseUrl: config.baseUrl
|
|
1773
|
-
});
|
|
1774
|
-
const result = await client.webhooks.enable(id);
|
|
1775
|
-
if (options.format === "json") {
|
|
1776
|
-
output(result, "json");
|
|
1777
|
-
} else {
|
|
1778
|
-
success("Webhook enabled successfully!");
|
|
1779
|
-
}
|
|
1780
|
-
} catch (err) {
|
|
1781
|
-
error(err instanceof Error ? err.message : "Failed to enable webhook");
|
|
1782
|
-
process.exit(1);
|
|
1783
|
-
}
|
|
1784
|
-
});
|
|
1785
|
-
cmd.command("disable <id>").description("Disable a webhook").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
1786
|
-
try {
|
|
1787
|
-
const config = loadConfig();
|
|
1788
|
-
const client = new Emailr6({
|
|
1789
|
-
apiKey: config.apiKey,
|
|
1790
|
-
baseUrl: config.baseUrl
|
|
1791
|
-
});
|
|
1792
|
-
const result = await client.webhooks.disable(id);
|
|
1793
|
-
if (options.format === "json") {
|
|
1794
|
-
output(result, "json");
|
|
1795
|
-
} else {
|
|
1796
|
-
success("Webhook disabled successfully!");
|
|
1797
|
-
}
|
|
1798
|
-
} catch (err) {
|
|
1799
|
-
error(err instanceof Error ? err.message : "Failed to disable webhook");
|
|
1800
|
-
process.exit(1);
|
|
1801
|
-
}
|
|
1802
|
-
});
|
|
1803
|
-
cmd.command("delete <id>").description("Delete a webhook").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
1804
|
-
try {
|
|
1805
|
-
const config = loadConfig();
|
|
1806
|
-
const client = new Emailr6({
|
|
1807
|
-
apiKey: config.apiKey,
|
|
1808
|
-
baseUrl: config.baseUrl
|
|
1809
|
-
});
|
|
1810
|
-
const result = await client.webhooks.delete(id);
|
|
1811
|
-
if (options.format === "json") {
|
|
1812
|
-
output(result, "json");
|
|
1813
|
-
} else {
|
|
1814
|
-
success("Webhook deleted successfully!");
|
|
1815
|
-
}
|
|
1816
|
-
} catch (err) {
|
|
1817
|
-
error(err instanceof Error ? err.message : "Failed to delete webhook");
|
|
1818
|
-
process.exit(1);
|
|
1819
|
-
}
|
|
1820
|
-
});
|
|
1821
|
-
return cmd;
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
// src/commands/segments.ts
|
|
1825
|
-
import { Command as Command8 } from "commander";
|
|
1826
|
-
import { Emailr as Emailr7 } from "emailr";
|
|
1827
|
-
function createSegmentsCommand() {
|
|
1828
|
-
const cmd = new Command8("segments").description("Manage contact segments");
|
|
1829
|
-
cmd.command("list").description("List all segments").option("--format <format>", "Output format (json|table)", "table").action(async (options) => {
|
|
1830
|
-
try {
|
|
1831
|
-
const config = loadConfig();
|
|
1832
|
-
const client = new Emailr7({
|
|
1833
|
-
apiKey: config.apiKey,
|
|
1834
|
-
baseUrl: config.baseUrl
|
|
1835
|
-
});
|
|
1836
|
-
const segments = await client.segments.list();
|
|
1837
|
-
if (options.format === "json") {
|
|
1838
|
-
output(segments, "json");
|
|
1839
|
-
} else {
|
|
1840
|
-
if (segments.length === 0) {
|
|
1841
|
-
console.log("No segments found.");
|
|
1842
|
-
return;
|
|
1843
|
-
}
|
|
1844
|
-
const tableData = segments.map((s) => ({
|
|
1845
|
-
ID: s.id,
|
|
1846
|
-
Name: s.name,
|
|
1847
|
-
Description: (s.description || "").substring(0, 30) + ((s.description?.length || 0) > 30 ? "..." : ""),
|
|
1848
|
-
"Created At": new Date(s.created_at).toLocaleDateString()
|
|
1849
|
-
}));
|
|
1850
|
-
output(tableData, "table");
|
|
1851
|
-
}
|
|
1852
|
-
} catch (err) {
|
|
1853
|
-
error(err instanceof Error ? err.message : "Failed to list segments");
|
|
1854
|
-
process.exit(1);
|
|
1855
|
-
}
|
|
1856
|
-
});
|
|
1857
|
-
cmd.command("get <id>").description("Get segment details").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
1858
|
-
try {
|
|
1859
|
-
const config = loadConfig();
|
|
1860
|
-
const client = new Emailr7({
|
|
1861
|
-
apiKey: config.apiKey,
|
|
1862
|
-
baseUrl: config.baseUrl
|
|
1863
|
-
});
|
|
1864
|
-
const segment = await client.segments.get(id);
|
|
1865
|
-
if (options.format === "json") {
|
|
1866
|
-
output(segment, "json");
|
|
1867
|
-
} else {
|
|
1868
|
-
output({
|
|
1869
|
-
ID: segment.id,
|
|
1870
|
-
Name: segment.name,
|
|
1871
|
-
Description: segment.description || "N/A",
|
|
1872
|
-
Conditions: JSON.stringify(segment.conditions),
|
|
1873
|
-
"Created At": segment.created_at,
|
|
1874
|
-
"Updated At": segment.updated_at
|
|
1875
|
-
}, "table");
|
|
1876
|
-
}
|
|
1877
|
-
} catch (err) {
|
|
1878
|
-
error(err instanceof Error ? err.message : "Failed to get segment");
|
|
1879
|
-
process.exit(1);
|
|
1880
|
-
}
|
|
1881
|
-
});
|
|
1882
|
-
cmd.command("create").description("Create a new segment").requiredOption("--name <name>", "Segment name").requiredOption("--conditions <json>", "Segment conditions as JSON").option("--description <description>", "Segment description").option("--format <format>", "Output format (json|table)", "table").action(async (options) => {
|
|
1883
|
-
try {
|
|
1884
|
-
const config = loadConfig();
|
|
1885
|
-
const client = new Emailr7({
|
|
1886
|
-
apiKey: config.apiKey,
|
|
1887
|
-
baseUrl: config.baseUrl
|
|
1888
|
-
});
|
|
1889
|
-
let conditions;
|
|
1890
|
-
try {
|
|
1891
|
-
conditions = JSON.parse(options.conditions);
|
|
1892
|
-
} catch {
|
|
1893
|
-
error("Invalid JSON for --conditions");
|
|
1894
|
-
process.exit(1);
|
|
1895
|
-
}
|
|
1896
|
-
const segment = await client.segments.create({
|
|
1897
|
-
name: options.name,
|
|
1898
|
-
description: options.description,
|
|
1899
|
-
conditions
|
|
1900
|
-
});
|
|
1901
|
-
if (options.format === "json") {
|
|
1902
|
-
output(segment, "json");
|
|
1903
|
-
} else {
|
|
1904
|
-
success("Segment created successfully!");
|
|
1905
|
-
output({
|
|
1906
|
-
ID: segment.id,
|
|
1907
|
-
Name: segment.name
|
|
1908
|
-
}, "table");
|
|
1909
|
-
}
|
|
1910
|
-
} catch (err) {
|
|
1911
|
-
error(err instanceof Error ? err.message : "Failed to create segment");
|
|
1912
|
-
process.exit(1);
|
|
1913
|
-
}
|
|
1914
|
-
});
|
|
1915
|
-
cmd.command("update <id>").description("Update a segment").option("--name <name>", "New segment name").option("--description <description>", "New description").option("--conditions <json>", "New conditions as JSON").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
1916
|
-
try {
|
|
1917
|
-
const config = loadConfig();
|
|
1918
|
-
const client = new Emailr7({
|
|
1919
|
-
apiKey: config.apiKey,
|
|
1920
|
-
baseUrl: config.baseUrl
|
|
1921
|
-
});
|
|
1922
|
-
const updateData = {};
|
|
1923
|
-
if (options.name) updateData.name = options.name;
|
|
1924
|
-
if (options.description) updateData.description = options.description;
|
|
1925
|
-
if (options.conditions) {
|
|
1926
|
-
try {
|
|
1927
|
-
updateData.conditions = JSON.parse(options.conditions);
|
|
1928
|
-
} catch {
|
|
1929
|
-
error("Invalid JSON for --conditions");
|
|
1930
|
-
process.exit(1);
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
const segment = await client.segments.update(id, updateData);
|
|
1934
|
-
if (options.format === "json") {
|
|
1935
|
-
output(segment, "json");
|
|
1936
|
-
} else {
|
|
1937
|
-
success("Segment updated successfully!");
|
|
1938
|
-
output({
|
|
1939
|
-
ID: segment.id,
|
|
1940
|
-
Name: segment.name
|
|
1941
|
-
}, "table");
|
|
1942
|
-
}
|
|
1943
|
-
} catch (err) {
|
|
1944
|
-
error(err instanceof Error ? err.message : "Failed to update segment");
|
|
1945
|
-
process.exit(1);
|
|
1946
|
-
}
|
|
1947
|
-
});
|
|
1948
|
-
cmd.command("delete <id>").description("Delete a segment").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
1949
|
-
try {
|
|
1950
|
-
const config = loadConfig();
|
|
1951
|
-
const client = new Emailr7({
|
|
1952
|
-
apiKey: config.apiKey,
|
|
1953
|
-
baseUrl: config.baseUrl
|
|
1954
|
-
});
|
|
1955
|
-
const result = await client.segments.delete(id);
|
|
1956
|
-
if (options.format === "json") {
|
|
1957
|
-
output(result, "json");
|
|
1958
|
-
} else {
|
|
1959
|
-
success("Segment deleted successfully!");
|
|
1960
|
-
}
|
|
1961
|
-
} catch (err) {
|
|
1962
|
-
error(err instanceof Error ? err.message : "Failed to delete segment");
|
|
1963
|
-
process.exit(1);
|
|
1964
|
-
}
|
|
1965
|
-
});
|
|
1966
|
-
cmd.command("count <id>").description("Get the number of contacts in a segment").option("--format <format>", "Output format (json|table)", "table").action(async (id, options) => {
|
|
1967
|
-
try {
|
|
1968
|
-
const config = loadConfig();
|
|
1969
|
-
const client = new Emailr7({
|
|
1970
|
-
apiKey: config.apiKey,
|
|
1971
|
-
baseUrl: config.baseUrl
|
|
1972
|
-
});
|
|
1973
|
-
const result = await client.segments.getContactsCount(id);
|
|
1974
|
-
if (options.format === "json") {
|
|
1975
|
-
output(result, "json");
|
|
1976
|
-
} else {
|
|
1977
|
-
console.log(`Segment contains ${result.count} contacts.`);
|
|
1978
|
-
}
|
|
1979
|
-
} catch (err) {
|
|
1980
|
-
error(err instanceof Error ? err.message : "Failed to get segment count");
|
|
1981
|
-
process.exit(1);
|
|
1982
|
-
}
|
|
1983
|
-
});
|
|
1984
|
-
return cmd;
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
// src/commands/login.ts
|
|
1988
|
-
import { Command as Command9 } from "commander";
|
|
1989
|
-
|
|
1990
|
-
// src/server/callback.ts
|
|
1991
|
-
import http3 from "http";
|
|
1992
|
-
import { URL } from "url";
|
|
1993
|
-
function parseCallbackParams(url) {
|
|
1994
|
-
try {
|
|
1995
|
-
const fullUrl = url.startsWith("http") ? url : `http://localhost${url}`;
|
|
1996
|
-
const parsed = new URL(fullUrl);
|
|
1997
|
-
return {
|
|
1998
|
-
key: parsed.searchParams.get("key") ?? void 0,
|
|
1999
|
-
state: parsed.searchParams.get("state") ?? void 0,
|
|
2000
|
-
error: parsed.searchParams.get("error") ?? void 0,
|
|
2001
|
-
message: parsed.searchParams.get("message") ?? void 0
|
|
2002
|
-
};
|
|
2003
|
-
} catch {
|
|
2004
|
-
return {};
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
function generateSuccessHtml() {
|
|
2008
|
-
return `<!DOCTYPE html>
|
|
286
|
+
</html>`,S.writeFileSync(e,o,"utf-8"),console.log(`Template draft created: ${e}`),t.background){let{spawn:r}=await import('child_process'),s=await import('os'),f=N.join(s.tmpdir(),"emailr-preview.pid");try{let x=S.readFileSync(f,"utf-8").trim();process.kill(parseInt(x,10),"SIGTERM");}catch{}let g=`
|
|
287
|
+
const http = require('http');
|
|
288
|
+
const fs = require('fs');
|
|
289
|
+
const path = require('path');
|
|
290
|
+
const os = require('os');
|
|
291
|
+
const filePath = ${JSON.stringify(e)};
|
|
292
|
+
const pidFile = path.join(os.tmpdir(), 'emailr-preview.pid');
|
|
293
|
+
const portFile = path.join(os.tmpdir(), 'emailr-preview.port');
|
|
294
|
+
let clients = [];
|
|
295
|
+
const script = '<script>(function(){const e=new EventSource("/__live-reload");e.onmessage=function(d){if(d.data==="reload")location.reload()};})();</script>';
|
|
296
|
+
const server = http.createServer((req, res) => {
|
|
297
|
+
if (req.url === '/__live-reload') {
|
|
298
|
+
res.writeHead(200, {'Content-Type':'text/event-stream','Cache-Control':'no-cache','Connection':'keep-alive'});
|
|
299
|
+
res.write('data: connected\\n\\n');
|
|
300
|
+
clients.push(res);
|
|
301
|
+
req.on('close', () => { clients = clients.filter(c => c !== res); });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
let html = fs.readFileSync(filePath, 'utf-8');
|
|
306
|
+
html = html.includes('</body>') ? html.replace('</body>', script + '</body>') : html + script;
|
|
307
|
+
res.writeHead(200, {'Content-Type':'text/html; charset=utf-8'});
|
|
308
|
+
res.end(html);
|
|
309
|
+
} catch (e) {
|
|
310
|
+
res.writeHead(500);
|
|
311
|
+
res.end('Error: ' + e.message);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
server.listen(0, '127.0.0.1', () => {
|
|
315
|
+
const port = server.address().port;
|
|
316
|
+
fs.writeFileSync(pidFile, String(process.pid));
|
|
317
|
+
fs.writeFileSync(portFile, String(port));
|
|
318
|
+
console.log('PORT:' + port);
|
|
319
|
+
let timer;
|
|
320
|
+
fs.watch(filePath, () => {
|
|
321
|
+
clearTimeout(timer);
|
|
322
|
+
timer = setTimeout(() => clients.forEach(c => { try { c.write('data: reload\\n\\n'); } catch {} }), 100);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
process.on('SIGTERM', () => { try { fs.unlinkSync(pidFile); fs.unlinkSync(portFile); } catch {} process.exit(0); });
|
|
326
|
+
`,y=r("node",["-e",g],{detached:!0,stdio:["ignore","pipe","ignore"]}),M="";await new Promise(x=>{y.stdout?.on("data",_=>{let K=_.toString().match(/PORT:(\d+)/);K&&(M=K[1],x());}),setTimeout(x,3e3);}),y.unref();let H=`http://127.0.0.1:${M}/`;if(console.log(`
|
|
327
|
+
Live Preview: ${H}`),console.log(`File: ${e}`),console.log(`
|
|
328
|
+
Preview server running in background (PID: ${y.pid}).`),console.log("Edit the file and see live updates in the browser."),console.log(`
|
|
329
|
+
When done:`),console.log(` emailr templates create --name "Template Name" --subject "Email Subject" --html-file ${t.file}`),console.log(" emailr templates stop-preview"),t.open!==!1)try{await(await import('open')).default(H);}catch{}process.exit(0);}let a=`http://127.0.0.1:${await Y(e,()=>{console.log("File changed - browser refreshed");})}/`;if(console.log(`
|
|
330
|
+
Live Preview: ${a}`),console.log(`File: ${e}`),console.log(`
|
|
331
|
+
Watching for changes... Edit the file and see live updates.`),console.log(`
|
|
332
|
+
When done, create the template:`),console.log(` emailr templates create --name "Template Name" --subject "Email Subject" --html-file ${t.file}`),console.log(`
|
|
333
|
+
Press Ctrl+C to stop.`),t.open!==!1)try{await(await import('open')).default(a);}catch{}process.on("SIGINT",async()=>{console.log(`
|
|
334
|
+
|
|
335
|
+
Stopping live preview server...`),await Q(),console.log("Done."),console.log(`
|
|
336
|
+
To create template: emailr templates create --name "Template Name" --subject "Email Subject" --html-file ${t.file}`),process.exit(0);});}catch(e){c(e instanceof Error?e.message:"Failed to start draft session"),process.exit(1);}}),n.command("stop-preview").description("Stop any running background preview server").action(async()=>{let t=await import('os'),e=N.join(t.tmpdir(),"emailr-preview.pid");try{let o=S.readFileSync(e,"utf-8").trim();process.kill(parseInt(o,10),"SIGTERM"),S.unlinkSync(e),d("Preview server stopped");}catch{d("No preview server running");}}),n}function Ce(){let n=new Command("domains").description("Manage sending domains");return n.command("list").description("List all domains").option("--format <format>","Output format (json|table)","table").action(async t=>{try{let e=m(),i=await new Emailr({apiKey:e.apiKey,baseUrl:e.baseUrl}).domains.list();if(t.format==="json")l(i,"json");else {let a=i.map(r=>({ID:r.id,Domain:r.domain,Status:r.status,DKIM:r.dkim_verified,SPF:r.spf_verified,DMARC:r.dmarc_verified,Created:r.created_at}));l(a,"table");}}catch(e){c(e instanceof Error?e.message:"Failed to list domains"),process.exit(1);}}),n.command("get <id>").description("Get a domain by ID").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),a=await new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}).domains.get(t);l(a,e.format);}catch(o){c(o instanceof Error?o.message:"Failed to get domain"),process.exit(1);}}),n.command("add <domain>").description("Add a new domain").option("--receiving-subdomain <subdomain>","Subdomain for receiving emails").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),i=new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}),a={domain:t};e.receivingSubdomain&&(a.receivingSubdomain=e.receivingSubdomain);let r=await i.domains.add(a);if(e.format==="json")l(r,"json");else {if(d(`Domain added: ${r.domain}`),p("Add the following DNS records to verify your domain:"),console.log(""),r.dns_records){let s=[{Type:"DKIM",...X(r.dns_records.dkim)},{Type:"SPF",...X(r.dns_records.spf)},{Type:"DMARC",...X(r.dns_records.dmarc)}];l(s,"table");}console.log(""),p(`Run 'emailr domains verify ${r.id}' after adding DNS records`);}}catch(o){c(o instanceof Error?o.message:"Failed to add domain"),process.exit(1);}}),n.command("verify <id>").description("Verify a domain").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),a=await new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}).domains.verify(t);e.format==="json"?l(a,"json"):a.verified?d("Domain verified successfully!"):(p(`Domain status: ${a.status}`),a.dkim_status&&p(`DKIM status: ${a.dkim_status}`));}catch(o){c(o instanceof Error?o.message:"Failed to verify domain"),process.exit(1);}}),n.command("check-dns <id>").description("Check DNS records for a domain").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),a=await new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}).domains.checkDns(t);if(e.format==="json")l(a,"json");else {let r=Object.entries(a).map(([s,f])=>({Record:s,Verified:f.verified,Expected:f.expected||"-",Found:f.found?.join(", ")||"-"}));l(r,"table");}}catch(o){c(o instanceof Error?o.message:"Failed to check DNS"),process.exit(1);}}),n.command("delete <id>").description("Delete a domain").action(async t=>{try{let e=m();await new Emailr({apiKey:e.apiKey,baseUrl:e.baseUrl}).domains.delete(t),d(`Domain deleted: ${t}`);}catch(e){c(e instanceof Error?e.message:"Failed to delete domain"),process.exit(1);}}),n}function X(n){return {"Record Type":n.type,Name:n.name,Value:n.value.length>50?n.value.substring(0,47)+"...":n.value,...n.priority!==void 0&&{Priority:n.priority}}}function ke(){let n=new Command("config").description("Manage CLI configuration");return n.command("set <key> <value>").description("Set a configuration value").action(async(t,e)=>{try{let o=["api-key","base-url","format"],i=t.toLowerCase();o.includes(i)||(c(`Invalid config key: ${t}`),p(`Valid keys: ${o.join(", ")}`),process.exit(1));let r={"api-key":"apiKey","base-url":"baseUrl",format:"format"}[i];i==="format"&&!["json","table"].includes(e)&&(c(`Invalid format value: ${e}`),p("Valid formats: json, table"),process.exit(1)),R({[r]:e}),d(`Configuration saved: ${t} = ${i==="api-key"?"***":e}`),p(`Config file: ${w()}`);}catch(o){c(o instanceof Error?o.message:"Failed to save configuration"),process.exit(1);}}),n.command("get <key>").description("Get a configuration value").action(async t=>{try{let e=["api-key","base-url","format"],o=t.toLowerCase();e.includes(o)||(c(`Invalid config key: ${t}`),p(`Valid keys: ${e.join(", ")}`),process.exit(1));let a={"api-key":"apiKey","base-url":"baseUrl",format:"format"}[o],r=re(a);r?console.log(o==="api-key"?r.substring(0,8)+"..."+r.substring(r.length-4):r):p(`${t} is not set`);}catch(e){c(e instanceof Error?e.message:"Failed to get configuration"),process.exit(1);}}),n.command("list").description("List all configuration values").option("--format <format>","Output format (json|table)","table").action(async t=>{try{let e=m(),o={"api-key":e.apiKey?e.apiKey.substring(0,8)+"..."+e.apiKey.substring(e.apiKey.length-4):"(not set)","base-url":e.baseUrl||"(default)",format:e.format||"table"};if(t.format==="json")l(o,"json");else {let i=Object.entries(o).map(([a,r])=>({Key:a,Value:r}));l(i,"table");}console.log(""),p(`Config file: ${w()}`);}catch(e){e instanceof Error&&e.message.includes("No API key configured")?(p("No configuration found."),p("Run 'emailr config set api-key <your-api-key>' to get started.")):(c(e instanceof Error?e.message:"Failed to list configuration"),process.exit(1));}}),n.command("path").description("Show the configuration file path").action(()=>{console.log(w());}),n.command("init").description("Initialize configuration interactively").option("--api-key <key>","API key to use").option("--base-url <url>","Base URL for API").action(async t=>{try{t.apiKey?(R({apiKey:t.apiKey,baseUrl:t.baseUrl}),d("Configuration initialized!"),p(`Config file: ${w()}`)):(p("Initialize your Emailr CLI configuration:"),console.log(""),p("Run with --api-key flag:"),console.log(" emailr config init --api-key <your-api-key>"),console.log(""),p("Or set environment variable:"),console.log(" export EMAILR_API_KEY=<your-api-key>"));}catch(e){c(e instanceof Error?e.message:"Failed to initialize configuration"),process.exit(1);}}),n}function je(){let n=new Command("broadcasts").description("Manage broadcast campaigns");return n.command("list").description("List all broadcasts").option("--status <status>","Filter by status (draft, scheduled, sending, sent)").option("--limit <number>","Number of broadcasts to return","20").option("--format <format>","Output format (json|table)","table").action(async t=>{try{let e=m(),i=await new Emailr({apiKey:e.apiKey,baseUrl:e.baseUrl}).broadcasts.list({status:t.status,limit:parseInt(t.limit)});if(t.format==="json")l(i,"json");else {if(i.length===0){console.log("No broadcasts found.");return}let a=i.map(r=>({ID:r.id,Name:r.name,Subject:r.subject.substring(0,30)+(r.subject.length>30?"...":""),Status:r.status,Recipients:r.total_recipients||0,Sent:r.sent_count||0,Created:new Date(r.created_at).toLocaleDateString()}));l(a,"table");}}catch(e){c(e instanceof Error?e.message:"Failed to list broadcasts"),process.exit(1);}}),n.command("get <id>").description("Get broadcast details").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),a=await new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}).broadcasts.get(t);e.format==="json"?l(a,"json"):l({ID:a.id,Name:a.name,Subject:a.subject,"From Email":a.from_email,Status:a.status,"Total Recipients":a.total_recipients||0,"Sent Count":a.sent_count||0,Delivered:a.delivered_count||0,Opened:a.opened_count||0,Clicked:a.clicked_count||0,Bounced:a.bounced_count||0,"Scheduled At":a.scheduled_at||"N/A","Started At":a.started_at||"N/A","Completed At":a.completed_at||"N/A","Created At":a.created_at},"table");}catch(o){c(o instanceof Error?o.message:"Failed to get broadcast"),process.exit(1);}}),n.command("create").description("Create a new broadcast").requiredOption("--name <name>","Broadcast name").requiredOption("--subject <subject>","Email subject").requiredOption("--from <email>","Sender email address").option("--template <id>","Template ID to use").option("--segment <id>","Segment ID to target").option("--html <html>","HTML content").option("--text <text>","Plain text content").option("--schedule <datetime>","Schedule time (ISO 8601)").option("--format <format>","Output format (json|table)","table").action(async t=>{try{let e=m(),i=await new Emailr({apiKey:e.apiKey,baseUrl:e.baseUrl}).broadcasts.create({name:t.name,subject:t.subject,from_email:t.from,template_id:t.template,segment_id:t.segment,html_content:t.html,text_content:t.text,scheduled_at:t.schedule});t.format==="json"?l(i,"json"):(d("Broadcast created successfully!"),l({ID:i.id,Name:i.name,Status:i.status,"Scheduled At":i.scheduled_at||"Not scheduled"},"table"));}catch(e){c(e instanceof Error?e.message:"Failed to create broadcast"),process.exit(1);}}),n.command("send <id>").description("Send a broadcast immediately").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),a=await new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}).broadcasts.send(t);e.format==="json"?l(a,"json"):(d("Broadcast sent successfully!"),l({Success:a.success,Sent:a.sent,Total:a.total},"table"));}catch(o){c(o instanceof Error?o.message:"Failed to send broadcast"),process.exit(1);}}),n.command("schedule <id>").description("Schedule a broadcast").requiredOption("--at <datetime>","Schedule time (ISO 8601)").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),a=await new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}).broadcasts.schedule(t,e.at);e.format==="json"?l(a,"json"):(d("Broadcast scheduled successfully!"),l({ID:a.id,Name:a.name,Status:a.status,"Scheduled At":a.scheduled_at},"table"));}catch(o){c(o instanceof Error?o.message:"Failed to schedule broadcast"),process.exit(1);}}),n.command("cancel <id>").description("Cancel a scheduled broadcast").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),a=await new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}).broadcasts.cancel(t);e.format==="json"?l(a,"json"):d("Broadcast cancelled successfully!");}catch(o){c(o instanceof Error?o.message:"Failed to cancel broadcast"),process.exit(1);}}),n.command("delete <id>").description("Delete a broadcast").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),a=await new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}).broadcasts.delete(t);e.format==="json"?l(a,"json"):d("Broadcast deleted successfully!");}catch(o){c(o instanceof Error?o.message:"Failed to delete broadcast"),process.exit(1);}}),n}function Ee(){let n=new Command("webhooks").description("Manage webhooks");return n.command("list").description("List all webhooks").option("--format <format>","Output format (json|table)","table").action(async t=>{try{let e=m(),i=await new Emailr({apiKey:e.apiKey,baseUrl:e.baseUrl}).webhooks.list();if(t.format==="json")l(i,"json");else {if(i.length===0){console.log("No webhooks found.");return}let a=i.map(r=>({ID:r.id,Name:r.name,URL:r.url.substring(0,40)+(r.url.length>40?"...":""),Events:r.events.join(", "),Active:r.active?"Yes":"No"}));l(a,"table");}}catch(e){c(e instanceof Error?e.message:"Failed to list webhooks"),process.exit(1);}}),n.command("get <id>").description("Get webhook details").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),a=await new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}).webhooks.get(t);e.format==="json"?l(a,"json"):l({ID:a.id,Name:a.name,URL:a.url,Events:a.events.join(", "),Active:a.active?"Yes":"No",Secret:a.secret,"Created At":a.created_at},"table");}catch(o){c(o instanceof Error?o.message:"Failed to get webhook"),process.exit(1);}}),n.command("create").description("Create a new webhook").requiredOption("--name <name>","Webhook name").requiredOption("--url <url>","Webhook URL").requiredOption("--events <events>","Events to subscribe to (comma-separated)").option("--format <format>","Output format (json|table)","table").action(async t=>{try{let e=m(),o=new Emailr({apiKey:e.apiKey,baseUrl:e.baseUrl}),i=t.events.split(",").map(r=>r.trim()),a=await o.webhooks.create({name:t.name,url:t.url,events:i});t.format==="json"?l(a,"json"):(d("Webhook created successfully!"),l({ID:a.id,Name:a.name,URL:a.url,Secret:a.secret},"table"));}catch(e){c(e instanceof Error?e.message:"Failed to create webhook"),process.exit(1);}}),n.command("update <id>").description("Update a webhook").option("--name <name>","New webhook name").option("--url <url>","New webhook URL").option("--events <events>","New events (comma-separated)").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),i=new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}),a={};e.name&&(a.name=e.name),e.url&&(a.url=e.url),e.events&&(a.events=e.events.split(",").map(s=>s.trim()));let r=await i.webhooks.update(t,a);e.format==="json"?l(r,"json"):(d("Webhook updated successfully!"),l({ID:r.id,Name:r.name,URL:r.url},"table"));}catch(o){c(o instanceof Error?o.message:"Failed to update webhook"),process.exit(1);}}),n.command("enable <id>").description("Enable a webhook").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),a=await new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}).webhooks.enable(t);e.format==="json"?l(a,"json"):d("Webhook enabled successfully!");}catch(o){c(o instanceof Error?o.message:"Failed to enable webhook"),process.exit(1);}}),n.command("disable <id>").description("Disable a webhook").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),a=await new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}).webhooks.disable(t);e.format==="json"?l(a,"json"):d("Webhook disabled successfully!");}catch(o){c(o instanceof Error?o.message:"Failed to disable webhook"),process.exit(1);}}),n.command("delete <id>").description("Delete a webhook").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),a=await new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}).webhooks.delete(t);e.format==="json"?l(a,"json"):d("Webhook deleted successfully!");}catch(o){c(o instanceof Error?o.message:"Failed to delete webhook"),process.exit(1);}}),n}function Pe(){let n=new Command("segments").description("Manage contact segments");return n.command("list").description("List all segments").option("--format <format>","Output format (json|table)","table").action(async t=>{try{let e=m(),i=await new Emailr({apiKey:e.apiKey,baseUrl:e.baseUrl}).segments.list();if(t.format==="json")l(i,"json");else {if(i.length===0){console.log("No segments found.");return}let a=i.map(r=>({ID:r.id,Name:r.name,Description:(r.description||"").substring(0,30)+((r.description?.length||0)>30?"...":""),"Created At":new Date(r.created_at).toLocaleDateString()}));l(a,"table");}}catch(e){c(e instanceof Error?e.message:"Failed to list segments"),process.exit(1);}}),n.command("get <id>").description("Get segment details").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),a=await new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}).segments.get(t);e.format==="json"?l(a,"json"):l({ID:a.id,Name:a.name,Description:a.description||"N/A",Conditions:JSON.stringify(a.conditions),"Created At":a.created_at,"Updated At":a.updated_at},"table");}catch(o){c(o instanceof Error?o.message:"Failed to get segment"),process.exit(1);}}),n.command("create").description("Create a new segment").requiredOption("--name <name>","Segment name").requiredOption("--conditions <json>","Segment conditions as JSON").option("--description <description>","Segment description").option("--format <format>","Output format (json|table)","table").action(async t=>{try{let e=m(),o=new Emailr({apiKey:e.apiKey,baseUrl:e.baseUrl}),i;try{i=JSON.parse(t.conditions);}catch{c("Invalid JSON for --conditions"),process.exit(1);}let a=await o.segments.create({name:t.name,description:t.description,conditions:i});t.format==="json"?l(a,"json"):(d("Segment created successfully!"),l({ID:a.id,Name:a.name},"table"));}catch(e){c(e instanceof Error?e.message:"Failed to create segment"),process.exit(1);}}),n.command("update <id>").description("Update a segment").option("--name <name>","New segment name").option("--description <description>","New description").option("--conditions <json>","New conditions as JSON").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),i=new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}),a={};if(e.name&&(a.name=e.name),e.description&&(a.description=e.description),e.conditions)try{a.conditions=JSON.parse(e.conditions);}catch{c("Invalid JSON for --conditions"),process.exit(1);}let r=await i.segments.update(t,a);e.format==="json"?l(r,"json"):(d("Segment updated successfully!"),l({ID:r.id,Name:r.name},"table"));}catch(o){c(o instanceof Error?o.message:"Failed to update segment"),process.exit(1);}}),n.command("delete <id>").description("Delete a segment").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),a=await new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}).segments.delete(t);e.format==="json"?l(a,"json"):d("Segment deleted successfully!");}catch(o){c(o instanceof Error?o.message:"Failed to delete segment"),process.exit(1);}}),n.command("count <id>").description("Get the number of contacts in a segment").option("--format <format>","Output format (json|table)","table").action(async(t,e)=>{try{let o=m(),a=await new Emailr({apiKey:o.apiKey,baseUrl:o.baseUrl}).segments.getContactsCount(t);e.format==="json"?l(a,"json"):console.log(`Segment contains ${a.count} contacts.`);}catch(o){c(o instanceof Error?o.message:"Failed to get segment count"),process.exit(1);}}),n}function st(n){try{let t=n.startsWith("http")?n:`http://localhost${n}`,e=new URL(t);return {key:e.searchParams.get("key")??void 0,state:e.searchParams.get("state")??void 0,error:e.searchParams.get("error")??void 0,message:e.searchParams.get("message")??void 0}}catch{return {}}}function lt(){return `<!DOCTYPE html>
|
|
2009
337
|
<html lang="en">
|
|
2010
338
|
<head>
|
|
2011
339
|
<meta charset="UTF-8">
|
|
@@ -2051,11 +379,7 @@ function generateSuccessHtml() {
|
|
|
2051
379
|
<p>You can close this window and return to your terminal.</p>
|
|
2052
380
|
</div>
|
|
2053
381
|
</body>
|
|
2054
|
-
</html
|
|
2055
|
-
}
|
|
2056
|
-
function generateErrorHtml(errorMessage) {
|
|
2057
|
-
const escapedMessage = errorMessage.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2058
|
-
return `<!DOCTYPE html>
|
|
382
|
+
</html>`}function Z(n){return `<!DOCTYPE html>
|
|
2059
383
|
<html lang="en">
|
|
2060
384
|
<head>
|
|
2061
385
|
<meta charset="UTF-8">
|
|
@@ -2106,230 +430,11 @@ function generateErrorHtml(errorMessage) {
|
|
|
2106
430
|
<div class="icon">\u2717</div>
|
|
2107
431
|
<h1>Login Failed</h1>
|
|
2108
432
|
<p>Something went wrong during authentication.</p>
|
|
2109
|
-
<div class="error-message">${
|
|
433
|
+
<div class="error-message">${n.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}</div>
|
|
2110
434
|
<p style="margin-top: 1rem;">Please close this window and try again.</p>
|
|
2111
435
|
</div>
|
|
2112
436
|
</body>
|
|
2113
|
-
</html
|
|
2114
|
-
}
|
|
2115
|
-
function createCallbackServer() {
|
|
2116
|
-
let server = null;
|
|
2117
|
-
let port = 0;
|
|
2118
|
-
let callbackPromiseResolve = null;
|
|
2119
|
-
let timeoutId = null;
|
|
2120
|
-
let expectedState = null;
|
|
2121
|
-
return {
|
|
2122
|
-
async start() {
|
|
2123
|
-
return new Promise((resolve, reject) => {
|
|
2124
|
-
server = http3.createServer((req, res) => {
|
|
2125
|
-
if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
|
|
2126
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
2127
|
-
res.end("Not Found");
|
|
2128
|
-
return;
|
|
2129
|
-
}
|
|
2130
|
-
const params = parseCallbackParams(req.url);
|
|
2131
|
-
if (expectedState && params.state !== expectedState) {
|
|
2132
|
-
res.writeHead(400, { "Content-Type": "text/html" });
|
|
2133
|
-
res.end(generateErrorHtml("Security verification failed. State parameter mismatch."));
|
|
2134
|
-
if (callbackPromiseResolve) {
|
|
2135
|
-
callbackPromiseResolve({
|
|
2136
|
-
success: false,
|
|
2137
|
-
error: "State parameter mismatch"
|
|
2138
|
-
});
|
|
2139
|
-
}
|
|
2140
|
-
return;
|
|
2141
|
-
}
|
|
2142
|
-
if (params.error) {
|
|
2143
|
-
const errorMessage = params.message || params.error;
|
|
2144
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2145
|
-
res.end(generateErrorHtml(errorMessage));
|
|
2146
|
-
if (callbackPromiseResolve) {
|
|
2147
|
-
callbackPromiseResolve({
|
|
2148
|
-
success: false,
|
|
2149
|
-
error: errorMessage
|
|
2150
|
-
});
|
|
2151
|
-
}
|
|
2152
|
-
return;
|
|
2153
|
-
}
|
|
2154
|
-
if (params.key) {
|
|
2155
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2156
|
-
res.end(generateSuccessHtml());
|
|
2157
|
-
if (callbackPromiseResolve) {
|
|
2158
|
-
callbackPromiseResolve({
|
|
2159
|
-
success: true,
|
|
2160
|
-
apiKey: params.key
|
|
2161
|
-
});
|
|
2162
|
-
}
|
|
2163
|
-
return;
|
|
2164
|
-
}
|
|
2165
|
-
res.writeHead(400, { "Content-Type": "text/html" });
|
|
2166
|
-
res.end(generateErrorHtml("Invalid callback: missing required parameters."));
|
|
2167
|
-
if (callbackPromiseResolve) {
|
|
2168
|
-
callbackPromiseResolve({
|
|
2169
|
-
success: false,
|
|
2170
|
-
error: "Invalid callback: missing required parameters"
|
|
2171
|
-
});
|
|
2172
|
-
}
|
|
2173
|
-
});
|
|
2174
|
-
server.listen(0, "127.0.0.1", () => {
|
|
2175
|
-
const address = server.address();
|
|
2176
|
-
if (address && typeof address === "object") {
|
|
2177
|
-
port = address.port;
|
|
2178
|
-
resolve({
|
|
2179
|
-
port,
|
|
2180
|
-
url: `http://127.0.0.1:${port}/callback`
|
|
2181
|
-
});
|
|
2182
|
-
} else {
|
|
2183
|
-
reject(new Error("Failed to get server address"));
|
|
2184
|
-
}
|
|
2185
|
-
});
|
|
2186
|
-
server.on("error", (err) => {
|
|
2187
|
-
reject(new Error(`Failed to start callback server: ${err.message}`));
|
|
2188
|
-
});
|
|
2189
|
-
});
|
|
2190
|
-
},
|
|
2191
|
-
async waitForCallback(state, timeoutMs) {
|
|
2192
|
-
expectedState = state;
|
|
2193
|
-
return new Promise((resolve) => {
|
|
2194
|
-
callbackPromiseResolve = resolve;
|
|
2195
|
-
timeoutId = setTimeout(() => {
|
|
2196
|
-
if (callbackPromiseResolve) {
|
|
2197
|
-
callbackPromiseResolve({
|
|
2198
|
-
success: false,
|
|
2199
|
-
error: "Login timed out. Please try again."
|
|
2200
|
-
});
|
|
2201
|
-
}
|
|
2202
|
-
}, timeoutMs);
|
|
2203
|
-
});
|
|
2204
|
-
},
|
|
2205
|
-
async stop() {
|
|
2206
|
-
if (timeoutId) {
|
|
2207
|
-
clearTimeout(timeoutId);
|
|
2208
|
-
timeoutId = null;
|
|
2209
|
-
}
|
|
2210
|
-
if (server) {
|
|
2211
|
-
return new Promise((resolve) => {
|
|
2212
|
-
server.close(() => {
|
|
2213
|
-
server = null;
|
|
2214
|
-
resolve();
|
|
2215
|
-
});
|
|
2216
|
-
});
|
|
2217
|
-
}
|
|
2218
|
-
}
|
|
2219
|
-
};
|
|
2220
|
-
}
|
|
2221
|
-
|
|
2222
|
-
// src/utils/state.ts
|
|
2223
|
-
import crypto from "crypto";
|
|
2224
|
-
function generateState() {
|
|
2225
|
-
return crypto.randomBytes(32).toString("hex");
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
// src/utils/browser.ts
|
|
2229
|
-
import { exec } from "child_process";
|
|
2230
|
-
function openBrowser(url) {
|
|
2231
|
-
return new Promise((resolve) => {
|
|
2232
|
-
const platform = process.platform;
|
|
2233
|
-
let command;
|
|
2234
|
-
switch (platform) {
|
|
2235
|
-
case "darwin":
|
|
2236
|
-
command = `open "${url}"`;
|
|
2237
|
-
break;
|
|
2238
|
-
case "win32":
|
|
2239
|
-
command = `start "" "${url}"`;
|
|
2240
|
-
break;
|
|
2241
|
-
default:
|
|
2242
|
-
command = `xdg-open "${url}"`;
|
|
2243
|
-
break;
|
|
2244
|
-
}
|
|
2245
|
-
exec(command, (error2) => {
|
|
2246
|
-
if (error2) {
|
|
2247
|
-
resolve(false);
|
|
2248
|
-
} else {
|
|
2249
|
-
resolve(true);
|
|
2250
|
-
}
|
|
2251
|
-
});
|
|
2252
|
-
});
|
|
2253
|
-
}
|
|
2254
|
-
|
|
2255
|
-
// src/commands/login.ts
|
|
2256
|
-
var DEFAULT_TIMEOUT_SECONDS = 120;
|
|
2257
|
-
var WEB_APP_BASE_URL = process.env.EMAILR_WEB_URL || "https://app.emailr.dev";
|
|
2258
|
-
function buildAuthorizationUrl(state, callbackPort) {
|
|
2259
|
-
const callbackUrl = `http://127.0.0.1:${callbackPort}/callback`;
|
|
2260
|
-
const params = new URLSearchParams({
|
|
2261
|
-
state,
|
|
2262
|
-
callback_url: callbackUrl
|
|
2263
|
-
});
|
|
2264
|
-
return `${WEB_APP_BASE_URL}/cli/authorize?${params.toString()}`;
|
|
2265
|
-
}
|
|
2266
|
-
function createLoginCommand() {
|
|
2267
|
-
const cmd = new Command9("login").description("Log in to Emailr via browser authentication").option("-t, --timeout <seconds>", "Timeout in seconds", String(DEFAULT_TIMEOUT_SECONDS)).option("--no-browser", "Don't automatically open the browser").action(async (options) => {
|
|
2268
|
-
await executeLogin({
|
|
2269
|
-
timeout: parseInt(options.timeout, 10) || DEFAULT_TIMEOUT_SECONDS,
|
|
2270
|
-
noBrowser: options.browser === false
|
|
2271
|
-
});
|
|
2272
|
-
});
|
|
2273
|
-
return cmd;
|
|
2274
|
-
}
|
|
2275
|
-
async function executeLogin(options) {
|
|
2276
|
-
const callbackServer = createCallbackServer();
|
|
2277
|
-
const timeoutMs = (options.timeout || DEFAULT_TIMEOUT_SECONDS) * 1e3;
|
|
2278
|
-
try {
|
|
2279
|
-
info("Starting authentication server...");
|
|
2280
|
-
const { port, url } = await callbackServer.start();
|
|
2281
|
-
const state = generateState();
|
|
2282
|
-
const authUrl = buildAuthorizationUrl(state, port);
|
|
2283
|
-
console.log("");
|
|
2284
|
-
info("Authorization URL:");
|
|
2285
|
-
console.log(` ${authUrl}`);
|
|
2286
|
-
console.log("");
|
|
2287
|
-
if (!options.noBrowser) {
|
|
2288
|
-
const browserOpened = await openBrowser(authUrl);
|
|
2289
|
-
if (browserOpened) {
|
|
2290
|
-
info("Browser opened. Please complete authentication in your browser.");
|
|
2291
|
-
} else {
|
|
2292
|
-
warn("Could not open browser automatically.");
|
|
2293
|
-
info("Please open the URL above in your browser to continue.");
|
|
2294
|
-
}
|
|
2295
|
-
} else {
|
|
2296
|
-
info("Please open the URL above in your browser to continue.");
|
|
2297
|
-
}
|
|
2298
|
-
console.log("");
|
|
2299
|
-
info(`Waiting for authentication (timeout: ${options.timeout || DEFAULT_TIMEOUT_SECONDS}s)...`);
|
|
2300
|
-
const result = await callbackServer.waitForCallback(state, timeoutMs);
|
|
2301
|
-
if (result.success && result.apiKey) {
|
|
2302
|
-
saveConfig({ apiKey: result.apiKey });
|
|
2303
|
-
console.log("");
|
|
2304
|
-
success("Login successful!");
|
|
2305
|
-
info(`API key saved to: ${getConfigPath()}`);
|
|
2306
|
-
info("You can now use the Emailr CLI.");
|
|
2307
|
-
} else {
|
|
2308
|
-
console.log("");
|
|
2309
|
-
error(result.error || "Authentication failed.");
|
|
2310
|
-
info("Please try again or use manual configuration:");
|
|
2311
|
-
console.log(" emailr config set api-key <your-api-key>");
|
|
2312
|
-
process.exit(1);
|
|
2313
|
-
}
|
|
2314
|
-
} catch (err) {
|
|
2315
|
-
console.log("");
|
|
2316
|
-
error(err instanceof Error ? err.message : "An unexpected error occurred.");
|
|
2317
|
-
info("Please try again or use manual configuration:");
|
|
2318
|
-
console.log(" emailr config set api-key <your-api-key>");
|
|
2319
|
-
process.exit(1);
|
|
2320
|
-
} finally {
|
|
2321
|
-
await callbackServer.stop();
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2324
|
-
|
|
2325
|
-
// src/commands/agent.ts
|
|
2326
|
-
import { Command as Command10 } from "commander";
|
|
2327
|
-
import { spawn, execSync } from "child_process";
|
|
2328
|
-
import fs5 from "fs";
|
|
2329
|
-
import path5 from "path";
|
|
2330
|
-
import os3 from "os";
|
|
2331
|
-
var OPENCODE_SKILL_DIR = path5.join(os3.homedir(), ".config", "opencode", "skills", "emailr-cli");
|
|
2332
|
-
var SKILL_MD = `---
|
|
437
|
+
</html>`}function Te(){let n=null,t=0,e=null,o=null,i=null;return {async start(){return new Promise((a,r)=>{n=Be.createServer((s,f)=>{if(s.method!=="GET"||!s.url?.startsWith("/callback")){f.writeHead(404,{"Content-Type":"text/plain"}),f.end("Not Found");return}let g=st(s.url);if(i&&g.state!==i){f.writeHead(400,{"Content-Type":"text/html"}),f.end(Z("Security verification failed. State parameter mismatch.")),e&&e({success:false,error:"State parameter mismatch"});return}if(g.error){let y=g.message||g.error;f.writeHead(200,{"Content-Type":"text/html"}),f.end(Z(y)),e&&e({success:false,error:y});return}if(g.key){f.writeHead(200,{"Content-Type":"text/html"}),f.end(lt()),e&&e({success:true,apiKey:g.key});return}f.writeHead(400,{"Content-Type":"text/html"}),f.end(Z("Invalid callback: missing required parameters.")),e&&e({success:false,error:"Invalid callback: missing required parameters"});}),n.listen(0,"127.0.0.1",()=>{let s=n.address();s&&typeof s=="object"?(t=s.port,a({port:t,url:`http://127.0.0.1:${t}/callback`})):r(new Error("Failed to get server address"));}),n.on("error",s=>{r(new Error(`Failed to start callback server: ${s.message}`));});})},async waitForCallback(a,r){return i=a,new Promise(s=>{e=s,o=setTimeout(()=>{e&&e({success:false,error:"Login timed out. Please try again."});},r);})},async stop(){if(o&&(clearTimeout(o),o=null),n)return new Promise(a=>{n.close(()=>{n=null,a();});})}}}function Ie(){return ct.randomBytes(32).toString("hex")}function Fe(n){return new Promise(t=>{let e=process.platform,o;switch(e){case "darwin":o=`open "${n}"`;break;case "win32":o=`start "" "${n}"`;break;default:o=`xdg-open "${n}"`;break}exec(o,i=>{t(!i);});})}var B=120,pt=process.env.EMAILR_WEB_URL||"https://app.emailr.dev";function ft(n,t){let e=`http://127.0.0.1:${t}/callback`,o=new URLSearchParams({state:n,callback_url:e});return `${pt}/consent/authorize?${o.toString()}`}function Ue(){return new Command("login").description("Log in to Emailr via browser authentication").option("-t, --timeout <seconds>","Timeout in seconds",String(B)).option("--no-browser","Don't automatically open the browser").action(async t=>{await ut({timeout:parseInt(t.timeout,10)||B,noBrowser:t.browser===false});})}async function ut(n){let t=Te(),e=(n.timeout||B)*1e3;try{p("Starting authentication server...");let{port:o,url:i}=await t.start(),a=Ie(),r=ft(a,o);console.log(""),p("Authorization URL:"),console.log(` ${r}`),console.log(""),n.noBrowser?p("Please open the URL above in your browser to continue."):await Fe(r)?p("Browser opened. Please complete authentication in your browser."):(v("Could not open browser automatically."),p("Please open the URL above in your browser to continue.")),console.log(""),p(`Waiting for authentication (timeout: ${n.timeout||B}s)...`);let s=await t.waitForCallback(a,e);s.success&&s.apiKey?(R({apiKey:s.apiKey}),console.log(""),d("Login successful!"),p(`API key saved to: ${w()}`),p("You can now use the Emailr CLI.")):(console.log(""),c(s.error||"Authentication failed."),p("Please try again or use manual configuration:"),console.log(" emailr config set api-key <your-api-key>"),process.exit(1));}catch(o){console.log(""),c(o instanceof Error?o.message:"An unexpected error occurred."),p("Please try again or use manual configuration:"),console.log(" emailr config set api-key <your-api-key>"),process.exit(1);}finally{await t.stop();}}var te=N.join(W.homedir(),".config","opencode","skills","emailr-cli"),yt=`---
|
|
2333
438
|
name: emailr-cli
|
|
2334
439
|
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.
|
|
2335
440
|
---
|
|
@@ -2341,15 +446,19 @@ description: Operate the Emailr CLI to send emails, manage contacts, templates,
|
|
|
2341
446
|
|
|
2342
447
|
## LIVE TEMPLATE EDITING (Agentic Workflow)
|
|
2343
448
|
|
|
449
|
+
IMPORTANT: Always use \`--background\` flag so the command returns immediately and you can continue editing.
|
|
450
|
+
|
|
2344
451
|
### 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
|
|
452
|
+
1. Start live editing: \`emailr templates edit <template_id> --file ./template.html --background\`
|
|
453
|
+
2. Edit the file at ./template.html - browser auto-refreshes on every save
|
|
2347
454
|
3. When satisfied: \`emailr templates update <template_id> --html-file ./template.html\`
|
|
455
|
+
4. Stop server: \`emailr templates stop-preview\`
|
|
2348
456
|
|
|
2349
457
|
### 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
|
|
458
|
+
1. Start drafting: \`emailr templates draft --file ./new-template.html --background\`
|
|
459
|
+
2. Edit the file at ./new-template.html - browser auto-refreshes on every save
|
|
2352
460
|
3. When satisfied: \`emailr templates create --name "Template Name" --subject "Subject" --html-file ./new-template.html\`
|
|
461
|
+
4. Stop server: \`emailr templates stop-preview\`
|
|
2353
462
|
|
|
2354
463
|
## Common commands
|
|
2355
464
|
|
|
@@ -2365,41 +474,7 @@ description: Operate the Emailr CLI to send emails, manage contacts, templates,
|
|
|
2365
474
|
- Create: \`emailr templates create --name "Name" --subject "Subject" --html-file ./template.html\`
|
|
2366
475
|
- Update: \`emailr templates update <id> --html-file ./template.html\`
|
|
2367
476
|
- Delete: \`emailr templates delete <id>\`
|
|
2368
|
-
`;
|
|
2369
|
-
function isOpenCodeInstalled() {
|
|
2370
|
-
try {
|
|
2371
|
-
execSync("which opencode", { stdio: "ignore" });
|
|
2372
|
-
return true;
|
|
2373
|
-
} catch {
|
|
2374
|
-
return false;
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
function installOpenCode() {
|
|
2378
|
-
console.log("Installing OpenCode AI agent...");
|
|
2379
|
-
try {
|
|
2380
|
-
execSync("npm install -g opencode-ai@latest", { stdio: "inherit" });
|
|
2381
|
-
return true;
|
|
2382
|
-
} catch {
|
|
2383
|
-
if (process.platform === "darwin") {
|
|
2384
|
-
try {
|
|
2385
|
-
execSync("brew install anomalyco/tap/opencode", { stdio: "inherit" });
|
|
2386
|
-
return true;
|
|
2387
|
-
} catch {
|
|
2388
|
-
return false;
|
|
2389
|
-
}
|
|
2390
|
-
}
|
|
2391
|
-
return false;
|
|
2392
|
-
}
|
|
2393
|
-
}
|
|
2394
|
-
function ensureSkillInstalled() {
|
|
2395
|
-
if (!fs5.existsSync(OPENCODE_SKILL_DIR)) {
|
|
2396
|
-
fs5.mkdirSync(OPENCODE_SKILL_DIR, { recursive: true });
|
|
2397
|
-
}
|
|
2398
|
-
const skillPath = path5.join(OPENCODE_SKILL_DIR, "SKILL.md");
|
|
2399
|
-
fs5.writeFileSync(skillPath, SKILL_MD, "utf-8");
|
|
2400
|
-
}
|
|
2401
|
-
function createAgentCommand() {
|
|
2402
|
-
const cmd = new Command10("agent").description(`Launch an AI agent with Emailr CLI expertise
|
|
477
|
+
`;function wt(){try{return execSync("which opencode",{stdio:"ignore"}),!0}catch{return false}}function vt(){console.log("Installing OpenCode AI agent...");try{return execSync("npm install -g opencode-ai@latest",{stdio:"inherit"}),!0}catch{if(process.platform==="darwin")try{return execSync("brew install anomalyco/tap/opencode",{stdio:"inherit"}),!0}catch{return false}return false}}function xt(){S.existsSync(te)||S.mkdirSync(te,{recursive:true});let n=N.join(te,"SKILL.md");S.writeFileSync(n,yt,"utf-8");}function _e(){return new Command("agent").description(`Launch an AI agent with Emailr CLI expertise
|
|
2403
478
|
|
|
2404
479
|
This opens OpenCode (opencode.ai), an AI coding agent that knows how to use all Emailr CLI commands.
|
|
2405
480
|
The agent can help you:
|
|
@@ -2407,65 +482,7 @@ The agent can help you:
|
|
|
2407
482
|
- Manage contacts, broadcasts, and segments
|
|
2408
483
|
- Configure domains and webhooks
|
|
2409
484
|
|
|
2410
|
-
The agent runs in your terminal with full access to the emailr CLI.`).option("--install",
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
if (!installed) {
|
|
2415
|
-
error("Failed to install OpenCode. Please install manually:");
|
|
2416
|
-
console.log(" npm install -g opencode-ai@latest");
|
|
2417
|
-
console.log(" # or");
|
|
2418
|
-
console.log(" brew install anomalyco/tap/opencode");
|
|
2419
|
-
process.exit(1);
|
|
2420
|
-
}
|
|
2421
|
-
} else {
|
|
2422
|
-
error("OpenCode AI agent is not installed.");
|
|
2423
|
-
console.log("\nInstall it with one of:");
|
|
2424
|
-
console.log(" emailr agent --install");
|
|
2425
|
-
console.log(" npm install -g opencode-ai@latest");
|
|
2426
|
-
console.log(" brew install anomalyco/tap/opencode");
|
|
2427
|
-
process.exit(1);
|
|
2428
|
-
}
|
|
2429
|
-
}
|
|
2430
|
-
try {
|
|
2431
|
-
ensureSkillInstalled();
|
|
2432
|
-
success("Emailr CLI skill loaded");
|
|
2433
|
-
} catch (err) {
|
|
2434
|
-
warn(`Could not install skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
2435
|
-
}
|
|
2436
|
-
const args = [];
|
|
2437
|
-
if (options.model) {
|
|
2438
|
-
args.push("--model", options.model);
|
|
2439
|
-
}
|
|
2440
|
-
console.log("\nStarting Emailr AI Agent...");
|
|
2441
|
-
console.log("The agent has the emailr-cli skill loaded.");
|
|
2442
|
-
console.log("Ask it to help with emails, templates, contacts, or broadcasts.\n");
|
|
2443
|
-
const opencode = spawn("opencode", args, {
|
|
2444
|
-
stdio: "inherit",
|
|
2445
|
-
env: process.env
|
|
2446
|
-
});
|
|
2447
|
-
opencode.on("error", (err) => {
|
|
2448
|
-
error(`Failed to start agent: ${err.message}`);
|
|
2449
|
-
process.exit(1);
|
|
2450
|
-
});
|
|
2451
|
-
opencode.on("exit", (code) => {
|
|
2452
|
-
process.exit(code ?? 0);
|
|
2453
|
-
});
|
|
2454
|
-
});
|
|
2455
|
-
return cmd;
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2458
|
-
// src/index.ts
|
|
2459
|
-
var program = new Command11();
|
|
2460
|
-
program.name("emailr").description("Emailr CLI - Send emails and manage your email infrastructure").version("1.0.0");
|
|
2461
|
-
program.addCommand(createSendCommand());
|
|
2462
|
-
program.addCommand(createContactsCommand());
|
|
2463
|
-
program.addCommand(createTemplatesCommand());
|
|
2464
|
-
program.addCommand(createDomainsCommand());
|
|
2465
|
-
program.addCommand(createBroadcastsCommand());
|
|
2466
|
-
program.addCommand(createWebhooksCommand());
|
|
2467
|
-
program.addCommand(createSegmentsCommand());
|
|
2468
|
-
program.addCommand(createConfigCommand());
|
|
2469
|
-
program.addCommand(createLoginCommand());
|
|
2470
|
-
program.addCommand(createAgentCommand());
|
|
2471
|
-
program.parse();
|
|
485
|
+
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 t=>{wt()||(t.install?vt()||(c("Failed to install OpenCode. Please install manually:"),console.log(" npm install -g opencode-ai@latest"),console.log(" # or"),console.log(" brew install anomalyco/tap/opencode"),process.exit(1)):(c("OpenCode AI agent is not installed."),console.log(`
|
|
486
|
+
Install it with one of:`),console.log(" emailr agent --install"),console.log(" npm install -g opencode-ai@latest"),console.log(" brew install anomalyco/tap/opencode"),process.exit(1)));try{xt(),d("Emailr CLI skill loaded");}catch(i){v(`Could not install skill: ${i instanceof Error?i.message:String(i)}`);}let e=[];t.model&&e.push("--model",t.model),console.log(`
|
|
487
|
+
Starting Emailr AI Agent...`),console.log("The agent has the emailr-cli skill loaded."),console.log(`Ask it to help with emails, templates, contacts, or broadcasts.
|
|
488
|
+
`);let o=spawn("opencode",e,{stdio:"inherit",env:process.env});o.on("error",i=>{c(`Failed to start agent: ${i.message}`),process.exit(1);}),o.on("exit",i=>{process.exit(i??0);});})}var u=new Command;u.name("emailr").description("Emailr CLI - Send emails and manage your email infrastructure").version("1.0.0");u.addCommand(le());u.addCommand(ce());u.addCommand(Se());u.addCommand(Ce());u.addCommand(je());u.addCommand(Ee());u.addCommand(Pe());u.addCommand(ke());u.addCommand(Ue());u.addCommand(_e());u.parse();
|