@mailmodo/cli 0.0.2 → 0.0.3-beta.pr5.8
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.d.ts +26 -0
- package/dist/commands/billing/index.js +92 -0
- package/dist/commands/contacts/index.d.ts +32 -0
- package/dist/commands/contacts/index.js +134 -0
- package/dist/commands/deploy/index.d.ts +25 -0
- package/dist/commands/deploy/index.js +194 -0
- package/dist/commands/domain/index.d.ts +27 -0
- package/dist/commands/domain/index.js +163 -0
- package/dist/commands/edit/index.d.ts +14 -0
- package/dist/commands/edit/index.js +96 -0
- package/dist/commands/emails/index.d.ts +10 -0
- package/dist/commands/emails/index.js +62 -0
- package/dist/commands/init/index.d.ts +11 -0
- package/dist/commands/init/index.js +124 -0
- package/dist/commands/login/index.d.ts +10 -0
- package/dist/commands/login/index.js +65 -0
- package/dist/commands/logs/index.d.ts +20 -0
- package/dist/commands/logs/index.js +82 -0
- package/dist/commands/preview/index.d.ts +30 -0
- package/dist/commands/preview/index.js +213 -0
- package/dist/commands/settings/index.d.ts +19 -0
- package/dist/commands/settings/index.js +147 -0
- package/dist/commands/status/index.d.ts +10 -0
- package/dist/commands/status/index.js +53 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/api-client.d.ts +41 -0
- package/dist/lib/api-client.js +125 -0
- package/dist/lib/base-command.d.ts +45 -0
- package/dist/lib/base-command.js +69 -0
- package/dist/lib/config.d.ts +30 -0
- package/dist/lib/config.js +47 -0
- package/dist/lib/constants.d.ts +27 -0
- package/dist/lib/constants.js +27 -0
- package/dist/lib/yaml-config.d.ts +65 -0
- package/dist/lib/yaml-config.js +70 -0
- package/oclif.manifest.json +582 -2
- package/package.json +8 -9
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { input } from '@inquirer/prompts';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
|
+
import { API_ENDPOINTS, DNS_GUIDE_URL } from '../../lib/constants.js';
|
|
6
|
+
import { saveYaml } from '../../lib/yaml-config.js';
|
|
7
|
+
export default class Domain extends BaseCommand {
|
|
8
|
+
static description = 'Set up and verify your sending domain';
|
|
9
|
+
static examples = [
|
|
10
|
+
'<%= config.bin %> domain',
|
|
11
|
+
'<%= config.bin %> domain --verify',
|
|
12
|
+
'<%= config.bin %> domain --status',
|
|
13
|
+
];
|
|
14
|
+
static flags = {
|
|
15
|
+
...BaseCommand.baseFlags,
|
|
16
|
+
status: Flags.boolean({ default: false, description: 'Show domain health status' }),
|
|
17
|
+
verify: Flags.boolean({ default: false, description: 'Verify DNS records' }),
|
|
18
|
+
};
|
|
19
|
+
async run() {
|
|
20
|
+
const { flags } = await this.parse(Domain);
|
|
21
|
+
await this.ensureAuth();
|
|
22
|
+
if (flags.verify) {
|
|
23
|
+
await this.verifyDomain(flags.json);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (flags.status) {
|
|
27
|
+
await this.showDomainStatus(flags.json);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
await this.setupDomain(flags);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Interactive domain setup: collects domain, sender email, and business address,
|
|
34
|
+
* then calls the API to retrieve the required DNS records.
|
|
35
|
+
*/
|
|
36
|
+
async setupDomain(flags) {
|
|
37
|
+
const yamlConfig = await this.ensureYaml();
|
|
38
|
+
this.log(`\n ${'─'.repeat(53)}`);
|
|
39
|
+
this.log(` ${chalk.bold('DOMAIN SETUP')}`);
|
|
40
|
+
this.log(` ${'─'.repeat(53)}\n`);
|
|
41
|
+
let domain;
|
|
42
|
+
let senderEmail;
|
|
43
|
+
let address;
|
|
44
|
+
if (flags.yes) {
|
|
45
|
+
domain = yamlConfig.project?.domain || '';
|
|
46
|
+
senderEmail = yamlConfig.project?.fromEmail || '';
|
|
47
|
+
address = yamlConfig.project?.address || '';
|
|
48
|
+
if (!domain) {
|
|
49
|
+
this.error('Domain is required. Set it in mailmodo.yaml or use interactive mode.');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
domain = await input({
|
|
54
|
+
default: yamlConfig.project?.domain,
|
|
55
|
+
message: 'What domain will you send from?',
|
|
56
|
+
validate: (v) => (v?.trim() ? true : 'Domain is required'),
|
|
57
|
+
});
|
|
58
|
+
senderEmail = await input({
|
|
59
|
+
default: yamlConfig.project?.fromEmail,
|
|
60
|
+
message: 'Sender email address:',
|
|
61
|
+
validate: (v) => (v?.includes('@') ? true : 'Please enter a valid email'),
|
|
62
|
+
});
|
|
63
|
+
address = await input({
|
|
64
|
+
default: yamlConfig.project?.address,
|
|
65
|
+
message: 'Business address (required by law):',
|
|
66
|
+
validate: (v) => (v?.trim() ? true : 'Address is required'),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const response = await this.apiClient.post(API_ENDPOINTS.DOMAIN, {
|
|
70
|
+
address,
|
|
71
|
+
domain,
|
|
72
|
+
fromEmail: senderEmail,
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
this.handleApiError(response);
|
|
76
|
+
}
|
|
77
|
+
yamlConfig.project.domain = domain;
|
|
78
|
+
yamlConfig.project.fromEmail = senderEmail;
|
|
79
|
+
yamlConfig.project.address = address;
|
|
80
|
+
await saveYaml(yamlConfig);
|
|
81
|
+
const records = response.data?.dnsRecords || [];
|
|
82
|
+
if (flags.json) {
|
|
83
|
+
this.log(JSON.stringify({ dnsRecords: records, domain }, null, 2));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.log(`\n Add these ${records.length} DNS records to your domain provider:\n`);
|
|
87
|
+
for (const [i, record] of records.entries()) {
|
|
88
|
+
this.log(` ${chalk.bold(`RECORD ${i + 1} — ${this.recordLabel(i)}`)}`);
|
|
89
|
+
this.log(` Type: ${record.type}`);
|
|
90
|
+
this.log(` Host: ${record.host}`);
|
|
91
|
+
this.log(` Value: ${record.value}\n`);
|
|
92
|
+
}
|
|
93
|
+
this.log(` DNS changes take 5–30 minutes to propagate.`);
|
|
94
|
+
this.log(` Full guide: ${chalk.cyan(DNS_GUIDE_URL)}\n`);
|
|
95
|
+
if (!flags.yes) {
|
|
96
|
+
const action = await input({
|
|
97
|
+
default: '',
|
|
98
|
+
message: "Press Enter once you've added the records, or 'skip'.",
|
|
99
|
+
});
|
|
100
|
+
if (action.toLowerCase() !== 'skip') {
|
|
101
|
+
await this.verifyDomain(false);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Calls the domain verification API and displays pass/fail for each DNS record.
|
|
107
|
+
*/
|
|
108
|
+
async verifyDomain(jsonOutput) {
|
|
109
|
+
if (!jsonOutput) {
|
|
110
|
+
this.log(`\n Checking DNS...`);
|
|
111
|
+
}
|
|
112
|
+
const response = await this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY);
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
this.handleApiError(response);
|
|
115
|
+
}
|
|
116
|
+
const { dkim, dmarc, spf } = response.data;
|
|
117
|
+
if (jsonOutput) {
|
|
118
|
+
this.log(JSON.stringify({ dkim, dmarc, spf }, null, 2));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.log(` SPF ${spf === 'pass' ? chalk.green('✓') : chalk.red('✗ Not found')}`);
|
|
122
|
+
this.log(` DKIM ${dkim === 'pass' ? chalk.green('✓') : chalk.red('✗ Not found')}`);
|
|
123
|
+
this.log(` DMARC ${dmarc === 'pass' ? chalk.green('✓') : chalk.red('✗ Not found')}`);
|
|
124
|
+
const allPassed = spf === 'pass' && dkim === 'pass' && dmarc === 'pass';
|
|
125
|
+
if (allPassed) {
|
|
126
|
+
this.log(`\n ${chalk.green('✓')} Domain verified.\n`);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
this.log(`\n ${chalk.yellow('Some records failed.')}`);
|
|
130
|
+
if (dkim !== 'pass') {
|
|
131
|
+
this.log(`\n DKIM common mistakes:`);
|
|
132
|
+
this.log(` - Using TXT instead of CNAME record type`);
|
|
133
|
+
this.log(` - Including the full domain in the Host field`);
|
|
134
|
+
this.log(` - Cloudflare: proxy must be OFF (grey cloud, not orange)`);
|
|
135
|
+
}
|
|
136
|
+
this.log(`\n Fix the records and run ${chalk.cyan('mailmodo domain --verify')} again.`);
|
|
137
|
+
this.log(` Help: ${chalk.cyan(DNS_GUIDE_URL)}\n`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Displays domain health metrics including verification status,
|
|
142
|
+
* bounce rate, and spam complaint rate.
|
|
143
|
+
*/
|
|
144
|
+
async showDomainStatus(jsonOutput) {
|
|
145
|
+
const response = await this.apiClient.get(API_ENDPOINTS.DOMAIN_STATUS);
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
this.handleApiError(response);
|
|
148
|
+
}
|
|
149
|
+
const { data } = response;
|
|
150
|
+
if (jsonOutput) {
|
|
151
|
+
this.log(JSON.stringify(data, null, 2));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.log(`\n Domain: ${chalk.bold(data.domain || 'not configured')}`);
|
|
155
|
+
this.log(` Status: ${data.verified ? chalk.green('✓ verified') : chalk.red('✗ not verified')}`);
|
|
156
|
+
this.log(` Bounce rate: ${data.bounceRate ?? 'N/A'}%`);
|
|
157
|
+
this.log(` Spam rate: ${data.spamRate ?? 'N/A'}%\n`);
|
|
158
|
+
}
|
|
159
|
+
recordLabel(index) {
|
|
160
|
+
const labels = ['SPF', 'DKIM', 'DMARC'];
|
|
161
|
+
return labels[index] || `Record ${index + 1}`;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
2
|
+
export default class Edit extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
change: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { confirm, input } 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
|
+
import { loadTemplate, saveTemplate, saveYaml } from '../../lib/yaml-config.js';
|
|
7
|
+
export default class Edit extends BaseCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
id: Args.string({ description: 'Email ID to edit', required: true }),
|
|
10
|
+
};
|
|
11
|
+
static description = 'Edit an email using AI-assisted natural language changes';
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> edit welcome',
|
|
14
|
+
'<%= config.bin %> edit welcome --change "make subject more urgent" --yes',
|
|
15
|
+
];
|
|
16
|
+
static flags = {
|
|
17
|
+
...BaseCommand.baseFlags,
|
|
18
|
+
change: Flags.string({ description: 'Natural language description of the change' }),
|
|
19
|
+
};
|
|
20
|
+
async run() {
|
|
21
|
+
const { args, flags } = await this.parse(Edit);
|
|
22
|
+
await this.ensureAuth();
|
|
23
|
+
const yamlConfig = await this.ensureYaml();
|
|
24
|
+
const emailIndex = yamlConfig.emails.findIndex((e) => e.id === args.id);
|
|
25
|
+
if (emailIndex === -1) {
|
|
26
|
+
this.error(`Email '${args.id}' not found in mailmodo.yaml.`);
|
|
27
|
+
}
|
|
28
|
+
const email = yamlConfig.emails[emailIndex];
|
|
29
|
+
const templateHtml = await loadTemplate(`${email.id}.html`);
|
|
30
|
+
if (!flags.json) {
|
|
31
|
+
this.log(`\n Current subject: '${chalk.cyan(email.subject)}'`);
|
|
32
|
+
}
|
|
33
|
+
let changeDescription = flags.change;
|
|
34
|
+
if (!changeDescription) {
|
|
35
|
+
changeDescription = await input({
|
|
36
|
+
message: 'What do you want to change?',
|
|
37
|
+
validate: (value) => (value?.trim() ? true : 'Please describe the change'),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const response = await this.apiClient.post(API_ENDPOINTS.EDIT, {
|
|
41
|
+
changeRequest: changeDescription,
|
|
42
|
+
currentEmail: {
|
|
43
|
+
condition: email.condition,
|
|
44
|
+
goal: email.goal,
|
|
45
|
+
html: templateHtml,
|
|
46
|
+
id: email.id,
|
|
47
|
+
previewText: email.previewText,
|
|
48
|
+
subject: email.subject,
|
|
49
|
+
trigger: email.trigger,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
this.handleApiError(response);
|
|
54
|
+
}
|
|
55
|
+
const updated = response.data;
|
|
56
|
+
const oldSubject = email.subject;
|
|
57
|
+
const newSubject = updated.subject || email.subject;
|
|
58
|
+
if (!flags.json && oldSubject !== newSubject) {
|
|
59
|
+
this.log(`\n Suggested subject:`);
|
|
60
|
+
this.log(` ${chalk.red(`- ${oldSubject}`)}`);
|
|
61
|
+
this.log(` ${chalk.green(`+ ${newSubject}`)}`);
|
|
62
|
+
}
|
|
63
|
+
if (!flags.yes) {
|
|
64
|
+
const accepted = await confirm({ default: true, message: 'Accept changes?' });
|
|
65
|
+
if (!accepted) {
|
|
66
|
+
this.log('\n Changes discarded.\n');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (updated.subject)
|
|
71
|
+
email.subject = updated.subject;
|
|
72
|
+
if (updated.previewText)
|
|
73
|
+
email.previewText = updated.previewText;
|
|
74
|
+
const updatedYaml = {
|
|
75
|
+
...yamlConfig,
|
|
76
|
+
emails: [...yamlConfig.emails],
|
|
77
|
+
};
|
|
78
|
+
updatedYaml.emails[emailIndex] = email;
|
|
79
|
+
await saveYaml(updatedYaml);
|
|
80
|
+
if (updated.html) {
|
|
81
|
+
await saveTemplate(`${email.id}.html`, updated.html);
|
|
82
|
+
}
|
|
83
|
+
if (flags.json) {
|
|
84
|
+
this.log(JSON.stringify({
|
|
85
|
+
diff: {
|
|
86
|
+
subject: oldSubject === newSubject ? undefined : { new: newSubject, old: oldSubject },
|
|
87
|
+
},
|
|
88
|
+
email,
|
|
89
|
+
status: 'updated',
|
|
90
|
+
}, null, 2));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this.log(`\n Updated ${chalk.green('mailmodo.yaml')}`);
|
|
94
|
+
this.log(` Preview the change: ${chalk.cyan(`mailmodo preview ${email.id}`)}\n`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
2
|
+
export default class Emails 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
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { input } from '@inquirer/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
4
|
+
export default class Emails extends BaseCommand {
|
|
5
|
+
static description = 'List and view configured email sequences';
|
|
6
|
+
static examples = [
|
|
7
|
+
'<%= config.bin %> emails',
|
|
8
|
+
'<%= config.bin %> emails --json',
|
|
9
|
+
];
|
|
10
|
+
static flags = {
|
|
11
|
+
...BaseCommand.baseFlags,
|
|
12
|
+
};
|
|
13
|
+
async run() {
|
|
14
|
+
const { flags } = await this.parse(Emails);
|
|
15
|
+
const yamlConfig = await this.ensureYaml();
|
|
16
|
+
const { emails } = yamlConfig;
|
|
17
|
+
if (flags.json) {
|
|
18
|
+
this.log(JSON.stringify({ emails, total: emails.length }, null, 2));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
this.log(`\n ${chalk.bold(String(emails.length))} emails configured in mailmodo.yaml:\n`);
|
|
22
|
+
const maxIdLen = Math.max(...emails.map((e) => e.id.length), 2);
|
|
23
|
+
const maxTriggerLen = Math.max(...emails.map((e) => e.trigger.length), 7);
|
|
24
|
+
for (const email of emails) {
|
|
25
|
+
const id = email.id.padEnd(maxIdLen + 2);
|
|
26
|
+
const trigger = `trigger: ${email.trigger}`.padEnd(maxTriggerLen + 12);
|
|
27
|
+
const delay = `delay: ${email.delay}`;
|
|
28
|
+
const condition = email.condition ? chalk.dim(` [if ${email.condition}]`) : '';
|
|
29
|
+
this.log(` ${chalk.cyan(id)} ${trigger} ${delay}${condition}`);
|
|
30
|
+
}
|
|
31
|
+
this.log('');
|
|
32
|
+
if (!flags.yes) {
|
|
33
|
+
const emailId = await input({
|
|
34
|
+
default: 'n',
|
|
35
|
+
message: "View an email? (id or 'n'):",
|
|
36
|
+
});
|
|
37
|
+
if (emailId !== 'n') {
|
|
38
|
+
const email = emails.find((e) => e.id === emailId);
|
|
39
|
+
if (!email) {
|
|
40
|
+
this.log(`\n Email '${emailId}' not found.\n`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
this.log('');
|
|
44
|
+
this.log(` ${chalk.bold('ID:')} ${email.id}`);
|
|
45
|
+
this.log(` ${chalk.bold('Trigger:')} ${email.trigger}`);
|
|
46
|
+
this.log(` ${chalk.bold('Delay:')} ${email.delay === 0 || email.delay === '0' ? '0 (immediate)' : email.delay}`);
|
|
47
|
+
this.log(` ${chalk.bold('Subject:')} ${email.subject}`);
|
|
48
|
+
this.log(` ${chalk.bold('Template:')} ${email.template}`);
|
|
49
|
+
if (email.style) {
|
|
50
|
+
this.log(` ${chalk.bold('Style:')} ${email.style}`);
|
|
51
|
+
}
|
|
52
|
+
if (email.condition) {
|
|
53
|
+
this.log(` ${chalk.bold('Condition:')} ${email.condition}`);
|
|
54
|
+
}
|
|
55
|
+
if (email.goal) {
|
|
56
|
+
this.log(` ${chalk.bold('Goal:')} ${email.goal}`);
|
|
57
|
+
}
|
|
58
|
+
this.log('');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
2
|
+
export default class Init extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
url: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { confirm, input } from '@inquirer/prompts';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
|
+
import { API_ENDPOINTS, DEFAULT_BRAND_COLOR } from '../../lib/constants.js';
|
|
6
|
+
import { saveTemplate, saveYaml } from '../../lib/yaml-config.js';
|
|
7
|
+
function isValidUrl(value) {
|
|
8
|
+
try {
|
|
9
|
+
return Boolean(new URL(value));
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export default class Init extends BaseCommand {
|
|
16
|
+
static description = 'Analyze your product and generate email sequences';
|
|
17
|
+
static examples = [
|
|
18
|
+
'<%= config.bin %> init',
|
|
19
|
+
'<%= config.bin %> init --url https://myapp.com --yes',
|
|
20
|
+
];
|
|
21
|
+
static flags = {
|
|
22
|
+
...BaseCommand.baseFlags,
|
|
23
|
+
url: Flags.string({ description: 'Product URL to analyze' }),
|
|
24
|
+
};
|
|
25
|
+
async run() {
|
|
26
|
+
const { flags } = await this.parse(Init);
|
|
27
|
+
await this.ensureAuth();
|
|
28
|
+
let productUrl = flags.url;
|
|
29
|
+
if (!productUrl) {
|
|
30
|
+
productUrl = await input({
|
|
31
|
+
message: 'What is your product URL?',
|
|
32
|
+
validate(value) {
|
|
33
|
+
if (!value?.trim())
|
|
34
|
+
return 'URL is required';
|
|
35
|
+
if (isValidUrl(value))
|
|
36
|
+
return true;
|
|
37
|
+
return 'Please enter a valid URL (e.g., https://myapp.com)';
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
this.log(`\n Analyzing your product...`);
|
|
42
|
+
this.log(` Scraping: homepage, pricing, features\n`);
|
|
43
|
+
const analysisResponse = await this.apiClient.post(API_ENDPOINTS.ANALYZE, { url: productUrl });
|
|
44
|
+
if (!analysisResponse.ok) {
|
|
45
|
+
this.handleApiError(analysisResponse);
|
|
46
|
+
}
|
|
47
|
+
const analysis = analysisResponse.data;
|
|
48
|
+
if (!flags.json) {
|
|
49
|
+
this.log(` Detected:`);
|
|
50
|
+
this.log(` - Type: ${analysis.businessType} — ${analysis.description}`);
|
|
51
|
+
this.log(` - Model: ${analysis.pricingModel}`);
|
|
52
|
+
this.log(` - Users: ${analysis.targetUser}`);
|
|
53
|
+
if (analysis.events?.length) {
|
|
54
|
+
this.log(` - Events: ${analysis.events.join(', ')}`);
|
|
55
|
+
}
|
|
56
|
+
this.log('');
|
|
57
|
+
}
|
|
58
|
+
if (!flags.yes) {
|
|
59
|
+
const ok = await confirm({ default: true, message: 'Does this look right?' });
|
|
60
|
+
if (!ok) {
|
|
61
|
+
this.log(`\n Run ${chalk.cyan('mailmodo init')} again to re-analyze.\n`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
this.log('\n Generating emails...\n');
|
|
66
|
+
const generateResponse = await this.apiClient.post(API_ENDPOINTS.GENERATE, { analysis, productUrl });
|
|
67
|
+
if (!generateResponse.ok) {
|
|
68
|
+
this.handleApiError(generateResponse);
|
|
69
|
+
}
|
|
70
|
+
const generatedEmails = generateResponse.data?.emails || [];
|
|
71
|
+
const emailConfigs = analysis.recommendedEmails.map((rec, index) => {
|
|
72
|
+
const generated = generatedEmails[index];
|
|
73
|
+
return {
|
|
74
|
+
delay: rec.delay || '0',
|
|
75
|
+
id: rec.id,
|
|
76
|
+
trigger: rec.trigger,
|
|
77
|
+
...(rec.condition ? { condition: rec.condition } : {}),
|
|
78
|
+
subject: generated?.subject || `Email for ${rec.id}`,
|
|
79
|
+
template: `mailmodo/${rec.id}.html`,
|
|
80
|
+
...(generated?.previewText ? { previewText: generated.previewText } : {}),
|
|
81
|
+
goal: rec.goal,
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
const yamlConfig = {
|
|
85
|
+
emails: emailConfigs,
|
|
86
|
+
project: {
|
|
87
|
+
brandColor: analysis.brand?.color || DEFAULT_BRAND_COLOR,
|
|
88
|
+
emailStyle: 'branded',
|
|
89
|
+
fromEmail: '',
|
|
90
|
+
fromName: `Team ${analysis.productName}`,
|
|
91
|
+
logoUrl: analysis.brand?.logoUrl || '',
|
|
92
|
+
name: analysis.productName,
|
|
93
|
+
replyTo: '',
|
|
94
|
+
type: analysis.pricingModel,
|
|
95
|
+
url: productUrl,
|
|
96
|
+
webhookUrl: '',
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
await saveYaml(yamlConfig);
|
|
100
|
+
const templateSaves = analysis.recommendedEmails.flatMap((rec, index) => {
|
|
101
|
+
const generated = generatedEmails[index];
|
|
102
|
+
const saves = [];
|
|
103
|
+
if (generated?.html) {
|
|
104
|
+
saves.push(saveTemplate(`${rec.id}.html`, generated.html));
|
|
105
|
+
}
|
|
106
|
+
if (generated?.plainHtml) {
|
|
107
|
+
saves.push(saveTemplate(`${rec.id}_plain.html`, generated.plainHtml));
|
|
108
|
+
}
|
|
109
|
+
return saves;
|
|
110
|
+
});
|
|
111
|
+
await Promise.all(templateSaves);
|
|
112
|
+
if (flags.json) {
|
|
113
|
+
this.log(JSON.stringify({
|
|
114
|
+
brandDetected: analysis.brand,
|
|
115
|
+
emails: emailConfigs,
|
|
116
|
+
emailsCreated: emailConfigs.length,
|
|
117
|
+
style: yamlConfig.project.emailStyle,
|
|
118
|
+
}, null, 2));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.log(` Created ${chalk.green('mailmodo.yaml')} + ${chalk.green(String(emailConfigs.length))} email templates in ${chalk.green('/mailmodo')}\n`);
|
|
122
|
+
this.log(` Run ${chalk.cyan("'mailmodo emails'")} to review.\n`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
2
|
+
export default class Login 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
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { input } from '@inquirer/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import open from 'open';
|
|
4
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
|
+
import { ApiClient } from '../../lib/api-client.js';
|
|
6
|
+
import { saveConfig } from '../../lib/config.js';
|
|
7
|
+
import { API_ENDPOINTS, SIGNUP_URL } from '../../lib/constants.js';
|
|
8
|
+
export default class Login extends BaseCommand {
|
|
9
|
+
static description = 'Authenticate with Mailmodo using your API key';
|
|
10
|
+
static examples = [
|
|
11
|
+
'<%= config.bin %> login',
|
|
12
|
+
'MAILMODO_API_KEY=mm_live_xxx <%= config.bin %> login',
|
|
13
|
+
];
|
|
14
|
+
static flags = {
|
|
15
|
+
...BaseCommand.baseFlags,
|
|
16
|
+
};
|
|
17
|
+
async run() {
|
|
18
|
+
const { flags } = await this.parse(Login);
|
|
19
|
+
let apiKey = process.env.MAILMODO_API_KEY;
|
|
20
|
+
if (apiKey) {
|
|
21
|
+
this.log('Detected MAILMODO_API_KEY from environment.');
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
this.log(`\n Get your free API key at: ${chalk.cyan(SIGNUP_URL)}\n`);
|
|
25
|
+
try {
|
|
26
|
+
await open(SIGNUP_URL);
|
|
27
|
+
this.log(' Opening in browser...\n');
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
this.log(` ${chalk.dim('Could not open browser. Visit the URL above manually.')}\n`);
|
|
31
|
+
}
|
|
32
|
+
apiKey = await input({
|
|
33
|
+
message: 'Paste your API key:',
|
|
34
|
+
validate(value) {
|
|
35
|
+
if (!value?.trim())
|
|
36
|
+
return 'API key is required';
|
|
37
|
+
if (!value.startsWith('mm_'))
|
|
38
|
+
return 'Invalid API key format. Keys start with mm_';
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const trimmedKey = apiKey.trim();
|
|
44
|
+
const client = new ApiClient(trimmedKey);
|
|
45
|
+
const response = await client.get(API_ENDPOINTS.AUTH_VALIDATE);
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
this.handleApiError(response);
|
|
48
|
+
}
|
|
49
|
+
const { accountName, email, freeRemaining } = response.data;
|
|
50
|
+
await saveConfig({
|
|
51
|
+
accountName,
|
|
52
|
+
apiKey: trimmedKey,
|
|
53
|
+
email,
|
|
54
|
+
freeRemaining,
|
|
55
|
+
});
|
|
56
|
+
if (flags.json) {
|
|
57
|
+
this.log(JSON.stringify({ accountName, email, freeRemaining, status: 'authenticated' }, null, 2));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.log(`\n Logged in as ${chalk.green(email)}`);
|
|
61
|
+
this.log(` Free tier: ${chalk.cyan(String(freeRemaining ?? 1000))} emails remaining`);
|
|
62
|
+
this.log(' No credit card required.\n');
|
|
63
|
+
this.log(` Next: Run ${chalk.cyan("'mailmodo init'")} to generate your email sequences.\n`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
2
|
+
export default class Logs extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
email: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
failed: 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
|
+
* Returns the appropriate chalk color function based on the delivery status.
|
|
14
|
+
* Green for sent/opened/clicked, red for bounced/failed, yellow for skipped.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} status - The delivery event status string.
|
|
17
|
+
* @returns {Function} A chalk color function to wrap the status display text.
|
|
18
|
+
*/
|
|
19
|
+
private statusColor;
|
|
20
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
4
|
+
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
5
|
+
export default class Logs extends BaseCommand {
|
|
6
|
+
static description = 'View email send logs and delivery events';
|
|
7
|
+
static examples = [
|
|
8
|
+
'<%= config.bin %> logs',
|
|
9
|
+
'<%= config.bin %> logs --email sarah@example.com',
|
|
10
|
+
'<%= config.bin %> logs --failed',
|
|
11
|
+
'<%= config.bin %> logs --json',
|
|
12
|
+
];
|
|
13
|
+
static flags = {
|
|
14
|
+
...BaseCommand.baseFlags,
|
|
15
|
+
email: Flags.string({ description: 'Filter logs by contact email' }),
|
|
16
|
+
failed: Flags.boolean({ default: false, description: 'Show only failed/bounced events' }),
|
|
17
|
+
};
|
|
18
|
+
async run() {
|
|
19
|
+
const { flags } = await this.parse(Logs);
|
|
20
|
+
await this.ensureAuth();
|
|
21
|
+
const params = {};
|
|
22
|
+
if (flags.email)
|
|
23
|
+
params.email = flags.email;
|
|
24
|
+
if (flags.failed)
|
|
25
|
+
params.failed = 'true';
|
|
26
|
+
const response = await this.apiClient.get(API_ENDPOINTS.LOGS, params);
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
this.handleApiError(response);
|
|
29
|
+
}
|
|
30
|
+
const { entries } = response.data;
|
|
31
|
+
if (flags.json) {
|
|
32
|
+
this.log(JSON.stringify(response.data, null, 2));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
this.log(`\n ${'Time'.padEnd(18)}${'Email'.padEnd(24)}${'Status'.padEnd(10)}Contact`);
|
|
36
|
+
this.log(` ${'─'.repeat(68)}`);
|
|
37
|
+
if (entries?.length) {
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
const time = (entry.timestamp || '').padEnd(18);
|
|
40
|
+
const emailId = (entry.emailId || '').padEnd(24);
|
|
41
|
+
const statusColor = this.statusColor(entry.status);
|
|
42
|
+
const status = statusColor((entry.status || '').padEnd(10));
|
|
43
|
+
const contact = entry.contact || '';
|
|
44
|
+
this.log(` ${time}${emailId}${status}${contact}`);
|
|
45
|
+
if (entry.reason) {
|
|
46
|
+
this.log(` ${' '.repeat(52)}${chalk.dim(`(reason: ${entry.reason})`)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
this.log(` ${chalk.dim('No log entries found.')}`);
|
|
52
|
+
}
|
|
53
|
+
this.log('');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Returns the appropriate chalk color function based on the delivery status.
|
|
57
|
+
* Green for sent/opened/clicked, red for bounced/failed, yellow for skipped.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} status - The delivery event status string.
|
|
60
|
+
* @returns {Function} A chalk color function to wrap the status display text.
|
|
61
|
+
*/
|
|
62
|
+
statusColor(status) {
|
|
63
|
+
switch (status) {
|
|
64
|
+
case 'bounced':
|
|
65
|
+
case 'complained':
|
|
66
|
+
case 'failed': {
|
|
67
|
+
return chalk.red;
|
|
68
|
+
}
|
|
69
|
+
case 'clicked':
|
|
70
|
+
case 'opened':
|
|
71
|
+
case 'sent': {
|
|
72
|
+
return chalk.green;
|
|
73
|
+
}
|
|
74
|
+
case 'skipped': {
|
|
75
|
+
return chalk.yellow;
|
|
76
|
+
}
|
|
77
|
+
default: {
|
|
78
|
+
return chalk.white;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|