@mailmodo/cli 0.0.20 → 0.0.21-beta.pr24.38
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/commands/deploy/index.d.ts +5 -28
- package/dist/commands/deploy/index.js +75 -55
- package/dist/commands/domain/index.js +12 -7
- package/dist/commands/edit/index.d.ts +10 -0
- package/dist/commands/edit/index.js +129 -9
- package/dist/commands/init/index.js +6 -1
- package/dist/commands/preview/index.js +7 -2
- package/dist/commands/settings/index.js +1 -1
- package/dist/lib/constants.d.ts +1 -1
- package/dist/lib/constants.js +1 -1
- package/dist/lib/yaml-config.d.ts +5 -0
- package/oclif.manifest.json +33 -33
- package/package.json +1 -1
|
@@ -6,45 +6,22 @@ export default class Deploy extends BaseCommand {
|
|
|
6
6
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
7
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
8
|
};
|
|
9
|
-
/**
|
|
10
|
-
* Fetches current DNS verification status for the deploy flow.
|
|
11
|
-
*
|
|
12
|
-
* @param jsonOutput - When true, spinner uses stderr for stdout-safe JSON runs.
|
|
13
|
-
* @param domain - Sending domain to check. If empty, request will fail and trigger setup flow.
|
|
14
|
-
*/
|
|
15
9
|
private fetchDomainVerifyForDeploy;
|
|
16
10
|
run(): Promise<void>;
|
|
11
|
+
private validateSequence;
|
|
17
12
|
private buildDeployPayload;
|
|
18
13
|
private mapEmailToPayload;
|
|
14
|
+
private buildBrandSection;
|
|
15
|
+
private buildProductSection;
|
|
16
|
+
private buildSenderSection;
|
|
19
17
|
private buildProjectPayload;
|
|
20
18
|
private confirmDeploy;
|
|
21
19
|
private ensureDomainReady;
|
|
22
|
-
/**
|
|
23
|
-
* Lists emails about to be deployed (skipped when `--json` is set).
|
|
24
|
-
*
|
|
25
|
-
* @param yamlConfig - Loaded project YAML.
|
|
26
|
-
* @param jsonOutput - When true, skip human-readable output.
|
|
27
|
-
*/
|
|
28
20
|
private logPreDeploySummary;
|
|
29
|
-
|
|
30
|
-
* Prints the post-deploy success message and SDK install snippet for interactive runs.
|
|
31
|
-
*/
|
|
21
|
+
private logDiff;
|
|
32
22
|
private logDeploySuccessInstructions;
|
|
33
|
-
/**
|
|
34
|
-
* Interactive domain setup flow. Collects domain, sender email, and business
|
|
35
|
-
* address from the user, then calls the API to get DNS records to configure.
|
|
36
|
-
* Polls for verification when the user indicates they've added the records.
|
|
37
|
-
*
|
|
38
|
-
* @returns {Promise<boolean>} true if domain was verified, false if skipped.
|
|
39
|
-
*/
|
|
40
23
|
private runDomainSetup;
|
|
41
24
|
private collectDomainInputs;
|
|
42
25
|
private showDnsRecords;
|
|
43
|
-
/**
|
|
44
|
-
* Calls the domain verification API endpoint and reports pass/fail
|
|
45
|
-
* status for each DNS record (DKIM, DMARC, Return-Path).
|
|
46
|
-
*
|
|
47
|
-
* @returns {Promise<boolean>} true if all records pass.
|
|
48
|
-
*/
|
|
49
26
|
private verifyDomain;
|
|
50
27
|
}
|
|
@@ -12,12 +12,6 @@ export default class Deploy extends BaseCommand {
|
|
|
12
12
|
static flags = {
|
|
13
13
|
...BaseCommand.baseFlags,
|
|
14
14
|
};
|
|
15
|
-
/**
|
|
16
|
-
* Fetches current DNS verification status for the deploy flow.
|
|
17
|
-
*
|
|
18
|
-
* @param jsonOutput - When true, spinner uses stderr for stdout-safe JSON runs.
|
|
19
|
-
* @param domain - Sending domain to check. If empty, request will fail and trigger setup flow.
|
|
20
|
-
*/
|
|
21
15
|
fetchDomainVerifyForDeploy(jsonOutput, domain) {
|
|
22
16
|
return this.withApiSpinner({ json: jsonOutput, text: ' Checking domain verification...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY, {
|
|
23
17
|
domain: domain || '',
|
|
@@ -30,11 +24,12 @@ export default class Deploy extends BaseCommand {
|
|
|
30
24
|
const domainReady = await this.ensureDomainReady(yamlConfig, flags);
|
|
31
25
|
if (!domainReady)
|
|
32
26
|
return;
|
|
33
|
-
this.
|
|
27
|
+
const payload = await this.buildDeployPayload(yamlConfig);
|
|
28
|
+
const validateResult = await this.validateSequence(payload, flags);
|
|
29
|
+
this.logPreDeploySummary(yamlConfig, validateResult, flags.json);
|
|
34
30
|
const confirmed = await this.confirmDeploy(yamlConfig, flags);
|
|
35
31
|
if (!confirmed)
|
|
36
32
|
return;
|
|
37
|
-
const payload = await this.buildDeployPayload(yamlConfig);
|
|
38
33
|
const response = await this.withApiSpinner({ json: flags.json, text: ' Deploying email sequences...' }, () => this.apiClient.post(API_ENDPOINTS.SEQUENCES_DEPLOY, payload));
|
|
39
34
|
if (!response.ok) {
|
|
40
35
|
this.handleApiError(response);
|
|
@@ -51,6 +46,19 @@ export default class Deploy extends BaseCommand {
|
|
|
51
46
|
}
|
|
52
47
|
this.logDeploySuccessInstructions(response.data.sdkSnippet);
|
|
53
48
|
}
|
|
49
|
+
async validateSequence(payload, flags) {
|
|
50
|
+
const response = await this.withApiSpinner({ json: flags.json, text: ' Validating sequence...' }, () => this.apiClient.post(API_ENDPOINTS.SEQUENCES_VALIDATE, payload));
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
if (response.data.error === 'senderDomainNotFound') {
|
|
53
|
+
this.error(`Sending domain not registered. Run: ${chalk.cyan('mailmodo domain')}`);
|
|
54
|
+
}
|
|
55
|
+
if (response.data.error === 'senderDomainNotVerified') {
|
|
56
|
+
this.error(`Sending domain not verified. Run: ${chalk.cyan('mailmodo domain --verify')}`);
|
|
57
|
+
}
|
|
58
|
+
this.handleApiError(response);
|
|
59
|
+
}
|
|
60
|
+
return response.data;
|
|
61
|
+
}
|
|
54
62
|
async buildDeployPayload(yamlConfig) {
|
|
55
63
|
const emailsWithHtml = await Promise.all(yamlConfig.emails.map(async (email) => {
|
|
56
64
|
const html = (await loadTemplate(`${email.id}.html`)) || '';
|
|
@@ -64,7 +72,7 @@ export default class Deploy extends BaseCommand {
|
|
|
64
72
|
mapEmailToPayload(email) {
|
|
65
73
|
return {
|
|
66
74
|
condition: email.condition || null,
|
|
67
|
-
ctaText: '',
|
|
75
|
+
ctaText: email.ctaText || '',
|
|
68
76
|
delay: typeof email.delay === 'string'
|
|
69
77
|
? Number.parseInt(email.delay, 10) || 0
|
|
70
78
|
: email.delay,
|
|
@@ -77,30 +85,39 @@ export default class Deploy extends BaseCommand {
|
|
|
77
85
|
trigger: email.trigger,
|
|
78
86
|
};
|
|
79
87
|
}
|
|
88
|
+
buildBrandSection(project) {
|
|
89
|
+
return {
|
|
90
|
+
colors: [project?.brandColor || DEFAULT_BRAND_COLOR],
|
|
91
|
+
logoUrl: project?.logoUrl || '',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
buildProductSection(project) {
|
|
95
|
+
return {
|
|
96
|
+
businessType: project?.type || '',
|
|
97
|
+
description: project?.description || '',
|
|
98
|
+
pricingModel: project?.pricingModel || '',
|
|
99
|
+
productName: project?.name || '',
|
|
100
|
+
saasModel: project?.saasModel || '',
|
|
101
|
+
targetUser: project?.targetUser || '',
|
|
102
|
+
url: project?.url || '',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
buildSenderSection(project) {
|
|
106
|
+
return {
|
|
107
|
+
address: project?.address || '',
|
|
108
|
+
domain: project?.domain || '',
|
|
109
|
+
fromEmail: project?.fromEmail || '',
|
|
110
|
+
fromName: project?.fromName || '',
|
|
111
|
+
replyTo: project?.replyTo || project?.fromEmail || '',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
80
114
|
buildProjectPayload(project) {
|
|
81
115
|
return {
|
|
82
|
-
brand:
|
|
83
|
-
colors: [project?.brandColor || DEFAULT_BRAND_COLOR],
|
|
84
|
-
logoUrl: project?.logoUrl || '',
|
|
85
|
-
},
|
|
116
|
+
brand: this.buildBrandSection(project),
|
|
86
117
|
emailStyle: project?.emailStyle || 'branded',
|
|
87
118
|
monthlyCap: project?.monthlyCap ?? DEFAULT_MONTHLY_CAP,
|
|
88
|
-
product:
|
|
89
|
-
|
|
90
|
-
description: '',
|
|
91
|
-
pricingModel: '',
|
|
92
|
-
productName: project?.name || '',
|
|
93
|
-
saasModel: '',
|
|
94
|
-
targetUser: '',
|
|
95
|
-
url: project?.url || '',
|
|
96
|
-
},
|
|
97
|
-
senderDetails: {
|
|
98
|
-
address: project?.address || '',
|
|
99
|
-
domain: project?.domain || '',
|
|
100
|
-
fromEmail: project?.fromEmail || '',
|
|
101
|
-
fromName: project?.fromName || '',
|
|
102
|
-
replyTo: project?.replyTo || project?.fromEmail || '',
|
|
103
|
-
},
|
|
119
|
+
product: this.buildProductSection(project),
|
|
120
|
+
senderDetails: this.buildSenderSection(project),
|
|
104
121
|
webhookUrl: project?.webhookUrl || '',
|
|
105
122
|
};
|
|
106
123
|
}
|
|
@@ -141,25 +158,41 @@ export default class Deploy extends BaseCommand {
|
|
|
141
158
|
}
|
|
142
159
|
return this.runDomainSetup(yamlConfig, flags);
|
|
143
160
|
}
|
|
144
|
-
|
|
145
|
-
* Lists emails about to be deployed (skipped when `--json` is set).
|
|
146
|
-
*
|
|
147
|
-
* @param yamlConfig - Loaded project YAML.
|
|
148
|
-
* @param jsonOutput - When true, skip human-readable output.
|
|
149
|
-
*/
|
|
150
|
-
logPreDeploySummary(yamlConfig, jsonOutput) {
|
|
161
|
+
logPreDeploySummary(yamlConfig, validateResult, jsonOutput) {
|
|
151
162
|
if (jsonOutput)
|
|
152
163
|
return;
|
|
153
164
|
this.log(`\n ${chalk.green('✓')} Domain: ${yamlConfig.project?.domain || 'verified'}\n`);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
165
|
+
if (!validateResult.existingDeployment || !validateResult.diff) {
|
|
166
|
+
this.log(` Deploying:`);
|
|
167
|
+
for (const email of yamlConfig.emails) {
|
|
168
|
+
this.log(` ${chalk.green('+')} ${email.id.padEnd(24)} ${email.trigger}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
this.logDiff(validateResult.diff);
|
|
157
173
|
}
|
|
158
174
|
this.log('');
|
|
159
175
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
176
|
+
logDiff(diff) {
|
|
177
|
+
if (!diff.hasChanges) {
|
|
178
|
+
this.log(` No changes from last deployment.`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.log(` Changes vs. last deployment:`);
|
|
182
|
+
for (const email of diff.added) {
|
|
183
|
+
this.log(` ${chalk.green('+')} ${email.id.padEnd(24)} ${email.trigger || ''}`);
|
|
184
|
+
}
|
|
185
|
+
for (const email of diff.removed) {
|
|
186
|
+
this.log(` ${chalk.red('-')} ${email.id.padEnd(24)} ${email.trigger || ''}`);
|
|
187
|
+
}
|
|
188
|
+
for (const email of diff.modified) {
|
|
189
|
+
const fields = email.changedFields?.join(', ') || '';
|
|
190
|
+
this.log(` ${chalk.yellow('~')} ${email.id.padEnd(24)} ${fields}`);
|
|
191
|
+
}
|
|
192
|
+
if (diff.unchanged.length > 0) {
|
|
193
|
+
this.log(` ${chalk.dim(`∙ ${diff.unchanged.length} unchanged`)}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
163
196
|
logDeploySuccessInstructions(sdkSnippet) {
|
|
164
197
|
this.log(` ${chalk.green('Deployed.')} Emails are live.\n`);
|
|
165
198
|
this.log(` ${'─'.repeat(53)}`);
|
|
@@ -176,13 +209,6 @@ export default class Deploy extends BaseCommand {
|
|
|
176
209
|
this.log(` Full SDK docs: ${chalk.cyan('mailmodo.com/docs/sdk')}\n`);
|
|
177
210
|
this.log(` ${'─'.repeat(53)}\n`);
|
|
178
211
|
}
|
|
179
|
-
/**
|
|
180
|
-
* Interactive domain setup flow. Collects domain, sender email, and business
|
|
181
|
-
* address from the user, then calls the API to get DNS records to configure.
|
|
182
|
-
* Polls for verification when the user indicates they've added the records.
|
|
183
|
-
*
|
|
184
|
-
* @returns {Promise<boolean>} true if domain was verified, false if skipped.
|
|
185
|
-
*/
|
|
186
212
|
async runDomainSetup(yamlConfig, flags) {
|
|
187
213
|
const { address, domain, senderEmail } = await this.collectDomainInputs(yamlConfig, flags);
|
|
188
214
|
const domainResponse = await this.withApiSpinner({ json: flags.json, text: ' Configuring domain...' }, () => this.apiClient.post(API_ENDPOINTS.DOMAIN, {
|
|
@@ -251,12 +277,6 @@ export default class Deploy extends BaseCommand {
|
|
|
251
277
|
this.log(` DNS changes take 5–30 minutes to propagate.`);
|
|
252
278
|
this.log(` Full guide: ${chalk.cyan(DNS_GUIDE_URL)}\n`);
|
|
253
279
|
}
|
|
254
|
-
/**
|
|
255
|
-
* Calls the domain verification API endpoint and reports pass/fail
|
|
256
|
-
* status for each DNS record (DKIM, DMARC, Return-Path).
|
|
257
|
-
*
|
|
258
|
-
* @returns {Promise<boolean>} true if all records pass.
|
|
259
|
-
*/
|
|
260
280
|
async verifyDomain(jsonOutput, domain) {
|
|
261
281
|
const verify = await this.withApiSpinner({ json: jsonOutput, text: ' Checking DNS...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY, {
|
|
262
282
|
domain,
|
|
@@ -124,15 +124,15 @@ export default class Domain extends BaseCommand {
|
|
|
124
124
|
if (!response.ok) {
|
|
125
125
|
this.handleApiError(response);
|
|
126
126
|
}
|
|
127
|
-
const { dkim, dmarc,
|
|
127
|
+
const { dkim, dmarc, returnPath, domainStatus } = response.data;
|
|
128
128
|
if (jsonOutput) {
|
|
129
|
-
this.log(JSON.stringify({ dkim, dmarc,
|
|
129
|
+
this.log(JSON.stringify({ dkim, dmarc, returnPath, domainStatus }, null, 2));
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
132
|
-
this.log(`
|
|
133
|
-
this.log(`
|
|
134
|
-
this.log(`
|
|
135
|
-
const allPassed =
|
|
132
|
+
this.log(` DKIM ${dkim ? chalk.green('✓') : chalk.red('✗ Not found')}`);
|
|
133
|
+
this.log(` DMARC ${dmarc ? chalk.green('✓') : chalk.red('✗ Not found')}`);
|
|
134
|
+
this.log(` Return Path ${returnPath ? chalk.green('✓') : chalk.red('✗ Not found')}`);
|
|
135
|
+
const allPassed = domainStatus === 'VERIFIED';
|
|
136
136
|
if (allPassed) {
|
|
137
137
|
this.log(`\n ${chalk.green('✓')} Domain verified.\n`);
|
|
138
138
|
}
|
|
@@ -144,6 +144,11 @@ export default class Domain extends BaseCommand {
|
|
|
144
144
|
this.log(` - Including the full domain in the Host field`);
|
|
145
145
|
this.log(` - Cloudflare: proxy must be OFF (grey cloud, not orange)`);
|
|
146
146
|
}
|
|
147
|
+
if (!returnPath) {
|
|
148
|
+
this.log(`\n Return Path common mistakes:`);
|
|
149
|
+
this.log(` - Missing or incorrect CNAME for mm-bounce subdomain`);
|
|
150
|
+
this.log(` - Cloudflare: proxy must be OFF (grey cloud, not orange)`);
|
|
151
|
+
}
|
|
147
152
|
this.log(`\n Fix the records and run ${chalk.cyan('mailmodo domain --verify')} again.`);
|
|
148
153
|
this.log(` Help: ${chalk.cyan(DNS_GUIDE_URL)}\n`);
|
|
149
154
|
}
|
|
@@ -174,7 +179,7 @@ export default class Domain extends BaseCommand {
|
|
|
174
179
|
this.log(` Spam rate: ${data.spamRate ?? 'N/A'}%\n`);
|
|
175
180
|
}
|
|
176
181
|
recordLabel(index) {
|
|
177
|
-
const labels = ['
|
|
182
|
+
const labels = ['DKIM', 'DMARC', 'Return Path'];
|
|
178
183
|
return labels[index] || `Record ${index + 1}`;
|
|
179
184
|
}
|
|
180
185
|
}
|
|
@@ -10,5 +10,15 @@ export default class Edit extends BaseCommand {
|
|
|
10
10
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
12
|
};
|
|
13
|
+
private showFieldDiff;
|
|
14
|
+
private stripHtml;
|
|
15
|
+
private truncate;
|
|
16
|
+
private showHtmlChange;
|
|
17
|
+
private showUnchangedField;
|
|
18
|
+
private showUnchangedHtml;
|
|
19
|
+
private showSuggestedChanges;
|
|
20
|
+
private showUnchanged;
|
|
21
|
+
private buildDiffPreview;
|
|
22
|
+
private showChangeSummary;
|
|
13
23
|
run(): Promise<void>;
|
|
14
24
|
}
|
|
@@ -22,6 +22,123 @@ export default class Edit extends BaseCommand {
|
|
|
22
22
|
description: 'Natural language description of the change',
|
|
23
23
|
}),
|
|
24
24
|
};
|
|
25
|
+
showFieldDiff(label, oldVal, newVal) {
|
|
26
|
+
if (!newVal || oldVal === newVal)
|
|
27
|
+
return false;
|
|
28
|
+
this.log(`\n ${label}:`);
|
|
29
|
+
if (oldVal)
|
|
30
|
+
this.log(` ${chalk.red(`- ${oldVal}`)}`);
|
|
31
|
+
this.log(` ${chalk.green(`+ ${newVal}`)}`);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
stripHtml(html) {
|
|
35
|
+
return html
|
|
36
|
+
.replaceAll(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
37
|
+
.replaceAll(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
38
|
+
.replaceAll(/<[^>]+>/g, ' ')
|
|
39
|
+
.replaceAll(' ', ' ')
|
|
40
|
+
.replaceAll('&', '&')
|
|
41
|
+
.replaceAll('<', '<')
|
|
42
|
+
.replaceAll('>', '>')
|
|
43
|
+
.replaceAll(/\s+/g, ' ')
|
|
44
|
+
.trim();
|
|
45
|
+
}
|
|
46
|
+
truncate(text, max) {
|
|
47
|
+
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
48
|
+
}
|
|
49
|
+
showHtmlChange(oldHtml, newHtml) {
|
|
50
|
+
if (!newHtml || oldHtml === newHtml)
|
|
51
|
+
return false;
|
|
52
|
+
this.log(`\n HTML Body:`);
|
|
53
|
+
const MAX = 500;
|
|
54
|
+
if (oldHtml) {
|
|
55
|
+
const oldText = this.truncate(this.stripHtml(oldHtml), MAX);
|
|
56
|
+
this.log(` ${chalk.red(`- ${oldText}`)}`);
|
|
57
|
+
}
|
|
58
|
+
const newText = this.truncate(this.stripHtml(newHtml), MAX);
|
|
59
|
+
this.log(` ${chalk.green(`+ ${newText}`)}`);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
showUnchangedField(label, value) {
|
|
63
|
+
if (!value)
|
|
64
|
+
return;
|
|
65
|
+
this.log(`\n ${label}:`);
|
|
66
|
+
this.log(` ${chalk.dim(value)}`);
|
|
67
|
+
}
|
|
68
|
+
showUnchangedHtml(templateHtml) {
|
|
69
|
+
if (!templateHtml)
|
|
70
|
+
return;
|
|
71
|
+
this.log(`\n HTML Body:`);
|
|
72
|
+
this.log(` ${chalk.dim(this.truncate(this.stripHtml(templateHtml), 500))}`);
|
|
73
|
+
}
|
|
74
|
+
showSuggestedChanges(email, updated, templateHtml, changed) {
|
|
75
|
+
this.log('\n Suggested Changes:');
|
|
76
|
+
if (changed.subject)
|
|
77
|
+
this.showFieldDiff('Subject', email.subject, updated.subject);
|
|
78
|
+
if (changed.preview)
|
|
79
|
+
this.showFieldDiff('Preview Text', email.previewText, updated.previewText);
|
|
80
|
+
if (changed.html)
|
|
81
|
+
this.showHtmlChange(templateHtml, updated.html);
|
|
82
|
+
if (changed.cta)
|
|
83
|
+
this.showFieldDiff('CTA Text', undefined, updated.ctaText);
|
|
84
|
+
if (!changed.subject && !changed.preview && !changed.html && !changed.cta) {
|
|
85
|
+
this.log(`\n ${chalk.dim('No changes detected.')}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
showUnchanged(email, templateHtml, changed) {
|
|
89
|
+
const hasContent = !changed.subject ||
|
|
90
|
+
(!changed.preview && Boolean(email.previewText)) ||
|
|
91
|
+
(!changed.html && Boolean(templateHtml));
|
|
92
|
+
if (!hasContent)
|
|
93
|
+
return;
|
|
94
|
+
this.log('\n Unchanged:');
|
|
95
|
+
if (!changed.subject)
|
|
96
|
+
this.showUnchangedField('Subject', email.subject);
|
|
97
|
+
if (!changed.preview)
|
|
98
|
+
this.showUnchangedField('Preview Text', email.previewText);
|
|
99
|
+
if (!changed.html)
|
|
100
|
+
this.showUnchangedHtml(templateHtml);
|
|
101
|
+
}
|
|
102
|
+
buildDiffPreview(email, updated, templateHtml) {
|
|
103
|
+
const subjectChanged = Boolean(updated.subject) && updated.subject !== email.subject;
|
|
104
|
+
const previewChanged = Boolean(updated.previewText) && updated.previewText !== email.previewText;
|
|
105
|
+
const htmlChanged = Boolean(updated.html) && updated.html !== templateHtml;
|
|
106
|
+
const diff = {};
|
|
107
|
+
diff.subject = subjectChanged
|
|
108
|
+
? { new: updated.subject, old: email.subject }
|
|
109
|
+
: { unchanged: true, value: email.subject };
|
|
110
|
+
if (email.previewText ?? updated.previewText) {
|
|
111
|
+
diff.previewText = previewChanged
|
|
112
|
+
? { new: updated.previewText, old: email.previewText }
|
|
113
|
+
: { unchanged: true, value: email.previewText };
|
|
114
|
+
}
|
|
115
|
+
if (templateHtml ?? updated.html) {
|
|
116
|
+
const oldText = templateHtml
|
|
117
|
+
? this.truncate(this.stripHtml(templateHtml), 500)
|
|
118
|
+
: null;
|
|
119
|
+
const newText = updated.html
|
|
120
|
+
? this.truncate(this.stripHtml(updated.html), 500)
|
|
121
|
+
: null;
|
|
122
|
+
diff.html = htmlChanged
|
|
123
|
+
? { new: newText, old: oldText }
|
|
124
|
+
: { unchanged: true, value: oldText };
|
|
125
|
+
}
|
|
126
|
+
if (updated.ctaText) {
|
|
127
|
+
diff.ctaText = { new: updated.ctaText };
|
|
128
|
+
}
|
|
129
|
+
return { diff };
|
|
130
|
+
}
|
|
131
|
+
showChangeSummary(email, updated, templateHtml) {
|
|
132
|
+
const changed = {
|
|
133
|
+
cta: Boolean(updated.ctaText),
|
|
134
|
+
html: Boolean(updated.html) && updated.html !== templateHtml,
|
|
135
|
+
preview: Boolean(updated.previewText) &&
|
|
136
|
+
updated.previewText !== email.previewText,
|
|
137
|
+
subject: Boolean(updated.subject) && updated.subject !== email.subject,
|
|
138
|
+
};
|
|
139
|
+
this.showSuggestedChanges(email, updated, templateHtml, changed);
|
|
140
|
+
this.showUnchanged(email, templateHtml, changed);
|
|
141
|
+
}
|
|
25
142
|
async run() {
|
|
26
143
|
const { args, flags } = await this.parse(Edit);
|
|
27
144
|
await this.ensureAuth();
|
|
@@ -32,9 +149,6 @@ export default class Edit extends BaseCommand {
|
|
|
32
149
|
}
|
|
33
150
|
const email = yamlConfig.emails[emailIndex];
|
|
34
151
|
const templateHtml = await loadTemplate(`${email.id}.html`);
|
|
35
|
-
if (!flags.json) {
|
|
36
|
-
this.log(`\n Current subject: '${chalk.cyan(email.subject)}'`);
|
|
37
|
-
}
|
|
38
152
|
let changeDescription = flags.change;
|
|
39
153
|
if (!changeDescription) {
|
|
40
154
|
changeDescription = await input({
|
|
@@ -58,12 +172,11 @@ export default class Edit extends BaseCommand {
|
|
|
58
172
|
this.handleApiError(response);
|
|
59
173
|
}
|
|
60
174
|
const updated = response.data;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
this.
|
|
66
|
-
this.log(` ${chalk.green(`+ ${newSubject}`)}`);
|
|
175
|
+
if (flags.json) {
|
|
176
|
+
this.log(JSON.stringify(this.buildDiffPreview(email, updated, templateHtml), null, 2));
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
this.showChangeSummary(email, updated, templateHtml);
|
|
67
180
|
}
|
|
68
181
|
if (!flags.yes) {
|
|
69
182
|
const accepted = await confirm({
|
|
@@ -75,10 +188,14 @@ export default class Edit extends BaseCommand {
|
|
|
75
188
|
return;
|
|
76
189
|
}
|
|
77
190
|
}
|
|
191
|
+
const oldSubject = email.subject;
|
|
192
|
+
const newSubject = updated.subject || email.subject;
|
|
78
193
|
if (updated.subject)
|
|
79
194
|
email.subject = updated.subject;
|
|
80
195
|
if (updated.previewText)
|
|
81
196
|
email.previewText = updated.previewText;
|
|
197
|
+
if (updated.ctaText)
|
|
198
|
+
email.ctaText = updated.ctaText;
|
|
82
199
|
const updatedYaml = {
|
|
83
200
|
...yamlConfig,
|
|
84
201
|
emails: [...yamlConfig.emails],
|
|
@@ -91,6 +208,9 @@ export default class Edit extends BaseCommand {
|
|
|
91
208
|
if (flags.json) {
|
|
92
209
|
this.log(JSON.stringify({
|
|
93
210
|
diff: {
|
|
211
|
+
previewText: updated.previewText && updated.previewText !== email.previewText
|
|
212
|
+
? { new: updated.previewText, old: email.previewText }
|
|
213
|
+
: undefined,
|
|
94
214
|
subject: oldSubject === newSubject
|
|
95
215
|
? undefined
|
|
96
216
|
: { new: newSubject, old: oldSubject },
|
|
@@ -128,6 +128,7 @@ export default class Init extends BaseCommand {
|
|
|
128
128
|
...(generated?.previewText
|
|
129
129
|
? { previewText: generated.previewText }
|
|
130
130
|
: {}),
|
|
131
|
+
...(generated?.ctaText ? { ctaText: generated.ctaText } : {}),
|
|
131
132
|
goal: rec.goal,
|
|
132
133
|
};
|
|
133
134
|
});
|
|
@@ -135,14 +136,18 @@ export default class Init extends BaseCommand {
|
|
|
135
136
|
emails: emailConfigs,
|
|
136
137
|
project: {
|
|
137
138
|
brandColor: analysisPayload.brand?.color || DEFAULT_BRAND_COLOR,
|
|
139
|
+
description: analysisPayload.description,
|
|
138
140
|
emailStyle: 'branded',
|
|
139
141
|
fromEmail: '',
|
|
140
142
|
fromName: `Team ${analysisPayload.productName}`,
|
|
141
143
|
logoUrl: analysisPayload.brand?.logoUrl || '',
|
|
142
144
|
monthlyCap: DEFAULT_MONTHLY_CAP,
|
|
143
145
|
name: analysisPayload.productName,
|
|
146
|
+
pricingModel: analysisPayload.pricingModel,
|
|
144
147
|
replyTo: '',
|
|
145
|
-
|
|
148
|
+
saasModel: analysisPayload.saasModel,
|
|
149
|
+
targetUser: analysisPayload.targetUser,
|
|
150
|
+
type: analysisPayload.businessType,
|
|
146
151
|
url: productUrl,
|
|
147
152
|
webhookUrl: '',
|
|
148
153
|
},
|
|
@@ -89,7 +89,11 @@ export default class Preview extends BaseCommand {
|
|
|
89
89
|
const rendered = templateHtml
|
|
90
90
|
? renderTemplate(templateHtml, sampleData)
|
|
91
91
|
: '';
|
|
92
|
-
await this.sendTestEmail(email, rendered,
|
|
92
|
+
await this.sendTestEmail(email, rendered, {
|
|
93
|
+
domain: yamlConfig.project?.domain,
|
|
94
|
+
jsonOutput: flags.json,
|
|
95
|
+
toAddress: flags.send,
|
|
96
|
+
});
|
|
93
97
|
return;
|
|
94
98
|
}
|
|
95
99
|
if (flags.text) {
|
|
@@ -126,7 +130,8 @@ export default class Preview extends BaseCommand {
|
|
|
126
130
|
* Calls the API to send a test email to the specified address.
|
|
127
131
|
* Before domain verification, tests send via the mailmodo.com domain.
|
|
128
132
|
*/
|
|
129
|
-
async sendTestEmail(email, html,
|
|
133
|
+
async sendTestEmail(email, html, opts) {
|
|
134
|
+
const { domain, jsonOutput, toAddress } = opts;
|
|
130
135
|
await this.ensureAuth();
|
|
131
136
|
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Sending test email...' }, () => this.apiClient.post(`${API_ENDPOINTS.PREVIEW}/send`, {
|
|
132
137
|
domain,
|
|
@@ -233,7 +233,7 @@ export default class Settings extends BaseCommand {
|
|
|
233
233
|
this.log(` Help: ${chalk.cyan(DNS_GUIDE_URL)}\n`);
|
|
234
234
|
}
|
|
235
235
|
recordLabel(index) {
|
|
236
|
-
const labels = ['
|
|
236
|
+
const labels = ['DKIM', 'DMARC', 'Return Path'];
|
|
237
237
|
return labels[index] || `Record ${index + 1}`;
|
|
238
238
|
}
|
|
239
239
|
/**
|
package/dist/lib/constants.d.ts
CHANGED
package/dist/lib/constants.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export interface EmailConfig {
|
|
2
2
|
condition?: string;
|
|
3
|
+
ctaText?: string;
|
|
3
4
|
delay: number | string;
|
|
4
5
|
goal?: string;
|
|
5
6
|
id: string;
|
|
@@ -12,6 +13,7 @@ export interface EmailConfig {
|
|
|
12
13
|
export interface ProjectConfig {
|
|
13
14
|
address?: string;
|
|
14
15
|
brandColor?: string;
|
|
16
|
+
description?: string;
|
|
15
17
|
domain?: string;
|
|
16
18
|
emailStyle?: 'branded' | 'plain';
|
|
17
19
|
fromEmail?: string;
|
|
@@ -20,7 +22,10 @@ export interface ProjectConfig {
|
|
|
20
22
|
logoUrl?: string;
|
|
21
23
|
monthlyCap?: number;
|
|
22
24
|
name?: string;
|
|
25
|
+
pricingModel?: string;
|
|
23
26
|
replyTo?: string;
|
|
27
|
+
saasModel?: string;
|
|
28
|
+
targetUser?: string;
|
|
24
29
|
type?: string;
|
|
25
30
|
url?: string;
|
|
26
31
|
webhookUrl?: string;
|
package/oclif.manifest.json
CHANGED
|
@@ -342,13 +342,12 @@
|
|
|
342
342
|
"index.js"
|
|
343
343
|
]
|
|
344
344
|
},
|
|
345
|
-
"
|
|
345
|
+
"logout": {
|
|
346
346
|
"aliases": [],
|
|
347
347
|
"args": {},
|
|
348
|
-
"description": "
|
|
348
|
+
"description": "Sign out by removing saved credentials from this machine",
|
|
349
349
|
"examples": [
|
|
350
|
-
"<%= config.bin %>
|
|
351
|
-
"MAILMODO_API_KEY=mm_live_xxx <%= config.bin %> login"
|
|
350
|
+
"<%= config.bin %> logout"
|
|
352
351
|
],
|
|
353
352
|
"flags": {
|
|
354
353
|
"json": {
|
|
@@ -367,7 +366,7 @@
|
|
|
367
366
|
},
|
|
368
367
|
"hasDynamicHelp": false,
|
|
369
368
|
"hiddenAliases": [],
|
|
370
|
-
"id": "
|
|
369
|
+
"id": "logout",
|
|
371
370
|
"pluginAlias": "@mailmodo/cli",
|
|
372
371
|
"pluginName": "@mailmodo/cli",
|
|
373
372
|
"pluginType": "core",
|
|
@@ -377,16 +376,19 @@
|
|
|
377
376
|
"relativePath": [
|
|
378
377
|
"dist",
|
|
379
378
|
"commands",
|
|
380
|
-
"
|
|
379
|
+
"logout",
|
|
381
380
|
"index.js"
|
|
382
381
|
]
|
|
383
382
|
},
|
|
384
|
-
"
|
|
383
|
+
"logs": {
|
|
385
384
|
"aliases": [],
|
|
386
385
|
"args": {},
|
|
387
|
-
"description": "
|
|
386
|
+
"description": "View email send logs and delivery events",
|
|
388
387
|
"examples": [
|
|
389
|
-
"<%= config.bin %>
|
|
388
|
+
"<%= config.bin %> logs",
|
|
389
|
+
"<%= config.bin %> logs --email sarah@example.com",
|
|
390
|
+
"<%= config.bin %> logs --failed",
|
|
391
|
+
"<%= config.bin %> logs --json"
|
|
390
392
|
],
|
|
391
393
|
"flags": {
|
|
392
394
|
"json": {
|
|
@@ -401,11 +403,24 @@
|
|
|
401
403
|
"name": "yes",
|
|
402
404
|
"allowNo": false,
|
|
403
405
|
"type": "boolean"
|
|
406
|
+
},
|
|
407
|
+
"email": {
|
|
408
|
+
"description": "Filter logs by contact email",
|
|
409
|
+
"name": "email",
|
|
410
|
+
"hasDynamicHelp": false,
|
|
411
|
+
"multiple": false,
|
|
412
|
+
"type": "option"
|
|
413
|
+
},
|
|
414
|
+
"failed": {
|
|
415
|
+
"description": "Show only failed/bounced events",
|
|
416
|
+
"name": "failed",
|
|
417
|
+
"allowNo": false,
|
|
418
|
+
"type": "boolean"
|
|
404
419
|
}
|
|
405
420
|
},
|
|
406
421
|
"hasDynamicHelp": false,
|
|
407
422
|
"hiddenAliases": [],
|
|
408
|
-
"id": "
|
|
423
|
+
"id": "logs",
|
|
409
424
|
"pluginAlias": "@mailmodo/cli",
|
|
410
425
|
"pluginName": "@mailmodo/cli",
|
|
411
426
|
"pluginType": "core",
|
|
@@ -415,19 +430,17 @@
|
|
|
415
430
|
"relativePath": [
|
|
416
431
|
"dist",
|
|
417
432
|
"commands",
|
|
418
|
-
"
|
|
433
|
+
"logs",
|
|
419
434
|
"index.js"
|
|
420
435
|
]
|
|
421
436
|
},
|
|
422
|
-
"
|
|
437
|
+
"login": {
|
|
423
438
|
"aliases": [],
|
|
424
439
|
"args": {},
|
|
425
|
-
"description": "
|
|
440
|
+
"description": "Authenticate with Mailmodo using your API key",
|
|
426
441
|
"examples": [
|
|
427
|
-
"<%= config.bin %>
|
|
428
|
-
"<%= config.bin %>
|
|
429
|
-
"<%= config.bin %> logs --failed",
|
|
430
|
-
"<%= config.bin %> logs --json"
|
|
442
|
+
"<%= config.bin %> login",
|
|
443
|
+
"MAILMODO_API_KEY=mm_live_xxx <%= config.bin %> login"
|
|
431
444
|
],
|
|
432
445
|
"flags": {
|
|
433
446
|
"json": {
|
|
@@ -442,24 +455,11 @@
|
|
|
442
455
|
"name": "yes",
|
|
443
456
|
"allowNo": false,
|
|
444
457
|
"type": "boolean"
|
|
445
|
-
},
|
|
446
|
-
"email": {
|
|
447
|
-
"description": "Filter logs by contact email",
|
|
448
|
-
"name": "email",
|
|
449
|
-
"hasDynamicHelp": false,
|
|
450
|
-
"multiple": false,
|
|
451
|
-
"type": "option"
|
|
452
|
-
},
|
|
453
|
-
"failed": {
|
|
454
|
-
"description": "Show only failed/bounced events",
|
|
455
|
-
"name": "failed",
|
|
456
|
-
"allowNo": false,
|
|
457
|
-
"type": "boolean"
|
|
458
458
|
}
|
|
459
459
|
},
|
|
460
460
|
"hasDynamicHelp": false,
|
|
461
461
|
"hiddenAliases": [],
|
|
462
|
-
"id": "
|
|
462
|
+
"id": "login",
|
|
463
463
|
"pluginAlias": "@mailmodo/cli",
|
|
464
464
|
"pluginName": "@mailmodo/cli",
|
|
465
465
|
"pluginType": "core",
|
|
@@ -469,7 +469,7 @@
|
|
|
469
469
|
"relativePath": [
|
|
470
470
|
"dist",
|
|
471
471
|
"commands",
|
|
472
|
-
"
|
|
472
|
+
"login",
|
|
473
473
|
"index.js"
|
|
474
474
|
]
|
|
475
475
|
},
|
|
@@ -618,5 +618,5 @@
|
|
|
618
618
|
]
|
|
619
619
|
}
|
|
620
620
|
},
|
|
621
|
-
"version": "0.0.
|
|
621
|
+
"version": "0.0.21-beta.pr24.38"
|
|
622
622
|
}
|