@mailmodo/cli 0.0.25-beta.pr27.42 → 0.0.26-beta.pr28.43
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 -12
- package/dist/commands/billing/index.js +151 -40
- package/dist/commands/contacts/index.js +6 -5
- package/dist/commands/domain/index.d.ts +1 -0
- package/dist/commands/domain/index.js +32 -29
- package/dist/lib/api-client.d.ts +4 -0
- package/dist/lib/api-client.js +7 -1
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/fetch-file.d.ts +4 -0
- package/dist/lib/fetch-file.js +46 -25
- package/oclif.manifest.json +79 -56
- package/package.json +1 -1
|
@@ -3,24 +3,23 @@ export default class Billing extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
'auto-charge-block-count': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
7
|
cap: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
checkout: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
purchase: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
10
|
status: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
11
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
12
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
13
|
};
|
|
11
14
|
run(): Promise<void>;
|
|
12
|
-
|
|
13
|
-
* Retrieves and displays the current billing status including card info,
|
|
14
|
-
* block usage, spending total, AI generation limits, and optionally
|
|
15
|
-
* opens Stripe Checkout if no card is on file.
|
|
16
|
-
*/
|
|
15
|
+
private startCheckout;
|
|
17
16
|
private showStatus;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
private formatAutoCharge;
|
|
18
|
+
private formatCap;
|
|
19
|
+
private formatCurrency;
|
|
20
|
+
private formatPaymentMethod;
|
|
21
|
+
private formatUsageBlock;
|
|
22
|
+
private pluralize;
|
|
23
|
+
private purchaseBlocks;
|
|
25
24
|
private setCap;
|
|
26
25
|
}
|
|
@@ -4,16 +4,29 @@ import open from 'open';
|
|
|
4
4
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
5
|
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
6
6
|
export default class Billing extends BaseCommand {
|
|
7
|
-
static description = 'View billing status,
|
|
7
|
+
static description = 'View billing status, purchase blocks, set cap, or add a payment method';
|
|
8
8
|
static examples = [
|
|
9
9
|
'<%= config.bin %> billing',
|
|
10
10
|
'<%= config.bin %> billing --status',
|
|
11
11
|
'<%= config.bin %> billing --cap 5',
|
|
12
|
+
'<%= config.bin %> billing --cap 5 --auto-charge-block-count 2',
|
|
13
|
+
'<%= config.bin %> billing --purchase 3',
|
|
14
|
+
'<%= config.bin %> billing --checkout',
|
|
12
15
|
];
|
|
13
16
|
static flags = {
|
|
14
17
|
...BaseCommand.baseFlags,
|
|
18
|
+
'auto-charge-block-count': Flags.integer({
|
|
19
|
+
description: 'Blocks to auto-purchase when the quota runs low (use with --cap)',
|
|
20
|
+
}),
|
|
15
21
|
cap: Flags.integer({
|
|
16
|
-
description: 'Set monthly
|
|
22
|
+
description: 'Set monthly sending cap in blocks',
|
|
23
|
+
}),
|
|
24
|
+
checkout: Flags.boolean({
|
|
25
|
+
default: false,
|
|
26
|
+
description: 'Open Stripe checkout to add or update a payment method',
|
|
27
|
+
}),
|
|
28
|
+
purchase: Flags.integer({
|
|
29
|
+
description: 'Manually purchase this many 10,000-email blocks',
|
|
17
30
|
}),
|
|
18
31
|
status: Flags.boolean({
|
|
19
32
|
default: false,
|
|
@@ -23,17 +36,44 @@ export default class Billing extends BaseCommand {
|
|
|
23
36
|
async run() {
|
|
24
37
|
const { flags } = await this.parse(Billing);
|
|
25
38
|
await this.ensureAuth();
|
|
39
|
+
const autoChargeBlockCount = flags['auto-charge-block-count'];
|
|
40
|
+
if (autoChargeBlockCount !== undefined && flags.cap === undefined) {
|
|
41
|
+
this.error(`Use ${chalk.cyan('--auto-charge-block-count')} together with ${chalk.cyan('--cap')}.`);
|
|
42
|
+
}
|
|
43
|
+
if (flags.checkout) {
|
|
44
|
+
await this.startCheckout(flags.json);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (flags.purchase !== undefined) {
|
|
48
|
+
await this.purchaseBlocks(flags.purchase, flags.json);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
26
51
|
if (flags.cap !== undefined) {
|
|
27
|
-
await this.setCap(flags.cap, flags.json);
|
|
52
|
+
await this.setCap(flags.cap, autoChargeBlockCount, flags.json);
|
|
28
53
|
return;
|
|
29
54
|
}
|
|
30
55
|
await this.showStatus(flags.json);
|
|
31
56
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
57
|
+
async startCheckout(jsonOutput) {
|
|
58
|
+
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Creating checkout session...' }, () => this.apiClient.post(API_ENDPOINTS.BILLING_CHECKOUT));
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
this.handleApiError(response);
|
|
61
|
+
}
|
|
62
|
+
const { checkoutUrl } = response.data;
|
|
63
|
+
if (jsonOutput) {
|
|
64
|
+
this.log(JSON.stringify({ checkoutUrl }, null, 2));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.log(`\n ${chalk.bold('Stripe Checkout')} — add or update your payment method.`);
|
|
68
|
+
this.log(` ${chalk.dim(checkoutUrl)}\n`);
|
|
69
|
+
try {
|
|
70
|
+
await open(checkoutUrl);
|
|
71
|
+
this.log(` Opening in browser...\n`);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
this.log(` ${chalk.dim('Could not open browser. Visit the URL above manually.')}\n`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
37
77
|
async showStatus(jsonOutput) {
|
|
38
78
|
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Loading billing status...' }, () => this.apiClient.get(API_ENDPOINTS.BILLING_STATUS));
|
|
39
79
|
if (!response.ok) {
|
|
@@ -45,55 +85,126 @@ export default class Billing extends BaseCommand {
|
|
|
45
85
|
return;
|
|
46
86
|
}
|
|
47
87
|
this.log('');
|
|
48
|
-
|
|
49
|
-
|
|
88
|
+
this.log(` Tier: ${data.tier}`);
|
|
89
|
+
this.log(` Payment: ${this.formatPaymentMethod(data)}`);
|
|
90
|
+
this.log(` Auto-charge: ${this.formatAutoCharge(data)}`);
|
|
91
|
+
this.log(` Monthly cap: ${this.formatCap(data.cap)}`);
|
|
92
|
+
this.log(` Total spent: ${this.formatCurrency(data.totalSpent, data.spentCurrency)}`);
|
|
93
|
+
if (data.activeBlocks.length === 0) {
|
|
94
|
+
this.log(' Active blocks: none');
|
|
95
|
+
this.log('');
|
|
96
|
+
return;
|
|
50
97
|
}
|
|
51
|
-
|
|
52
|
-
|
|
98
|
+
this.log(' Active blocks:');
|
|
99
|
+
for (const block of data.activeBlocks) {
|
|
100
|
+
this.log(` - ${this.formatUsageBlock(block)}`);
|
|
53
101
|
}
|
|
54
|
-
|
|
55
|
-
|
|
102
|
+
this.log('');
|
|
103
|
+
}
|
|
104
|
+
formatAutoCharge(data) {
|
|
105
|
+
if (!data.autoChargeEnabled) {
|
|
106
|
+
return 'disabled';
|
|
56
107
|
}
|
|
57
|
-
|
|
58
|
-
|
|
108
|
+
if (typeof data.autoChargeBlockCount !== 'number') {
|
|
109
|
+
return 'enabled';
|
|
59
110
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
this.log(` Edit uses remaining: ${data.aiRemaining.edits}/50 this month`);
|
|
111
|
+
return `enabled (${data.autoChargeBlockCount} ${this.pluralize('block', data.autoChargeBlockCount)})`;
|
|
112
|
+
}
|
|
113
|
+
formatCap(cap) {
|
|
114
|
+
const parts = [];
|
|
115
|
+
if (typeof cap.inBlocks === 'number') {
|
|
116
|
+
parts.push(`${cap.inBlocks} ${this.pluralize('block', cap.inBlocks)}`);
|
|
67
117
|
}
|
|
68
|
-
if (
|
|
69
|
-
|
|
118
|
+
if (typeof cap.inEmails === 'number') {
|
|
119
|
+
parts.push(`${cap.inEmails.toLocaleString()} emails`);
|
|
120
|
+
}
|
|
121
|
+
return parts.length > 0 ? parts.join(' / ') : 'not set';
|
|
122
|
+
}
|
|
123
|
+
formatCurrency(amount, currency) {
|
|
124
|
+
const numericAmount = typeof amount === 'number' ? amount : Number.parseFloat(amount);
|
|
125
|
+
const normalizedCurrency = currency.toUpperCase();
|
|
126
|
+
if (Number.isFinite(numericAmount)) {
|
|
70
127
|
try {
|
|
71
|
-
|
|
128
|
+
return new Intl.NumberFormat('en-US', {
|
|
129
|
+
currency: normalizedCurrency,
|
|
130
|
+
style: 'currency',
|
|
131
|
+
}).format(numericAmount);
|
|
72
132
|
}
|
|
73
133
|
catch {
|
|
74
|
-
|
|
134
|
+
return `${numericAmount.toFixed(2)} ${normalizedCurrency}`;
|
|
75
135
|
}
|
|
76
136
|
}
|
|
77
|
-
|
|
137
|
+
return `${String(amount)} ${normalizedCurrency}`;
|
|
138
|
+
}
|
|
139
|
+
formatPaymentMethod(data) {
|
|
140
|
+
if (!data.hasPaymentMethod) {
|
|
141
|
+
return 'No payment method on file';
|
|
142
|
+
}
|
|
143
|
+
const primaryMethod = data.paymentMethod[0];
|
|
144
|
+
if (!primaryMethod) {
|
|
145
|
+
return 'Payment method on file';
|
|
146
|
+
}
|
|
147
|
+
const brand = primaryMethod.brand || 'Card';
|
|
148
|
+
return primaryMethod.last4
|
|
149
|
+
? `${brand} ending ${primaryMethod.last4}`
|
|
150
|
+
: `${brand} on file`;
|
|
78
151
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
152
|
+
formatUsageBlock(block) {
|
|
153
|
+
const allowance = block.blockAllowance ?? Math.max(block.blocksCount * block.blockSize, 0);
|
|
154
|
+
const used = Math.max(block.emailsSent ?? 0, 0);
|
|
155
|
+
const activatedAt = block.activatedAt || block.acivatedAt;
|
|
156
|
+
const activationSuffix = activatedAt
|
|
157
|
+
? `, activated ${new Date(activatedAt).toLocaleDateString('en-US')}`
|
|
158
|
+
: '';
|
|
159
|
+
return `${block.type} block: ${used.toLocaleString()} / ${allowance.toLocaleString()} used (${block.status}${activationSuffix})`;
|
|
160
|
+
}
|
|
161
|
+
pluralize(word, count) {
|
|
162
|
+
return count === 1 ? word : `${word}s`;
|
|
163
|
+
}
|
|
164
|
+
async purchaseBlocks(blocksCount, jsonOutput) {
|
|
165
|
+
if (blocksCount < 1) {
|
|
166
|
+
this.error('Purchase block count must be at least 1 block.');
|
|
167
|
+
}
|
|
168
|
+
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Initiating block purchase...' }, () => this.apiClient.post(API_ENDPOINTS.BILLING_PURCHASE, {
|
|
169
|
+
blocksCount,
|
|
89
170
|
}));
|
|
90
171
|
if (!response.ok) {
|
|
91
172
|
this.handleApiError(response);
|
|
92
173
|
}
|
|
93
174
|
if (jsonOutput) {
|
|
94
|
-
this.log(JSON.stringify(
|
|
175
|
+
this.log(JSON.stringify(response.data, null, 2));
|
|
95
176
|
return;
|
|
96
177
|
}
|
|
97
|
-
this.log(`\n ${chalk.green('✓')}
|
|
178
|
+
this.log(`\n ${chalk.green('✓')} ${response.data.message}`);
|
|
179
|
+
this.log(` Blocks: ${blocksCount} ${this.pluralize('block', blocksCount)}`);
|
|
180
|
+
this.log(` Payment intent:${` ${chalk.dim(response.data.paymentIntentId)}`}`);
|
|
181
|
+
this.log('');
|
|
182
|
+
}
|
|
183
|
+
async setCap(cap, autoChargeBlockCount, jsonOutput) {
|
|
184
|
+
if (cap < 1) {
|
|
185
|
+
this.error('Cap must be at least 1 block.');
|
|
186
|
+
}
|
|
187
|
+
if (autoChargeBlockCount !== undefined && autoChargeBlockCount < 1) {
|
|
188
|
+
this.error('Auto-charge block count must be at least 1 block.');
|
|
189
|
+
}
|
|
190
|
+
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Updating spending cap...' }, () => {
|
|
191
|
+
const payload = autoChargeBlockCount === undefined
|
|
192
|
+
? { cap }
|
|
193
|
+
: { autoChargeBlockCount, cap };
|
|
194
|
+
return this.apiClient.post(API_ENDPOINTS.BILLING_CAP, payload);
|
|
195
|
+
});
|
|
196
|
+
if (!response.ok) {
|
|
197
|
+
this.handleApiError(response);
|
|
198
|
+
}
|
|
199
|
+
if (jsonOutput) {
|
|
200
|
+
this.log(JSON.stringify(response.data, null, 2));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
this.log(`\n ${chalk.green('✓')} ${response.data.message}`);
|
|
204
|
+
this.log(` Monthly cap: ${chalk.bold(String(response.data.capBlocks))} ${this.pluralize('block', response.data.capBlocks)} (${response.data.capEmails.toLocaleString()} emails)`);
|
|
205
|
+
if (response.data.autoChargeBlockCount !== undefined) {
|
|
206
|
+
this.log(` Auto-charge: ${response.data.autoChargeBlockCount} ${this.pluralize('block', response.data.autoChargeBlockCount)}`);
|
|
207
|
+
}
|
|
208
|
+
this.log('');
|
|
98
209
|
}
|
|
99
210
|
}
|
|
@@ -2,6 +2,7 @@ import { Flags } from '@oclif/core';
|
|
|
2
2
|
import { confirm } from '@inquirer/prompts';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { writeFile } from 'node:fs/promises';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
5
6
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
6
7
|
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
7
8
|
export default class Contacts extends BaseCommand {
|
|
@@ -9,7 +10,7 @@ export default class Contacts extends BaseCommand {
|
|
|
9
10
|
static examples = [
|
|
10
11
|
'<%= config.bin %> contacts',
|
|
11
12
|
'<%= config.bin %> contacts --search sarah@example.com',
|
|
12
|
-
|
|
13
|
+
'<%= config.bin %> contacts --export # GDPR CSV → contacts.csv',
|
|
13
14
|
'<%= config.bin %> contacts --delete sarah@example.com',
|
|
14
15
|
];
|
|
15
16
|
static flags = {
|
|
@@ -112,14 +113,14 @@ export default class Contacts extends BaseCommand {
|
|
|
112
113
|
this.log(`\n Export status: ${status ?? 'unknown'}. No download URL yet.\n`);
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
115
|
-
const
|
|
116
|
-
const fileResult = await this.apiClient.getFile(url);
|
|
116
|
+
const fileResult = await this.apiClient.getPublicFile(downloadUrl.trim());
|
|
117
117
|
if (!fileResult.ok) {
|
|
118
118
|
this.error(`Download failed: ${fileResult.status} ${fileResult.error ?? ''}\n` +
|
|
119
119
|
` URL: ${fileResult.debug.fullUrl}`);
|
|
120
120
|
}
|
|
121
|
-
|
|
122
|
-
|
|
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`);
|
|
123
124
|
}
|
|
124
125
|
/**
|
|
125
126
|
* Performs a GDPR-compliant hard delete of a contact and all their
|
|
@@ -45,34 +45,7 @@ export default class Domain extends BaseCommand {
|
|
|
45
45
|
this.log(`\n ${'─'.repeat(53)}`);
|
|
46
46
|
this.log(` ${chalk.bold('DOMAIN SETUP')}`);
|
|
47
47
|
this.log(` ${'─'.repeat(53)}\n`);
|
|
48
|
-
|
|
49
|
-
let senderEmail;
|
|
50
|
-
let address;
|
|
51
|
-
if (flags.yes) {
|
|
52
|
-
domain = yamlConfig.project?.domain || '';
|
|
53
|
-
senderEmail = yamlConfig.project?.fromEmail || '';
|
|
54
|
-
address = yamlConfig.project?.address || '';
|
|
55
|
-
if (!domain) {
|
|
56
|
-
this.error('Domain is required. Set it in mailmodo.yaml or use interactive mode.');
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
domain = await input({
|
|
61
|
-
default: yamlConfig.project?.domain,
|
|
62
|
-
message: 'What domain will you send from?',
|
|
63
|
-
validate: (v) => (v?.trim() ? true : 'Domain is required'),
|
|
64
|
-
});
|
|
65
|
-
senderEmail = await input({
|
|
66
|
-
default: yamlConfig.project?.fromEmail,
|
|
67
|
-
message: 'Sender email address:',
|
|
68
|
-
validate: (v) => v?.includes('@') ? true : 'Please enter a valid email',
|
|
69
|
-
});
|
|
70
|
-
address = await input({
|
|
71
|
-
default: yamlConfig.project?.address,
|
|
72
|
-
message: 'Business address (required by law):',
|
|
73
|
-
validate: (v) => (v?.trim() ? true : 'Address is required'),
|
|
74
|
-
});
|
|
75
|
-
}
|
|
48
|
+
const { domain, senderEmail, address } = await this.collectDomainInputs(flags.yes, yamlConfig);
|
|
76
49
|
const response = await this.withApiSpinner({ json: flags.json, text: ' Configuring domain...' }, () => this.apiClient.post(API_ENDPOINTS.DOMAIN, {
|
|
77
50
|
address,
|
|
78
51
|
domain,
|
|
@@ -87,6 +60,7 @@ export default class Domain extends BaseCommand {
|
|
|
87
60
|
await saveYaml(yamlConfig);
|
|
88
61
|
await saveConfig({ ...config, domain });
|
|
89
62
|
const records = response.data?.dnsRecords || [];
|
|
63
|
+
const guideUrl = response.data?.dnsGuideUrl ?? DNS_GUIDE_URL;
|
|
90
64
|
if (flags.json) {
|
|
91
65
|
this.log(JSON.stringify({ dnsRecords: records, domain }, null, 2));
|
|
92
66
|
return;
|
|
@@ -99,7 +73,7 @@ export default class Domain extends BaseCommand {
|
|
|
99
73
|
this.log(` Value: ${record.value}\n`);
|
|
100
74
|
}
|
|
101
75
|
this.log(` DNS changes take 5–30 minutes to propagate.`);
|
|
102
|
-
this.log(` Full guide: ${chalk.cyan(
|
|
76
|
+
this.log(` Full guide: ${chalk.cyan(guideUrl)}\n`);
|
|
103
77
|
if (!flags.yes) {
|
|
104
78
|
const action = await input({
|
|
105
79
|
default: '',
|
|
@@ -178,6 +152,35 @@ export default class Domain extends BaseCommand {
|
|
|
178
152
|
this.log(` Bounce rate: ${data.bounceRate ?? 'N/A'}%`);
|
|
179
153
|
this.log(` Spam rate: ${data.spamRate ?? 'N/A'}%\n`);
|
|
180
154
|
}
|
|
155
|
+
async collectDomainInputs(skipPrompts, yamlConfig) {
|
|
156
|
+
if (skipPrompts) {
|
|
157
|
+
const domain = yamlConfig.project?.domain || '';
|
|
158
|
+
if (!domain) {
|
|
159
|
+
this.error('Domain is required. Set it in mailmodo.yaml or use interactive mode.');
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
address: yamlConfig.project?.address || '',
|
|
163
|
+
domain,
|
|
164
|
+
senderEmail: yamlConfig.project?.fromEmail || '',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const domain = await input({
|
|
168
|
+
default: yamlConfig.project?.domain,
|
|
169
|
+
message: 'What domain will you send from?',
|
|
170
|
+
validate: (v) => (v?.trim() ? true : 'Domain is required'),
|
|
171
|
+
});
|
|
172
|
+
const senderEmail = await input({
|
|
173
|
+
default: yamlConfig.project?.fromEmail,
|
|
174
|
+
message: 'Sender email address:',
|
|
175
|
+
validate: (v) => (v?.includes('@') ? true : 'Please enter a valid email'),
|
|
176
|
+
});
|
|
177
|
+
const address = await input({
|
|
178
|
+
default: yamlConfig.project?.address,
|
|
179
|
+
message: 'Business address (required by law):',
|
|
180
|
+
validate: (v) => (v?.trim() ? true : 'Address is required'),
|
|
181
|
+
});
|
|
182
|
+
return { address, domain, senderEmail };
|
|
183
|
+
}
|
|
181
184
|
recordLabel(index) {
|
|
182
185
|
const labels = ['DKIM', 'DMARC', 'Return Path'];
|
|
183
186
|
return labels[index] || `Record ${index + 1}`;
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -44,6 +44,10 @@ export declare class ApiClient {
|
|
|
44
44
|
* Bearer auth as other requests; does not parse JSON.
|
|
45
45
|
*/
|
|
46
46
|
getFile(url: string): Promise<FileFetchResult>;
|
|
47
|
+
/**
|
|
48
|
+
* GET an external URL (e.g. blob storage) without auth headers.
|
|
49
|
+
*/
|
|
50
|
+
getPublicFile(url: string): Promise<FileFetchResult>;
|
|
47
51
|
patch<T = Record<string, unknown>>(path: string, body?: Record<string, unknown>): Promise<ApiResponse<T>>;
|
|
48
52
|
post<T = Record<string, unknown>>(path: string, body?: Record<string, unknown> | unknown): Promise<ApiResponse<T>>;
|
|
49
53
|
postFormData<T = Record<string, unknown>>(path: string, formData: FormData): Promise<ApiResponse<T>>;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { API_BASE_URL } from './constants.js';
|
|
2
|
-
import { fetchFileWithBearerAuth, } from './fetch-file.js';
|
|
2
|
+
import { fetchFileNoAuth, fetchFileWithBearerAuth, } from './fetch-file.js';
|
|
3
3
|
/**
|
|
4
4
|
* HTTP client for the Mailmodo CLI API.
|
|
5
5
|
* Wraps the native fetch API with Bearer token authentication,
|
|
@@ -123,6 +123,12 @@ export class ApiClient {
|
|
|
123
123
|
async getFile(url) {
|
|
124
124
|
return fetchFileWithBearerAuth(url, this.apiKey);
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* GET an external URL (e.g. blob storage) without auth headers.
|
|
128
|
+
*/
|
|
129
|
+
async getPublicFile(url) {
|
|
130
|
+
return fetchFileNoAuth(url);
|
|
131
|
+
}
|
|
126
132
|
async patch(path, body) {
|
|
127
133
|
return this.request('PATCH', path, body);
|
|
128
134
|
}
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export declare const API_ENDPOINTS: Readonly<{
|
|
|
5
5
|
ASSETS_LOGO: "/assets/logo";
|
|
6
6
|
AUTH_VALIDATE: "/auth/validate";
|
|
7
7
|
BILLING_CAP: "/billing/cap";
|
|
8
|
+
BILLING_CHECKOUT: "/billing/checkout";
|
|
9
|
+
BILLING_PURCHASE: "/billing/purchase";
|
|
8
10
|
BILLING_STATUS: "/billing/status";
|
|
9
11
|
CONTACTS: "/contacts";
|
|
10
12
|
CONTACTS_EXPORT: "/contacts/export";
|
package/dist/lib/constants.js
CHANGED
|
@@ -11,6 +11,8 @@ export const API_ENDPOINTS = Object.freeze({
|
|
|
11
11
|
ASSETS_LOGO: '/assets/logo',
|
|
12
12
|
AUTH_VALIDATE: '/auth/validate',
|
|
13
13
|
BILLING_CAP: '/billing/cap',
|
|
14
|
+
BILLING_CHECKOUT: '/billing/checkout',
|
|
15
|
+
BILLING_PURCHASE: '/billing/purchase',
|
|
14
16
|
BILLING_STATUS: '/billing/status',
|
|
15
17
|
CONTACTS: '/contacts',
|
|
16
18
|
CONTACTS_EXPORT: '/contacts/export',
|
package/dist/lib/fetch-file.d.ts
CHANGED
|
@@ -12,3 +12,7 @@ export interface FileFetchResult {
|
|
|
12
12
|
* does not parse JSON.
|
|
13
13
|
*/
|
|
14
14
|
export declare function fetchFileWithBearerAuth(url: string, apiKey: string): Promise<FileFetchResult>;
|
|
15
|
+
/**
|
|
16
|
+
* GET an absolute external URL (e.g. blob storage) without auth headers.
|
|
17
|
+
*/
|
|
18
|
+
export declare function fetchFileNoAuth(url: string): Promise<FileFetchResult>;
|
package/dist/lib/fetch-file.js
CHANGED
|
@@ -1,31 +1,8 @@
|
|
|
1
1
|
const USER_AGENT = '@mailmodo/cli';
|
|
2
|
-
|
|
3
|
-
* GET an absolute URL and return the raw body (e.g. CSV). Uses Bearer auth;
|
|
4
|
-
* does not parse JSON.
|
|
5
|
-
*/
|
|
6
|
-
export async function fetchFileWithBearerAuth(url, apiKey) {
|
|
7
|
-
let href;
|
|
8
|
-
try {
|
|
9
|
-
href = new URL(url.trim()).toString();
|
|
10
|
-
}
|
|
11
|
-
catch {
|
|
12
|
-
return {
|
|
13
|
-
body: new ArrayBuffer(0),
|
|
14
|
-
debug: { fullUrl: url.trim() },
|
|
15
|
-
error: 'Invalid URL',
|
|
16
|
-
ok: false,
|
|
17
|
-
status: 0,
|
|
18
|
-
};
|
|
19
|
-
}
|
|
2
|
+
async function doFetch(href, headers) {
|
|
20
3
|
const debug = { fullUrl: href };
|
|
21
4
|
try {
|
|
22
|
-
const response = await fetch(href, {
|
|
23
|
-
headers: {
|
|
24
|
-
Authorization: `Bearer ${apiKey}`,
|
|
25
|
-
'User-Agent': USER_AGENT,
|
|
26
|
-
},
|
|
27
|
-
method: 'GET',
|
|
28
|
-
});
|
|
5
|
+
const response = await fetch(href, { headers, method: 'GET' });
|
|
29
6
|
const body = await response.arrayBuffer();
|
|
30
7
|
if (!response.ok) {
|
|
31
8
|
return {
|
|
@@ -49,3 +26,47 @@ export async function fetchFileWithBearerAuth(url, apiKey) {
|
|
|
49
26
|
};
|
|
50
27
|
}
|
|
51
28
|
}
|
|
29
|
+
function parseUrl(url) {
|
|
30
|
+
try {
|
|
31
|
+
return new URL(url.trim()).toString();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* GET an absolute URL and return the raw body (e.g. CSV). Uses Bearer auth;
|
|
39
|
+
* does not parse JSON.
|
|
40
|
+
*/
|
|
41
|
+
export async function fetchFileWithBearerAuth(url, apiKey) {
|
|
42
|
+
const href = parseUrl(url);
|
|
43
|
+
if (!href) {
|
|
44
|
+
return {
|
|
45
|
+
body: new ArrayBuffer(0),
|
|
46
|
+
debug: { fullUrl: url.trim() },
|
|
47
|
+
error: 'Invalid URL',
|
|
48
|
+
ok: false,
|
|
49
|
+
status: 0,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return doFetch(href, {
|
|
53
|
+
Authorization: `Bearer ${apiKey}`,
|
|
54
|
+
'User-Agent': USER_AGENT,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* GET an absolute external URL (e.g. blob storage) without auth headers.
|
|
59
|
+
*/
|
|
60
|
+
export async function fetchFileNoAuth(url) {
|
|
61
|
+
const href = parseUrl(url);
|
|
62
|
+
if (!href) {
|
|
63
|
+
return {
|
|
64
|
+
body: new ArrayBuffer(0),
|
|
65
|
+
debug: { fullUrl: url.trim() },
|
|
66
|
+
error: 'Invalid URL',
|
|
67
|
+
ok: false,
|
|
68
|
+
status: 0,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return doFetch(href, { 'User-Agent': USER_AGENT });
|
|
72
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
"billing": {
|
|
4
4
|
"aliases": [],
|
|
5
5
|
"args": {},
|
|
6
|
-
"description": "View billing status,
|
|
6
|
+
"description": "View billing status, purchase blocks, set cap, or add a payment method",
|
|
7
7
|
"examples": [
|
|
8
8
|
"<%= config.bin %> billing",
|
|
9
9
|
"<%= config.bin %> billing --status",
|
|
10
|
-
"<%= config.bin %> billing --cap 5"
|
|
10
|
+
"<%= config.bin %> billing --cap 5",
|
|
11
|
+
"<%= config.bin %> billing --cap 5 --auto-charge-block-count 2",
|
|
12
|
+
"<%= config.bin %> billing --purchase 3",
|
|
13
|
+
"<%= config.bin %> billing --checkout"
|
|
11
14
|
],
|
|
12
15
|
"flags": {
|
|
13
16
|
"json": {
|
|
@@ -23,13 +26,33 @@
|
|
|
23
26
|
"allowNo": false,
|
|
24
27
|
"type": "boolean"
|
|
25
28
|
},
|
|
29
|
+
"auto-charge-block-count": {
|
|
30
|
+
"description": "Blocks to auto-purchase when the quota runs low (use with --cap)",
|
|
31
|
+
"name": "auto-charge-block-count",
|
|
32
|
+
"hasDynamicHelp": false,
|
|
33
|
+
"multiple": false,
|
|
34
|
+
"type": "option"
|
|
35
|
+
},
|
|
26
36
|
"cap": {
|
|
27
|
-
"description": "Set monthly
|
|
37
|
+
"description": "Set monthly sending cap in blocks",
|
|
28
38
|
"name": "cap",
|
|
29
39
|
"hasDynamicHelp": false,
|
|
30
40
|
"multiple": false,
|
|
31
41
|
"type": "option"
|
|
32
42
|
},
|
|
43
|
+
"checkout": {
|
|
44
|
+
"description": "Open Stripe checkout to add or update a payment method",
|
|
45
|
+
"name": "checkout",
|
|
46
|
+
"allowNo": false,
|
|
47
|
+
"type": "boolean"
|
|
48
|
+
},
|
|
49
|
+
"purchase": {
|
|
50
|
+
"description": "Manually purchase this many 10,000-email blocks",
|
|
51
|
+
"name": "purchase",
|
|
52
|
+
"hasDynamicHelp": false,
|
|
53
|
+
"multiple": false,
|
|
54
|
+
"type": "option"
|
|
55
|
+
},
|
|
33
56
|
"status": {
|
|
34
57
|
"description": "Show billing status only",
|
|
35
58
|
"name": "status",
|
|
@@ -205,6 +228,58 @@
|
|
|
205
228
|
"index.js"
|
|
206
229
|
]
|
|
207
230
|
},
|
|
231
|
+
"edit": {
|
|
232
|
+
"aliases": [],
|
|
233
|
+
"args": {
|
|
234
|
+
"id": {
|
|
235
|
+
"description": "Email template ID to edit",
|
|
236
|
+
"name": "id",
|
|
237
|
+
"required": true
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
"description": "Edit an email using AI-assisted natural language changes",
|
|
241
|
+
"examples": [
|
|
242
|
+
"<%= config.bin %> edit welcome",
|
|
243
|
+
"<%= config.bin %> edit welcome --change \"make subject more urgent\" --yes"
|
|
244
|
+
],
|
|
245
|
+
"flags": {
|
|
246
|
+
"json": {
|
|
247
|
+
"description": "Output as JSON",
|
|
248
|
+
"name": "json",
|
|
249
|
+
"allowNo": false,
|
|
250
|
+
"type": "boolean"
|
|
251
|
+
},
|
|
252
|
+
"yes": {
|
|
253
|
+
"char": "y",
|
|
254
|
+
"description": "Skip confirmation prompts",
|
|
255
|
+
"name": "yes",
|
|
256
|
+
"allowNo": false,
|
|
257
|
+
"type": "boolean"
|
|
258
|
+
},
|
|
259
|
+
"change": {
|
|
260
|
+
"description": "Natural language description of the change",
|
|
261
|
+
"name": "change",
|
|
262
|
+
"hasDynamicHelp": false,
|
|
263
|
+
"multiple": false,
|
|
264
|
+
"type": "option"
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
"hasDynamicHelp": false,
|
|
268
|
+
"hiddenAliases": [],
|
|
269
|
+
"id": "edit",
|
|
270
|
+
"pluginAlias": "@mailmodo/cli",
|
|
271
|
+
"pluginName": "@mailmodo/cli",
|
|
272
|
+
"pluginType": "core",
|
|
273
|
+
"strict": true,
|
|
274
|
+
"enableJsonFlag": false,
|
|
275
|
+
"isESM": true,
|
|
276
|
+
"relativePath": [
|
|
277
|
+
"dist",
|
|
278
|
+
"commands",
|
|
279
|
+
"edit",
|
|
280
|
+
"index.js"
|
|
281
|
+
]
|
|
282
|
+
},
|
|
208
283
|
"emails": {
|
|
209
284
|
"aliases": [],
|
|
210
285
|
"args": {},
|
|
@@ -437,58 +512,6 @@
|
|
|
437
512
|
"index.js"
|
|
438
513
|
]
|
|
439
514
|
},
|
|
440
|
-
"edit": {
|
|
441
|
-
"aliases": [],
|
|
442
|
-
"args": {
|
|
443
|
-
"id": {
|
|
444
|
-
"description": "Email template ID to edit",
|
|
445
|
-
"name": "id",
|
|
446
|
-
"required": true
|
|
447
|
-
}
|
|
448
|
-
},
|
|
449
|
-
"description": "Edit an email using AI-assisted natural language changes",
|
|
450
|
-
"examples": [
|
|
451
|
-
"<%= config.bin %> edit welcome",
|
|
452
|
-
"<%= config.bin %> edit welcome --change \"make subject more urgent\" --yes"
|
|
453
|
-
],
|
|
454
|
-
"flags": {
|
|
455
|
-
"json": {
|
|
456
|
-
"description": "Output as JSON",
|
|
457
|
-
"name": "json",
|
|
458
|
-
"allowNo": false,
|
|
459
|
-
"type": "boolean"
|
|
460
|
-
},
|
|
461
|
-
"yes": {
|
|
462
|
-
"char": "y",
|
|
463
|
-
"description": "Skip confirmation prompts",
|
|
464
|
-
"name": "yes",
|
|
465
|
-
"allowNo": false,
|
|
466
|
-
"type": "boolean"
|
|
467
|
-
},
|
|
468
|
-
"change": {
|
|
469
|
-
"description": "Natural language description of the change",
|
|
470
|
-
"name": "change",
|
|
471
|
-
"hasDynamicHelp": false,
|
|
472
|
-
"multiple": false,
|
|
473
|
-
"type": "option"
|
|
474
|
-
}
|
|
475
|
-
},
|
|
476
|
-
"hasDynamicHelp": false,
|
|
477
|
-
"hiddenAliases": [],
|
|
478
|
-
"id": "edit",
|
|
479
|
-
"pluginAlias": "@mailmodo/cli",
|
|
480
|
-
"pluginName": "@mailmodo/cli",
|
|
481
|
-
"pluginType": "core",
|
|
482
|
-
"strict": true,
|
|
483
|
-
"enableJsonFlag": false,
|
|
484
|
-
"isESM": true,
|
|
485
|
-
"relativePath": [
|
|
486
|
-
"dist",
|
|
487
|
-
"commands",
|
|
488
|
-
"edit",
|
|
489
|
-
"index.js"
|
|
490
|
-
]
|
|
491
|
-
},
|
|
492
515
|
"preview": {
|
|
493
516
|
"aliases": [],
|
|
494
517
|
"args": {
|
|
@@ -634,5 +657,5 @@
|
|
|
634
657
|
]
|
|
635
658
|
}
|
|
636
659
|
},
|
|
637
|
-
"version": "0.0.
|
|
660
|
+
"version": "0.0.26-beta.pr28.43"
|
|
638
661
|
}
|
package/package.json
CHANGED