@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.
- package/dist/commands/billing/index.d.ts +1 -11
- package/dist/commands/billing/index.js +28 -184
- package/dist/commands/contacts/index.d.ts +1 -19
- package/dist/commands/contacts/index.js +21 -114
- package/dist/commands/deploy/index.js +12 -7
- package/dist/commands/deployments/index.d.ts +1 -4
- package/dist/commands/deployments/index.js +11 -52
- package/dist/commands/domain/index.d.ts +1 -14
- package/dist/commands/domain/index.js +19 -100
- package/dist/commands/edit/index.d.ts +2 -20
- package/dist/commands/edit/index.js +33 -258
- package/dist/commands/emails/index.d.ts +1 -2
- package/dist/commands/emails/index.js +26 -91
- package/dist/commands/init/index.d.ts +1 -3
- package/dist/commands/init/index.js +51 -200
- package/dist/commands/login/index.d.ts +2 -0
- package/dist/commands/login/index.js +32 -79
- package/dist/commands/logs/index.d.ts +1 -8
- package/dist/commands/logs/index.js +12 -55
- package/dist/commands/preview/index.d.ts +1 -19
- package/dist/commands/preview/index.js +32 -212
- package/dist/commands/sdk/index.d.ts +1 -3
- package/dist/commands/sdk/index.js +14 -46
- package/dist/commands/settings/index.d.ts +1 -22
- package/dist/commands/settings/index.js +34 -246
- package/dist/commands/status/index.d.ts +1 -0
- package/dist/commands/status/index.js +13 -39
- package/dist/lib/base-command.d.ts +38 -10
- package/dist/lib/base-command.js +171 -18
- package/dist/lib/commands/billing/checkout-status.d.ts +3 -0
- package/dist/lib/commands/billing/checkout-status.js +63 -0
- package/dist/lib/commands/billing/format.d.ts +7 -0
- package/dist/lib/commands/billing/format.js +63 -0
- package/dist/lib/commands/billing/purchase-cap.d.ts +7 -0
- package/dist/lib/commands/billing/purchase-cap.js +57 -0
- package/dist/lib/commands/billing/types.d.ts +72 -0
- package/dist/lib/commands/contacts/actions.d.ts +3 -0
- package/dist/lib/commands/contacts/actions.js +49 -0
- package/dist/lib/commands/contacts/export-delete.d.ts +9 -0
- package/dist/lib/commands/contacts/export-delete.js +51 -0
- package/dist/lib/commands/contacts/types.d.ts +35 -0
- package/dist/lib/commands/contacts/types.js +1 -0
- package/dist/lib/{deploy → commands/deploy}/domain-setup.d.ts +1 -1
- package/dist/lib/{deploy → commands/deploy}/domain-setup.js +2 -2
- package/dist/lib/{deploy → commands/deploy}/output.d.ts +1 -1
- package/dist/lib/{deploy → commands/deploy}/output.js +2 -2
- package/dist/lib/{deploy → commands/deploy}/payload.d.ts +1 -1
- package/dist/lib/{deploy → commands/deploy}/payload.js +2 -2
- package/dist/lib/{deploy → commands/deploy}/sequence-status.js +2 -2
- package/dist/lib/{deploy → commands/deploy}/types.d.ts +4 -4
- package/dist/lib/commands/deploy/types.js +1 -0
- package/dist/lib/commands/deployments/output.d.ts +2 -0
- package/dist/lib/commands/deployments/output.js +68 -0
- package/dist/lib/commands/deployments/types.d.ts +24 -0
- package/dist/lib/commands/deployments/types.js +1 -0
- package/dist/lib/commands/domain/setup.d.ts +8 -0
- package/dist/lib/commands/domain/setup.js +53 -0
- package/dist/lib/commands/domain/types.d.ts +56 -0
- package/dist/lib/commands/domain/types.js +1 -0
- package/dist/lib/commands/domain/verify.d.ts +5 -0
- package/dist/lib/commands/domain/verify.js +50 -0
- package/dist/lib/commands/edit/diff.d.ts +7 -0
- package/dist/lib/commands/edit/diff.js +65 -0
- package/dist/lib/commands/edit/display.d.ts +5 -0
- package/dist/lib/commands/edit/display.js +53 -0
- package/dist/lib/commands/edit/flow.d.ts +8 -0
- package/dist/lib/commands/edit/flow.js +70 -0
- package/dist/lib/commands/edit/persist.d.ts +5 -0
- package/dist/lib/commands/edit/persist.js +67 -0
- package/dist/lib/commands/edit/types.d.ts +38 -0
- package/dist/lib/commands/edit/types.js +1 -0
- package/dist/lib/commands/emails/editor.d.ts +2 -0
- package/dist/lib/commands/emails/editor.js +43 -0
- package/dist/lib/commands/emails/output.d.ts +4 -0
- package/dist/lib/commands/emails/output.js +36 -0
- package/dist/lib/commands/emails/types.d.ts +3 -0
- package/dist/lib/commands/emails/types.js +1 -0
- package/dist/lib/commands/init/analysis.d.ts +3 -0
- package/dist/lib/commands/init/analysis.js +73 -0
- package/dist/lib/commands/init/output.d.ts +12 -0
- package/dist/lib/commands/init/output.js +39 -0
- package/dist/lib/commands/init/payload.d.ts +8 -0
- package/dist/lib/commands/init/payload.js +78 -0
- package/dist/lib/commands/init/types.d.ts +57 -0
- package/dist/lib/commands/init/types.js +1 -0
- package/dist/lib/commands/login/output.d.ts +8 -0
- package/dist/lib/commands/login/output.js +40 -0
- package/dist/lib/commands/login/types.d.ts +19 -0
- package/dist/lib/commands/login/types.js +1 -0
- package/dist/lib/commands/logs/output.d.ts +2 -0
- package/dist/lib/commands/logs/output.js +52 -0
- package/dist/lib/commands/logs/types.d.ts +23 -0
- package/dist/lib/commands/logs/types.js +1 -0
- package/dist/lib/commands/preview/actions.d.ts +11 -0
- package/dist/lib/commands/preview/actions.js +43 -0
- package/dist/lib/commands/preview/render.d.ts +3 -0
- package/dist/lib/commands/preview/render.js +30 -0
- package/dist/lib/commands/preview/server.d.ts +8 -0
- package/dist/lib/commands/preview/server.js +63 -0
- package/dist/lib/commands/preview/types.d.ts +22 -0
- package/dist/lib/commands/preview/types.js +1 -0
- package/dist/lib/commands/preview/wrapper-html.d.ts +2 -0
- package/dist/lib/commands/preview/wrapper-html.js +35 -0
- package/dist/lib/commands/sdk/output.d.ts +2 -0
- package/dist/lib/commands/sdk/output.js +42 -0
- package/dist/lib/commands/sdk/types.d.ts +21 -0
- package/dist/lib/commands/sdk/types.js +1 -0
- package/dist/lib/commands/settings/actions.d.ts +10 -0
- package/dist/lib/commands/settings/actions.js +56 -0
- package/dist/lib/commands/settings/display.d.ts +15 -0
- package/dist/lib/commands/settings/display.js +69 -0
- package/dist/lib/commands/settings/logo-domain.d.ts +3 -0
- package/dist/lib/commands/settings/logo-domain.js +47 -0
- package/dist/lib/commands/settings/prompt.d.ts +2 -0
- package/dist/lib/commands/settings/prompt.js +82 -0
- package/dist/lib/commands/settings/types.d.ts +65 -0
- package/dist/lib/commands/settings/types.js +1 -0
- package/dist/lib/commands/status/output.d.ts +2 -0
- package/dist/lib/commands/status/output.js +49 -0
- package/dist/lib/commands/status/types.d.ts +28 -0
- package/dist/lib/commands/status/types.js +1 -0
- package/dist/lib/constants.d.ts +3 -2
- package/dist/lib/constants.js +4 -5
- package/dist/lib/messages.d.ts +11 -0
- package/dist/lib/messages.js +31 -0
- package/dist/lib/templates/missing-templates.d.ts +16 -2
- package/dist/lib/templates/missing-templates.js +34 -22
- package/dist/lib/templates/regenerate.d.ts +10 -0
- package/dist/lib/templates/regenerate.js +29 -0
- package/dist/lib/templates/sync.d.ts +33 -0
- package/dist/lib/templates/sync.js +106 -0
- package/dist/lib/templates/types.d.ts +3 -0
- package/dist/lib/yaml-config.d.ts +1 -0
- package/dist/lib/yaml-config.js +8 -0
- package/oclif.manifest.json +100 -100
- package/package.json +1 -1
- /package/dist/lib/{deploy → commands/billing}/types.js +0 -0
- /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
|
|
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
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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
|
|
41
|
+
await startCheckout(ctx, flags.json);
|
|
47
42
|
return;
|
|
48
43
|
}
|
|
49
44
|
if (flags.purchase !== undefined) {
|
|
50
|
-
await
|
|
45
|
+
await purchaseBlocks(ctx, flags.purchase, flags.json);
|
|
51
46
|
return;
|
|
52
47
|
}
|
|
53
48
|
if (flags.cap !== undefined) {
|
|
54
|
-
await
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
this.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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 {
|
|
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
|
|
29
|
+
await searchContact(ctx, flags.search, flags.json);
|
|
32
30
|
return;
|
|
33
31
|
}
|
|
34
32
|
if (flags.export) {
|
|
35
|
-
await
|
|
33
|
+
await exportContacts(ctx, { json: flags.json });
|
|
36
34
|
return;
|
|
37
35
|
}
|
|
38
36
|
if (flags.delete) {
|
|
39
|
-
await
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
48
|
-
if (
|
|
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
|
}
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
25
|
+
renderDeploymentsTable(ctx, response.data);
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
}
|