@mailmodo/cli 0.0.20-beta.pr22.37 → 0.0.20-beta.pr23.34

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,9 +16,6 @@ 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;
22
19
  private buildProjectPayload;
23
20
  private confirmDeploy;
24
21
  private ensureDomainReady;
@@ -77,39 +77,30 @@ 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
- }
106
80
  buildProjectPayload(project) {
107
81
  return {
108
- brand: this.buildBrandSection(project),
82
+ brand: {
83
+ colors: [project?.brandColor || DEFAULT_BRAND_COLOR],
84
+ logoUrl: project?.logoUrl || '',
85
+ },
109
86
  emailStyle: project?.emailStyle || 'branded',
110
87
  monthlyCap: project?.monthlyCap ?? DEFAULT_MONTHLY_CAP,
111
- product: this.buildProductSection(project),
112
- senderDetails: this.buildSenderSection(project),
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
+ },
113
104
  webhookUrl: project?.webhookUrl || '',
114
105
  };
115
106
  }
@@ -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, returnPath, domainStatus } = response.data;
127
+ const { dkim, dmarc, spf } = response.data;
128
128
  if (jsonOutput) {
129
- this.log(JSON.stringify({ dkim, dmarc, returnPath, domainStatus }, null, 2));
129
+ this.log(JSON.stringify({ dkim, dmarc, spf }, null, 2));
130
130
  return;
131
131
  }
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';
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;
136
136
  if (allPassed) {
137
137
  this.log(`\n ${chalk.green('✓')} Domain verified.\n`);
138
138
  }
@@ -144,11 +144,6 @@ 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
- }
152
147
  this.log(`\n Fix the records and run ${chalk.cyan('mailmodo domain --verify')} again.`);
153
148
  this.log(` Help: ${chalk.cyan(DNS_GUIDE_URL)}\n`);
154
149
  }
@@ -10,15 +10,5 @@ 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;
23
13
  run(): Promise<void>;
24
14
  }
@@ -22,123 +22,6 @@ 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
- }
142
25
  async run() {
143
26
  const { args, flags } = await this.parse(Edit);
144
27
  await this.ensureAuth();
@@ -149,6 +32,9 @@ export default class Edit extends BaseCommand {
149
32
  }
150
33
  const email = yamlConfig.emails[emailIndex];
151
34
  const templateHtml = await loadTemplate(`${email.id}.html`);
35
+ if (!flags.json) {
36
+ this.log(`\n Current subject: '${chalk.cyan(email.subject)}'`);
37
+ }
152
38
  let changeDescription = flags.change;
153
39
  if (!changeDescription) {
154
40
  changeDescription = await input({
@@ -172,11 +58,12 @@ export default class Edit extends BaseCommand {
172
58
  this.handleApiError(response);
173
59
  }
174
60
  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);
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}`)}`);
180
67
  }
181
68
  if (!flags.yes) {
182
69
  const accepted = await confirm({
@@ -188,8 +75,6 @@ export default class Edit extends BaseCommand {
188
75
  return;
189
76
  }
190
77
  }
191
- const oldSubject = email.subject;
192
- const newSubject = updated.subject || email.subject;
193
78
  if (updated.subject)
194
79
  email.subject = updated.subject;
195
80
  if (updated.previewText)
@@ -206,9 +91,6 @@ export default class Edit extends BaseCommand {
206
91
  if (flags.json) {
207
92
  this.log(JSON.stringify({
208
93
  diff: {
209
- previewText: updated.previewText && updated.previewText !== email.previewText
210
- ? { new: updated.previewText, old: email.previewText }
211
- : undefined,
212
94
  subject: oldSubject === newSubject
213
95
  ? undefined
214
96
  : { new: newSubject, old: oldSubject },
@@ -5,6 +5,8 @@ export default class Logs extends BaseCommand {
5
5
  static flags: {
6
6
  email: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
7
  failed: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
9
+ page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
8
10
  json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
11
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
12
  };
@@ -17,11 +17,22 @@ export default class Logs extends BaseCommand {
17
17
  default: false,
18
18
  description: 'Show only failed/bounced events',
19
19
  }),
20
+ limit: Flags.integer({
21
+ default: 50,
22
+ description: 'Entries per page (max 200)',
23
+ }),
24
+ page: Flags.integer({
25
+ default: 1,
26
+ description: 'Page number',
27
+ }),
20
28
  };
21
29
  async run() {
22
30
  const { flags } = await this.parse(Logs);
23
31
  await this.ensureAuth();
24
- const params = {};
32
+ const params = {
33
+ limit: String(flags.limit),
34
+ page: String(flags.page),
35
+ };
25
36
  if (flags.email)
26
37
  params.email = flags.email;
27
38
  if (flags.failed)
@@ -30,7 +41,7 @@ export default class Logs extends BaseCommand {
30
41
  if (!response.ok) {
31
42
  this.handleApiError(response);
32
43
  }
33
- const { entries } = response.data;
44
+ const { entries, limit, page, total } = response.data;
34
45
  if (flags.json) {
35
46
  this.log(JSON.stringify(response.data, null, 2));
36
47
  return;
@@ -49,6 +60,11 @@ export default class Logs extends BaseCommand {
49
60
  this.log(` ${' '.repeat(52)}${chalk.dim(`(reason: ${entry.reason})`)}`);
50
61
  }
51
62
  }
63
+ const totalPages = Math.ceil(total / limit);
64
+ this.log(`\n Page ${page} of ${totalPages} · ${total} total entries`);
65
+ if (page < totalPages) {
66
+ this.log(` ${chalk.dim(`Next: --page ${page + 1}`)}`);
67
+ }
52
68
  }
53
69
  else {
54
70
  this.log(` ${chalk.dim('No log entries found.')}`);
@@ -89,11 +89,7 @@ export default class Preview extends BaseCommand {
89
89
  const rendered = templateHtml
90
90
  ? renderTemplate(templateHtml, sampleData)
91
91
  : '';
92
- await this.sendTestEmail(email, rendered, {
93
- domain: yamlConfig.project?.domain,
94
- jsonOutput: flags.json,
95
- toAddress: flags.send,
96
- });
92
+ await this.sendTestEmail(email, rendered, yamlConfig.project?.domain, flags.send, flags.json);
97
93
  return;
98
94
  }
99
95
  if (flags.text) {
@@ -130,8 +126,7 @@ export default class Preview extends BaseCommand {
130
126
  * Calls the API to send a test email to the specified address.
131
127
  * Before domain verification, tests send via the mailmodo.com domain.
132
128
  */
133
- async sendTestEmail(email, html, opts) {
134
- const { domain, jsonOutput, toAddress } = opts;
129
+ async sendTestEmail(email, html, domain, toAddress, jsonOutput) {
135
130
  await this.ensureAuth();
136
131
  const response = await this.withApiSpinner({ json: jsonOutput, text: ' Sending test email...' }, () => this.apiClient.post(`${API_ENDPOINTS.PREVIEW}/send`, {
137
132
  domain,
@@ -52,7 +52,7 @@ export default class Settings extends BaseCommand {
52
52
  const key = flags.set.slice(0, eqIndex).trim();
53
53
  const propKey = settingKeyToProp(key);
54
54
  const value = flags.set.slice(eqIndex + 1).trim();
55
- if (!(propKey in project)) {
55
+ if (!(propKey in project) && key !== 'logo_file') {
56
56
  this.error(`Unknown setting: ${key}`);
57
57
  }
58
58
  project[propKey] =
@@ -73,7 +73,11 @@ export default class Settings extends BaseCommand {
73
73
  const domainVerified = await this.fetchDomainVerified(project.domain);
74
74
  this.log(`\n Current settings for ${chalk.bold(project.name || 'project')}:\n`);
75
75
  for (const [group, keys] of Object.entries(SETTINGS_GROUPS)) {
76
- const availableKeys = keys.filter((key) => settingKeyToProp(key) in project);
76
+ const availableKeys = keys.filter((key) => {
77
+ if (group === 'brand' && key === 'logo_file')
78
+ return true;
79
+ return settingKeyToProp(key) in project;
80
+ });
77
81
  if (availableKeys.length === 0) {
78
82
  const hint = SETUP_HINTS[settingKeyToProp(keys[0])];
79
83
  if (hint) {
@@ -98,7 +102,8 @@ export default class Settings extends BaseCommand {
98
102
  }
99
103
  this.log(` ${key.padEnd(16)} ${displayValue}`);
100
104
  }
101
- const missingKeys = keys.filter((key) => !(settingKeyToProp(key) in project));
105
+ const missingKeys = keys.filter((key) => !(settingKeyToProp(key) in project) &&
106
+ !(group === 'brand' && key === 'logo_file'));
102
107
  for (const key of missingKeys) {
103
108
  const hint = SETUP_HINTS[settingKeyToProp(key)];
104
109
  if (hint) {
@@ -125,6 +130,10 @@ export default class Settings extends BaseCommand {
125
130
  return;
126
131
  const editPropKey = settingKeyToProp(editKey);
127
132
  if (!(editPropKey in project)) {
133
+ if (editKey === 'logo_file') {
134
+ await this.handleLogoUpload(yamlConfig);
135
+ return;
136
+ }
128
137
  const hint = SETUP_HINTS[editPropKey];
129
138
  if (hint) {
130
139
  this.log(`\n ${editKey} is not configured yet. Run ${chalk.cyan(hint)} to set it up.\n`);
@@ -252,8 +261,16 @@ export default class Settings extends BaseCommand {
252
261
  }
253
262
  await this.ensureAuth();
254
263
  const fileBuffer = await readFile(resolvedPath);
264
+ const ext = resolvedPath.split('.').pop()?.toLowerCase();
265
+ const mimeTypes = {
266
+ png: 'image/png',
267
+ jpg: 'image/jpeg',
268
+ jpeg: 'image/jpeg',
269
+ svg: 'image/svg+xml',
270
+ };
271
+ const mimeType = mimeTypes[ext ?? ''] ?? 'application/octet-stream';
255
272
  const formData = new FormData();
256
- formData.append('logo', new Blob([new Uint8Array(fileBuffer)]), logoPath.split(/[/\\]/).pop() || 'logo.png');
273
+ formData.append('logo', new Blob([new Uint8Array(fileBuffer)], { type: mimeType }), logoPath.split(/[/\\]/).pop() || 'logo.png');
257
274
  const response = await this.apiClient.postFormData(API_ENDPOINTS.ASSETS_LOGO, formData);
258
275
  if (!response.ok) {
259
276
  this.handleApiError(response);
@@ -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: "/email/edit";
14
+ EDIT: "/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: '/email/edit',
20
+ EDIT: '/edit',
21
21
  EVENTS: '/events',
22
22
  GENERATE: '/email/generate',
23
23
  LOGS: '/logs',
@@ -455,6 +455,22 @@
455
455
  "name": "failed",
456
456
  "allowNo": false,
457
457
  "type": "boolean"
458
+ },
459
+ "limit": {
460
+ "description": "Entries per page (max 200)",
461
+ "name": "limit",
462
+ "default": 50,
463
+ "hasDynamicHelp": false,
464
+ "multiple": false,
465
+ "type": "option"
466
+ },
467
+ "page": {
468
+ "description": "Page number",
469
+ "name": "page",
470
+ "default": 1,
471
+ "hasDynamicHelp": false,
472
+ "multiple": false,
473
+ "type": "option"
458
474
  }
459
475
  },
460
476
  "hasDynamicHelp": false,
@@ -618,5 +634,5 @@
618
634
  ]
619
635
  }
620
636
  },
621
- "version": "0.0.20-beta.pr22.37"
637
+ "version": "0.0.20-beta.pr23.34"
622
638
  }
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-beta.pr22.37",
4
+ "version": "0.0.20-beta.pr23.34",
5
5
  "author": "provishalk",
6
6
  "bin": {
7
7
  "mailmodo": "bin/run.js"