@mailmodo/cli 0.0.20 → 0.0.21-beta.pr24.38

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