@mailmodo/cli 0.0.46 → 0.0.47
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 +7 -1
- 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 +1 -1
- 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';
|
|
@@ -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