@mailmodo/cli 0.0.55-beta.pr57.93 → 0.0.55
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.
- package/dist/commands/billing/index.d.ts +11 -1
- package/dist/commands/billing/index.js +184 -28
- package/dist/commands/contacts/index.d.ts +19 -1
- package/dist/commands/contacts/index.js +114 -21
- package/dist/commands/deploy/index.js +4 -4
- package/dist/commands/deployments/index.d.ts +4 -1
- package/dist/commands/deployments/index.js +52 -11
- package/dist/commands/domain/index.d.ts +14 -1
- package/dist/commands/domain/index.js +100 -19
- package/dist/commands/edit/index.d.ts +20 -2
- package/dist/commands/edit/index.js +258 -30
- package/dist/commands/emails/index.d.ts +2 -1
- package/dist/commands/emails/index.js +91 -26
- package/dist/commands/init/index.d.ts +3 -1
- package/dist/commands/init/index.js +199 -41
- package/dist/commands/login/index.d.ts +0 -2
- package/dist/commands/login/index.js +76 -32
- package/dist/commands/logs/index.d.ts +8 -1
- package/dist/commands/logs/index.js +55 -12
- package/dist/commands/preview/index.d.ts +19 -1
- package/dist/commands/preview/index.js +212 -30
- package/dist/commands/sdk/index.d.ts +3 -1
- package/dist/commands/sdk/index.js +46 -14
- package/dist/commands/settings/index.d.ts +22 -1
- package/dist/commands/settings/index.js +246 -34
- package/dist/commands/status/index.d.ts +0 -1
- package/dist/commands/status/index.js +39 -13
- package/dist/lib/{commands/deploy → deploy}/domain-setup.d.ts +1 -1
- package/dist/lib/{commands/deploy → deploy}/domain-setup.js +2 -2
- package/dist/lib/{commands/deploy → deploy}/output.d.ts +1 -1
- package/dist/lib/{commands/deploy → deploy}/output.js +2 -2
- package/dist/lib/{commands/deploy → deploy}/payload.d.ts +1 -1
- package/dist/lib/{commands/deploy → deploy}/payload.js +2 -2
- package/dist/lib/{commands/deploy → deploy}/sequence-status.js +2 -2
- package/dist/lib/{commands/deploy → deploy}/types.d.ts +4 -4
- package/dist/lib/templates/missing-templates.d.ts +1 -1
- package/dist/lib/templates/missing-templates.js +1 -1
- package/oclif.manifest.json +54 -54
- package/package.json +1 -1
- package/dist/lib/commands/billing/checkout-status.d.ts +0 -3
- package/dist/lib/commands/billing/checkout-status.js +0 -63
- package/dist/lib/commands/billing/format.d.ts +0 -7
- package/dist/lib/commands/billing/format.js +0 -63
- package/dist/lib/commands/billing/purchase-cap.d.ts +0 -7
- package/dist/lib/commands/billing/purchase-cap.js +0 -57
- package/dist/lib/commands/billing/types.d.ts +0 -72
- package/dist/lib/commands/contacts/actions.d.ts +0 -3
- package/dist/lib/commands/contacts/actions.js +0 -49
- package/dist/lib/commands/contacts/export-delete.d.ts +0 -9
- package/dist/lib/commands/contacts/export-delete.js +0 -51
- package/dist/lib/commands/contacts/types.d.ts +0 -35
- package/dist/lib/commands/contacts/types.js +0 -1
- package/dist/lib/commands/deploy/types.js +0 -1
- package/dist/lib/commands/deployments/output.d.ts +0 -2
- package/dist/lib/commands/deployments/output.js +0 -68
- package/dist/lib/commands/deployments/types.d.ts +0 -24
- package/dist/lib/commands/deployments/types.js +0 -1
- package/dist/lib/commands/domain/setup.d.ts +0 -8
- package/dist/lib/commands/domain/setup.js +0 -53
- package/dist/lib/commands/domain/types.d.ts +0 -56
- package/dist/lib/commands/domain/types.js +0 -1
- package/dist/lib/commands/domain/verify.d.ts +0 -5
- package/dist/lib/commands/domain/verify.js +0 -50
- package/dist/lib/commands/edit/diff.d.ts +0 -7
- package/dist/lib/commands/edit/diff.js +0 -65
- package/dist/lib/commands/edit/display.d.ts +0 -5
- package/dist/lib/commands/edit/display.js +0 -53
- package/dist/lib/commands/edit/flow.d.ts +0 -8
- package/dist/lib/commands/edit/flow.js +0 -70
- package/dist/lib/commands/edit/persist.d.ts +0 -5
- package/dist/lib/commands/edit/persist.js +0 -65
- package/dist/lib/commands/edit/types.d.ts +0 -37
- package/dist/lib/commands/edit/types.js +0 -1
- package/dist/lib/commands/emails/editor.d.ts +0 -2
- package/dist/lib/commands/emails/editor.js +0 -43
- package/dist/lib/commands/emails/output.d.ts +0 -4
- package/dist/lib/commands/emails/output.js +0 -36
- package/dist/lib/commands/emails/types.d.ts +0 -3
- package/dist/lib/commands/emails/types.js +0 -1
- package/dist/lib/commands/init/analysis.d.ts +0 -3
- package/dist/lib/commands/init/analysis.js +0 -69
- package/dist/lib/commands/init/output.d.ts +0 -12
- package/dist/lib/commands/init/output.js +0 -39
- package/dist/lib/commands/init/payload.d.ts +0 -8
- package/dist/lib/commands/init/payload.js +0 -78
- package/dist/lib/commands/init/types.d.ts +0 -57
- package/dist/lib/commands/init/types.js +0 -1
- package/dist/lib/commands/login/output.d.ts +0 -8
- package/dist/lib/commands/login/output.js +0 -53
- package/dist/lib/commands/login/types.d.ts +0 -19
- package/dist/lib/commands/login/types.js +0 -1
- package/dist/lib/commands/logs/output.d.ts +0 -2
- package/dist/lib/commands/logs/output.js +0 -52
- package/dist/lib/commands/logs/types.d.ts +0 -23
- package/dist/lib/commands/logs/types.js +0 -1
- package/dist/lib/commands/preview/actions.d.ts +0 -11
- package/dist/lib/commands/preview/actions.js +0 -43
- package/dist/lib/commands/preview/render.d.ts +0 -3
- package/dist/lib/commands/preview/render.js +0 -30
- package/dist/lib/commands/preview/server.d.ts +0 -8
- package/dist/lib/commands/preview/server.js +0 -63
- package/dist/lib/commands/preview/types.d.ts +0 -19
- package/dist/lib/commands/preview/types.js +0 -1
- package/dist/lib/commands/preview/wrapper-html.d.ts +0 -2
- package/dist/lib/commands/preview/wrapper-html.js +0 -35
- package/dist/lib/commands/sdk/output.d.ts +0 -2
- package/dist/lib/commands/sdk/output.js +0 -42
- package/dist/lib/commands/sdk/types.d.ts +0 -21
- package/dist/lib/commands/sdk/types.js +0 -1
- package/dist/lib/commands/settings/actions.d.ts +0 -10
- package/dist/lib/commands/settings/actions.js +0 -56
- package/dist/lib/commands/settings/display.d.ts +0 -15
- package/dist/lib/commands/settings/display.js +0 -69
- package/dist/lib/commands/settings/logo-domain.d.ts +0 -3
- package/dist/lib/commands/settings/logo-domain.js +0 -47
- package/dist/lib/commands/settings/prompt.d.ts +0 -2
- package/dist/lib/commands/settings/prompt.js +0 -82
- package/dist/lib/commands/settings/types.d.ts +0 -65
- package/dist/lib/commands/settings/types.js +0 -1
- package/dist/lib/commands/status/output.d.ts +0 -2
- package/dist/lib/commands/status/output.js +0 -49
- package/dist/lib/commands/status/types.d.ts +0 -28
- package/dist/lib/commands/status/types.js +0 -1
- /package/dist/lib/{commands/deploy → deploy}/sequence-status.d.ts +0 -0
- /package/dist/lib/{commands/billing → deploy}/types.js +0 -0
|
@@ -12,5 +12,15 @@ export default class Billing extends BaseCommand {
|
|
|
12
12
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
13
|
};
|
|
14
14
|
run(): Promise<void>;
|
|
15
|
-
private
|
|
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;
|
|
16
26
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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';
|
|
6
8
|
export default class Billing extends BaseCommand {
|
|
7
9
|
static description = 'View billing status, purchase blocks, set cap, or add a payment method';
|
|
8
10
|
static examples = [
|
|
@@ -18,12 +20,16 @@ export default class Billing extends BaseCommand {
|
|
|
18
20
|
'auto-charge-block-count': Flags.integer({
|
|
19
21
|
description: 'Blocks to auto-purchase when the quota runs low (use with --cap)',
|
|
20
22
|
}),
|
|
21
|
-
cap: Flags.integer({
|
|
23
|
+
cap: Flags.integer({
|
|
24
|
+
description: 'Set monthly sending cap in blocks',
|
|
25
|
+
}),
|
|
22
26
|
checkout: Flags.boolean({
|
|
23
27
|
default: false,
|
|
24
28
|
description: 'Open Stripe checkout to add or update a payment method',
|
|
25
29
|
}),
|
|
26
|
-
purchase: Flags.integer({
|
|
30
|
+
purchase: Flags.integer({
|
|
31
|
+
description: 'Manually purchase email blocks',
|
|
32
|
+
}),
|
|
27
33
|
status: Flags.boolean({
|
|
28
34
|
default: false,
|
|
29
35
|
description: 'Show billing status only',
|
|
@@ -32,42 +38,192 @@ export default class Billing extends BaseCommand {
|
|
|
32
38
|
async run() {
|
|
33
39
|
const { flags } = await this.parse(Billing);
|
|
34
40
|
await this.ensureAuth();
|
|
35
|
-
const ctx = this.makeCtx();
|
|
36
41
|
const autoChargeBlockCount = flags['auto-charge-block-count'];
|
|
37
42
|
if (autoChargeBlockCount !== undefined && flags.cap === undefined) {
|
|
38
43
|
this.error(`Use ${chalk.cyan('--auto-charge-block-count')} together with ${chalk.cyan('--cap')}.`);
|
|
39
44
|
}
|
|
40
45
|
if (flags.checkout) {
|
|
41
|
-
await startCheckout(
|
|
46
|
+
await this.startCheckout(flags.json);
|
|
42
47
|
return;
|
|
43
48
|
}
|
|
44
49
|
if (flags.purchase !== undefined) {
|
|
45
|
-
await purchaseBlocks(
|
|
50
|
+
await this.purchaseBlocks(flags.purchase, flags.json);
|
|
46
51
|
return;
|
|
47
52
|
}
|
|
48
53
|
if (flags.cap !== undefined) {
|
|
49
|
-
await setCap(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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));
|
|
54
198
|
return;
|
|
55
199
|
}
|
|
56
|
-
|
|
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('');
|
|
57
204
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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('');
|
|
72
228
|
}
|
|
73
229
|
}
|
|
@@ -10,5 +10,23 @@ export default class Contacts extends BaseCommand {
|
|
|
10
10
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
};
|
|
12
12
|
run(): Promise<void>;
|
|
13
|
-
|
|
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;
|
|
14
32
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
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';
|
|
2
6
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
3
|
-
import {
|
|
4
|
-
import { searchContact, showSummary, } from '../../lib/commands/contacts/actions.js';
|
|
7
|
+
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
5
8
|
export default class Contacts extends BaseCommand {
|
|
6
9
|
static description = 'Manage contacts — search, export, or delete';
|
|
7
10
|
static examples = [
|
|
@@ -24,34 +27,124 @@ export default class Contacts extends BaseCommand {
|
|
|
24
27
|
async run() {
|
|
25
28
|
const { flags } = await this.parse(Contacts);
|
|
26
29
|
await this.ensureAuth();
|
|
27
|
-
const ctx = this.makeCtx();
|
|
28
30
|
if (flags.search) {
|
|
29
|
-
await searchContact(
|
|
31
|
+
await this.searchContact(flags.search, flags.json);
|
|
30
32
|
return;
|
|
31
33
|
}
|
|
32
34
|
if (flags.export) {
|
|
33
|
-
await exportContacts(
|
|
35
|
+
await this.exportContacts(flags.json);
|
|
34
36
|
return;
|
|
35
37
|
}
|
|
36
38
|
if (flags.delete) {
|
|
37
|
-
await deleteContact(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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));
|
|
42
108
|
return;
|
|
43
109
|
}
|
|
44
|
-
|
|
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`);
|
|
45
124
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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.`,
|
|
134
|
+
});
|
|
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
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
this.log(`\n ${chalk.green('✓')} Contact ${email} permanently deleted.\n`);
|
|
56
149
|
}
|
|
57
150
|
}
|
|
@@ -4,10 +4,10 @@ import chalk from 'chalk';
|
|
|
4
4
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
5
|
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
6
6
|
import { MISSING_TEMPLATES } from '../../lib/messages.js';
|
|
7
|
-
import { buildDeployPayload } from '../../lib/
|
|
8
|
-
import { logDeploySuccessInstructions, logPreDeploySummary, } from '../../lib/
|
|
9
|
-
import { pauseSequence, resumeSequence, } from '../../lib/
|
|
10
|
-
import { ensureDomainReady, validateDeploySequence, } from '../../lib/
|
|
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';
|
|
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';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
1
2
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
2
3
|
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,23 +13,64 @@ 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
|
|
17
|
-
const response = await ctx.spinner(' Loading deployments...', flags.json, () => ctx.get(API_ENDPOINTS.SEQUENCES));
|
|
16
|
+
const response = await this.withApiSpinner({ json: flags.json, text: ' Loading deployments...' }, () => this.apiClient.get(API_ENDPOINTS.SEQUENCES));
|
|
18
17
|
if (!response.ok) {
|
|
19
|
-
|
|
18
|
+
this.handleApiError(response);
|
|
20
19
|
}
|
|
21
20
|
if (flags.json) {
|
|
22
21
|
this.log(JSON.stringify(response.data, null, 2));
|
|
23
22
|
return;
|
|
24
23
|
}
|
|
25
|
-
|
|
24
|
+
this.renderTable(response.data);
|
|
26
25
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
log
|
|
31
|
-
|
|
32
|
-
|
|
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'),
|
|
33
46
|
};
|
|
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);
|
|
34
75
|
}
|
|
35
76
|
}
|
|
@@ -9,5 +9,18 @@ export default class Domain extends BaseCommand {
|
|
|
9
9
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
10
|
};
|
|
11
11
|
run(): Promise<void>;
|
|
12
|
-
|
|
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;
|
|
13
26
|
}
|