@mailmodo/cli 0.0.29 → 0.0.30-beta.pr32.51
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.js +10 -7
- package/dist/commands/domain/index.js +7 -5
- package/dist/commands/edit/index.d.ts +11 -1
- package/dist/commands/edit/index.js +148 -86
- package/dist/commands/emails/index.d.ts +2 -0
- package/dist/commands/emails/index.js +45 -1
- package/dist/commands/init/index.d.ts +1 -0
- package/dist/commands/init/index.js +22 -3
- package/dist/commands/preview/index.js +2 -2
- package/dist/commands/settings/index.js +4 -2
- package/dist/lib/api-client.d.ts +2 -0
- package/dist/lib/api-client.js +20 -2
- package/dist/lib/base-command.js +5 -5
- package/dist/lib/yaml-config.d.ts +1 -0
- package/dist/lib/yaml-config.js +4 -0
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
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, DEFAULT_BRAND_COLOR, DEFAULT_MONTHLY_CAP,
|
|
4
|
+
import { API_ENDPOINTS, DEFAULT_BRAND_COLOR, DEFAULT_MONTHLY_CAP, } from '../../lib/constants.js';
|
|
5
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';
|
|
@@ -62,7 +62,8 @@ export default class Deploy extends BaseCommand {
|
|
|
62
62
|
async buildDeployPayload(yamlConfig) {
|
|
63
63
|
const emailsWithHtml = await Promise.all(yamlConfig.emails.map(async (email) => {
|
|
64
64
|
const html = (await loadTemplate(`${email.id}.html`)) || '';
|
|
65
|
-
|
|
65
|
+
const plainHtml = (await loadTemplate(`${email.id}_plain.html`)) || html;
|
|
66
|
+
return { ...this.mapEmailToPayload(email), html, plainHtml };
|
|
66
67
|
}));
|
|
67
68
|
return {
|
|
68
69
|
...this.buildProjectPayload(yamlConfig.project),
|
|
@@ -232,7 +233,7 @@ export default class Deploy extends BaseCommand {
|
|
|
232
233
|
yamlConfig.project.fromEmail = senderEmail;
|
|
233
234
|
yamlConfig.project.address = address;
|
|
234
235
|
await saveYaml(yamlConfig);
|
|
235
|
-
this.showDnsRecords(domainResponse.data?.dnsRecords || [], flags.json);
|
|
236
|
+
this.showDnsRecords(domainResponse.data?.dnsRecords || [], flags.json, domainResponse.data?.dnsGuideUrl);
|
|
236
237
|
if (flags.yes) {
|
|
237
238
|
return this.verifyDomain(flags.json, domain);
|
|
238
239
|
}
|
|
@@ -273,7 +274,7 @@ export default class Deploy extends BaseCommand {
|
|
|
273
274
|
});
|
|
274
275
|
return { address, domain, senderEmail };
|
|
275
276
|
}
|
|
276
|
-
showDnsRecords(dnsRecords, jsonOutput) {
|
|
277
|
+
showDnsRecords(dnsRecords, jsonOutput, dnsGuideUrl) {
|
|
277
278
|
if (jsonOutput)
|
|
278
279
|
return;
|
|
279
280
|
this.log(`\n Add these ${dnsRecords.length} DNS records to your domain provider:\n`);
|
|
@@ -284,7 +285,8 @@ export default class Deploy extends BaseCommand {
|
|
|
284
285
|
this.log(` Value: ${record.value}\n`);
|
|
285
286
|
}
|
|
286
287
|
this.log(` DNS changes take 5–30 minutes to propagate.`);
|
|
287
|
-
|
|
288
|
+
if (dnsGuideUrl)
|
|
289
|
+
this.log(` Full guide: ${chalk.cyan(dnsGuideUrl)}\n`);
|
|
288
290
|
}
|
|
289
291
|
async verifyDomain(jsonOutput, domain) {
|
|
290
292
|
const verify = await this.withApiSpinner({ json: jsonOutput, text: ' Checking DNS...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY, {
|
|
@@ -293,7 +295,7 @@ export default class Deploy extends BaseCommand {
|
|
|
293
295
|
if (!verify.ok) {
|
|
294
296
|
this.handleApiError(verify);
|
|
295
297
|
}
|
|
296
|
-
const { dkim, dmarc, domainStatus, returnPath } = verify.data;
|
|
298
|
+
const { dkim, dmarc, dnsGuideUrl, domainStatus, returnPath } = verify.data;
|
|
297
299
|
const allPassed = domainStatus === 'VERIFIED';
|
|
298
300
|
if (!jsonOutput) {
|
|
299
301
|
this.log(` DKIM ${dkim ? chalk.green('✓') : chalk.red('✗')}`);
|
|
@@ -304,7 +306,8 @@ export default class Deploy extends BaseCommand {
|
|
|
304
306
|
}
|
|
305
307
|
else {
|
|
306
308
|
this.log(`\n ${chalk.yellow('Some records failed.')} Fix them and run ${chalk.cyan('mailmodo domain --verify')}.`);
|
|
307
|
-
|
|
309
|
+
if (dnsGuideUrl)
|
|
310
|
+
this.log(` Help: ${chalk.cyan(dnsGuideUrl)}\n`);
|
|
308
311
|
}
|
|
309
312
|
}
|
|
310
313
|
return allPassed;
|
|
@@ -2,7 +2,7 @@ import { Flags } from '@oclif/core';
|
|
|
2
2
|
import { input } from '@inquirer/prompts';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
|
-
import { API_ENDPOINTS
|
|
5
|
+
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
6
6
|
import { saveConfig } from '../../lib/config.js';
|
|
7
7
|
import { saveYaml } from '../../lib/yaml-config.js';
|
|
8
8
|
export default class Domain extends BaseCommand {
|
|
@@ -69,7 +69,7 @@ export default class Domain extends BaseCommand {
|
|
|
69
69
|
await saveYaml(yamlConfig);
|
|
70
70
|
await saveConfig({ ...config, domain });
|
|
71
71
|
const records = response.data?.dnsRecords || [];
|
|
72
|
-
const guideUrl = response.data?.dnsGuideUrl
|
|
72
|
+
const guideUrl = response.data?.dnsGuideUrl;
|
|
73
73
|
if (flags.json) {
|
|
74
74
|
this.log(JSON.stringify({ dnsRecords: records, domain }, null, 2));
|
|
75
75
|
return;
|
|
@@ -82,7 +82,8 @@ export default class Domain extends BaseCommand {
|
|
|
82
82
|
this.log(` Value: ${record.value}\n`);
|
|
83
83
|
}
|
|
84
84
|
this.log(` DNS changes take 5–30 minutes to propagate.`);
|
|
85
|
-
|
|
85
|
+
if (guideUrl)
|
|
86
|
+
this.log(` Full guide: ${chalk.cyan(guideUrl)}\n`);
|
|
86
87
|
if (!flags.yes) {
|
|
87
88
|
const action = await input({
|
|
88
89
|
default: '',
|
|
@@ -107,7 +108,7 @@ export default class Domain extends BaseCommand {
|
|
|
107
108
|
if (!response.ok) {
|
|
108
109
|
this.handleApiError(response);
|
|
109
110
|
}
|
|
110
|
-
const { dkim, dmarc, returnPath, domainStatus } = response.data;
|
|
111
|
+
const { dkim, dmarc, dnsGuideUrl, returnPath, domainStatus } = response.data;
|
|
111
112
|
if (jsonOutput) {
|
|
112
113
|
this.log(JSON.stringify({ dkim, dmarc, returnPath, domainStatus }, null, 2));
|
|
113
114
|
return;
|
|
@@ -133,7 +134,8 @@ export default class Domain extends BaseCommand {
|
|
|
133
134
|
this.log(` - Cloudflare: proxy must be OFF (grey cloud, not orange)`);
|
|
134
135
|
}
|
|
135
136
|
this.log(`\n Fix the records and run ${chalk.cyan('mailmodo domain --verify')} again.`);
|
|
136
|
-
|
|
137
|
+
if (dnsGuideUrl)
|
|
138
|
+
this.log(` Help: ${chalk.cyan(dnsGuideUrl)}\n`);
|
|
137
139
|
}
|
|
138
140
|
}
|
|
139
141
|
/**
|
|
@@ -10,6 +10,17 @@ 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
|
+
run(): Promise<void>;
|
|
14
|
+
private runEditStep;
|
|
15
|
+
private handleUserAction;
|
|
16
|
+
private callEditApi;
|
|
17
|
+
private finalizeEdit;
|
|
18
|
+
private applyEmailChanges;
|
|
19
|
+
private persistChanges;
|
|
20
|
+
private logJsonResult;
|
|
21
|
+
private handleAcceptOutput;
|
|
22
|
+
private promptEditAction;
|
|
23
|
+
private askChangeDescription;
|
|
13
24
|
private showFieldDiff;
|
|
14
25
|
private stripHtml;
|
|
15
26
|
private truncate;
|
|
@@ -20,5 +31,4 @@ export default class Edit extends BaseCommand {
|
|
|
20
31
|
private showUnchanged;
|
|
21
32
|
private buildDiffPreview;
|
|
22
33
|
private showChangeSummary;
|
|
23
|
-
run(): Promise<void>;
|
|
24
34
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
|
-
import { confirm, input } from '@inquirer/prompts';
|
|
2
|
+
import { confirm, input, select } from '@inquirer/prompts';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
5
|
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
6
|
-
import { loadTemplate, saveTemplate, saveYaml, } from '../../lib/yaml-config.js';
|
|
6
|
+
import { loadTemplate, resolveTemplateFilename, saveTemplate, saveYaml, } from '../../lib/yaml-config.js';
|
|
7
7
|
export default class Edit extends BaseCommand {
|
|
8
8
|
static args = {
|
|
9
9
|
id: Args.string({
|
|
@@ -22,6 +22,152 @@ export default class Edit extends BaseCommand {
|
|
|
22
22
|
description: 'Natural language description of the change',
|
|
23
23
|
}),
|
|
24
24
|
};
|
|
25
|
+
async run() {
|
|
26
|
+
const { args, flags } = await this.parse(Edit);
|
|
27
|
+
await this.ensureAuth();
|
|
28
|
+
const yamlConfig = await this.ensureYaml();
|
|
29
|
+
const emailIndex = yamlConfig.emails.findIndex((e) => e.id === args.id);
|
|
30
|
+
if (emailIndex === -1) {
|
|
31
|
+
this.error(`Email '${args.id}' not found in mailmodo.yaml.`);
|
|
32
|
+
}
|
|
33
|
+
const email = yamlConfig.emails[emailIndex];
|
|
34
|
+
const templateFilename = resolveTemplateFilename(email.id, email.style, yamlConfig.project?.emailStyle);
|
|
35
|
+
const ctx = {
|
|
36
|
+
email,
|
|
37
|
+
emailIndex,
|
|
38
|
+
templateFilename,
|
|
39
|
+
templateHtml: await loadTemplate(templateFilename),
|
|
40
|
+
yamlConfig,
|
|
41
|
+
};
|
|
42
|
+
const editFlags = {
|
|
43
|
+
json: flags.json ?? false,
|
|
44
|
+
yes: flags.yes ?? false,
|
|
45
|
+
};
|
|
46
|
+
const initialChange = flags.change ?? (await this.askChangeDescription());
|
|
47
|
+
await this.runEditStep(ctx, initialChange, editFlags);
|
|
48
|
+
}
|
|
49
|
+
async runEditStep(ctx, changeDescription, flags) {
|
|
50
|
+
const response = await this.withApiSpinner({ json: flags.json, text: ' Applying AI edits...' }, () => this.callEditApi(changeDescription, ctx.email, ctx.templateHtml));
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
this.handleApiError(response);
|
|
53
|
+
}
|
|
54
|
+
const updated = response.data;
|
|
55
|
+
if (flags.json) {
|
|
56
|
+
this.log(JSON.stringify(this.buildDiffPreview(ctx.email, updated, ctx.templateHtml), null, 2));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
this.showChangeSummary(ctx.email, updated, ctx.templateHtml);
|
|
60
|
+
}
|
|
61
|
+
if (flags.yes) {
|
|
62
|
+
await this.finalizeEdit(ctx, updated, flags);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
await this.handleUserAction(ctx, updated, changeDescription, flags);
|
|
66
|
+
}
|
|
67
|
+
async handleUserAction(ctx, updated, changeDescription, flags) {
|
|
68
|
+
this.log('');
|
|
69
|
+
const action = await this.promptEditAction();
|
|
70
|
+
if (action === 'skip') {
|
|
71
|
+
this.log('\n Changes discarded.\n');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (action === 'retry') {
|
|
75
|
+
const newChange = await this.askChangeDescription();
|
|
76
|
+
await this.runEditStep(ctx, newChange, flags);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await this.finalizeEdit(ctx, updated, flags);
|
|
80
|
+
}
|
|
81
|
+
callEditApi(changeDescription, email, templateHtml) {
|
|
82
|
+
return this.apiClient.post(API_ENDPOINTS.EDIT, {
|
|
83
|
+
changeRequest: changeDescription,
|
|
84
|
+
currentEmail: {
|
|
85
|
+
condition: email.condition,
|
|
86
|
+
goal: email.goal,
|
|
87
|
+
html: templateHtml,
|
|
88
|
+
id: email.id,
|
|
89
|
+
previewText: email.previewText,
|
|
90
|
+
subject: email.subject,
|
|
91
|
+
trigger: email.trigger,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
async finalizeEdit(ctx, updated, flags) {
|
|
96
|
+
const oldSubject = ctx.email.subject;
|
|
97
|
+
this.applyEmailChanges(ctx.email, updated);
|
|
98
|
+
await this.persistChanges(ctx, updated);
|
|
99
|
+
if (flags.json) {
|
|
100
|
+
this.logJsonResult(ctx.email, updated, oldSubject);
|
|
101
|
+
}
|
|
102
|
+
else if (flags.yes) {
|
|
103
|
+
this.log(`\n Updated ${chalk.green('mailmodo.yaml')}\n`);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
await this.handleAcceptOutput(ctx.email);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
applyEmailChanges(email, updated) {
|
|
110
|
+
if (updated.subject)
|
|
111
|
+
email.subject = updated.subject;
|
|
112
|
+
if (updated.previewText)
|
|
113
|
+
email.previewText = updated.previewText;
|
|
114
|
+
if (updated.ctaText)
|
|
115
|
+
email.ctaText = updated.ctaText;
|
|
116
|
+
}
|
|
117
|
+
async persistChanges(ctx, updated) {
|
|
118
|
+
const updatedYaml = {
|
|
119
|
+
...ctx.yamlConfig,
|
|
120
|
+
emails: [...ctx.yamlConfig.emails],
|
|
121
|
+
};
|
|
122
|
+
updatedYaml.emails[ctx.emailIndex] = ctx.email;
|
|
123
|
+
await saveYaml(updatedYaml);
|
|
124
|
+
if (updated.html) {
|
|
125
|
+
await saveTemplate(ctx.templateFilename, updated.html);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
logJsonResult(email, updated, oldSubject) {
|
|
129
|
+
this.log(JSON.stringify({
|
|
130
|
+
diff: {
|
|
131
|
+
previewText: updated.previewText && updated.previewText !== email.previewText
|
|
132
|
+
? { new: updated.previewText, old: email.previewText }
|
|
133
|
+
: undefined,
|
|
134
|
+
subject: oldSubject === email.subject
|
|
135
|
+
? undefined
|
|
136
|
+
: { new: email.subject, old: oldSubject },
|
|
137
|
+
},
|
|
138
|
+
email,
|
|
139
|
+
status: 'updated',
|
|
140
|
+
}, null, 2));
|
|
141
|
+
}
|
|
142
|
+
async handleAcceptOutput(email) {
|
|
143
|
+
this.log(`\n Updated ${chalk.green('mailmodo.yaml')}`);
|
|
144
|
+
const shouldPreview = await confirm({
|
|
145
|
+
default: true,
|
|
146
|
+
message: 'Preview the change?',
|
|
147
|
+
});
|
|
148
|
+
if (shouldPreview) {
|
|
149
|
+
await this.config.runCommand('preview', [email.id]);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
this.log(` Run: ${chalk.cyan(`mailmodo preview ${email.id}`)}\n`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async promptEditAction() {
|
|
156
|
+
return select({
|
|
157
|
+
message: 'Accept, try again, or skip?',
|
|
158
|
+
choices: [
|
|
159
|
+
{ name: 'Accept', value: 'accept' },
|
|
160
|
+
{ name: 'Try again', value: 'retry' },
|
|
161
|
+
{ name: 'Skip', value: 'skip' },
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
async askChangeDescription() {
|
|
166
|
+
return input({
|
|
167
|
+
message: 'What do you want to change?',
|
|
168
|
+
validate: (value) => value?.trim() ? true : 'Please describe the change',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
25
171
|
showFieldDiff(label, oldVal, newVal) {
|
|
26
172
|
if (!newVal || oldVal === newVal)
|
|
27
173
|
return false;
|
|
@@ -139,88 +285,4 @@ export default class Edit extends BaseCommand {
|
|
|
139
285
|
this.showSuggestedChanges(email, updated, templateHtml, changed);
|
|
140
286
|
this.showUnchanged(email, templateHtml, changed);
|
|
141
287
|
}
|
|
142
|
-
async run() {
|
|
143
|
-
const { args, flags } = await this.parse(Edit);
|
|
144
|
-
await this.ensureAuth();
|
|
145
|
-
const yamlConfig = await this.ensureYaml();
|
|
146
|
-
const emailIndex = yamlConfig.emails.findIndex((e) => e.id === args.id);
|
|
147
|
-
if (emailIndex === -1) {
|
|
148
|
-
this.error(`Email '${args.id}' not found in mailmodo.yaml.`);
|
|
149
|
-
}
|
|
150
|
-
const email = yamlConfig.emails[emailIndex];
|
|
151
|
-
const templateHtml = await loadTemplate(`${email.id}.html`);
|
|
152
|
-
let changeDescription = flags.change;
|
|
153
|
-
if (!changeDescription) {
|
|
154
|
-
changeDescription = await input({
|
|
155
|
-
message: 'What do you want to change?',
|
|
156
|
-
validate: (value) => value?.trim() ? true : 'Please describe the change',
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
const response = await this.withApiSpinner({ json: flags.json, text: ' Applying AI edits...' }, () => this.apiClient.post(API_ENDPOINTS.EDIT, {
|
|
160
|
-
changeRequest: changeDescription,
|
|
161
|
-
currentEmail: {
|
|
162
|
-
condition: email.condition,
|
|
163
|
-
goal: email.goal,
|
|
164
|
-
html: templateHtml,
|
|
165
|
-
id: email.id,
|
|
166
|
-
previewText: email.previewText,
|
|
167
|
-
subject: email.subject,
|
|
168
|
-
trigger: email.trigger,
|
|
169
|
-
},
|
|
170
|
-
}));
|
|
171
|
-
if (!response.ok) {
|
|
172
|
-
this.handleApiError(response);
|
|
173
|
-
}
|
|
174
|
-
const updated = response.data;
|
|
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);
|
|
180
|
-
}
|
|
181
|
-
if (!flags.yes) {
|
|
182
|
-
const accepted = await confirm({
|
|
183
|
-
default: true,
|
|
184
|
-
message: 'Accept changes?',
|
|
185
|
-
});
|
|
186
|
-
if (!accepted) {
|
|
187
|
-
this.log('\n Changes discarded.\n');
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
const oldSubject = email.subject;
|
|
192
|
-
const newSubject = updated.subject || email.subject;
|
|
193
|
-
if (updated.subject)
|
|
194
|
-
email.subject = updated.subject;
|
|
195
|
-
if (updated.previewText)
|
|
196
|
-
email.previewText = updated.previewText;
|
|
197
|
-
if (updated.ctaText)
|
|
198
|
-
email.ctaText = updated.ctaText;
|
|
199
|
-
const updatedYaml = {
|
|
200
|
-
...yamlConfig,
|
|
201
|
-
emails: [...yamlConfig.emails],
|
|
202
|
-
};
|
|
203
|
-
updatedYaml.emails[emailIndex] = email;
|
|
204
|
-
await saveYaml(updatedYaml);
|
|
205
|
-
if (updated.html) {
|
|
206
|
-
await saveTemplate(`${email.id}.html`, updated.html);
|
|
207
|
-
}
|
|
208
|
-
if (flags.json) {
|
|
209
|
-
this.log(JSON.stringify({
|
|
210
|
-
diff: {
|
|
211
|
-
previewText: updated.previewText && updated.previewText !== email.previewText
|
|
212
|
-
? { new: updated.previewText, old: email.previewText }
|
|
213
|
-
: undefined,
|
|
214
|
-
subject: oldSubject === newSubject
|
|
215
|
-
? undefined
|
|
216
|
-
: { new: newSubject, old: oldSubject },
|
|
217
|
-
},
|
|
218
|
-
email,
|
|
219
|
-
status: 'updated',
|
|
220
|
-
}, null, 2));
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
this.log(`\n Updated ${chalk.green('mailmodo.yaml')}`);
|
|
224
|
-
this.log(` Preview the change: ${chalk.cyan(`mailmodo preview ${email.id}`)}\n`);
|
|
225
|
-
}
|
|
226
288
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { confirm, input } from '@inquirer/prompts';
|
|
2
4
|
import chalk from 'chalk';
|
|
5
|
+
import open from 'open';
|
|
3
6
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
4
7
|
export default class Emails extends BaseCommand {
|
|
5
8
|
static description = 'List and view configured email sequences';
|
|
@@ -61,7 +64,48 @@ export default class Emails extends BaseCommand {
|
|
|
61
64
|
this.log(` ${chalk.bold('Goal:')} ${email.goal}`);
|
|
62
65
|
}
|
|
63
66
|
this.log('');
|
|
67
|
+
const openIt = await confirm({
|
|
68
|
+
default: true,
|
|
69
|
+
message: 'Open template in editor?',
|
|
70
|
+
});
|
|
71
|
+
if (openIt) {
|
|
72
|
+
await this.openTemplateInEditor(email.template);
|
|
73
|
+
}
|
|
64
74
|
}
|
|
65
75
|
}
|
|
66
76
|
}
|
|
77
|
+
async openTemplateInEditor(template) {
|
|
78
|
+
const templatePath = join(process.cwd(), template);
|
|
79
|
+
this.log(`\n Opening ${template}...\n`);
|
|
80
|
+
const editor = process.env.VISUAL || process.env.EDITOR;
|
|
81
|
+
if (editor) {
|
|
82
|
+
const child = spawn(editor, [templatePath], { stdio: 'inherit' });
|
|
83
|
+
await new Promise((resolve) => {
|
|
84
|
+
child.on('close', resolve);
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (await this.trySpawnEditor('code', templatePath))
|
|
89
|
+
return;
|
|
90
|
+
try {
|
|
91
|
+
await open(templatePath);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
this.log(` ${chalk.dim(`Could not open editor. Open the file manually: ${templatePath}`)}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
trySpawnEditor(editor, filePath) {
|
|
98
|
+
const [cmd, args] = process.platform === 'win32'
|
|
99
|
+
? ['cmd.exe', ['/c', editor, filePath]]
|
|
100
|
+
: [editor, [filePath]];
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
const child = spawn(cmd, [...args], { stdio: 'ignore' });
|
|
103
|
+
child.on('error', () => {
|
|
104
|
+
resolve(false);
|
|
105
|
+
});
|
|
106
|
+
child.on('close', (code) => {
|
|
107
|
+
resolve(code === 0);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
67
111
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
-
import { editor, input, select } from '@inquirer/prompts';
|
|
2
|
+
import { confirm, editor, input, select } from '@inquirer/prompts';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
5
|
import { API_ENDPOINTS, DEFAULT_BRAND_COLOR, DEFAULT_MONTHLY_CAP, } from '../../lib/constants.js';
|
|
6
|
-
import { saveTemplate, saveYaml, } from '../../lib/yaml-config.js';
|
|
6
|
+
import { loadYaml, saveTemplate, saveYaml, } from '../../lib/yaml-config.js';
|
|
7
7
|
function isValidUrl(value) {
|
|
8
8
|
try {
|
|
9
9
|
return Boolean(new URL(value));
|
|
@@ -42,6 +42,8 @@ export default class Init extends BaseCommand {
|
|
|
42
42
|
async run() {
|
|
43
43
|
const { flags } = await this.parse(Init);
|
|
44
44
|
await this.ensureAuth();
|
|
45
|
+
if (!(await this.confirmOverwriteIfNeeded(flags)))
|
|
46
|
+
return;
|
|
45
47
|
let productUrl = flags.url;
|
|
46
48
|
if (!productUrl) {
|
|
47
49
|
productUrl = await input({
|
|
@@ -137,7 +139,7 @@ export default class Init extends BaseCommand {
|
|
|
137
139
|
project: {
|
|
138
140
|
brandColor: analysisPayload.brand?.color || DEFAULT_BRAND_COLOR,
|
|
139
141
|
description: analysisPayload.description,
|
|
140
|
-
emailStyle: '
|
|
142
|
+
emailStyle: 'plain',
|
|
141
143
|
fromEmail: '',
|
|
142
144
|
fromName: `Team ${analysisPayload.productName}`,
|
|
143
145
|
logoUrl: analysisPayload.brand?.logoUrl || '',
|
|
@@ -177,4 +179,21 @@ export default class Init extends BaseCommand {
|
|
|
177
179
|
this.log(` Created ${chalk.green('mailmodo.yaml')} + ${chalk.green(String(emailConfigs.length))} email templates in ${chalk.green('/mailmodo')}\n`);
|
|
178
180
|
this.log(` Run ${chalk.cyan("'mailmodo emails'")} to review.\n`);
|
|
179
181
|
}
|
|
182
|
+
async confirmOverwriteIfNeeded(flags) {
|
|
183
|
+
const existing = await loadYaml();
|
|
184
|
+
if (!existing)
|
|
185
|
+
return true;
|
|
186
|
+
if (flags.yes)
|
|
187
|
+
return true;
|
|
188
|
+
this.log(`\n ${chalk.yellow('⚠')} ${chalk.bold('mailmodo.yaml already exists.')}`);
|
|
189
|
+
this.log(` Running init will overwrite your current project configuration and all email templates.\n`);
|
|
190
|
+
const proceed = await confirm({
|
|
191
|
+
default: false,
|
|
192
|
+
message: 'Overwrite existing configuration and templates?',
|
|
193
|
+
});
|
|
194
|
+
if (!proceed) {
|
|
195
|
+
this.log(`\n Init cancelled. Run ${chalk.cyan('mailmodo edit')} to modify individual emails.\n`);
|
|
196
|
+
}
|
|
197
|
+
return proceed;
|
|
198
|
+
}
|
|
180
199
|
}
|
|
@@ -4,7 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
import open from 'open';
|
|
5
5
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
6
6
|
import { API_ENDPOINTS, PREVIEW_PORT } from '../../lib/constants.js';
|
|
7
|
-
import { loadTemplate } from '../../lib/yaml-config.js';
|
|
7
|
+
import { loadTemplate, resolveTemplateFilename, } from '../../lib/yaml-config.js';
|
|
8
8
|
/* eslint-disable camelcase */
|
|
9
9
|
const SAMPLE_DATA = Object.freeze({
|
|
10
10
|
app_url: 'https://yourapp.com',
|
|
@@ -84,7 +84,7 @@ export default class Preview extends BaseCommand {
|
|
|
84
84
|
app_url: yamlConfig.project?.url || 'https://yourapp.com', // eslint-disable-line camelcase
|
|
85
85
|
product_name: yamlConfig.project?.name || 'YourApp', // eslint-disable-line camelcase
|
|
86
86
|
};
|
|
87
|
-
const templateHtml = await loadTemplate(
|
|
87
|
+
const templateHtml = await loadTemplate(resolveTemplateFilename(email.id, email.style, yamlConfig.project?.emailStyle));
|
|
88
88
|
if (flags.send) {
|
|
89
89
|
const rendered = templateHtml
|
|
90
90
|
? renderTemplate(templateHtml, sampleData)
|
|
@@ -5,7 +5,7 @@ import { existsSync } from 'node:fs';
|
|
|
5
5
|
import { readFile } from 'node:fs/promises';
|
|
6
6
|
import { resolve } from 'node:path';
|
|
7
7
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
8
|
-
import { API_ENDPOINTS
|
|
8
|
+
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
9
9
|
import { saveYaml } from '../../lib/yaml-config.js';
|
|
10
10
|
const SETTINGS_GROUPS = Object.freeze({
|
|
11
11
|
billing: ['monthly_cap'],
|
|
@@ -232,6 +232,7 @@ export default class Settings extends BaseCommand {
|
|
|
232
232
|
this.handleApiError(response);
|
|
233
233
|
}
|
|
234
234
|
const records = response.data?.dnsRecords || [];
|
|
235
|
+
const dnsGuideUrl = response.data?.dnsGuideUrl;
|
|
235
236
|
yamlConfig.project.domain = newDomain;
|
|
236
237
|
yamlConfig.project.fromEmail = newFromEmail;
|
|
237
238
|
yamlConfig.project.address = newAddress;
|
|
@@ -246,7 +247,8 @@ export default class Settings extends BaseCommand {
|
|
|
246
247
|
}
|
|
247
248
|
this.log(` Run ${chalk.cyan("'mailmodo domain --verify'")} once records are added.`);
|
|
248
249
|
this.log(` Emails will not send until the new domain is verified.`);
|
|
249
|
-
|
|
250
|
+
if (dnsGuideUrl)
|
|
251
|
+
this.log(` Help: ${chalk.cyan(dnsGuideUrl)}\n`);
|
|
250
252
|
}
|
|
251
253
|
recordLabel(index) {
|
|
252
254
|
const labels = ['DKIM', 'DMARC', 'Return Path'];
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface ApiRequestDebugInfo {
|
|
|
12
12
|
responseSummary?: string;
|
|
13
13
|
}
|
|
14
14
|
export interface ApiResponse<T = Record<string, unknown>> {
|
|
15
|
+
/** Warning string from the API when the user's card is expiring or has issues. */
|
|
16
|
+
addCardWarning?: string;
|
|
15
17
|
data: T;
|
|
16
18
|
/** Populated for tracing; especially useful when `ok` is false. */
|
|
17
19
|
debug?: ApiRequestDebugInfo;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -14,6 +14,12 @@ import { fetchFileNoAuth, fetchFileWithBearerAuth, } from './fetch-file.js';
|
|
|
14
14
|
* indicating the API may not be available, rather than a raw stack trace.
|
|
15
15
|
*/
|
|
16
16
|
const RESPONSE_BODY_DEBUG_MAX = 800;
|
|
17
|
+
function extractAddCardWarning(data) {
|
|
18
|
+
const raw = data;
|
|
19
|
+
return typeof raw.addCardWarning === 'string'
|
|
20
|
+
? raw.addCardWarning
|
|
21
|
+
: undefined;
|
|
22
|
+
}
|
|
17
23
|
function summarizeResponseBody(data) {
|
|
18
24
|
if (data === null || data === undefined)
|
|
19
25
|
return undefined;
|
|
@@ -90,7 +96,13 @@ export class ApiClient {
|
|
|
90
96
|
status: response.status,
|
|
91
97
|
};
|
|
92
98
|
}
|
|
93
|
-
|
|
99
|
+
const addCardWarning = extractAddCardWarning(data);
|
|
100
|
+
return {
|
|
101
|
+
...(addCardWarning === undefined ? {} : { addCardWarning }),
|
|
102
|
+
data: data,
|
|
103
|
+
ok: true,
|
|
104
|
+
status: response.status,
|
|
105
|
+
};
|
|
94
106
|
}
|
|
95
107
|
catch (error) {
|
|
96
108
|
const err = error;
|
|
@@ -164,7 +176,13 @@ export class ApiClient {
|
|
|
164
176
|
status: response.status,
|
|
165
177
|
};
|
|
166
178
|
}
|
|
167
|
-
|
|
179
|
+
const addCardWarning = extractAddCardWarning(data);
|
|
180
|
+
return {
|
|
181
|
+
...(addCardWarning === undefined ? {} : { addCardWarning }),
|
|
182
|
+
data: data,
|
|
183
|
+
ok: true,
|
|
184
|
+
status: response.status,
|
|
185
|
+
};
|
|
168
186
|
}
|
|
169
187
|
catch (error) {
|
|
170
188
|
const err = error;
|
package/dist/lib/base-command.js
CHANGED
|
@@ -63,12 +63,12 @@ export class BaseCommand extends Command {
|
|
|
63
63
|
color: 'cyan',
|
|
64
64
|
text: options.text,
|
|
65
65
|
}).start();
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
spinner.stop();
|
|
66
|
+
const result = await work().finally(() => spinner.stop());
|
|
67
|
+
const warning = result.addCardWarning;
|
|
68
|
+
if (typeof warning === 'string' && warning) {
|
|
69
|
+
this.warn(warning);
|
|
71
70
|
}
|
|
71
|
+
return result;
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
74
|
* Loads and returns the mailmodo.yaml configuration from the current directory.
|
|
@@ -68,3 +68,4 @@ export declare function saveTemplate(filename: string, html: string, cwd?: strin
|
|
|
68
68
|
* or null if the file doesn't exist or can't be read.
|
|
69
69
|
*/
|
|
70
70
|
export declare function loadTemplate(filename: string, cwd?: string): Promise<null | string>;
|
|
71
|
+
export declare function resolveTemplateFilename(emailId: string, emailStyle?: 'branded' | 'plain', projectStyle?: 'branded' | 'plain'): string;
|
package/dist/lib/yaml-config.js
CHANGED
|
@@ -72,3 +72,7 @@ export async function loadTemplate(filename, cwd) {
|
|
|
72
72
|
return null;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
export function resolveTemplateFilename(emailId, emailStyle, projectStyle) {
|
|
76
|
+
const style = emailStyle ?? projectStyle ?? 'branded';
|
|
77
|
+
return style === 'plain' ? `${emailId}_plain.html` : `${emailId}.html`;
|
|
78
|
+
}
|
package/oclif.manifest.json
CHANGED