@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.
@@ -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, DNS_GUIDE_URL } from '../../lib/constants.js';
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 availableKeys) {
72
+ for (const key of keys) {
90
73
  const propKey = settingKeyToProp(key);
91
74
  const value = project[propKey];
92
- let displayValue = value ? String(value) : chalk.dim('(not set)');
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 this.promptEditSetting(yamlConfig);
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
- project.emailStyle = style;
154
- await saveYaml(yamlConfig);
155
- this.log(`\n ${chalk.green('✓')} email_style updated to ${chalk.cyan(style)}`);
156
- this.log(` Run ${chalk.cyan("'mailmodo deploy'")} to apply.\n`);
157
- return;
158
- }
159
- const newValue = await input({
160
- message: `New value for ${editKey}:`,
161
- });
162
- project[editPropKey] =
163
- editPropKey === 'monthlyCap' ? Number(newValue) : newValue;
164
- await saveYaml(yamlConfig);
165
- this.log(`\n ${chalk.green('✓')} Updated. Run ${chalk.cyan("'mailmodo deploy'")} to apply.\n`);
166
- }
167
- /**
168
- * Fetches the domain verification status from the API.
169
- * Returns true/false for verified/unverified, or null if unavailable.
170
- */
171
- async fetchDomainVerified(domain) {
172
- if (!domain)
173
- return null;
174
- try {
175
- await this.ensureAuth();
176
- const response = await this.apiClient.get(API_ENDPOINTS.DOMAIN_STATUS, { domain });
177
- if (!response.ok) {
178
- this.log(` ${chalk.dim('Could not fetch domain status. Run')} ${chalk.cyan("'mailmodo domain --status'")} ${chalk.dim('to check manually.')}`);
179
- return null;
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
  }
@@ -473,19 +473,14 @@
473
473
  "index.js"
474
474
  ]
475
475
  },
476
- "preview": {
476
+ "settings": {
477
477
  "aliases": [],
478
- "args": {
479
- "id": {
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 %> preview welcome",
487
- "<%= config.bin %> preview welcome --text",
488
- "<%= config.bin %> preview welcome --send me@example.com"
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
- "send": {
505
- "description": "Send test email to this address",
506
- "name": "send",
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": "preview",
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
- "preview",
519
+ "settings",
531
520
  "index.js"
532
521
  ]
533
522
  },
534
- "settings": {
523
+ "preview": {
535
524
  "aliases": [],
536
- "args": {},
537
- "description": "View and update project settings",
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 %> settings",
540
- "<%= config.bin %> settings --set brand_color=#0F3460",
541
- "<%= config.bin %> settings --json"
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
- "set": {
558
- "description": "Set a setting (format: key=value)",
559
- "name": "set",
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": "settings",
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
- "settings",
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-beta.pr15.19"
621
+ "version": "0.0.12"
622
622
  }
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.12-beta.pr15.19",
4
+ "version": "0.0.12",
5
5
  "author": "provishalk",
6
6
  "bin": {
7
7
  "mailmodo": "bin/run.js"