@mailmodo/cli 0.0.55 → 0.0.56-beta.pr58.100
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 +1 -11
- package/dist/commands/billing/index.js +28 -184
- package/dist/commands/contacts/index.d.ts +1 -19
- package/dist/commands/contacts/index.js +21 -114
- package/dist/commands/deploy/index.js +12 -7
- package/dist/commands/deployments/index.d.ts +1 -4
- package/dist/commands/deployments/index.js +11 -52
- package/dist/commands/domain/index.d.ts +1 -14
- package/dist/commands/domain/index.js +19 -100
- package/dist/commands/edit/index.d.ts +2 -20
- package/dist/commands/edit/index.js +33 -258
- package/dist/commands/emails/index.d.ts +1 -2
- package/dist/commands/emails/index.js +26 -91
- package/dist/commands/init/index.d.ts +1 -3
- package/dist/commands/init/index.js +47 -199
- package/dist/commands/login/index.d.ts +2 -0
- package/dist/commands/login/index.js +32 -79
- package/dist/commands/logs/index.d.ts +1 -8
- package/dist/commands/logs/index.js +12 -55
- package/dist/commands/preview/index.d.ts +1 -19
- package/dist/commands/preview/index.js +32 -212
- package/dist/commands/sdk/index.d.ts +1 -3
- package/dist/commands/sdk/index.js +14 -46
- package/dist/commands/settings/index.d.ts +1 -22
- package/dist/commands/settings/index.js +34 -246
- package/dist/commands/status/index.d.ts +1 -0
- package/dist/commands/status/index.js +13 -39
- package/dist/lib/base-command.d.ts +35 -10
- package/dist/lib/base-command.js +169 -17
- package/dist/lib/commands/billing/checkout-status.d.ts +3 -0
- package/dist/lib/commands/billing/checkout-status.js +63 -0
- package/dist/lib/commands/billing/format.d.ts +7 -0
- package/dist/lib/commands/billing/format.js +63 -0
- package/dist/lib/commands/billing/purchase-cap.d.ts +7 -0
- package/dist/lib/commands/billing/purchase-cap.js +57 -0
- package/dist/lib/commands/billing/types.d.ts +72 -0
- package/dist/lib/commands/contacts/actions.d.ts +3 -0
- package/dist/lib/commands/contacts/actions.js +49 -0
- package/dist/lib/commands/contacts/export-delete.d.ts +9 -0
- package/dist/lib/commands/contacts/export-delete.js +51 -0
- package/dist/lib/commands/contacts/types.d.ts +35 -0
- package/dist/lib/commands/contacts/types.js +1 -0
- package/dist/lib/{deploy → commands/deploy}/domain-setup.d.ts +1 -1
- package/dist/lib/{deploy → commands/deploy}/domain-setup.js +2 -2
- package/dist/lib/{deploy → commands/deploy}/output.d.ts +1 -1
- package/dist/lib/{deploy → commands/deploy}/output.js +2 -2
- package/dist/lib/{deploy → commands/deploy}/payload.d.ts +1 -1
- package/dist/lib/{deploy → commands/deploy}/payload.js +2 -2
- package/dist/lib/{deploy → commands/deploy}/sequence-status.js +2 -2
- package/dist/lib/{deploy → commands/deploy}/types.d.ts +4 -4
- package/dist/lib/commands/deploy/types.js +1 -0
- package/dist/lib/commands/deployments/output.d.ts +2 -0
- package/dist/lib/commands/deployments/output.js +68 -0
- package/dist/lib/commands/deployments/types.d.ts +24 -0
- package/dist/lib/commands/deployments/types.js +1 -0
- package/dist/lib/commands/domain/setup.d.ts +8 -0
- package/dist/lib/commands/domain/setup.js +53 -0
- package/dist/lib/commands/domain/types.d.ts +56 -0
- package/dist/lib/commands/domain/types.js +1 -0
- package/dist/lib/commands/domain/verify.d.ts +5 -0
- package/dist/lib/commands/domain/verify.js +50 -0
- package/dist/lib/commands/edit/diff.d.ts +7 -0
- package/dist/lib/commands/edit/diff.js +65 -0
- package/dist/lib/commands/edit/display.d.ts +5 -0
- package/dist/lib/commands/edit/display.js +53 -0
- package/dist/lib/commands/edit/flow.d.ts +8 -0
- package/dist/lib/commands/edit/flow.js +70 -0
- package/dist/lib/commands/edit/persist.d.ts +5 -0
- package/dist/lib/commands/edit/persist.js +67 -0
- package/dist/lib/commands/edit/types.d.ts +38 -0
- package/dist/lib/commands/edit/types.js +1 -0
- package/dist/lib/commands/emails/editor.d.ts +2 -0
- package/dist/lib/commands/emails/editor.js +43 -0
- package/dist/lib/commands/emails/output.d.ts +4 -0
- package/dist/lib/commands/emails/output.js +36 -0
- package/dist/lib/commands/emails/types.d.ts +3 -0
- package/dist/lib/commands/emails/types.js +1 -0
- package/dist/lib/commands/init/analysis.d.ts +3 -0
- package/dist/lib/commands/init/analysis.js +73 -0
- package/dist/lib/commands/init/output.d.ts +12 -0
- package/dist/lib/commands/init/output.js +39 -0
- package/dist/lib/commands/init/payload.d.ts +8 -0
- package/dist/lib/commands/init/payload.js +78 -0
- package/dist/lib/commands/init/types.d.ts +57 -0
- package/dist/lib/commands/init/types.js +1 -0
- package/dist/lib/commands/login/output.d.ts +8 -0
- package/dist/lib/commands/login/output.js +40 -0
- package/dist/lib/commands/login/types.d.ts +19 -0
- package/dist/lib/commands/login/types.js +1 -0
- package/dist/lib/commands/logs/output.d.ts +2 -0
- package/dist/lib/commands/logs/output.js +52 -0
- package/dist/lib/commands/logs/types.d.ts +23 -0
- package/dist/lib/commands/logs/types.js +1 -0
- package/dist/lib/commands/preview/actions.d.ts +11 -0
- package/dist/lib/commands/preview/actions.js +43 -0
- package/dist/lib/commands/preview/render.d.ts +3 -0
- package/dist/lib/commands/preview/render.js +30 -0
- package/dist/lib/commands/preview/server.d.ts +8 -0
- package/dist/lib/commands/preview/server.js +63 -0
- package/dist/lib/commands/preview/types.d.ts +22 -0
- package/dist/lib/commands/preview/types.js +1 -0
- package/dist/lib/commands/preview/wrapper-html.d.ts +2 -0
- package/dist/lib/commands/preview/wrapper-html.js +35 -0
- package/dist/lib/commands/sdk/output.d.ts +2 -0
- package/dist/lib/commands/sdk/output.js +42 -0
- package/dist/lib/commands/sdk/types.d.ts +21 -0
- package/dist/lib/commands/sdk/types.js +1 -0
- package/dist/lib/commands/settings/actions.d.ts +10 -0
- package/dist/lib/commands/settings/actions.js +56 -0
- package/dist/lib/commands/settings/display.d.ts +15 -0
- package/dist/lib/commands/settings/display.js +69 -0
- package/dist/lib/commands/settings/logo-domain.d.ts +3 -0
- package/dist/lib/commands/settings/logo-domain.js +47 -0
- package/dist/lib/commands/settings/prompt.d.ts +2 -0
- package/dist/lib/commands/settings/prompt.js +82 -0
- package/dist/lib/commands/settings/types.d.ts +65 -0
- package/dist/lib/commands/settings/types.js +1 -0
- package/dist/lib/commands/status/output.d.ts +2 -0
- package/dist/lib/commands/status/output.js +49 -0
- package/dist/lib/commands/status/types.d.ts +28 -0
- package/dist/lib/commands/status/types.js +1 -0
- package/dist/lib/constants.d.ts +3 -2
- package/dist/lib/constants.js +4 -5
- package/dist/lib/messages.d.ts +11 -0
- package/dist/lib/messages.js +31 -0
- package/dist/lib/templates/missing-templates.d.ts +16 -2
- package/dist/lib/templates/missing-templates.js +34 -22
- package/dist/lib/templates/regenerate.d.ts +10 -0
- package/dist/lib/templates/regenerate.js +29 -0
- package/dist/lib/templates/sync.d.ts +33 -0
- package/dist/lib/templates/sync.js +106 -0
- package/dist/lib/templates/types.d.ts +3 -0
- package/oclif.manifest.json +54 -54
- package/package.json +1 -1
- /package/dist/lib/{deploy → commands/billing}/types.js +0 -0
- /package/dist/lib/{deploy → commands/deploy}/sequence-status.d.ts +0 -0
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
-
import { input } from '@inquirer/prompts';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
2
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { getDomainOrError, setupDomain, showDomainStatus, } from '../../lib/commands/domain/setup.js';
|
|
4
|
+
import { verifyDomain } from '../../lib/commands/domain/verify.js';
|
|
7
5
|
export default class Domain extends BaseCommand {
|
|
8
6
|
static description = 'Set up and verify your sending domain';
|
|
9
7
|
static examples = [
|
|
@@ -25,111 +23,32 @@ export default class Domain extends BaseCommand {
|
|
|
25
23
|
async run() {
|
|
26
24
|
const { flags } = await this.parse(Domain);
|
|
27
25
|
await this.ensureAuth();
|
|
26
|
+
const ctx = this.makeCtx();
|
|
28
27
|
if (flags.verify) {
|
|
29
28
|
const yamlConfig = await this.ensureYaml();
|
|
30
|
-
const domain = yamlConfig
|
|
31
|
-
|
|
32
|
-
this.error(ERRORS.DOMAIN_NOT_CONFIGURED);
|
|
33
|
-
}
|
|
34
|
-
await this.verifyDomain(flags.json, domain);
|
|
29
|
+
const domain = getDomainOrError(ctx, yamlConfig);
|
|
30
|
+
await verifyDomain(ctx, { domain, json: flags.json });
|
|
35
31
|
return;
|
|
36
32
|
}
|
|
37
33
|
if (flags.status) {
|
|
38
34
|
const yamlConfig = await this.ensureYaml();
|
|
39
|
-
const domain = yamlConfig
|
|
40
|
-
|
|
41
|
-
this.error(ERRORS.DOMAIN_NOT_CONFIGURED);
|
|
42
|
-
}
|
|
43
|
-
await this.showDomainStatus(flags.json, domain);
|
|
35
|
+
const domain = getDomainOrError(ctx, yamlConfig);
|
|
36
|
+
await showDomainStatus(ctx, { domain, json: flags.json });
|
|
44
37
|
return;
|
|
45
38
|
}
|
|
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) {
|
|
53
39
|
const yamlConfig = await this.ensureYaml();
|
|
54
|
-
|
|
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
|
-
}
|
|
40
|
+
await setupDomain(ctx, yamlConfig, flags);
|
|
73
41
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
this.
|
|
83
|
-
|
|
84
|
-
|
|
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`);
|
|
42
|
+
makeCtx() {
|
|
43
|
+
return {
|
|
44
|
+
collectDomainInputs: (yaml, skip) => this.collectDomainSetupInputs(yaml, skip),
|
|
45
|
+
error: (msg) => this.error(msg),
|
|
46
|
+
get: (path, params) => this.apiClient.get(path, params),
|
|
47
|
+
log: (msg) => this.log(msg),
|
|
48
|
+
onApiError: (r) => this.handleApiError(r),
|
|
49
|
+
registerDomainAndSave: (yaml, inputs, json) => this.registerDomain(yaml, inputs, json),
|
|
50
|
+
showDnsRecords: (records, url, json) => this.logDnsRecords(records, url, json),
|
|
51
|
+
spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
|
|
52
|
+
};
|
|
134
53
|
}
|
|
135
54
|
}
|
|
@@ -11,24 +11,6 @@ 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
|
|
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;
|
|
14
|
+
private makeCtx;
|
|
15
|
+
private makeRegenCtx;
|
|
34
16
|
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
|
-
import { confirm, input, select } from '@inquirer/prompts';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
2
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
|
-
import {
|
|
6
|
-
import { loadTemplate, getTemplateFilename, saveTemplate, saveYaml, } from '../../lib/yaml-config.js';
|
|
3
|
+
import { getTemplateFilename, loadTemplate, } from '../../lib/yaml-config.js';
|
|
7
4
|
import { handleMissingTemplates } from '../../lib/templates/missing-templates.js';
|
|
5
|
+
import { askChangeDescription, runEditStep, } from '../../lib/commands/edit/flow.js';
|
|
8
6
|
export default class Edit extends BaseCommand {
|
|
9
7
|
static args = {
|
|
10
8
|
id: Args.string({
|
|
@@ -33,7 +31,7 @@ export default class Edit extends BaseCommand {
|
|
|
33
31
|
}
|
|
34
32
|
const email = yamlConfig.emails[emailIndex];
|
|
35
33
|
const templateFilename = getTemplateFilename(email.id, email.style, yamlConfig.project?.emailStyle);
|
|
36
|
-
const
|
|
34
|
+
const editCtx = {
|
|
37
35
|
email,
|
|
38
36
|
emailIndex,
|
|
39
37
|
templateFilename,
|
|
@@ -44,265 +42,42 @@ export default class Edit extends BaseCommand {
|
|
|
44
42
|
json: flags.json ?? false,
|
|
45
43
|
yes: flags.yes ?? false,
|
|
46
44
|
};
|
|
47
|
-
if (!
|
|
48
|
-
const
|
|
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);
|
|
45
|
+
if (!editCtx.templateHtml) {
|
|
46
|
+
const regenerated = await handleMissingTemplates(this.makeRegenCtx(), yamlConfig, [email.id], editFlags);
|
|
58
47
|
if (!regenerated)
|
|
59
48
|
return;
|
|
60
|
-
|
|
61
|
-
if (!
|
|
49
|
+
editCtx.templateHtml = await loadTemplate(templateFilename);
|
|
50
|
+
if (!editCtx.templateHtml)
|
|
62
51
|
this.error('Template regeneration failed.');
|
|
63
52
|
}
|
|
64
|
-
const initialChange = flags.change?.trim() || (await
|
|
65
|
-
await this.
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this.
|
|
71
|
-
this.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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],
|
|
53
|
+
const initialChange = flags.change?.trim() || (await askChangeDescription());
|
|
54
|
+
await runEditStep(this.makeCtx(), editCtx, initialChange, editFlags);
|
|
55
|
+
}
|
|
56
|
+
makeCtx() {
|
|
57
|
+
return {
|
|
58
|
+
error: (msg) => this.error(msg),
|
|
59
|
+
exit: (code) => this.exit(code),
|
|
60
|
+
handleAiQuotaError: (r, feature) => this.handleAiQuotaError(r, feature),
|
|
61
|
+
log: (msg) => this.log(msg),
|
|
62
|
+
onApiError: (r) => this.handleApiError(r),
|
|
63
|
+
post: (path, body) => this.apiClient.post(path, body),
|
|
64
|
+
runCommand: (id, argv) => this.config.runCommand(id, argv),
|
|
65
|
+
spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
|
|
66
|
+
syncTemplate: (emailId) => this.syncTemplateToServer(emailId),
|
|
67
|
+
syncYaml: () => this.syncYamlToServer(),
|
|
140
68
|
};
|
|
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 };
|
|
296
69
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
70
|
+
makeRegenCtx() {
|
|
71
|
+
return {
|
|
72
|
+
error: (msg) => this.error(msg),
|
|
73
|
+
exit: (code) => this.exit(code),
|
|
74
|
+
fetchTemplate: (emailId) => this.getTemplateFromServer(emailId),
|
|
75
|
+
log: (msg) => this.log(msg),
|
|
76
|
+
onApiError: (r) => this.handleApiError(r),
|
|
77
|
+
post: (path, body) => this.apiClient.post(path, body),
|
|
78
|
+
spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
|
|
79
|
+
syncTemplates: (yaml) => this.syncTemplatesToServer(yaml),
|
|
80
|
+
syncYaml: () => this.syncYamlToServer(),
|
|
304
81
|
};
|
|
305
|
-
this.showSuggestedChanges(email, updated, templateHtml, changed);
|
|
306
|
-
this.showUnchanged(email, templateHtml, changed);
|
|
307
82
|
}
|
|
308
83
|
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
1
|
import { confirm, input } from '@inquirer/prompts';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import open from 'open';
|
|
6
2
|
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';
|
|
7
5
|
export default class Emails extends BaseCommand {
|
|
8
6
|
static description = 'List and view configured email sequences';
|
|
9
7
|
static examples = [
|
|
@@ -17,100 +15,37 @@ export default class Emails extends BaseCommand {
|
|
|
17
15
|
const { flags } = await this.parse(Emails);
|
|
18
16
|
const yamlConfig = await this.ensureYaml();
|
|
19
17
|
const { emails } = yamlConfig;
|
|
18
|
+
const ctx = this.makeCtx();
|
|
20
19
|
if (flags.json) {
|
|
21
20
|
this.log(JSON.stringify({ emails, total: emails.length }, null, 2));
|
|
22
21
|
return;
|
|
23
22
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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))
|
|
23
|
+
renderEmailTable(ctx, emails);
|
|
24
|
+
if (flags.yes)
|
|
25
|
+
return;
|
|
26
|
+
const templateId = await input({
|
|
27
|
+
default: 'n',
|
|
28
|
+
message: "View an email? (id or 'n'):",
|
|
29
|
+
});
|
|
30
|
+
if (templateId === 'n')
|
|
31
|
+
return;
|
|
32
|
+
const email = emails.find((e) => e.id === templateId);
|
|
33
|
+
if (!email) {
|
|
34
|
+
this.log(`\n Template '${templateId}' not found.\n`);
|
|
94
35
|
return;
|
|
95
|
-
try {
|
|
96
|
-
await open(templatePath);
|
|
97
36
|
}
|
|
98
|
-
|
|
99
|
-
|
|
37
|
+
renderEmailDetail(ctx, email);
|
|
38
|
+
const openIt = await confirm({
|
|
39
|
+
default: true,
|
|
40
|
+
message: 'Open template in editor?',
|
|
41
|
+
});
|
|
42
|
+
if (openIt) {
|
|
43
|
+
await openTemplateInEditor(ctx, email.template);
|
|
100
44
|
}
|
|
101
45
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
});
|
|
46
|
+
makeCtx() {
|
|
47
|
+
return {
|
|
48
|
+
log: (msg) => this.log(msg),
|
|
49
|
+
};
|
|
115
50
|
}
|
|
116
51
|
}
|