@mailmodo/cli 0.0.30 → 0.0.31-beta.pr33.55
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 +23 -22
- package/dist/commands/domain/index.d.ts +0 -2
- package/dist/commands/domain/index.js +28 -102
- package/dist/commands/edit/index.d.ts +11 -1
- package/dist/commands/edit/index.js +148 -86
- package/dist/commands/emails/index.d.ts +2 -0
- package/dist/commands/emails/index.js +50 -1
- package/dist/commands/init/index.js +1 -1
- package/dist/commands/login/index.js +3 -2
- package/dist/commands/preview/index.d.ts +6 -2
- package/dist/commands/preview/index.js +41 -14
- package/dist/commands/settings/index.d.ts +0 -7
- package/dist/commands/settings/index.js +8 -51
- package/dist/lib/base-command.d.ts +26 -0
- package/dist/lib/base-command.js +89 -6
- package/dist/lib/config.d.ts +0 -1
- package/dist/lib/messages.d.ts +36 -0
- package/dist/lib/messages.js +39 -0
- package/dist/lib/yaml-config.d.ts +2 -0
- package/dist/lib/yaml-config.js +7 -0
- package/oclif.manifest.json +40 -40
- package/package.json +1 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { confirm, input } from '@inquirer/prompts';
|
|
2
4
|
import chalk from 'chalk';
|
|
5
|
+
import open from 'open';
|
|
3
6
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
4
7
|
export default class Emails extends BaseCommand {
|
|
5
8
|
static description = 'List and view configured email sequences';
|
|
@@ -61,7 +64,53 @@ export default class Emails extends BaseCommand {
|
|
|
61
64
|
this.log(` ${chalk.bold('Goal:')} ${email.goal}`);
|
|
62
65
|
}
|
|
63
66
|
this.log('');
|
|
67
|
+
const openIt = await confirm({
|
|
68
|
+
default: true,
|
|
69
|
+
message: 'Open template in editor?',
|
|
70
|
+
});
|
|
71
|
+
if (openIt) {
|
|
72
|
+
await this.openTemplateInEditor(email.template);
|
|
73
|
+
}
|
|
64
74
|
}
|
|
65
75
|
}
|
|
66
76
|
}
|
|
77
|
+
async openTemplateInEditor(template) {
|
|
78
|
+
const templatePath = join(process.cwd(), template);
|
|
79
|
+
this.log(`\n Opening ${template}...\n`);
|
|
80
|
+
const editor = process.env.VISUAL || process.env.EDITOR;
|
|
81
|
+
if (editor) {
|
|
82
|
+
const [cmd, ...editorArgs] = editor.trim().split(/\s+/);
|
|
83
|
+
const launched = await new Promise((resolve) => {
|
|
84
|
+
const child = spawn(cmd, [...editorArgs, templatePath], {
|
|
85
|
+
stdio: 'inherit',
|
|
86
|
+
});
|
|
87
|
+
child.on('error', () => resolve(false));
|
|
88
|
+
child.on('close', () => resolve(true));
|
|
89
|
+
});
|
|
90
|
+
if (launched)
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (await this.trySpawnEditor('code', templatePath))
|
|
94
|
+
return;
|
|
95
|
+
try {
|
|
96
|
+
await open(templatePath);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
this.log(` ${chalk.dim(`Could not open editor. Open the file manually: ${templatePath}`)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
trySpawnEditor(editor, filePath) {
|
|
103
|
+
const [cmd, args] = process.platform === 'win32'
|
|
104
|
+
? ['cmd.exe', ['/c', editor, filePath]]
|
|
105
|
+
: [editor, [filePath]];
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
const child = spawn(cmd, [...args], { stdio: 'ignore' });
|
|
108
|
+
child.on('error', () => {
|
|
109
|
+
resolve(false);
|
|
110
|
+
});
|
|
111
|
+
child.on('close', (code) => {
|
|
112
|
+
resolve(code === 0);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
67
116
|
}
|
|
@@ -139,7 +139,7 @@ export default class Init extends BaseCommand {
|
|
|
139
139
|
project: {
|
|
140
140
|
brandColor: analysisPayload.brand?.color || DEFAULT_BRAND_COLOR,
|
|
141
141
|
description: analysisPayload.description,
|
|
142
|
-
emailStyle: '
|
|
142
|
+
emailStyle: 'plain',
|
|
143
143
|
fromEmail: '',
|
|
144
144
|
fromName: `Team ${analysisPayload.productName}`,
|
|
145
145
|
logoUrl: analysisPayload.brand?.logoUrl || '',
|
|
@@ -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 = [
|
|
@@ -47,10 +48,10 @@ export default class Login extends BaseCommand {
|
|
|
47
48
|
this.log(`\n Get your free API key at: ${chalk.cyan(LOGIN_URL)}\n`);
|
|
48
49
|
try {
|
|
49
50
|
await open(LOGIN_URL);
|
|
50
|
-
this.log(
|
|
51
|
+
this.log(` ${INFO.BROWSER_OPENING}\n`);
|
|
51
52
|
}
|
|
52
53
|
catch {
|
|
53
|
-
this.log(` ${
|
|
54
|
+
this.log(` ${INFO.BROWSER_OPEN_FAILED}\n`);
|
|
54
55
|
}
|
|
55
56
|
apiKey = await input({
|
|
56
57
|
message: 'Paste your API key:',
|
|
@@ -23,8 +23,12 @@ export default class Preview extends BaseCommand {
|
|
|
23
23
|
*/
|
|
24
24
|
private sendTestEmail;
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
27
|
-
|
|
26
|
+
* Probes ports starting at startPort and returns the first one not in use.
|
|
27
|
+
*/
|
|
28
|
+
private findAvailablePort;
|
|
29
|
+
/**
|
|
30
|
+
* Starts a local HTTP server to serve the rendered email template,
|
|
31
|
+
* then opens the user's default browser to view it.
|
|
28
32
|
*/
|
|
29
33
|
private startPreviewServer;
|
|
30
34
|
}
|
|
@@ -4,7 +4,8 @@ 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 {
|
|
7
|
+
import { INFO } from '../../lib/messages.js';
|
|
8
|
+
import { loadTemplate, getEmailStyle, getTemplateFilename, } from '../../lib/yaml-config.js';
|
|
8
9
|
/* eslint-disable camelcase */
|
|
9
10
|
const SAMPLE_DATA = Object.freeze({
|
|
10
11
|
app_url: 'https://yourapp.com',
|
|
@@ -84,7 +85,8 @@ export default class Preview extends BaseCommand {
|
|
|
84
85
|
app_url: yamlConfig.project?.url || 'https://yourapp.com', // eslint-disable-line camelcase
|
|
85
86
|
product_name: yamlConfig.project?.name || 'YourApp', // eslint-disable-line camelcase
|
|
86
87
|
};
|
|
87
|
-
const
|
|
88
|
+
const effectiveStyle = getEmailStyle(email.style, yamlConfig.project?.emailStyle);
|
|
89
|
+
const templateHtml = await loadTemplate(getTemplateFilename(email.id, email.style, yamlConfig.project?.emailStyle));
|
|
88
90
|
if (flags.send) {
|
|
89
91
|
const rendered = templateHtml
|
|
90
92
|
? renderTemplate(templateHtml, sampleData)
|
|
@@ -100,7 +102,10 @@ export default class Preview extends BaseCommand {
|
|
|
100
102
|
await this.renderText(email, templateHtml, sampleData, flags.json);
|
|
101
103
|
return;
|
|
102
104
|
}
|
|
103
|
-
await this.startPreviewServer(email, templateHtml, sampleData,
|
|
105
|
+
await this.startPreviewServer(email, templateHtml, sampleData, {
|
|
106
|
+
effectiveStyle,
|
|
107
|
+
jsonOutput: flags.json,
|
|
108
|
+
});
|
|
104
109
|
}
|
|
105
110
|
/**
|
|
106
111
|
* Renders a plain text version of the email to stdout. Used by AI agents
|
|
@@ -154,10 +159,28 @@ export default class Preview extends BaseCommand {
|
|
|
154
159
|
this.log('');
|
|
155
160
|
}
|
|
156
161
|
/**
|
|
157
|
-
*
|
|
158
|
-
* template, then opens the user's default browser to view it.
|
|
162
|
+
* Probes ports starting at startPort and returns the first one not in use.
|
|
159
163
|
*/
|
|
160
|
-
async
|
|
164
|
+
async findAvailablePort(startPort, endPort = startPort + 10) {
|
|
165
|
+
if (startPort > endPort) {
|
|
166
|
+
throw new Error(`No available port found starting from port ${endPort - 10}`);
|
|
167
|
+
}
|
|
168
|
+
const available = await new Promise((resolve) => {
|
|
169
|
+
const probe = createServer();
|
|
170
|
+
probe.once('error', () => resolve(false));
|
|
171
|
+
probe.once('listening', () => probe.close(() => resolve(true)));
|
|
172
|
+
probe.listen(startPort);
|
|
173
|
+
});
|
|
174
|
+
return available
|
|
175
|
+
? startPort
|
|
176
|
+
: this.findAvailablePort(startPort + 1, endPort);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Starts a local HTTP server to serve the rendered email template,
|
|
180
|
+
* then opens the user's default browser to view it.
|
|
181
|
+
*/
|
|
182
|
+
async startPreviewServer(email, templateHtml, sampleData, opts) {
|
|
183
|
+
const { effectiveStyle, jsonOutput } = opts;
|
|
161
184
|
const rendered = templateHtml
|
|
162
185
|
? renderTemplate(templateHtml, sampleData)
|
|
163
186
|
: '<p>No template found.</p>';
|
|
@@ -183,7 +206,7 @@ export default class Preview extends BaseCommand {
|
|
|
183
206
|
<body>
|
|
184
207
|
<div class="preview-bar">
|
|
185
208
|
<h3>Mailmodo Preview — ${email.id}</h3>
|
|
186
|
-
<span>Style: ${
|
|
209
|
+
<span>Style: ${effectiveStyle} · Press Ctrl+C in terminal to stop</span>
|
|
187
210
|
</div>
|
|
188
211
|
<div class="email-frame">
|
|
189
212
|
<div class="email-header">
|
|
@@ -194,11 +217,15 @@ export default class Preview extends BaseCommand {
|
|
|
194
217
|
</div>
|
|
195
218
|
</body>
|
|
196
219
|
</html>`;
|
|
220
|
+
const port = await this.findAvailablePort(PREVIEW_PORT);
|
|
221
|
+
if (!jsonOutput && port !== PREVIEW_PORT) {
|
|
222
|
+
this.log(`\n ${chalk.yellow('!')} Port ${PREVIEW_PORT} is already in use. Opening preview on port ${chalk.cyan(String(port))}.`);
|
|
223
|
+
}
|
|
197
224
|
if (jsonOutput) {
|
|
198
225
|
this.log(JSON.stringify({
|
|
199
226
|
id: email.id,
|
|
200
|
-
style:
|
|
201
|
-
url: `http://localhost:${
|
|
227
|
+
style: effectiveStyle,
|
|
228
|
+
url: `http://localhost:${port}`,
|
|
202
229
|
}, null, 2));
|
|
203
230
|
}
|
|
204
231
|
const server = createServer((_req, res) => {
|
|
@@ -206,13 +233,13 @@ export default class Preview extends BaseCommand {
|
|
|
206
233
|
res.end(wrapperHtml);
|
|
207
234
|
});
|
|
208
235
|
await new Promise((resolve) => {
|
|
209
|
-
server.listen(
|
|
236
|
+
server.listen(port, () => resolve());
|
|
210
237
|
});
|
|
211
|
-
const url = `http://localhost:${
|
|
238
|
+
const url = `http://localhost:${port}`;
|
|
212
239
|
if (!jsonOutput) {
|
|
213
|
-
this.log(`\n Style: ${chalk.cyan(
|
|
240
|
+
this.log(`\n Style: ${chalk.cyan(effectiveStyle)}`);
|
|
214
241
|
this.log(` Preview server at ${chalk.cyan(url)}`);
|
|
215
|
-
this.log(`
|
|
242
|
+
this.log(` ${INFO.BROWSER_OPENING}\n`);
|
|
216
243
|
this.log(` ${chalk.dim('Press Ctrl+C to stop the preview server.')}\n`);
|
|
217
244
|
}
|
|
218
245
|
try {
|
|
@@ -220,7 +247,7 @@ export default class Preview extends BaseCommand {
|
|
|
220
247
|
}
|
|
221
248
|
catch {
|
|
222
249
|
if (!jsonOutput) {
|
|
223
|
-
this.log(` ${
|
|
250
|
+
this.log(` ${INFO.BROWSER_OPEN_FAILED}`);
|
|
224
251
|
}
|
|
225
252
|
}
|
|
226
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,
|
|
@@ -69,5 +69,31 @@ export declare abstract class BaseCommand extends Command {
|
|
|
69
69
|
* @param {{ status: number; debug?: ApiRequestDebugInfo }} response - Failed API response.
|
|
70
70
|
* @returns {string} Message plus indented Request details for troubleshooting.
|
|
71
71
|
*/
|
|
72
|
+
protected collectDomainSetupInputs(yamlConfig: MailmodoYaml, skipPrompts: boolean): Promise<{
|
|
73
|
+
address: string;
|
|
74
|
+
domain: string;
|
|
75
|
+
fromEmail: string;
|
|
76
|
+
fromName: string;
|
|
77
|
+
replyTo: string;
|
|
78
|
+
}>;
|
|
79
|
+
protected registerDomain(yamlConfig: MailmodoYaml, inputs: {
|
|
80
|
+
address: string;
|
|
81
|
+
domain: string;
|
|
82
|
+
fromEmail: string;
|
|
83
|
+
fromName?: string;
|
|
84
|
+
replyTo?: string;
|
|
85
|
+
}, json: boolean): Promise<{
|
|
86
|
+
dnsGuideUrl?: string;
|
|
87
|
+
dnsRecords: Array<{
|
|
88
|
+
host: string;
|
|
89
|
+
type: string;
|
|
90
|
+
value: string;
|
|
91
|
+
}>;
|
|
92
|
+
}>;
|
|
93
|
+
protected logDnsRecords(records: Array<{
|
|
94
|
+
host: string;
|
|
95
|
+
type: string;
|
|
96
|
+
value: string;
|
|
97
|
+
}>, guideUrl: string | undefined, json: boolean): void;
|
|
72
98
|
private formatApiFailure;
|
|
73
99
|
}
|
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,12 @@ 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));
|
|
105
108
|
}
|
|
106
109
|
/**
|
|
107
110
|
* Builds the terminal error string for a failed API call, appending request
|
|
@@ -111,6 +114,86 @@ export class BaseCommand extends Command {
|
|
|
111
114
|
* @param {{ status: number; debug?: ApiRequestDebugInfo }} response - Failed API response.
|
|
112
115
|
* @returns {string} Message plus indented Request details for troubleshooting.
|
|
113
116
|
*/
|
|
117
|
+
async collectDomainSetupInputs(yamlConfig, skipPrompts) {
|
|
118
|
+
if (skipPrompts) {
|
|
119
|
+
const domain = yamlConfig.project?.domain || '';
|
|
120
|
+
if (!domain) {
|
|
121
|
+
this.error('Domain is required. Set it in mailmodo.yaml or use interactive mode.');
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
address: yamlConfig.project?.address || '',
|
|
125
|
+
domain,
|
|
126
|
+
fromEmail: yamlConfig.project?.fromEmail || '',
|
|
127
|
+
fromName: yamlConfig.project?.fromName || '',
|
|
128
|
+
replyTo: yamlConfig.project?.replyTo || '',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const domain = await input({
|
|
132
|
+
default: yamlConfig.project?.domain,
|
|
133
|
+
message: PROMPTS.DOMAIN,
|
|
134
|
+
validate: (v) => (v?.trim() ? true : VALIDATION.DOMAIN_REQUIRED),
|
|
135
|
+
});
|
|
136
|
+
const fromEmail = await input({
|
|
137
|
+
default: yamlConfig.project?.fromEmail,
|
|
138
|
+
message: PROMPTS.SENDER_EMAIL,
|
|
139
|
+
validate: (v) => (v?.includes('@') ? true : VALIDATION.EMAIL_INVALID),
|
|
140
|
+
});
|
|
141
|
+
const fromName = await input({
|
|
142
|
+
default: yamlConfig.project?.fromName || '',
|
|
143
|
+
message: PROMPTS.FROM_NAME,
|
|
144
|
+
});
|
|
145
|
+
const replyTo = await input({
|
|
146
|
+
default: yamlConfig.project?.replyTo || '',
|
|
147
|
+
message: PROMPTS.REPLY_TO,
|
|
148
|
+
});
|
|
149
|
+
const address = await input({
|
|
150
|
+
default: yamlConfig.project?.address,
|
|
151
|
+
message: PROMPTS.BUSINESS_ADDRESS,
|
|
152
|
+
validate: (v) => (v?.trim() ? true : VALIDATION.ADDRESS_REQUIRED),
|
|
153
|
+
});
|
|
154
|
+
return { address, domain, fromEmail, fromName, replyTo };
|
|
155
|
+
}
|
|
156
|
+
async registerDomain(yamlConfig, inputs, json) {
|
|
157
|
+
const apiPayload = {
|
|
158
|
+
address: inputs.address,
|
|
159
|
+
domain: inputs.domain,
|
|
160
|
+
fromEmail: inputs.fromEmail,
|
|
161
|
+
};
|
|
162
|
+
if (inputs.fromName)
|
|
163
|
+
apiPayload.fromName = inputs.fromName;
|
|
164
|
+
if (inputs.replyTo)
|
|
165
|
+
apiPayload.replyTo = inputs.replyTo;
|
|
166
|
+
const response = await this.withApiSpinner({ json, text: ' Configuring domain...' }, () => this.apiClient.post(API_ENDPOINTS.DOMAIN, apiPayload));
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
this.handleApiError(response);
|
|
169
|
+
}
|
|
170
|
+
yamlConfig.project.domain = inputs.domain;
|
|
171
|
+
yamlConfig.project.fromEmail = inputs.fromEmail;
|
|
172
|
+
yamlConfig.project.address = inputs.address;
|
|
173
|
+
if (inputs.fromName)
|
|
174
|
+
yamlConfig.project.fromName = inputs.fromName;
|
|
175
|
+
if (inputs.replyTo)
|
|
176
|
+
yamlConfig.project.replyTo = inputs.replyTo;
|
|
177
|
+
await saveYaml(yamlConfig);
|
|
178
|
+
return {
|
|
179
|
+
dnsGuideUrl: response.data?.dnsGuideUrl,
|
|
180
|
+
dnsRecords: response.data?.dnsRecords || [],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
logDnsRecords(records, guideUrl, json) {
|
|
184
|
+
if (json)
|
|
185
|
+
return;
|
|
186
|
+
this.log(`\n Add these ${records.length} DNS records to your domain provider:\n`);
|
|
187
|
+
for (const [i, record] of records.entries()) {
|
|
188
|
+
this.log(` ${chalk.bold(`RECORD ${i + 1} — ${recordLabel(i)}`)}`);
|
|
189
|
+
this.log(` Type: ${record.type}`);
|
|
190
|
+
this.log(` Host: ${record.host}`);
|
|
191
|
+
this.log(` Value: ${record.value}\n`);
|
|
192
|
+
}
|
|
193
|
+
this.log(` ${INFO.DNS_PROPAGATION}`);
|
|
194
|
+
if (guideUrl)
|
|
195
|
+
this.log(` Full guide: ${chalk.cyan(guideUrl)}\n`);
|
|
196
|
+
}
|
|
114
197
|
formatApiFailure(message, response) {
|
|
115
198
|
const { debug, status } = response;
|
|
116
199
|
if (!debug) {
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -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
|
+
}
|
|
@@ -68,3 +68,5 @@ export declare function saveTemplate(filename: string, html: string, cwd?: strin
|
|
|
68
68
|
* or null if the file doesn't exist or can't be read.
|
|
69
69
|
*/
|
|
70
70
|
export declare function loadTemplate(filename: string, cwd?: string): Promise<null | string>;
|
|
71
|
+
export declare function getEmailStyle(emailStyle?: 'branded' | 'plain', projectStyle?: 'branded' | 'plain'): 'branded' | 'plain';
|
|
72
|
+
export declare function getTemplateFilename(emailId: string, emailStyle?: 'branded' | 'plain', projectStyle?: 'branded' | 'plain'): string;
|
package/dist/lib/yaml-config.js
CHANGED
|
@@ -72,3 +72,10 @@ export async function loadTemplate(filename, cwd) {
|
|
|
72
72
|
return null;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
export function getEmailStyle(emailStyle, projectStyle) {
|
|
76
|
+
return emailStyle ?? projectStyle ?? 'branded';
|
|
77
|
+
}
|
|
78
|
+
export function getTemplateFilename(emailId, emailStyle, projectStyle) {
|
|
79
|
+
const style = getEmailStyle(emailStyle, projectStyle);
|
|
80
|
+
return style === 'plain' ? `${emailId}_plain.html` : `${emailId}.html`;
|
|
81
|
+
}
|