@emailens/cli 0.1.0 → 0.3.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/README.md +44 -0
- package/dist/index.js +351 -56
- package/dist/index.js.map +1 -1
- package/dist/sdk-HQZAL655.js +3841 -0
- package/dist/sdk-HQZAL655.js.map +1 -0
- package/package.json +71 -72
- package/dist/maizzle-JGKTYDDO.js +0 -55
- package/dist/maizzle-JGKTYDDO.js.map +0 -1
- package/dist/mjml-KG66APFB.js +0 -48
- package/dist/mjml-KG66APFB.js.map +0 -1
- package/dist/react-email-ZWLGXROP.js +0 -164
- package/dist/react-email-ZWLGXROP.js.map +0 -1
package/README.md
CHANGED
|
@@ -48,6 +48,32 @@ emailens export email.html --json -o ./report
|
|
|
48
48
|
emailens export email.html --dark-mode --screenshots -o ./report
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
### `emailens fix <file>`
|
|
52
|
+
|
|
53
|
+
Generate AI-powered fixes for email compatibility issues. Uses `@emailens/engine` analysis to build a structured prompt, then calls Claude to fix structural issues (table layouts, VML, MSO conditionals) that static snippets can't handle.
|
|
54
|
+
|
|
55
|
+
Requires `ANTHROPIC_API_KEY` environment variable and the optional `@anthropic-ai/sdk` dependency.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
emailens fix email.html # Fix and print to stdout
|
|
59
|
+
emailens fix email.html -o fixed.html # Write to file
|
|
60
|
+
emailens fix email.html --estimate # Show token estimate only (no AI call)
|
|
61
|
+
emailens fix email.html --clients outlook-windows # Scope to one client
|
|
62
|
+
emailens fix email.html --json # Full JSON output with metadata
|
|
63
|
+
emailens fix email.html --max-tokens 8000 # Limit prompt size
|
|
64
|
+
cat email.html | emailens fix - --format jsx # Pipe from stdin
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
| Flag | Alias | Description |
|
|
68
|
+
|------|-------|-------------|
|
|
69
|
+
| `--format` | `-f` | Input format: `html`, `jsx`, `mjml`, `maizzle` |
|
|
70
|
+
| `--clients` | `-c` | Comma-separated client IDs to scope the fix |
|
|
71
|
+
| `--output` | `-o` | Write fixed code to file instead of stdout |
|
|
72
|
+
| `--json` | | Output as JSON (includes token estimates and metadata) |
|
|
73
|
+
| `--quiet` | `-q` | Suppress spinners and decorations |
|
|
74
|
+
| `--estimate` | | Only show token estimate without calling the AI |
|
|
75
|
+
| `--max-tokens` | | Maximum input tokens for the prompt (default: 16000) |
|
|
76
|
+
|
|
51
77
|
### `emailens clients`
|
|
52
78
|
|
|
53
79
|
List all 12 supported email clients.
|
|
@@ -76,6 +102,24 @@ Preview and export add:
|
|
|
76
102
|
| `--screenshots` | | Capture screenshots (requires `BROWSERLESS_URL`) |
|
|
77
103
|
| `--out` | `-o` | Output directory |
|
|
78
104
|
|
|
105
|
+
## AI Fixes
|
|
106
|
+
|
|
107
|
+
The `fix` command requires an `ANTHROPIC_API_KEY` environment variable and the `@anthropic-ai/sdk` package:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npm install @anthropic-ai/sdk
|
|
111
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Use `--estimate` to check token usage before making an API call:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
emailens fix email.html --estimate
|
|
118
|
+
# Input tokens: ~4,200
|
|
119
|
+
# Output tokens: ~5,400
|
|
120
|
+
# Warnings: 23 (5 structural)
|
|
121
|
+
```
|
|
122
|
+
|
|
79
123
|
## Framework Support
|
|
80
124
|
|
|
81
125
|
The CLI can compile React Email (JSX/TSX), MJML, and Maizzle templates to HTML before analysis. Format is auto-detected from file extension, or specify with `--format`.
|
package/dist/index.js
CHANGED
|
@@ -1,39 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { defineCommand as
|
|
4
|
+
import { defineCommand as defineCommand7, runMain } from "citty";
|
|
5
5
|
|
|
6
6
|
// src/commands/analyze.ts
|
|
7
7
|
import { defineCommand } from "citty";
|
|
8
8
|
import ora from "ora";
|
|
9
9
|
import {
|
|
10
10
|
analyzeEmail,
|
|
11
|
-
generateCompatibilityScore
|
|
11
|
+
generateCompatibilityScore,
|
|
12
|
+
warningsForClient,
|
|
13
|
+
CompileError
|
|
12
14
|
} from "@emailens/engine";
|
|
13
15
|
|
|
14
16
|
// src/utils.ts
|
|
15
17
|
import { readFileSync } from "fs";
|
|
16
18
|
import { EMAIL_CLIENTS } from "@emailens/engine";
|
|
17
|
-
|
|
18
|
-
// src/compile/detect.ts
|
|
19
|
-
import { extname } from "path";
|
|
20
|
-
function detectFormat(filePath) {
|
|
21
|
-
const ext = extname(filePath).toLowerCase();
|
|
22
|
-
switch (ext) {
|
|
23
|
-
case ".tsx":
|
|
24
|
-
case ".jsx":
|
|
25
|
-
return "jsx";
|
|
26
|
-
case ".mjml":
|
|
27
|
-
return "mjml";
|
|
28
|
-
case ".html":
|
|
29
|
-
case ".htm":
|
|
30
|
-
return "html";
|
|
31
|
-
default:
|
|
32
|
-
return "html";
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// src/utils.ts
|
|
19
|
+
import { detectFormat } from "@emailens/engine/compile";
|
|
37
20
|
var VALID_FORMATS = /* @__PURE__ */ new Set(["html", "jsx", "mjml", "maizzle"]);
|
|
38
21
|
async function readInput(pathOrStdin) {
|
|
39
22
|
if (pathOrStdin === "-") {
|
|
@@ -99,27 +82,8 @@ function toFramework(format) {
|
|
|
99
82
|
return void 0;
|
|
100
83
|
}
|
|
101
84
|
|
|
102
|
-
// src/
|
|
103
|
-
|
|
104
|
-
switch (format) {
|
|
105
|
-
case "html":
|
|
106
|
-
return source;
|
|
107
|
-
case "jsx": {
|
|
108
|
-
const { compileReactEmail } = await import("./react-email-ZWLGXROP.js");
|
|
109
|
-
return compileReactEmail(source);
|
|
110
|
-
}
|
|
111
|
-
case "mjml": {
|
|
112
|
-
const { compileMjml } = await import("./mjml-KG66APFB.js");
|
|
113
|
-
return compileMjml(source);
|
|
114
|
-
}
|
|
115
|
-
case "maizzle": {
|
|
116
|
-
const { compileMaizzle } = await import("./maizzle-JGKTYDDO.js");
|
|
117
|
-
return compileMaizzle(source);
|
|
118
|
-
}
|
|
119
|
-
default:
|
|
120
|
-
throw new Error(`Unknown format: "${format}". Use html, jsx, mjml, or maizzle.`);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
85
|
+
// src/commands/analyze.ts
|
|
86
|
+
import { compile } from "@emailens/engine/compile";
|
|
123
87
|
|
|
124
88
|
// src/output/terminal.ts
|
|
125
89
|
import pc from "picocolors";
|
|
@@ -315,7 +279,7 @@ var analyze_default = defineCommand({
|
|
|
315
279
|
scores[id] = data;
|
|
316
280
|
}
|
|
317
281
|
}
|
|
318
|
-
const filteredWarnings =
|
|
282
|
+
const filteredWarnings = clientIds.flatMap((id) => warningsForClient(warnings, id));
|
|
319
283
|
spinner?.succeed("Analysis complete");
|
|
320
284
|
if (args.json) {
|
|
321
285
|
printJson({ scores, warnings: filteredWarnings });
|
|
@@ -324,7 +288,11 @@ var analyze_default = defineCommand({
|
|
|
324
288
|
printWarnings(filteredWarnings, { quiet: args.quiet });
|
|
325
289
|
}
|
|
326
290
|
} catch (err) {
|
|
327
|
-
|
|
291
|
+
if (err instanceof CompileError) {
|
|
292
|
+
spinner?.fail(`Compilation failed (${err.phase}): ${err.message}`);
|
|
293
|
+
} else {
|
|
294
|
+
spinner?.fail(err.message);
|
|
295
|
+
}
|
|
328
296
|
process.exit(1);
|
|
329
297
|
}
|
|
330
298
|
}
|
|
@@ -337,8 +305,11 @@ import {
|
|
|
337
305
|
transformForAllClients,
|
|
338
306
|
analyzeEmail as analyzeEmail2,
|
|
339
307
|
generateCompatibilityScore as generateCompatibilityScore2,
|
|
340
|
-
simulateDarkMode
|
|
308
|
+
simulateDarkMode,
|
|
309
|
+
warningsForClient as warningsForClient2,
|
|
310
|
+
CompileError as CompileError2
|
|
341
311
|
} from "@emailens/engine";
|
|
312
|
+
import { compile as compile2 } from "@emailens/engine/compile";
|
|
342
313
|
var preview_default = defineCommand2({
|
|
343
314
|
meta: {
|
|
344
315
|
name: "preview",
|
|
@@ -391,7 +362,7 @@ var preview_default = defineCommand2({
|
|
|
391
362
|
spinner?.start("Reading input...");
|
|
392
363
|
const source = await readInput(args.input);
|
|
393
364
|
spinner?.succeed("Input read");
|
|
394
|
-
const html = await
|
|
365
|
+
const html = await compile2(source, format, args.input !== "-" ? args.input : void 0);
|
|
395
366
|
const clientIds = resolveClients(args.clients);
|
|
396
367
|
const clientSet = new Set(clientIds);
|
|
397
368
|
spinner?.start("Transforming for all clients...");
|
|
@@ -408,7 +379,7 @@ var preview_default = defineCommand2({
|
|
|
408
379
|
scores[id] = data;
|
|
409
380
|
}
|
|
410
381
|
}
|
|
411
|
-
const filteredWarnings =
|
|
382
|
+
const filteredWarnings = clientIds.flatMap((id) => warningsForClient2(warnings, id));
|
|
412
383
|
spinner?.succeed("Analysis complete");
|
|
413
384
|
let darkMode;
|
|
414
385
|
if (args["dark-mode"]) {
|
|
@@ -443,7 +414,11 @@ var preview_default = defineCommand2({
|
|
|
443
414
|
printWarnings(filteredWarnings, { quiet: args.quiet });
|
|
444
415
|
}
|
|
445
416
|
} catch (err) {
|
|
446
|
-
|
|
417
|
+
if (err instanceof CompileError2) {
|
|
418
|
+
spinner?.fail(`Compilation failed (${err.phase}): ${err.message}`);
|
|
419
|
+
} else {
|
|
420
|
+
spinner?.fail(err.message);
|
|
421
|
+
}
|
|
447
422
|
process.exit(1);
|
|
448
423
|
}
|
|
449
424
|
}
|
|
@@ -515,8 +490,11 @@ import {
|
|
|
515
490
|
transformForAllClients as transformForAllClients2,
|
|
516
491
|
analyzeEmail as analyzeEmail3,
|
|
517
492
|
generateCompatibilityScore as generateCompatibilityScore3,
|
|
518
|
-
simulateDarkMode as simulateDarkMode2
|
|
493
|
+
simulateDarkMode as simulateDarkMode2,
|
|
494
|
+
warningsForClient as warningsForClient3,
|
|
495
|
+
CompileError as CompileError3
|
|
519
496
|
} from "@emailens/engine";
|
|
497
|
+
import { compile as compile3 } from "@emailens/engine/compile";
|
|
520
498
|
|
|
521
499
|
// src/output/html-report.ts
|
|
522
500
|
import { EMAIL_CLIENTS as EMAIL_CLIENTS4 } from "@emailens/engine";
|
|
@@ -641,7 +619,7 @@ var export_default = defineCommand4({
|
|
|
641
619
|
spinner?.start("Reading input...");
|
|
642
620
|
const source = await readInput(args.input);
|
|
643
621
|
spinner?.succeed("Input read");
|
|
644
|
-
const html = await
|
|
622
|
+
const html = await compile3(source, format, args.input !== "-" ? args.input : void 0);
|
|
645
623
|
const clientIds = resolveClients(args.clients);
|
|
646
624
|
const clientSet = new Set(clientIds);
|
|
647
625
|
spinner?.start("Transforming for all clients...");
|
|
@@ -658,7 +636,7 @@ var export_default = defineCommand4({
|
|
|
658
636
|
scores[id] = data;
|
|
659
637
|
}
|
|
660
638
|
}
|
|
661
|
-
const filteredWarnings =
|
|
639
|
+
const filteredWarnings = clientIds.flatMap((id) => warningsForClient3(warnings, id));
|
|
662
640
|
spinner?.succeed("Analysis complete");
|
|
663
641
|
let darkMode;
|
|
664
642
|
if (args["dark-mode"]) {
|
|
@@ -712,24 +690,341 @@ var export_default = defineCommand4({
|
|
|
712
690
|
console.log();
|
|
713
691
|
}
|
|
714
692
|
} catch (err) {
|
|
715
|
-
|
|
693
|
+
if (err instanceof CompileError3) {
|
|
694
|
+
spinner?.fail(`Compilation failed (${err.phase}): ${err.message}`);
|
|
695
|
+
} else {
|
|
696
|
+
spinner?.fail(err.message);
|
|
697
|
+
}
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// src/commands/fix.ts
|
|
704
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
705
|
+
import ora4 from "ora";
|
|
706
|
+
import pc3 from "picocolors";
|
|
707
|
+
import {
|
|
708
|
+
analyzeEmail as analyzeEmail4,
|
|
709
|
+
generateCompatibilityScore as generateCompatibilityScore4,
|
|
710
|
+
generateAiFix,
|
|
711
|
+
estimateAiFixTokens,
|
|
712
|
+
AI_FIX_SYSTEM_PROMPT,
|
|
713
|
+
CompileError as CompileError4
|
|
714
|
+
} from "@emailens/engine";
|
|
715
|
+
import { compile as compile4 } from "@emailens/engine/compile";
|
|
716
|
+
var fix_default = defineCommand5({
|
|
717
|
+
meta: {
|
|
718
|
+
name: "fix",
|
|
719
|
+
description: "Generate AI-powered fixes for email compatibility issues"
|
|
720
|
+
},
|
|
721
|
+
args: {
|
|
722
|
+
input: {
|
|
723
|
+
type: "positional",
|
|
724
|
+
description: "HTML file path or - for stdin",
|
|
725
|
+
required: true
|
|
726
|
+
},
|
|
727
|
+
format: {
|
|
728
|
+
type: "string",
|
|
729
|
+
alias: "f",
|
|
730
|
+
description: "Input format: html, jsx, mjml, maizzle"
|
|
731
|
+
},
|
|
732
|
+
clients: {
|
|
733
|
+
type: "string",
|
|
734
|
+
alias: "c",
|
|
735
|
+
description: "Comma-separated client IDs to scope the fix"
|
|
736
|
+
},
|
|
737
|
+
output: {
|
|
738
|
+
type: "string",
|
|
739
|
+
alias: "o",
|
|
740
|
+
description: "Write fixed code to file instead of stdout"
|
|
741
|
+
},
|
|
742
|
+
json: {
|
|
743
|
+
type: "boolean",
|
|
744
|
+
description: "Output as JSON (includes token estimates and metadata)"
|
|
745
|
+
},
|
|
746
|
+
quiet: {
|
|
747
|
+
type: "boolean",
|
|
748
|
+
alias: "q",
|
|
749
|
+
description: "Suppress spinners and decorations"
|
|
750
|
+
},
|
|
751
|
+
estimate: {
|
|
752
|
+
type: "boolean",
|
|
753
|
+
description: "Only show token estimate without calling the AI"
|
|
754
|
+
},
|
|
755
|
+
"max-tokens": {
|
|
756
|
+
type: "string",
|
|
757
|
+
description: "Maximum input tokens for the prompt (default: 16000)"
|
|
758
|
+
}
|
|
759
|
+
},
|
|
760
|
+
async run({ args }) {
|
|
761
|
+
const spinner = args.quiet || args.json ? null : ora4();
|
|
762
|
+
try {
|
|
763
|
+
const format = resolveFormat(args.format, args.input);
|
|
764
|
+
const framework = toFramework(format);
|
|
765
|
+
spinner?.start("Reading input...");
|
|
766
|
+
const source = await readInput(args.input);
|
|
767
|
+
const html = await compile4(source, format, args.input !== "-" ? args.input : void 0);
|
|
768
|
+
spinner?.succeed("Input read");
|
|
769
|
+
spinner?.start("Analyzing compatibility...");
|
|
770
|
+
const warnings = analyzeEmail4(html, framework);
|
|
771
|
+
const scores = generateCompatibilityScore4(warnings);
|
|
772
|
+
spinner?.succeed(`Found ${warnings.length} warnings`);
|
|
773
|
+
if (warnings.length === 0) {
|
|
774
|
+
if (args.json) {
|
|
775
|
+
console.log(JSON.stringify({ message: "No issues found", code: source }, null, 2));
|
|
776
|
+
} else {
|
|
777
|
+
console.log(pc3.green("\n No compatibility issues found \u2014 nothing to fix.\n"));
|
|
778
|
+
}
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const clientIds = resolveClients(args.clients);
|
|
782
|
+
let scope;
|
|
783
|
+
let selectedClientId;
|
|
784
|
+
let scopedWarnings = warnings;
|
|
785
|
+
let scopedScores = scores;
|
|
786
|
+
if (args.clients && clientIds.length === 1) {
|
|
787
|
+
scope = "current";
|
|
788
|
+
selectedClientId = clientIds[0];
|
|
789
|
+
} else if (args.clients && clientIds.length > 1) {
|
|
790
|
+
const clientSet = new Set(clientIds);
|
|
791
|
+
scope = "all";
|
|
792
|
+
selectedClientId = void 0;
|
|
793
|
+
scopedWarnings = warnings.filter((w) => clientSet.has(w.client));
|
|
794
|
+
scopedScores = Object.fromEntries(
|
|
795
|
+
Object.entries(scores).filter(([id]) => clientSet.has(id))
|
|
796
|
+
);
|
|
797
|
+
} else {
|
|
798
|
+
scope = "all";
|
|
799
|
+
selectedClientId = void 0;
|
|
800
|
+
}
|
|
801
|
+
const maxInputTokens = args["max-tokens"] ? parseInt(args["max-tokens"], 10) : 16e3;
|
|
802
|
+
const estimate = await estimateAiFixTokens({
|
|
803
|
+
originalHtml: html,
|
|
804
|
+
warnings: scopedWarnings,
|
|
805
|
+
scores: scopedScores,
|
|
806
|
+
scope,
|
|
807
|
+
selectedClientId,
|
|
808
|
+
format,
|
|
809
|
+
maxInputTokens
|
|
810
|
+
});
|
|
811
|
+
const structuralPct = estimate.warningCount > 0 ? Math.round(estimate.structuralCount / estimate.warningCount * 100) : 0;
|
|
812
|
+
if (args.estimate) {
|
|
813
|
+
if (args.json) {
|
|
814
|
+
console.log(JSON.stringify(estimate, null, 2));
|
|
815
|
+
} else {
|
|
816
|
+
console.log(`
|
|
817
|
+
${pc3.bold("Token Estimate")}`);
|
|
818
|
+
console.log(` Input tokens: ~${estimate.inputTokens.toLocaleString()}`);
|
|
819
|
+
console.log(` Output tokens: ~${estimate.estimatedOutputTokens.toLocaleString()}`);
|
|
820
|
+
console.log(` Warnings: ${estimate.warningCount} (${estimate.structuralCount} structural)`);
|
|
821
|
+
if (estimate.truncated) {
|
|
822
|
+
console.log(` ${pc3.yellow(`Truncated: ${estimate.warningsRemoved} low-priority warnings removed`)}`);
|
|
823
|
+
}
|
|
824
|
+
console.log();
|
|
825
|
+
}
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
829
|
+
if (!apiKey) {
|
|
830
|
+
spinner?.fail("ANTHROPIC_API_KEY environment variable is required");
|
|
831
|
+
console.error(pc3.dim("\n Set it with: export ANTHROPIC_API_KEY=sk-ant-...\n"));
|
|
832
|
+
process.exit(1);
|
|
833
|
+
}
|
|
834
|
+
if (!args.quiet && !args.json) {
|
|
835
|
+
console.log(pc3.dim(` ~${estimate.inputTokens.toLocaleString()} input tokens, ${estimate.warningCount} warnings (${structuralPct}% structural)`));
|
|
836
|
+
if (estimate.truncated) {
|
|
837
|
+
console.log(pc3.yellow(` Truncated ${estimate.warningsRemoved} low-priority warnings to fit token limit`));
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
spinner?.start("Generating AI fix...");
|
|
841
|
+
let Anthropic;
|
|
842
|
+
try {
|
|
843
|
+
Anthropic = (await import("./sdk-HQZAL655.js")).default;
|
|
844
|
+
} catch {
|
|
845
|
+
spinner?.fail("@anthropic-ai/sdk is required for the fix command");
|
|
846
|
+
console.error(pc3.dim("\n Install it with: npm install @anthropic-ai/sdk\n"));
|
|
847
|
+
process.exit(1);
|
|
848
|
+
}
|
|
849
|
+
const anthropic = new Anthropic({ apiKey });
|
|
850
|
+
const provider = async (prompt) => {
|
|
851
|
+
const msg = await anthropic.messages.create({
|
|
852
|
+
model: "claude-sonnet-4-6",
|
|
853
|
+
max_tokens: 8192,
|
|
854
|
+
system: AI_FIX_SYSTEM_PROMPT,
|
|
855
|
+
messages: [{ role: "user", content: prompt }]
|
|
856
|
+
});
|
|
857
|
+
const textBlock = msg.content.find((b) => b.type === "text");
|
|
858
|
+
return textBlock?.type === "text" ? textBlock.text : "";
|
|
859
|
+
};
|
|
860
|
+
const result = await generateAiFix({
|
|
861
|
+
originalHtml: html,
|
|
862
|
+
warnings: scopedWarnings,
|
|
863
|
+
scores: scopedScores,
|
|
864
|
+
scope,
|
|
865
|
+
selectedClientId,
|
|
866
|
+
format,
|
|
867
|
+
maxInputTokens,
|
|
868
|
+
provider
|
|
869
|
+
});
|
|
870
|
+
spinner?.succeed(`Fixed ${result.targetedWarnings} warnings (${result.structuralCount} structural)`);
|
|
871
|
+
if (args.output) {
|
|
872
|
+
const { writeFileSync: writeFileSync2 } = await import("fs");
|
|
873
|
+
writeFileSync2(args.output, result.code, "utf-8");
|
|
874
|
+
if (!args.quiet) {
|
|
875
|
+
console.log(pc3.green(`
|
|
876
|
+
Fixed code written to ${args.output}
|
|
877
|
+
`));
|
|
878
|
+
}
|
|
879
|
+
} else if (args.json) {
|
|
880
|
+
console.log(JSON.stringify({
|
|
881
|
+
code: result.code,
|
|
882
|
+
targetedWarnings: result.targetedWarnings,
|
|
883
|
+
structuralCount: result.structuralCount,
|
|
884
|
+
tokenEstimate: result.tokenEstimate
|
|
885
|
+
}, null, 2));
|
|
886
|
+
} else {
|
|
887
|
+
console.log("\n" + result.code);
|
|
888
|
+
}
|
|
889
|
+
} catch (err) {
|
|
890
|
+
if (err instanceof CompileError4) {
|
|
891
|
+
spinner?.fail(`Compilation failed (${err.phase}): ${err.message}`);
|
|
892
|
+
} else {
|
|
893
|
+
spinner?.fail(err.message);
|
|
894
|
+
}
|
|
716
895
|
process.exit(1);
|
|
717
896
|
}
|
|
718
897
|
}
|
|
719
898
|
});
|
|
720
899
|
|
|
900
|
+
// src/commands/audit.ts
|
|
901
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
902
|
+
import ora5 from "ora";
|
|
903
|
+
import pc4 from "picocolors";
|
|
904
|
+
import Table3 from "cli-table3";
|
|
905
|
+
import {
|
|
906
|
+
auditEmail,
|
|
907
|
+
CompileError as CompileError5
|
|
908
|
+
} from "@emailens/engine";
|
|
909
|
+
import { compile as compile5 } from "@emailens/engine/compile";
|
|
910
|
+
var VALID_SKIPS = /* @__PURE__ */ new Set(["spam", "links", "accessibility", "images", "compatibility"]);
|
|
911
|
+
var audit_default = defineCommand6({
|
|
912
|
+
meta: {
|
|
913
|
+
name: "audit",
|
|
914
|
+
description: "Full email audit: compatibility + spam + links + accessibility + images"
|
|
915
|
+
},
|
|
916
|
+
args: {
|
|
917
|
+
input: {
|
|
918
|
+
type: "positional",
|
|
919
|
+
description: "HTML file path or - for stdin",
|
|
920
|
+
required: true
|
|
921
|
+
},
|
|
922
|
+
format: {
|
|
923
|
+
type: "string",
|
|
924
|
+
alias: "f",
|
|
925
|
+
description: "Input format: html, jsx, mjml, maizzle"
|
|
926
|
+
},
|
|
927
|
+
json: {
|
|
928
|
+
type: "boolean",
|
|
929
|
+
description: "Output as JSON"
|
|
930
|
+
},
|
|
931
|
+
quiet: {
|
|
932
|
+
type: "boolean",
|
|
933
|
+
alias: "q",
|
|
934
|
+
description: "Suppress spinners and decorations"
|
|
935
|
+
},
|
|
936
|
+
skip: {
|
|
937
|
+
type: "string",
|
|
938
|
+
description: "Comma-separated checks to skip: spam,links,accessibility,images,compatibility"
|
|
939
|
+
}
|
|
940
|
+
},
|
|
941
|
+
async run({ args }) {
|
|
942
|
+
const spinner = args.quiet || args.json ? null : ora5();
|
|
943
|
+
try {
|
|
944
|
+
const format = resolveFormat(args.format, args.input);
|
|
945
|
+
spinner?.start("Reading input...");
|
|
946
|
+
const source = await readInput(args.input);
|
|
947
|
+
spinner?.succeed("Input read");
|
|
948
|
+
const html = await compile5(source, format, args.input !== "-" ? args.input : void 0);
|
|
949
|
+
const skip = [];
|
|
950
|
+
if (args.skip) {
|
|
951
|
+
for (const s of args.skip.split(",").map((s2) => s2.trim()).filter(Boolean)) {
|
|
952
|
+
if (!VALID_SKIPS.has(s)) {
|
|
953
|
+
throw new Error(`Unknown skip value "${s}". Valid: ${Array.from(VALID_SKIPS).join(", ")}`);
|
|
954
|
+
}
|
|
955
|
+
skip.push(s);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
spinner?.start("Running full audit...");
|
|
959
|
+
const framework = toFramework(format);
|
|
960
|
+
const report = auditEmail(html, { framework, skip });
|
|
961
|
+
spinner?.succeed("Audit complete");
|
|
962
|
+
if (args.json) {
|
|
963
|
+
console.log(JSON.stringify(report, null, 2));
|
|
964
|
+
} else {
|
|
965
|
+
if (!skip.includes("compatibility")) {
|
|
966
|
+
printScoreTable(report.compatibility.scores, report.compatibility.warnings, { quiet: args.quiet });
|
|
967
|
+
printWarnings(report.compatibility.warnings, { quiet: args.quiet });
|
|
968
|
+
}
|
|
969
|
+
printQualitySummary(report, skip, { quiet: args.quiet });
|
|
970
|
+
}
|
|
971
|
+
} catch (err) {
|
|
972
|
+
if (err instanceof CompileError5) {
|
|
973
|
+
spinner?.fail(`Compilation failed (${err.phase}): ${err.message}`);
|
|
974
|
+
} else {
|
|
975
|
+
spinner?.fail(err.message);
|
|
976
|
+
}
|
|
977
|
+
process.exit(1);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
function printQualitySummary(report, skip, options) {
|
|
982
|
+
if (!options?.quiet) {
|
|
983
|
+
console.log(pc4.bold(" Quality Summary"));
|
|
984
|
+
console.log();
|
|
985
|
+
}
|
|
986
|
+
const table = new Table3({
|
|
987
|
+
head: [pc4.bold("Check"), pc4.bold("Result")],
|
|
988
|
+
style: { head: [], border: [] }
|
|
989
|
+
});
|
|
990
|
+
if (!skip.includes("spam")) {
|
|
991
|
+
const { score, level, issues } = report.spam;
|
|
992
|
+
const color = score >= 90 ? pc4.green : score >= 70 ? pc4.yellow : pc4.red;
|
|
993
|
+
table.push(["Spam Score", `${color(String(score))}/100 (${level}) \u2014 ${issues.length} issue${issues.length === 1 ? "" : "s"}`]);
|
|
994
|
+
}
|
|
995
|
+
if (!skip.includes("links")) {
|
|
996
|
+
const { totalLinks, issues } = report.links;
|
|
997
|
+
const color = issues.length === 0 ? pc4.green : pc4.yellow;
|
|
998
|
+
table.push(["Links", `${totalLinks} link${totalLinks === 1 ? "" : "s"} \u2014 ${color(`${issues.length} issue${issues.length === 1 ? "" : "s"}`)}`]);
|
|
999
|
+
}
|
|
1000
|
+
if (!skip.includes("accessibility")) {
|
|
1001
|
+
const { score, issues } = report.accessibility;
|
|
1002
|
+
const color = score >= 90 ? pc4.green : score >= 70 ? pc4.yellow : pc4.red;
|
|
1003
|
+
table.push(["Accessibility", `${color(String(score))}/100 \u2014 ${issues.length} issue${issues.length === 1 ? "" : "s"}`]);
|
|
1004
|
+
}
|
|
1005
|
+
if (!skip.includes("images")) {
|
|
1006
|
+
const { total, issues } = report.images;
|
|
1007
|
+
const color = issues.length === 0 ? pc4.green : pc4.yellow;
|
|
1008
|
+
table.push(["Images", `${total} image${total === 1 ? "" : "s"} \u2014 ${color(`${issues.length} issue${issues.length === 1 ? "" : "s"}`)}`]);
|
|
1009
|
+
}
|
|
1010
|
+
console.log(table.toString());
|
|
1011
|
+
console.log();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
721
1014
|
// src/index.ts
|
|
722
|
-
var main =
|
|
1015
|
+
var main = defineCommand7({
|
|
723
1016
|
meta: {
|
|
724
1017
|
name: "emailens",
|
|
725
|
-
version: "0.
|
|
1018
|
+
version: "0.3.0",
|
|
726
1019
|
description: "Email compatibility analysis CLI \u2014 preview how HTML emails render across 12 email clients."
|
|
727
1020
|
},
|
|
728
1021
|
subCommands: {
|
|
729
1022
|
analyze: analyze_default,
|
|
730
1023
|
preview: preview_default,
|
|
731
1024
|
clients: clients_default,
|
|
732
|
-
export: export_default
|
|
1025
|
+
export: export_default,
|
|
1026
|
+
fix: fix_default,
|
|
1027
|
+
audit: audit_default
|
|
733
1028
|
}
|
|
734
1029
|
});
|
|
735
1030
|
runMain(main);
|