@mailmodo/cli 0.0.19-beta.pr21.31 → 0.0.20-beta.pr22.32

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.
@@ -16,6 +16,9 @@ export default class Deploy extends BaseCommand {
16
16
  run(): Promise<void>;
17
17
  private buildDeployPayload;
18
18
  private mapEmailToPayload;
19
+ private buildBrandSection;
20
+ private buildProductSection;
21
+ private buildSenderSection;
19
22
  private buildProjectPayload;
20
23
  private confirmDeploy;
21
24
  private ensureDomainReady;
@@ -77,30 +77,39 @@ export default class Deploy extends BaseCommand {
77
77
  trigger: email.trigger,
78
78
  };
79
79
  }
80
+ buildBrandSection(project) {
81
+ return {
82
+ colors: [project?.brandColor || DEFAULT_BRAND_COLOR],
83
+ logoUrl: project?.logoUrl || '',
84
+ };
85
+ }
86
+ buildProductSection(project) {
87
+ return {
88
+ businessType: project?.type || '',
89
+ description: '',
90
+ pricingModel: '',
91
+ productName: project?.name || '',
92
+ saasModel: '',
93
+ targetUser: '',
94
+ url: project?.url || '',
95
+ };
96
+ }
97
+ buildSenderSection(project) {
98
+ return {
99
+ address: project?.address || '',
100
+ domain: project?.domain || '',
101
+ fromEmail: project?.fromEmail || '',
102
+ fromName: project?.fromName || '',
103
+ replyTo: project?.replyTo || project?.fromEmail || '',
104
+ };
105
+ }
80
106
  buildProjectPayload(project) {
81
107
  return {
82
- brand: {
83
- colors: [project?.brandColor || DEFAULT_BRAND_COLOR],
84
- logoUrl: project?.logoUrl || '',
85
- },
108
+ brand: this.buildBrandSection(project),
86
109
  emailStyle: project?.emailStyle || 'branded',
87
110
  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
- },
111
+ product: this.buildProductSection(project),
112
+ senderDetails: this.buildSenderSection(project),
104
113
  webhookUrl: project?.webhookUrl || '',
105
114
  };
106
115
  }
@@ -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,6 +188,8 @@ 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)
@@ -91,6 +206,9 @@ export default class Edit extends BaseCommand {
91
206
  if (flags.json) {
92
207
  this.log(JSON.stringify({
93
208
  diff: {
209
+ previewText: updated.previewText && updated.previewText !== email.previewText
210
+ ? { new: updated.previewText, old: email.previewText }
211
+ : undefined,
94
212
  subject: oldSubject === newSubject
95
213
  ? undefined
96
214
  : { new: newSubject, old: oldSubject },
@@ -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,
@@ -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',
@@ -114,6 +114,45 @@
114
114
  "index.js"
115
115
  ]
116
116
  },
117
+ "deploy": {
118
+ "aliases": [],
119
+ "args": {},
120
+ "description": "Deploy email sequences and verify sending domain",
121
+ "examples": [
122
+ "<%= config.bin %> deploy",
123
+ "<%= config.bin %> deploy --yes"
124
+ ],
125
+ "flags": {
126
+ "json": {
127
+ "description": "Output as JSON",
128
+ "name": "json",
129
+ "allowNo": false,
130
+ "type": "boolean"
131
+ },
132
+ "yes": {
133
+ "char": "y",
134
+ "description": "Skip confirmation prompts",
135
+ "name": "yes",
136
+ "allowNo": false,
137
+ "type": "boolean"
138
+ }
139
+ },
140
+ "hasDynamicHelp": false,
141
+ "hiddenAliases": [],
142
+ "id": "deploy",
143
+ "pluginAlias": "@mailmodo/cli",
144
+ "pluginName": "@mailmodo/cli",
145
+ "pluginType": "core",
146
+ "strict": true,
147
+ "enableJsonFlag": false,
148
+ "isESM": true,
149
+ "relativePath": [
150
+ "dist",
151
+ "commands",
152
+ "deploy",
153
+ "index.js"
154
+ ]
155
+ },
117
156
  "domain": {
118
157
  "aliases": [],
119
158
  "args": {},
@@ -218,45 +257,6 @@
218
257
  "index.js"
219
258
  ]
220
259
  },
221
- "deploy": {
222
- "aliases": [],
223
- "args": {},
224
- "description": "Deploy email sequences and verify sending domain",
225
- "examples": [
226
- "<%= config.bin %> deploy",
227
- "<%= config.bin %> deploy --yes"
228
- ],
229
- "flags": {
230
- "json": {
231
- "description": "Output as JSON",
232
- "name": "json",
233
- "allowNo": false,
234
- "type": "boolean"
235
- },
236
- "yes": {
237
- "char": "y",
238
- "description": "Skip confirmation prompts",
239
- "name": "yes",
240
- "allowNo": false,
241
- "type": "boolean"
242
- }
243
- },
244
- "hasDynamicHelp": false,
245
- "hiddenAliases": [],
246
- "id": "deploy",
247
- "pluginAlias": "@mailmodo/cli",
248
- "pluginName": "@mailmodo/cli",
249
- "pluginType": "core",
250
- "strict": true,
251
- "enableJsonFlag": false,
252
- "isESM": true,
253
- "relativePath": [
254
- "dist",
255
- "commands",
256
- "deploy",
257
- "index.js"
258
- ]
259
- },
260
260
  "emails": {
261
261
  "aliases": [],
262
262
  "args": {},
@@ -618,5 +618,5 @@
618
618
  ]
619
619
  }
620
620
  },
621
- "version": "0.0.19-beta.pr21.31"
621
+ "version": "0.0.20-beta.pr22.32"
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.19-beta.pr21.31",
4
+ "version": "0.0.20-beta.pr22.32",
5
5
  "author": "provishalk",
6
6
  "bin": {
7
7
  "mailmodo": "bin/run.js"