@mailmodo/cli 0.0.22 → 0.0.23

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,14 +6,9 @@ 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;
19
14
  private buildBrandSection;
@@ -22,32 +17,11 @@ export default class Deploy extends BaseCommand {
22
17
  private buildProjectPayload;
23
18
  private confirmDeploy;
24
19
  private ensureDomainReady;
25
- /**
26
- * Lists emails about to be deployed (skipped when `--json` is set).
27
- *
28
- * @param yamlConfig - Loaded project YAML.
29
- * @param jsonOutput - When true, skip human-readable output.
30
- */
31
20
  private logPreDeploySummary;
32
- /**
33
- * Prints the post-deploy success message and SDK install snippet for interactive runs.
34
- */
21
+ private logDiff;
35
22
  private logDeploySuccessInstructions;
36
- /**
37
- * Interactive domain setup flow. Collects domain, sender email, and business
38
- * address from the user, then calls the API to get DNS records to configure.
39
- * Polls for verification when the user indicates they've added the records.
40
- *
41
- * @returns {Promise<boolean>} true if domain was verified, false if skipped.
42
- */
43
23
  private runDomainSetup;
44
24
  private collectDomainInputs;
45
25
  private showDnsRecords;
46
- /**
47
- * Calls the domain verification API endpoint and reports pass/fail
48
- * status for each DNS record (DKIM, DMARC, Return-Path).
49
- *
50
- * @returns {Promise<boolean>} true if all records pass.
51
- */
52
26
  private verifyDomain;
53
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,
@@ -86,11 +94,11 @@ export default class Deploy extends BaseCommand {
86
94
  buildProductSection(project) {
87
95
  return {
88
96
  businessType: project?.type || '',
89
- description: '',
90
- pricingModel: '',
97
+ description: project?.description || '',
98
+ pricingModel: project?.pricingModel || '',
91
99
  productName: project?.name || '',
92
- saasModel: '',
93
- targetUser: '',
100
+ saasModel: project?.saasModel || '',
101
+ targetUser: project?.targetUser || '',
94
102
  url: project?.url || '',
95
103
  };
96
104
  }
@@ -150,25 +158,41 @@ export default class Deploy extends BaseCommand {
150
158
  }
151
159
  return this.runDomainSetup(yamlConfig, flags);
152
160
  }
153
- /**
154
- * Lists emails about to be deployed (skipped when `--json` is set).
155
- *
156
- * @param yamlConfig - Loaded project YAML.
157
- * @param jsonOutput - When true, skip human-readable output.
158
- */
159
- logPreDeploySummary(yamlConfig, jsonOutput) {
161
+ logPreDeploySummary(yamlConfig, validateResult, jsonOutput) {
160
162
  if (jsonOutput)
161
163
  return;
162
164
  this.log(`\n ${chalk.green('✓')} Domain: ${yamlConfig.project?.domain || 'verified'}\n`);
163
- this.log(` Deploying:`);
164
- for (const email of yamlConfig.emails) {
165
- 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);
166
173
  }
167
174
  this.log('');
168
175
  }
169
- /**
170
- * Prints the post-deploy success message and SDK install snippet for interactive runs.
171
- */
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
+ }
172
196
  logDeploySuccessInstructions(sdkSnippet) {
173
197
  this.log(` ${chalk.green('Deployed.')} Emails are live.\n`);
174
198
  this.log(` ${'─'.repeat(53)}`);
@@ -185,13 +209,6 @@ export default class Deploy extends BaseCommand {
185
209
  this.log(` Full SDK docs: ${chalk.cyan('mailmodo.com/docs/sdk')}\n`);
186
210
  this.log(` ${'─'.repeat(53)}\n`);
187
211
  }
188
- /**
189
- * Interactive domain setup flow. Collects domain, sender email, and business
190
- * address from the user, then calls the API to get DNS records to configure.
191
- * Polls for verification when the user indicates they've added the records.
192
- *
193
- * @returns {Promise<boolean>} true if domain was verified, false if skipped.
194
- */
195
212
  async runDomainSetup(yamlConfig, flags) {
196
213
  const { address, domain, senderEmail } = await this.collectDomainInputs(yamlConfig, flags);
197
214
  const domainResponse = await this.withApiSpinner({ json: flags.json, text: ' Configuring domain...' }, () => this.apiClient.post(API_ENDPOINTS.DOMAIN, {
@@ -260,12 +277,6 @@ export default class Deploy extends BaseCommand {
260
277
  this.log(` DNS changes take 5–30 minutes to propagate.`);
261
278
  this.log(` Full guide: ${chalk.cyan(DNS_GUIDE_URL)}\n`);
262
279
  }
263
- /**
264
- * Calls the domain verification API endpoint and reports pass/fail
265
- * status for each DNS record (DKIM, DMARC, Return-Path).
266
- *
267
- * @returns {Promise<boolean>} true if all records pass.
268
- */
269
280
  async verifyDomain(jsonOutput, domain) {
270
281
  const verify = await this.withApiSpinner({ json: jsonOutput, text: ' Checking DNS...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY, {
271
282
  domain,
@@ -179,7 +179,7 @@ export default class Domain extends BaseCommand {
179
179
  this.log(` Spam rate: ${data.spamRate ?? 'N/A'}%\n`);
180
180
  }
181
181
  recordLabel(index) {
182
- const labels = ['SPF', 'DKIM', 'DMARC'];
182
+ const labels = ['DKIM', 'DMARC', 'Return Path'];
183
183
  return labels[index] || `Record ${index + 1}`;
184
184
  }
185
185
  }
@@ -194,6 +194,8 @@ export default class Edit extends BaseCommand {
194
194
  email.subject = updated.subject;
195
195
  if (updated.previewText)
196
196
  email.previewText = updated.previewText;
197
+ if (updated.ctaText)
198
+ email.ctaText = updated.ctaText;
197
199
  const updatedYaml = {
198
200
  ...yamlConfig,
199
201
  emails: [...yamlConfig.emails],
@@ -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
  },
@@ -249,7 +249,7 @@ export default class Settings extends BaseCommand {
249
249
  this.log(` Help: ${chalk.cyan(DNS_GUIDE_URL)}\n`);
250
250
  }
251
251
  recordLabel(index) {
252
- const labels = ['SPF', 'DKIM', 'DMARC'];
252
+ const labels = ['DKIM', 'DMARC', 'Return Path'];
253
253
  return labels[index] || `Record ${index + 1}`;
254
254
  }
255
255
  /**
@@ -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;
@@ -634,5 +634,5 @@
634
634
  ]
635
635
  }
636
636
  },
637
- "version": "0.0.22"
637
+ "version": "0.0.23"
638
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.22",
4
+ "version": "0.0.23",
5
5
  "author": "provishalk",
6
6
  "bin": {
7
7
  "mailmodo": "bin/run.js"