@mailmodo/cli 0.0.31 → 0.0.32-beta.pr34.57
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 +3 -2
- package/dist/commands/deploy/index.js +21 -21
- package/dist/commands/domain/index.d.ts +0 -2
- package/dist/commands/domain/index.js +28 -102
- package/dist/commands/login/index.js +10 -9
- package/dist/commands/preview/index.js +3 -2
- package/dist/commands/settings/index.d.ts +0 -7
- package/dist/commands/settings/index.js +8 -51
- package/dist/commands/status/index.js +7 -2
- package/dist/lib/base-command.d.ts +26 -0
- package/dist/lib/base-command.js +89 -6
- package/dist/lib/config.d.ts +3 -5
- package/dist/lib/config.js +2 -2
- package/dist/lib/messages.d.ts +36 -0
- package/dist/lib/messages.js +39 -0
- package/oclif.manifest.json +28 -28
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import open from 'open';
|
|
4
4
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
5
|
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
6
|
+
import { INFO } from '../../lib/messages.js';
|
|
6
7
|
export default class Billing extends BaseCommand {
|
|
7
8
|
static description = 'View billing status, purchase blocks, set cap, or add a payment method';
|
|
8
9
|
static examples = [
|
|
@@ -68,10 +69,10 @@ export default class Billing extends BaseCommand {
|
|
|
68
69
|
this.log(` ${chalk.dim(checkoutUrl)}\n`);
|
|
69
70
|
try {
|
|
70
71
|
await open(checkoutUrl);
|
|
71
|
-
this.log(`
|
|
72
|
+
this.log(` ${INFO.BROWSER_OPENING}\n`);
|
|
72
73
|
}
|
|
73
74
|
catch {
|
|
74
|
-
this.log(` ${
|
|
75
|
+
this.log(` ${INFO.BROWSER_OPEN_FAILED}\n`);
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
async showStatus(jsonOutput) {
|
|
@@ -2,6 +2,7 @@ import { confirm, input } from '@inquirer/prompts';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
4
4
|
import { API_ENDPOINTS, DEFAULT_BRAND_COLOR, DEFAULT_MONTHLY_CAP, } from '../../lib/constants.js';
|
|
5
|
+
import { ERRORS, INFO, PROMPTS, SEPARATOR, VALIDATION, } from '../../lib/messages.js';
|
|
5
6
|
import { loadTemplate, saveYaml, } from '../../lib/yaml-config.js';
|
|
6
7
|
export default class Deploy extends BaseCommand {
|
|
7
8
|
static description = 'Deploy email sequences and verify sending domain';
|
|
@@ -50,10 +51,10 @@ export default class Deploy extends BaseCommand {
|
|
|
50
51
|
const response = await this.withApiSpinner({ json: flags.json, text: ' Validating sequence...' }, () => this.apiClient.post(API_ENDPOINTS.SEQUENCES_VALIDATE, payload));
|
|
51
52
|
if (!response.ok) {
|
|
52
53
|
if (response.data.error === 'senderDomainNotFound') {
|
|
53
|
-
this.error(
|
|
54
|
+
this.error(ERRORS.DOMAIN_NOT_REGISTERED);
|
|
54
55
|
}
|
|
55
56
|
if (response.data.error === 'senderDomainNotVerified') {
|
|
56
|
-
this.error(
|
|
57
|
+
this.error(ERRORS.DOMAIN_NOT_VERIFIED);
|
|
57
58
|
}
|
|
58
59
|
this.handleApiError(response);
|
|
59
60
|
}
|
|
@@ -148,10 +149,9 @@ export default class Deploy extends BaseCommand {
|
|
|
148
149
|
message: 'Set up your sending domain now?',
|
|
149
150
|
});
|
|
150
151
|
if (!setupNow) {
|
|
151
|
-
this.log(`\n
|
|
152
|
+
this.log(`\n ${INFO.SEQUENCES_NOT_DEPLOYED}`);
|
|
152
153
|
this.log(` Emails will not send until your domain is verified.`);
|
|
153
|
-
this.log(`
|
|
154
|
-
this.log(` Then: ${chalk.cyan('mailmodo deploy')}\n`);
|
|
154
|
+
this.log(` ${INFO.DOMAIN_NOT_DEPLOYED_HINT}\n`);
|
|
155
155
|
return false;
|
|
156
156
|
}
|
|
157
157
|
}
|
|
@@ -194,9 +194,9 @@ export default class Deploy extends BaseCommand {
|
|
|
194
194
|
}
|
|
195
195
|
logDeploySuccessInstructions(sdkSnippet) {
|
|
196
196
|
this.log(` ${chalk.green('Deployed.')} Emails are live.\n`);
|
|
197
|
-
this.log(` ${
|
|
197
|
+
this.log(` ${SEPARATOR}`);
|
|
198
198
|
this.log(` ${chalk.bold('ADD THIS TO YOUR APP (one-time only):')}`);
|
|
199
|
-
this.log(` ${
|
|
199
|
+
this.log(` ${SEPARATOR}\n`);
|
|
200
200
|
this.log(` ${chalk.cyan(sdkSnippet.install ?? 'npm install @mailmodo/sdk')}\n`);
|
|
201
201
|
this.log(` ${chalk.dim("import { track, identify } from '@mailmodo/sdk'")}\n`);
|
|
202
202
|
if (sdkSnippet.examples) {
|
|
@@ -217,7 +217,7 @@ export default class Deploy extends BaseCommand {
|
|
|
217
217
|
if (identifyCalls.length > 0)
|
|
218
218
|
this.log('');
|
|
219
219
|
this.log(` Full SDK docs: ${chalk.cyan('mailmodo.com/docs/sdk')}\n`);
|
|
220
|
-
this.log(` ${
|
|
220
|
+
this.log(` ${SEPARATOR}\n`);
|
|
221
221
|
}
|
|
222
222
|
async runDomainSetup(yamlConfig, flags) {
|
|
223
223
|
const { address, domain, senderEmail } = await this.collectDomainInputs(yamlConfig, flags);
|
|
@@ -242,9 +242,8 @@ export default class Deploy extends BaseCommand {
|
|
|
242
242
|
message: "Press Enter once you've added the records, or 'skip' to do this later.",
|
|
243
243
|
});
|
|
244
244
|
if (action.toLowerCase() === 'skip') {
|
|
245
|
-
this.log(`\n
|
|
246
|
-
this.log(`
|
|
247
|
-
this.log(` Then: ${chalk.cyan('mailmodo deploy')}\n`);
|
|
245
|
+
this.log(`\n ${INFO.SEQUENCES_NOT_DEPLOYED}`);
|
|
246
|
+
this.log(` ${INFO.DOMAIN_NOT_DEPLOYED_HINT}\n`);
|
|
248
247
|
return false;
|
|
249
248
|
}
|
|
250
249
|
return this.verifyDomain(flags.json, domain);
|
|
@@ -257,20 +256,20 @@ export default class Deploy extends BaseCommand {
|
|
|
257
256
|
senderEmail: yamlConfig.project?.fromEmail || '',
|
|
258
257
|
};
|
|
259
258
|
}
|
|
260
|
-
this.log(`\n ${
|
|
259
|
+
this.log(`\n ${SEPARATOR}`);
|
|
261
260
|
this.log(` ${chalk.bold('DOMAIN SETUP')}`);
|
|
262
|
-
this.log(` ${
|
|
261
|
+
this.log(` ${SEPARATOR}\n`);
|
|
263
262
|
const domain = await input({
|
|
264
|
-
message:
|
|
265
|
-
validate: (v) => (v?.trim() ? true :
|
|
263
|
+
message: PROMPTS.DOMAIN,
|
|
264
|
+
validate: (v) => (v?.trim() ? true : VALIDATION.DOMAIN_REQUIRED),
|
|
266
265
|
});
|
|
267
266
|
const senderEmail = await input({
|
|
268
|
-
message:
|
|
269
|
-
validate: (v) => (v?.includes('@') ? true :
|
|
267
|
+
message: PROMPTS.SENDER_EMAIL,
|
|
268
|
+
validate: (v) => (v?.includes('@') ? true : VALIDATION.EMAIL_INVALID),
|
|
270
269
|
});
|
|
271
270
|
const address = await input({
|
|
272
|
-
message:
|
|
273
|
-
validate: (v) => (v?.trim() ? true :
|
|
271
|
+
message: PROMPTS.BUSINESS_ADDRESS,
|
|
272
|
+
validate: (v) => (v?.trim() ? true : VALIDATION.ADDRESS_REQUIRED),
|
|
274
273
|
});
|
|
275
274
|
return { address, domain, senderEmail };
|
|
276
275
|
}
|
|
@@ -284,7 +283,7 @@ export default class Deploy extends BaseCommand {
|
|
|
284
283
|
this.log(` Host: ${record.host}`);
|
|
285
284
|
this.log(` Value: ${record.value}\n`);
|
|
286
285
|
}
|
|
287
|
-
this.log(`
|
|
286
|
+
this.log(` ${INFO.DNS_PROPAGATION}`);
|
|
288
287
|
if (dnsGuideUrl)
|
|
289
288
|
this.log(` Full guide: ${chalk.cyan(dnsGuideUrl)}\n`);
|
|
290
289
|
}
|
|
@@ -305,7 +304,8 @@ export default class Deploy extends BaseCommand {
|
|
|
305
304
|
this.log(`\n ${chalk.green('Domain verified.')} Continuing deploy...\n`);
|
|
306
305
|
}
|
|
307
306
|
else {
|
|
308
|
-
this.log(`\n ${
|
|
307
|
+
this.log(`\n ${INFO.DNS_RECORDS_FAILED}`);
|
|
308
|
+
this.log(`\n ${INFO.DNS_FIX_AND_VERIFY}`);
|
|
309
309
|
if (dnsGuideUrl)
|
|
310
310
|
this.log(` Help: ${chalk.cyan(dnsGuideUrl)}\n`);
|
|
311
311
|
}
|
|
@@ -3,8 +3,7 @@ import { input } from '@inquirer/prompts';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
5
|
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
6
|
-
import {
|
|
7
|
-
import { saveYaml } from '../../lib/yaml-config.js';
|
|
6
|
+
import { ERRORS, INFO, PROMPTS, SEPARATOR } from '../../lib/messages.js';
|
|
8
7
|
export default class Domain extends BaseCommand {
|
|
9
8
|
static description = 'Set up and verify your sending domain';
|
|
10
9
|
static examples = [
|
|
@@ -25,83 +24,57 @@ export default class Domain extends BaseCommand {
|
|
|
25
24
|
};
|
|
26
25
|
async run() {
|
|
27
26
|
const { flags } = await this.parse(Domain);
|
|
28
|
-
|
|
27
|
+
await this.ensureAuth();
|
|
29
28
|
if (flags.verify) {
|
|
30
|
-
await this.
|
|
29
|
+
const yamlConfig = await this.ensureYaml();
|
|
30
|
+
const domain = yamlConfig.project?.domain;
|
|
31
|
+
if (!domain) {
|
|
32
|
+
this.error(ERRORS.DOMAIN_NOT_CONFIGURED);
|
|
33
|
+
}
|
|
34
|
+
await this.verifyDomain(flags.json, domain);
|
|
31
35
|
return;
|
|
32
36
|
}
|
|
33
37
|
if (flags.status) {
|
|
34
|
-
await this.
|
|
38
|
+
const yamlConfig = await this.ensureYaml();
|
|
39
|
+
const domain = yamlConfig.project?.domain;
|
|
40
|
+
if (!domain) {
|
|
41
|
+
this.error(ERRORS.DOMAIN_NOT_CONFIGURED);
|
|
42
|
+
}
|
|
43
|
+
await this.showDomainStatus(flags.json, domain);
|
|
35
44
|
return;
|
|
36
45
|
}
|
|
37
|
-
await this.setupDomain(flags
|
|
46
|
+
await this.setupDomain(flags);
|
|
38
47
|
}
|
|
39
48
|
/**
|
|
40
49
|
* Interactive domain setup: collects domain, sender email, and business address,
|
|
41
50
|
* then calls the API to retrieve the required DNS records.
|
|
42
51
|
*/
|
|
43
|
-
async setupDomain(flags
|
|
52
|
+
async setupDomain(flags) {
|
|
44
53
|
const yamlConfig = await this.ensureYaml();
|
|
45
|
-
this.log(`\n ${
|
|
54
|
+
this.log(`\n ${SEPARATOR}`);
|
|
46
55
|
this.log(` ${chalk.bold('DOMAIN SETUP')}`);
|
|
47
|
-
this.log(` ${
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
address,
|
|
51
|
-
domain,
|
|
52
|
-
fromEmail: senderEmail,
|
|
53
|
-
};
|
|
54
|
-
if (fromName)
|
|
55
|
-
apiPayload.fromName = fromName;
|
|
56
|
-
if (replyTo)
|
|
57
|
-
apiPayload.replyTo = replyTo;
|
|
58
|
-
const response = await this.withApiSpinner({ json: flags.json, text: ' Configuring domain...' }, () => this.apiClient.post(API_ENDPOINTS.DOMAIN, apiPayload));
|
|
59
|
-
if (!response.ok) {
|
|
60
|
-
this.handleApiError(response);
|
|
61
|
-
}
|
|
62
|
-
yamlConfig.project.domain = domain;
|
|
63
|
-
yamlConfig.project.fromEmail = senderEmail;
|
|
64
|
-
yamlConfig.project.address = address;
|
|
65
|
-
if (fromName)
|
|
66
|
-
yamlConfig.project.fromName = fromName;
|
|
67
|
-
if (replyTo)
|
|
68
|
-
yamlConfig.project.replyTo = replyTo;
|
|
69
|
-
await saveYaml(yamlConfig);
|
|
70
|
-
await saveConfig({ ...config, domain });
|
|
71
|
-
const records = response.data?.dnsRecords || [];
|
|
72
|
-
const guideUrl = response.data?.dnsGuideUrl;
|
|
56
|
+
this.log(` ${SEPARATOR}\n`);
|
|
57
|
+
const inputs = await this.collectDomainSetupInputs(yamlConfig, flags.yes);
|
|
58
|
+
const { dnsRecords, dnsGuideUrl } = await this.registerDomain(yamlConfig, inputs, flags.json);
|
|
73
59
|
if (flags.json) {
|
|
74
|
-
this.log(JSON.stringify({ dnsRecords
|
|
60
|
+
this.log(JSON.stringify({ dnsRecords, domain: inputs.domain }, null, 2));
|
|
75
61
|
return;
|
|
76
62
|
}
|
|
77
|
-
this.
|
|
78
|
-
for (const [i, record] of records.entries()) {
|
|
79
|
-
this.log(` ${chalk.bold(`RECORD ${i + 1} — ${this.recordLabel(i)}`)}`);
|
|
80
|
-
this.log(` Type: ${record.type}`);
|
|
81
|
-
this.log(` Host: ${record.host}`);
|
|
82
|
-
this.log(` Value: ${record.value}\n`);
|
|
83
|
-
}
|
|
84
|
-
this.log(` DNS changes take 5–30 minutes to propagate.`);
|
|
85
|
-
if (guideUrl)
|
|
86
|
-
this.log(` Full guide: ${chalk.cyan(guideUrl)}\n`);
|
|
63
|
+
this.logDnsRecords(dnsRecords, dnsGuideUrl, flags.json);
|
|
87
64
|
if (!flags.yes) {
|
|
88
65
|
const action = await input({
|
|
89
66
|
default: '',
|
|
90
|
-
message:
|
|
67
|
+
message: PROMPTS.ENTER_AFTER_RECORDS,
|
|
91
68
|
});
|
|
92
69
|
if (action.toLowerCase() !== 'skip') {
|
|
93
|
-
await this.verifyDomain(false,
|
|
70
|
+
await this.verifyDomain(false, inputs.domain);
|
|
94
71
|
}
|
|
95
72
|
}
|
|
96
73
|
}
|
|
97
74
|
/**
|
|
98
75
|
* Calls the domain verification API and displays pass/fail for each DNS record.
|
|
99
76
|
*/
|
|
100
|
-
async verifyDomain(jsonOutput,
|
|
101
|
-
if (!config.domain) {
|
|
102
|
-
this.error(`No domain configured. Run ${chalk.cyan('mailmodo domain')} to set up your sending domain.`);
|
|
103
|
-
}
|
|
104
|
-
const domain = config.domain;
|
|
77
|
+
async verifyDomain(jsonOutput, domain) {
|
|
105
78
|
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Checking DNS...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY, {
|
|
106
79
|
domain,
|
|
107
80
|
}));
|
|
@@ -121,7 +94,7 @@ export default class Domain extends BaseCommand {
|
|
|
121
94
|
this.log(`\n ${chalk.green('✓')} Domain verified.\n`);
|
|
122
95
|
}
|
|
123
96
|
else {
|
|
124
|
-
this.log(`\n ${
|
|
97
|
+
this.log(`\n ${INFO.DNS_RECORDS_FAILED}`);
|
|
125
98
|
if (!dkim) {
|
|
126
99
|
this.log(`\n DKIM common mistakes:`);
|
|
127
100
|
this.log(` - Using TXT instead of CNAME record type`);
|
|
@@ -133,7 +106,7 @@ export default class Domain extends BaseCommand {
|
|
|
133
106
|
this.log(` - Missing or incorrect CNAME for mm-bounce subdomain`);
|
|
134
107
|
this.log(` - Cloudflare: proxy must be OFF (grey cloud, not orange)`);
|
|
135
108
|
}
|
|
136
|
-
this.log(`\n
|
|
109
|
+
this.log(`\n ${INFO.DNS_FIX_AND_VERIFY}`);
|
|
137
110
|
if (dnsGuideUrl)
|
|
138
111
|
this.log(` Help: ${chalk.cyan(dnsGuideUrl)}\n`);
|
|
139
112
|
}
|
|
@@ -142,11 +115,7 @@ export default class Domain extends BaseCommand {
|
|
|
142
115
|
* Displays domain health metrics including verification status,
|
|
143
116
|
* bounce rate, and spam complaint rate.
|
|
144
117
|
*/
|
|
145
|
-
async showDomainStatus(jsonOutput,
|
|
146
|
-
if (!config.domain) {
|
|
147
|
-
this.error(`No domain configured. Run ${chalk.cyan('mailmodo domain')} to set up your sending domain.`);
|
|
148
|
-
}
|
|
149
|
-
const domain = config.domain;
|
|
118
|
+
async showDomainStatus(jsonOutput, domain) {
|
|
150
119
|
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Loading domain status...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_STATUS, {
|
|
151
120
|
domain,
|
|
152
121
|
}));
|
|
@@ -163,47 +132,4 @@ export default class Domain extends BaseCommand {
|
|
|
163
132
|
this.log(` Bounce rate: ${data.bounceRate ?? 'N/A'}%`);
|
|
164
133
|
this.log(` Spam rate: ${data.spamRate ?? 'N/A'}%\n`);
|
|
165
134
|
}
|
|
166
|
-
async collectDomainInputs(skipPrompts, yamlConfig) {
|
|
167
|
-
if (skipPrompts) {
|
|
168
|
-
const domain = yamlConfig.project?.domain || '';
|
|
169
|
-
if (!domain) {
|
|
170
|
-
this.error('Domain is required. Set it in mailmodo.yaml or use interactive mode.');
|
|
171
|
-
}
|
|
172
|
-
return {
|
|
173
|
-
address: yamlConfig.project?.address || '',
|
|
174
|
-
domain,
|
|
175
|
-
fromName: yamlConfig.project?.fromName || '',
|
|
176
|
-
replyTo: yamlConfig.project?.replyTo || '',
|
|
177
|
-
senderEmail: yamlConfig.project?.fromEmail || '',
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
const domain = await input({
|
|
181
|
-
default: yamlConfig.project?.domain,
|
|
182
|
-
message: 'What domain will you send from?',
|
|
183
|
-
validate: (v) => (v?.trim() ? true : 'Domain is required'),
|
|
184
|
-
});
|
|
185
|
-
const senderEmail = await input({
|
|
186
|
-
default: yamlConfig.project?.fromEmail,
|
|
187
|
-
message: 'Sender email address:',
|
|
188
|
-
validate: (v) => (v?.includes('@') ? true : 'Please enter a valid email'),
|
|
189
|
-
});
|
|
190
|
-
const fromName = await input({
|
|
191
|
-
default: yamlConfig.project?.fromName || '',
|
|
192
|
-
message: 'Display name (optional, shown as sender name):',
|
|
193
|
-
});
|
|
194
|
-
const replyTo = await input({
|
|
195
|
-
default: yamlConfig.project?.replyTo || '',
|
|
196
|
-
message: 'Reply-to address (optional, press Enter to use sender email):',
|
|
197
|
-
});
|
|
198
|
-
const address = await input({
|
|
199
|
-
default: yamlConfig.project?.address,
|
|
200
|
-
message: 'Business address (required by law):',
|
|
201
|
-
validate: (v) => (v?.trim() ? true : 'Address is required'),
|
|
202
|
-
});
|
|
203
|
-
return { address, domain, fromName, replyTo, senderEmail };
|
|
204
|
-
}
|
|
205
|
-
recordLabel(index) {
|
|
206
|
-
const labels = ['DKIM', 'DMARC', 'Return Path'];
|
|
207
|
-
return labels[index] || `Record ${index + 1}`;
|
|
208
|
-
}
|
|
209
135
|
}
|
|
@@ -5,6 +5,7 @@ import { BaseCommand } from '../../lib/base-command.js';
|
|
|
5
5
|
import { ApiClient } from '../../lib/api-client.js';
|
|
6
6
|
import { loadConfig, saveConfig } from '../../lib/config.js';
|
|
7
7
|
import { API_ENDPOINTS, LOGIN_URL } from '../../lib/constants.js';
|
|
8
|
+
import { INFO } from '../../lib/messages.js';
|
|
8
9
|
export default class Login extends BaseCommand {
|
|
9
10
|
static description = 'Authenticate with Mailmodo using your API key';
|
|
10
11
|
static examples = [
|
|
@@ -22,10 +23,9 @@ export default class Login extends BaseCommand {
|
|
|
22
23
|
if (existing?.apiKey) {
|
|
23
24
|
if (flags.json) {
|
|
24
25
|
this.log(JSON.stringify({
|
|
25
|
-
accountName: existing.accountName ?? null,
|
|
26
26
|
email: existing.email ?? null,
|
|
27
|
-
freeRemaining: existing.freeRemaining ?? null,
|
|
28
27
|
status: 'already_logged_in',
|
|
28
|
+
totalFreeRemaining: existing.totalFreeRemaining ?? null,
|
|
29
29
|
}, null, 2));
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
@@ -47,10 +47,10 @@ export default class Login extends BaseCommand {
|
|
|
47
47
|
this.log(`\n Get your free API key at: ${chalk.cyan(LOGIN_URL)}\n`);
|
|
48
48
|
try {
|
|
49
49
|
await open(LOGIN_URL);
|
|
50
|
-
this.log(
|
|
50
|
+
this.log(` ${INFO.BROWSER_OPENING}\n`);
|
|
51
51
|
}
|
|
52
52
|
catch {
|
|
53
|
-
this.log(` ${
|
|
53
|
+
this.log(` ${INFO.BROWSER_OPEN_FAILED}\n`);
|
|
54
54
|
}
|
|
55
55
|
apiKey = await input({
|
|
56
56
|
message: 'Paste your API key:',
|
|
@@ -67,19 +67,20 @@ export default class Login extends BaseCommand {
|
|
|
67
67
|
if (!response.ok) {
|
|
68
68
|
this.handleApiError(response);
|
|
69
69
|
}
|
|
70
|
-
const {
|
|
70
|
+
const { email, totalFreeRemaining } = response.data;
|
|
71
71
|
await saveConfig({
|
|
72
|
-
accountName,
|
|
73
72
|
apiKey: trimmedKey,
|
|
74
73
|
email,
|
|
75
|
-
|
|
74
|
+
totalFreeRemaining,
|
|
76
75
|
});
|
|
77
76
|
if (flags.json) {
|
|
78
|
-
this.log(JSON.stringify({
|
|
77
|
+
this.log(JSON.stringify({ email, status: 'authenticated', totalFreeRemaining }, null, 2));
|
|
79
78
|
return;
|
|
80
79
|
}
|
|
81
80
|
this.log(`\n Logged in as ${chalk.green(email)}`);
|
|
82
|
-
|
|
81
|
+
if (totalFreeRemaining !== null && totalFreeRemaining !== undefined) {
|
|
82
|
+
this.log(` Free tier: ${chalk.cyan(String(totalFreeRemaining))} emails remaining`);
|
|
83
|
+
}
|
|
83
84
|
this.log(' No credit card required.\n');
|
|
84
85
|
this.log(` Next: Run ${chalk.cyan("'mailmodo init'")} to generate your email sequences.\n`);
|
|
85
86
|
}
|
|
@@ -4,6 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
import open from 'open';
|
|
5
5
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
6
6
|
import { API_ENDPOINTS, PREVIEW_PORT } from '../../lib/constants.js';
|
|
7
|
+
import { INFO } from '../../lib/messages.js';
|
|
7
8
|
import { loadTemplate, getEmailStyle, getTemplateFilename, } from '../../lib/yaml-config.js';
|
|
8
9
|
/* eslint-disable camelcase */
|
|
9
10
|
const SAMPLE_DATA = Object.freeze({
|
|
@@ -238,7 +239,7 @@ export default class Preview extends BaseCommand {
|
|
|
238
239
|
if (!jsonOutput) {
|
|
239
240
|
this.log(`\n Style: ${chalk.cyan(effectiveStyle)}`);
|
|
240
241
|
this.log(` Preview server at ${chalk.cyan(url)}`);
|
|
241
|
-
this.log(`
|
|
242
|
+
this.log(` ${INFO.BROWSER_OPENING}\n`);
|
|
242
243
|
this.log(` ${chalk.dim('Press Ctrl+C to stop the preview server.')}\n`);
|
|
243
244
|
}
|
|
244
245
|
try {
|
|
@@ -246,7 +247,7 @@ export default class Preview extends BaseCommand {
|
|
|
246
247
|
}
|
|
247
248
|
catch {
|
|
248
249
|
if (!jsonOutput) {
|
|
249
|
-
this.log(` ${
|
|
250
|
+
this.log(` ${INFO.BROWSER_OPEN_FAILED}`);
|
|
250
251
|
}
|
|
251
252
|
}
|
|
252
253
|
await new Promise((resolve) => {
|
|
@@ -20,14 +20,7 @@ export default class Settings extends BaseCommand {
|
|
|
20
20
|
* Returns true/false for verified/unverified, or null if unavailable.
|
|
21
21
|
*/
|
|
22
22
|
private fetchDomainVerified;
|
|
23
|
-
/**
|
|
24
|
-
* Handles domain change: collects the new domain, sender email, and
|
|
25
|
-
* business address, calls the API to register them, displays the required
|
|
26
|
-
* DNS records, and saves the updated config. Emails won't send until the
|
|
27
|
-
* domain is re-verified.
|
|
28
|
-
*/
|
|
29
23
|
private handleDomainChange;
|
|
30
|
-
private recordLabel;
|
|
31
24
|
/**
|
|
32
25
|
* Handles the logo file upload flow: validates the local file exists,
|
|
33
26
|
* reads it, uploads to Mailmodo CDN via API, and updates both logoFile
|
|
@@ -6,6 +6,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
6
6
|
import { resolve } from 'node:path';
|
|
7
7
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
8
8
|
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
9
|
+
import { INFO } from '../../lib/messages.js';
|
|
9
10
|
import { saveYaml } from '../../lib/yaml-config.js';
|
|
10
11
|
const SETTINGS_GROUPS = Object.freeze({
|
|
11
12
|
billing: ['monthly_cap'],
|
|
@@ -80,7 +81,7 @@ export default class Settings extends BaseCommand {
|
|
|
80
81
|
return;
|
|
81
82
|
}
|
|
82
83
|
this.log(`\n ${chalk.green('✓')} ${key} updated to ${chalk.cyan(value)}`);
|
|
83
|
-
this.log(`
|
|
84
|
+
this.log(` ${INFO.DEPLOY_TO_APPLY}\n`);
|
|
84
85
|
}
|
|
85
86
|
displaySettingsGroup(group, keys, project, domainVerified) {
|
|
86
87
|
const availableKeys = keys.filter((key) => {
|
|
@@ -169,7 +170,7 @@ export default class Settings extends BaseCommand {
|
|
|
169
170
|
project.emailStyle = style;
|
|
170
171
|
await saveYaml(yamlConfig);
|
|
171
172
|
this.log(`\n ${chalk.green('✓')} email_style updated to ${chalk.cyan(style)}`);
|
|
172
|
-
this.log(`
|
|
173
|
+
this.log(` ${INFO.DEPLOY_TO_APPLY}\n`);
|
|
173
174
|
return;
|
|
174
175
|
}
|
|
175
176
|
const newValue = await input({
|
|
@@ -178,7 +179,7 @@ export default class Settings extends BaseCommand {
|
|
|
178
179
|
project[editPropKey] =
|
|
179
180
|
editPropKey === 'monthlyCap' ? Number(newValue) : newValue;
|
|
180
181
|
await saveYaml(yamlConfig);
|
|
181
|
-
this.log(`\n ${chalk.green('✓')} Updated.
|
|
182
|
+
this.log(`\n ${chalk.green('✓')} Updated. ${INFO.DEPLOY_TO_APPLY}\n`);
|
|
182
183
|
}
|
|
183
184
|
/**
|
|
184
185
|
* Fetches the domain verification status from the API.
|
|
@@ -201,58 +202,14 @@ export default class Settings extends BaseCommand {
|
|
|
201
202
|
return null;
|
|
202
203
|
}
|
|
203
204
|
}
|
|
204
|
-
/**
|
|
205
|
-
* Handles domain change: collects the new domain, sender email, and
|
|
206
|
-
* business address, calls the API to register them, displays the required
|
|
207
|
-
* DNS records, and saves the updated config. Emails won't send until the
|
|
208
|
-
* domain is re-verified.
|
|
209
|
-
*/
|
|
210
205
|
async handleDomainChange(yamlConfig) {
|
|
211
|
-
const newDomain = await input({
|
|
212
|
-
message: 'New domain:',
|
|
213
|
-
validate: (v) => (v?.trim() ? true : 'Domain is required'),
|
|
214
|
-
});
|
|
215
|
-
const newFromEmail = await input({
|
|
216
|
-
default: yamlConfig.project.fromEmail || '',
|
|
217
|
-
message: 'Sender email (from address):',
|
|
218
|
-
validate: (v) => (v?.includes('@') ? true : 'Please enter a valid email'),
|
|
219
|
-
});
|
|
220
|
-
const newAddress = await input({
|
|
221
|
-
default: yamlConfig.project.address || '',
|
|
222
|
-
message: 'Business address (required by law):',
|
|
223
|
-
validate: (v) => (v?.trim() ? true : 'Address is required'),
|
|
224
|
-
});
|
|
225
206
|
await this.ensureAuth();
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
});
|
|
231
|
-
if (!response.ok) {
|
|
232
|
-
this.handleApiError(response);
|
|
233
|
-
}
|
|
234
|
-
const records = response.data?.dnsRecords || [];
|
|
235
|
-
const dnsGuideUrl = response.data?.dnsGuideUrl;
|
|
236
|
-
yamlConfig.project.domain = newDomain;
|
|
237
|
-
yamlConfig.project.fromEmail = newFromEmail;
|
|
238
|
-
yamlConfig.project.address = newAddress;
|
|
239
|
-
await saveYaml(yamlConfig);
|
|
240
|
-
this.log(`\n Domain, sender email, and business address updated. You will need to re-verify.`);
|
|
241
|
-
this.log(` New DNS records:\n`);
|
|
242
|
-
for (const [i, record] of records.entries()) {
|
|
243
|
-
this.log(` ${chalk.bold(`RECORD ${i + 1} — ${this.recordLabel(i)}`)}`);
|
|
244
|
-
this.log(` Type: ${record.type}`);
|
|
245
|
-
this.log(` Host: ${record.host}`);
|
|
246
|
-
this.log(` Value: ${record.value}\n`);
|
|
247
|
-
}
|
|
207
|
+
const inputs = await this.collectDomainSetupInputs(yamlConfig, false);
|
|
208
|
+
const { dnsRecords, dnsGuideUrl } = await this.registerDomain(yamlConfig, inputs, false);
|
|
209
|
+
this.log(`\n Domain and sender details updated. You will need to re-verify.`);
|
|
210
|
+
this.logDnsRecords(dnsRecords, dnsGuideUrl, false);
|
|
248
211
|
this.log(` Run ${chalk.cyan("'mailmodo domain --verify'")} once records are added.`);
|
|
249
212
|
this.log(` Emails will not send until the new domain is verified.`);
|
|
250
|
-
if (dnsGuideUrl)
|
|
251
|
-
this.log(` Help: ${chalk.cyan(dnsGuideUrl)}\n`);
|
|
252
|
-
}
|
|
253
|
-
recordLabel(index) {
|
|
254
|
-
const labels = ['DKIM', 'DMARC', 'Return Path'];
|
|
255
|
-
return labels[index] || `Record ${index + 1}`;
|
|
256
213
|
}
|
|
257
214
|
/**
|
|
258
215
|
* Handles the logo file upload flow: validates the local file exists,
|
|
@@ -38,13 +38,18 @@ export default class Status extends BaseCommand {
|
|
|
38
38
|
this.log(` ${chalk.dim('No data yet. Deploy emails first.')}`);
|
|
39
39
|
}
|
|
40
40
|
this.log('');
|
|
41
|
-
|
|
41
|
+
if (monthlySent !== null && monthlySent !== undefined) {
|
|
42
|
+
this.log(` Emails sent this month: ${chalk.bold(String(monthlySent))}`);
|
|
43
|
+
}
|
|
42
44
|
if (quota) {
|
|
43
45
|
if (quota.freeRemaining > 0) {
|
|
44
46
|
this.log(` Free tier remaining: ${chalk.cyan(String(quota.freeRemaining))}`);
|
|
45
47
|
}
|
|
46
48
|
else if (quota.blocksUsed !== undefined) {
|
|
47
|
-
|
|
49
|
+
if (quota.currentBlockRemaining !== null &&
|
|
50
|
+
quota.currentBlockRemaining !== undefined) {
|
|
51
|
+
this.log(` Current paid block: ${chalk.cyan(`${quota.currentBlockRemaining} / 10,000 used`)}`);
|
|
52
|
+
}
|
|
48
53
|
this.log(` Blocks purchased: ${quota.blocksUsed}`);
|
|
49
54
|
}
|
|
50
55
|
}
|
|
@@ -61,6 +61,32 @@ export declare abstract class BaseCommand extends Command {
|
|
|
61
61
|
error?: string;
|
|
62
62
|
status: number;
|
|
63
63
|
}): never;
|
|
64
|
+
protected collectDomainSetupInputs(yamlConfig: MailmodoYaml, skipPrompts: boolean): Promise<{
|
|
65
|
+
address: string;
|
|
66
|
+
domain: string;
|
|
67
|
+
fromEmail: string;
|
|
68
|
+
fromName: string;
|
|
69
|
+
replyTo: string;
|
|
70
|
+
}>;
|
|
71
|
+
protected registerDomain(yamlConfig: MailmodoYaml, inputs: {
|
|
72
|
+
address: string;
|
|
73
|
+
domain: string;
|
|
74
|
+
fromEmail: string;
|
|
75
|
+
fromName?: string;
|
|
76
|
+
replyTo?: string;
|
|
77
|
+
}, json: boolean): Promise<{
|
|
78
|
+
dnsGuideUrl?: string;
|
|
79
|
+
dnsRecords: Array<{
|
|
80
|
+
host: string;
|
|
81
|
+
type: string;
|
|
82
|
+
value: string;
|
|
83
|
+
}>;
|
|
84
|
+
}>;
|
|
85
|
+
protected logDnsRecords(records: Array<{
|
|
86
|
+
host: string;
|
|
87
|
+
type: string;
|
|
88
|
+
value: string;
|
|
89
|
+
}>, guideUrl: string | undefined, json: boolean): void;
|
|
64
90
|
/**
|
|
65
91
|
* Builds the terminal error string for a failed API call, appending request
|
|
66
92
|
* metadata when {@link ApiRequestDebugInfo} is available.
|
package/dist/lib/base-command.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { input } from '@inquirer/prompts';
|
|
1
2
|
import { Command, Flags } from '@oclif/core';
|
|
2
3
|
import chalk from 'chalk';
|
|
3
4
|
import ora from 'ora';
|
|
4
5
|
import { ApiClient } from './api-client.js';
|
|
5
6
|
import { loadConfig } from './config.js';
|
|
6
|
-
import {
|
|
7
|
+
import { API_ENDPOINTS } from './constants.js';
|
|
8
|
+
import { ERRORS, INFO, PROMPTS, recordLabel, VALIDATION } from './messages.js';
|
|
9
|
+
import { loadYaml, saveYaml } from './yaml-config.js';
|
|
7
10
|
/**
|
|
8
11
|
* Abstract base command providing shared functionality for all Mailmodo CLI commands.
|
|
9
12
|
* Subclasses inherit --json and --yes base flags, authentication enforcement,
|
|
@@ -35,7 +38,7 @@ export class BaseCommand extends Command {
|
|
|
35
38
|
}
|
|
36
39
|
const config = await loadConfig();
|
|
37
40
|
if (!config?.apiKey) {
|
|
38
|
-
this.error(
|
|
41
|
+
this.error(ERRORS.NOT_LOGGED_IN);
|
|
39
42
|
}
|
|
40
43
|
this.apiClient = new ApiClient(config.apiKey);
|
|
41
44
|
return config;
|
|
@@ -80,7 +83,7 @@ export class BaseCommand extends Command {
|
|
|
80
83
|
async ensureYaml() {
|
|
81
84
|
const config = await loadYaml();
|
|
82
85
|
if (!config) {
|
|
83
|
-
this.error(
|
|
86
|
+
this.error(ERRORS.NO_YAML);
|
|
84
87
|
}
|
|
85
88
|
return config;
|
|
86
89
|
}
|
|
@@ -96,12 +99,92 @@ export class BaseCommand extends Command {
|
|
|
96
99
|
*/
|
|
97
100
|
handleApiError(response) {
|
|
98
101
|
if (response.status === 401) {
|
|
99
|
-
this.error(this.formatApiFailure(
|
|
102
|
+
this.error(this.formatApiFailure(ERRORS.INVALID_API_KEY, response));
|
|
100
103
|
}
|
|
101
104
|
if (response.status === 429) {
|
|
102
|
-
this.error(this.formatApiFailure(
|
|
105
|
+
this.error(this.formatApiFailure(ERRORS.RATE_LIMIT, response));
|
|
103
106
|
}
|
|
104
|
-
this.error(this.formatApiFailure(response.error ||
|
|
107
|
+
this.error(this.formatApiFailure(response.error || ERRORS.UNEXPECTED_API, response));
|
|
108
|
+
}
|
|
109
|
+
async collectDomainSetupInputs(yamlConfig, skipPrompts) {
|
|
110
|
+
if (skipPrompts) {
|
|
111
|
+
const domain = yamlConfig.project?.domain || '';
|
|
112
|
+
if (!domain) {
|
|
113
|
+
this.error('Domain is required. Set it in mailmodo.yaml or use interactive mode.');
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
address: yamlConfig.project?.address || '',
|
|
117
|
+
domain,
|
|
118
|
+
fromEmail: yamlConfig.project?.fromEmail || '',
|
|
119
|
+
fromName: yamlConfig.project?.fromName || '',
|
|
120
|
+
replyTo: yamlConfig.project?.replyTo || '',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const domain = await input({
|
|
124
|
+
default: yamlConfig.project?.domain,
|
|
125
|
+
message: PROMPTS.DOMAIN,
|
|
126
|
+
validate: (v) => (v?.trim() ? true : VALIDATION.DOMAIN_REQUIRED),
|
|
127
|
+
});
|
|
128
|
+
const fromEmail = await input({
|
|
129
|
+
default: yamlConfig.project?.fromEmail,
|
|
130
|
+
message: PROMPTS.SENDER_EMAIL,
|
|
131
|
+
validate: (v) => (v?.includes('@') ? true : VALIDATION.EMAIL_INVALID),
|
|
132
|
+
});
|
|
133
|
+
const fromName = await input({
|
|
134
|
+
default: yamlConfig.project?.fromName || '',
|
|
135
|
+
message: PROMPTS.FROM_NAME,
|
|
136
|
+
});
|
|
137
|
+
const replyTo = await input({
|
|
138
|
+
default: yamlConfig.project?.replyTo || '',
|
|
139
|
+
message: PROMPTS.REPLY_TO,
|
|
140
|
+
});
|
|
141
|
+
const address = await input({
|
|
142
|
+
default: yamlConfig.project?.address,
|
|
143
|
+
message: PROMPTS.BUSINESS_ADDRESS,
|
|
144
|
+
validate: (v) => (v?.trim() ? true : VALIDATION.ADDRESS_REQUIRED),
|
|
145
|
+
});
|
|
146
|
+
return { address, domain, fromEmail, fromName, replyTo };
|
|
147
|
+
}
|
|
148
|
+
async registerDomain(yamlConfig, inputs, json) {
|
|
149
|
+
const apiPayload = {
|
|
150
|
+
address: inputs.address,
|
|
151
|
+
domain: inputs.domain,
|
|
152
|
+
fromEmail: inputs.fromEmail,
|
|
153
|
+
};
|
|
154
|
+
if (inputs.fromName)
|
|
155
|
+
apiPayload.fromName = inputs.fromName;
|
|
156
|
+
if (inputs.replyTo)
|
|
157
|
+
apiPayload.replyTo = inputs.replyTo;
|
|
158
|
+
const response = await this.withApiSpinner({ json, text: ' Configuring domain...' }, () => this.apiClient.post(API_ENDPOINTS.DOMAIN, apiPayload));
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
this.handleApiError(response);
|
|
161
|
+
}
|
|
162
|
+
yamlConfig.project.domain = inputs.domain;
|
|
163
|
+
yamlConfig.project.fromEmail = inputs.fromEmail;
|
|
164
|
+
yamlConfig.project.address = inputs.address;
|
|
165
|
+
if (inputs.fromName)
|
|
166
|
+
yamlConfig.project.fromName = inputs.fromName;
|
|
167
|
+
if (inputs.replyTo)
|
|
168
|
+
yamlConfig.project.replyTo = inputs.replyTo;
|
|
169
|
+
await saveYaml(yamlConfig);
|
|
170
|
+
return {
|
|
171
|
+
dnsGuideUrl: response.data?.dnsGuideUrl,
|
|
172
|
+
dnsRecords: response.data?.dnsRecords || [],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
logDnsRecords(records, guideUrl, json) {
|
|
176
|
+
if (json)
|
|
177
|
+
return;
|
|
178
|
+
this.log(`\n Add these ${records.length} DNS records to your domain provider:\n`);
|
|
179
|
+
for (const [i, record] of records.entries()) {
|
|
180
|
+
this.log(` ${chalk.bold(`RECORD ${i + 1} — ${recordLabel(i)}`)}`);
|
|
181
|
+
this.log(` Type: ${record.type}`);
|
|
182
|
+
this.log(` Host: ${record.host}`);
|
|
183
|
+
this.log(` Value: ${record.value}\n`);
|
|
184
|
+
}
|
|
185
|
+
this.log(` ${INFO.DNS_PROPAGATION}`);
|
|
186
|
+
if (guideUrl)
|
|
187
|
+
this.log(` Full guide: ${chalk.cyan(guideUrl)}\n`);
|
|
105
188
|
}
|
|
106
189
|
/**
|
|
107
190
|
* Builds the terminal error string for a failed API call, appending request
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
export interface MailmodoConfig {
|
|
2
|
-
accountName?: string;
|
|
3
2
|
apiKey: string;
|
|
4
|
-
domain?: string;
|
|
5
3
|
email?: string;
|
|
6
|
-
|
|
4
|
+
totalFreeRemaining?: number;
|
|
7
5
|
}
|
|
8
6
|
/**
|
|
9
7
|
* Loads the Mailmodo CLI configuration from ~/.mailmodo/config.
|
|
10
8
|
* The config file stores the API key and account metadata as JSON.
|
|
11
9
|
*
|
|
12
10
|
* @returns {Promise<MailmodoConfig | null>} The parsed configuration object
|
|
13
|
-
* containing apiKey, email,
|
|
11
|
+
* containing apiKey, email, and totalFreeRemaining quota,
|
|
14
12
|
* or null if the config file does not exist or is corrupted.
|
|
15
13
|
*/
|
|
16
14
|
export declare function loadConfig(): Promise<MailmodoConfig | null>;
|
|
@@ -20,7 +18,7 @@ export declare function loadConfig(): Promise<MailmodoConfig | null>;
|
|
|
20
18
|
* Overwrites any existing config file.
|
|
21
19
|
*
|
|
22
20
|
* @param {MailmodoConfig} config - The configuration to persist, must include
|
|
23
|
-
* at minimum an apiKey. Optional fields: email,
|
|
21
|
+
* at minimum an apiKey. Optional fields: email, totalFreeRemaining.
|
|
24
22
|
*/
|
|
25
23
|
export declare function saveConfig(config: MailmodoConfig): Promise<void>;
|
|
26
24
|
/**
|
package/dist/lib/config.js
CHANGED
|
@@ -9,7 +9,7 @@ const CONFIG_FILE = join(CONFIG_DIR, 'config');
|
|
|
9
9
|
* The config file stores the API key and account metadata as JSON.
|
|
10
10
|
*
|
|
11
11
|
* @returns {Promise<MailmodoConfig | null>} The parsed configuration object
|
|
12
|
-
* containing apiKey, email,
|
|
12
|
+
* containing apiKey, email, and totalFreeRemaining quota,
|
|
13
13
|
* or null if the config file does not exist or is corrupted.
|
|
14
14
|
*/
|
|
15
15
|
export async function loadConfig() {
|
|
@@ -29,7 +29,7 @@ export async function loadConfig() {
|
|
|
29
29
|
* Overwrites any existing config file.
|
|
30
30
|
*
|
|
31
31
|
* @param {MailmodoConfig} config - The configuration to persist, must include
|
|
32
|
-
* at minimum an apiKey. Optional fields: email,
|
|
32
|
+
* at minimum an apiKey. Optional fields: email, totalFreeRemaining.
|
|
33
33
|
*/
|
|
34
34
|
export async function saveConfig(config) {
|
|
35
35
|
if (!existsSync(CONFIG_DIR)) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export declare const SEPARATOR: string;
|
|
2
|
+
export declare const VALIDATION: {
|
|
3
|
+
readonly ADDRESS_REQUIRED: "Address is required";
|
|
4
|
+
readonly DOMAIN_REQUIRED: "Domain is required";
|
|
5
|
+
readonly EMAIL_INVALID: "Please enter a valid email";
|
|
6
|
+
};
|
|
7
|
+
export declare const PROMPTS: {
|
|
8
|
+
readonly BUSINESS_ADDRESS: "Business address (required by law):";
|
|
9
|
+
readonly DOMAIN: "What domain will you send from?";
|
|
10
|
+
readonly ENTER_AFTER_RECORDS: "Press Enter once you've added the records, or 'skip'.";
|
|
11
|
+
readonly FROM_NAME: "Display name (optional, shown as sender name):";
|
|
12
|
+
readonly REPLY_TO: "Reply-to address (optional, press Enter to use sender email):";
|
|
13
|
+
readonly SENDER_EMAIL: "Sender email address:";
|
|
14
|
+
};
|
|
15
|
+
export declare const ERRORS: {
|
|
16
|
+
readonly DOMAIN_NOT_CONFIGURED: `No domain configured. Run ${string} to set up your sending domain.`;
|
|
17
|
+
readonly DOMAIN_NOT_REGISTERED: `Sending domain not registered. Run: ${string}`;
|
|
18
|
+
readonly DOMAIN_NOT_VERIFIED: `Sending domain not verified. Run: ${string}`;
|
|
19
|
+
readonly INVALID_API_KEY: `Invalid API key. Run ${string} to re-authenticate.`;
|
|
20
|
+
readonly NOT_LOGGED_IN: `Not logged in. Run ${string} to authenticate.`;
|
|
21
|
+
readonly NO_YAML: `No mailmodo.yaml found. Run ${string} first.`;
|
|
22
|
+
readonly RATE_LIMIT: "Rate limit exceeded. Please try again later.";
|
|
23
|
+
readonly UNEXPECTED_API: "An unexpected API error occurred.";
|
|
24
|
+
};
|
|
25
|
+
export declare const INFO: {
|
|
26
|
+
readonly BROWSER_OPEN_FAILED: string;
|
|
27
|
+
readonly BROWSER_OPENING: "Opening in browser...";
|
|
28
|
+
readonly DEPLOY_TO_APPLY: `Run ${string} to apply.`;
|
|
29
|
+
readonly DNS_FIX_AND_VERIFY: `Fix the records and run ${string} again.`;
|
|
30
|
+
readonly DNS_PROPAGATION: "DNS changes take 5–30 minutes to propagate.";
|
|
31
|
+
readonly DNS_RECORDS_FAILED: string;
|
|
32
|
+
readonly DOMAIN_NOT_DEPLOYED_HINT: `When ready, run: ${string}
|
|
33
|
+
Then: ${string}`;
|
|
34
|
+
readonly SEQUENCES_NOT_DEPLOYED: `Sequences saved but ${string}.`;
|
|
35
|
+
};
|
|
36
|
+
export declare function recordLabel(index: number): string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export const SEPARATOR = '─'.repeat(53);
|
|
3
|
+
export const VALIDATION = {
|
|
4
|
+
ADDRESS_REQUIRED: 'Address is required',
|
|
5
|
+
DOMAIN_REQUIRED: 'Domain is required',
|
|
6
|
+
EMAIL_INVALID: 'Please enter a valid email',
|
|
7
|
+
};
|
|
8
|
+
export const PROMPTS = {
|
|
9
|
+
BUSINESS_ADDRESS: 'Business address (required by law):',
|
|
10
|
+
DOMAIN: 'What domain will you send from?',
|
|
11
|
+
ENTER_AFTER_RECORDS: "Press Enter once you've added the records, or 'skip'.",
|
|
12
|
+
FROM_NAME: 'Display name (optional, shown as sender name):',
|
|
13
|
+
REPLY_TO: 'Reply-to address (optional, press Enter to use sender email):',
|
|
14
|
+
SENDER_EMAIL: 'Sender email address:',
|
|
15
|
+
};
|
|
16
|
+
export const ERRORS = {
|
|
17
|
+
DOMAIN_NOT_CONFIGURED: `No domain configured. Run ${chalk.cyan('mailmodo domain')} to set up your sending domain.`,
|
|
18
|
+
DOMAIN_NOT_REGISTERED: `Sending domain not registered. Run: ${chalk.cyan('mailmodo domain')}`,
|
|
19
|
+
DOMAIN_NOT_VERIFIED: `Sending domain not verified. Run: ${chalk.cyan('mailmodo domain --verify')}`,
|
|
20
|
+
INVALID_API_KEY: `Invalid API key. Run ${chalk.cyan('mailmodo login')} to re-authenticate.`,
|
|
21
|
+
NOT_LOGGED_IN: `Not logged in. Run ${chalk.cyan('mailmodo login')} to authenticate.`,
|
|
22
|
+
NO_YAML: `No mailmodo.yaml found. Run ${chalk.cyan('mailmodo init')} first.`,
|
|
23
|
+
RATE_LIMIT: 'Rate limit exceeded. Please try again later.',
|
|
24
|
+
UNEXPECTED_API: 'An unexpected API error occurred.',
|
|
25
|
+
};
|
|
26
|
+
export const INFO = {
|
|
27
|
+
BROWSER_OPEN_FAILED: chalk.dim('Could not open browser. Visit the URL above manually.'),
|
|
28
|
+
BROWSER_OPENING: 'Opening in browser...',
|
|
29
|
+
DEPLOY_TO_APPLY: `Run ${chalk.cyan("'mailmodo deploy'")} to apply.`,
|
|
30
|
+
DNS_FIX_AND_VERIFY: `Fix the records and run ${chalk.cyan('mailmodo domain --verify')} again.`,
|
|
31
|
+
DNS_PROPAGATION: 'DNS changes take 5–30 minutes to propagate.',
|
|
32
|
+
DNS_RECORDS_FAILED: chalk.yellow('Some records failed.'),
|
|
33
|
+
DOMAIN_NOT_DEPLOYED_HINT: `When ready, run: ${chalk.cyan('mailmodo domain')}\n Then: ${chalk.cyan('mailmodo deploy')}`,
|
|
34
|
+
SEQUENCES_NOT_DEPLOYED: `Sequences saved but ${chalk.yellow('NOT deployed')}.`,
|
|
35
|
+
};
|
|
36
|
+
export function recordLabel(index) {
|
|
37
|
+
const labels = ['DKIM', 'DMARC', 'Return Path'];
|
|
38
|
+
return labels[index] || `Record ${index + 1}`;
|
|
39
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -228,19 +228,13 @@
|
|
|
228
228
|
"index.js"
|
|
229
229
|
]
|
|
230
230
|
},
|
|
231
|
-
"
|
|
231
|
+
"emails": {
|
|
232
232
|
"aliases": [],
|
|
233
|
-
"args": {
|
|
234
|
-
|
|
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",
|
|
233
|
+
"args": {},
|
|
234
|
+
"description": "List and view configured email sequences",
|
|
241
235
|
"examples": [
|
|
242
|
-
"<%= config.bin %>
|
|
243
|
-
"<%= config.bin %>
|
|
236
|
+
"<%= config.bin %> emails",
|
|
237
|
+
"<%= config.bin %> emails --json"
|
|
244
238
|
],
|
|
245
239
|
"flags": {
|
|
246
240
|
"json": {
|
|
@@ -255,18 +249,11 @@
|
|
|
255
249
|
"name": "yes",
|
|
256
250
|
"allowNo": false,
|
|
257
251
|
"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
252
|
}
|
|
266
253
|
},
|
|
267
254
|
"hasDynamicHelp": false,
|
|
268
255
|
"hiddenAliases": [],
|
|
269
|
-
"id": "
|
|
256
|
+
"id": "emails",
|
|
270
257
|
"pluginAlias": "@mailmodo/cli",
|
|
271
258
|
"pluginName": "@mailmodo/cli",
|
|
272
259
|
"pluginType": "core",
|
|
@@ -276,7 +263,7 @@
|
|
|
276
263
|
"relativePath": [
|
|
277
264
|
"dist",
|
|
278
265
|
"commands",
|
|
279
|
-
"
|
|
266
|
+
"emails",
|
|
280
267
|
"index.js"
|
|
281
268
|
]
|
|
282
269
|
},
|
|
@@ -617,13 +604,19 @@
|
|
|
617
604
|
"index.js"
|
|
618
605
|
]
|
|
619
606
|
},
|
|
620
|
-
"
|
|
607
|
+
"edit": {
|
|
621
608
|
"aliases": [],
|
|
622
|
-
"args": {
|
|
623
|
-
|
|
609
|
+
"args": {
|
|
610
|
+
"id": {
|
|
611
|
+
"description": "Email template ID to edit",
|
|
612
|
+
"name": "id",
|
|
613
|
+
"required": true
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
"description": "Edit an email using AI-assisted natural language changes",
|
|
624
617
|
"examples": [
|
|
625
|
-
"<%= config.bin %>
|
|
626
|
-
"<%= config.bin %>
|
|
618
|
+
"<%= config.bin %> edit welcome",
|
|
619
|
+
"<%= config.bin %> edit welcome --change \"make subject more urgent\" --yes"
|
|
627
620
|
],
|
|
628
621
|
"flags": {
|
|
629
622
|
"json": {
|
|
@@ -638,11 +631,18 @@
|
|
|
638
631
|
"name": "yes",
|
|
639
632
|
"allowNo": false,
|
|
640
633
|
"type": "boolean"
|
|
634
|
+
},
|
|
635
|
+
"change": {
|
|
636
|
+
"description": "Natural language description of the change",
|
|
637
|
+
"name": "change",
|
|
638
|
+
"hasDynamicHelp": false,
|
|
639
|
+
"multiple": false,
|
|
640
|
+
"type": "option"
|
|
641
641
|
}
|
|
642
642
|
},
|
|
643
643
|
"hasDynamicHelp": false,
|
|
644
644
|
"hiddenAliases": [],
|
|
645
|
-
"id": "
|
|
645
|
+
"id": "edit",
|
|
646
646
|
"pluginAlias": "@mailmodo/cli",
|
|
647
647
|
"pluginName": "@mailmodo/cli",
|
|
648
648
|
"pluginType": "core",
|
|
@@ -652,10 +652,10 @@
|
|
|
652
652
|
"relativePath": [
|
|
653
653
|
"dist",
|
|
654
654
|
"commands",
|
|
655
|
-
"
|
|
655
|
+
"edit",
|
|
656
656
|
"index.js"
|
|
657
657
|
]
|
|
658
658
|
}
|
|
659
659
|
},
|
|
660
|
-
"version": "0.0.
|
|
660
|
+
"version": "0.0.32-beta.pr34.57"
|
|
661
661
|
}
|