@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.
@@ -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(` Opening in browser...\n`);
72
+ this.log(` ${INFO.BROWSER_OPENING}\n`);
72
73
  }
73
74
  catch {
74
- this.log(` ${chalk.dim('Could not open browser. Visit the URL above manually.')}\n`);
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(`Sending domain not registered. Run: ${chalk.cyan('mailmodo domain')}`);
54
+ this.error(ERRORS.DOMAIN_NOT_REGISTERED);
54
55
  }
55
56
  if (response.data.error === 'senderDomainNotVerified') {
56
- this.error(`Sending domain not verified. Run: ${chalk.cyan('mailmodo domain --verify')}`);
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
- return { ...this.mapEmailToPayload(email), html, plainHtml: html };
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 Sequences saved but ${chalk.yellow('NOT deployed')}.`);
152
+ this.log(`\n ${INFO.SEQUENCES_NOT_DEPLOYED}`);
151
153
  this.log(` Emails will not send until your domain is verified.`);
152
- this.log(` When ready, run: ${chalk.cyan('mailmodo domain')}`);
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(` ${'─'.repeat(53)}`);
197
+ this.log(` ${SEPARATOR}`);
197
198
  this.log(` ${chalk.bold('ADD THIS TO YOUR APP (one-time only):')}`);
198
- this.log(` ${'─'.repeat(53)}\n`);
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(` ${'─'.repeat(53)}\n`);
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 Sequences saved but ${chalk.yellow('NOT deployed')}.`);
245
- this.log(` When ready, run: ${chalk.cyan('mailmodo domain')}`);
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 ${'─'.repeat(53)}`);
259
+ this.log(`\n ${SEPARATOR}`);
260
260
  this.log(` ${chalk.bold('DOMAIN SETUP')}`);
261
- this.log(` ${'─'.repeat(53)}\n`);
261
+ this.log(` ${SEPARATOR}\n`);
262
262
  const domain = await input({
263
- message: 'What domain will you send from?',
264
- validate: (v) => (v?.trim() ? true : 'Domain is required'),
263
+ message: PROMPTS.DOMAIN,
264
+ validate: (v) => (v?.trim() ? true : VALIDATION.DOMAIN_REQUIRED),
265
265
  });
266
266
  const senderEmail = await input({
267
- message: 'Sender email address:',
268
- validate: (v) => (v?.includes('@') ? true : 'Please enter a valid email'),
267
+ message: PROMPTS.SENDER_EMAIL,
268
+ validate: (v) => (v?.includes('@') ? true : VALIDATION.EMAIL_INVALID),
269
269
  });
270
270
  const address = await input({
271
- message: 'Business address (required by law for email footers):',
272
- validate: (v) => (v?.trim() ? true : 'Address is required'),
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(` DNS changes take 5–30 minutes to propagate.`);
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 ${chalk.yellow('Some records failed.')} Fix them and run ${chalk.cyan('mailmodo domain --verify')}.`);
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
  }
@@ -23,6 +23,4 @@ export default class Domain extends BaseCommand {
23
23
  * bounce rate, and spam complaint rate.
24
24
  */
25
25
  private showDomainStatus;
26
- private collectDomainInputs;
27
- private recordLabel;
28
26
  }
@@ -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 { saveConfig } from '../../lib/config.js';
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
- const config = await this.ensureAuth();
27
+ await this.ensureAuth();
29
28
  if (flags.verify) {
30
- await this.verifyDomain(flags.json, config);
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.showDomainStatus(flags.json, config);
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, config);
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, config) {
52
+ async setupDomain(flags) {
44
53
  const yamlConfig = await this.ensureYaml();
45
- this.log(`\n ${'─'.repeat(53)}`);
54
+ this.log(`\n ${SEPARATOR}`);
46
55
  this.log(` ${chalk.bold('DOMAIN SETUP')}`);
47
- this.log(` ${'─'.repeat(53)}\n`);
48
- const { domain, senderEmail, fromName, replyTo, address } = await this.collectDomainInputs(flags.yes, yamlConfig);
49
- const apiPayload = {
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: records, domain }, null, 2));
60
+ this.log(JSON.stringify({ dnsRecords, domain: inputs.domain }, null, 2));
75
61
  return;
76
62
  }
77
- this.log(`\n Add these ${records.length} DNS records to your domain provider:\n`);
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: "Press Enter once you've added the records, or 'skip'.",
67
+ message: PROMPTS.ENTER_AFTER_RECORDS,
91
68
  });
92
69
  if (action.toLowerCase() !== 'skip') {
93
- await this.verifyDomain(false, { ...config, domain });
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, config) {
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 ${chalk.yellow('Some records failed.')}`);
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 Fix the records and run ${chalk.cyan('mailmodo domain --verify')} again.`);
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, config) {
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
  }
@@ -7,4 +7,6 @@ export default class Emails extends BaseCommand {
7
7
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
8
  };
9
9
  run(): Promise<void>;
10
+ private openTemplateInEditor;
11
+ private trySpawnEditor;
10
12
  }