@mailmodo/cli 0.0.54 → 0.0.55-beta.pr57.93

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.
Files changed (136) hide show
  1. package/dist/commands/billing/index.d.ts +1 -11
  2. package/dist/commands/billing/index.js +28 -181
  3. package/dist/commands/contacts/index.d.ts +1 -19
  4. package/dist/commands/contacts/index.js +21 -114
  5. package/dist/commands/deploy/index.d.ts +1 -32
  6. package/dist/commands/deploy/index.js +52 -303
  7. package/dist/commands/deployments/index.d.ts +1 -4
  8. package/dist/commands/deployments/index.js +11 -52
  9. package/dist/commands/domain/index.d.ts +1 -14
  10. package/dist/commands/domain/index.js +19 -100
  11. package/dist/commands/edit/index.d.ts +2 -20
  12. package/dist/commands/edit/index.js +35 -244
  13. package/dist/commands/emails/index.d.ts +1 -2
  14. package/dist/commands/emails/index.js +26 -91
  15. package/dist/commands/init/index.d.ts +1 -2
  16. package/dist/commands/init/index.js +43 -179
  17. package/dist/commands/login/index.d.ts +2 -0
  18. package/dist/commands/login/index.js +35 -64
  19. package/dist/commands/logs/index.d.ts +1 -8
  20. package/dist/commands/logs/index.js +12 -55
  21. package/dist/commands/preview/index.d.ts +1 -19
  22. package/dist/commands/preview/index.js +40 -210
  23. package/dist/commands/sdk/index.d.ts +1 -3
  24. package/dist/commands/sdk/index.js +14 -46
  25. package/dist/commands/settings/index.d.ts +1 -22
  26. package/dist/commands/settings/index.js +35 -241
  27. package/dist/commands/status/index.d.ts +1 -0
  28. package/dist/commands/status/index.js +13 -39
  29. package/dist/lib/api-client.d.ts +5 -0
  30. package/dist/lib/api-client.js +45 -0
  31. package/dist/lib/base-command.d.ts +25 -1
  32. package/dist/lib/base-command.js +91 -5
  33. package/dist/lib/commands/billing/checkout-status.d.ts +3 -0
  34. package/dist/lib/commands/billing/checkout-status.js +63 -0
  35. package/dist/lib/commands/billing/format.d.ts +7 -0
  36. package/dist/lib/commands/billing/format.js +63 -0
  37. package/dist/lib/commands/billing/purchase-cap.d.ts +7 -0
  38. package/dist/lib/commands/billing/purchase-cap.js +57 -0
  39. package/dist/lib/commands/billing/types.d.ts +72 -0
  40. package/dist/lib/commands/billing/types.js +1 -0
  41. package/dist/lib/commands/contacts/actions.d.ts +3 -0
  42. package/dist/lib/commands/contacts/actions.js +49 -0
  43. package/dist/lib/commands/contacts/export-delete.d.ts +9 -0
  44. package/dist/lib/commands/contacts/export-delete.js +51 -0
  45. package/dist/lib/commands/contacts/types.d.ts +35 -0
  46. package/dist/lib/commands/contacts/types.js +1 -0
  47. package/dist/lib/commands/deploy/domain-setup.d.ts +8 -0
  48. package/dist/lib/commands/deploy/domain-setup.js +82 -0
  49. package/dist/lib/commands/deploy/output.d.ts +5 -0
  50. package/dist/lib/commands/deploy/output.js +61 -0
  51. package/dist/lib/commands/deploy/payload.d.ts +41 -0
  52. package/dist/lib/commands/deploy/payload.js +95 -0
  53. package/dist/lib/commands/deploy/sequence-status.d.ts +3 -0
  54. package/dist/lib/commands/deploy/sequence-status.js +56 -0
  55. package/dist/lib/commands/deploy/types.d.ts +88 -0
  56. package/dist/lib/commands/deploy/types.js +1 -0
  57. package/dist/lib/commands/deployments/output.d.ts +2 -0
  58. package/dist/lib/commands/deployments/output.js +68 -0
  59. package/dist/lib/commands/deployments/types.d.ts +24 -0
  60. package/dist/lib/commands/deployments/types.js +1 -0
  61. package/dist/lib/commands/domain/setup.d.ts +8 -0
  62. package/dist/lib/commands/domain/setup.js +53 -0
  63. package/dist/lib/commands/domain/types.d.ts +56 -0
  64. package/dist/lib/commands/domain/types.js +1 -0
  65. package/dist/lib/commands/domain/verify.d.ts +5 -0
  66. package/dist/lib/commands/domain/verify.js +50 -0
  67. package/dist/lib/commands/edit/diff.d.ts +7 -0
  68. package/dist/lib/commands/edit/diff.js +65 -0
  69. package/dist/lib/commands/edit/display.d.ts +5 -0
  70. package/dist/lib/commands/edit/display.js +53 -0
  71. package/dist/lib/commands/edit/flow.d.ts +8 -0
  72. package/dist/lib/commands/edit/flow.js +70 -0
  73. package/dist/lib/commands/edit/persist.d.ts +5 -0
  74. package/dist/lib/commands/edit/persist.js +65 -0
  75. package/dist/lib/commands/edit/types.d.ts +37 -0
  76. package/dist/lib/commands/edit/types.js +1 -0
  77. package/dist/lib/commands/emails/editor.d.ts +2 -0
  78. package/dist/lib/commands/emails/editor.js +43 -0
  79. package/dist/lib/commands/emails/output.d.ts +4 -0
  80. package/dist/lib/commands/emails/output.js +36 -0
  81. package/dist/lib/commands/emails/types.d.ts +3 -0
  82. package/dist/lib/commands/emails/types.js +1 -0
  83. package/dist/lib/commands/init/analysis.d.ts +3 -0
  84. package/dist/lib/commands/init/analysis.js +69 -0
  85. package/dist/lib/commands/init/output.d.ts +12 -0
  86. package/dist/lib/commands/init/output.js +39 -0
  87. package/dist/lib/commands/init/payload.d.ts +8 -0
  88. package/dist/lib/commands/init/payload.js +78 -0
  89. package/dist/lib/commands/init/types.d.ts +57 -0
  90. package/dist/lib/commands/init/types.js +1 -0
  91. package/dist/lib/commands/login/output.d.ts +8 -0
  92. package/dist/lib/commands/login/output.js +53 -0
  93. package/dist/lib/commands/login/types.d.ts +19 -0
  94. package/dist/lib/commands/login/types.js +1 -0
  95. package/dist/lib/commands/logs/output.d.ts +2 -0
  96. package/dist/lib/commands/logs/output.js +52 -0
  97. package/dist/lib/commands/logs/types.d.ts +23 -0
  98. package/dist/lib/commands/logs/types.js +1 -0
  99. package/dist/lib/commands/preview/actions.d.ts +11 -0
  100. package/dist/lib/commands/preview/actions.js +43 -0
  101. package/dist/lib/commands/preview/render.d.ts +3 -0
  102. package/dist/lib/commands/preview/render.js +30 -0
  103. package/dist/lib/commands/preview/server.d.ts +8 -0
  104. package/dist/lib/commands/preview/server.js +63 -0
  105. package/dist/lib/commands/preview/types.d.ts +19 -0
  106. package/dist/lib/commands/preview/types.js +1 -0
  107. package/dist/lib/commands/preview/wrapper-html.d.ts +2 -0
  108. package/dist/lib/commands/preview/wrapper-html.js +35 -0
  109. package/dist/lib/commands/sdk/output.d.ts +2 -0
  110. package/dist/lib/commands/sdk/output.js +42 -0
  111. package/dist/lib/commands/sdk/types.d.ts +21 -0
  112. package/dist/lib/commands/sdk/types.js +1 -0
  113. package/dist/lib/commands/settings/actions.d.ts +10 -0
  114. package/dist/lib/commands/settings/actions.js +56 -0
  115. package/dist/lib/commands/settings/display.d.ts +15 -0
  116. package/dist/lib/commands/settings/display.js +69 -0
  117. package/dist/lib/commands/settings/logo-domain.d.ts +3 -0
  118. package/dist/lib/commands/settings/logo-domain.js +47 -0
  119. package/dist/lib/commands/settings/prompt.d.ts +2 -0
  120. package/dist/lib/commands/settings/prompt.js +82 -0
  121. package/dist/lib/commands/settings/types.d.ts +65 -0
  122. package/dist/lib/commands/settings/types.js +1 -0
  123. package/dist/lib/commands/status/output.d.ts +2 -0
  124. package/dist/lib/commands/status/output.js +49 -0
  125. package/dist/lib/commands/status/types.d.ts +28 -0
  126. package/dist/lib/commands/status/types.js +1 -0
  127. package/dist/lib/constants.d.ts +1 -0
  128. package/dist/lib/constants.js +1 -0
  129. package/dist/lib/messages.d.ts +22 -0
  130. package/dist/lib/messages.js +22 -0
  131. package/dist/lib/templates/missing-templates.d.ts +5 -0
  132. package/dist/lib/templates/missing-templates.js +61 -0
  133. package/dist/lib/templates/types.d.ts +13 -0
  134. package/dist/lib/templates/types.js +1 -0
  135. package/oclif.manifest.json +40 -40
  136. package/package.json +1 -1
@@ -1,10 +1,14 @@
1
1
  import { Flags } from '@oclif/core';
2
- import { confirm, input } from '@inquirer/prompts';
2
+ import { confirm } from '@inquirer/prompts';
3
3
  import chalk from 'chalk';
4
4
  import { BaseCommand } from '../../lib/base-command.js';
5
- import { API_ENDPOINTS, DEFAULT_BRAND_COLOR, SDK_IMPORT_SNIPPET, SDK_INSTALL_COMMAND, } from '../../lib/constants.js';
6
- import { ERRORS, INFO, pauseAlready, pauseSuccess, PROMPTS, resumeAlready, resumeSuccess, SEPARATOR, } from '../../lib/messages.js';
7
- import { loadTemplate, } from '../../lib/yaml-config.js';
5
+ import { API_ENDPOINTS } from '../../lib/constants.js';
6
+ import { MISSING_TEMPLATES } from '../../lib/messages.js';
7
+ import { buildDeployPayload } from '../../lib/commands/deploy/payload.js';
8
+ import { logDeploySuccessInstructions, logPreDeploySummary, } from '../../lib/commands/deploy/output.js';
9
+ import { pauseSequence, resumeSequence, } from '../../lib/commands/deploy/sequence-status.js';
10
+ import { ensureDomainReady, validateDeploySequence, } from '../../lib/commands/deploy/domain-setup.js';
11
+ import { getMissingTemplateIds, handleMissingTemplates, } from '../../lib/templates/missing-templates.js';
8
12
  export default class Deploy extends BaseCommand {
9
13
  static description = 'Deploy, pause, or resume an email sequence';
10
14
  static examples = [
@@ -24,37 +28,47 @@ export default class Deploy extends BaseCommand {
24
28
  exclusive: ['pause'],
25
29
  }),
26
30
  };
27
- fetchDomainVerifyForDeploy(jsonOutput, domain) {
28
- return this.withApiSpinner({ json: jsonOutput, text: ' Checking domain verification...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY, {
29
- domain: domain || '',
30
- }));
31
- }
32
31
  async run() {
33
32
  const { flags } = await this.parse(Deploy);
34
33
  await this.ensureAuth();
34
+ const ctx = this.makeCtx();
35
35
  const baseFlags = { json: flags.json, yes: flags.yes };
36
36
  if (flags.pause) {
37
- await this.pauseSequence(flags.pause, baseFlags);
37
+ await pauseSequence(ctx, flags.pause, baseFlags);
38
38
  return;
39
39
  }
40
40
  if (flags.resume) {
41
- await this.resumeSequence(flags.resume, baseFlags);
41
+ await resumeSequence(ctx, flags.resume, baseFlags);
42
42
  return;
43
43
  }
44
44
  const yamlConfig = await this.ensureYaml();
45
- const domainReady = await this.ensureDomainReady(yamlConfig, flags);
46
- if (!domainReady)
45
+ const missingIds = getMissingTemplateIds(yamlConfig);
46
+ if (missingIds.length > 0) {
47
+ const regenerated = await handleMissingTemplates(ctx, yamlConfig, missingIds, baseFlags);
48
+ if (regenerated)
49
+ ctx.log(`\n ${chalk.green('✓')} ${MISSING_TEMPLATES.REVIEW_HINT}\n`);
47
50
  return;
48
- const payload = await this.buildDeployPayload(yamlConfig);
49
- const validateResult = await this.validateSequence(payload, flags);
50
- this.logPreDeploySummary(yamlConfig, validateResult, flags.json);
51
- const confirmed = await this.confirmDeploy(yamlConfig, flags);
52
- if (!confirmed)
51
+ }
52
+ const domainReady = await ensureDomainReady(ctx, yamlConfig, baseFlags);
53
+ if (!domainReady)
53
54
  return;
54
- const response = await this.withApiSpinner({ json: flags.json, text: ' Deploying email sequences...' }, () => this.apiClient.post(API_ENDPOINTS.SEQUENCES_DEPLOY, payload));
55
- if (!response.ok) {
56
- this.handleApiError(response);
55
+ const payload = await buildDeployPayload(ctx, yamlConfig);
56
+ const validateResult = await validateDeploySequence(ctx, payload, baseFlags);
57
+ logPreDeploySummary(ctx, yamlConfig, validateResult, flags.json);
58
+ if (!flags.yes) {
59
+ const proceed = await confirm({
60
+ default: true,
61
+ message: `Deploy ${yamlConfig.emails.length} emails?`,
62
+ });
63
+ if (!proceed) {
64
+ this.log('\n Deploy cancelled.\n');
65
+ return;
66
+ }
57
67
  }
68
+ const response = await ctx.spinner(' Deploying email sequences...', flags.json, () => ctx.post(API_ENDPOINTS.SEQUENCES_DEPLOY, payload));
69
+ if (!response.ok)
70
+ ctx.onApiError(response);
71
+ await ctx.syncYaml();
58
72
  if (flags.json) {
59
73
  this.log(JSON.stringify({
60
74
  deployed: response.data.deployed,
@@ -65,290 +79,25 @@ export default class Deploy extends BaseCommand {
65
79
  }, null, 2));
66
80
  return;
67
81
  }
68
- this.logDeploySuccessInstructions(response.data.sdkSnippet);
82
+ logDeploySuccessInstructions(ctx, response.data.sdkSnippet);
69
83
  }
70
- async validateSequence(payload, flags) {
71
- const response = await this.withApiSpinner({ json: flags.json, text: ' Validating sequence...' }, () => this.apiClient.post(API_ENDPOINTS.SEQUENCES_VALIDATE, payload));
72
- if (!response.ok) {
73
- if (response.data.error === 'senderDomainNotFound') {
74
- this.error(ERRORS.DOMAIN_NOT_REGISTERED);
75
- }
76
- if (response.data.error === 'senderDomainNotVerified') {
77
- this.error(ERRORS.DOMAIN_NOT_VERIFIED);
78
- }
79
- this.handleApiError(response);
80
- }
81
- return response.data;
82
- }
83
- /**
84
- * Calls `POST /sequences/{id}/status` with `{ status: "paused" }` and prints
85
- * a confirmation-aware success/no-op message. Skips the prompt entirely when
86
- * `--yes` is set so the command stays scriptable. `--json` always emits the
87
- * raw server payload (sequenceId, status, alreadyInStatus).
88
- */
89
- async pauseSequence(sequenceId, flags) {
90
- if (!flags.yes) {
91
- const confirmed = await confirm({
92
- default: false,
93
- message: PROMPTS.PAUSE_CONFIRM,
94
- });
95
- if (!confirmed) {
96
- this.log(`\n ${INFO.PAUSE_CANCELLED}\n`);
97
- return;
98
- }
99
- }
100
- const data = await this.updateSequenceStatus(sequenceId, 'paused', flags, ' Pausing sequence...');
101
- if (flags.json) {
102
- this.log(JSON.stringify(data, null, 2));
103
- return;
104
- }
105
- const message = data.alreadyInStatus
106
- ? pauseAlready(data.sequenceId || sequenceId)
107
- : pauseSuccess(data.sequenceId || sequenceId);
108
- this.log(`\n ${message}\n`);
109
- }
110
- /**
111
- * Calls `POST /sequences/{id}/status` with `{ status: "active" }`. No prompt
112
- * — resuming is the safe direction (it does not start sends that weren't
113
- * already queued). `--json` always emits the raw server payload (sequenceId,
114
- * status, alreadyInStatus).
115
- */
116
- async resumeSequence(sequenceId, flags) {
117
- const data = await this.updateSequenceStatus(sequenceId, 'active', flags, ' Resuming sequence...');
118
- if (flags.json) {
119
- this.log(JSON.stringify(data, null, 2));
120
- return;
121
- }
122
- const message = data.alreadyInStatus
123
- ? resumeAlready(data.sequenceId || sequenceId)
124
- : resumeSuccess(data.sequenceId || sequenceId);
125
- this.log(`\n ${message}\n`);
126
- }
127
- async updateSequenceStatus(sequenceId, status, flags, spinnerText) {
128
- const response = await this.withApiSpinner({ json: flags.json, text: spinnerText }, () => this.apiClient.post(this.sequenceStatusPath(sequenceId), { status }));
129
- if (!response.ok) {
130
- this.handleApiError(response);
131
- }
132
- return response.data;
133
- }
134
- sequenceStatusPath(sequenceId) {
135
- return `${API_ENDPOINTS.SEQUENCES}/${encodeURIComponent(sequenceId)}/status`;
136
- }
137
- async buildDeployPayload(yamlConfig) {
138
- const [emailsWithHtml, monthlyCap] = await Promise.all([
139
- Promise.all(yamlConfig.emails.map(async (email) => {
140
- const html = (await loadTemplate(`${email.id}.html`)) || '';
141
- const plainHtml = (await loadTemplate(`${email.id}_plain.html`)) || html;
142
- return { ...this.mapEmailToPayload(email), html, plainHtml };
143
- })),
144
- this.resolveMonthlyCapForDeploy(yamlConfig.project.monthlyCap),
145
- ]);
146
- return {
147
- ...this.buildProjectPayload(yamlConfig.project, monthlyCap),
148
- emails: emailsWithHtml,
149
- };
150
- }
151
- async resolveMonthlyCapForDeploy(yamlMonthlyCap) {
152
- if (yamlMonthlyCap !== undefined)
153
- return yamlMonthlyCap;
154
- const billingStatus = await this.fetchBillingStatus();
155
- return billingStatus?.cap?.inBlocks ?? undefined;
156
- }
157
- mapEmailToPayload(email) {
158
- return {
159
- condition: email.condition || null,
160
- ctaText: email.ctaText || '',
161
- delay: email.delay,
162
- goal: email.goal || '',
163
- id: email.id,
164
- isReminder: false,
165
- previewText: email.previewText || '',
166
- priority: 'medium',
167
- subject: email.subject,
168
- trigger: email.trigger,
169
- };
170
- }
171
- buildBrandSection(project) {
84
+ makeCtx() {
172
85
  return {
173
- colors: [project?.brandColor || DEFAULT_BRAND_COLOR],
174
- logoUrl: project?.logoUrl || '',
86
+ collectDomainInputs: (yaml, skip) => this.collectDomainSetupInputs(yaml, skip),
87
+ error: (msg) => this.error(msg),
88
+ exit: (code) => this.exit(code),
89
+ get: (path, params) => this.apiClient.get(path, params),
90
+ getBillingCap: async () => {
91
+ const s = await this.fetchBillingStatus();
92
+ return s?.cap?.inBlocks ?? undefined;
93
+ },
94
+ log: (msg) => this.log(msg),
95
+ onApiError: (r) => this.handleApiError(r),
96
+ post: (path, body) => this.apiClient.post(path, body),
97
+ registerDomainAndSave: (yaml, inputs, json) => this.registerDomain(yaml, inputs, json),
98
+ showDnsRecords: (records, url, json) => this.logDnsRecords(records, url, json),
99
+ spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
100
+ syncYaml: () => this.syncYamlToServer(),
175
101
  };
176
102
  }
177
- buildProductSection(project) {
178
- return {
179
- businessType: project?.type || '',
180
- description: project?.description || '',
181
- pricingModel: project?.pricingModel || '',
182
- productName: project?.name || '',
183
- saasModel: project?.saasModel || '',
184
- targetUser: project?.targetUser || '',
185
- url: project?.url || '',
186
- };
187
- }
188
- buildSenderSection(project) {
189
- return {
190
- address: project?.address || '',
191
- domain: project?.domain || '',
192
- fromEmail: project?.fromEmail || '',
193
- fromName: project?.fromName || '',
194
- replyTo: project?.replyTo || project?.fromEmail || '',
195
- };
196
- }
197
- buildProjectPayload(project, monthlyCap) {
198
- return {
199
- brand: this.buildBrandSection(project),
200
- emailStyle: project?.emailStyle || 'branded',
201
- ...(monthlyCap === undefined ? {} : { monthlyCap }),
202
- product: this.buildProductSection(project),
203
- senderDetails: this.buildSenderSection(project),
204
- ...(project?.webhookUrl ? { webhookUrl: project.webhookUrl } : {}),
205
- };
206
- }
207
- async confirmDeploy(yamlConfig, flags) {
208
- if (flags.yes)
209
- return true;
210
- const proceed = await confirm({
211
- default: true,
212
- message: `Deploy ${yamlConfig.emails.length} emails?`,
213
- });
214
- if (!proceed) {
215
- this.log('\n Deploy cancelled.\n');
216
- }
217
- return proceed;
218
- }
219
- async ensureDomainReady(yamlConfig, flags) {
220
- const domainVerify = await this.fetchDomainVerifyForDeploy(flags.json, yamlConfig.project?.domain);
221
- if (domainVerify.ok && domainVerify.data?.domainStatus === 'VERIFIED') {
222
- return true;
223
- }
224
- if (yamlConfig.project?.domain) {
225
- if (!flags.json) {
226
- this.log(`\n ${INFO.DOMAIN_PENDING_VERIFICATION}\n`);
227
- }
228
- return false;
229
- }
230
- if (!flags.json) {
231
- this.log(`\n No sending domain set up yet.`);
232
- this.log(` You need a verified domain before sending emails.`);
233
- this.log(` This is a one-time setup. Takes about 5 minutes.\n`);
234
- }
235
- if (!flags.yes) {
236
- const setupNow = await confirm({
237
- default: true,
238
- message: 'Set up your sending domain now?',
239
- });
240
- if (!setupNow) {
241
- this.log(`\n ${INFO.SEQUENCES_NOT_DEPLOYED}`);
242
- this.log(` Emails will not send until your domain is verified.`);
243
- this.log(` ${INFO.DOMAIN_NOT_DEPLOYED_HINT}\n`);
244
- return false;
245
- }
246
- }
247
- return this.runDomainSetup(yamlConfig, flags);
248
- }
249
- logPreDeploySummary(yamlConfig, validateResult, jsonOutput) {
250
- if (jsonOutput)
251
- return;
252
- this.log(`\n ${chalk.green('✓')} Domain: ${yamlConfig.project?.domain || 'verified'}\n`);
253
- if (!validateResult.existingDeployment || !validateResult.diff) {
254
- this.log(` Deploying:`);
255
- for (const email of yamlConfig.emails) {
256
- this.log(` ${chalk.green('+')} ${email.id.padEnd(24)} ${email.trigger}`);
257
- }
258
- }
259
- else {
260
- this.logDiff(validateResult.diff);
261
- }
262
- this.log('');
263
- }
264
- logDiff(diff) {
265
- if (!diff.hasChanges) {
266
- this.log(` No changes from last deployment.`);
267
- return;
268
- }
269
- this.log(` Changes vs. last deployment:`);
270
- for (const email of diff.added) {
271
- this.log(` ${chalk.green('+')} ${email.id.padEnd(24)} ${email.trigger || ''}`);
272
- }
273
- for (const email of diff.removed) {
274
- this.log(` ${chalk.red('-')} ${email.id.padEnd(24)} ${email.trigger || ''}`);
275
- }
276
- for (const email of diff.modified) {
277
- const fields = email.changedFields?.join(', ') || '';
278
- this.log(` ${chalk.yellow('~')} ${email.id.padEnd(24)} ${fields}`);
279
- }
280
- if (diff.unchanged.length > 0) {
281
- this.log(` ${chalk.dim(`∙ ${diff.unchanged.length} unchanged`)}`);
282
- }
283
- }
284
- logDeploySuccessInstructions(sdkSnippet) {
285
- this.log(` ${chalk.green('Deployed.')} Emails are live.\n`);
286
- this.log(` ${SEPARATOR}`);
287
- this.log(` ${chalk.bold('ADD THIS TO YOUR APP (one-time only):')}`);
288
- this.log(` ${SEPARATOR}\n`);
289
- this.log(` ${chalk.cyan(sdkSnippet.install ?? SDK_INSTALL_COMMAND)}\n`);
290
- this.log(` ${chalk.dim(SDK_IMPORT_SNIPPET)}\n`);
291
- if (sdkSnippet.examples) {
292
- this.log(` ${chalk.dim('// Example usage:')}`);
293
- this.log(` ${chalk.dim(sdkSnippet.examples.track)}`);
294
- this.log(` ${chalk.dim(sdkSnippet.examples.identify)}\n`);
295
- }
296
- const trackCalls = [...new Set(sdkSnippet.trackCalls ?? [])];
297
- for (const call of trackCalls) {
298
- this.log(` ${chalk.dim(call)}`);
299
- }
300
- if (trackCalls.length > 0)
301
- this.log('');
302
- const identifyCalls = [...new Set(sdkSnippet.identifyCalls ?? [])];
303
- for (const call of identifyCalls) {
304
- this.log(` ${chalk.dim(call)}`);
305
- }
306
- if (identifyCalls.length > 0)
307
- this.log('');
308
- this.log(` Full SDK docs: ${chalk.cyan('mailmodo.com/docs/sdk')}\n`);
309
- this.log(` ${SEPARATOR}\n`);
310
- }
311
- async runDomainSetup(yamlConfig, flags) {
312
- const inputs = await this.collectDomainSetupInputs(yamlConfig, flags.yes);
313
- const { dnsRecords, dnsGuideUrl } = await this.registerDomain(yamlConfig, inputs, flags.json);
314
- this.logDnsRecords(dnsRecords, dnsGuideUrl, flags.json);
315
- if (flags.yes) {
316
- return this.verifyDomain(flags.json, inputs.domain);
317
- }
318
- const action = await input({
319
- default: '',
320
- message: PROMPTS.ENTER_AFTER_RECORDS,
321
- });
322
- if (action.toLowerCase() === 'skip') {
323
- this.log(`\n ${INFO.SEQUENCES_NOT_DEPLOYED}`);
324
- this.log(` ${INFO.DOMAIN_NOT_DEPLOYED_HINT}\n`);
325
- return false;
326
- }
327
- return this.verifyDomain(flags.json, inputs.domain);
328
- }
329
- async verifyDomain(jsonOutput, domain) {
330
- const verify = await this.withApiSpinner({ json: jsonOutput, text: ' Checking DNS...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY, {
331
- domain,
332
- }));
333
- if (!verify.ok) {
334
- this.handleApiError(verify);
335
- }
336
- const { dkim, dmarc, dnsGuideUrl, domainStatus, returnPath } = verify.data;
337
- const allPassed = domainStatus === 'VERIFIED';
338
- if (!jsonOutput) {
339
- this.log(` DKIM ${dkim ? chalk.green('✓') : chalk.red('✗')}`);
340
- this.log(` DMARC ${dmarc ? chalk.green('✓') : chalk.red('✗')}`);
341
- this.log(` Return-Path ${returnPath ? chalk.green('✓') : chalk.red('✗')}`);
342
- if (allPassed) {
343
- this.log(`\n ${chalk.green('Domain verified.')} Continuing deploy...\n`);
344
- }
345
- else {
346
- this.log(`\n ${INFO.DNS_RECORDS_FAILED}`);
347
- this.log(`\n ${INFO.DNS_FIX_AND_VERIFY}`);
348
- if (dnsGuideUrl)
349
- this.log(` Help: ${chalk.cyan(dnsGuideUrl)}\n`);
350
- }
351
- }
352
- return allPassed;
353
- }
354
103
  }
@@ -7,8 +7,5 @@ export default class Deployments extends BaseCommand {
7
7
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
8
  };
9
9
  run(): Promise<void>;
10
- private renderTable;
11
- private statusColor;
12
- private colWidth;
13
- private formatDate;
10
+ private makeCtx;
14
11
  }
@@ -1,6 +1,6 @@
1
- import chalk from 'chalk';
2
1
  import { BaseCommand } from '../../lib/base-command.js';
3
2
  import { API_ENDPOINTS } from '../../lib/constants.js';
3
+ import { renderDeploymentsTable } from '../../lib/commands/deployments/output.js';
4
4
  export default class Deployments extends BaseCommand {
5
5
  static description = 'List every deployed sequence on this account, with the IDs needed for deploy --pause / --resume';
6
6
  static examples = [
@@ -13,64 +13,23 @@ export default class Deployments extends BaseCommand {
13
13
  async run() {
14
14
  const { flags } = await this.parse(Deployments);
15
15
  await this.ensureAuth();
16
- const response = await this.withApiSpinner({ json: flags.json, text: ' Loading deployments...' }, () => this.apiClient.get(API_ENDPOINTS.SEQUENCES));
16
+ const ctx = this.makeCtx();
17
+ const response = await ctx.spinner(' Loading deployments...', flags.json, () => ctx.get(API_ENDPOINTS.SEQUENCES));
17
18
  if (!response.ok) {
18
- this.handleApiError(response);
19
+ ctx.onApiError(response);
19
20
  }
20
21
  if (flags.json) {
21
22
  this.log(JSON.stringify(response.data, null, 2));
22
23
  return;
23
24
  }
24
- this.renderTable(response.data);
25
+ renderDeploymentsTable(ctx, response.data);
25
26
  }
26
- renderTable(data) {
27
- const sequences = data.sequences ?? [];
28
- if (sequences.length === 0) {
29
- this.log(`\n ${chalk.dim('No deployed sequences yet.')}`);
30
- this.log(` Run ${chalk.cyan('mailmodo deploy')} to deploy one.\n`);
31
- return;
32
- }
33
- const rows = sequences.map((seq) => ({
34
- emails: String(seq.emailCount ?? 0),
35
- product: seq.productName ?? '',
36
- sequenceId: seq.sequenceId ?? '',
37
- status: seq.status ?? '',
38
- updated: this.formatDate(seq.updatedAt),
39
- }));
40
- const widths = {
41
- emails: this.colWidth(rows, 'emails', 'Emails'),
42
- product: this.colWidth(rows, 'product', 'Product'),
43
- sequenceId: this.colWidth(rows, 'sequenceId', 'Sequence ID'),
44
- status: this.colWidth(rows, 'status', 'Status'),
45
- updated: this.colWidth(rows, 'updated', 'Updated'),
27
+ makeCtx() {
28
+ return {
29
+ get: (path, params) => this.apiClient.get(path, params),
30
+ log: (msg) => this.log(msg),
31
+ onApiError: (r) => this.handleApiError(r),
32
+ spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
46
33
  };
47
- this.log(`\n ${chalk.bold(String(sequences.length))} deployed ${sequences.length === 1 ? 'sequence' : 'sequences'}:\n`);
48
- this.log(` ${chalk.bold('Product'.padEnd(widths.product))}${chalk.bold('Status'.padEnd(widths.status))}${chalk.bold('Emails'.padEnd(widths.emails))}${chalk.bold('Sequence ID'.padEnd(widths.sequenceId))}${chalk.bold('Updated')}`);
49
- this.log(` ${'─'.repeat(widths.product + widths.status + widths.emails + widths.sequenceId + widths.updated)}`);
50
- for (const row of rows) {
51
- const status = this.statusColor(row.status)(row.status.padEnd(widths.status));
52
- this.log(` ${row.product.padEnd(widths.product)}${status}${row.emails.padEnd(widths.emails)}${chalk.cyan(row.sequenceId.padEnd(widths.sequenceId))}${chalk.dim(row.updated)}`);
53
- }
54
- this.log('');
55
- this.log(` Pause: ${chalk.cyan('mailmodo deploy --pause <sequence-id>')}`);
56
- this.log(` Resume: ${chalk.cyan('mailmodo deploy --resume <sequence-id>')}\n`);
57
- }
58
- statusColor(status) {
59
- if (status === 'active')
60
- return chalk.green;
61
- if (status === 'paused')
62
- return chalk.yellow;
63
- return chalk.white;
64
- }
65
- colWidth(rows, key, header) {
66
- return Math.max(...rows.map((r) => r[key].length), header.length) + 2;
67
- }
68
- formatDate(iso) {
69
- if (!iso)
70
- return '';
71
- const parsed = new Date(iso);
72
- if (Number.isNaN(parsed.getTime()))
73
- return iso;
74
- return parsed.toISOString().slice(0, 10);
75
34
  }
76
35
  }
@@ -9,18 +9,5 @@ export default class Domain extends BaseCommand {
9
9
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
10
  };
11
11
  run(): Promise<void>;
12
- /**
13
- * Interactive domain setup: collects domain, sender email, and business address,
14
- * then calls the API to retrieve the required DNS records.
15
- */
16
- private setupDomain;
17
- /**
18
- * Calls the domain verification API and displays pass/fail for each DNS record.
19
- */
20
- private verifyDomain;
21
- /**
22
- * Displays domain health metrics including verification status,
23
- * bounce rate, and spam complaint rate.
24
- */
25
- private showDomainStatus;
12
+ private makeCtx;
26
13
  }
@@ -1,9 +1,7 @@
1
1
  import { Flags } from '@oclif/core';
2
- import { input } from '@inquirer/prompts';
3
- import chalk from 'chalk';
4
2
  import { BaseCommand } from '../../lib/base-command.js';
5
- import { API_ENDPOINTS } from '../../lib/constants.js';
6
- import { ERRORS, INFO, PROMPTS, SEPARATOR } from '../../lib/messages.js';
3
+ import { getDomainOrError, setupDomain, showDomainStatus, } from '../../lib/commands/domain/setup.js';
4
+ import { verifyDomain } from '../../lib/commands/domain/verify.js';
7
5
  export default class Domain extends BaseCommand {
8
6
  static description = 'Set up and verify your sending domain';
9
7
  static examples = [
@@ -25,111 +23,32 @@ export default class Domain extends BaseCommand {
25
23
  async run() {
26
24
  const { flags } = await this.parse(Domain);
27
25
  await this.ensureAuth();
26
+ const ctx = this.makeCtx();
28
27
  if (flags.verify) {
29
28
  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);
29
+ const domain = getDomainOrError(ctx, yamlConfig);
30
+ await verifyDomain(ctx, { domain, json: flags.json });
35
31
  return;
36
32
  }
37
33
  if (flags.status) {
38
34
  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
+ const domain = getDomainOrError(ctx, yamlConfig);
36
+ await showDomainStatus(ctx, { domain, json: flags.json });
44
37
  return;
45
38
  }
46
- await this.setupDomain(flags);
47
- }
48
- /**
49
- * Interactive domain setup: collects domain, sender email, and business address,
50
- * then calls the API to retrieve the required DNS records.
51
- */
52
- async setupDomain(flags) {
53
39
  const yamlConfig = await this.ensureYaml();
54
- this.log(`\n ${SEPARATOR}`);
55
- this.log(` ${chalk.bold('DOMAIN SETUP')}`);
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);
59
- if (flags.json) {
60
- this.log(JSON.stringify({ dnsRecords, domain: inputs.domain }, null, 2));
61
- return;
62
- }
63
- this.logDnsRecords(dnsRecords, dnsGuideUrl, flags.json);
64
- if (!flags.yes) {
65
- const action = await input({
66
- default: '',
67
- message: PROMPTS.ENTER_AFTER_RECORDS,
68
- });
69
- if (action.toLowerCase() !== 'skip') {
70
- await this.verifyDomain(false, inputs.domain);
71
- }
72
- }
40
+ await setupDomain(ctx, yamlConfig, flags);
73
41
  }
74
- /**
75
- * Calls the domain verification API and displays pass/fail for each DNS record.
76
- */
77
- async verifyDomain(jsonOutput, domain) {
78
- const response = await this.withApiSpinner({ json: jsonOutput, text: ' Checking DNS...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY, {
79
- domain,
80
- }));
81
- if (!response.ok) {
82
- this.handleApiError(response);
83
- }
84
- const { dkim, dmarc, dnsGuideUrl, returnPath, domainStatus } = response.data;
85
- if (jsonOutput) {
86
- this.log(JSON.stringify({ dkim, dmarc, returnPath, domainStatus }, null, 2));
87
- return;
88
- }
89
- this.log(` DKIM ${dkim ? chalk.green('✓') : chalk.red('✗ Not found')}`);
90
- this.log(` DMARC ${dmarc ? chalk.green('✓') : chalk.red('✗ Not found')}`);
91
- this.log(` Return Path ${returnPath ? chalk.green('✓') : chalk.red('✗ Not found')}`);
92
- const allPassed = domainStatus === 'VERIFIED';
93
- if (allPassed) {
94
- this.log(`\n ${chalk.green('✓')} Domain verified.\n`);
95
- }
96
- else {
97
- this.log(`\n ${INFO.DNS_RECORDS_FAILED}`);
98
- if (!dkim) {
99
- this.log(`\n DKIM common mistakes:`);
100
- this.log(` - Using CNAME instead of TXT record type`);
101
- this.log(` - Including the full domain in the Host field`);
102
- this.log(` - Cloudflare: proxy must be OFF (grey cloud, not orange)`);
103
- }
104
- if (!returnPath) {
105
- this.log(`\n Return Path common mistakes:`);
106
- this.log(` - Missing or incorrect CNAME for mm-bounce subdomain`);
107
- this.log(` - Cloudflare: proxy must be OFF (grey cloud, not orange)`);
108
- }
109
- this.log(`\n ${INFO.DNS_FIX_AND_VERIFY}`);
110
- if (dnsGuideUrl)
111
- this.log(` Help: ${chalk.cyan(dnsGuideUrl)}\n`);
112
- }
113
- }
114
- /**
115
- * Displays domain health metrics including verification status,
116
- * bounce rate, and spam complaint rate.
117
- */
118
- async showDomainStatus(jsonOutput, domain) {
119
- const response = await this.withApiSpinner({ json: jsonOutput, text: ' Loading domain status...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_STATUS, {
120
- domain,
121
- }));
122
- if (!response.ok) {
123
- this.handleApiError(response);
124
- }
125
- const { data } = response;
126
- if (jsonOutput) {
127
- this.log(JSON.stringify(data, null, 2));
128
- return;
129
- }
130
- this.log(`\n Domain: ${chalk.bold(data.domain || 'not configured')}`);
131
- this.log(` Status: ${data.verified ? chalk.green('✓ verified') : chalk.red('✗ not verified')}`);
132
- this.log(` Bounce rate: ${data.bounceRate ?? 'N/A'}%`);
133
- this.log(` Spam rate: ${data.spamRate ?? 'N/A'}%\n`);
42
+ makeCtx() {
43
+ return {
44
+ collectDomainInputs: (yaml, skip) => this.collectDomainSetupInputs(yaml, skip),
45
+ error: (msg) => this.error(msg),
46
+ get: (path, params) => this.apiClient.get(path, params),
47
+ log: (msg) => this.log(msg),
48
+ onApiError: (r) => this.handleApiError(r),
49
+ registerDomainAndSave: (yaml, inputs, json) => this.registerDomain(yaml, inputs, json),
50
+ showDnsRecords: (records, url, json) => this.logDnsRecords(records, url, json),
51
+ spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
52
+ };
134
53
  }
135
54
  }
@@ -11,24 +11,6 @@ export default class Edit extends BaseCommand {
11
11
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
12
  };
13
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;
24
- private showFieldDiff;
25
- private stripHtml;
26
- private truncate;
27
- private showHtmlChange;
28
- private showUnchangedField;
29
- private showUnchangedHtml;
30
- private showSuggestedChanges;
31
- private showUnchanged;
32
- private buildDiffPreview;
33
- private showChangeSummary;
14
+ private makeCtx;
15
+ private makeRegenCtx;
34
16
  }