@mailmodo/cli 0.0.21-beta.pr24.38 → 0.0.22
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/deploy/index.d.ts +28 -2
- package/dist/commands/deploy/index.js +39 -50
- package/dist/commands/domain/index.js +1 -1
- package/dist/commands/edit/index.js +0 -2
- package/dist/commands/init/index.js +1 -6
- package/dist/commands/logs/index.d.ts +2 -0
- package/dist/commands/logs/index.js +18 -2
- package/dist/commands/settings/index.d.ts +2 -0
- package/dist/commands/settings/index.js +81 -57
- package/dist/lib/yaml-config.d.ts +0 -5
- package/oclif.manifest.json +49 -33
- package/package.json +1 -1
|
@@ -6,9 +6,14 @@ export default class Deploy extends BaseCommand {
|
|
|
6
6
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
7
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* Fetches current DNS verification status for the deploy flow.
|
|
11
|
+
*
|
|
12
|
+
* @param jsonOutput - When true, spinner uses stderr for stdout-safe JSON runs.
|
|
13
|
+
* @param domain - Sending domain to check. If empty, request will fail and trigger setup flow.
|
|
14
|
+
*/
|
|
9
15
|
private fetchDomainVerifyForDeploy;
|
|
10
16
|
run(): Promise<void>;
|
|
11
|
-
private validateSequence;
|
|
12
17
|
private buildDeployPayload;
|
|
13
18
|
private mapEmailToPayload;
|
|
14
19
|
private buildBrandSection;
|
|
@@ -17,11 +22,32 @@ export default class Deploy extends BaseCommand {
|
|
|
17
22
|
private buildProjectPayload;
|
|
18
23
|
private confirmDeploy;
|
|
19
24
|
private ensureDomainReady;
|
|
25
|
+
/**
|
|
26
|
+
* Lists emails about to be deployed (skipped when `--json` is set).
|
|
27
|
+
*
|
|
28
|
+
* @param yamlConfig - Loaded project YAML.
|
|
29
|
+
* @param jsonOutput - When true, skip human-readable output.
|
|
30
|
+
*/
|
|
20
31
|
private logPreDeploySummary;
|
|
21
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Prints the post-deploy success message and SDK install snippet for interactive runs.
|
|
34
|
+
*/
|
|
22
35
|
private logDeploySuccessInstructions;
|
|
36
|
+
/**
|
|
37
|
+
* Interactive domain setup flow. Collects domain, sender email, and business
|
|
38
|
+
* address from the user, then calls the API to get DNS records to configure.
|
|
39
|
+
* Polls for verification when the user indicates they've added the records.
|
|
40
|
+
*
|
|
41
|
+
* @returns {Promise<boolean>} true if domain was verified, false if skipped.
|
|
42
|
+
*/
|
|
23
43
|
private runDomainSetup;
|
|
24
44
|
private collectDomainInputs;
|
|
25
45
|
private showDnsRecords;
|
|
46
|
+
/**
|
|
47
|
+
* Calls the domain verification API endpoint and reports pass/fail
|
|
48
|
+
* status for each DNS record (DKIM, DMARC, Return-Path).
|
|
49
|
+
*
|
|
50
|
+
* @returns {Promise<boolean>} true if all records pass.
|
|
51
|
+
*/
|
|
26
52
|
private verifyDomain;
|
|
27
53
|
}
|
|
@@ -12,6 +12,12 @@ export default class Deploy extends BaseCommand {
|
|
|
12
12
|
static flags = {
|
|
13
13
|
...BaseCommand.baseFlags,
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* Fetches current DNS verification status for the deploy flow.
|
|
17
|
+
*
|
|
18
|
+
* @param jsonOutput - When true, spinner uses stderr for stdout-safe JSON runs.
|
|
19
|
+
* @param domain - Sending domain to check. If empty, request will fail and trigger setup flow.
|
|
20
|
+
*/
|
|
15
21
|
fetchDomainVerifyForDeploy(jsonOutput, domain) {
|
|
16
22
|
return this.withApiSpinner({ json: jsonOutput, text: ' Checking domain verification...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY, {
|
|
17
23
|
domain: domain || '',
|
|
@@ -24,12 +30,11 @@ export default class Deploy extends BaseCommand {
|
|
|
24
30
|
const domainReady = await this.ensureDomainReady(yamlConfig, flags);
|
|
25
31
|
if (!domainReady)
|
|
26
32
|
return;
|
|
27
|
-
|
|
28
|
-
const validateResult = await this.validateSequence(payload, flags);
|
|
29
|
-
this.logPreDeploySummary(yamlConfig, validateResult, flags.json);
|
|
33
|
+
this.logPreDeploySummary(yamlConfig, flags.json);
|
|
30
34
|
const confirmed = await this.confirmDeploy(yamlConfig, flags);
|
|
31
35
|
if (!confirmed)
|
|
32
36
|
return;
|
|
37
|
+
const payload = await this.buildDeployPayload(yamlConfig);
|
|
33
38
|
const response = await this.withApiSpinner({ json: flags.json, text: ' Deploying email sequences...' }, () => this.apiClient.post(API_ENDPOINTS.SEQUENCES_DEPLOY, payload));
|
|
34
39
|
if (!response.ok) {
|
|
35
40
|
this.handleApiError(response);
|
|
@@ -46,19 +51,6 @@ export default class Deploy extends BaseCommand {
|
|
|
46
51
|
}
|
|
47
52
|
this.logDeploySuccessInstructions(response.data.sdkSnippet);
|
|
48
53
|
}
|
|
49
|
-
async validateSequence(payload, flags) {
|
|
50
|
-
const response = await this.withApiSpinner({ json: flags.json, text: ' Validating sequence...' }, () => this.apiClient.post(API_ENDPOINTS.SEQUENCES_VALIDATE, payload));
|
|
51
|
-
if (!response.ok) {
|
|
52
|
-
if (response.data.error === 'senderDomainNotFound') {
|
|
53
|
-
this.error(`Sending domain not registered. Run: ${chalk.cyan('mailmodo domain')}`);
|
|
54
|
-
}
|
|
55
|
-
if (response.data.error === 'senderDomainNotVerified') {
|
|
56
|
-
this.error(`Sending domain not verified. Run: ${chalk.cyan('mailmodo domain --verify')}`);
|
|
57
|
-
}
|
|
58
|
-
this.handleApiError(response);
|
|
59
|
-
}
|
|
60
|
-
return response.data;
|
|
61
|
-
}
|
|
62
54
|
async buildDeployPayload(yamlConfig) {
|
|
63
55
|
const emailsWithHtml = await Promise.all(yamlConfig.emails.map(async (email) => {
|
|
64
56
|
const html = (await loadTemplate(`${email.id}.html`)) || '';
|
|
@@ -72,7 +64,7 @@ export default class Deploy extends BaseCommand {
|
|
|
72
64
|
mapEmailToPayload(email) {
|
|
73
65
|
return {
|
|
74
66
|
condition: email.condition || null,
|
|
75
|
-
ctaText:
|
|
67
|
+
ctaText: '',
|
|
76
68
|
delay: typeof email.delay === 'string'
|
|
77
69
|
? Number.parseInt(email.delay, 10) || 0
|
|
78
70
|
: email.delay,
|
|
@@ -94,11 +86,11 @@ export default class Deploy extends BaseCommand {
|
|
|
94
86
|
buildProductSection(project) {
|
|
95
87
|
return {
|
|
96
88
|
businessType: project?.type || '',
|
|
97
|
-
description:
|
|
98
|
-
pricingModel:
|
|
89
|
+
description: '',
|
|
90
|
+
pricingModel: '',
|
|
99
91
|
productName: project?.name || '',
|
|
100
|
-
saasModel:
|
|
101
|
-
targetUser:
|
|
92
|
+
saasModel: '',
|
|
93
|
+
targetUser: '',
|
|
102
94
|
url: project?.url || '',
|
|
103
95
|
};
|
|
104
96
|
}
|
|
@@ -158,41 +150,25 @@ export default class Deploy extends BaseCommand {
|
|
|
158
150
|
}
|
|
159
151
|
return this.runDomainSetup(yamlConfig, flags);
|
|
160
152
|
}
|
|
161
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Lists emails about to be deployed (skipped when `--json` is set).
|
|
155
|
+
*
|
|
156
|
+
* @param yamlConfig - Loaded project YAML.
|
|
157
|
+
* @param jsonOutput - When true, skip human-readable output.
|
|
158
|
+
*/
|
|
159
|
+
logPreDeploySummary(yamlConfig, jsonOutput) {
|
|
162
160
|
if (jsonOutput)
|
|
163
161
|
return;
|
|
164
162
|
this.log(`\n ${chalk.green('✓')} Domain: ${yamlConfig.project?.domain || 'verified'}\n`);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
this.log(` ${chalk.green('+')} ${email.id.padEnd(24)} ${email.trigger}`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
this.logDiff(validateResult.diff);
|
|
163
|
+
this.log(` Deploying:`);
|
|
164
|
+
for (const email of yamlConfig.emails) {
|
|
165
|
+
this.log(` ${chalk.green('+')} ${email.id.padEnd(24)} ${email.trigger}`);
|
|
173
166
|
}
|
|
174
167
|
this.log('');
|
|
175
168
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
this.log(` Changes vs. last deployment:`);
|
|
182
|
-
for (const email of diff.added) {
|
|
183
|
-
this.log(` ${chalk.green('+')} ${email.id.padEnd(24)} ${email.trigger || ''}`);
|
|
184
|
-
}
|
|
185
|
-
for (const email of diff.removed) {
|
|
186
|
-
this.log(` ${chalk.red('-')} ${email.id.padEnd(24)} ${email.trigger || ''}`);
|
|
187
|
-
}
|
|
188
|
-
for (const email of diff.modified) {
|
|
189
|
-
const fields = email.changedFields?.join(', ') || '';
|
|
190
|
-
this.log(` ${chalk.yellow('~')} ${email.id.padEnd(24)} ${fields}`);
|
|
191
|
-
}
|
|
192
|
-
if (diff.unchanged.length > 0) {
|
|
193
|
-
this.log(` ${chalk.dim(`∙ ${diff.unchanged.length} unchanged`)}`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
169
|
+
/**
|
|
170
|
+
* Prints the post-deploy success message and SDK install snippet for interactive runs.
|
|
171
|
+
*/
|
|
196
172
|
logDeploySuccessInstructions(sdkSnippet) {
|
|
197
173
|
this.log(` ${chalk.green('Deployed.')} Emails are live.\n`);
|
|
198
174
|
this.log(` ${'─'.repeat(53)}`);
|
|
@@ -209,6 +185,13 @@ export default class Deploy extends BaseCommand {
|
|
|
209
185
|
this.log(` Full SDK docs: ${chalk.cyan('mailmodo.com/docs/sdk')}\n`);
|
|
210
186
|
this.log(` ${'─'.repeat(53)}\n`);
|
|
211
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Interactive domain setup flow. Collects domain, sender email, and business
|
|
190
|
+
* address from the user, then calls the API to get DNS records to configure.
|
|
191
|
+
* Polls for verification when the user indicates they've added the records.
|
|
192
|
+
*
|
|
193
|
+
* @returns {Promise<boolean>} true if domain was verified, false if skipped.
|
|
194
|
+
*/
|
|
212
195
|
async runDomainSetup(yamlConfig, flags) {
|
|
213
196
|
const { address, domain, senderEmail } = await this.collectDomainInputs(yamlConfig, flags);
|
|
214
197
|
const domainResponse = await this.withApiSpinner({ json: flags.json, text: ' Configuring domain...' }, () => this.apiClient.post(API_ENDPOINTS.DOMAIN, {
|
|
@@ -277,6 +260,12 @@ export default class Deploy extends BaseCommand {
|
|
|
277
260
|
this.log(` DNS changes take 5–30 minutes to propagate.`);
|
|
278
261
|
this.log(` Full guide: ${chalk.cyan(DNS_GUIDE_URL)}\n`);
|
|
279
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Calls the domain verification API endpoint and reports pass/fail
|
|
265
|
+
* status for each DNS record (DKIM, DMARC, Return-Path).
|
|
266
|
+
*
|
|
267
|
+
* @returns {Promise<boolean>} true if all records pass.
|
|
268
|
+
*/
|
|
280
269
|
async verifyDomain(jsonOutput, domain) {
|
|
281
270
|
const verify = await this.withApiSpinner({ json: jsonOutput, text: ' Checking DNS...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY, {
|
|
282
271
|
domain,
|
|
@@ -179,7 +179,7 @@ export default class Domain extends BaseCommand {
|
|
|
179
179
|
this.log(` Spam rate: ${data.spamRate ?? 'N/A'}%\n`);
|
|
180
180
|
}
|
|
181
181
|
recordLabel(index) {
|
|
182
|
-
const labels = ['
|
|
182
|
+
const labels = ['SPF', 'DKIM', 'DMARC'];
|
|
183
183
|
return labels[index] || `Record ${index + 1}`;
|
|
184
184
|
}
|
|
185
185
|
}
|
|
@@ -194,8 +194,6 @@ export default class Edit extends BaseCommand {
|
|
|
194
194
|
email.subject = updated.subject;
|
|
195
195
|
if (updated.previewText)
|
|
196
196
|
email.previewText = updated.previewText;
|
|
197
|
-
if (updated.ctaText)
|
|
198
|
-
email.ctaText = updated.ctaText;
|
|
199
197
|
const updatedYaml = {
|
|
200
198
|
...yamlConfig,
|
|
201
199
|
emails: [...yamlConfig.emails],
|
|
@@ -128,7 +128,6 @@ export default class Init extends BaseCommand {
|
|
|
128
128
|
...(generated?.previewText
|
|
129
129
|
? { previewText: generated.previewText }
|
|
130
130
|
: {}),
|
|
131
|
-
...(generated?.ctaText ? { ctaText: generated.ctaText } : {}),
|
|
132
131
|
goal: rec.goal,
|
|
133
132
|
};
|
|
134
133
|
});
|
|
@@ -136,18 +135,14 @@ export default class Init extends BaseCommand {
|
|
|
136
135
|
emails: emailConfigs,
|
|
137
136
|
project: {
|
|
138
137
|
brandColor: analysisPayload.brand?.color || DEFAULT_BRAND_COLOR,
|
|
139
|
-
description: analysisPayload.description,
|
|
140
138
|
emailStyle: 'branded',
|
|
141
139
|
fromEmail: '',
|
|
142
140
|
fromName: `Team ${analysisPayload.productName}`,
|
|
143
141
|
logoUrl: analysisPayload.brand?.logoUrl || '',
|
|
144
142
|
monthlyCap: DEFAULT_MONTHLY_CAP,
|
|
145
143
|
name: analysisPayload.productName,
|
|
146
|
-
pricingModel: analysisPayload.pricingModel,
|
|
147
144
|
replyTo: '',
|
|
148
|
-
|
|
149
|
-
targetUser: analysisPayload.targetUser,
|
|
150
|
-
type: analysisPayload.businessType,
|
|
145
|
+
type: analysisPayload.pricingModel,
|
|
151
146
|
url: productUrl,
|
|
152
147
|
webhookUrl: '',
|
|
153
148
|
},
|
|
@@ -5,6 +5,8 @@ export default class Logs extends BaseCommand {
|
|
|
5
5
|
static flags: {
|
|
6
6
|
email: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
7
|
failed: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
limit: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
page: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
10
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
11
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
12
|
};
|
|
@@ -17,11 +17,22 @@ export default class Logs extends BaseCommand {
|
|
|
17
17
|
default: false,
|
|
18
18
|
description: 'Show only failed/bounced events',
|
|
19
19
|
}),
|
|
20
|
+
limit: Flags.integer({
|
|
21
|
+
default: 50,
|
|
22
|
+
description: 'Entries per page (max 200)',
|
|
23
|
+
}),
|
|
24
|
+
page: Flags.integer({
|
|
25
|
+
default: 1,
|
|
26
|
+
description: 'Page number',
|
|
27
|
+
}),
|
|
20
28
|
};
|
|
21
29
|
async run() {
|
|
22
30
|
const { flags } = await this.parse(Logs);
|
|
23
31
|
await this.ensureAuth();
|
|
24
|
-
const params = {
|
|
32
|
+
const params = {
|
|
33
|
+
limit: String(flags.limit),
|
|
34
|
+
page: String(flags.page),
|
|
35
|
+
};
|
|
25
36
|
if (flags.email)
|
|
26
37
|
params.email = flags.email;
|
|
27
38
|
if (flags.failed)
|
|
@@ -30,7 +41,7 @@ export default class Logs extends BaseCommand {
|
|
|
30
41
|
if (!response.ok) {
|
|
31
42
|
this.handleApiError(response);
|
|
32
43
|
}
|
|
33
|
-
const { entries } = response.data;
|
|
44
|
+
const { entries, limit, page, total } = response.data;
|
|
34
45
|
if (flags.json) {
|
|
35
46
|
this.log(JSON.stringify(response.data, null, 2));
|
|
36
47
|
return;
|
|
@@ -49,6 +60,11 @@ export default class Logs extends BaseCommand {
|
|
|
49
60
|
this.log(` ${' '.repeat(52)}${chalk.dim(`(reason: ${entry.reason})`)}`);
|
|
50
61
|
}
|
|
51
62
|
}
|
|
63
|
+
const totalPages = Math.ceil(total / limit);
|
|
64
|
+
this.log(`\n Page ${page} of ${totalPages} · ${total} total entries`);
|
|
65
|
+
if (page < totalPages) {
|
|
66
|
+
this.log(` ${chalk.dim(`Next: --page ${page + 1}`)}`);
|
|
67
|
+
}
|
|
52
68
|
}
|
|
53
69
|
else {
|
|
54
70
|
this.log(` ${chalk.dim('No log entries found.')}`);
|
|
@@ -8,6 +8,8 @@ export default class Settings extends BaseCommand {
|
|
|
8
8
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
9
|
};
|
|
10
10
|
run(): Promise<void>;
|
|
11
|
+
private applySetFlag;
|
|
12
|
+
private displaySettingsGroup;
|
|
11
13
|
/**
|
|
12
14
|
* Prompts the user to pick a setting key to edit and dispatches
|
|
13
15
|
* to the appropriate handler for that key.
|
|
@@ -43,73 +43,85 @@ export default class Settings extends BaseCommand {
|
|
|
43
43
|
async run() {
|
|
44
44
|
const { flags } = await this.parse(Settings);
|
|
45
45
|
const yamlConfig = await this.ensureYaml();
|
|
46
|
-
const { project } = yamlConfig;
|
|
47
46
|
if (flags.set) {
|
|
48
|
-
|
|
49
|
-
if (eqIndex === -1) {
|
|
50
|
-
this.error('Invalid format. Use --set key=value (e.g., --set brand_color=#0F3460)');
|
|
51
|
-
}
|
|
52
|
-
const key = flags.set.slice(0, eqIndex).trim();
|
|
53
|
-
const propKey = settingKeyToProp(key);
|
|
54
|
-
const value = flags.set.slice(eqIndex + 1).trim();
|
|
55
|
-
if (!(propKey in project)) {
|
|
56
|
-
this.error(`Unknown setting: ${key}`);
|
|
57
|
-
}
|
|
58
|
-
project[propKey] =
|
|
59
|
-
propKey === 'monthlyCap' ? Number(value) : value;
|
|
60
|
-
await saveYaml(yamlConfig);
|
|
61
|
-
if (flags.json) {
|
|
62
|
-
this.log(JSON.stringify({ [propKey]: value, status: 'updated' }, null, 2));
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
this.log(`\n ${chalk.green('✓')} ${key} updated to ${chalk.cyan(value)}`);
|
|
66
|
-
this.log(` Run ${chalk.cyan("'mailmodo deploy'")} to apply.\n`);
|
|
47
|
+
await this.applySetFlag(flags.set, yamlConfig, flags.json ?? false);
|
|
67
48
|
return;
|
|
68
49
|
}
|
|
69
50
|
if (flags.json) {
|
|
70
|
-
this.log(JSON.stringify({ settings: project }, null, 2));
|
|
51
|
+
this.log(JSON.stringify({ settings: yamlConfig.project }, null, 2));
|
|
71
52
|
return;
|
|
72
53
|
}
|
|
73
|
-
const domainVerified = await this.fetchDomainVerified(project.domain);
|
|
74
|
-
this.log(`\n Current settings for ${chalk.bold(project.name || 'project')}:\n`);
|
|
54
|
+
const domainVerified = await this.fetchDomainVerified(yamlConfig.project.domain);
|
|
55
|
+
this.log(`\n Current settings for ${chalk.bold(yamlConfig.project.name || 'project')}:\n`);
|
|
75
56
|
for (const [group, keys] of Object.entries(SETTINGS_GROUPS)) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
57
|
+
this.displaySettingsGroup(group, keys, yamlConfig.project, domainVerified);
|
|
58
|
+
}
|
|
59
|
+
if (!flags.yes) {
|
|
60
|
+
await this.promptEditSetting(yamlConfig);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async applySetFlag(setFlag, yamlConfig, isJson) {
|
|
64
|
+
const { project } = yamlConfig;
|
|
65
|
+
const eqIndex = setFlag.indexOf('=');
|
|
66
|
+
if (eqIndex === -1) {
|
|
67
|
+
this.error('Invalid format. Use --set key=value (e.g., --set brand_color=#0F3460)');
|
|
68
|
+
}
|
|
69
|
+
const key = setFlag.slice(0, eqIndex).trim();
|
|
70
|
+
const propKey = settingKeyToProp(key);
|
|
71
|
+
const value = setFlag.slice(eqIndex + 1).trim();
|
|
72
|
+
if (!(propKey in project) && key !== 'logo_file') {
|
|
73
|
+
this.error(`Unknown setting: ${key}`);
|
|
74
|
+
}
|
|
75
|
+
project[propKey] =
|
|
76
|
+
propKey === 'monthlyCap' ? Number(value) : value;
|
|
77
|
+
await saveYaml(yamlConfig);
|
|
78
|
+
if (isJson) {
|
|
79
|
+
this.log(JSON.stringify({ [propKey]: value, status: 'updated' }, null, 2));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
this.log(`\n ${chalk.green('✓')} ${key} updated to ${chalk.cyan(value)}`);
|
|
83
|
+
this.log(` Run ${chalk.cyan("'mailmodo deploy'")} to apply.\n`);
|
|
84
|
+
}
|
|
85
|
+
displaySettingsGroup(group, keys, project, domainVerified) {
|
|
86
|
+
const availableKeys = keys.filter((key) => {
|
|
87
|
+
if (group === 'brand' && key === 'logo_file')
|
|
88
|
+
return true;
|
|
89
|
+
return settingKeyToProp(key) in project;
|
|
90
|
+
});
|
|
91
|
+
const groupTitle = ` ${chalk.bold(group.charAt(0).toUpperCase() + group.slice(1))}`;
|
|
92
|
+
if (availableKeys.length === 0) {
|
|
93
|
+
const hint = SETUP_HINTS[settingKeyToProp(keys[0])];
|
|
94
|
+
if (hint) {
|
|
95
|
+
this.log(groupTitle);
|
|
96
|
+
this.log(` ${'─'.repeat(49)}`);
|
|
97
|
+
this.log(` ${chalk.dim(`Run ${hint} to configure.`)}`);
|
|
98
|
+
this.log('');
|
|
86
99
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
displayValue += ` ${chalk.red('✗ not verified')}`;
|
|
98
|
-
}
|
|
99
|
-
this.log(` ${key.padEnd(16)} ${displayValue}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.log(groupTitle);
|
|
103
|
+
this.log(` ${'─'.repeat(49)}`);
|
|
104
|
+
for (const key of availableKeys) {
|
|
105
|
+
const propKey = settingKeyToProp(key);
|
|
106
|
+
const value = project[propKey];
|
|
107
|
+
let displayValue = value ? String(value) : chalk.dim('(not set)');
|
|
108
|
+
if (key === 'domain' && value && domainVerified === true) {
|
|
109
|
+
displayValue += ` ${chalk.green('✓ verified')}`;
|
|
100
110
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const hint = SETUP_HINTS[settingKeyToProp(key)];
|
|
104
|
-
if (hint) {
|
|
105
|
-
this.log(` ${key.padEnd(16)} ${chalk.dim(`(run ${hint} to set up)`)}`);
|
|
106
|
-
}
|
|
111
|
+
else if (key === 'domain' && value && domainVerified === false) {
|
|
112
|
+
displayValue += ` ${chalk.red('✗ not verified')}`;
|
|
107
113
|
}
|
|
108
|
-
this.log(
|
|
114
|
+
this.log(` ${key.padEnd(16)} ${displayValue}`);
|
|
109
115
|
}
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
const missingKeys = keys.filter((key) => !(settingKeyToProp(key) in project) &&
|
|
117
|
+
!(group === 'brand' && key === 'logo_file'));
|
|
118
|
+
for (const key of missingKeys) {
|
|
119
|
+
const hint = SETUP_HINTS[settingKeyToProp(key)];
|
|
120
|
+
if (hint) {
|
|
121
|
+
this.log(` ${key.padEnd(16)} ${chalk.dim(`(run ${hint} to set up)`)}`);
|
|
122
|
+
}
|
|
112
123
|
}
|
|
124
|
+
this.log('');
|
|
113
125
|
}
|
|
114
126
|
/**
|
|
115
127
|
* Prompts the user to pick a setting key to edit and dispatches
|
|
@@ -125,6 +137,10 @@ export default class Settings extends BaseCommand {
|
|
|
125
137
|
return;
|
|
126
138
|
const editPropKey = settingKeyToProp(editKey);
|
|
127
139
|
if (!(editPropKey in project)) {
|
|
140
|
+
if (editKey === 'logo_file') {
|
|
141
|
+
await this.handleLogoUpload(yamlConfig);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
128
144
|
const hint = SETUP_HINTS[editPropKey];
|
|
129
145
|
if (hint) {
|
|
130
146
|
this.log(`\n ${editKey} is not configured yet. Run ${chalk.cyan(hint)} to set it up.\n`);
|
|
@@ -233,7 +249,7 @@ export default class Settings extends BaseCommand {
|
|
|
233
249
|
this.log(` Help: ${chalk.cyan(DNS_GUIDE_URL)}\n`);
|
|
234
250
|
}
|
|
235
251
|
recordLabel(index) {
|
|
236
|
-
const labels = ['
|
|
252
|
+
const labels = ['SPF', 'DKIM', 'DMARC'];
|
|
237
253
|
return labels[index] || `Record ${index + 1}`;
|
|
238
254
|
}
|
|
239
255
|
/**
|
|
@@ -252,8 +268,16 @@ export default class Settings extends BaseCommand {
|
|
|
252
268
|
}
|
|
253
269
|
await this.ensureAuth();
|
|
254
270
|
const fileBuffer = await readFile(resolvedPath);
|
|
271
|
+
const ext = resolvedPath.split('.').pop()?.toLowerCase();
|
|
272
|
+
const mimeTypes = {
|
|
273
|
+
png: 'image/png',
|
|
274
|
+
jpg: 'image/jpeg',
|
|
275
|
+
jpeg: 'image/jpeg',
|
|
276
|
+
svg: 'image/svg+xml',
|
|
277
|
+
};
|
|
278
|
+
const mimeType = mimeTypes[ext ?? ''] ?? 'application/octet-stream';
|
|
255
279
|
const formData = new FormData();
|
|
256
|
-
formData.append('logo', new Blob([new Uint8Array(fileBuffer)]), logoPath.split(/[/\\]/).pop() || 'logo.png');
|
|
280
|
+
formData.append('logo', new Blob([new Uint8Array(fileBuffer)], { type: mimeType }), logoPath.split(/[/\\]/).pop() || 'logo.png');
|
|
257
281
|
const response = await this.apiClient.postFormData(API_ENDPOINTS.ASSETS_LOGO, formData);
|
|
258
282
|
if (!response.ok) {
|
|
259
283
|
this.handleApiError(response);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export interface EmailConfig {
|
|
2
2
|
condition?: string;
|
|
3
|
-
ctaText?: string;
|
|
4
3
|
delay: number | string;
|
|
5
4
|
goal?: string;
|
|
6
5
|
id: string;
|
|
@@ -13,7 +12,6 @@ export interface EmailConfig {
|
|
|
13
12
|
export interface ProjectConfig {
|
|
14
13
|
address?: string;
|
|
15
14
|
brandColor?: string;
|
|
16
|
-
description?: string;
|
|
17
15
|
domain?: string;
|
|
18
16
|
emailStyle?: 'branded' | 'plain';
|
|
19
17
|
fromEmail?: string;
|
|
@@ -22,10 +20,7 @@ export interface ProjectConfig {
|
|
|
22
20
|
logoUrl?: string;
|
|
23
21
|
monthlyCap?: number;
|
|
24
22
|
name?: string;
|
|
25
|
-
pricingModel?: string;
|
|
26
23
|
replyTo?: string;
|
|
27
|
-
saasModel?: string;
|
|
28
|
-
targetUser?: string;
|
|
29
24
|
type?: string;
|
|
30
25
|
url?: string;
|
|
31
26
|
webhookUrl?: string;
|
package/oclif.manifest.json
CHANGED
|
@@ -342,12 +342,13 @@
|
|
|
342
342
|
"index.js"
|
|
343
343
|
]
|
|
344
344
|
},
|
|
345
|
-
"
|
|
345
|
+
"login": {
|
|
346
346
|
"aliases": [],
|
|
347
347
|
"args": {},
|
|
348
|
-
"description": "
|
|
348
|
+
"description": "Authenticate with Mailmodo using your API key",
|
|
349
349
|
"examples": [
|
|
350
|
-
"<%= config.bin %>
|
|
350
|
+
"<%= config.bin %> login",
|
|
351
|
+
"MAILMODO_API_KEY=mm_live_xxx <%= config.bin %> login"
|
|
351
352
|
],
|
|
352
353
|
"flags": {
|
|
353
354
|
"json": {
|
|
@@ -366,7 +367,7 @@
|
|
|
366
367
|
},
|
|
367
368
|
"hasDynamicHelp": false,
|
|
368
369
|
"hiddenAliases": [],
|
|
369
|
-
"id": "
|
|
370
|
+
"id": "login",
|
|
370
371
|
"pluginAlias": "@mailmodo/cli",
|
|
371
372
|
"pluginName": "@mailmodo/cli",
|
|
372
373
|
"pluginType": "core",
|
|
@@ -376,19 +377,16 @@
|
|
|
376
377
|
"relativePath": [
|
|
377
378
|
"dist",
|
|
378
379
|
"commands",
|
|
379
|
-
"
|
|
380
|
+
"login",
|
|
380
381
|
"index.js"
|
|
381
382
|
]
|
|
382
383
|
},
|
|
383
|
-
"
|
|
384
|
+
"logout": {
|
|
384
385
|
"aliases": [],
|
|
385
386
|
"args": {},
|
|
386
|
-
"description": "
|
|
387
|
+
"description": "Sign out by removing saved credentials from this machine",
|
|
387
388
|
"examples": [
|
|
388
|
-
"<%= config.bin %>
|
|
389
|
-
"<%= config.bin %> logs --email sarah@example.com",
|
|
390
|
-
"<%= config.bin %> logs --failed",
|
|
391
|
-
"<%= config.bin %> logs --json"
|
|
389
|
+
"<%= config.bin %> logout"
|
|
392
390
|
],
|
|
393
391
|
"flags": {
|
|
394
392
|
"json": {
|
|
@@ -403,24 +401,11 @@
|
|
|
403
401
|
"name": "yes",
|
|
404
402
|
"allowNo": false,
|
|
405
403
|
"type": "boolean"
|
|
406
|
-
},
|
|
407
|
-
"email": {
|
|
408
|
-
"description": "Filter logs by contact email",
|
|
409
|
-
"name": "email",
|
|
410
|
-
"hasDynamicHelp": false,
|
|
411
|
-
"multiple": false,
|
|
412
|
-
"type": "option"
|
|
413
|
-
},
|
|
414
|
-
"failed": {
|
|
415
|
-
"description": "Show only failed/bounced events",
|
|
416
|
-
"name": "failed",
|
|
417
|
-
"allowNo": false,
|
|
418
|
-
"type": "boolean"
|
|
419
404
|
}
|
|
420
405
|
},
|
|
421
406
|
"hasDynamicHelp": false,
|
|
422
407
|
"hiddenAliases": [],
|
|
423
|
-
"id": "
|
|
408
|
+
"id": "logout",
|
|
424
409
|
"pluginAlias": "@mailmodo/cli",
|
|
425
410
|
"pluginName": "@mailmodo/cli",
|
|
426
411
|
"pluginType": "core",
|
|
@@ -430,17 +415,19 @@
|
|
|
430
415
|
"relativePath": [
|
|
431
416
|
"dist",
|
|
432
417
|
"commands",
|
|
433
|
-
"
|
|
418
|
+
"logout",
|
|
434
419
|
"index.js"
|
|
435
420
|
]
|
|
436
421
|
},
|
|
437
|
-
"
|
|
422
|
+
"logs": {
|
|
438
423
|
"aliases": [],
|
|
439
424
|
"args": {},
|
|
440
|
-
"description": "
|
|
425
|
+
"description": "View email send logs and delivery events",
|
|
441
426
|
"examples": [
|
|
442
|
-
"<%= config.bin %>
|
|
443
|
-
"
|
|
427
|
+
"<%= config.bin %> logs",
|
|
428
|
+
"<%= config.bin %> logs --email sarah@example.com",
|
|
429
|
+
"<%= config.bin %> logs --failed",
|
|
430
|
+
"<%= config.bin %> logs --json"
|
|
444
431
|
],
|
|
445
432
|
"flags": {
|
|
446
433
|
"json": {
|
|
@@ -455,11 +442,40 @@
|
|
|
455
442
|
"name": "yes",
|
|
456
443
|
"allowNo": false,
|
|
457
444
|
"type": "boolean"
|
|
445
|
+
},
|
|
446
|
+
"email": {
|
|
447
|
+
"description": "Filter logs by contact email",
|
|
448
|
+
"name": "email",
|
|
449
|
+
"hasDynamicHelp": false,
|
|
450
|
+
"multiple": false,
|
|
451
|
+
"type": "option"
|
|
452
|
+
},
|
|
453
|
+
"failed": {
|
|
454
|
+
"description": "Show only failed/bounced events",
|
|
455
|
+
"name": "failed",
|
|
456
|
+
"allowNo": false,
|
|
457
|
+
"type": "boolean"
|
|
458
|
+
},
|
|
459
|
+
"limit": {
|
|
460
|
+
"description": "Entries per page (max 200)",
|
|
461
|
+
"name": "limit",
|
|
462
|
+
"default": 50,
|
|
463
|
+
"hasDynamicHelp": false,
|
|
464
|
+
"multiple": false,
|
|
465
|
+
"type": "option"
|
|
466
|
+
},
|
|
467
|
+
"page": {
|
|
468
|
+
"description": "Page number",
|
|
469
|
+
"name": "page",
|
|
470
|
+
"default": 1,
|
|
471
|
+
"hasDynamicHelp": false,
|
|
472
|
+
"multiple": false,
|
|
473
|
+
"type": "option"
|
|
458
474
|
}
|
|
459
475
|
},
|
|
460
476
|
"hasDynamicHelp": false,
|
|
461
477
|
"hiddenAliases": [],
|
|
462
|
-
"id": "
|
|
478
|
+
"id": "logs",
|
|
463
479
|
"pluginAlias": "@mailmodo/cli",
|
|
464
480
|
"pluginName": "@mailmodo/cli",
|
|
465
481
|
"pluginType": "core",
|
|
@@ -469,7 +485,7 @@
|
|
|
469
485
|
"relativePath": [
|
|
470
486
|
"dist",
|
|
471
487
|
"commands",
|
|
472
|
-
"
|
|
488
|
+
"logs",
|
|
473
489
|
"index.js"
|
|
474
490
|
]
|
|
475
491
|
},
|
|
@@ -618,5 +634,5 @@
|
|
|
618
634
|
]
|
|
619
635
|
}
|
|
620
636
|
},
|
|
621
|
-
"version": "0.0.
|
|
637
|
+
"version": "0.0.22"
|
|
622
638
|
}
|