@mailmodo/cli 0.0.45 → 0.0.46-beta.pr48.74
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.js +8 -2
- package/dist/commands/settings/index.js +21 -7
- package/dist/lib/base-command.d.ts +25 -0
- package/dist/lib/base-command.js +50 -5
- package/dist/lib/messages.d.ts +1 -0
- package/dist/lib/messages.js +1 -0
- package/oclif.manifest.json +36 -36
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import open from 'open';
|
|
4
|
-
import { BaseCommand } from '../../lib/base-command.js';
|
|
4
|
+
import { BaseCommand, FREE_TIER } from '../../lib/base-command.js';
|
|
5
5
|
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
6
6
|
import { INFO } from '../../lib/messages.js';
|
|
7
7
|
import { loadYaml, saveYaml } from '../../lib/yaml-config.js';
|
|
@@ -28,7 +28,7 @@ export default class Billing extends BaseCommand {
|
|
|
28
28
|
description: 'Open Stripe checkout to add or update a payment method',
|
|
29
29
|
}),
|
|
30
30
|
purchase: Flags.integer({
|
|
31
|
-
description: 'Manually purchase
|
|
31
|
+
description: 'Manually purchase email blocks',
|
|
32
32
|
}),
|
|
33
33
|
status: Flags.boolean({
|
|
34
34
|
default: false,
|
|
@@ -200,6 +200,12 @@ export default class Billing extends BaseCommand {
|
|
|
200
200
|
this.log('');
|
|
201
201
|
}
|
|
202
202
|
async setCap(cap, autoChargeBlockCount, jsonOutput) {
|
|
203
|
+
this.validateBillingCapInputs({ autoChargeBlockCount, cap });
|
|
204
|
+
const tier = await this.fetchBillingTier();
|
|
205
|
+
if (tier === FREE_TIER) {
|
|
206
|
+
this.warnFreeTierCapBlocked(jsonOutput);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
203
209
|
const data = await this.applyBillingCap({
|
|
204
210
|
autoChargeBlockCount,
|
|
205
211
|
cap,
|
|
@@ -4,7 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
5
|
import { readFile } from 'node:fs/promises';
|
|
6
6
|
import { resolve } from 'node:path';
|
|
7
|
-
import { BaseCommand } from '../../lib/base-command.js';
|
|
7
|
+
import { BaseCommand, FREE_TIER } from '../../lib/base-command.js';
|
|
8
8
|
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
9
9
|
import { INFO } from '../../lib/messages.js';
|
|
10
10
|
import { saveYaml } from '../../lib/yaml-config.js';
|
|
@@ -52,13 +52,18 @@ export default class Settings extends BaseCommand {
|
|
|
52
52
|
this.log(JSON.stringify({ settings: yamlConfig.project }, null, 2));
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
|
-
const domainVerified = await
|
|
55
|
+
const [domainVerified, tier] = await Promise.all([
|
|
56
|
+
this.fetchDomainVerified(yamlConfig.project.domain),
|
|
57
|
+
this.fetchBillingTier(),
|
|
58
|
+
]);
|
|
56
59
|
this.log(`\n Current settings for ${chalk.bold(yamlConfig.project.name || 'project')}:\n`);
|
|
57
60
|
for (const [group, keys] of Object.entries(SETTINGS_GROUPS)) {
|
|
61
|
+
if (group === 'billing' && tier === FREE_TIER)
|
|
62
|
+
continue;
|
|
58
63
|
this.displaySettingsGroup(group, keys, yamlConfig.project, domainVerified);
|
|
59
64
|
}
|
|
60
65
|
if (!flags.yes) {
|
|
61
|
-
await this.promptEditSetting(yamlConfig);
|
|
66
|
+
await this.promptEditSetting(yamlConfig, tier);
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
async applySetFlag(setFlag, yamlConfig, isJson) {
|
|
@@ -74,7 +79,7 @@ export default class Settings extends BaseCommand {
|
|
|
74
79
|
this.error(`Unknown setting: ${key}`);
|
|
75
80
|
}
|
|
76
81
|
if (propKey === 'monthlyCap') {
|
|
77
|
-
await this.applyMonthlyCapChange(yamlConfig, value, isJson);
|
|
82
|
+
await this.applyMonthlyCapChange(yamlConfig, value, isJson, null);
|
|
78
83
|
return;
|
|
79
84
|
}
|
|
80
85
|
project[propKey] = value;
|
|
@@ -86,12 +91,17 @@ export default class Settings extends BaseCommand {
|
|
|
86
91
|
this.log(`\n ${chalk.green('✓')} ${key} updated to ${chalk.cyan(value)}`);
|
|
87
92
|
this.log(` ${INFO.DEPLOY_TO_APPLY}\n`);
|
|
88
93
|
}
|
|
89
|
-
async applyMonthlyCapChange(yamlConfig, rawValue, isJson) {
|
|
94
|
+
async applyMonthlyCapChange(yamlConfig, rawValue, isJson, knownTier) {
|
|
90
95
|
const parsed = Number(rawValue);
|
|
91
96
|
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
92
97
|
this.error('monthly_cap must be a positive integer (blocks).');
|
|
93
98
|
}
|
|
94
99
|
await this.ensureAuth();
|
|
100
|
+
const tier = knownTier ?? (await this.fetchBillingTier());
|
|
101
|
+
if (tier === FREE_TIER) {
|
|
102
|
+
this.warnFreeTierCapBlocked(isJson);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
95
105
|
const data = await this.applyBillingCap({ cap: parsed, json: isJson });
|
|
96
106
|
yamlConfig.project.monthlyCap = data.capBlocks;
|
|
97
107
|
await saveYaml(yamlConfig);
|
|
@@ -146,7 +156,7 @@ export default class Settings extends BaseCommand {
|
|
|
146
156
|
* Prompts the user to pick a setting key to edit and dispatches
|
|
147
157
|
* to the appropriate handler for that key.
|
|
148
158
|
*/
|
|
149
|
-
async promptEditSetting(yamlConfig) {
|
|
159
|
+
async promptEditSetting(yamlConfig, tier) {
|
|
150
160
|
const { project } = yamlConfig;
|
|
151
161
|
const editKey = await input({
|
|
152
162
|
default: 'n',
|
|
@@ -154,6 +164,10 @@ export default class Settings extends BaseCommand {
|
|
|
154
164
|
});
|
|
155
165
|
if (editKey === 'n')
|
|
156
166
|
return;
|
|
167
|
+
if (editKey === 'monthly_cap' && tier === FREE_TIER) {
|
|
168
|
+
this.warnFreeTierCapBlocked(false);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
157
171
|
const editPropKey = settingKeyToProp(editKey);
|
|
158
172
|
if (!(editPropKey in project)) {
|
|
159
173
|
if (editKey === 'logo_file') {
|
|
@@ -181,7 +195,7 @@ export default class Settings extends BaseCommand {
|
|
|
181
195
|
const newValue = await input({
|
|
182
196
|
message: 'New monthly cap (blocks):',
|
|
183
197
|
});
|
|
184
|
-
await this.applyMonthlyCapChange(yamlConfig, newValue, false);
|
|
198
|
+
await this.applyMonthlyCapChange(yamlConfig, newValue, false, tier);
|
|
185
199
|
return;
|
|
186
200
|
}
|
|
187
201
|
if (editKey === 'email_style') {
|
|
@@ -8,6 +8,7 @@ export interface BillingCapUpdateResult {
|
|
|
8
8
|
capEmails: number;
|
|
9
9
|
message: string;
|
|
10
10
|
}
|
|
11
|
+
export declare const FREE_TIER = "free";
|
|
11
12
|
/**
|
|
12
13
|
* Abstract base command providing shared functionality for all Mailmodo CLI commands.
|
|
13
14
|
* Subclasses inherit --json and --yes base flags, authentication enforcement,
|
|
@@ -74,6 +75,15 @@ export declare abstract class BaseCommand extends Command {
|
|
|
74
75
|
fromName: string;
|
|
75
76
|
replyTo: string;
|
|
76
77
|
}>;
|
|
78
|
+
/**
|
|
79
|
+
* Validates the inputs that would be sent to `POST /billing/cap`. Extracted
|
|
80
|
+
* so callers can run cheap, no-API local checks (positive integers) before
|
|
81
|
+
* the tier lookup or any other network round trip.
|
|
82
|
+
*/
|
|
83
|
+
protected validateBillingCapInputs(options: {
|
|
84
|
+
autoChargeBlockCount?: number;
|
|
85
|
+
cap: number;
|
|
86
|
+
}): void;
|
|
77
87
|
/**
|
|
78
88
|
* Updates the account's monthly sending cap on the server.
|
|
79
89
|
* Validates inputs, wraps the POST /billing/cap call in a spinner, and
|
|
@@ -85,6 +95,21 @@ export declare abstract class BaseCommand extends Command {
|
|
|
85
95
|
cap: number;
|
|
86
96
|
json: boolean;
|
|
87
97
|
}): Promise<BillingCapUpdateResult>;
|
|
98
|
+
/**
|
|
99
|
+
* Shared "free-tier users can't set a monthly cap" notice for both the
|
|
100
|
+
* billing and settings commands. Prints a yellow warning in text mode and a
|
|
101
|
+
* `status: 'blocked'` payload in JSON mode so machine-readable callers can
|
|
102
|
+
* distinguish from a successful update.
|
|
103
|
+
*/
|
|
104
|
+
protected warnFreeTierCapBlocked(jsonOutput: boolean): void;
|
|
105
|
+
/**
|
|
106
|
+
* Fetches the account's billing tier from `/billing/status`. Returns `null`
|
|
107
|
+
* when the request cannot be completed (no credentials, network failure,
|
|
108
|
+
* non-OK response) so callers can degrade gracefully without halting.
|
|
109
|
+
* Callers that need a hard "free vs paid" decision should treat `null` as
|
|
110
|
+
* unknown and fall through to their default behavior.
|
|
111
|
+
*/
|
|
112
|
+
protected fetchBillingTier(): Promise<null | string>;
|
|
88
113
|
protected registerDomain(yamlConfig: MailmodoYaml, inputs: {
|
|
89
114
|
address: string;
|
|
90
115
|
domain: string;
|
package/dist/lib/base-command.js
CHANGED
|
@@ -7,6 +7,7 @@ import { loadConfig } from './config.js';
|
|
|
7
7
|
import { API_ENDPOINTS } from './constants.js';
|
|
8
8
|
import { ERRORS, INFO, PROMPTS, recordLabel, VALIDATION } from './messages.js';
|
|
9
9
|
import { loadYaml, saveYaml } from './yaml-config.js';
|
|
10
|
+
export const FREE_TIER = 'free';
|
|
10
11
|
/**
|
|
11
12
|
* Abstract base command providing shared functionality for all Mailmodo CLI commands.
|
|
12
13
|
* Subclasses inherit --json and --yes base flags, authentication enforcement,
|
|
@@ -146,12 +147,11 @@ export class BaseCommand extends Command {
|
|
|
146
147
|
return { address, domain, fromEmail, fromName, replyTo };
|
|
147
148
|
}
|
|
148
149
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* local persistence (e.g. writing `monthlyCap` to mailmodo.yaml).
|
|
150
|
+
* Validates the inputs that would be sent to `POST /billing/cap`. Extracted
|
|
151
|
+
* so callers can run cheap, no-API local checks (positive integers) before
|
|
152
|
+
* the tier lookup or any other network round trip.
|
|
153
153
|
*/
|
|
154
|
-
|
|
154
|
+
validateBillingCapInputs(options) {
|
|
155
155
|
if (options.cap < 1) {
|
|
156
156
|
this.error('Cap must be at least 1 block.');
|
|
157
157
|
}
|
|
@@ -159,6 +159,15 @@ export class BaseCommand extends Command {
|
|
|
159
159
|
options.autoChargeBlockCount < 1) {
|
|
160
160
|
this.error('Auto-charge block count must be at least 1 block.');
|
|
161
161
|
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Updates the account's monthly sending cap on the server.
|
|
165
|
+
* Validates inputs, wraps the POST /billing/cap call in a spinner, and
|
|
166
|
+
* surfaces errors via handleApiError. The caller is responsible for any
|
|
167
|
+
* local persistence (e.g. writing `monthlyCap` to mailmodo.yaml).
|
|
168
|
+
*/
|
|
169
|
+
async applyBillingCap(options) {
|
|
170
|
+
this.validateBillingCapInputs(options);
|
|
162
171
|
const payload = options.autoChargeBlockCount === undefined
|
|
163
172
|
? { cap: options.cap }
|
|
164
173
|
: {
|
|
@@ -171,6 +180,42 @@ export class BaseCommand extends Command {
|
|
|
171
180
|
}
|
|
172
181
|
return response.data;
|
|
173
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Shared "free-tier users can't set a monthly cap" notice for both the
|
|
185
|
+
* billing and settings commands. Prints a yellow warning in text mode and a
|
|
186
|
+
* `status: 'blocked'` payload in JSON mode so machine-readable callers can
|
|
187
|
+
* distinguish from a successful update.
|
|
188
|
+
*/
|
|
189
|
+
warnFreeTierCapBlocked(jsonOutput) {
|
|
190
|
+
if (jsonOutput) {
|
|
191
|
+
this.log(JSON.stringify({
|
|
192
|
+
message: INFO.FREE_TIER_CAP_BLOCKED,
|
|
193
|
+
status: 'blocked',
|
|
194
|
+
tier: FREE_TIER,
|
|
195
|
+
}, null, 2));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
this.warn(chalk.yellow(INFO.FREE_TIER_CAP_BLOCKED));
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Fetches the account's billing tier from `/billing/status`. Returns `null`
|
|
202
|
+
* when the request cannot be completed (no credentials, network failure,
|
|
203
|
+
* non-OK response) so callers can degrade gracefully without halting.
|
|
204
|
+
* Callers that need a hard "free vs paid" decision should treat `null` as
|
|
205
|
+
* unknown and fall through to their default behavior.
|
|
206
|
+
*/
|
|
207
|
+
async fetchBillingTier() {
|
|
208
|
+
try {
|
|
209
|
+
await this.ensureAuth();
|
|
210
|
+
const response = await this.apiClient.get(API_ENDPOINTS.BILLING_STATUS);
|
|
211
|
+
if (!response.ok)
|
|
212
|
+
return null;
|
|
213
|
+
return response.data?.tier ?? null;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
174
219
|
async registerDomain(yamlConfig, inputs, json) {
|
|
175
220
|
const apiPayload = {
|
|
176
221
|
address: inputs.address,
|
package/dist/lib/messages.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export declare const INFO: {
|
|
|
32
32
|
readonly DOMAIN_NOT_DEPLOYED_HINT: `When ready, run: ${string}
|
|
33
33
|
Then: ${string}`;
|
|
34
34
|
readonly DOMAIN_PENDING_VERIFICATION: `Your domain is not verified yet. Please verify it first. Run ${string} to check the status.`;
|
|
35
|
+
readonly FREE_TIER_CAP_BLOCKED: `Monthly cap is a paid-tier setting and is not available on the free tier. Run ${string} to add a payment method, then set a cap.`;
|
|
35
36
|
readonly SEQUENCES_NOT_DEPLOYED: `Sequences saved but ${string}.`;
|
|
36
37
|
};
|
|
37
38
|
export declare function yamlParseError(detail: string): string;
|
package/dist/lib/messages.js
CHANGED
|
@@ -32,6 +32,7 @@ export const INFO = {
|
|
|
32
32
|
DNS_RECORDS_FAILED: chalk.yellow('Some records failed.'),
|
|
33
33
|
DOMAIN_NOT_DEPLOYED_HINT: `When ready, run: ${chalk.cyan('mailmodo domain')}\n Then: ${chalk.cyan('mailmodo deploy')}`,
|
|
34
34
|
DOMAIN_PENDING_VERIFICATION: `Your domain is not verified yet. Please verify it first. Run ${chalk.cyan('mailmodo domain --verify')} to check the status.`,
|
|
35
|
+
FREE_TIER_CAP_BLOCKED: `Monthly cap is a paid-tier setting and is not available on the free tier. Run ${chalk.cyan("'mailmodo billing --checkout'")} to add a payment method, then set a cap.`,
|
|
35
36
|
SEQUENCES_NOT_DEPLOYED: `Sequences saved but ${chalk.yellow('NOT deployed')}.`,
|
|
36
37
|
};
|
|
37
38
|
export function yamlParseError(detail) {
|
package/oclif.manifest.json
CHANGED
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"type": "boolean"
|
|
48
48
|
},
|
|
49
49
|
"purchase": {
|
|
50
|
-
"description": "Manually purchase
|
|
50
|
+
"description": "Manually purchase email blocks",
|
|
51
51
|
"name": "purchase",
|
|
52
52
|
"hasDynamicHelp": false,
|
|
53
53
|
"multiple": false,
|
|
@@ -76,13 +76,15 @@
|
|
|
76
76
|
"index.js"
|
|
77
77
|
]
|
|
78
78
|
},
|
|
79
|
-
"
|
|
79
|
+
"contacts": {
|
|
80
80
|
"aliases": [],
|
|
81
81
|
"args": {},
|
|
82
|
-
"description": "
|
|
82
|
+
"description": "Manage contacts — search, export, or delete",
|
|
83
83
|
"examples": [
|
|
84
|
-
"<%= config.bin %>
|
|
85
|
-
"<%= config.bin %>
|
|
84
|
+
"<%= config.bin %> contacts",
|
|
85
|
+
"<%= config.bin %> contacts --search sarah@example.com",
|
|
86
|
+
"<%= config.bin %> contacts --export # GDPR CSV → contacts.csv",
|
|
87
|
+
"<%= config.bin %> contacts --delete sarah@example.com"
|
|
86
88
|
],
|
|
87
89
|
"flags": {
|
|
88
90
|
"json": {
|
|
@@ -97,11 +99,31 @@
|
|
|
97
99
|
"name": "yes",
|
|
98
100
|
"allowNo": false,
|
|
99
101
|
"type": "boolean"
|
|
102
|
+
},
|
|
103
|
+
"delete": {
|
|
104
|
+
"description": "GDPR hard delete a contact by email",
|
|
105
|
+
"name": "delete",
|
|
106
|
+
"hasDynamicHelp": false,
|
|
107
|
+
"multiple": false,
|
|
108
|
+
"type": "option"
|
|
109
|
+
},
|
|
110
|
+
"export": {
|
|
111
|
+
"description": "Export all contacts as GDPR-compliant CSV (writes contacts.csv in the current directory)",
|
|
112
|
+
"name": "export",
|
|
113
|
+
"allowNo": false,
|
|
114
|
+
"type": "boolean"
|
|
115
|
+
},
|
|
116
|
+
"search": {
|
|
117
|
+
"description": "Search for a contact by email",
|
|
118
|
+
"name": "search",
|
|
119
|
+
"hasDynamicHelp": false,
|
|
120
|
+
"multiple": false,
|
|
121
|
+
"type": "option"
|
|
100
122
|
}
|
|
101
123
|
},
|
|
102
124
|
"hasDynamicHelp": false,
|
|
103
125
|
"hiddenAliases": [],
|
|
104
|
-
"id": "
|
|
126
|
+
"id": "contacts",
|
|
105
127
|
"pluginAlias": "@mailmodo/cli",
|
|
106
128
|
"pluginName": "@mailmodo/cli",
|
|
107
129
|
"pluginType": "core",
|
|
@@ -111,19 +133,17 @@
|
|
|
111
133
|
"relativePath": [
|
|
112
134
|
"dist",
|
|
113
135
|
"commands",
|
|
114
|
-
"
|
|
136
|
+
"contacts",
|
|
115
137
|
"index.js"
|
|
116
138
|
]
|
|
117
139
|
},
|
|
118
|
-
"
|
|
140
|
+
"deploy": {
|
|
119
141
|
"aliases": [],
|
|
120
142
|
"args": {},
|
|
121
|
-
"description": "
|
|
143
|
+
"description": "Deploy email sequences and verify sending domain",
|
|
122
144
|
"examples": [
|
|
123
|
-
"<%= config.bin %>
|
|
124
|
-
"<%= config.bin %>
|
|
125
|
-
"<%= config.bin %> contacts --export # GDPR CSV → contacts.csv",
|
|
126
|
-
"<%= config.bin %> contacts --delete sarah@example.com"
|
|
145
|
+
"<%= config.bin %> deploy",
|
|
146
|
+
"<%= config.bin %> deploy --yes"
|
|
127
147
|
],
|
|
128
148
|
"flags": {
|
|
129
149
|
"json": {
|
|
@@ -138,31 +158,11 @@
|
|
|
138
158
|
"name": "yes",
|
|
139
159
|
"allowNo": false,
|
|
140
160
|
"type": "boolean"
|
|
141
|
-
},
|
|
142
|
-
"delete": {
|
|
143
|
-
"description": "GDPR hard delete a contact by email",
|
|
144
|
-
"name": "delete",
|
|
145
|
-
"hasDynamicHelp": false,
|
|
146
|
-
"multiple": false,
|
|
147
|
-
"type": "option"
|
|
148
|
-
},
|
|
149
|
-
"export": {
|
|
150
|
-
"description": "Export all contacts as GDPR-compliant CSV (writes contacts.csv in the current directory)",
|
|
151
|
-
"name": "export",
|
|
152
|
-
"allowNo": false,
|
|
153
|
-
"type": "boolean"
|
|
154
|
-
},
|
|
155
|
-
"search": {
|
|
156
|
-
"description": "Search for a contact by email",
|
|
157
|
-
"name": "search",
|
|
158
|
-
"hasDynamicHelp": false,
|
|
159
|
-
"multiple": false,
|
|
160
|
-
"type": "option"
|
|
161
161
|
}
|
|
162
162
|
},
|
|
163
163
|
"hasDynamicHelp": false,
|
|
164
164
|
"hiddenAliases": [],
|
|
165
|
-
"id": "
|
|
165
|
+
"id": "deploy",
|
|
166
166
|
"pluginAlias": "@mailmodo/cli",
|
|
167
167
|
"pluginName": "@mailmodo/cli",
|
|
168
168
|
"pluginType": "core",
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
"relativePath": [
|
|
173
173
|
"dist",
|
|
174
174
|
"commands",
|
|
175
|
-
"
|
|
175
|
+
"deploy",
|
|
176
176
|
"index.js"
|
|
177
177
|
]
|
|
178
178
|
},
|
|
@@ -657,5 +657,5 @@
|
|
|
657
657
|
]
|
|
658
658
|
}
|
|
659
659
|
},
|
|
660
|
-
"version": "0.0.
|
|
660
|
+
"version": "0.0.46-beta.pr48.74"
|
|
661
661
|
}
|