@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.
@@ -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 this many 10,000-email blocks',
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 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) {
@@ -47,7 +47,7 @@
47
47
  "type": "boolean"
48
48
  },
49
49
  "purchase": {
50
- "description": "Manually purchase this many 10,000-email blocks",
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
- "deploy": {
79
+ "contacts": {
80
80
  "aliases": [],
81
81
  "args": {},
82
- "description": "Deploy email sequences and verify sending domain",
82
+ "description": "Manage contacts search, export, or delete",
83
83
  "examples": [
84
- "<%= config.bin %> deploy",
85
- "<%= config.bin %> deploy --yes"
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": "deploy",
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
- "deploy",
136
+ "contacts",
115
137
  "index.js"
116
138
  ]
117
139
  },
118
- "contacts": {
140
+ "deploy": {
119
141
  "aliases": [],
120
142
  "args": {},
121
- "description": "Manage contacts search, export, or delete",
143
+ "description": "Deploy email sequences and verify sending domain",
122
144
  "examples": [
123
- "<%= config.bin %> contacts",
124
- "<%= config.bin %> contacts --search sarah@example.com",
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": "contacts",
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
- "contacts",
175
+ "deploy",
176
176
  "index.js"
177
177
  ]
178
178
  },
@@ -657,5 +657,5 @@
657
657
  ]
658
658
  }
659
659
  },
660
- "version": "0.0.45"
660
+ "version": "0.0.46-beta.pr48.74"
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.45",
4
+ "version": "0.0.46-beta.pr48.74",
5
5
  "author": "provishalk",
6
6
  "bin": {
7
7
  "mailmodo": "bin/run.js"