@mailmodo/cli 0.0.30 → 0.0.31-beta.pr33.54
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/billing/index.js +3 -2
- package/dist/commands/deploy/index.js +23 -22
- package/dist/commands/domain/index.d.ts +0 -2
- package/dist/commands/domain/index.js +28 -102
- 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 +50 -1
- package/dist/commands/init/index.js +1 -1
- package/dist/commands/login/index.js +3 -2
- package/dist/commands/preview/index.d.ts +6 -2
- package/dist/commands/preview/index.js +41 -14
- package/dist/commands/settings/index.d.ts +0 -7
- package/dist/commands/settings/index.js +8 -51
- package/dist/lib/base-command.d.ts +26 -0
- package/dist/lib/base-command.js +89 -6
- package/dist/lib/config.d.ts +0 -1
- package/dist/lib/messages.d.ts +36 -0
- package/dist/lib/messages.js +39 -0
- package/dist/lib/yaml-config.d.ts +2 -0
- package/dist/lib/yaml-config.js +7 -0
- package/oclif.manifest.json +82 -82
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import open from 'open';
|
|
4
4
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
5
|
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
6
|
+
import { INFO } from '../../lib/messages.js';
|
|
6
7
|
export default class Billing extends BaseCommand {
|
|
7
8
|
static description = 'View billing status, purchase blocks, set cap, or add a payment method';
|
|
8
9
|
static examples = [
|
|
@@ -68,10 +69,10 @@ export default class Billing extends BaseCommand {
|
|
|
68
69
|
this.log(` ${chalk.dim(checkoutUrl)}\n`);
|
|
69
70
|
try {
|
|
70
71
|
await open(checkoutUrl);
|
|
71
|
-
this.log(`
|
|
72
|
+
this.log(` ${INFO.BROWSER_OPENING}\n`);
|
|
72
73
|
}
|
|
73
74
|
catch {
|
|
74
|
-
this.log(` ${
|
|
75
|
+
this.log(` ${INFO.BROWSER_OPEN_FAILED}\n`);
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
async showStatus(jsonOutput) {
|
|
@@ -2,6 +2,7 @@ import { confirm, input } from '@inquirer/prompts';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
4
4
|
import { API_ENDPOINTS, DEFAULT_BRAND_COLOR, DEFAULT_MONTHLY_CAP, } from '../../lib/constants.js';
|
|
5
|
+
import { ERRORS, INFO, PROMPTS, SEPARATOR, VALIDATION, } from '../../lib/messages.js';
|
|
5
6
|
import { loadTemplate, saveYaml, } from '../../lib/yaml-config.js';
|
|
6
7
|
export default class Deploy extends BaseCommand {
|
|
7
8
|
static description = 'Deploy email sequences and verify sending domain';
|
|
@@ -50,10 +51,10 @@ export default class Deploy extends BaseCommand {
|
|
|
50
51
|
const response = await this.withApiSpinner({ json: flags.json, text: ' Validating sequence...' }, () => this.apiClient.post(API_ENDPOINTS.SEQUENCES_VALIDATE, payload));
|
|
51
52
|
if (!response.ok) {
|
|
52
53
|
if (response.data.error === 'senderDomainNotFound') {
|
|
53
|
-
this.error(
|
|
54
|
+
this.error(ERRORS.DOMAIN_NOT_REGISTERED);
|
|
54
55
|
}
|
|
55
56
|
if (response.data.error === 'senderDomainNotVerified') {
|
|
56
|
-
this.error(
|
|
57
|
+
this.error(ERRORS.DOMAIN_NOT_VERIFIED);
|
|
57
58
|
}
|
|
58
59
|
this.handleApiError(response);
|
|
59
60
|
}
|
|
@@ -62,7 +63,8 @@ export default class Deploy extends BaseCommand {
|
|
|
62
63
|
async buildDeployPayload(yamlConfig) {
|
|
63
64
|
const emailsWithHtml = await Promise.all(yamlConfig.emails.map(async (email) => {
|
|
64
65
|
const html = (await loadTemplate(`${email.id}.html`)) || '';
|
|
65
|
-
|
|
66
|
+
const plainHtml = (await loadTemplate(`${email.id}_plain.html`)) || html;
|
|
67
|
+
return { ...this.mapEmailToPayload(email), html, plainHtml };
|
|
66
68
|
}));
|
|
67
69
|
return {
|
|
68
70
|
...this.buildProjectPayload(yamlConfig.project),
|
|
@@ -147,10 +149,9 @@ export default class Deploy extends BaseCommand {
|
|
|
147
149
|
message: 'Set up your sending domain now?',
|
|
148
150
|
});
|
|
149
151
|
if (!setupNow) {
|
|
150
|
-
this.log(`\n
|
|
152
|
+
this.log(`\n ${INFO.SEQUENCES_NOT_DEPLOYED}`);
|
|
151
153
|
this.log(` Emails will not send until your domain is verified.`);
|
|
152
|
-
this.log(`
|
|
153
|
-
this.log(` Then: ${chalk.cyan('mailmodo deploy')}\n`);
|
|
154
|
+
this.log(` ${INFO.DOMAIN_NOT_DEPLOYED_HINT}\n`);
|
|
154
155
|
return false;
|
|
155
156
|
}
|
|
156
157
|
}
|
|
@@ -193,9 +194,9 @@ export default class Deploy extends BaseCommand {
|
|
|
193
194
|
}
|
|
194
195
|
logDeploySuccessInstructions(sdkSnippet) {
|
|
195
196
|
this.log(` ${chalk.green('Deployed.')} Emails are live.\n`);
|
|
196
|
-
this.log(` ${
|
|
197
|
+
this.log(` ${SEPARATOR}`);
|
|
197
198
|
this.log(` ${chalk.bold('ADD THIS TO YOUR APP (one-time only):')}`);
|
|
198
|
-
this.log(` ${
|
|
199
|
+
this.log(` ${SEPARATOR}\n`);
|
|
199
200
|
this.log(` ${chalk.cyan(sdkSnippet.install ?? 'npm install @mailmodo/sdk')}\n`);
|
|
200
201
|
this.log(` ${chalk.dim("import { track, identify } from '@mailmodo/sdk'")}\n`);
|
|
201
202
|
if (sdkSnippet.examples) {
|
|
@@ -216,7 +217,7 @@ export default class Deploy extends BaseCommand {
|
|
|
216
217
|
if (identifyCalls.length > 0)
|
|
217
218
|
this.log('');
|
|
218
219
|
this.log(` Full SDK docs: ${chalk.cyan('mailmodo.com/docs/sdk')}\n`);
|
|
219
|
-
this.log(` ${
|
|
220
|
+
this.log(` ${SEPARATOR}\n`);
|
|
220
221
|
}
|
|
221
222
|
async runDomainSetup(yamlConfig, flags) {
|
|
222
223
|
const { address, domain, senderEmail } = await this.collectDomainInputs(yamlConfig, flags);
|
|
@@ -241,9 +242,8 @@ export default class Deploy extends BaseCommand {
|
|
|
241
242
|
message: "Press Enter once you've added the records, or 'skip' to do this later.",
|
|
242
243
|
});
|
|
243
244
|
if (action.toLowerCase() === 'skip') {
|
|
244
|
-
this.log(`\n
|
|
245
|
-
this.log(`
|
|
246
|
-
this.log(` Then: ${chalk.cyan('mailmodo deploy')}\n`);
|
|
245
|
+
this.log(`\n ${INFO.SEQUENCES_NOT_DEPLOYED}`);
|
|
246
|
+
this.log(` ${INFO.DOMAIN_NOT_DEPLOYED_HINT}\n`);
|
|
247
247
|
return false;
|
|
248
248
|
}
|
|
249
249
|
return this.verifyDomain(flags.json, domain);
|
|
@@ -256,20 +256,20 @@ export default class Deploy extends BaseCommand {
|
|
|
256
256
|
senderEmail: yamlConfig.project?.fromEmail || '',
|
|
257
257
|
};
|
|
258
258
|
}
|
|
259
|
-
this.log(`\n ${
|
|
259
|
+
this.log(`\n ${SEPARATOR}`);
|
|
260
260
|
this.log(` ${chalk.bold('DOMAIN SETUP')}`);
|
|
261
|
-
this.log(` ${
|
|
261
|
+
this.log(` ${SEPARATOR}\n`);
|
|
262
262
|
const domain = await input({
|
|
263
|
-
message:
|
|
264
|
-
validate: (v) => (v?.trim() ? true :
|
|
263
|
+
message: PROMPTS.DOMAIN,
|
|
264
|
+
validate: (v) => (v?.trim() ? true : VALIDATION.DOMAIN_REQUIRED),
|
|
265
265
|
});
|
|
266
266
|
const senderEmail = await input({
|
|
267
|
-
message:
|
|
268
|
-
validate: (v) => (v?.includes('@') ? true :
|
|
267
|
+
message: PROMPTS.SENDER_EMAIL,
|
|
268
|
+
validate: (v) => (v?.includes('@') ? true : VALIDATION.EMAIL_INVALID),
|
|
269
269
|
});
|
|
270
270
|
const address = await input({
|
|
271
|
-
message:
|
|
272
|
-
validate: (v) => (v?.trim() ? true :
|
|
271
|
+
message: PROMPTS.BUSINESS_ADDRESS,
|
|
272
|
+
validate: (v) => (v?.trim() ? true : VALIDATION.ADDRESS_REQUIRED),
|
|
273
273
|
});
|
|
274
274
|
return { address, domain, senderEmail };
|
|
275
275
|
}
|
|
@@ -283,7 +283,7 @@ export default class Deploy extends BaseCommand {
|
|
|
283
283
|
this.log(` Host: ${record.host}`);
|
|
284
284
|
this.log(` Value: ${record.value}\n`);
|
|
285
285
|
}
|
|
286
|
-
this.log(`
|
|
286
|
+
this.log(` ${INFO.DNS_PROPAGATION}`);
|
|
287
287
|
if (dnsGuideUrl)
|
|
288
288
|
this.log(` Full guide: ${chalk.cyan(dnsGuideUrl)}\n`);
|
|
289
289
|
}
|
|
@@ -304,7 +304,8 @@ export default class Deploy extends BaseCommand {
|
|
|
304
304
|
this.log(`\n ${chalk.green('Domain verified.')} Continuing deploy...\n`);
|
|
305
305
|
}
|
|
306
306
|
else {
|
|
307
|
-
this.log(`\n ${
|
|
307
|
+
this.log(`\n ${INFO.DNS_RECORDS_FAILED}`);
|
|
308
|
+
this.log(`\n ${INFO.DNS_FIX_AND_VERIFY}`);
|
|
308
309
|
if (dnsGuideUrl)
|
|
309
310
|
this.log(` Help: ${chalk.cyan(dnsGuideUrl)}\n`);
|
|
310
311
|
}
|
|
@@ -3,8 +3,7 @@ import { input } 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 {
|
|
7
|
-
import { saveYaml } from '../../lib/yaml-config.js';
|
|
6
|
+
import { ERRORS, INFO, PROMPTS, SEPARATOR } from '../../lib/messages.js';
|
|
8
7
|
export default class Domain extends BaseCommand {
|
|
9
8
|
static description = 'Set up and verify your sending domain';
|
|
10
9
|
static examples = [
|
|
@@ -25,83 +24,57 @@ export default class Domain extends BaseCommand {
|
|
|
25
24
|
};
|
|
26
25
|
async run() {
|
|
27
26
|
const { flags } = await this.parse(Domain);
|
|
28
|
-
|
|
27
|
+
await this.ensureAuth();
|
|
29
28
|
if (flags.verify) {
|
|
30
|
-
await this.
|
|
29
|
+
const yamlConfig = await this.ensureYaml();
|
|
30
|
+
const domain = yamlConfig.project?.domain;
|
|
31
|
+
if (!domain) {
|
|
32
|
+
this.error(ERRORS.DOMAIN_NOT_CONFIGURED);
|
|
33
|
+
}
|
|
34
|
+
await this.verifyDomain(flags.json, domain);
|
|
31
35
|
return;
|
|
32
36
|
}
|
|
33
37
|
if (flags.status) {
|
|
34
|
-
await this.
|
|
38
|
+
const yamlConfig = await this.ensureYaml();
|
|
39
|
+
const domain = yamlConfig.project?.domain;
|
|
40
|
+
if (!domain) {
|
|
41
|
+
this.error(ERRORS.DOMAIN_NOT_CONFIGURED);
|
|
42
|
+
}
|
|
43
|
+
await this.showDomainStatus(flags.json, domain);
|
|
35
44
|
return;
|
|
36
45
|
}
|
|
37
|
-
await this.setupDomain(flags
|
|
46
|
+
await this.setupDomain(flags);
|
|
38
47
|
}
|
|
39
48
|
/**
|
|
40
49
|
* Interactive domain setup: collects domain, sender email, and business address,
|
|
41
50
|
* then calls the API to retrieve the required DNS records.
|
|
42
51
|
*/
|
|
43
|
-
async setupDomain(flags
|
|
52
|
+
async setupDomain(flags) {
|
|
44
53
|
const yamlConfig = await this.ensureYaml();
|
|
45
|
-
this.log(`\n ${
|
|
54
|
+
this.log(`\n ${SEPARATOR}`);
|
|
46
55
|
this.log(` ${chalk.bold('DOMAIN SETUP')}`);
|
|
47
|
-
this.log(` ${
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
address,
|
|
51
|
-
domain,
|
|
52
|
-
fromEmail: senderEmail,
|
|
53
|
-
};
|
|
54
|
-
if (fromName)
|
|
55
|
-
apiPayload.fromName = fromName;
|
|
56
|
-
if (replyTo)
|
|
57
|
-
apiPayload.replyTo = replyTo;
|
|
58
|
-
const response = await this.withApiSpinner({ json: flags.json, text: ' Configuring domain...' }, () => this.apiClient.post(API_ENDPOINTS.DOMAIN, apiPayload));
|
|
59
|
-
if (!response.ok) {
|
|
60
|
-
this.handleApiError(response);
|
|
61
|
-
}
|
|
62
|
-
yamlConfig.project.domain = domain;
|
|
63
|
-
yamlConfig.project.fromEmail = senderEmail;
|
|
64
|
-
yamlConfig.project.address = address;
|
|
65
|
-
if (fromName)
|
|
66
|
-
yamlConfig.project.fromName = fromName;
|
|
67
|
-
if (replyTo)
|
|
68
|
-
yamlConfig.project.replyTo = replyTo;
|
|
69
|
-
await saveYaml(yamlConfig);
|
|
70
|
-
await saveConfig({ ...config, domain });
|
|
71
|
-
const records = response.data?.dnsRecords || [];
|
|
72
|
-
const guideUrl = response.data?.dnsGuideUrl;
|
|
56
|
+
this.log(` ${SEPARATOR}\n`);
|
|
57
|
+
const inputs = await this.collectDomainSetupInputs(yamlConfig, flags.yes);
|
|
58
|
+
const { dnsRecords, dnsGuideUrl } = await this.registerDomain(yamlConfig, inputs, flags.json);
|
|
73
59
|
if (flags.json) {
|
|
74
|
-
this.log(JSON.stringify({ dnsRecords
|
|
60
|
+
this.log(JSON.stringify({ dnsRecords, domain: inputs.domain }, null, 2));
|
|
75
61
|
return;
|
|
76
62
|
}
|
|
77
|
-
this.
|
|
78
|
-
for (const [i, record] of records.entries()) {
|
|
79
|
-
this.log(` ${chalk.bold(`RECORD ${i + 1} — ${this.recordLabel(i)}`)}`);
|
|
80
|
-
this.log(` Type: ${record.type}`);
|
|
81
|
-
this.log(` Host: ${record.host}`);
|
|
82
|
-
this.log(` Value: ${record.value}\n`);
|
|
83
|
-
}
|
|
84
|
-
this.log(` DNS changes take 5–30 minutes to propagate.`);
|
|
85
|
-
if (guideUrl)
|
|
86
|
-
this.log(` Full guide: ${chalk.cyan(guideUrl)}\n`);
|
|
63
|
+
this.logDnsRecords(dnsRecords, dnsGuideUrl, flags.json);
|
|
87
64
|
if (!flags.yes) {
|
|
88
65
|
const action = await input({
|
|
89
66
|
default: '',
|
|
90
|
-
message:
|
|
67
|
+
message: PROMPTS.ENTER_AFTER_RECORDS,
|
|
91
68
|
});
|
|
92
69
|
if (action.toLowerCase() !== 'skip') {
|
|
93
|
-
await this.verifyDomain(false,
|
|
70
|
+
await this.verifyDomain(false, inputs.domain);
|
|
94
71
|
}
|
|
95
72
|
}
|
|
96
73
|
}
|
|
97
74
|
/**
|
|
98
75
|
* Calls the domain verification API and displays pass/fail for each DNS record.
|
|
99
76
|
*/
|
|
100
|
-
async verifyDomain(jsonOutput,
|
|
101
|
-
if (!config.domain) {
|
|
102
|
-
this.error(`No domain configured. Run ${chalk.cyan('mailmodo domain')} to set up your sending domain.`);
|
|
103
|
-
}
|
|
104
|
-
const domain = config.domain;
|
|
77
|
+
async verifyDomain(jsonOutput, domain) {
|
|
105
78
|
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Checking DNS...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY, {
|
|
106
79
|
domain,
|
|
107
80
|
}));
|
|
@@ -121,7 +94,7 @@ export default class Domain extends BaseCommand {
|
|
|
121
94
|
this.log(`\n ${chalk.green('✓')} Domain verified.\n`);
|
|
122
95
|
}
|
|
123
96
|
else {
|
|
124
|
-
this.log(`\n ${
|
|
97
|
+
this.log(`\n ${INFO.DNS_RECORDS_FAILED}`);
|
|
125
98
|
if (!dkim) {
|
|
126
99
|
this.log(`\n DKIM common mistakes:`);
|
|
127
100
|
this.log(` - Using TXT instead of CNAME record type`);
|
|
@@ -133,7 +106,7 @@ export default class Domain extends BaseCommand {
|
|
|
133
106
|
this.log(` - Missing or incorrect CNAME for mm-bounce subdomain`);
|
|
134
107
|
this.log(` - Cloudflare: proxy must be OFF (grey cloud, not orange)`);
|
|
135
108
|
}
|
|
136
|
-
this.log(`\n
|
|
109
|
+
this.log(`\n ${INFO.DNS_FIX_AND_VERIFY}`);
|
|
137
110
|
if (dnsGuideUrl)
|
|
138
111
|
this.log(` Help: ${chalk.cyan(dnsGuideUrl)}\n`);
|
|
139
112
|
}
|
|
@@ -142,11 +115,7 @@ export default class Domain extends BaseCommand {
|
|
|
142
115
|
* Displays domain health metrics including verification status,
|
|
143
116
|
* bounce rate, and spam complaint rate.
|
|
144
117
|
*/
|
|
145
|
-
async showDomainStatus(jsonOutput,
|
|
146
|
-
if (!config.domain) {
|
|
147
|
-
this.error(`No domain configured. Run ${chalk.cyan('mailmodo domain')} to set up your sending domain.`);
|
|
148
|
-
}
|
|
149
|
-
const domain = config.domain;
|
|
118
|
+
async showDomainStatus(jsonOutput, domain) {
|
|
150
119
|
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Loading domain status...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_STATUS, {
|
|
151
120
|
domain,
|
|
152
121
|
}));
|
|
@@ -163,47 +132,4 @@ export default class Domain extends BaseCommand {
|
|
|
163
132
|
this.log(` Bounce rate: ${data.bounceRate ?? 'N/A'}%`);
|
|
164
133
|
this.log(` Spam rate: ${data.spamRate ?? 'N/A'}%\n`);
|
|
165
134
|
}
|
|
166
|
-
async collectDomainInputs(skipPrompts, yamlConfig) {
|
|
167
|
-
if (skipPrompts) {
|
|
168
|
-
const domain = yamlConfig.project?.domain || '';
|
|
169
|
-
if (!domain) {
|
|
170
|
-
this.error('Domain is required. Set it in mailmodo.yaml or use interactive mode.');
|
|
171
|
-
}
|
|
172
|
-
return {
|
|
173
|
-
address: yamlConfig.project?.address || '',
|
|
174
|
-
domain,
|
|
175
|
-
fromName: yamlConfig.project?.fromName || '',
|
|
176
|
-
replyTo: yamlConfig.project?.replyTo || '',
|
|
177
|
-
senderEmail: yamlConfig.project?.fromEmail || '',
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
const domain = await input({
|
|
181
|
-
default: yamlConfig.project?.domain,
|
|
182
|
-
message: 'What domain will you send from?',
|
|
183
|
-
validate: (v) => (v?.trim() ? true : 'Domain is required'),
|
|
184
|
-
});
|
|
185
|
-
const senderEmail = await input({
|
|
186
|
-
default: yamlConfig.project?.fromEmail,
|
|
187
|
-
message: 'Sender email address:',
|
|
188
|
-
validate: (v) => (v?.includes('@') ? true : 'Please enter a valid email'),
|
|
189
|
-
});
|
|
190
|
-
const fromName = await input({
|
|
191
|
-
default: yamlConfig.project?.fromName || '',
|
|
192
|
-
message: 'Display name (optional, shown as sender name):',
|
|
193
|
-
});
|
|
194
|
-
const replyTo = await input({
|
|
195
|
-
default: yamlConfig.project?.replyTo || '',
|
|
196
|
-
message: 'Reply-to address (optional, press Enter to use sender email):',
|
|
197
|
-
});
|
|
198
|
-
const address = await input({
|
|
199
|
-
default: yamlConfig.project?.address,
|
|
200
|
-
message: 'Business address (required by law):',
|
|
201
|
-
validate: (v) => (v?.trim() ? true : 'Address is required'),
|
|
202
|
-
});
|
|
203
|
-
return { address, domain, fromName, replyTo, senderEmail };
|
|
204
|
-
}
|
|
205
|
-
recordLabel(index) {
|
|
206
|
-
const labels = ['DKIM', 'DMARC', 'Return Path'];
|
|
207
|
-
return labels[index] || `Record ${index + 1}`;
|
|
208
|
-
}
|
|
209
135
|
}
|
|
@@ -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, getTemplateFilename, 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 = getTemplateFilename(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?.trim() || (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
|
}
|