@mailmodo/cli 0.0.2-beta.pr4.6 → 0.0.3-beta.pr5.7

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.
Files changed (36) hide show
  1. package/dist/commands/billing/index.d.ts +26 -0
  2. package/dist/commands/billing/index.js +92 -0
  3. package/dist/commands/contacts/index.d.ts +32 -0
  4. package/dist/commands/contacts/index.js +134 -0
  5. package/dist/commands/deploy/index.d.ts +25 -0
  6. package/dist/commands/deploy/index.js +194 -0
  7. package/dist/commands/domain/index.d.ts +27 -0
  8. package/dist/commands/domain/index.js +163 -0
  9. package/dist/commands/edit/index.d.ts +14 -0
  10. package/dist/commands/edit/index.js +96 -0
  11. package/dist/commands/emails/index.d.ts +10 -0
  12. package/dist/commands/emails/index.js +62 -0
  13. package/dist/commands/init/index.d.ts +11 -0
  14. package/dist/commands/init/index.js +124 -0
  15. package/dist/commands/login/index.d.ts +6 -2
  16. package/dist/commands/login/index.js +62 -6
  17. package/dist/commands/logs/index.d.ts +20 -0
  18. package/dist/commands/logs/index.js +82 -0
  19. package/dist/commands/preview/index.d.ts +30 -0
  20. package/dist/commands/preview/index.js +213 -0
  21. package/dist/commands/settings/index.d.ts +19 -0
  22. package/dist/commands/settings/index.js +147 -0
  23. package/dist/commands/status/index.d.ts +10 -0
  24. package/dist/commands/status/index.js +53 -0
  25. package/dist/lib/api-client.d.ts +41 -0
  26. package/dist/lib/api-client.js +125 -0
  27. package/dist/lib/base-command.d.ts +45 -0
  28. package/dist/lib/base-command.js +69 -0
  29. package/dist/lib/config.d.ts +30 -0
  30. package/dist/lib/config.js +47 -0
  31. package/dist/lib/constants.d.ts +27 -0
  32. package/dist/lib/constants.js +27 -0
  33. package/dist/lib/yaml-config.d.ts +65 -0
  34. package/dist/lib/yaml-config.js +70 -0
  35. package/oclif.manifest.json +559 -4
  36. package/package.json +7 -8
@@ -0,0 +1,26 @@
1
+ import { BaseCommand } from '../../lib/base-command.js';
2
+ export default class Billing extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ cap: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ status: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ };
11
+ run(): Promise<void>;
12
+ /**
13
+ * Retrieves and displays the current billing status including card info,
14
+ * block usage, spending total, AI generation limits, and optionally
15
+ * opens Stripe Checkout if no card is on file.
16
+ */
17
+ private showStatus;
18
+ /**
19
+ * Updates the monthly block spending cap. This limits how many 10k-email
20
+ * blocks will be auto-charged per billing cycle.
21
+ *
22
+ * @param {number} cap - The maximum number of blocks per month.
23
+ * @param {boolean} jsonOutput - Whether to output JSON instead of formatted text.
24
+ */
25
+ private setCap;
26
+ }
@@ -0,0 +1,92 @@
1
+ import { Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import open from 'open';
4
+ import { BaseCommand } from '../../lib/base-command.js';
5
+ import { API_ENDPOINTS } from '../../lib/constants.js';
6
+ export default class Billing extends BaseCommand {
7
+ static description = 'View billing status, manage payment, and set spending cap';
8
+ static examples = [
9
+ '<%= config.bin %> billing',
10
+ '<%= config.bin %> billing --status',
11
+ '<%= config.bin %> billing --cap 5',
12
+ ];
13
+ static flags = {
14
+ ...BaseCommand.baseFlags,
15
+ cap: Flags.integer({ description: 'Set monthly block cap (max blocks to auto-charge)' }),
16
+ status: Flags.boolean({ default: false, description: 'Show billing status only' }),
17
+ };
18
+ async run() {
19
+ const { flags } = await this.parse(Billing);
20
+ await this.ensureAuth();
21
+ if (flags.cap !== undefined) {
22
+ await this.setCap(flags.cap, flags.json);
23
+ return;
24
+ }
25
+ await this.showStatus(flags.json);
26
+ }
27
+ /**
28
+ * Retrieves and displays the current billing status including card info,
29
+ * block usage, spending total, AI generation limits, and optionally
30
+ * opens Stripe Checkout if no card is on file.
31
+ */
32
+ async showStatus(jsonOutput) {
33
+ const response = await this.apiClient.get(API_ENDPOINTS.BILLING_STATUS);
34
+ if (!response.ok) {
35
+ this.handleApiError(response);
36
+ }
37
+ const { data } = response;
38
+ if (jsonOutput) {
39
+ this.log(JSON.stringify(data, null, 2));
40
+ return;
41
+ }
42
+ this.log('');
43
+ if (data.cardOnFile) {
44
+ this.log(` Card: ${data.cardSummary || 'Card on file'}`);
45
+ }
46
+ else {
47
+ this.log(` Card: ${chalk.yellow('No card on file')}`);
48
+ }
49
+ if (data.freeExhausted) {
50
+ this.log(` Free tier: ${chalk.dim('exhausted')}`);
51
+ }
52
+ else {
53
+ this.log(` Free tier: active`);
54
+ }
55
+ this.log(` Current block: ${data.currentBlockUsed ?? 0} / 10,000 used`);
56
+ this.log(` Blocks used: ${data.blocksUsed ?? 0} ($${data.totalSpent || '0'} total)`);
57
+ this.log(` Monthly cap: ${data.cap ?? 'not set'} blocks${data.cap ? ` ($${data.cap * 19} max/month)` : ''}`);
58
+ if (data.aiRemaining) {
59
+ this.log(`\n AI generation:`);
60
+ this.log(` Init uses remaining: ${data.aiRemaining.inits}/5 this month`);
61
+ this.log(` Edit uses remaining: ${data.aiRemaining.edits}/50 this month`);
62
+ }
63
+ if (!data.cardOnFile && data.freeExhausted && data.checkoutUrl) {
64
+ this.log(`\n ${chalk.yellow('Free tier exhausted.')} Opening payment page...\n`);
65
+ try {
66
+ await open(data.checkoutUrl);
67
+ }
68
+ catch {
69
+ this.log(` Visit: ${chalk.cyan(data.checkoutUrl)}`);
70
+ }
71
+ }
72
+ this.log('');
73
+ }
74
+ /**
75
+ * Updates the monthly block spending cap. This limits how many 10k-email
76
+ * blocks will be auto-charged per billing cycle.
77
+ *
78
+ * @param {number} cap - The maximum number of blocks per month.
79
+ * @param {boolean} jsonOutput - Whether to output JSON instead of formatted text.
80
+ */
81
+ async setCap(cap, jsonOutput) {
82
+ const response = await this.apiClient.patch(API_ENDPOINTS.BILLING_CAP, { cap });
83
+ if (!response.ok) {
84
+ this.handleApiError(response);
85
+ }
86
+ if (jsonOutput) {
87
+ this.log(JSON.stringify({ cap, maxMonthlySpend: `$${cap * 19}`, status: 'updated' }, null, 2));
88
+ return;
89
+ }
90
+ this.log(`\n ${chalk.green('✓')} Cap set to ${chalk.bold(String(cap))} blocks ($${cap * 19} max/month).\n`);
91
+ }
92
+ }
@@ -0,0 +1,32 @@
1
+ import { BaseCommand } from '../../lib/base-command.js';
2
+ export default class Contacts extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ delete: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ export: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<void>;
13
+ /**
14
+ * Displays aggregate contact counts: total, active, unsubscribed, bounced.
15
+ */
16
+ private showSummary;
17
+ /**
18
+ * Looks up a single contact by email address and displays their
19
+ * status, properties, and pending emails.
20
+ */
21
+ private searchContact;
22
+ /**
23
+ * Triggers a CSV export of all contacts. The API returns a download URL
24
+ * for the generated file.
25
+ */
26
+ private exportContacts;
27
+ /**
28
+ * Performs a GDPR-compliant hard delete of a contact and all their
29
+ * send history. Requires confirmation unless --yes flag is used.
30
+ */
31
+ private deleteContact;
32
+ }
@@ -0,0 +1,134 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { confirm } from '@inquirer/prompts';
3
+ import chalk from 'chalk';
4
+ import { BaseCommand } from '../../lib/base-command.js';
5
+ import { API_ENDPOINTS } from '../../lib/constants.js';
6
+ export default class Contacts extends BaseCommand {
7
+ static description = 'Manage contacts — search, export, or delete';
8
+ static examples = [
9
+ '<%= config.bin %> contacts',
10
+ '<%= config.bin %> contacts --search sarah@example.com',
11
+ '<%= config.bin %> contacts --export',
12
+ '<%= config.bin %> contacts --delete sarah@example.com',
13
+ ];
14
+ static flags = {
15
+ ...BaseCommand.baseFlags,
16
+ delete: Flags.string({ description: 'GDPR hard delete a contact by email' }),
17
+ export: Flags.boolean({ default: false, description: 'Export all contacts as CSV' }),
18
+ search: Flags.string({ description: 'Search for a contact by email' }),
19
+ };
20
+ async run() {
21
+ const { flags } = await this.parse(Contacts);
22
+ await this.ensureAuth();
23
+ if (flags.search) {
24
+ await this.searchContact(flags.search, flags.json);
25
+ return;
26
+ }
27
+ if (flags.export) {
28
+ await this.exportContacts(flags.json);
29
+ return;
30
+ }
31
+ if (flags.delete) {
32
+ await this.deleteContact(flags.delete, flags.json, flags.yes);
33
+ return;
34
+ }
35
+ await this.showSummary(flags.json);
36
+ }
37
+ /**
38
+ * Displays aggregate contact counts: total, active, unsubscribed, bounced.
39
+ */
40
+ async showSummary(jsonOutput) {
41
+ const response = await this.apiClient.get(API_ENDPOINTS.CONTACTS);
42
+ if (!response.ok) {
43
+ this.handleApiError(response);
44
+ }
45
+ const { data } = response;
46
+ if (jsonOutput) {
47
+ this.log(JSON.stringify(data, null, 2));
48
+ return;
49
+ }
50
+ this.log(`\n Total: ${chalk.bold(String(data.total ?? 0))} ` +
51
+ `Active: ${chalk.green(String(data.active ?? 0))} ` +
52
+ `Unsubscribed: ${chalk.yellow(String(data.unsubscribed ?? 0))} ` +
53
+ `Bounced: ${chalk.red(String(data.bounced ?? 0))}\n`);
54
+ }
55
+ /**
56
+ * Looks up a single contact by email address and displays their
57
+ * status, properties, and pending emails.
58
+ */
59
+ async searchContact(email, jsonOutput) {
60
+ const response = await this.apiClient.get(`${API_ENDPOINTS.CONTACTS}/${encodeURIComponent(email)}`);
61
+ if (!response.ok) {
62
+ if (response.status === 404) {
63
+ if (jsonOutput) {
64
+ this.log(JSON.stringify({ email, error: 'Contact not found' }, null, 2));
65
+ }
66
+ else {
67
+ this.log(`\n Contact '${email}' not found.\n`);
68
+ }
69
+ return;
70
+ }
71
+ this.handleApiError(response);
72
+ }
73
+ const contact = response.data;
74
+ if (jsonOutput) {
75
+ this.log(JSON.stringify(contact, null, 2));
76
+ return;
77
+ }
78
+ this.log(`\n ${chalk.bold('Email:')} ${contact.email}`);
79
+ this.log(` ${chalk.bold('Status:')} ${contact.status}`);
80
+ this.log(` ${chalk.bold('Added:')} ${contact.added}`);
81
+ this.log(` ${chalk.bold('Next email:')} ${contact.nextEmail || 'None'}`);
82
+ if (contact.properties && Object.keys(contact.properties).length > 0) {
83
+ this.log(` ${chalk.bold('Properties:')}`);
84
+ for (const [key, value] of Object.entries(contact.properties)) {
85
+ this.log(` ${key}: ${String(value)}`);
86
+ }
87
+ }
88
+ this.log('');
89
+ }
90
+ /**
91
+ * Triggers a CSV export of all contacts. The API returns a download URL
92
+ * for the generated file.
93
+ */
94
+ async exportContacts(jsonOutput) {
95
+ const response = await this.apiClient.get(API_ENDPOINTS.CONTACTS_EXPORT);
96
+ if (!response.ok) {
97
+ this.handleApiError(response);
98
+ }
99
+ if (jsonOutput) {
100
+ this.log(JSON.stringify(response.data, null, 2));
101
+ return;
102
+ }
103
+ this.log(`\n ${chalk.green('✓')} Contact export started.`);
104
+ if (response.data?.downloadUrl) {
105
+ this.log(` Download: ${chalk.cyan(response.data.downloadUrl)}`);
106
+ }
107
+ this.log('');
108
+ }
109
+ /**
110
+ * Performs a GDPR-compliant hard delete of a contact and all their
111
+ * send history. Requires confirmation unless --yes flag is used.
112
+ */
113
+ async deleteContact(email, jsonOutput, skipConfirm) {
114
+ if (!skipConfirm) {
115
+ const confirmed = await confirm({
116
+ default: false,
117
+ message: `Permanently delete ${email} and all their data? This cannot be undone.`,
118
+ });
119
+ if (!confirmed) {
120
+ this.log('\n Delete cancelled.\n');
121
+ return;
122
+ }
123
+ }
124
+ const response = await this.apiClient.delete(`${API_ENDPOINTS.CONTACTS}/${encodeURIComponent(email)}`);
125
+ if (!response.ok) {
126
+ this.handleApiError(response);
127
+ }
128
+ if (jsonOutput) {
129
+ this.log(JSON.stringify({ deleted: true, email }, null, 2));
130
+ return;
131
+ }
132
+ this.log(`\n ${chalk.green('✓')} Contact ${email} permanently deleted.\n`);
133
+ }
134
+ }
@@ -0,0 +1,25 @@
1
+ import { BaseCommand } from '../../lib/base-command.js';
2
+ export default class Deploy extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ };
9
+ run(): Promise<void>;
10
+ /**
11
+ * Interactive domain setup flow. Collects domain, sender email, and business
12
+ * address from the user, then calls the API to get DNS records to configure.
13
+ * Polls for verification when the user indicates they've added the records.
14
+ *
15
+ * @returns {Promise<boolean>} true if domain was verified, false if skipped.
16
+ */
17
+ private runDomainSetup;
18
+ /**
19
+ * Calls the domain verification API endpoint and reports pass/fail
20
+ * status for each DNS record (SPF, DKIM, DMARC).
21
+ *
22
+ * @returns {Promise<boolean>} true if all records pass.
23
+ */
24
+ private verifyDomain;
25
+ }
@@ -0,0 +1,194 @@
1
+ import { confirm, input } from '@inquirer/prompts';
2
+ import chalk from 'chalk';
3
+ import { BaseCommand } from '../../lib/base-command.js';
4
+ import { API_ENDPOINTS, DNS_GUIDE_URL } from '../../lib/constants.js';
5
+ import { saveYaml } from '../../lib/yaml-config.js';
6
+ export default class Deploy extends BaseCommand {
7
+ static description = 'Deploy email sequences and verify sending domain';
8
+ static examples = [
9
+ '<%= config.bin %> deploy',
10
+ '<%= config.bin %> deploy --yes',
11
+ ];
12
+ static flags = {
13
+ ...BaseCommand.baseFlags,
14
+ };
15
+ async run() {
16
+ const { flags } = await this.parse(Deploy);
17
+ await this.ensureAuth();
18
+ const yamlConfig = await this.ensureYaml();
19
+ const domainVerify = await this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY);
20
+ const domainVerified = domainVerify.ok &&
21
+ domainVerify.data?.spf === 'pass' &&
22
+ domainVerify.data?.dkim === 'pass' &&
23
+ domainVerify.data?.dmarc === 'pass';
24
+ if (!domainVerified) {
25
+ if (!flags.json) {
26
+ this.log(`\n No sending domain verified yet.`);
27
+ this.log(` You need to verify a domain before sending emails.`);
28
+ this.log(` This is a one-time setup. Takes about 5 minutes.\n`);
29
+ }
30
+ if (!flags.yes) {
31
+ const setupNow = await confirm({ default: true, message: 'Set up your sending domain now?' });
32
+ if (!setupNow) {
33
+ this.log(`\n Sequences saved but ${chalk.yellow('NOT deployed')}.`);
34
+ this.log(` Emails will not send until your domain is verified.`);
35
+ this.log(` When ready, run: ${chalk.cyan('mailmodo domain')}`);
36
+ this.log(` Then: ${chalk.cyan('mailmodo deploy')}\n`);
37
+ return;
38
+ }
39
+ }
40
+ const completed = await this.runDomainSetup(yamlConfig, flags);
41
+ if (!completed)
42
+ return;
43
+ }
44
+ if (!flags.json) {
45
+ this.log(`\n ${chalk.green('✓')} Domain: ${yamlConfig.project?.domain || 'verified'}\n`);
46
+ this.log(` Deploying:`);
47
+ for (const email of yamlConfig.emails) {
48
+ this.log(` ${chalk.green('+')} ${email.id.padEnd(24)} ${email.trigger}`);
49
+ }
50
+ this.log('');
51
+ }
52
+ if (!flags.yes) {
53
+ const proceed = await confirm({
54
+ default: true,
55
+ message: `Deploy ${yamlConfig.emails.length} emails?`,
56
+ });
57
+ if (!proceed) {
58
+ this.log('\n Deploy cancelled.\n');
59
+ return;
60
+ }
61
+ }
62
+ const response = await this.apiClient.post(API_ENDPOINTS.SEQUENCES, {
63
+ emails: yamlConfig.emails,
64
+ project: yamlConfig.project,
65
+ });
66
+ if (!response.ok) {
67
+ this.handleApiError(response);
68
+ }
69
+ if (flags.json) {
70
+ this.log(JSON.stringify({
71
+ deployed: true,
72
+ emailsLive: yamlConfig.emails.length,
73
+ sdkSnippet: response.data?.sdkSnippet,
74
+ }, null, 2));
75
+ return;
76
+ }
77
+ this.log(` ${chalk.green('Deployed.')} Emails are live.\n`);
78
+ this.log(` ${'─'.repeat(53)}`);
79
+ this.log(` ${chalk.bold('ADD THIS TO YOUR APP (one-time only):')}`);
80
+ this.log(` ${'─'.repeat(53)}\n`);
81
+ this.log(` ${chalk.cyan('npm install @mailmodo/sdk')}\n`);
82
+ this.log(` ${chalk.dim("import { track, identify } from '@mailmodo/sdk'")}\n`);
83
+ this.log(` ${chalk.dim('// On user signup:')}`);
84
+ this.log(` ${chalk.dim("track('user.signup', { email, first_name, app_url })")}\n`);
85
+ this.log(` ${chalk.dim('// When user creates a project:')}`);
86
+ this.log(` ${chalk.dim("identify(email, { has_created_project: true })")}\n`);
87
+ this.log(` ${chalk.dim('// On trial expiry:')}`);
88
+ this.log(` ${chalk.dim("track('user.trial_expiry', { email, first_name })")}\n`);
89
+ this.log(` Full SDK docs: ${chalk.cyan('mailmodo.com/docs/sdk')}\n`);
90
+ this.log(` ${'─'.repeat(53)}\n`);
91
+ }
92
+ /**
93
+ * Interactive domain setup flow. Collects domain, sender email, and business
94
+ * address from the user, then calls the API to get DNS records to configure.
95
+ * Polls for verification when the user indicates they've added the records.
96
+ *
97
+ * @returns {Promise<boolean>} true if domain was verified, false if skipped.
98
+ */
99
+ async runDomainSetup(yamlConfig, flags) {
100
+ let domain;
101
+ let senderEmail;
102
+ let address;
103
+ if (flags.yes) {
104
+ domain = yamlConfig.project?.domain || '';
105
+ senderEmail = yamlConfig.project?.fromEmail || '';
106
+ address = yamlConfig.project?.address || '';
107
+ }
108
+ else {
109
+ this.log(`\n ${'─'.repeat(53)}`);
110
+ this.log(` ${chalk.bold('DOMAIN SETUP')}`);
111
+ this.log(` ${'─'.repeat(53)}\n`);
112
+ domain = await input({
113
+ message: 'What domain will you send from?',
114
+ validate: (v) => (v?.trim() ? true : 'Domain is required'),
115
+ });
116
+ senderEmail = await input({
117
+ message: 'Sender email address:',
118
+ validate: (v) => (v?.includes('@') ? true : 'Please enter a valid email'),
119
+ });
120
+ address = await input({
121
+ message: 'Business address (required by law for email footers):',
122
+ validate: (v) => (v?.trim() ? true : 'Address is required'),
123
+ });
124
+ }
125
+ const domainResponse = await this.apiClient.post(API_ENDPOINTS.DOMAIN, {
126
+ address,
127
+ domain,
128
+ fromEmail: senderEmail,
129
+ });
130
+ if (!domainResponse.ok) {
131
+ this.handleApiError(domainResponse);
132
+ }
133
+ yamlConfig.project.domain = domain;
134
+ yamlConfig.project.fromEmail = senderEmail;
135
+ yamlConfig.project.address = address;
136
+ await saveYaml(yamlConfig);
137
+ const dnsRecords = domainResponse.data?.dnsRecords || [];
138
+ if (!flags.json) {
139
+ this.log(`\n Add these ${dnsRecords.length} DNS records to your domain provider:\n`);
140
+ for (const [i, record] of dnsRecords.entries()) {
141
+ this.log(` ${chalk.bold(`RECORD ${i + 1}`)}`);
142
+ this.log(` Type: ${record.type}`);
143
+ this.log(` Host: ${record.host}`);
144
+ this.log(` Value: ${record.value}\n`);
145
+ }
146
+ this.log(` DNS changes take 5–30 minutes to propagate.`);
147
+ this.log(` Full guide: ${chalk.cyan(DNS_GUIDE_URL)}\n`);
148
+ }
149
+ if (flags.yes) {
150
+ return this.verifyDomain(flags.json);
151
+ }
152
+ const action = await input({
153
+ default: '',
154
+ message: "Press Enter once you've added the records, or 'skip' to do this later.",
155
+ });
156
+ if (action.toLowerCase() === 'skip') {
157
+ this.log(`\n Sequences saved but ${chalk.yellow('NOT deployed')}.`);
158
+ this.log(` When ready, run: ${chalk.cyan('mailmodo domain')}`);
159
+ this.log(` Then: ${chalk.cyan('mailmodo deploy')}\n`);
160
+ return false;
161
+ }
162
+ return this.verifyDomain(flags.json);
163
+ }
164
+ /**
165
+ * Calls the domain verification API endpoint and reports pass/fail
166
+ * status for each DNS record (SPF, DKIM, DMARC).
167
+ *
168
+ * @returns {Promise<boolean>} true if all records pass.
169
+ */
170
+ async verifyDomain(jsonOutput) {
171
+ if (!jsonOutput) {
172
+ this.log(`\n Checking DNS...`);
173
+ }
174
+ const verify = await this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY);
175
+ if (!verify.ok) {
176
+ this.handleApiError(verify);
177
+ }
178
+ const { dkim, dmarc, spf } = verify.data;
179
+ const allPassed = spf === 'pass' && dkim === 'pass' && dmarc === 'pass';
180
+ if (!jsonOutput) {
181
+ this.log(` SPF ${spf === 'pass' ? chalk.green('✓') : chalk.red('✗')}`);
182
+ this.log(` DKIM ${dkim === 'pass' ? chalk.green('✓') : chalk.red('✗')}`);
183
+ this.log(` DMARC ${dmarc === 'pass' ? chalk.green('✓') : chalk.red('✗')}`);
184
+ if (allPassed) {
185
+ this.log(`\n ${chalk.green('Domain verified.')} Continuing deploy...\n`);
186
+ }
187
+ else {
188
+ this.log(`\n ${chalk.yellow('Some records failed.')} Fix them and run ${chalk.cyan('mailmodo domain --verify')}.`);
189
+ this.log(` Help: ${chalk.cyan(DNS_GUIDE_URL)}\n`);
190
+ }
191
+ }
192
+ return allPassed;
193
+ }
194
+ }
@@ -0,0 +1,27 @@
1
+ import { BaseCommand } from '../../lib/base-command.js';
2
+ export default class Domain extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ status: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ verify: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ };
11
+ run(): Promise<void>;
12
+ /**
13
+ * Interactive domain setup: collects domain, sender email, and business address,
14
+ * then calls the API to retrieve the required DNS records.
15
+ */
16
+ private setupDomain;
17
+ /**
18
+ * Calls the domain verification API and displays pass/fail for each DNS record.
19
+ */
20
+ private verifyDomain;
21
+ /**
22
+ * Displays domain health metrics including verification status,
23
+ * bounce rate, and spam complaint rate.
24
+ */
25
+ private showDomainStatus;
26
+ private recordLabel;
27
+ }