@mailmodo/cli 0.0.18 → 0.0.19-beta.pr21.30
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 +9 -1
- package/dist/commands/deploy/index.js +171 -105
- package/dist/commands/edit/index.js +4 -1
- package/dist/commands/emails/index.js +16 -13
- package/dist/commands/logs/index.js +2 -2
- package/dist/commands/preview/index.js +15 -14
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +2 -0
- package/oclif.manifest.json +73 -73
- package/package.json +1 -1
|
@@ -10,9 +10,15 @@ export default class Deploy extends BaseCommand {
|
|
|
10
10
|
* Fetches current DNS verification status for the deploy flow.
|
|
11
11
|
*
|
|
12
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.
|
|
13
14
|
*/
|
|
14
15
|
private fetchDomainVerifyForDeploy;
|
|
15
16
|
run(): Promise<void>;
|
|
17
|
+
private buildDeployPayload;
|
|
18
|
+
private mapEmailToPayload;
|
|
19
|
+
private buildProjectPayload;
|
|
20
|
+
private confirmDeploy;
|
|
21
|
+
private ensureDomainReady;
|
|
16
22
|
/**
|
|
17
23
|
* Lists emails about to be deployed (skipped when `--json` is set).
|
|
18
24
|
*
|
|
@@ -32,9 +38,11 @@ export default class Deploy extends BaseCommand {
|
|
|
32
38
|
* @returns {Promise<boolean>} true if domain was verified, false if skipped.
|
|
33
39
|
*/
|
|
34
40
|
private runDomainSetup;
|
|
41
|
+
private collectDomainInputs;
|
|
42
|
+
private showDnsRecords;
|
|
35
43
|
/**
|
|
36
44
|
* Calls the domain verification API endpoint and reports pass/fail
|
|
37
|
-
* status for each DNS record (
|
|
45
|
+
* status for each DNS record (DKIM, DMARC, Return-Path).
|
|
38
46
|
*
|
|
39
47
|
* @returns {Promise<boolean>} true if all records pass.
|
|
40
48
|
*/
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { confirm, input } from '@inquirer/prompts';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
4
|
-
import { API_ENDPOINTS, DNS_GUIDE_URL } from '../../lib/constants.js';
|
|
5
|
-
import { saveYaml } from '../../lib/yaml-config.js';
|
|
4
|
+
import { API_ENDPOINTS, DEFAULT_BRAND_COLOR, DEFAULT_MONTHLY_CAP, DNS_GUIDE_URL, } from '../../lib/constants.js';
|
|
5
|
+
import { loadTemplate, saveYaml, } from '../../lib/yaml-config.js';
|
|
6
6
|
export default class Deploy extends BaseCommand {
|
|
7
7
|
static description = 'Deploy email sequences and verify sending domain';
|
|
8
8
|
static examples = [
|
|
@@ -16,69 +16,130 @@ export default class Deploy extends BaseCommand {
|
|
|
16
16
|
* Fetches current DNS verification status for the deploy flow.
|
|
17
17
|
*
|
|
18
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.
|
|
19
20
|
*/
|
|
20
|
-
fetchDomainVerifyForDeploy(jsonOutput) {
|
|
21
|
-
return this.withApiSpinner({ json: jsonOutput, text: ' Checking domain verification...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY
|
|
21
|
+
fetchDomainVerifyForDeploy(jsonOutput, domain) {
|
|
22
|
+
return this.withApiSpinner({ json: jsonOutput, text: ' Checking domain verification...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY, {
|
|
23
|
+
domain: domain || '',
|
|
24
|
+
}));
|
|
22
25
|
}
|
|
23
26
|
async run() {
|
|
24
27
|
const { flags } = await this.parse(Deploy);
|
|
25
28
|
await this.ensureAuth();
|
|
26
29
|
const yamlConfig = await this.ensureYaml();
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
domainVerify.data?.dkim === 'pass' &&
|
|
31
|
-
domainVerify.data?.dmarc === 'pass';
|
|
32
|
-
if (!domainVerified) {
|
|
33
|
-
if (!flags.json) {
|
|
34
|
-
this.log(`\n No sending domain verified yet.`);
|
|
35
|
-
this.log(` You need to verify a domain before sending emails.`);
|
|
36
|
-
this.log(` This is a one-time setup. Takes about 5 minutes.\n`);
|
|
37
|
-
}
|
|
38
|
-
if (!flags.yes) {
|
|
39
|
-
const setupNow = await confirm({
|
|
40
|
-
default: true,
|
|
41
|
-
message: 'Set up your sending domain now?',
|
|
42
|
-
});
|
|
43
|
-
if (!setupNow) {
|
|
44
|
-
this.log(`\n Sequences saved but ${chalk.yellow('NOT deployed')}.`);
|
|
45
|
-
this.log(` Emails will not send until your domain is verified.`);
|
|
46
|
-
this.log(` When ready, run: ${chalk.cyan('mailmodo domain')}`);
|
|
47
|
-
this.log(` Then: ${chalk.cyan('mailmodo deploy')}\n`);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
const completed = await this.runDomainSetup(yamlConfig, flags);
|
|
52
|
-
if (!completed)
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
30
|
+
const domainReady = await this.ensureDomainReady(yamlConfig, flags);
|
|
31
|
+
if (!domainReady)
|
|
32
|
+
return;
|
|
55
33
|
this.logPreDeploySummary(yamlConfig, flags.json);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (!proceed) {
|
|
62
|
-
this.log('\n Deploy cancelled.\n');
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
const response = await this.withApiSpinner({ json: flags.json, text: ' Deploying email sequences...' }, () => this.apiClient.post(API_ENDPOINTS.SEQUENCES, {
|
|
67
|
-
emails: yamlConfig.emails,
|
|
68
|
-
project: yamlConfig.project,
|
|
69
|
-
}));
|
|
34
|
+
const confirmed = await this.confirmDeploy(yamlConfig, flags);
|
|
35
|
+
if (!confirmed)
|
|
36
|
+
return;
|
|
37
|
+
const payload = await this.buildDeployPayload(yamlConfig);
|
|
38
|
+
const response = await this.withApiSpinner({ json: flags.json, text: ' Deploying email sequences...' }, () => this.apiClient.post(API_ENDPOINTS.SEQUENCES_DEPLOY, payload));
|
|
70
39
|
if (!response.ok) {
|
|
71
40
|
this.handleApiError(response);
|
|
72
41
|
}
|
|
73
42
|
if (flags.json) {
|
|
74
43
|
this.log(JSON.stringify({
|
|
75
|
-
deployed:
|
|
76
|
-
|
|
77
|
-
|
|
44
|
+
deployed: response.data.deployed,
|
|
45
|
+
diff: response.data.diff,
|
|
46
|
+
emailsLive: response.data.emailsLive,
|
|
47
|
+
sdkSnippet: response.data.sdkSnippet,
|
|
48
|
+
sequenceId: response.data.sequenceId,
|
|
78
49
|
}, null, 2));
|
|
79
50
|
return;
|
|
80
51
|
}
|
|
81
|
-
this.logDeploySuccessInstructions();
|
|
52
|
+
this.logDeploySuccessInstructions(response.data.sdkSnippet);
|
|
53
|
+
}
|
|
54
|
+
async buildDeployPayload(yamlConfig) {
|
|
55
|
+
const emailsWithHtml = await Promise.all(yamlConfig.emails.map(async (email) => {
|
|
56
|
+
const html = (await loadTemplate(`${email.id}.html`)) || '';
|
|
57
|
+
return { ...this.mapEmailToPayload(email), html, plainHtml: html };
|
|
58
|
+
}));
|
|
59
|
+
return {
|
|
60
|
+
...this.buildProjectPayload(yamlConfig.project),
|
|
61
|
+
emails: emailsWithHtml,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
mapEmailToPayload(email) {
|
|
65
|
+
return {
|
|
66
|
+
condition: email.condition || null,
|
|
67
|
+
ctaText: '',
|
|
68
|
+
delay: typeof email.delay === 'string'
|
|
69
|
+
? Number.parseInt(email.delay, 10) || 0
|
|
70
|
+
: email.delay,
|
|
71
|
+
goal: email.goal || '',
|
|
72
|
+
id: email.id,
|
|
73
|
+
isReminder: false,
|
|
74
|
+
previewText: email.previewText || '',
|
|
75
|
+
priority: 'medium',
|
|
76
|
+
subject: email.subject,
|
|
77
|
+
trigger: email.trigger,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
buildProjectPayload(project) {
|
|
81
|
+
return {
|
|
82
|
+
brand: {
|
|
83
|
+
colors: [project?.brandColor || DEFAULT_BRAND_COLOR],
|
|
84
|
+
logoUrl: project?.logoUrl || '',
|
|
85
|
+
},
|
|
86
|
+
emailStyle: project?.emailStyle || 'branded',
|
|
87
|
+
monthlyCap: project?.monthlyCap ?? DEFAULT_MONTHLY_CAP,
|
|
88
|
+
product: {
|
|
89
|
+
businessType: project?.type || '',
|
|
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
|
+
},
|
|
104
|
+
webhookUrl: project?.webhookUrl || '',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async confirmDeploy(yamlConfig, flags) {
|
|
108
|
+
if (flags.yes)
|
|
109
|
+
return true;
|
|
110
|
+
const proceed = await confirm({
|
|
111
|
+
default: true,
|
|
112
|
+
message: `Deploy ${yamlConfig.emails.length} emails?`,
|
|
113
|
+
});
|
|
114
|
+
if (!proceed) {
|
|
115
|
+
this.log('\n Deploy cancelled.\n');
|
|
116
|
+
}
|
|
117
|
+
return proceed;
|
|
118
|
+
}
|
|
119
|
+
async ensureDomainReady(yamlConfig, flags) {
|
|
120
|
+
const domainVerify = await this.fetchDomainVerifyForDeploy(flags.json, yamlConfig.project?.domain);
|
|
121
|
+
if (domainVerify.ok && domainVerify.data?.domainStatus === 'VERIFIED') {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
if (!flags.json) {
|
|
125
|
+
this.log(`\n No sending domain verified yet.`);
|
|
126
|
+
this.log(` You need to verify a domain before sending emails.`);
|
|
127
|
+
this.log(` This is a one-time setup. Takes about 5 minutes.\n`);
|
|
128
|
+
}
|
|
129
|
+
if (!flags.yes) {
|
|
130
|
+
const setupNow = await confirm({
|
|
131
|
+
default: true,
|
|
132
|
+
message: 'Set up your sending domain now?',
|
|
133
|
+
});
|
|
134
|
+
if (!setupNow) {
|
|
135
|
+
this.log(`\n Sequences saved but ${chalk.yellow('NOT deployed')}.`);
|
|
136
|
+
this.log(` Emails will not send until your domain is verified.`);
|
|
137
|
+
this.log(` When ready, run: ${chalk.cyan('mailmodo domain')}`);
|
|
138
|
+
this.log(` Then: ${chalk.cyan('mailmodo deploy')}\n`);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return this.runDomainSetup(yamlConfig, flags);
|
|
82
143
|
}
|
|
83
144
|
/**
|
|
84
145
|
* Lists emails about to be deployed (skipped when `--json` is set).
|
|
@@ -99,19 +160,19 @@ export default class Deploy extends BaseCommand {
|
|
|
99
160
|
/**
|
|
100
161
|
* Prints the post-deploy success message and SDK install snippet for interactive runs.
|
|
101
162
|
*/
|
|
102
|
-
logDeploySuccessInstructions() {
|
|
163
|
+
logDeploySuccessInstructions(sdkSnippet) {
|
|
103
164
|
this.log(` ${chalk.green('Deployed.')} Emails are live.\n`);
|
|
104
165
|
this.log(` ${'─'.repeat(53)}`);
|
|
105
166
|
this.log(` ${chalk.bold('ADD THIS TO YOUR APP (one-time only):')}`);
|
|
106
167
|
this.log(` ${'─'.repeat(53)}\n`);
|
|
107
|
-
this.log(` ${chalk.cyan('npm install @mailmodo/sdk')}\n`);
|
|
168
|
+
this.log(` ${chalk.cyan(sdkSnippet.install ?? 'npm install @mailmodo/sdk')}\n`);
|
|
108
169
|
this.log(` ${chalk.dim("import { track, identify } from '@mailmodo/sdk'")}\n`);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
this.log(
|
|
170
|
+
for (const [key, snippet] of Object.entries(sdkSnippet)) {
|
|
171
|
+
if (key === 'install')
|
|
172
|
+
continue;
|
|
173
|
+
this.log(` ${chalk.dim(snippet)}`);
|
|
174
|
+
}
|
|
175
|
+
this.log('');
|
|
115
176
|
this.log(` Full SDK docs: ${chalk.cyan('mailmodo.com/docs/sdk')}\n`);
|
|
116
177
|
this.log(` ${'─'.repeat(53)}\n`);
|
|
117
178
|
}
|
|
@@ -123,31 +184,7 @@ export default class Deploy extends BaseCommand {
|
|
|
123
184
|
* @returns {Promise<boolean>} true if domain was verified, false if skipped.
|
|
124
185
|
*/
|
|
125
186
|
async runDomainSetup(yamlConfig, flags) {
|
|
126
|
-
|
|
127
|
-
let senderEmail;
|
|
128
|
-
let address;
|
|
129
|
-
if (flags.yes) {
|
|
130
|
-
domain = yamlConfig.project?.domain || '';
|
|
131
|
-
senderEmail = yamlConfig.project?.fromEmail || '';
|
|
132
|
-
address = yamlConfig.project?.address || '';
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
this.log(`\n ${'─'.repeat(53)}`);
|
|
136
|
-
this.log(` ${chalk.bold('DOMAIN SETUP')}`);
|
|
137
|
-
this.log(` ${'─'.repeat(53)}\n`);
|
|
138
|
-
domain = await input({
|
|
139
|
-
message: 'What domain will you send from?',
|
|
140
|
-
validate: (v) => (v?.trim() ? true : 'Domain is required'),
|
|
141
|
-
});
|
|
142
|
-
senderEmail = await input({
|
|
143
|
-
message: 'Sender email address:',
|
|
144
|
-
validate: (v) => v?.includes('@') ? true : 'Please enter a valid email',
|
|
145
|
-
});
|
|
146
|
-
address = await input({
|
|
147
|
-
message: 'Business address (required by law for email footers):',
|
|
148
|
-
validate: (v) => (v?.trim() ? true : 'Address is required'),
|
|
149
|
-
});
|
|
150
|
-
}
|
|
187
|
+
const { address, domain, senderEmail } = await this.collectDomainInputs(yamlConfig, flags);
|
|
151
188
|
const domainResponse = await this.withApiSpinner({ json: flags.json, text: ' Configuring domain...' }, () => this.apiClient.post(API_ENDPOINTS.DOMAIN, {
|
|
152
189
|
address,
|
|
153
190
|
domain,
|
|
@@ -160,20 +197,9 @@ export default class Deploy extends BaseCommand {
|
|
|
160
197
|
yamlConfig.project.fromEmail = senderEmail;
|
|
161
198
|
yamlConfig.project.address = address;
|
|
162
199
|
await saveYaml(yamlConfig);
|
|
163
|
-
|
|
164
|
-
if (!flags.json) {
|
|
165
|
-
this.log(`\n Add these ${dnsRecords.length} DNS records to your domain provider:\n`);
|
|
166
|
-
for (const [i, record] of dnsRecords.entries()) {
|
|
167
|
-
this.log(` ${chalk.bold(`RECORD ${i + 1}`)}`);
|
|
168
|
-
this.log(` Type: ${record.type}`);
|
|
169
|
-
this.log(` Host: ${record.host}`);
|
|
170
|
-
this.log(` Value: ${record.value}\n`);
|
|
171
|
-
}
|
|
172
|
-
this.log(` DNS changes take 5–30 minutes to propagate.`);
|
|
173
|
-
this.log(` Full guide: ${chalk.cyan(DNS_GUIDE_URL)}\n`);
|
|
174
|
-
}
|
|
200
|
+
this.showDnsRecords(domainResponse.data?.dnsRecords || [], flags.json);
|
|
175
201
|
if (flags.yes) {
|
|
176
|
-
return this.verifyDomain(flags.json);
|
|
202
|
+
return this.verifyDomain(flags.json, domain);
|
|
177
203
|
}
|
|
178
204
|
const action = await input({
|
|
179
205
|
default: '',
|
|
@@ -185,25 +211,65 @@ export default class Deploy extends BaseCommand {
|
|
|
185
211
|
this.log(` Then: ${chalk.cyan('mailmodo deploy')}\n`);
|
|
186
212
|
return false;
|
|
187
213
|
}
|
|
188
|
-
return this.verifyDomain(flags.json);
|
|
214
|
+
return this.verifyDomain(flags.json, domain);
|
|
215
|
+
}
|
|
216
|
+
async collectDomainInputs(yamlConfig, flags) {
|
|
217
|
+
if (flags.yes) {
|
|
218
|
+
return {
|
|
219
|
+
address: yamlConfig.project?.address || '',
|
|
220
|
+
domain: yamlConfig.project?.domain || '',
|
|
221
|
+
senderEmail: yamlConfig.project?.fromEmail || '',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
this.log(`\n ${'─'.repeat(53)}`);
|
|
225
|
+
this.log(` ${chalk.bold('DOMAIN SETUP')}`);
|
|
226
|
+
this.log(` ${'─'.repeat(53)}\n`);
|
|
227
|
+
const domain = await input({
|
|
228
|
+
message: 'What domain will you send from?',
|
|
229
|
+
validate: (v) => (v?.trim() ? true : 'Domain is required'),
|
|
230
|
+
});
|
|
231
|
+
const senderEmail = await input({
|
|
232
|
+
message: 'Sender email address:',
|
|
233
|
+
validate: (v) => (v?.includes('@') ? true : 'Please enter a valid email'),
|
|
234
|
+
});
|
|
235
|
+
const address = await input({
|
|
236
|
+
message: 'Business address (required by law for email footers):',
|
|
237
|
+
validate: (v) => (v?.trim() ? true : 'Address is required'),
|
|
238
|
+
});
|
|
239
|
+
return { address, domain, senderEmail };
|
|
240
|
+
}
|
|
241
|
+
showDnsRecords(dnsRecords, jsonOutput) {
|
|
242
|
+
if (jsonOutput)
|
|
243
|
+
return;
|
|
244
|
+
this.log(`\n Add these ${dnsRecords.length} DNS records to your domain provider:\n`);
|
|
245
|
+
for (const [i, record] of dnsRecords.entries()) {
|
|
246
|
+
this.log(` ${chalk.bold(`RECORD ${i + 1}`)}`);
|
|
247
|
+
this.log(` Type: ${record.type}`);
|
|
248
|
+
this.log(` Host: ${record.host}`);
|
|
249
|
+
this.log(` Value: ${record.value}\n`);
|
|
250
|
+
}
|
|
251
|
+
this.log(` DNS changes take 5–30 minutes to propagate.`);
|
|
252
|
+
this.log(` Full guide: ${chalk.cyan(DNS_GUIDE_URL)}\n`);
|
|
189
253
|
}
|
|
190
254
|
/**
|
|
191
255
|
* Calls the domain verification API endpoint and reports pass/fail
|
|
192
|
-
* status for each DNS record (
|
|
256
|
+
* status for each DNS record (DKIM, DMARC, Return-Path).
|
|
193
257
|
*
|
|
194
258
|
* @returns {Promise<boolean>} true if all records pass.
|
|
195
259
|
*/
|
|
196
|
-
async verifyDomain(jsonOutput) {
|
|
197
|
-
const verify = await this.withApiSpinner({ json: jsonOutput, text: ' Checking DNS...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY
|
|
260
|
+
async verifyDomain(jsonOutput, domain) {
|
|
261
|
+
const verify = await this.withApiSpinner({ json: jsonOutput, text: ' Checking DNS...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY, {
|
|
262
|
+
domain,
|
|
263
|
+
}));
|
|
198
264
|
if (!verify.ok) {
|
|
199
265
|
this.handleApiError(verify);
|
|
200
266
|
}
|
|
201
|
-
const { dkim, dmarc,
|
|
202
|
-
const allPassed =
|
|
267
|
+
const { dkim, dmarc, domainStatus, returnPath } = verify.data;
|
|
268
|
+
const allPassed = domainStatus === 'VERIFIED';
|
|
203
269
|
if (!jsonOutput) {
|
|
204
|
-
this.log(`
|
|
205
|
-
this.log(`
|
|
206
|
-
this.log(`
|
|
270
|
+
this.log(` DKIM ${dkim ? chalk.green('✓') : chalk.red('✗')}`);
|
|
271
|
+
this.log(` DMARC ${dmarc ? chalk.green('✓') : chalk.red('✗')}`);
|
|
272
|
+
this.log(` Return-Path ${returnPath ? chalk.green('✓') : chalk.red('✗')}`);
|
|
207
273
|
if (allPassed) {
|
|
208
274
|
this.log(`\n ${chalk.green('Domain verified.')} Continuing deploy...\n`);
|
|
209
275
|
}
|
|
@@ -6,7 +6,10 @@ import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
|
6
6
|
import { loadTemplate, saveTemplate, saveYaml, } from '../../lib/yaml-config.js';
|
|
7
7
|
export default class Edit extends BaseCommand {
|
|
8
8
|
static args = {
|
|
9
|
-
id: Args.string({
|
|
9
|
+
id: Args.string({
|
|
10
|
+
description: 'Email template ID to edit',
|
|
11
|
+
required: true,
|
|
12
|
+
}),
|
|
10
13
|
};
|
|
11
14
|
static description = 'Edit an email using AI-assisted natural language changes';
|
|
12
15
|
static examples = [
|
|
@@ -19,27 +19,30 @@ export default class Emails extends BaseCommand {
|
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
21
|
this.log(`\n ${chalk.bold(String(emails.length))} emails configured in mailmodo.yaml:\n`);
|
|
22
|
-
const
|
|
23
|
-
const
|
|
22
|
+
const idColW = Math.max(...emails.map((e) => e.id.length), 'ID'.length) + 2;
|
|
23
|
+
const triggerColW = Math.max(...emails.map((e) => e.trigger.length), 'Trigger'.length) + 2;
|
|
24
|
+
const delayColW = Math.max(...emails.map((e) => String(e.delay).length), 'Delay'.length) +
|
|
25
|
+
2;
|
|
26
|
+
const hasConditions = emails.some((e) => e.condition);
|
|
27
|
+
this.log(` ${chalk.bold('ID'.padEnd(idColW))}${chalk.bold('Trigger'.padEnd(triggerColW))}${chalk.bold('Delay'.padEnd(delayColW))}${hasConditions ? chalk.bold('Condition') : ''}`);
|
|
28
|
+
this.log(` ${'─'.repeat(idColW + triggerColW + delayColW + (hasConditions ? 'Condition'.length : 0))}`);
|
|
24
29
|
for (const email of emails) {
|
|
25
|
-
const id = email.id.padEnd(
|
|
26
|
-
const trigger =
|
|
27
|
-
const delay =
|
|
28
|
-
const condition = email.condition
|
|
29
|
-
|
|
30
|
-
: '';
|
|
31
|
-
this.log(` ${chalk.cyan(id)} ${trigger} ${delay}${condition}`);
|
|
30
|
+
const id = chalk.cyan(email.id.padEnd(idColW));
|
|
31
|
+
const trigger = email.trigger.padEnd(triggerColW);
|
|
32
|
+
const delay = String(email.delay).padEnd(delayColW);
|
|
33
|
+
const condition = email.condition ? chalk.dim(email.condition) : '';
|
|
34
|
+
this.log(` ${id}${trigger}${delay}${condition}`);
|
|
32
35
|
}
|
|
33
36
|
this.log('');
|
|
34
37
|
if (!flags.yes) {
|
|
35
|
-
const
|
|
38
|
+
const templateId = await input({
|
|
36
39
|
default: 'n',
|
|
37
40
|
message: "View an email? (id or 'n'):",
|
|
38
41
|
});
|
|
39
|
-
if (
|
|
40
|
-
const email = emails.find((e) => e.id ===
|
|
42
|
+
if (templateId !== 'n') {
|
|
43
|
+
const email = emails.find((e) => e.id === templateId);
|
|
41
44
|
if (!email) {
|
|
42
|
-
this.log(`\n
|
|
45
|
+
this.log(`\n Template '${templateId}' not found.\n`);
|
|
43
46
|
return;
|
|
44
47
|
}
|
|
45
48
|
this.log('');
|
|
@@ -40,11 +40,11 @@ export default class Logs extends BaseCommand {
|
|
|
40
40
|
if (entries?.length) {
|
|
41
41
|
for (const entry of entries) {
|
|
42
42
|
const time = (entry.timestamp || '').padEnd(18);
|
|
43
|
-
const
|
|
43
|
+
const templateId = (entry.emailId || '').padEnd(24);
|
|
44
44
|
const statusColor = this.statusColor(entry.status);
|
|
45
45
|
const status = statusColor((entry.status || '').padEnd(10));
|
|
46
46
|
const contact = entry.contact || '';
|
|
47
|
-
this.log(` ${time}${
|
|
47
|
+
this.log(` ${time}${templateId}${status}${contact}`);
|
|
48
48
|
if (entry.reason) {
|
|
49
49
|
this.log(` ${' '.repeat(52)}${chalk.dim(`(reason: ${entry.reason})`)}`);
|
|
50
50
|
}
|
|
@@ -52,7 +52,7 @@ function htmlToText(html) {
|
|
|
52
52
|
}
|
|
53
53
|
export default class Preview extends BaseCommand {
|
|
54
54
|
static args = {
|
|
55
|
-
id: Args.string({ description: 'Email ID to preview' }),
|
|
55
|
+
id: Args.string({ description: 'Email template ID to preview' }),
|
|
56
56
|
};
|
|
57
57
|
static description = 'Preview an email in browser, as text, or send a test';
|
|
58
58
|
static examples = [
|
|
@@ -71,13 +71,13 @@ export default class Preview extends BaseCommand {
|
|
|
71
71
|
async run() {
|
|
72
72
|
const { args, flags } = await this.parse(Preview);
|
|
73
73
|
const yamlConfig = await this.ensureYaml();
|
|
74
|
-
const
|
|
75
|
-
if (!
|
|
74
|
+
const templateId = args.id || yamlConfig.emails[0]?.id;
|
|
75
|
+
if (!templateId) {
|
|
76
76
|
this.error('No emails configured. Run mailmodo init first.');
|
|
77
77
|
}
|
|
78
|
-
const email = yamlConfig.emails.find((e) => e.id ===
|
|
78
|
+
const email = yamlConfig.emails.find((e) => e.id === templateId);
|
|
79
79
|
if (!email) {
|
|
80
|
-
this.error(`
|
|
80
|
+
this.error(`Template '${templateId}' not found in mailmodo.yaml.`);
|
|
81
81
|
}
|
|
82
82
|
const sampleData = {
|
|
83
83
|
...SAMPLE_DATA,
|
|
@@ -85,7 +85,7 @@ export default class Preview extends BaseCommand {
|
|
|
85
85
|
product_name: yamlConfig.project?.name || 'YourApp', // eslint-disable-line camelcase
|
|
86
86
|
};
|
|
87
87
|
if (flags.send) {
|
|
88
|
-
await this.sendTestEmail(
|
|
88
|
+
await this.sendTestEmail(templateId, flags.send, flags.json);
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
91
|
const templateHtml = await loadTemplate(`${email.id}.html`);
|
|
@@ -123,21 +123,22 @@ export default class Preview extends BaseCommand {
|
|
|
123
123
|
* Calls the API to send a test email to the specified address.
|
|
124
124
|
* Before domain verification, tests send via the mailmodo.com domain.
|
|
125
125
|
*/
|
|
126
|
-
async sendTestEmail(
|
|
126
|
+
async sendTestEmail(templateId, toAddress, jsonOutput) {
|
|
127
127
|
await this.ensureAuth();
|
|
128
|
-
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Sending test email...' }, () => this.apiClient.post(`${API_ENDPOINTS.PREVIEW}/${
|
|
129
|
-
to: toAddress,
|
|
130
|
-
}));
|
|
128
|
+
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Sending test email...' }, () => this.apiClient.post(`${API_ENDPOINTS.PREVIEW}/${templateId}/send`, { to: toAddress }));
|
|
131
129
|
if (!response.ok) {
|
|
132
130
|
this.handleApiError(response);
|
|
133
131
|
}
|
|
132
|
+
const { note, sentTo, sentVia, status } = response.data;
|
|
134
133
|
if (jsonOutput) {
|
|
135
|
-
this.log(JSON.stringify({
|
|
134
|
+
this.log(JSON.stringify({ note, sentTo, sentVia, status }, null, 2));
|
|
136
135
|
return;
|
|
137
136
|
}
|
|
138
|
-
this.log(`\n
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
this.log(`\n ${chalk.green('✓')} Test email sent to ${chalk.cyan(sentTo)} via ${chalk.cyan(sentVia)}.`);
|
|
138
|
+
if (note) {
|
|
139
|
+
this.log(` ${chalk.dim(note)}`);
|
|
140
|
+
}
|
|
141
|
+
this.log('');
|
|
141
142
|
}
|
|
142
143
|
/**
|
|
143
144
|
* Starts a local HTTP server on PREVIEW_PORT to serve the rendered email
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export declare const API_ENDPOINTS: Readonly<{
|
|
|
17
17
|
LOGS: "/logs";
|
|
18
18
|
PREVIEW: "/preview";
|
|
19
19
|
SEQUENCES: "/sequences";
|
|
20
|
+
SEQUENCES_DEPLOY: "/sequences/deploy";
|
|
21
|
+
SEQUENCES_VALIDATE: "/sequences/validate";
|
|
20
22
|
}>;
|
|
21
23
|
export declare const LOGIN_URL = "https://app-vertex-debug.azurewebsites.net/login.html";
|
|
22
24
|
export declare const DOCS_URL = "https://mailmodo.com/docs/cli";
|
package/dist/lib/constants.js
CHANGED
|
@@ -23,6 +23,8 @@ export const API_ENDPOINTS = Object.freeze({
|
|
|
23
23
|
LOGS: '/logs',
|
|
24
24
|
PREVIEW: '/preview',
|
|
25
25
|
SEQUENCES: '/sequences',
|
|
26
|
+
SEQUENCES_DEPLOY: '/sequences/deploy',
|
|
27
|
+
SEQUENCES_VALIDATE: '/sequences/validate',
|
|
26
28
|
});
|
|
27
29
|
const DEV_LOGIN_URL = 'https://app-vertex-debug.azurewebsites.net/login.html';
|
|
28
30
|
// const PRODUCTION_LOGIN_URL = 'https://mailmodo.com/cli';
|
package/oclif.manifest.json
CHANGED
|
@@ -209,7 +209,7 @@
|
|
|
209
209
|
"aliases": [],
|
|
210
210
|
"args": {
|
|
211
211
|
"id": {
|
|
212
|
-
"description": "Email ID to edit",
|
|
212
|
+
"description": "Email template ID to edit",
|
|
213
213
|
"name": "id",
|
|
214
214
|
"required": true
|
|
215
215
|
}
|
|
@@ -257,6 +257,45 @@
|
|
|
257
257
|
"index.js"
|
|
258
258
|
]
|
|
259
259
|
},
|
|
260
|
+
"emails": {
|
|
261
|
+
"aliases": [],
|
|
262
|
+
"args": {},
|
|
263
|
+
"description": "List and view configured email sequences",
|
|
264
|
+
"examples": [
|
|
265
|
+
"<%= config.bin %> emails",
|
|
266
|
+
"<%= config.bin %> emails --json"
|
|
267
|
+
],
|
|
268
|
+
"flags": {
|
|
269
|
+
"json": {
|
|
270
|
+
"description": "Output as JSON",
|
|
271
|
+
"name": "json",
|
|
272
|
+
"allowNo": false,
|
|
273
|
+
"type": "boolean"
|
|
274
|
+
},
|
|
275
|
+
"yes": {
|
|
276
|
+
"char": "y",
|
|
277
|
+
"description": "Skip confirmation prompts",
|
|
278
|
+
"name": "yes",
|
|
279
|
+
"allowNo": false,
|
|
280
|
+
"type": "boolean"
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
"hasDynamicHelp": false,
|
|
284
|
+
"hiddenAliases": [],
|
|
285
|
+
"id": "emails",
|
|
286
|
+
"pluginAlias": "@mailmodo/cli",
|
|
287
|
+
"pluginName": "@mailmodo/cli",
|
|
288
|
+
"pluginType": "core",
|
|
289
|
+
"strict": true,
|
|
290
|
+
"enableJsonFlag": false,
|
|
291
|
+
"isESM": true,
|
|
292
|
+
"relativePath": [
|
|
293
|
+
"dist",
|
|
294
|
+
"commands",
|
|
295
|
+
"emails",
|
|
296
|
+
"index.js"
|
|
297
|
+
]
|
|
298
|
+
},
|
|
260
299
|
"init": {
|
|
261
300
|
"aliases": [],
|
|
262
301
|
"args": {},
|
|
@@ -434,64 +473,6 @@
|
|
|
434
473
|
"index.js"
|
|
435
474
|
]
|
|
436
475
|
},
|
|
437
|
-
"preview": {
|
|
438
|
-
"aliases": [],
|
|
439
|
-
"args": {
|
|
440
|
-
"id": {
|
|
441
|
-
"description": "Email ID to preview",
|
|
442
|
-
"name": "id"
|
|
443
|
-
}
|
|
444
|
-
},
|
|
445
|
-
"description": "Preview an email in browser, as text, or send a test",
|
|
446
|
-
"examples": [
|
|
447
|
-
"<%= config.bin %> preview welcome",
|
|
448
|
-
"<%= config.bin %> preview welcome --text",
|
|
449
|
-
"<%= config.bin %> preview welcome --send me@example.com"
|
|
450
|
-
],
|
|
451
|
-
"flags": {
|
|
452
|
-
"json": {
|
|
453
|
-
"description": "Output as JSON",
|
|
454
|
-
"name": "json",
|
|
455
|
-
"allowNo": false,
|
|
456
|
-
"type": "boolean"
|
|
457
|
-
},
|
|
458
|
-
"yes": {
|
|
459
|
-
"char": "y",
|
|
460
|
-
"description": "Skip confirmation prompts",
|
|
461
|
-
"name": "yes",
|
|
462
|
-
"allowNo": false,
|
|
463
|
-
"type": "boolean"
|
|
464
|
-
},
|
|
465
|
-
"send": {
|
|
466
|
-
"description": "Send test email to this address",
|
|
467
|
-
"name": "send",
|
|
468
|
-
"hasDynamicHelp": false,
|
|
469
|
-
"multiple": false,
|
|
470
|
-
"type": "option"
|
|
471
|
-
},
|
|
472
|
-
"text": {
|
|
473
|
-
"description": "Output plain text version (for AI agents)",
|
|
474
|
-
"name": "text",
|
|
475
|
-
"allowNo": false,
|
|
476
|
-
"type": "boolean"
|
|
477
|
-
}
|
|
478
|
-
},
|
|
479
|
-
"hasDynamicHelp": false,
|
|
480
|
-
"hiddenAliases": [],
|
|
481
|
-
"id": "preview",
|
|
482
|
-
"pluginAlias": "@mailmodo/cli",
|
|
483
|
-
"pluginName": "@mailmodo/cli",
|
|
484
|
-
"pluginType": "core",
|
|
485
|
-
"strict": true,
|
|
486
|
-
"enableJsonFlag": false,
|
|
487
|
-
"isESM": true,
|
|
488
|
-
"relativePath": [
|
|
489
|
-
"dist",
|
|
490
|
-
"commands",
|
|
491
|
-
"preview",
|
|
492
|
-
"index.js"
|
|
493
|
-
]
|
|
494
|
-
},
|
|
495
476
|
"settings": {
|
|
496
477
|
"aliases": [],
|
|
497
478
|
"args": {},
|
|
@@ -539,13 +520,19 @@
|
|
|
539
520
|
"index.js"
|
|
540
521
|
]
|
|
541
522
|
},
|
|
542
|
-
"
|
|
523
|
+
"preview": {
|
|
543
524
|
"aliases": [],
|
|
544
|
-
"args": {
|
|
545
|
-
|
|
525
|
+
"args": {
|
|
526
|
+
"id": {
|
|
527
|
+
"description": "Email template ID to preview",
|
|
528
|
+
"name": "id"
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
"description": "Preview an email in browser, as text, or send a test",
|
|
546
532
|
"examples": [
|
|
547
|
-
"<%= config.bin %>
|
|
548
|
-
"<%= config.bin %>
|
|
533
|
+
"<%= config.bin %> preview welcome",
|
|
534
|
+
"<%= config.bin %> preview welcome --text",
|
|
535
|
+
"<%= config.bin %> preview welcome --send me@example.com"
|
|
549
536
|
],
|
|
550
537
|
"flags": {
|
|
551
538
|
"json": {
|
|
@@ -560,11 +547,24 @@
|
|
|
560
547
|
"name": "yes",
|
|
561
548
|
"allowNo": false,
|
|
562
549
|
"type": "boolean"
|
|
550
|
+
},
|
|
551
|
+
"send": {
|
|
552
|
+
"description": "Send test email to this address",
|
|
553
|
+
"name": "send",
|
|
554
|
+
"hasDynamicHelp": false,
|
|
555
|
+
"multiple": false,
|
|
556
|
+
"type": "option"
|
|
557
|
+
},
|
|
558
|
+
"text": {
|
|
559
|
+
"description": "Output plain text version (for AI agents)",
|
|
560
|
+
"name": "text",
|
|
561
|
+
"allowNo": false,
|
|
562
|
+
"type": "boolean"
|
|
563
563
|
}
|
|
564
564
|
},
|
|
565
565
|
"hasDynamicHelp": false,
|
|
566
566
|
"hiddenAliases": [],
|
|
567
|
-
"id": "
|
|
567
|
+
"id": "preview",
|
|
568
568
|
"pluginAlias": "@mailmodo/cli",
|
|
569
569
|
"pluginName": "@mailmodo/cli",
|
|
570
570
|
"pluginType": "core",
|
|
@@ -574,17 +574,17 @@
|
|
|
574
574
|
"relativePath": [
|
|
575
575
|
"dist",
|
|
576
576
|
"commands",
|
|
577
|
-
"
|
|
577
|
+
"preview",
|
|
578
578
|
"index.js"
|
|
579
579
|
]
|
|
580
580
|
},
|
|
581
|
-
"
|
|
581
|
+
"status": {
|
|
582
582
|
"aliases": [],
|
|
583
583
|
"args": {},
|
|
584
|
-
"description": "
|
|
584
|
+
"description": "View email performance metrics and quota usage",
|
|
585
585
|
"examples": [
|
|
586
|
-
"<%= config.bin %>
|
|
587
|
-
"<%= config.bin %>
|
|
586
|
+
"<%= config.bin %> status",
|
|
587
|
+
"<%= config.bin %> status --json"
|
|
588
588
|
],
|
|
589
589
|
"flags": {
|
|
590
590
|
"json": {
|
|
@@ -603,7 +603,7 @@
|
|
|
603
603
|
},
|
|
604
604
|
"hasDynamicHelp": false,
|
|
605
605
|
"hiddenAliases": [],
|
|
606
|
-
"id": "
|
|
606
|
+
"id": "status",
|
|
607
607
|
"pluginAlias": "@mailmodo/cli",
|
|
608
608
|
"pluginName": "@mailmodo/cli",
|
|
609
609
|
"pluginType": "core",
|
|
@@ -613,10 +613,10 @@
|
|
|
613
613
|
"relativePath": [
|
|
614
614
|
"dist",
|
|
615
615
|
"commands",
|
|
616
|
-
"
|
|
616
|
+
"status",
|
|
617
617
|
"index.js"
|
|
618
618
|
]
|
|
619
619
|
}
|
|
620
620
|
},
|
|
621
|
-
"version": "0.0.
|
|
621
|
+
"version": "0.0.19-beta.pr21.30"
|
|
622
622
|
}
|