@mailmodo/cli 0.0.55 → 0.0.56-beta.pr58.101

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 (138) hide show
  1. package/dist/commands/billing/index.d.ts +1 -11
  2. package/dist/commands/billing/index.js +28 -184
  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.js +12 -7
  6. package/dist/commands/deployments/index.d.ts +1 -4
  7. package/dist/commands/deployments/index.js +11 -52
  8. package/dist/commands/domain/index.d.ts +1 -14
  9. package/dist/commands/domain/index.js +19 -100
  10. package/dist/commands/edit/index.d.ts +2 -20
  11. package/dist/commands/edit/index.js +33 -258
  12. package/dist/commands/emails/index.d.ts +1 -2
  13. package/dist/commands/emails/index.js +26 -91
  14. package/dist/commands/init/index.d.ts +1 -3
  15. package/dist/commands/init/index.js +51 -200
  16. package/dist/commands/login/index.d.ts +2 -0
  17. package/dist/commands/login/index.js +32 -79
  18. package/dist/commands/logs/index.d.ts +1 -8
  19. package/dist/commands/logs/index.js +12 -55
  20. package/dist/commands/preview/index.d.ts +1 -19
  21. package/dist/commands/preview/index.js +32 -212
  22. package/dist/commands/sdk/index.d.ts +1 -3
  23. package/dist/commands/sdk/index.js +14 -46
  24. package/dist/commands/settings/index.d.ts +1 -22
  25. package/dist/commands/settings/index.js +34 -246
  26. package/dist/commands/status/index.d.ts +1 -0
  27. package/dist/commands/status/index.js +13 -39
  28. package/dist/lib/base-command.d.ts +38 -10
  29. package/dist/lib/base-command.js +171 -18
  30. package/dist/lib/commands/billing/checkout-status.d.ts +3 -0
  31. package/dist/lib/commands/billing/checkout-status.js +63 -0
  32. package/dist/lib/commands/billing/format.d.ts +7 -0
  33. package/dist/lib/commands/billing/format.js +63 -0
  34. package/dist/lib/commands/billing/purchase-cap.d.ts +7 -0
  35. package/dist/lib/commands/billing/purchase-cap.js +57 -0
  36. package/dist/lib/commands/billing/types.d.ts +72 -0
  37. package/dist/lib/commands/contacts/actions.d.ts +3 -0
  38. package/dist/lib/commands/contacts/actions.js +49 -0
  39. package/dist/lib/commands/contacts/export-delete.d.ts +9 -0
  40. package/dist/lib/commands/contacts/export-delete.js +51 -0
  41. package/dist/lib/commands/contacts/types.d.ts +35 -0
  42. package/dist/lib/commands/contacts/types.js +1 -0
  43. package/dist/lib/{deploy → commands/deploy}/domain-setup.d.ts +1 -1
  44. package/dist/lib/{deploy → commands/deploy}/domain-setup.js +2 -2
  45. package/dist/lib/{deploy → commands/deploy}/output.d.ts +1 -1
  46. package/dist/lib/{deploy → commands/deploy}/output.js +2 -2
  47. package/dist/lib/{deploy → commands/deploy}/payload.d.ts +1 -1
  48. package/dist/lib/{deploy → commands/deploy}/payload.js +2 -2
  49. package/dist/lib/{deploy → commands/deploy}/sequence-status.js +2 -2
  50. package/dist/lib/{deploy → commands/deploy}/types.d.ts +4 -4
  51. package/dist/lib/commands/deploy/types.js +1 -0
  52. package/dist/lib/commands/deployments/output.d.ts +2 -0
  53. package/dist/lib/commands/deployments/output.js +68 -0
  54. package/dist/lib/commands/deployments/types.d.ts +24 -0
  55. package/dist/lib/commands/deployments/types.js +1 -0
  56. package/dist/lib/commands/domain/setup.d.ts +8 -0
  57. package/dist/lib/commands/domain/setup.js +53 -0
  58. package/dist/lib/commands/domain/types.d.ts +56 -0
  59. package/dist/lib/commands/domain/types.js +1 -0
  60. package/dist/lib/commands/domain/verify.d.ts +5 -0
  61. package/dist/lib/commands/domain/verify.js +50 -0
  62. package/dist/lib/commands/edit/diff.d.ts +7 -0
  63. package/dist/lib/commands/edit/diff.js +65 -0
  64. package/dist/lib/commands/edit/display.d.ts +5 -0
  65. package/dist/lib/commands/edit/display.js +53 -0
  66. package/dist/lib/commands/edit/flow.d.ts +8 -0
  67. package/dist/lib/commands/edit/flow.js +70 -0
  68. package/dist/lib/commands/edit/persist.d.ts +5 -0
  69. package/dist/lib/commands/edit/persist.js +67 -0
  70. package/dist/lib/commands/edit/types.d.ts +38 -0
  71. package/dist/lib/commands/edit/types.js +1 -0
  72. package/dist/lib/commands/emails/editor.d.ts +2 -0
  73. package/dist/lib/commands/emails/editor.js +43 -0
  74. package/dist/lib/commands/emails/output.d.ts +4 -0
  75. package/dist/lib/commands/emails/output.js +36 -0
  76. package/dist/lib/commands/emails/types.d.ts +3 -0
  77. package/dist/lib/commands/emails/types.js +1 -0
  78. package/dist/lib/commands/init/analysis.d.ts +3 -0
  79. package/dist/lib/commands/init/analysis.js +73 -0
  80. package/dist/lib/commands/init/output.d.ts +12 -0
  81. package/dist/lib/commands/init/output.js +39 -0
  82. package/dist/lib/commands/init/payload.d.ts +8 -0
  83. package/dist/lib/commands/init/payload.js +78 -0
  84. package/dist/lib/commands/init/types.d.ts +57 -0
  85. package/dist/lib/commands/init/types.js +1 -0
  86. package/dist/lib/commands/login/output.d.ts +8 -0
  87. package/dist/lib/commands/login/output.js +40 -0
  88. package/dist/lib/commands/login/types.d.ts +19 -0
  89. package/dist/lib/commands/login/types.js +1 -0
  90. package/dist/lib/commands/logs/output.d.ts +2 -0
  91. package/dist/lib/commands/logs/output.js +52 -0
  92. package/dist/lib/commands/logs/types.d.ts +23 -0
  93. package/dist/lib/commands/logs/types.js +1 -0
  94. package/dist/lib/commands/preview/actions.d.ts +11 -0
  95. package/dist/lib/commands/preview/actions.js +43 -0
  96. package/dist/lib/commands/preview/render.d.ts +3 -0
  97. package/dist/lib/commands/preview/render.js +30 -0
  98. package/dist/lib/commands/preview/server.d.ts +8 -0
  99. package/dist/lib/commands/preview/server.js +63 -0
  100. package/dist/lib/commands/preview/types.d.ts +22 -0
  101. package/dist/lib/commands/preview/types.js +1 -0
  102. package/dist/lib/commands/preview/wrapper-html.d.ts +2 -0
  103. package/dist/lib/commands/preview/wrapper-html.js +35 -0
  104. package/dist/lib/commands/sdk/output.d.ts +2 -0
  105. package/dist/lib/commands/sdk/output.js +42 -0
  106. package/dist/lib/commands/sdk/types.d.ts +21 -0
  107. package/dist/lib/commands/sdk/types.js +1 -0
  108. package/dist/lib/commands/settings/actions.d.ts +10 -0
  109. package/dist/lib/commands/settings/actions.js +56 -0
  110. package/dist/lib/commands/settings/display.d.ts +15 -0
  111. package/dist/lib/commands/settings/display.js +69 -0
  112. package/dist/lib/commands/settings/logo-domain.d.ts +3 -0
  113. package/dist/lib/commands/settings/logo-domain.js +47 -0
  114. package/dist/lib/commands/settings/prompt.d.ts +2 -0
  115. package/dist/lib/commands/settings/prompt.js +82 -0
  116. package/dist/lib/commands/settings/types.d.ts +65 -0
  117. package/dist/lib/commands/settings/types.js +1 -0
  118. package/dist/lib/commands/status/output.d.ts +2 -0
  119. package/dist/lib/commands/status/output.js +49 -0
  120. package/dist/lib/commands/status/types.d.ts +28 -0
  121. package/dist/lib/commands/status/types.js +1 -0
  122. package/dist/lib/constants.d.ts +3 -2
  123. package/dist/lib/constants.js +4 -5
  124. package/dist/lib/messages.d.ts +11 -0
  125. package/dist/lib/messages.js +31 -0
  126. package/dist/lib/templates/missing-templates.d.ts +16 -2
  127. package/dist/lib/templates/missing-templates.js +34 -22
  128. package/dist/lib/templates/regenerate.d.ts +10 -0
  129. package/dist/lib/templates/regenerate.js +29 -0
  130. package/dist/lib/templates/sync.d.ts +33 -0
  131. package/dist/lib/templates/sync.js +106 -0
  132. package/dist/lib/templates/types.d.ts +3 -0
  133. package/dist/lib/yaml-config.d.ts +1 -0
  134. package/dist/lib/yaml-config.js +8 -0
  135. package/oclif.manifest.json +100 -100
  136. package/package.json +1 -1
  137. /package/dist/lib/{deploy → commands/billing}/types.js +0 -0
  138. /package/dist/lib/{deploy → commands/deploy}/sequence-status.d.ts +0 -0
@@ -12,15 +12,5 @@ export default class Billing extends BaseCommand {
12
12
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
13
  };
14
14
  run(): Promise<void>;
15
- private startCheckout;
16
- private showStatus;
17
- private formatAutoCharge;
18
- private formatCap;
19
- private formatCurrency;
20
- private formatPaymentMethod;
21
- private formatUsageBlock;
22
- private persistMonthlyCap;
23
- private pluralize;
24
- private purchaseBlocks;
25
- private setCap;
15
+ private makeCtx;
26
16
  }
@@ -1,10 +1,8 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import chalk from 'chalk';
3
- import open from 'open';
4
- import { BaseCommand, FREE_TIER } from '../../lib/base-command.js';
5
- import { API_ENDPOINTS } from '../../lib/constants.js';
6
- import { INFO } from '../../lib/messages.js';
7
- import { loadYaml, saveYaml } from '../../lib/yaml-config.js';
3
+ import { BaseCommand } from '../../lib/base-command.js';
4
+ import { showStatus, startCheckout, } from '../../lib/commands/billing/checkout-status.js';
5
+ import { purchaseBlocks, setCap, } from '../../lib/commands/billing/purchase-cap.js';
8
6
  export default class Billing extends BaseCommand {
9
7
  static description = 'View billing status, purchase blocks, set cap, or add a payment method';
10
8
  static examples = [
@@ -20,16 +18,12 @@ export default class Billing extends BaseCommand {
20
18
  'auto-charge-block-count': Flags.integer({
21
19
  description: 'Blocks to auto-purchase when the quota runs low (use with --cap)',
22
20
  }),
23
- cap: Flags.integer({
24
- description: 'Set monthly sending cap in blocks',
25
- }),
21
+ cap: Flags.integer({ description: 'Set monthly sending cap in blocks' }),
26
22
  checkout: Flags.boolean({
27
23
  default: false,
28
24
  description: 'Open Stripe checkout to add or update a payment method',
29
25
  }),
30
- purchase: Flags.integer({
31
- description: 'Manually purchase email blocks',
32
- }),
26
+ purchase: Flags.integer({ description: 'Manually purchase email blocks' }),
33
27
  status: Flags.boolean({
34
28
  default: false,
35
29
  description: 'Show billing status only',
@@ -38,192 +32,42 @@ export default class Billing extends BaseCommand {
38
32
  async run() {
39
33
  const { flags } = await this.parse(Billing);
40
34
  await this.ensureAuth();
35
+ const ctx = this.makeCtx();
41
36
  const autoChargeBlockCount = flags['auto-charge-block-count'];
42
37
  if (autoChargeBlockCount !== undefined && flags.cap === undefined) {
43
38
  this.error(`Use ${chalk.cyan('--auto-charge-block-count')} together with ${chalk.cyan('--cap')}.`);
44
39
  }
45
40
  if (flags.checkout) {
46
- await this.startCheckout(flags.json);
41
+ await startCheckout(ctx, flags.json);
47
42
  return;
48
43
  }
49
44
  if (flags.purchase !== undefined) {
50
- await this.purchaseBlocks(flags.purchase, flags.json);
45
+ await purchaseBlocks(ctx, flags.purchase, flags.json);
51
46
  return;
52
47
  }
53
48
  if (flags.cap !== undefined) {
54
- await this.setCap(flags.cap, autoChargeBlockCount, flags.json);
55
- return;
56
- }
57
- await this.showStatus(flags.json, flags.status);
58
- }
59
- async startCheckout(jsonOutput) {
60
- const response = await this.withApiSpinner({ json: jsonOutput, text: ' Creating checkout session...' }, () => this.apiClient.post(API_ENDPOINTS.BILLING_CHECKOUT));
61
- if (!response.ok) {
62
- this.handleApiError(response);
63
- }
64
- const { checkoutUrl } = response.data;
65
- if (jsonOutput) {
66
- this.log(JSON.stringify({ checkoutUrl }, null, 2));
67
- return;
68
- }
69
- this.log(`\n ${chalk.bold('Stripe Checkout')} — add or update your payment method.`);
70
- this.log(` ${chalk.dim(checkoutUrl)}\n`);
71
- if (!process.env.CI) {
72
- try {
73
- await open(checkoutUrl);
74
- this.log(` ${INFO.BROWSER_OPENING}\n`);
75
- }
76
- catch {
77
- this.log(` ${INFO.BROWSER_OPEN_FAILED}\n`);
78
- }
79
- }
80
- }
81
- async showStatus(jsonOutput, statusOnly) {
82
- const response = await this.withApiSpinner({ json: jsonOutput, text: ' Loading billing status...' }, () => this.apiClient.get(API_ENDPOINTS.BILLING_STATUS));
83
- if (!response.ok) {
84
- this.handleApiError(response);
85
- }
86
- const { data } = response;
87
- if (jsonOutput) {
88
- this.log(JSON.stringify(data, null, 2));
89
- return;
90
- }
91
- this.log('');
92
- this.log(` Tier: ${data.tier}`);
93
- this.log(` Payment: ${this.formatPaymentMethod(data)}`);
94
- this.log(` Auto-charge: ${this.formatAutoCharge(data)}`);
95
- if (data.tier !== 'free') {
96
- this.log(` Monthly cap: ${this.formatCap(data.cap)}`);
97
- }
98
- this.log(` Total spent: ${this.formatCurrency(data.totalSpent, data.spentCurrency)}`);
99
- if (data.activeBlocks.length === 0) {
100
- this.log(' Active blocks: none');
101
- }
102
- else {
103
- this.log(' Active blocks:');
104
- for (const block of data.activeBlocks) {
105
- this.log(` - ${this.formatUsageBlock(block)}`);
106
- }
107
- }
108
- this.log('');
109
- if (!data.hasPaymentMethod && !statusOnly) {
110
- await this.startCheckout(jsonOutput);
111
- }
112
- }
113
- formatAutoCharge(data) {
114
- if (!data.autoChargeEnabled) {
115
- return 'disabled';
116
- }
117
- if (typeof data.autoChargeBlockCount !== 'number') {
118
- return 'enabled';
119
- }
120
- return `enabled (${data.autoChargeBlockCount} ${this.pluralize('block', data.autoChargeBlockCount)})`;
121
- }
122
- formatCap(cap) {
123
- const hasBlocks = typeof cap.inBlocks === 'number';
124
- const hasEmails = typeof cap.inEmails === 'number';
125
- if (hasBlocks && hasEmails) {
126
- return `${cap.inBlocks} ${this.pluralize('block', cap.inBlocks)} (${cap.inEmails.toLocaleString()} ${this.pluralize('email', cap.inEmails)})`;
127
- }
128
- if (hasBlocks) {
129
- return `${cap.inBlocks} ${this.pluralize('block', cap.inBlocks)}`;
130
- }
131
- if (hasEmails) {
132
- return `${cap.inEmails.toLocaleString()} ${this.pluralize('email', cap.inEmails)}`;
133
- }
134
- return 'not set';
135
- }
136
- formatCurrency(amount, currency) {
137
- const numericAmount = typeof amount === 'number' ? amount : Number.parseFloat(amount);
138
- const normalizedCurrency = currency.toUpperCase();
139
- if (Number.isFinite(numericAmount)) {
140
- try {
141
- return new Intl.NumberFormat('en-US', {
142
- currency: normalizedCurrency,
143
- style: 'currency',
144
- }).format(numericAmount);
145
- }
146
- catch {
147
- return `${numericAmount.toFixed(2)} ${normalizedCurrency}`;
148
- }
149
- }
150
- return `${String(amount)} ${normalizedCurrency}`;
151
- }
152
- formatPaymentMethod(data) {
153
- if (!data.hasPaymentMethod) {
154
- return 'No payment method on file';
155
- }
156
- const primaryMethod = data.paymentMethod[0];
157
- if (!primaryMethod) {
158
- return 'Payment method on file';
159
- }
160
- const brand = primaryMethod.brand || 'Card';
161
- return primaryMethod.last4
162
- ? `${brand} ending ${primaryMethod.last4}`
163
- : `${brand} on file`;
164
- }
165
- formatUsageBlock(block) {
166
- const allowance = block.blockAllowance ?? Math.max(block.blocksCount * block.blockSize, 0);
167
- const used = Math.max(block.emailsSent ?? 0, 0);
168
- const activationSuffix = block.activatedAt
169
- ? `, activated at ${new Date(block.activatedAt).toLocaleDateString('en-US')}`
170
- : '';
171
- return `${block.type} block (${used + allowance}): ${used} ${this.pluralize('email', used)} sent, ${allowance} ${this.pluralize('email', allowance)} remaining${activationSuffix}`;
172
- }
173
- async persistMonthlyCap(capBlocks) {
174
- const yamlConfig = await loadYaml();
175
- if (!yamlConfig)
176
- return;
177
- if (yamlConfig.project.monthlyCap === capBlocks)
178
- return;
179
- yamlConfig.project.monthlyCap = capBlocks;
180
- await saveYaml(yamlConfig);
181
- await this.syncYamlToServer();
182
- }
183
- pluralize(word, count) {
184
- return count === 1 ? word : `${word}s`;
185
- }
186
- async purchaseBlocks(blocksCount, jsonOutput) {
187
- if (blocksCount < 1) {
188
- this.error('Purchase block count must be at least 1 block.');
189
- }
190
- const response = await this.withApiSpinner({ json: jsonOutput, text: ' Initiating block purchase...' }, () => this.apiClient.post(API_ENDPOINTS.BILLING_PURCHASE, {
191
- blocksCount,
192
- }));
193
- if (!response.ok) {
194
- this.handleApiError(response);
195
- }
196
- if (jsonOutput) {
197
- this.log(JSON.stringify(response.data, null, 2));
49
+ await setCap(ctx, {
50
+ autoChargeBlockCount,
51
+ cap: flags.cap,
52
+ json: flags.json,
53
+ });
198
54
  return;
199
55
  }
200
- this.log(`\n ${chalk.green('✓')} ${response.data.message}`);
201
- this.log(` Blocks: ${blocksCount} ${this.pluralize('block', blocksCount)}`);
202
- this.log(` Payment intent:${` ${chalk.dim(response.data.paymentIntentId)}`}`);
203
- this.log('');
56
+ await showStatus(ctx, flags.json, flags.status);
204
57
  }
205
- async setCap(cap, autoChargeBlockCount, jsonOutput) {
206
- this.validateBillingCapInputs({ autoChargeBlockCount, cap });
207
- const tier = await this.fetchBillingTier();
208
- if (tier === FREE_TIER) {
209
- this.warnFreeTierCapBlocked(jsonOutput);
210
- return;
211
- }
212
- const data = await this.applyBillingCap({
213
- autoChargeBlockCount,
214
- cap,
215
- json: jsonOutput,
216
- });
217
- await this.persistMonthlyCap(data.capBlocks);
218
- if (jsonOutput) {
219
- this.log(JSON.stringify(data, null, 2));
220
- return;
221
- }
222
- this.log(`\n ${chalk.green('✓')} ${data.message}`);
223
- this.log(` Monthly cap: ${chalk.bold(String(data.capBlocks))} ${this.pluralize('block', data.capBlocks)} (${data.capEmails.toLocaleString()} emails)`);
224
- if (data.autoChargeBlockCount !== undefined) {
225
- this.log(` Auto-charge: ${data.autoChargeBlockCount} ${this.pluralize('block', data.autoChargeBlockCount)}`);
226
- }
227
- this.log('');
58
+ makeCtx() {
59
+ return {
60
+ applyBillingCap: (opts) => this.applyBillingCap(opts),
61
+ error: (msg) => this.error(msg),
62
+ fetchBillingTier: () => this.fetchBillingTier(),
63
+ get: (path, params) => this.apiClient.get(path, params),
64
+ log: (msg) => this.log(msg),
65
+ onApiError: (r) => this.handleApiError(r),
66
+ post: (path, body) => this.apiClient.post(path, body),
67
+ spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
68
+ syncYaml: () => this.syncYamlToServer(),
69
+ validateBillingCapInputs: (opts) => this.validateBillingCapInputs(opts),
70
+ warnFreeTierCapBlocked: (json) => this.warnFreeTierCapBlocked(json),
71
+ };
228
72
  }
229
73
  }
@@ -10,23 +10,5 @@ export default class Contacts extends BaseCommand {
10
10
  yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
11
  };
12
12
  run(): Promise<void>;
13
- /**
14
- * Displays aggregate contact counts: total, active, unsubscribed, bounced.
15
- */
16
- private showSummary;
17
- /**
18
- * Looks up a single contact by email address and displays their
19
- * status, properties, and pending emails.
20
- */
21
- private searchContact;
22
- /**
23
- * Triggers a CSV export of all contacts. The API returns a download URL
24
- * for the generated file.
25
- */
26
- private exportContacts;
27
- /**
28
- * Performs a GDPR-compliant hard delete of a contact and all their
29
- * send history. Requires confirmation unless --yes flag is used.
30
- */
31
- private deleteContact;
13
+ private makeCtx;
32
14
  }
@@ -1,10 +1,7 @@
1
1
  import { Flags } from '@oclif/core';
2
- import { confirm } from '@inquirer/prompts';
3
- import chalk from 'chalk';
4
- import { writeFile } from 'node:fs/promises';
5
- import { resolve } from 'node:path';
6
2
  import { BaseCommand } from '../../lib/base-command.js';
7
- import { API_ENDPOINTS } from '../../lib/constants.js';
3
+ import { deleteContact, exportContacts, } from '../../lib/commands/contacts/export-delete.js';
4
+ import { searchContact, showSummary, } from '../../lib/commands/contacts/actions.js';
8
5
  export default class Contacts extends BaseCommand {
9
6
  static description = 'Manage contacts — search, export, or delete';
10
7
  static examples = [
@@ -27,124 +24,34 @@ export default class Contacts extends BaseCommand {
27
24
  async run() {
28
25
  const { flags } = await this.parse(Contacts);
29
26
  await this.ensureAuth();
27
+ const ctx = this.makeCtx();
30
28
  if (flags.search) {
31
- await this.searchContact(flags.search, flags.json);
29
+ await searchContact(ctx, flags.search, flags.json);
32
30
  return;
33
31
  }
34
32
  if (flags.export) {
35
- await this.exportContacts(flags.json);
33
+ await exportContacts(ctx, { json: flags.json });
36
34
  return;
37
35
  }
38
36
  if (flags.delete) {
39
- await this.deleteContact(flags.delete, flags.json, flags.yes);
40
- return;
41
- }
42
- await this.showSummary(flags.json);
43
- }
44
- /**
45
- * Displays aggregate contact counts: total, active, unsubscribed, bounced.
46
- */
47
- async showSummary(jsonOutput) {
48
- const response = await this.withApiSpinner({ json: jsonOutput, text: ' Loading contacts...' }, () => this.apiClient.get(API_ENDPOINTS.CONTACTS));
49
- if (!response.ok) {
50
- this.handleApiError(response);
51
- }
52
- const { data } = response;
53
- if (jsonOutput) {
54
- this.log(JSON.stringify(data, null, 2));
55
- return;
56
- }
57
- this.log(`\n Total: ${chalk.bold(String(data.total ?? 0))} ` +
58
- `Active: ${chalk.green(String(data.active ?? 0))} ` +
59
- `Unsubscribed: ${chalk.yellow(String(data.unsubscribed ?? 0))} ` +
60
- `Bounced: ${chalk.red(String(data.bounced ?? 0))}\n`);
61
- }
62
- /**
63
- * Looks up a single contact by email address and displays their
64
- * status, properties, and pending emails.
65
- */
66
- async searchContact(email, jsonOutput) {
67
- const response = await this.withApiSpinner({ json: jsonOutput, text: ' Looking up contact...' }, () => this.apiClient.get(`${API_ENDPOINTS.CONTACTS}/${encodeURIComponent(email)}`));
68
- if (!response.ok) {
69
- if (response.status === 404) {
70
- if (jsonOutput) {
71
- this.log(JSON.stringify({ email, error: 'Contact not found' }, null, 2));
72
- }
73
- else {
74
- this.log(`\n Contact '${email}' not found.\n`);
75
- }
76
- return;
77
- }
78
- this.handleApiError(response);
79
- }
80
- const contact = response.data;
81
- if (jsonOutput) {
82
- this.log(JSON.stringify(contact, null, 2));
83
- return;
84
- }
85
- this.log(`\n ${chalk.bold('Email:')} ${contact.email}`);
86
- this.log(` ${chalk.bold('Status:')} ${contact.status}`);
87
- this.log(` ${chalk.bold('Added:')} ${contact.added}`);
88
- this.log(` ${chalk.bold('Next email:')} ${contact.nextEmail || 'None'}`);
89
- if (contact.properties && Object.keys(contact.properties).length > 0) {
90
- this.log(` ${chalk.bold('Properties:')}`);
91
- for (const [key, value] of Object.entries(contact.properties)) {
92
- this.log(` ${key}: ${String(value)}`);
93
- }
94
- }
95
- this.log('');
96
- }
97
- /**
98
- * Triggers a CSV export of all contacts. The API returns a download URL
99
- * for the generated file.
100
- */
101
- async exportContacts(jsonOutput) {
102
- const response = await this.withApiSpinner({ json: jsonOutput, text: ' Preparing contact export...' }, () => this.apiClient.get(API_ENDPOINTS.CONTACTS_EXPORT));
103
- if (!response.ok) {
104
- this.handleApiError(response);
105
- }
106
- if (jsonOutput) {
107
- this.log(JSON.stringify(response.data, null, 2));
108
- return;
109
- }
110
- this.log(`\n ${chalk.green('✓')} Contact export started.`);
111
- const { downloadUrl, status } = response.data;
112
- if (!downloadUrl) {
113
- this.log(`\n Export status: ${status ?? 'unknown'}. No download URL yet.\n`);
114
- return;
115
- }
116
- const fileResult = await this.withApiSpinner({ json: jsonOutput, text: ' Downloading CSV file...' }, () => this.apiClient.getPublicFile(downloadUrl.trim()));
117
- if (!fileResult.ok) {
118
- this.error(`Download failed: ${fileResult.status} ${fileResult.error ?? ''}\n` +
119
- ` URL: ${fileResult.debug.fullUrl}`);
120
- }
121
- const outputPath = resolve('contacts.csv');
122
- await writeFile(outputPath, Buffer.from(fileResult.body));
123
- this.log(`\n ${chalk.green('✓')} Contact export saved to ${chalk.cyan(outputPath)}\n`);
124
- }
125
- /**
126
- * Performs a GDPR-compliant hard delete of a contact and all their
127
- * send history. Requires confirmation unless --yes flag is used.
128
- */
129
- async deleteContact(email, jsonOutput, skipConfirm) {
130
- if (!skipConfirm) {
131
- const confirmed = await confirm({
132
- default: false,
133
- message: `Permanently delete ${email} and all their data? This cannot be undone.`,
37
+ await deleteContact(ctx, {
38
+ email: flags.delete,
39
+ json: flags.json,
40
+ skipConfirm: flags.yes,
134
41
  });
135
- if (!confirmed) {
136
- this.log('\n Delete cancelled.\n');
137
- return;
138
- }
139
- }
140
- const response = await this.withApiSpinner({ json: jsonOutput, text: ' Deleting contact...' }, () => this.apiClient.delete(`${API_ENDPOINTS.CONTACTS}/${encodeURIComponent(email)}`));
141
- if (!response.ok) {
142
- this.handleApiError(response);
143
- }
144
- if (jsonOutput) {
145
- this.log(JSON.stringify({ deleted: true, email }, null, 2));
146
42
  return;
147
43
  }
148
- this.log(`\n ${chalk.green('✓')} Contact ${email} permanently deleted.\n`);
44
+ await showSummary(ctx, flags.json);
45
+ }
46
+ makeCtx() {
47
+ return {
48
+ delete: (path) => this.apiClient.delete(path),
49
+ error: (msg) => this.error(msg),
50
+ get: (path, params) => this.apiClient.get(path, params),
51
+ getPublicFile: (url) => this.apiClient.getPublicFile(url),
52
+ log: (msg) => this.log(msg),
53
+ onApiError: (r) => this.handleApiError(r),
54
+ spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
55
+ };
149
56
  }
150
57
  }
@@ -3,11 +3,11 @@ import { confirm } from '@inquirer/prompts';
3
3
  import chalk from 'chalk';
4
4
  import { BaseCommand } from '../../lib/base-command.js';
5
5
  import { API_ENDPOINTS } from '../../lib/constants.js';
6
- import { MISSING_TEMPLATES } from '../../lib/messages.js';
7
- import { buildDeployPayload } from '../../lib/deploy/payload.js';
8
- import { logDeploySuccessInstructions, logPreDeploySummary, } from '../../lib/deploy/output.js';
9
- import { pauseSequence, resumeSequence, } from '../../lib/deploy/sequence-status.js';
10
- import { ensureDomainReady, validateDeploySequence, } from '../../lib/deploy/domain-setup.js';
6
+ import { MISSING_TEMPLATES, restoredFromServerHint, } 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
11
  import { getMissingTemplateIds, handleMissingTemplates, } from '../../lib/templates/missing-templates.js';
12
12
  export default class Deploy extends BaseCommand {
13
13
  static description = 'Deploy, pause, or resume an email sequence';
@@ -44,8 +44,10 @@ export default class Deploy extends BaseCommand {
44
44
  const yamlConfig = await this.ensureYaml();
45
45
  const missingIds = getMissingTemplateIds(yamlConfig);
46
46
  if (missingIds.length > 0) {
47
- const regenerated = await handleMissingTemplates(ctx, yamlConfig, missingIds, baseFlags);
48
- if (regenerated)
47
+ const result = await handleMissingTemplates(ctx, yamlConfig, missingIds, baseFlags);
48
+ if (result === 'restored')
49
+ ctx.log(`\n ${chalk.green('✓')} ${restoredFromServerHint(missingIds)}\n`);
50
+ else if (result === 'regenerated')
49
51
  ctx.log(`\n ${chalk.green('✓')} ${MISSING_TEMPLATES.REVIEW_HINT}\n`);
50
52
  return;
51
53
  }
@@ -69,6 +71,7 @@ export default class Deploy extends BaseCommand {
69
71
  if (!response.ok)
70
72
  ctx.onApiError(response);
71
73
  await ctx.syncYaml();
74
+ await ctx.syncTemplates(yamlConfig);
72
75
  if (flags.json) {
73
76
  this.log(JSON.stringify({
74
77
  deployed: response.data.deployed,
@@ -86,6 +89,7 @@ export default class Deploy extends BaseCommand {
86
89
  collectDomainInputs: (yaml, skip) => this.collectDomainSetupInputs(yaml, skip),
87
90
  error: (msg) => this.error(msg),
88
91
  exit: (code) => this.exit(code),
92
+ fetchTemplate: (emailId) => this.getTemplateFromServer(emailId),
89
93
  get: (path, params) => this.apiClient.get(path, params),
90
94
  getBillingCap: async () => {
91
95
  const s = await this.fetchBillingStatus();
@@ -97,6 +101,7 @@ export default class Deploy extends BaseCommand {
97
101
  registerDomainAndSave: (yaml, inputs, json) => this.registerDomain(yaml, inputs, json),
98
102
  showDnsRecords: (records, url, json) => this.logDnsRecords(records, url, json),
99
103
  spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
104
+ syncTemplates: (yaml) => this.syncTemplatesToServer(yaml),
100
105
  syncYaml: () => this.syncYamlToServer(),
101
106
  };
102
107
  }
@@ -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
  }