emailr-cli 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +740 -14
- package/package.json +16 -12
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command10 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/send.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -142,6 +142,9 @@ function success(message) {
|
|
|
142
142
|
function error(message) {
|
|
143
143
|
console.error(chalk.red("\u2717"), message);
|
|
144
144
|
}
|
|
145
|
+
function warn(message) {
|
|
146
|
+
console.warn(chalk.yellow("\u26A0"), message);
|
|
147
|
+
}
|
|
145
148
|
function info(message) {
|
|
146
149
|
console.log(chalk.blue("\u2139"), message);
|
|
147
150
|
}
|
|
@@ -340,6 +343,309 @@ Total: ${result.total}`);
|
|
|
340
343
|
// src/commands/templates.ts
|
|
341
344
|
import { Command as Command3 } from "commander";
|
|
342
345
|
import { Emailr as Emailr3 } from "emailr";
|
|
346
|
+
|
|
347
|
+
// src/server/template-store.ts
|
|
348
|
+
import fs2 from "fs";
|
|
349
|
+
import os2 from "os";
|
|
350
|
+
import path2 from "path";
|
|
351
|
+
function getTemplatesDir() {
|
|
352
|
+
return path2.join(os2.homedir(), ".config", "emailr", "templates");
|
|
353
|
+
}
|
|
354
|
+
function ensureTemplatesDir() {
|
|
355
|
+
const templatesDir = getTemplatesDir();
|
|
356
|
+
if (!fs2.existsSync(templatesDir)) {
|
|
357
|
+
fs2.mkdirSync(templatesDir, { recursive: true });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function getTemplatePath(templateId) {
|
|
361
|
+
return path2.join(getTemplatesDir(), `${templateId}.html`);
|
|
362
|
+
}
|
|
363
|
+
function saveTemplate(templateId, htmlContent) {
|
|
364
|
+
ensureTemplatesDir();
|
|
365
|
+
const templatePath = getTemplatePath(templateId);
|
|
366
|
+
fs2.writeFileSync(templatePath, htmlContent, "utf-8");
|
|
367
|
+
}
|
|
368
|
+
function readTemplate(templateId) {
|
|
369
|
+
const templatePath = getTemplatePath(templateId);
|
|
370
|
+
if (!fs2.existsSync(templatePath)) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
return fs2.readFileSync(templatePath, "utf-8");
|
|
374
|
+
}
|
|
375
|
+
function templateExists(templateId) {
|
|
376
|
+
const templatePath = getTemplatePath(templateId);
|
|
377
|
+
return fs2.existsSync(templatePath);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/server/preview-server.ts
|
|
381
|
+
import http from "http";
|
|
382
|
+
var serverInstance = null;
|
|
383
|
+
var serverPort = null;
|
|
384
|
+
var serverRunning = false;
|
|
385
|
+
function escapeHtml(str) {
|
|
386
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
387
|
+
}
|
|
388
|
+
function isEmptyContent(content) {
|
|
389
|
+
return content === null || content.trim() === "";
|
|
390
|
+
}
|
|
391
|
+
function generateEmptyContentPlaceholder(templateId) {
|
|
392
|
+
const escapedId = escapeHtml(templateId);
|
|
393
|
+
return `<!DOCTYPE html>
|
|
394
|
+
<html lang="en">
|
|
395
|
+
<head>
|
|
396
|
+
<meta charset="UTF-8">
|
|
397
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
398
|
+
<title>No Content - Emailr CLI</title>
|
|
399
|
+
<style>
|
|
400
|
+
body {
|
|
401
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
402
|
+
display: flex;
|
|
403
|
+
justify-content: center;
|
|
404
|
+
align-items: center;
|
|
405
|
+
min-height: 100vh;
|
|
406
|
+
margin: 0;
|
|
407
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
408
|
+
color: #fff;
|
|
409
|
+
}
|
|
410
|
+
.container {
|
|
411
|
+
text-align: center;
|
|
412
|
+
padding: 2rem;
|
|
413
|
+
background: rgba(255, 255, 255, 0.1);
|
|
414
|
+
border-radius: 16px;
|
|
415
|
+
backdrop-filter: blur(10px);
|
|
416
|
+
max-width: 500px;
|
|
417
|
+
}
|
|
418
|
+
.icon {
|
|
419
|
+
font-size: 4rem;
|
|
420
|
+
margin-bottom: 1rem;
|
|
421
|
+
}
|
|
422
|
+
h1 {
|
|
423
|
+
margin: 0 0 1rem 0;
|
|
424
|
+
font-size: 1.5rem;
|
|
425
|
+
}
|
|
426
|
+
p {
|
|
427
|
+
margin: 0;
|
|
428
|
+
opacity: 0.9;
|
|
429
|
+
line-height: 1.6;
|
|
430
|
+
}
|
|
431
|
+
.template-id {
|
|
432
|
+
margin-top: 1rem;
|
|
433
|
+
padding: 1rem;
|
|
434
|
+
background: rgba(0, 0, 0, 0.2);
|
|
435
|
+
border-radius: 8px;
|
|
436
|
+
font-size: 0.9rem;
|
|
437
|
+
font-family: monospace;
|
|
438
|
+
}
|
|
439
|
+
.hint {
|
|
440
|
+
margin-top: 1.5rem;
|
|
441
|
+
padding: 1rem;
|
|
442
|
+
background: rgba(255, 255, 255, 0.15);
|
|
443
|
+
border-radius: 8px;
|
|
444
|
+
font-size: 0.85rem;
|
|
445
|
+
}
|
|
446
|
+
code {
|
|
447
|
+
background: rgba(0, 0, 0, 0.2);
|
|
448
|
+
padding: 0.2rem 0.4rem;
|
|
449
|
+
border-radius: 4px;
|
|
450
|
+
font-family: monospace;
|
|
451
|
+
}
|
|
452
|
+
</style>
|
|
453
|
+
</head>
|
|
454
|
+
<body>
|
|
455
|
+
<div class="container">
|
|
456
|
+
<div class="icon">\u{1F4ED}</div>
|
|
457
|
+
<h1>No Content Available</h1>
|
|
458
|
+
<p>This template exists but has no HTML content to display.</p>
|
|
459
|
+
<div class="template-id">${escapedId}</div>
|
|
460
|
+
<div class="hint">
|
|
461
|
+
<p><strong>To add content:</strong></p>
|
|
462
|
+
<p>Update the template using the CLI with the <code>--html</code> option or provide HTML content when creating the template.</p>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
</body>
|
|
466
|
+
</html>`;
|
|
467
|
+
}
|
|
468
|
+
function generateNotFoundHtml(templateId) {
|
|
469
|
+
const escapedId = escapeHtml(templateId);
|
|
470
|
+
return `<!DOCTYPE html>
|
|
471
|
+
<html lang="en">
|
|
472
|
+
<head>
|
|
473
|
+
<meta charset="UTF-8">
|
|
474
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
475
|
+
<title>Template Not Found - Emailr CLI</title>
|
|
476
|
+
<style>
|
|
477
|
+
body {
|
|
478
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
479
|
+
display: flex;
|
|
480
|
+
justify-content: center;
|
|
481
|
+
align-items: center;
|
|
482
|
+
min-height: 100vh;
|
|
483
|
+
margin: 0;
|
|
484
|
+
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
485
|
+
color: #fff;
|
|
486
|
+
}
|
|
487
|
+
.container {
|
|
488
|
+
text-align: center;
|
|
489
|
+
padding: 2rem;
|
|
490
|
+
background: rgba(255, 255, 255, 0.1);
|
|
491
|
+
border-radius: 16px;
|
|
492
|
+
backdrop-filter: blur(10px);
|
|
493
|
+
max-width: 400px;
|
|
494
|
+
}
|
|
495
|
+
.icon {
|
|
496
|
+
font-size: 4rem;
|
|
497
|
+
margin-bottom: 1rem;
|
|
498
|
+
}
|
|
499
|
+
h1 {
|
|
500
|
+
margin: 0 0 1rem 0;
|
|
501
|
+
font-size: 1.5rem;
|
|
502
|
+
}
|
|
503
|
+
p {
|
|
504
|
+
margin: 0;
|
|
505
|
+
opacity: 0.9;
|
|
506
|
+
}
|
|
507
|
+
.template-id {
|
|
508
|
+
margin-top: 1rem;
|
|
509
|
+
padding: 1rem;
|
|
510
|
+
background: rgba(0, 0, 0, 0.2);
|
|
511
|
+
border-radius: 8px;
|
|
512
|
+
font-size: 0.9rem;
|
|
513
|
+
font-family: monospace;
|
|
514
|
+
}
|
|
515
|
+
</style>
|
|
516
|
+
</head>
|
|
517
|
+
<body>
|
|
518
|
+
<div class="container">
|
|
519
|
+
<div class="icon">404</div>
|
|
520
|
+
<h1>Template Not Found</h1>
|
|
521
|
+
<p>The requested template could not be found in local storage.</p>
|
|
522
|
+
<div class="template-id">${escapedId}</div>
|
|
523
|
+
<p style="margin-top: 1rem;">Try creating or retrieving the template first using the CLI.</p>
|
|
524
|
+
</div>
|
|
525
|
+
</body>
|
|
526
|
+
</html>`;
|
|
527
|
+
}
|
|
528
|
+
function parseTemplateIdFromPath(urlPath) {
|
|
529
|
+
const match = urlPath.match(/^\/preview\/([^/]+)$/);
|
|
530
|
+
return match ? match[1] : null;
|
|
531
|
+
}
|
|
532
|
+
function createRequestHandler() {
|
|
533
|
+
return (req, res) => {
|
|
534
|
+
if (req.method !== "GET") {
|
|
535
|
+
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
536
|
+
res.end("Method Not Allowed");
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const urlPath = req.url || "/";
|
|
540
|
+
const templateId = parseTemplateIdFromPath(urlPath);
|
|
541
|
+
if (!templateId) {
|
|
542
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
543
|
+
res.end("Not Found");
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (!templateExists(templateId)) {
|
|
547
|
+
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
548
|
+
res.end(generateNotFoundHtml(templateId));
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const htmlContent = readTemplate(templateId);
|
|
552
|
+
if (htmlContent === null) {
|
|
553
|
+
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
554
|
+
res.end(generateNotFoundHtml(templateId));
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (isEmptyContent(htmlContent)) {
|
|
558
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
559
|
+
res.end(generateEmptyContentPlaceholder(templateId));
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
563
|
+
res.end(htmlContent);
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
var previewServer = {
|
|
567
|
+
async start() {
|
|
568
|
+
if (serverRunning && serverPort !== null) {
|
|
569
|
+
return serverPort;
|
|
570
|
+
}
|
|
571
|
+
return new Promise((resolve, reject) => {
|
|
572
|
+
serverInstance = http.createServer(createRequestHandler());
|
|
573
|
+
serverInstance.listen(0, "127.0.0.1", () => {
|
|
574
|
+
const address = serverInstance.address();
|
|
575
|
+
if (address && typeof address === "object") {
|
|
576
|
+
serverPort = address.port;
|
|
577
|
+
serverRunning = true;
|
|
578
|
+
serverInstance.unref();
|
|
579
|
+
resolve(serverPort);
|
|
580
|
+
} else {
|
|
581
|
+
reject(new Error("Failed to get server address"));
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
serverInstance.on("error", (err) => {
|
|
585
|
+
serverRunning = false;
|
|
586
|
+
serverPort = null;
|
|
587
|
+
serverInstance = null;
|
|
588
|
+
reject(new Error(`Failed to start preview server: ${err.message}`));
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
},
|
|
592
|
+
getPort() {
|
|
593
|
+
return serverPort;
|
|
594
|
+
},
|
|
595
|
+
isRunning() {
|
|
596
|
+
return serverRunning;
|
|
597
|
+
},
|
|
598
|
+
async stop() {
|
|
599
|
+
if (serverInstance) {
|
|
600
|
+
return new Promise((resolve) => {
|
|
601
|
+
serverInstance.close(() => {
|
|
602
|
+
serverInstance = null;
|
|
603
|
+
serverPort = null;
|
|
604
|
+
serverRunning = false;
|
|
605
|
+
resolve();
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
function getPreviewServer() {
|
|
612
|
+
return previewServer;
|
|
613
|
+
}
|
|
614
|
+
async function getPreviewUrl(templateId) {
|
|
615
|
+
try {
|
|
616
|
+
const port = await previewServer.start();
|
|
617
|
+
return `http://127.0.0.1:${port}/preview/${templateId}`;
|
|
618
|
+
} catch {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function keepServerAlive() {
|
|
623
|
+
if (serverInstance) {
|
|
624
|
+
serverInstance.ref();
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/commands/templates.ts
|
|
629
|
+
async function handleTemplatePreview(template) {
|
|
630
|
+
try {
|
|
631
|
+
const htmlContent = template.html_content ?? "";
|
|
632
|
+
saveTemplate(template.id, htmlContent);
|
|
633
|
+
} catch (err) {
|
|
634
|
+
warn(`Could not save template for preview: ${err instanceof Error ? err.message : String(err)}`);
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
const previewUrl = await getPreviewUrl(template.id);
|
|
639
|
+
if (previewUrl === null) {
|
|
640
|
+
warn("Could not start preview server");
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
return previewUrl;
|
|
644
|
+
} catch (err) {
|
|
645
|
+
warn(`Could not generate preview URL: ${err instanceof Error ? err.message : String(err)}`);
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
343
649
|
function createTemplatesCommand() {
|
|
344
650
|
const cmd = new Command3("templates").description("Manage email templates");
|
|
345
651
|
cmd.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 (options) => {
|
|
@@ -380,7 +686,28 @@ Total: ${result.length}`);
|
|
|
380
686
|
baseUrl: config.baseUrl
|
|
381
687
|
});
|
|
382
688
|
const template = await client.templates.get(id);
|
|
383
|
-
|
|
689
|
+
const previewUrl = await handleTemplatePreview({
|
|
690
|
+
id: template.id,
|
|
691
|
+
html_content: template.html_content ?? void 0
|
|
692
|
+
});
|
|
693
|
+
if (options.format === "json") {
|
|
694
|
+
output({
|
|
695
|
+
...template,
|
|
696
|
+
preview_url: previewUrl
|
|
697
|
+
}, "json");
|
|
698
|
+
} else {
|
|
699
|
+
const tableData = {
|
|
700
|
+
ID: template.id,
|
|
701
|
+
Name: template.name,
|
|
702
|
+
Subject: template.subject,
|
|
703
|
+
Variables: template.variables?.join(", ") || "-",
|
|
704
|
+
Created: template.created_at
|
|
705
|
+
};
|
|
706
|
+
if (previewUrl) {
|
|
707
|
+
tableData["Preview URL"] = previewUrl;
|
|
708
|
+
}
|
|
709
|
+
output(tableData, "table");
|
|
710
|
+
}
|
|
384
711
|
} catch (err) {
|
|
385
712
|
error(err instanceof Error ? err.message : "Failed to get template");
|
|
386
713
|
process.exit(1);
|
|
@@ -398,14 +725,14 @@ Total: ${result.length}`);
|
|
|
398
725
|
subject: options.subject
|
|
399
726
|
};
|
|
400
727
|
if (options.htmlFile) {
|
|
401
|
-
const
|
|
402
|
-
request.html_content =
|
|
728
|
+
const fs3 = await import("fs");
|
|
729
|
+
request.html_content = fs3.readFileSync(options.htmlFile, "utf-8");
|
|
403
730
|
} else if (options.html) {
|
|
404
731
|
request.html_content = options.html;
|
|
405
732
|
}
|
|
406
733
|
if (options.textFile) {
|
|
407
|
-
const
|
|
408
|
-
request.text_content =
|
|
734
|
+
const fs3 = await import("fs");
|
|
735
|
+
request.text_content = fs3.readFileSync(options.textFile, "utf-8");
|
|
409
736
|
} else if (options.text) {
|
|
410
737
|
request.text_content = options.text;
|
|
411
738
|
}
|
|
@@ -413,16 +740,31 @@ Total: ${result.length}`);
|
|
|
413
740
|
if (options.replyTo) request.reply_to = options.replyTo;
|
|
414
741
|
if (options.previewText) request.preview_text = options.previewText;
|
|
415
742
|
const template = await client.templates.create(request);
|
|
743
|
+
const previewUrl = await handleTemplatePreview({
|
|
744
|
+
id: template.id,
|
|
745
|
+
html_content: template.html_content ?? void 0
|
|
746
|
+
});
|
|
416
747
|
if (options.format === "json") {
|
|
417
|
-
output(
|
|
748
|
+
output({
|
|
749
|
+
...template,
|
|
750
|
+
preview_url: previewUrl
|
|
751
|
+
}, "json");
|
|
418
752
|
} else {
|
|
419
753
|
success(`Template created: ${template.id}`);
|
|
420
|
-
|
|
754
|
+
const tableData = {
|
|
421
755
|
ID: template.id,
|
|
422
756
|
Name: template.name,
|
|
423
757
|
Subject: template.subject,
|
|
424
758
|
Variables: template.variables?.join(", ") || "-"
|
|
425
|
-
}
|
|
759
|
+
};
|
|
760
|
+
if (previewUrl) {
|
|
761
|
+
tableData["Preview URL"] = previewUrl;
|
|
762
|
+
}
|
|
763
|
+
output(tableData, "table");
|
|
764
|
+
if (previewUrl) {
|
|
765
|
+
console.log(`
|
|
766
|
+
Open the Preview URL in your browser to view the rendered template.`);
|
|
767
|
+
}
|
|
426
768
|
}
|
|
427
769
|
} catch (err) {
|
|
428
770
|
error(err instanceof Error ? err.message : "Failed to create template");
|
|
@@ -440,14 +782,14 @@ Total: ${result.length}`);
|
|
|
440
782
|
if (options.name) request.name = options.name;
|
|
441
783
|
if (options.subject) request.subject = options.subject;
|
|
442
784
|
if (options.htmlFile) {
|
|
443
|
-
const
|
|
444
|
-
request.html_content =
|
|
785
|
+
const fs3 = await import("fs");
|
|
786
|
+
request.html_content = fs3.readFileSync(options.htmlFile, "utf-8");
|
|
445
787
|
} else if (options.html) {
|
|
446
788
|
request.html_content = options.html;
|
|
447
789
|
}
|
|
448
790
|
if (options.textFile) {
|
|
449
|
-
const
|
|
450
|
-
request.text_content =
|
|
791
|
+
const fs3 = await import("fs");
|
|
792
|
+
request.text_content = fs3.readFileSync(options.textFile, "utf-8");
|
|
451
793
|
} else if (options.text) {
|
|
452
794
|
request.text_content = options.text;
|
|
453
795
|
}
|
|
@@ -480,6 +822,51 @@ Total: ${result.length}`);
|
|
|
480
822
|
process.exit(1);
|
|
481
823
|
}
|
|
482
824
|
});
|
|
825
|
+
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) => {
|
|
826
|
+
try {
|
|
827
|
+
const config = loadConfig();
|
|
828
|
+
const client = new Emailr3({
|
|
829
|
+
apiKey: config.apiKey,
|
|
830
|
+
baseUrl: config.baseUrl
|
|
831
|
+
});
|
|
832
|
+
console.log(`Fetching template ${id}...`);
|
|
833
|
+
const template = await client.templates.get(id);
|
|
834
|
+
const htmlContent = template.html_content ?? "";
|
|
835
|
+
saveTemplate(template.id, htmlContent);
|
|
836
|
+
const previewUrl = await getPreviewUrl(template.id);
|
|
837
|
+
if (!previewUrl) {
|
|
838
|
+
error("Failed to start preview server");
|
|
839
|
+
process.exit(1);
|
|
840
|
+
}
|
|
841
|
+
keepServerAlive();
|
|
842
|
+
console.log(`
|
|
843
|
+
Template: ${template.name}`);
|
|
844
|
+
console.log(`Preview URL: ${previewUrl}`);
|
|
845
|
+
if (options.open !== false) {
|
|
846
|
+
try {
|
|
847
|
+
const open = await import("open");
|
|
848
|
+
await open.default(previewUrl);
|
|
849
|
+
console.log("\nBrowser opened. Press Ctrl+C to stop the preview server.");
|
|
850
|
+
} catch {
|
|
851
|
+
console.log("\nCould not open browser automatically. Open the URL above manually.");
|
|
852
|
+
console.log("Press Ctrl+C to stop the preview server.");
|
|
853
|
+
}
|
|
854
|
+
} else {
|
|
855
|
+
console.log("\nOpen the URL above in your browser.");
|
|
856
|
+
console.log("Press Ctrl+C to stop the preview server.");
|
|
857
|
+
}
|
|
858
|
+
process.on("SIGINT", async () => {
|
|
859
|
+
console.log("\n\nStopping preview server...");
|
|
860
|
+
const server = getPreviewServer();
|
|
861
|
+
await server.stop();
|
|
862
|
+
console.log("Done.");
|
|
863
|
+
process.exit(0);
|
|
864
|
+
});
|
|
865
|
+
} catch (err) {
|
|
866
|
+
error(err instanceof Error ? err.message : "Failed to preview template");
|
|
867
|
+
process.exit(1);
|
|
868
|
+
}
|
|
869
|
+
});
|
|
483
870
|
return cmd;
|
|
484
871
|
}
|
|
485
872
|
|
|
@@ -1289,8 +1676,346 @@ function createSegmentsCommand() {
|
|
|
1289
1676
|
return cmd;
|
|
1290
1677
|
}
|
|
1291
1678
|
|
|
1679
|
+
// src/commands/login.ts
|
|
1680
|
+
import { Command as Command9 } from "commander";
|
|
1681
|
+
|
|
1682
|
+
// src/server/callback.ts
|
|
1683
|
+
import http2 from "http";
|
|
1684
|
+
import { URL } from "url";
|
|
1685
|
+
function parseCallbackParams(url) {
|
|
1686
|
+
try {
|
|
1687
|
+
const fullUrl = url.startsWith("http") ? url : `http://localhost${url}`;
|
|
1688
|
+
const parsed = new URL(fullUrl);
|
|
1689
|
+
return {
|
|
1690
|
+
key: parsed.searchParams.get("key") ?? void 0,
|
|
1691
|
+
state: parsed.searchParams.get("state") ?? void 0,
|
|
1692
|
+
error: parsed.searchParams.get("error") ?? void 0,
|
|
1693
|
+
message: parsed.searchParams.get("message") ?? void 0
|
|
1694
|
+
};
|
|
1695
|
+
} catch {
|
|
1696
|
+
return {};
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
function generateSuccessHtml() {
|
|
1700
|
+
return `<!DOCTYPE html>
|
|
1701
|
+
<html lang="en">
|
|
1702
|
+
<head>
|
|
1703
|
+
<meta charset="UTF-8">
|
|
1704
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1705
|
+
<title>Login Successful - Emailr CLI</title>
|
|
1706
|
+
<style>
|
|
1707
|
+
body {
|
|
1708
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
1709
|
+
display: flex;
|
|
1710
|
+
justify-content: center;
|
|
1711
|
+
align-items: center;
|
|
1712
|
+
min-height: 100vh;
|
|
1713
|
+
margin: 0;
|
|
1714
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
1715
|
+
color: #fff;
|
|
1716
|
+
}
|
|
1717
|
+
.container {
|
|
1718
|
+
text-align: center;
|
|
1719
|
+
padding: 2rem;
|
|
1720
|
+
background: rgba(255, 255, 255, 0.1);
|
|
1721
|
+
border-radius: 16px;
|
|
1722
|
+
backdrop-filter: blur(10px);
|
|
1723
|
+
max-width: 400px;
|
|
1724
|
+
}
|
|
1725
|
+
.icon {
|
|
1726
|
+
font-size: 4rem;
|
|
1727
|
+
margin-bottom: 1rem;
|
|
1728
|
+
}
|
|
1729
|
+
h1 {
|
|
1730
|
+
margin: 0 0 1rem 0;
|
|
1731
|
+
font-size: 1.5rem;
|
|
1732
|
+
}
|
|
1733
|
+
p {
|
|
1734
|
+
margin: 0;
|
|
1735
|
+
opacity: 0.9;
|
|
1736
|
+
}
|
|
1737
|
+
</style>
|
|
1738
|
+
</head>
|
|
1739
|
+
<body>
|
|
1740
|
+
<div class="container">
|
|
1741
|
+
<div class="icon">\u2713</div>
|
|
1742
|
+
<h1>Login Successful!</h1>
|
|
1743
|
+
<p>You can close this window and return to your terminal.</p>
|
|
1744
|
+
</div>
|
|
1745
|
+
</body>
|
|
1746
|
+
</html>`;
|
|
1747
|
+
}
|
|
1748
|
+
function generateErrorHtml(errorMessage) {
|
|
1749
|
+
const escapedMessage = errorMessage.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1750
|
+
return `<!DOCTYPE html>
|
|
1751
|
+
<html lang="en">
|
|
1752
|
+
<head>
|
|
1753
|
+
<meta charset="UTF-8">
|
|
1754
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1755
|
+
<title>Login Failed - Emailr CLI</title>
|
|
1756
|
+
<style>
|
|
1757
|
+
body {
|
|
1758
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
1759
|
+
display: flex;
|
|
1760
|
+
justify-content: center;
|
|
1761
|
+
align-items: center;
|
|
1762
|
+
min-height: 100vh;
|
|
1763
|
+
margin: 0;
|
|
1764
|
+
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
1765
|
+
color: #fff;
|
|
1766
|
+
}
|
|
1767
|
+
.container {
|
|
1768
|
+
text-align: center;
|
|
1769
|
+
padding: 2rem;
|
|
1770
|
+
background: rgba(255, 255, 255, 0.1);
|
|
1771
|
+
border-radius: 16px;
|
|
1772
|
+
backdrop-filter: blur(10px);
|
|
1773
|
+
max-width: 400px;
|
|
1774
|
+
}
|
|
1775
|
+
.icon {
|
|
1776
|
+
font-size: 4rem;
|
|
1777
|
+
margin-bottom: 1rem;
|
|
1778
|
+
}
|
|
1779
|
+
h1 {
|
|
1780
|
+
margin: 0 0 1rem 0;
|
|
1781
|
+
font-size: 1.5rem;
|
|
1782
|
+
}
|
|
1783
|
+
p {
|
|
1784
|
+
margin: 0;
|
|
1785
|
+
opacity: 0.9;
|
|
1786
|
+
}
|
|
1787
|
+
.error-message {
|
|
1788
|
+
margin-top: 1rem;
|
|
1789
|
+
padding: 1rem;
|
|
1790
|
+
background: rgba(0, 0, 0, 0.2);
|
|
1791
|
+
border-radius: 8px;
|
|
1792
|
+
font-size: 0.9rem;
|
|
1793
|
+
}
|
|
1794
|
+
</style>
|
|
1795
|
+
</head>
|
|
1796
|
+
<body>
|
|
1797
|
+
<div class="container">
|
|
1798
|
+
<div class="icon">\u2717</div>
|
|
1799
|
+
<h1>Login Failed</h1>
|
|
1800
|
+
<p>Something went wrong during authentication.</p>
|
|
1801
|
+
<div class="error-message">${escapedMessage}</div>
|
|
1802
|
+
<p style="margin-top: 1rem;">Please close this window and try again.</p>
|
|
1803
|
+
</div>
|
|
1804
|
+
</body>
|
|
1805
|
+
</html>`;
|
|
1806
|
+
}
|
|
1807
|
+
function createCallbackServer() {
|
|
1808
|
+
let server = null;
|
|
1809
|
+
let port = 0;
|
|
1810
|
+
let callbackPromiseResolve = null;
|
|
1811
|
+
let timeoutId = null;
|
|
1812
|
+
let expectedState = null;
|
|
1813
|
+
return {
|
|
1814
|
+
async start() {
|
|
1815
|
+
return new Promise((resolve, reject) => {
|
|
1816
|
+
server = http2.createServer((req, res) => {
|
|
1817
|
+
if (req.method !== "GET" || !req.url?.startsWith("/callback")) {
|
|
1818
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
1819
|
+
res.end("Not Found");
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
const params = parseCallbackParams(req.url);
|
|
1823
|
+
if (expectedState && params.state !== expectedState) {
|
|
1824
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
1825
|
+
res.end(generateErrorHtml("Security verification failed. State parameter mismatch."));
|
|
1826
|
+
if (callbackPromiseResolve) {
|
|
1827
|
+
callbackPromiseResolve({
|
|
1828
|
+
success: false,
|
|
1829
|
+
error: "State parameter mismatch"
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
if (params.error) {
|
|
1835
|
+
const errorMessage = params.message || params.error;
|
|
1836
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1837
|
+
res.end(generateErrorHtml(errorMessage));
|
|
1838
|
+
if (callbackPromiseResolve) {
|
|
1839
|
+
callbackPromiseResolve({
|
|
1840
|
+
success: false,
|
|
1841
|
+
error: errorMessage
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
if (params.key) {
|
|
1847
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1848
|
+
res.end(generateSuccessHtml());
|
|
1849
|
+
if (callbackPromiseResolve) {
|
|
1850
|
+
callbackPromiseResolve({
|
|
1851
|
+
success: true,
|
|
1852
|
+
apiKey: params.key
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
1858
|
+
res.end(generateErrorHtml("Invalid callback: missing required parameters."));
|
|
1859
|
+
if (callbackPromiseResolve) {
|
|
1860
|
+
callbackPromiseResolve({
|
|
1861
|
+
success: false,
|
|
1862
|
+
error: "Invalid callback: missing required parameters"
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1867
|
+
const address = server.address();
|
|
1868
|
+
if (address && typeof address === "object") {
|
|
1869
|
+
port = address.port;
|
|
1870
|
+
resolve({
|
|
1871
|
+
port,
|
|
1872
|
+
url: `http://127.0.0.1:${port}/callback`
|
|
1873
|
+
});
|
|
1874
|
+
} else {
|
|
1875
|
+
reject(new Error("Failed to get server address"));
|
|
1876
|
+
}
|
|
1877
|
+
});
|
|
1878
|
+
server.on("error", (err) => {
|
|
1879
|
+
reject(new Error(`Failed to start callback server: ${err.message}`));
|
|
1880
|
+
});
|
|
1881
|
+
});
|
|
1882
|
+
},
|
|
1883
|
+
async waitForCallback(state, timeoutMs) {
|
|
1884
|
+
expectedState = state;
|
|
1885
|
+
return new Promise((resolve) => {
|
|
1886
|
+
callbackPromiseResolve = resolve;
|
|
1887
|
+
timeoutId = setTimeout(() => {
|
|
1888
|
+
if (callbackPromiseResolve) {
|
|
1889
|
+
callbackPromiseResolve({
|
|
1890
|
+
success: false,
|
|
1891
|
+
error: "Login timed out. Please try again."
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
}, timeoutMs);
|
|
1895
|
+
});
|
|
1896
|
+
},
|
|
1897
|
+
async stop() {
|
|
1898
|
+
if (timeoutId) {
|
|
1899
|
+
clearTimeout(timeoutId);
|
|
1900
|
+
timeoutId = null;
|
|
1901
|
+
}
|
|
1902
|
+
if (server) {
|
|
1903
|
+
return new Promise((resolve) => {
|
|
1904
|
+
server.close(() => {
|
|
1905
|
+
server = null;
|
|
1906
|
+
resolve();
|
|
1907
|
+
});
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// src/utils/state.ts
|
|
1915
|
+
import crypto from "crypto";
|
|
1916
|
+
function generateState() {
|
|
1917
|
+
return crypto.randomBytes(32).toString("hex");
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// src/utils/browser.ts
|
|
1921
|
+
import { exec } from "child_process";
|
|
1922
|
+
function openBrowser(url) {
|
|
1923
|
+
return new Promise((resolve) => {
|
|
1924
|
+
const platform = process.platform;
|
|
1925
|
+
let command;
|
|
1926
|
+
switch (platform) {
|
|
1927
|
+
case "darwin":
|
|
1928
|
+
command = `open "${url}"`;
|
|
1929
|
+
break;
|
|
1930
|
+
case "win32":
|
|
1931
|
+
command = `start "" "${url}"`;
|
|
1932
|
+
break;
|
|
1933
|
+
default:
|
|
1934
|
+
command = `xdg-open "${url}"`;
|
|
1935
|
+
break;
|
|
1936
|
+
}
|
|
1937
|
+
exec(command, (error2) => {
|
|
1938
|
+
if (error2) {
|
|
1939
|
+
resolve(false);
|
|
1940
|
+
} else {
|
|
1941
|
+
resolve(true);
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
// src/commands/login.ts
|
|
1948
|
+
var DEFAULT_TIMEOUT_SECONDS = 120;
|
|
1949
|
+
var WEB_APP_BASE_URL = process.env.EMAILR_WEB_URL || "https://emailr.dev";
|
|
1950
|
+
function buildAuthorizationUrl(state, callbackPort) {
|
|
1951
|
+
const callbackUrl = `http://127.0.0.1:${callbackPort}/callback`;
|
|
1952
|
+
const params = new URLSearchParams({
|
|
1953
|
+
state,
|
|
1954
|
+
callback_url: callbackUrl
|
|
1955
|
+
});
|
|
1956
|
+
return `${WEB_APP_BASE_URL}/cli/authorize?${params.toString()}`;
|
|
1957
|
+
}
|
|
1958
|
+
function createLoginCommand() {
|
|
1959
|
+
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) => {
|
|
1960
|
+
await executeLogin({
|
|
1961
|
+
timeout: parseInt(options.timeout, 10) || DEFAULT_TIMEOUT_SECONDS,
|
|
1962
|
+
noBrowser: options.browser === false
|
|
1963
|
+
});
|
|
1964
|
+
});
|
|
1965
|
+
return cmd;
|
|
1966
|
+
}
|
|
1967
|
+
async function executeLogin(options) {
|
|
1968
|
+
const callbackServer = createCallbackServer();
|
|
1969
|
+
const timeoutMs = (options.timeout || DEFAULT_TIMEOUT_SECONDS) * 1e3;
|
|
1970
|
+
try {
|
|
1971
|
+
info("Starting authentication server...");
|
|
1972
|
+
const { port, url } = await callbackServer.start();
|
|
1973
|
+
const state = generateState();
|
|
1974
|
+
const authUrl = buildAuthorizationUrl(state, port);
|
|
1975
|
+
console.log("");
|
|
1976
|
+
info("Authorization URL:");
|
|
1977
|
+
console.log(` ${authUrl}`);
|
|
1978
|
+
console.log("");
|
|
1979
|
+
if (!options.noBrowser) {
|
|
1980
|
+
const browserOpened = await openBrowser(authUrl);
|
|
1981
|
+
if (browserOpened) {
|
|
1982
|
+
info("Browser opened. Please complete authentication in your browser.");
|
|
1983
|
+
} else {
|
|
1984
|
+
warn("Could not open browser automatically.");
|
|
1985
|
+
info("Please open the URL above in your browser to continue.");
|
|
1986
|
+
}
|
|
1987
|
+
} else {
|
|
1988
|
+
info("Please open the URL above in your browser to continue.");
|
|
1989
|
+
}
|
|
1990
|
+
console.log("");
|
|
1991
|
+
info(`Waiting for authentication (timeout: ${options.timeout || DEFAULT_TIMEOUT_SECONDS}s)...`);
|
|
1992
|
+
const result = await callbackServer.waitForCallback(state, timeoutMs);
|
|
1993
|
+
if (result.success && result.apiKey) {
|
|
1994
|
+
saveConfig({ apiKey: result.apiKey });
|
|
1995
|
+
console.log("");
|
|
1996
|
+
success("Login successful!");
|
|
1997
|
+
info(`API key saved to: ${getConfigPath()}`);
|
|
1998
|
+
info("You can now use the Emailr CLI.");
|
|
1999
|
+
} else {
|
|
2000
|
+
console.log("");
|
|
2001
|
+
error(result.error || "Authentication failed.");
|
|
2002
|
+
info("Please try again or use manual configuration:");
|
|
2003
|
+
console.log(" emailr config set api-key <your-api-key>");
|
|
2004
|
+
process.exit(1);
|
|
2005
|
+
}
|
|
2006
|
+
} catch (err) {
|
|
2007
|
+
console.log("");
|
|
2008
|
+
error(err instanceof Error ? err.message : "An unexpected error occurred.");
|
|
2009
|
+
info("Please try again or use manual configuration:");
|
|
2010
|
+
console.log(" emailr config set api-key <your-api-key>");
|
|
2011
|
+
process.exit(1);
|
|
2012
|
+
} finally {
|
|
2013
|
+
await callbackServer.stop();
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
1292
2017
|
// src/index.ts
|
|
1293
|
-
var program = new
|
|
2018
|
+
var program = new Command10();
|
|
1294
2019
|
program.name("emailr").description("Emailr CLI - Send emails and manage your email infrastructure").version("1.0.0");
|
|
1295
2020
|
program.addCommand(createSendCommand());
|
|
1296
2021
|
program.addCommand(createContactsCommand());
|
|
@@ -1300,4 +2025,5 @@ program.addCommand(createBroadcastsCommand());
|
|
|
1300
2025
|
program.addCommand(createWebhooksCommand());
|
|
1301
2026
|
program.addCommand(createSegmentsCommand());
|
|
1302
2027
|
program.addCommand(createConfigCommand());
|
|
2028
|
+
program.addCommand(createLoginCommand());
|
|
1303
2029
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "emailr-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Command-line interface for the Emailr email API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,25 +10,21 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"dist"
|
|
12
12
|
],
|
|
13
|
-
"scripts": {
|
|
14
|
-
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
15
|
-
"build:standalone": "npm run build && node scripts/build-standalone.js",
|
|
16
|
-
"dev": "tsup src/index.ts --format esm --watch",
|
|
17
|
-
"typecheck": "tsc --noEmit",
|
|
18
|
-
"prepublishOnly": "npm run build"
|
|
19
|
-
},
|
|
20
13
|
"dependencies": {
|
|
21
|
-
"emailr": "workspace:*",
|
|
22
14
|
"chalk": "^5.3.0",
|
|
23
15
|
"cli-table3": "^0.6.3",
|
|
24
|
-
"commander": "^12.0.0"
|
|
16
|
+
"commander": "^12.0.0",
|
|
17
|
+
"emailr": "^1.1.0",
|
|
18
|
+
"open": "^11.0.0"
|
|
25
19
|
},
|
|
26
20
|
"devDependencies": {
|
|
27
21
|
"@types/node": "^20.0.0",
|
|
28
22
|
"esbuild": "^0.20.0",
|
|
23
|
+
"fast-check": "^3.15.0",
|
|
29
24
|
"postject": "1.0.0-alpha.6",
|
|
30
25
|
"tsup": "^8.0.0",
|
|
31
|
-
"typescript": "^5.0.0"
|
|
26
|
+
"typescript": "^5.0.0",
|
|
27
|
+
"vitest": "^1.6.0"
|
|
32
28
|
},
|
|
33
29
|
"keywords": [
|
|
34
30
|
"emailr",
|
|
@@ -54,5 +50,13 @@
|
|
|
54
50
|
},
|
|
55
51
|
"publishConfig": {
|
|
56
52
|
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
56
|
+
"build:standalone": "npm run build && node scripts/build-standalone.js",
|
|
57
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
58
|
+
"typecheck": "tsc --noEmit",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"test:watch": "vitest"
|
|
57
61
|
}
|
|
58
|
-
}
|
|
62
|
+
}
|