@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.
@@ -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 this.fetchDomainVerified(yamlConfig.project.domain);
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;
@@ -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
- * Updates the account's monthly sending cap on the server.
150
- * Validates inputs, wraps the POST /billing/cap call in a spinner, and
151
- * surfaces errors via handleApiError. The caller is responsible for any
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
- async applyBillingCap(options) {
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,
@@ -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;
@@ -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) {
@@ -657,5 +657,5 @@
657
657
  ]
658
658
  }
659
659
  },
660
- "version": "0.0.46"
660
+ "version": "0.0.47"
661
661
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mailmodo/cli",
3
3
  "description": "Email lifecycle automation for the AI-native builder generation.",
4
- "version": "0.0.46",
4
+ "version": "0.0.47",
5
5
  "author": "provishalk",
6
6
  "bin": {
7
7
  "mailmodo": "bin/run.js"