@mailmodo/cli 0.0.55-beta.pr57.93 → 0.0.55
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 +11 -1
- package/dist/commands/billing/index.js +184 -28
- package/dist/commands/contacts/index.d.ts +19 -1
- package/dist/commands/contacts/index.js +114 -21
- package/dist/commands/deploy/index.js +4 -4
- package/dist/commands/deployments/index.d.ts +4 -1
- package/dist/commands/deployments/index.js +52 -11
- package/dist/commands/domain/index.d.ts +14 -1
- package/dist/commands/domain/index.js +100 -19
- package/dist/commands/edit/index.d.ts +20 -2
- package/dist/commands/edit/index.js +258 -30
- package/dist/commands/emails/index.d.ts +2 -1
- package/dist/commands/emails/index.js +91 -26
- package/dist/commands/init/index.d.ts +3 -1
- package/dist/commands/init/index.js +199 -41
- package/dist/commands/login/index.d.ts +0 -2
- package/dist/commands/login/index.js +76 -32
- package/dist/commands/logs/index.d.ts +8 -1
- package/dist/commands/logs/index.js +55 -12
- package/dist/commands/preview/index.d.ts +19 -1
- package/dist/commands/preview/index.js +212 -30
- package/dist/commands/sdk/index.d.ts +3 -1
- package/dist/commands/sdk/index.js +46 -14
- package/dist/commands/settings/index.d.ts +22 -1
- package/dist/commands/settings/index.js +246 -34
- package/dist/commands/status/index.d.ts +0 -1
- package/dist/commands/status/index.js +39 -13
- package/dist/lib/{commands/deploy → deploy}/domain-setup.d.ts +1 -1
- package/dist/lib/{commands/deploy → deploy}/domain-setup.js +2 -2
- package/dist/lib/{commands/deploy → deploy}/output.d.ts +1 -1
- package/dist/lib/{commands/deploy → deploy}/output.js +2 -2
- package/dist/lib/{commands/deploy → deploy}/payload.d.ts +1 -1
- package/dist/lib/{commands/deploy → deploy}/payload.js +2 -2
- package/dist/lib/{commands/deploy → deploy}/sequence-status.js +2 -2
- package/dist/lib/{commands/deploy → deploy}/types.d.ts +4 -4
- package/dist/lib/templates/missing-templates.d.ts +1 -1
- package/dist/lib/templates/missing-templates.js +1 -1
- package/oclif.manifest.json +54 -54
- package/package.json +1 -1
- package/dist/lib/commands/billing/checkout-status.d.ts +0 -3
- package/dist/lib/commands/billing/checkout-status.js +0 -63
- package/dist/lib/commands/billing/format.d.ts +0 -7
- package/dist/lib/commands/billing/format.js +0 -63
- package/dist/lib/commands/billing/purchase-cap.d.ts +0 -7
- package/dist/lib/commands/billing/purchase-cap.js +0 -57
- package/dist/lib/commands/billing/types.d.ts +0 -72
- package/dist/lib/commands/contacts/actions.d.ts +0 -3
- package/dist/lib/commands/contacts/actions.js +0 -49
- package/dist/lib/commands/contacts/export-delete.d.ts +0 -9
- package/dist/lib/commands/contacts/export-delete.js +0 -51
- package/dist/lib/commands/contacts/types.d.ts +0 -35
- package/dist/lib/commands/contacts/types.js +0 -1
- package/dist/lib/commands/deploy/types.js +0 -1
- package/dist/lib/commands/deployments/output.d.ts +0 -2
- package/dist/lib/commands/deployments/output.js +0 -68
- package/dist/lib/commands/deployments/types.d.ts +0 -24
- package/dist/lib/commands/deployments/types.js +0 -1
- package/dist/lib/commands/domain/setup.d.ts +0 -8
- package/dist/lib/commands/domain/setup.js +0 -53
- package/dist/lib/commands/domain/types.d.ts +0 -56
- package/dist/lib/commands/domain/types.js +0 -1
- package/dist/lib/commands/domain/verify.d.ts +0 -5
- package/dist/lib/commands/domain/verify.js +0 -50
- package/dist/lib/commands/edit/diff.d.ts +0 -7
- package/dist/lib/commands/edit/diff.js +0 -65
- package/dist/lib/commands/edit/display.d.ts +0 -5
- package/dist/lib/commands/edit/display.js +0 -53
- package/dist/lib/commands/edit/flow.d.ts +0 -8
- package/dist/lib/commands/edit/flow.js +0 -70
- package/dist/lib/commands/edit/persist.d.ts +0 -5
- package/dist/lib/commands/edit/persist.js +0 -65
- package/dist/lib/commands/edit/types.d.ts +0 -37
- package/dist/lib/commands/edit/types.js +0 -1
- package/dist/lib/commands/emails/editor.d.ts +0 -2
- package/dist/lib/commands/emails/editor.js +0 -43
- package/dist/lib/commands/emails/output.d.ts +0 -4
- package/dist/lib/commands/emails/output.js +0 -36
- package/dist/lib/commands/emails/types.d.ts +0 -3
- package/dist/lib/commands/emails/types.js +0 -1
- package/dist/lib/commands/init/analysis.d.ts +0 -3
- package/dist/lib/commands/init/analysis.js +0 -69
- package/dist/lib/commands/init/output.d.ts +0 -12
- package/dist/lib/commands/init/output.js +0 -39
- package/dist/lib/commands/init/payload.d.ts +0 -8
- package/dist/lib/commands/init/payload.js +0 -78
- package/dist/lib/commands/init/types.d.ts +0 -57
- package/dist/lib/commands/init/types.js +0 -1
- package/dist/lib/commands/login/output.d.ts +0 -8
- package/dist/lib/commands/login/output.js +0 -53
- package/dist/lib/commands/login/types.d.ts +0 -19
- package/dist/lib/commands/login/types.js +0 -1
- package/dist/lib/commands/logs/output.d.ts +0 -2
- package/dist/lib/commands/logs/output.js +0 -52
- package/dist/lib/commands/logs/types.d.ts +0 -23
- package/dist/lib/commands/logs/types.js +0 -1
- package/dist/lib/commands/preview/actions.d.ts +0 -11
- package/dist/lib/commands/preview/actions.js +0 -43
- package/dist/lib/commands/preview/render.d.ts +0 -3
- package/dist/lib/commands/preview/render.js +0 -30
- package/dist/lib/commands/preview/server.d.ts +0 -8
- package/dist/lib/commands/preview/server.js +0 -63
- package/dist/lib/commands/preview/types.d.ts +0 -19
- package/dist/lib/commands/preview/types.js +0 -1
- package/dist/lib/commands/preview/wrapper-html.d.ts +0 -2
- package/dist/lib/commands/preview/wrapper-html.js +0 -35
- package/dist/lib/commands/sdk/output.d.ts +0 -2
- package/dist/lib/commands/sdk/output.js +0 -42
- package/dist/lib/commands/sdk/types.d.ts +0 -21
- package/dist/lib/commands/sdk/types.js +0 -1
- package/dist/lib/commands/settings/actions.d.ts +0 -10
- package/dist/lib/commands/settings/actions.js +0 -56
- package/dist/lib/commands/settings/display.d.ts +0 -15
- package/dist/lib/commands/settings/display.js +0 -69
- package/dist/lib/commands/settings/logo-domain.d.ts +0 -3
- package/dist/lib/commands/settings/logo-domain.js +0 -47
- package/dist/lib/commands/settings/prompt.d.ts +0 -2
- package/dist/lib/commands/settings/prompt.js +0 -82
- package/dist/lib/commands/settings/types.d.ts +0 -65
- package/dist/lib/commands/settings/types.js +0 -1
- package/dist/lib/commands/status/output.d.ts +0 -2
- package/dist/lib/commands/status/output.js +0 -49
- package/dist/lib/commands/status/types.d.ts +0 -28
- package/dist/lib/commands/status/types.js +0 -1
- /package/dist/lib/{commands/deploy → deploy}/sequence-status.d.ts +0 -0
- /package/dist/lib/{commands/billing → deploy}/types.js +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
+
import { input } from '@inquirer/prompts';
|
|
3
|
+
import chalk from 'chalk';
|
|
2
4
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
+
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
6
|
+
import { ERRORS, INFO, PROMPTS, SEPARATOR } from '../../lib/messages.js';
|
|
5
7
|
export default class Domain extends BaseCommand {
|
|
6
8
|
static description = 'Set up and verify your sending domain';
|
|
7
9
|
static examples = [
|
|
@@ -23,32 +25,111 @@ export default class Domain extends BaseCommand {
|
|
|
23
25
|
async run() {
|
|
24
26
|
const { flags } = await this.parse(Domain);
|
|
25
27
|
await this.ensureAuth();
|
|
26
|
-
const ctx = this.makeCtx();
|
|
27
28
|
if (flags.verify) {
|
|
28
29
|
const yamlConfig = await this.ensureYaml();
|
|
29
|
-
const domain =
|
|
30
|
-
|
|
30
|
+
const domain = yamlConfig.project?.domain;
|
|
31
|
+
if (!domain) {
|
|
32
|
+
this.error(ERRORS.DOMAIN_NOT_CONFIGURED);
|
|
33
|
+
}
|
|
34
|
+
await this.verifyDomain(flags.json, domain);
|
|
31
35
|
return;
|
|
32
36
|
}
|
|
33
37
|
if (flags.status) {
|
|
34
38
|
const yamlConfig = await this.ensureYaml();
|
|
35
|
-
const domain =
|
|
36
|
-
|
|
39
|
+
const domain = yamlConfig.project?.domain;
|
|
40
|
+
if (!domain) {
|
|
41
|
+
this.error(ERRORS.DOMAIN_NOT_CONFIGURED);
|
|
42
|
+
}
|
|
43
|
+
await this.showDomainStatus(flags.json, domain);
|
|
37
44
|
return;
|
|
38
45
|
}
|
|
46
|
+
await this.setupDomain(flags);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Interactive domain setup: collects domain, sender email, and business address,
|
|
50
|
+
* then calls the API to retrieve the required DNS records.
|
|
51
|
+
*/
|
|
52
|
+
async setupDomain(flags) {
|
|
39
53
|
const yamlConfig = await this.ensureYaml();
|
|
40
|
-
|
|
54
|
+
this.log(`\n ${SEPARATOR}`);
|
|
55
|
+
this.log(` ${chalk.bold('DOMAIN SETUP')}`);
|
|
56
|
+
this.log(` ${SEPARATOR}\n`);
|
|
57
|
+
const inputs = await this.collectDomainSetupInputs(yamlConfig, flags.yes);
|
|
58
|
+
const { dnsRecords, dnsGuideUrl } = await this.registerDomain(yamlConfig, inputs, flags.json);
|
|
59
|
+
if (flags.json) {
|
|
60
|
+
this.log(JSON.stringify({ dnsRecords, domain: inputs.domain }, null, 2));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.logDnsRecords(dnsRecords, dnsGuideUrl, flags.json);
|
|
64
|
+
if (!flags.yes) {
|
|
65
|
+
const action = await input({
|
|
66
|
+
default: '',
|
|
67
|
+
message: PROMPTS.ENTER_AFTER_RECORDS,
|
|
68
|
+
});
|
|
69
|
+
if (action.toLowerCase() !== 'skip') {
|
|
70
|
+
await this.verifyDomain(false, inputs.domain);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
41
73
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
};
|
|
74
|
+
/**
|
|
75
|
+
* Calls the domain verification API and displays pass/fail for each DNS record.
|
|
76
|
+
*/
|
|
77
|
+
async verifyDomain(jsonOutput, domain) {
|
|
78
|
+
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Checking DNS...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY, {
|
|
79
|
+
domain,
|
|
80
|
+
}));
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
this.handleApiError(response);
|
|
83
|
+
}
|
|
84
|
+
const { dkim, dmarc, dnsGuideUrl, returnPath, domainStatus } = response.data;
|
|
85
|
+
if (jsonOutput) {
|
|
86
|
+
this.log(JSON.stringify({ dkim, dmarc, returnPath, domainStatus }, null, 2));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this.log(` DKIM ${dkim ? chalk.green('✓') : chalk.red('✗ Not found')}`);
|
|
90
|
+
this.log(` DMARC ${dmarc ? chalk.green('✓') : chalk.red('✗ Not found')}`);
|
|
91
|
+
this.log(` Return Path ${returnPath ? chalk.green('✓') : chalk.red('✗ Not found')}`);
|
|
92
|
+
const allPassed = domainStatus === 'VERIFIED';
|
|
93
|
+
if (allPassed) {
|
|
94
|
+
this.log(`\n ${chalk.green('✓')} Domain verified.\n`);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
this.log(`\n ${INFO.DNS_RECORDS_FAILED}`);
|
|
98
|
+
if (!dkim) {
|
|
99
|
+
this.log(`\n DKIM common mistakes:`);
|
|
100
|
+
this.log(` - Using CNAME instead of TXT record type`);
|
|
101
|
+
this.log(` - Including the full domain in the Host field`);
|
|
102
|
+
this.log(` - Cloudflare: proxy must be OFF (grey cloud, not orange)`);
|
|
103
|
+
}
|
|
104
|
+
if (!returnPath) {
|
|
105
|
+
this.log(`\n Return Path common mistakes:`);
|
|
106
|
+
this.log(` - Missing or incorrect CNAME for mm-bounce subdomain`);
|
|
107
|
+
this.log(` - Cloudflare: proxy must be OFF (grey cloud, not orange)`);
|
|
108
|
+
}
|
|
109
|
+
this.log(`\n ${INFO.DNS_FIX_AND_VERIFY}`);
|
|
110
|
+
if (dnsGuideUrl)
|
|
111
|
+
this.log(` Help: ${chalk.cyan(dnsGuideUrl)}\n`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Displays domain health metrics including verification status,
|
|
116
|
+
* bounce rate, and spam complaint rate.
|
|
117
|
+
*/
|
|
118
|
+
async showDomainStatus(jsonOutput, domain) {
|
|
119
|
+
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Loading domain status...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_STATUS, {
|
|
120
|
+
domain,
|
|
121
|
+
}));
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
this.handleApiError(response);
|
|
124
|
+
}
|
|
125
|
+
const { data } = response;
|
|
126
|
+
if (jsonOutput) {
|
|
127
|
+
this.log(JSON.stringify(data, null, 2));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
this.log(`\n Domain: ${chalk.bold(data.domain || 'not configured')}`);
|
|
131
|
+
this.log(` Status: ${data.verified ? chalk.green('✓ verified') : chalk.red('✗ not verified')}`);
|
|
132
|
+
this.log(` Bounce rate: ${data.bounceRate ?? 'N/A'}%`);
|
|
133
|
+
this.log(` Spam rate: ${data.spamRate ?? 'N/A'}%\n`);
|
|
53
134
|
}
|
|
54
135
|
}
|
|
@@ -11,6 +11,24 @@ export default class Edit extends BaseCommand {
|
|
|
11
11
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
12
|
};
|
|
13
13
|
run(): Promise<void>;
|
|
14
|
-
private
|
|
15
|
-
private
|
|
14
|
+
private runEditStep;
|
|
15
|
+
private handleUserAction;
|
|
16
|
+
private callEditApi;
|
|
17
|
+
private finalizeEdit;
|
|
18
|
+
private applyEmailChanges;
|
|
19
|
+
private persistChanges;
|
|
20
|
+
private logJsonResult;
|
|
21
|
+
private handleAcceptOutput;
|
|
22
|
+
private promptEditAction;
|
|
23
|
+
private askChangeDescription;
|
|
24
|
+
private showFieldDiff;
|
|
25
|
+
private stripHtml;
|
|
26
|
+
private truncate;
|
|
27
|
+
private showHtmlChange;
|
|
28
|
+
private showUnchangedField;
|
|
29
|
+
private showUnchangedHtml;
|
|
30
|
+
private showSuggestedChanges;
|
|
31
|
+
private showUnchanged;
|
|
32
|
+
private buildDiffPreview;
|
|
33
|
+
private showChangeSummary;
|
|
16
34
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { confirm, input, select } from '@inquirer/prompts';
|
|
3
|
+
import chalk from 'chalk';
|
|
2
4
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
3
|
-
import {
|
|
5
|
+
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
6
|
+
import { loadTemplate, getTemplateFilename, saveTemplate, saveYaml, } from '../../lib/yaml-config.js';
|
|
4
7
|
import { handleMissingTemplates } from '../../lib/templates/missing-templates.js';
|
|
5
|
-
import { askChangeDescription, runEditStep, } from '../../lib/commands/edit/flow.js';
|
|
6
8
|
export default class Edit extends BaseCommand {
|
|
7
9
|
static args = {
|
|
8
10
|
id: Args.string({
|
|
@@ -31,7 +33,7 @@ export default class Edit extends BaseCommand {
|
|
|
31
33
|
}
|
|
32
34
|
const email = yamlConfig.emails[emailIndex];
|
|
33
35
|
const templateFilename = getTemplateFilename(email.id, email.style, yamlConfig.project?.emailStyle);
|
|
34
|
-
const
|
|
36
|
+
const ctx = {
|
|
35
37
|
email,
|
|
36
38
|
emailIndex,
|
|
37
39
|
templateFilename,
|
|
@@ -42,39 +44,265 @@ export default class Edit extends BaseCommand {
|
|
|
42
44
|
json: flags.json ?? false,
|
|
43
45
|
yes: flags.yes ?? false,
|
|
44
46
|
};
|
|
45
|
-
if (!
|
|
46
|
-
const
|
|
47
|
+
if (!ctx.templateHtml) {
|
|
48
|
+
const regenCtx = {
|
|
49
|
+
error: (msg) => this.error(msg),
|
|
50
|
+
exit: (code) => this.exit(code),
|
|
51
|
+
log: (msg) => this.log(msg),
|
|
52
|
+
onApiError: (r) => this.handleApiError(r),
|
|
53
|
+
post: (path, body) => this.apiClient.post(path, body),
|
|
54
|
+
spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
|
|
55
|
+
syncYaml: () => this.syncYamlToServer(),
|
|
56
|
+
};
|
|
57
|
+
const regenerated = await handleMissingTemplates(regenCtx, yamlConfig, [email.id], editFlags);
|
|
47
58
|
if (!regenerated)
|
|
48
59
|
return;
|
|
49
|
-
|
|
50
|
-
if (!
|
|
60
|
+
ctx.templateHtml = await loadTemplate(templateFilename);
|
|
61
|
+
if (!ctx.templateHtml)
|
|
51
62
|
this.error('Template regeneration failed.');
|
|
52
63
|
}
|
|
53
|
-
const initialChange = flags.change?.trim() || (await askChangeDescription());
|
|
54
|
-
await
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
const initialChange = flags.change?.trim() || (await this.askChangeDescription());
|
|
65
|
+
await this.runEditStep(ctx, initialChange, editFlags);
|
|
66
|
+
}
|
|
67
|
+
async runEditStep(ctx, changeDescription, flags) {
|
|
68
|
+
const response = await this.withApiSpinner({ json: flags.json, text: ' Applying AI edits...' }, () => this.callEditApi(changeDescription, ctx.email, ctx.templateHtml));
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
this.handleAiQuotaError(response, 'edit');
|
|
71
|
+
this.handleApiError(response);
|
|
72
|
+
}
|
|
73
|
+
const updated = response.data;
|
|
74
|
+
if (flags.json) {
|
|
75
|
+
this.log(JSON.stringify(this.buildDiffPreview(ctx.email, updated, ctx.templateHtml), null, 2));
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
this.showChangeSummary(ctx.email, updated, ctx.templateHtml);
|
|
79
|
+
}
|
|
80
|
+
if (flags.yes) {
|
|
81
|
+
await this.finalizeEdit(ctx, updated, flags);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
await this.handleUserAction(ctx, updated, changeDescription, flags);
|
|
85
|
+
}
|
|
86
|
+
async handleUserAction(ctx, updated, changeDescription, flags) {
|
|
87
|
+
this.log('');
|
|
88
|
+
const action = await this.promptEditAction();
|
|
89
|
+
if (action === 'skip') {
|
|
90
|
+
this.log('\n Changes discarded.\n');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (action === 'retry') {
|
|
94
|
+
const newChange = await this.askChangeDescription();
|
|
95
|
+
await this.runEditStep(ctx, newChange, flags);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await this.finalizeEdit(ctx, updated, flags);
|
|
99
|
+
}
|
|
100
|
+
callEditApi(changeDescription, email, templateHtml) {
|
|
101
|
+
return this.apiClient.post(API_ENDPOINTS.EDIT, {
|
|
102
|
+
changeRequest: changeDescription,
|
|
103
|
+
currentEmail: {
|
|
104
|
+
condition: email.condition,
|
|
105
|
+
goal: email.goal,
|
|
106
|
+
html: templateHtml,
|
|
107
|
+
id: email.id,
|
|
108
|
+
previewText: email.previewText,
|
|
109
|
+
subject: email.subject,
|
|
110
|
+
trigger: email.trigger,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async finalizeEdit(ctx, updated, flags) {
|
|
115
|
+
const oldSubject = ctx.email.subject;
|
|
116
|
+
this.applyEmailChanges(ctx.email, updated);
|
|
117
|
+
await this.persistChanges(ctx, updated);
|
|
118
|
+
if (flags.json) {
|
|
119
|
+
this.logJsonResult(ctx.email, updated, oldSubject);
|
|
120
|
+
}
|
|
121
|
+
else if (flags.yes) {
|
|
122
|
+
this.log(`\n Updated ${chalk.green('mailmodo.yaml')}\n`);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
await this.handleAcceptOutput(ctx.email);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
applyEmailChanges(email, updated) {
|
|
129
|
+
if (updated.subject)
|
|
130
|
+
email.subject = updated.subject;
|
|
131
|
+
if (updated.previewText)
|
|
132
|
+
email.previewText = updated.previewText;
|
|
133
|
+
if (updated.ctaText)
|
|
134
|
+
email.ctaText = updated.ctaText;
|
|
135
|
+
}
|
|
136
|
+
async persistChanges(ctx, updated) {
|
|
137
|
+
const updatedYaml = {
|
|
138
|
+
...ctx.yamlConfig,
|
|
139
|
+
emails: [...ctx.yamlConfig.emails],
|
|
67
140
|
};
|
|
141
|
+
updatedYaml.emails[ctx.emailIndex] = ctx.email;
|
|
142
|
+
await saveYaml(updatedYaml);
|
|
143
|
+
if (updated.html) {
|
|
144
|
+
await saveTemplate(ctx.templateFilename, updated.html);
|
|
145
|
+
}
|
|
146
|
+
await this.syncYamlToServer();
|
|
147
|
+
}
|
|
148
|
+
logJsonResult(email, updated, oldSubject) {
|
|
149
|
+
this.log(JSON.stringify({
|
|
150
|
+
diff: {
|
|
151
|
+
previewText: updated.previewText && updated.previewText !== email.previewText
|
|
152
|
+
? { new: updated.previewText, old: email.previewText }
|
|
153
|
+
: undefined,
|
|
154
|
+
subject: oldSubject === email.subject
|
|
155
|
+
? undefined
|
|
156
|
+
: { new: email.subject, old: oldSubject },
|
|
157
|
+
},
|
|
158
|
+
email,
|
|
159
|
+
status: 'updated',
|
|
160
|
+
}, null, 2));
|
|
161
|
+
}
|
|
162
|
+
async handleAcceptOutput(email) {
|
|
163
|
+
this.log(`\n Updated ${chalk.green('mailmodo.yaml')}`);
|
|
164
|
+
const shouldPreview = await confirm({
|
|
165
|
+
default: true,
|
|
166
|
+
message: 'Preview the change?',
|
|
167
|
+
});
|
|
168
|
+
if (shouldPreview) {
|
|
169
|
+
await this.config.runCommand('preview', [email.id]);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
this.log(` Run: ${chalk.cyan(`mailmodo preview ${email.id}`)}\n`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async promptEditAction() {
|
|
176
|
+
return select({
|
|
177
|
+
message: 'Accept, try again, or skip?',
|
|
178
|
+
choices: [
|
|
179
|
+
{ name: 'Accept', value: 'accept' },
|
|
180
|
+
{ name: 'Try again', value: 'retry' },
|
|
181
|
+
{ name: 'Skip', value: 'skip' },
|
|
182
|
+
],
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async askChangeDescription() {
|
|
186
|
+
return input({
|
|
187
|
+
message: 'What do you want to change?',
|
|
188
|
+
validate: (value) => value?.trim() ? true : 'Please describe the change',
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
showFieldDiff(label, oldVal, newVal) {
|
|
192
|
+
if (!newVal || oldVal === newVal)
|
|
193
|
+
return false;
|
|
194
|
+
this.log(`\n ${label}:`);
|
|
195
|
+
if (oldVal)
|
|
196
|
+
this.log(` ${chalk.red(`- ${oldVal}`)}`);
|
|
197
|
+
this.log(` ${chalk.green(`+ ${newVal}`)}`);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
stripHtml(html) {
|
|
201
|
+
return html
|
|
202
|
+
.replaceAll(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
203
|
+
.replaceAll(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
204
|
+
.replaceAll(/<[^>]+>/g, ' ')
|
|
205
|
+
.replaceAll(' ', ' ')
|
|
206
|
+
.replaceAll('&', '&')
|
|
207
|
+
.replaceAll('<', '<')
|
|
208
|
+
.replaceAll('>', '>')
|
|
209
|
+
.replaceAll(/\s+/g, ' ')
|
|
210
|
+
.trim();
|
|
211
|
+
}
|
|
212
|
+
truncate(text, max) {
|
|
213
|
+
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
214
|
+
}
|
|
215
|
+
showHtmlChange(oldHtml, newHtml) {
|
|
216
|
+
if (!newHtml || oldHtml === newHtml)
|
|
217
|
+
return false;
|
|
218
|
+
this.log(`\n HTML Body:`);
|
|
219
|
+
const MAX = 500;
|
|
220
|
+
if (oldHtml) {
|
|
221
|
+
const oldText = this.truncate(this.stripHtml(oldHtml), MAX);
|
|
222
|
+
this.log(` ${chalk.red(`- ${oldText}`)}`);
|
|
223
|
+
}
|
|
224
|
+
const newText = this.truncate(this.stripHtml(newHtml), MAX);
|
|
225
|
+
this.log(` ${chalk.green(`+ ${newText}`)}`);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
showUnchangedField(label, value) {
|
|
229
|
+
if (!value)
|
|
230
|
+
return;
|
|
231
|
+
this.log(`\n ${label}:`);
|
|
232
|
+
this.log(` ${chalk.dim(value)}`);
|
|
233
|
+
}
|
|
234
|
+
showUnchangedHtml(templateHtml) {
|
|
235
|
+
if (!templateHtml)
|
|
236
|
+
return;
|
|
237
|
+
this.log(`\n HTML Body:`);
|
|
238
|
+
this.log(` ${chalk.dim(this.truncate(this.stripHtml(templateHtml), 500))}`);
|
|
239
|
+
}
|
|
240
|
+
showSuggestedChanges(email, updated, templateHtml, changed) {
|
|
241
|
+
this.log('\n Suggested Changes:');
|
|
242
|
+
if (changed.subject)
|
|
243
|
+
this.showFieldDiff('Subject', email.subject, updated.subject);
|
|
244
|
+
if (changed.preview)
|
|
245
|
+
this.showFieldDiff('Preview Text', email.previewText, updated.previewText);
|
|
246
|
+
if (changed.html)
|
|
247
|
+
this.showHtmlChange(templateHtml, updated.html);
|
|
248
|
+
if (changed.cta)
|
|
249
|
+
this.showFieldDiff('CTA Text', undefined, updated.ctaText);
|
|
250
|
+
if (!changed.subject && !changed.preview && !changed.html && !changed.cta) {
|
|
251
|
+
this.log(`\n ${chalk.dim('No changes detected.')}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
showUnchanged(email, templateHtml, changed) {
|
|
255
|
+
const hasContent = !changed.subject ||
|
|
256
|
+
(!changed.preview && Boolean(email.previewText)) ||
|
|
257
|
+
(!changed.html && Boolean(templateHtml));
|
|
258
|
+
if (!hasContent)
|
|
259
|
+
return;
|
|
260
|
+
this.log('\n Unchanged:');
|
|
261
|
+
if (!changed.subject)
|
|
262
|
+
this.showUnchangedField('Subject', email.subject);
|
|
263
|
+
if (!changed.preview)
|
|
264
|
+
this.showUnchangedField('Preview Text', email.previewText);
|
|
265
|
+
if (!changed.html)
|
|
266
|
+
this.showUnchangedHtml(templateHtml);
|
|
267
|
+
}
|
|
268
|
+
buildDiffPreview(email, updated, templateHtml) {
|
|
269
|
+
const subjectChanged = Boolean(updated.subject) && updated.subject !== email.subject;
|
|
270
|
+
const previewChanged = Boolean(updated.previewText) && updated.previewText !== email.previewText;
|
|
271
|
+
const htmlChanged = Boolean(updated.html) && updated.html !== templateHtml;
|
|
272
|
+
const diff = {};
|
|
273
|
+
diff.subject = subjectChanged
|
|
274
|
+
? { new: updated.subject, old: email.subject }
|
|
275
|
+
: { unchanged: true, value: email.subject };
|
|
276
|
+
if (email.previewText ?? updated.previewText) {
|
|
277
|
+
diff.previewText = previewChanged
|
|
278
|
+
? { new: updated.previewText, old: email.previewText }
|
|
279
|
+
: { unchanged: true, value: email.previewText };
|
|
280
|
+
}
|
|
281
|
+
if (templateHtml ?? updated.html) {
|
|
282
|
+
const oldText = templateHtml
|
|
283
|
+
? this.truncate(this.stripHtml(templateHtml), 500)
|
|
284
|
+
: null;
|
|
285
|
+
const newText = updated.html
|
|
286
|
+
? this.truncate(this.stripHtml(updated.html), 500)
|
|
287
|
+
: null;
|
|
288
|
+
diff.html = htmlChanged
|
|
289
|
+
? { new: newText, old: oldText }
|
|
290
|
+
: { unchanged: true, value: oldText };
|
|
291
|
+
}
|
|
292
|
+
if (updated.ctaText) {
|
|
293
|
+
diff.ctaText = { new: updated.ctaText };
|
|
294
|
+
}
|
|
295
|
+
return { diff };
|
|
68
296
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
|
|
77
|
-
syncYaml: () => this.syncYamlToServer(),
|
|
297
|
+
showChangeSummary(email, updated, templateHtml) {
|
|
298
|
+
const changed = {
|
|
299
|
+
cta: Boolean(updated.ctaText),
|
|
300
|
+
html: Boolean(updated.html) && updated.html !== templateHtml,
|
|
301
|
+
preview: Boolean(updated.previewText) &&
|
|
302
|
+
updated.previewText !== email.previewText,
|
|
303
|
+
subject: Boolean(updated.subject) && updated.subject !== email.subject,
|
|
78
304
|
};
|
|
305
|
+
this.showSuggestedChanges(email, updated, templateHtml, changed);
|
|
306
|
+
this.showUnchanged(email, templateHtml, changed);
|
|
79
307
|
}
|
|
80
308
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { join } from 'node:path';
|
|
1
3
|
import { confirm, input } from '@inquirer/prompts';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import open from 'open';
|
|
2
6
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
3
|
-
import { openTemplateInEditor } from '../../lib/commands/emails/editor.js';
|
|
4
|
-
import { renderEmailDetail, renderEmailTable, } from '../../lib/commands/emails/output.js';
|
|
5
7
|
export default class Emails extends BaseCommand {
|
|
6
8
|
static description = 'List and view configured email sequences';
|
|
7
9
|
static examples = [
|
|
@@ -15,37 +17,100 @@ export default class Emails extends BaseCommand {
|
|
|
15
17
|
const { flags } = await this.parse(Emails);
|
|
16
18
|
const yamlConfig = await this.ensureYaml();
|
|
17
19
|
const { emails } = yamlConfig;
|
|
18
|
-
const ctx = this.makeCtx();
|
|
19
20
|
if (flags.json) {
|
|
20
21
|
this.log(JSON.stringify({ emails, total: emails.length }, null, 2));
|
|
21
22
|
return;
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
this.log(`\n ${chalk.bold(String(emails.length))} emails configured in mailmodo.yaml:\n`);
|
|
25
|
+
const idColW = Math.max(...emails.map((e) => e.id.length), 'ID'.length) + 2;
|
|
26
|
+
const triggerColW = Math.max(...emails.map((e) => e.trigger.length), 'Trigger'.length) + 2;
|
|
27
|
+
const delayColW = Math.max(...emails.map((e) => String(e.delay).length), 'Delay'.length) +
|
|
28
|
+
2;
|
|
29
|
+
const hasConditions = emails.some((e) => e.condition);
|
|
30
|
+
this.log(` ${chalk.bold('ID'.padEnd(idColW))}${chalk.bold('Trigger'.padEnd(triggerColW))}${chalk.bold('Delay'.padEnd(delayColW))}${hasConditions ? chalk.bold('Condition') : ''}`);
|
|
31
|
+
this.log(` ${'─'.repeat(idColW + triggerColW + delayColW + (hasConditions ? 'Condition'.length : 0))}`);
|
|
32
|
+
for (const email of emails) {
|
|
33
|
+
const id = chalk.cyan(email.id.padEnd(idColW));
|
|
34
|
+
const trigger = email.trigger.padEnd(triggerColW);
|
|
35
|
+
const delay = String(email.delay).padEnd(delayColW);
|
|
36
|
+
const condition = email.condition ? chalk.dim(email.condition) : '';
|
|
37
|
+
this.log(` ${id}${trigger}${delay}${condition}`);
|
|
38
|
+
}
|
|
39
|
+
this.log('');
|
|
40
|
+
if (!flags.yes) {
|
|
41
|
+
const templateId = await input({
|
|
42
|
+
default: 'n',
|
|
43
|
+
message: "View an email? (id or 'n'):",
|
|
44
|
+
});
|
|
45
|
+
if (templateId !== 'n') {
|
|
46
|
+
const email = emails.find((e) => e.id === templateId);
|
|
47
|
+
if (!email) {
|
|
48
|
+
this.log(`\n Template '${templateId}' not found.\n`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.log('');
|
|
52
|
+
this.log(` ${chalk.bold('ID:')} ${email.id}`);
|
|
53
|
+
this.log(` ${chalk.bold('Trigger:')} ${email.trigger}`);
|
|
54
|
+
this.log(` ${chalk.bold('Delay:')} ${email.delay === 0 || email.delay === '0' ? '0 (immediate)' : email.delay}`);
|
|
55
|
+
this.log(` ${chalk.bold('Subject:')} ${email.subject}`);
|
|
56
|
+
this.log(` ${chalk.bold('Template:')} ${email.template}`);
|
|
57
|
+
if (email.style) {
|
|
58
|
+
this.log(` ${chalk.bold('Style:')} ${email.style}`);
|
|
59
|
+
}
|
|
60
|
+
if (email.condition) {
|
|
61
|
+
this.log(` ${chalk.bold('Condition:')} ${email.condition}`);
|
|
62
|
+
}
|
|
63
|
+
if (email.goal) {
|
|
64
|
+
this.log(` ${chalk.bold('Goal:')} ${email.goal}`);
|
|
65
|
+
}
|
|
66
|
+
this.log('');
|
|
67
|
+
const openIt = await confirm({
|
|
68
|
+
default: true,
|
|
69
|
+
message: 'Open template in editor?',
|
|
70
|
+
});
|
|
71
|
+
if (openIt) {
|
|
72
|
+
await this.openTemplateInEditor(email.template);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async openTemplateInEditor(template) {
|
|
78
|
+
const templatePath = join(process.cwd(), template);
|
|
79
|
+
this.log(`\n Opening ${template}...\n`);
|
|
80
|
+
const editor = process.env.VISUAL || process.env.EDITOR;
|
|
81
|
+
if (editor) {
|
|
82
|
+
const [cmd, ...editorArgs] = editor.trim().split(/\s+/);
|
|
83
|
+
const launched = await new Promise((resolve) => {
|
|
84
|
+
const child = spawn(cmd, [...editorArgs, templatePath], {
|
|
85
|
+
stdio: 'inherit',
|
|
86
|
+
});
|
|
87
|
+
child.on('error', () => resolve(false));
|
|
88
|
+
child.on('close', () => resolve(true));
|
|
89
|
+
});
|
|
90
|
+
if (launched)
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (await this.trySpawnEditor('code', templatePath))
|
|
35
94
|
return;
|
|
95
|
+
try {
|
|
96
|
+
await open(templatePath);
|
|
36
97
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
default: true,
|
|
40
|
-
message: 'Open template in editor?',
|
|
41
|
-
});
|
|
42
|
-
if (openIt) {
|
|
43
|
-
await openTemplateInEditor(ctx, email.template);
|
|
98
|
+
catch {
|
|
99
|
+
this.log(` ${chalk.dim(`Could not open editor. Open the file manually: ${templatePath}`)}`);
|
|
44
100
|
}
|
|
45
101
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
102
|
+
trySpawnEditor(editor, filePath) {
|
|
103
|
+
const [cmd, args] = process.platform === 'win32'
|
|
104
|
+
? ['cmd.exe', ['/c', editor, filePath]]
|
|
105
|
+
: [editor, [filePath]];
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
const child = spawn(cmd, [...args], { stdio: 'ignore' });
|
|
108
|
+
child.on('error', () => {
|
|
109
|
+
resolve(false);
|
|
110
|
+
});
|
|
111
|
+
child.on('close', (code) => {
|
|
112
|
+
resolve(code === 0);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
50
115
|
}
|
|
51
116
|
}
|