@mailmodo/cli 0.0.12-beta.pr15.19 → 0.0.12
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/settings/index.d.ts +1 -18
- package/dist/commands/settings/index.js +40 -159
- package/oclif.manifest.json +34 -34
- package/package.json +1 -1
|
@@ -8,30 +8,13 @@ export default class Settings extends BaseCommand {
|
|
|
8
8
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
9
|
};
|
|
10
10
|
run(): Promise<void>;
|
|
11
|
-
/**
|
|
12
|
-
* Prompts the user to pick a setting key to edit and dispatches
|
|
13
|
-
* to the appropriate handler for that key.
|
|
14
|
-
*/
|
|
15
|
-
private promptEditSetting;
|
|
16
|
-
/**
|
|
17
|
-
* Fetches the domain verification status from the API.
|
|
18
|
-
* Returns true/false for verified/unverified, or null if unavailable.
|
|
19
|
-
*/
|
|
20
|
-
private fetchDomainVerified;
|
|
21
|
-
/**
|
|
22
|
-
* Handles domain change: collects the new domain, sender email, and
|
|
23
|
-
* business address, calls the API to register them, displays the required
|
|
24
|
-
* DNS records, and saves the updated config. Emails won't send until the
|
|
25
|
-
* domain is re-verified.
|
|
26
|
-
*/
|
|
27
|
-
private handleDomainChange;
|
|
28
|
-
private recordLabel;
|
|
29
11
|
/**
|
|
30
12
|
* Handles the logo file upload flow: validates the local file exists,
|
|
31
13
|
* reads it, uploads to Mailmodo CDN via API, and updates both logoFile
|
|
32
14
|
* and logoUrl in the project config.
|
|
33
15
|
*
|
|
34
16
|
* @param {import('../../lib/yaml-config.js').MailmodoYaml} yamlConfig - The full YAML config to update and save.
|
|
17
|
+
* @param {boolean} jsonOutput - When true, spinner uses stderr so stdout stays clean.
|
|
35
18
|
*/
|
|
36
19
|
private handleLogoUpload;
|
|
37
20
|
}
|
|
@@ -5,7 +5,7 @@ import { existsSync } from 'node:fs';
|
|
|
5
5
|
import { readFile } from 'node:fs/promises';
|
|
6
6
|
import { resolve } from 'node:path';
|
|
7
7
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
8
|
-
import { API_ENDPOINTS
|
|
8
|
+
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
9
9
|
import { saveYaml } from '../../lib/yaml-config.js';
|
|
10
10
|
const SETTINGS_GROUPS = Object.freeze({
|
|
11
11
|
billing: ['monthly_cap'],
|
|
@@ -14,11 +14,6 @@ const SETTINGS_GROUPS = Object.freeze({
|
|
|
14
14
|
identity: ['from_name', 'from_email', 'reply_to'],
|
|
15
15
|
integrations: ['webhook_url'],
|
|
16
16
|
});
|
|
17
|
-
const SETUP_HINTS = {
|
|
18
|
-
address: "'mailmodo domain'",
|
|
19
|
-
domain: "'mailmodo domain'",
|
|
20
|
-
monthlyCap: "'mailmodo billing --cap <n>'",
|
|
21
|
-
};
|
|
22
17
|
/**
|
|
23
18
|
* Converts a user-facing snake_case YAML setting key to its
|
|
24
19
|
* corresponding camelCase TypeScript property name.
|
|
@@ -70,180 +65,66 @@ export default class Settings extends BaseCommand {
|
|
|
70
65
|
this.log(JSON.stringify({ settings: project }, null, 2));
|
|
71
66
|
return;
|
|
72
67
|
}
|
|
73
|
-
const domainVerified = await this.fetchDomainVerified(project.domain);
|
|
74
68
|
this.log(`\n Current settings for ${chalk.bold(project.name || 'project')}:\n`);
|
|
75
69
|
for (const [group, keys] of Object.entries(SETTINGS_GROUPS)) {
|
|
76
|
-
const availableKeys = keys.filter((key) => settingKeyToProp(key) in project);
|
|
77
|
-
if (availableKeys.length === 0) {
|
|
78
|
-
const hint = SETUP_HINTS[settingKeyToProp(keys[0])];
|
|
79
|
-
if (hint) {
|
|
80
|
-
this.log(` ${chalk.bold(group.charAt(0).toUpperCase() + group.slice(1))}`);
|
|
81
|
-
this.log(` ${'─'.repeat(49)}`);
|
|
82
|
-
this.log(` ${chalk.dim(`Run ${hint} to configure.`)}`);
|
|
83
|
-
this.log('');
|
|
84
|
-
}
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
70
|
this.log(` ${chalk.bold(group.charAt(0).toUpperCase() + group.slice(1))}`);
|
|
88
71
|
this.log(` ${'─'.repeat(49)}`);
|
|
89
|
-
for (const key of
|
|
72
|
+
for (const key of keys) {
|
|
90
73
|
const propKey = settingKeyToProp(key);
|
|
91
74
|
const value = project[propKey];
|
|
92
|
-
|
|
93
|
-
if (key === 'domain' && value && domainVerified === true) {
|
|
94
|
-
displayValue += ` ${chalk.green('✓ verified')}`;
|
|
95
|
-
}
|
|
96
|
-
else if (key === 'domain' && value && domainVerified === false) {
|
|
97
|
-
displayValue += ` ${chalk.red('✗ not verified')}`;
|
|
98
|
-
}
|
|
75
|
+
const displayValue = value ? String(value) : chalk.dim('(not set)');
|
|
99
76
|
this.log(` ${key.padEnd(16)} ${displayValue}`);
|
|
100
77
|
}
|
|
101
|
-
const missingKeys = keys.filter((key) => !(settingKeyToProp(key) in project));
|
|
102
|
-
for (const key of missingKeys) {
|
|
103
|
-
const hint = SETUP_HINTS[settingKeyToProp(key)];
|
|
104
|
-
if (hint) {
|
|
105
|
-
this.log(` ${key.padEnd(16)} ${chalk.dim(`(run ${hint} to set up)`)}`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
78
|
this.log('');
|
|
109
79
|
}
|
|
110
80
|
if (!flags.yes) {
|
|
111
|
-
await
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Prompts the user to pick a setting key to edit and dispatches
|
|
116
|
-
* to the appropriate handler for that key.
|
|
117
|
-
*/
|
|
118
|
-
async promptEditSetting(yamlConfig) {
|
|
119
|
-
const { project } = yamlConfig;
|
|
120
|
-
const editKey = await input({
|
|
121
|
-
default: 'n',
|
|
122
|
-
message: "Edit a setting? (key or 'n'):",
|
|
123
|
-
});
|
|
124
|
-
if (editKey === 'n')
|
|
125
|
-
return;
|
|
126
|
-
const editPropKey = settingKeyToProp(editKey);
|
|
127
|
-
if (!(editPropKey in project)) {
|
|
128
|
-
const hint = SETUP_HINTS[editPropKey];
|
|
129
|
-
if (hint) {
|
|
130
|
-
this.log(`\n ${editKey} is not configured yet. Run ${chalk.cyan(hint)} to set it up.\n`);
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
this.log(`\n Unknown setting: ${editKey}\n`);
|
|
134
|
-
}
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
if (editKey === 'logo_file') {
|
|
138
|
-
await this.handleLogoUpload(yamlConfig);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (editKey === 'domain') {
|
|
142
|
-
await this.handleDomainChange(yamlConfig);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
if (editKey === 'email_style') {
|
|
146
|
-
const style = await select({
|
|
147
|
-
choices: [
|
|
148
|
-
{ name: 'plain', value: 'plain' },
|
|
149
|
-
{ name: 'branded', value: 'branded' },
|
|
150
|
-
],
|
|
151
|
-
message: 'Email style:',
|
|
81
|
+
const editKey = await input({
|
|
82
|
+
default: 'n',
|
|
83
|
+
message: "Edit a setting? (key or 'n'):",
|
|
152
84
|
});
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
85
|
+
if (editKey !== 'n') {
|
|
86
|
+
const editPropKey = settingKeyToProp(editKey);
|
|
87
|
+
if (!(editPropKey in project)) {
|
|
88
|
+
this.log(`\n Unknown setting: ${editKey}\n`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (editKey === 'logo_file') {
|
|
92
|
+
await this.handleLogoUpload(yamlConfig, flags.json);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (editKey === 'email_style') {
|
|
96
|
+
const style = await select({
|
|
97
|
+
choices: [
|
|
98
|
+
{ name: 'plain', value: 'plain' },
|
|
99
|
+
{ name: 'branded', value: 'branded' },
|
|
100
|
+
],
|
|
101
|
+
message: 'Email style:',
|
|
102
|
+
});
|
|
103
|
+
project.emailStyle = style;
|
|
104
|
+
await saveYaml(yamlConfig);
|
|
105
|
+
this.log(`\n ${chalk.green('✓')} email_style updated to ${chalk.cyan(style)}`);
|
|
106
|
+
this.log(` Run ${chalk.cyan("'mailmodo deploy'")} to apply.\n`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const newValue = await input({
|
|
110
|
+
message: `New value for ${editKey}:`,
|
|
111
|
+
});
|
|
112
|
+
project[editPropKey] =
|
|
113
|
+
editPropKey === 'monthlyCap' ? Number(newValue) : newValue;
|
|
114
|
+
await saveYaml(yamlConfig);
|
|
115
|
+
this.log(`\n ${chalk.green('✓')} Updated. Run ${chalk.cyan("'mailmodo deploy'")} to apply.\n`);
|
|
180
116
|
}
|
|
181
|
-
return response.data?.verified === true;
|
|
182
|
-
}
|
|
183
|
-
catch {
|
|
184
|
-
this.log(` ${chalk.dim('Could not reach API for domain status. Skipping verification check.')}`);
|
|
185
|
-
return null;
|
|
186
117
|
}
|
|
187
118
|
}
|
|
188
|
-
/**
|
|
189
|
-
* Handles domain change: collects the new domain, sender email, and
|
|
190
|
-
* business address, calls the API to register them, displays the required
|
|
191
|
-
* DNS records, and saves the updated config. Emails won't send until the
|
|
192
|
-
* domain is re-verified.
|
|
193
|
-
*/
|
|
194
|
-
async handleDomainChange(yamlConfig) {
|
|
195
|
-
const newDomain = await input({
|
|
196
|
-
message: 'New domain:',
|
|
197
|
-
validate: (v) => (v?.trim() ? true : 'Domain is required'),
|
|
198
|
-
});
|
|
199
|
-
const newFromEmail = await input({
|
|
200
|
-
default: yamlConfig.project.fromEmail || '',
|
|
201
|
-
message: 'Sender email (from address):',
|
|
202
|
-
validate: (v) => v?.includes('@') ? true : 'Please enter a valid email',
|
|
203
|
-
});
|
|
204
|
-
const newAddress = await input({
|
|
205
|
-
default: yamlConfig.project.address || '',
|
|
206
|
-
message: 'Business address (required by law):',
|
|
207
|
-
validate: (v) => (v?.trim() ? true : 'Address is required'),
|
|
208
|
-
});
|
|
209
|
-
await this.ensureAuth();
|
|
210
|
-
const response = await this.apiClient.post(API_ENDPOINTS.DOMAIN, {
|
|
211
|
-
address: newAddress,
|
|
212
|
-
domain: newDomain,
|
|
213
|
-
fromEmail: newFromEmail,
|
|
214
|
-
});
|
|
215
|
-
if (!response.ok) {
|
|
216
|
-
this.handleApiError(response);
|
|
217
|
-
}
|
|
218
|
-
const records = response.data?.dnsRecords || [];
|
|
219
|
-
yamlConfig.project.domain = newDomain;
|
|
220
|
-
yamlConfig.project.fromEmail = newFromEmail;
|
|
221
|
-
yamlConfig.project.address = newAddress;
|
|
222
|
-
await saveYaml(yamlConfig);
|
|
223
|
-
this.log(`\n Domain, sender email, and business address updated. You will need to re-verify.`);
|
|
224
|
-
this.log(` New DNS records:\n`);
|
|
225
|
-
for (const [i, record] of records.entries()) {
|
|
226
|
-
this.log(` ${chalk.bold(`RECORD ${i + 1} — ${this.recordLabel(i)}`)}`);
|
|
227
|
-
this.log(` Type: ${record.type}`);
|
|
228
|
-
this.log(` Host: ${record.host}`);
|
|
229
|
-
this.log(` Value: ${record.value}\n`);
|
|
230
|
-
}
|
|
231
|
-
this.log(` Run ${chalk.cyan("'mailmodo domain --verify'")} once records are added.`);
|
|
232
|
-
this.log(` Emails will not send until the new domain is verified.`);
|
|
233
|
-
this.log(` Help: ${chalk.cyan(DNS_GUIDE_URL)}\n`);
|
|
234
|
-
}
|
|
235
|
-
recordLabel(index) {
|
|
236
|
-
const labels = ['SPF', 'DKIM', 'DMARC'];
|
|
237
|
-
return labels[index] || `Record ${index + 1}`;
|
|
238
|
-
}
|
|
239
119
|
/**
|
|
240
120
|
* Handles the logo file upload flow: validates the local file exists,
|
|
241
121
|
* reads it, uploads to Mailmodo CDN via API, and updates both logoFile
|
|
242
122
|
* and logoUrl in the project config.
|
|
243
123
|
*
|
|
244
124
|
* @param {import('../../lib/yaml-config.js').MailmodoYaml} yamlConfig - The full YAML config to update and save.
|
|
125
|
+
* @param {boolean} jsonOutput - When true, spinner uses stderr so stdout stays clean.
|
|
245
126
|
*/
|
|
246
|
-
async handleLogoUpload(yamlConfig) {
|
|
127
|
+
async handleLogoUpload(yamlConfig, jsonOutput) {
|
|
247
128
|
const logoPath = await input({ message: 'Path to logo file:' });
|
|
248
129
|
const resolvedPath = resolve(logoPath);
|
|
249
130
|
if (!existsSync(resolvedPath)) {
|
|
@@ -254,7 +135,7 @@ export default class Settings extends BaseCommand {
|
|
|
254
135
|
const fileBuffer = await readFile(resolvedPath);
|
|
255
136
|
const formData = new FormData();
|
|
256
137
|
formData.append('logo', new Blob([new Uint8Array(fileBuffer)]), logoPath.split(/[/\\]/).pop() || 'logo.png');
|
|
257
|
-
const response = await this.apiClient.postFormData(API_ENDPOINTS.ASSETS_LOGO, formData);
|
|
138
|
+
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Uploading logo...' }, () => this.apiClient.postFormData(API_ENDPOINTS.ASSETS_LOGO, formData));
|
|
258
139
|
if (!response.ok) {
|
|
259
140
|
this.handleApiError(response);
|
|
260
141
|
}
|
package/oclif.manifest.json
CHANGED
|
@@ -473,19 +473,14 @@
|
|
|
473
473
|
"index.js"
|
|
474
474
|
]
|
|
475
475
|
},
|
|
476
|
-
"
|
|
476
|
+
"settings": {
|
|
477
477
|
"aliases": [],
|
|
478
|
-
"args": {
|
|
479
|
-
|
|
480
|
-
"description": "Email ID to preview",
|
|
481
|
-
"name": "id"
|
|
482
|
-
}
|
|
483
|
-
},
|
|
484
|
-
"description": "Preview an email in browser, as text, or send a test",
|
|
478
|
+
"args": {},
|
|
479
|
+
"description": "View and update project settings",
|
|
485
480
|
"examples": [
|
|
486
|
-
"<%= config.bin %>
|
|
487
|
-
"<%= config.bin %>
|
|
488
|
-
"<%= config.bin %>
|
|
481
|
+
"<%= config.bin %> settings",
|
|
482
|
+
"<%= config.bin %> settings --set brand_color=#0F3460",
|
|
483
|
+
"<%= config.bin %> settings --json"
|
|
489
484
|
],
|
|
490
485
|
"flags": {
|
|
491
486
|
"json": {
|
|
@@ -501,23 +496,17 @@
|
|
|
501
496
|
"allowNo": false,
|
|
502
497
|
"type": "boolean"
|
|
503
498
|
},
|
|
504
|
-
"
|
|
505
|
-
"description": "
|
|
506
|
-
"name": "
|
|
499
|
+
"set": {
|
|
500
|
+
"description": "Set a setting (format: key=value)",
|
|
501
|
+
"name": "set",
|
|
507
502
|
"hasDynamicHelp": false,
|
|
508
503
|
"multiple": false,
|
|
509
504
|
"type": "option"
|
|
510
|
-
},
|
|
511
|
-
"text": {
|
|
512
|
-
"description": "Output plain text version (for AI agents)",
|
|
513
|
-
"name": "text",
|
|
514
|
-
"allowNo": false,
|
|
515
|
-
"type": "boolean"
|
|
516
505
|
}
|
|
517
506
|
},
|
|
518
507
|
"hasDynamicHelp": false,
|
|
519
508
|
"hiddenAliases": [],
|
|
520
|
-
"id": "
|
|
509
|
+
"id": "settings",
|
|
521
510
|
"pluginAlias": "@mailmodo/cli",
|
|
522
511
|
"pluginName": "@mailmodo/cli",
|
|
523
512
|
"pluginType": "core",
|
|
@@ -527,18 +516,23 @@
|
|
|
527
516
|
"relativePath": [
|
|
528
517
|
"dist",
|
|
529
518
|
"commands",
|
|
530
|
-
"
|
|
519
|
+
"settings",
|
|
531
520
|
"index.js"
|
|
532
521
|
]
|
|
533
522
|
},
|
|
534
|
-
"
|
|
523
|
+
"preview": {
|
|
535
524
|
"aliases": [],
|
|
536
|
-
"args": {
|
|
537
|
-
|
|
525
|
+
"args": {
|
|
526
|
+
"id": {
|
|
527
|
+
"description": "Email ID to preview",
|
|
528
|
+
"name": "id"
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
"description": "Preview an email in browser, as text, or send a test",
|
|
538
532
|
"examples": [
|
|
539
|
-
"<%= config.bin %>
|
|
540
|
-
"<%= config.bin %>
|
|
541
|
-
"<%= config.bin %>
|
|
533
|
+
"<%= config.bin %> preview welcome",
|
|
534
|
+
"<%= config.bin %> preview welcome --text",
|
|
535
|
+
"<%= config.bin %> preview welcome --send me@example.com"
|
|
542
536
|
],
|
|
543
537
|
"flags": {
|
|
544
538
|
"json": {
|
|
@@ -554,17 +548,23 @@
|
|
|
554
548
|
"allowNo": false,
|
|
555
549
|
"type": "boolean"
|
|
556
550
|
},
|
|
557
|
-
"
|
|
558
|
-
"description": "
|
|
559
|
-
"name": "
|
|
551
|
+
"send": {
|
|
552
|
+
"description": "Send test email to this address",
|
|
553
|
+
"name": "send",
|
|
560
554
|
"hasDynamicHelp": false,
|
|
561
555
|
"multiple": false,
|
|
562
556
|
"type": "option"
|
|
557
|
+
},
|
|
558
|
+
"text": {
|
|
559
|
+
"description": "Output plain text version (for AI agents)",
|
|
560
|
+
"name": "text",
|
|
561
|
+
"allowNo": false,
|
|
562
|
+
"type": "boolean"
|
|
563
563
|
}
|
|
564
564
|
},
|
|
565
565
|
"hasDynamicHelp": false,
|
|
566
566
|
"hiddenAliases": [],
|
|
567
|
-
"id": "
|
|
567
|
+
"id": "preview",
|
|
568
568
|
"pluginAlias": "@mailmodo/cli",
|
|
569
569
|
"pluginName": "@mailmodo/cli",
|
|
570
570
|
"pluginType": "core",
|
|
@@ -574,7 +574,7 @@
|
|
|
574
574
|
"relativePath": [
|
|
575
575
|
"dist",
|
|
576
576
|
"commands",
|
|
577
|
-
"
|
|
577
|
+
"preview",
|
|
578
578
|
"index.js"
|
|
579
579
|
]
|
|
580
580
|
},
|
|
@@ -618,5 +618,5 @@
|
|
|
618
618
|
]
|
|
619
619
|
}
|
|
620
620
|
},
|
|
621
|
-
"version": "0.0.12
|
|
621
|
+
"version": "0.0.12"
|
|
622
622
|
}
|