@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
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { input } from '@inquirer/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { API_ENDPOINTS } from '../../constants.js';
|
|
4
|
+
import { ERRORS, PROMPTS, SEPARATOR } from '../../messages.js';
|
|
5
|
+
import { verifyDomain } from './verify.js';
|
|
6
|
+
async function promptAndVerify(ctx, domain) {
|
|
7
|
+
const action = await input({
|
|
8
|
+
default: '',
|
|
9
|
+
message: PROMPTS.ENTER_AFTER_RECORDS,
|
|
10
|
+
});
|
|
11
|
+
if (action.toLowerCase() !== 'skip') {
|
|
12
|
+
await verifyDomain(ctx, { domain, json: false });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function setupDomain(ctx, yamlConfig, flags) {
|
|
16
|
+
ctx.log(`\n ${SEPARATOR}`);
|
|
17
|
+
ctx.log(` ${chalk.bold('DOMAIN SETUP')}`);
|
|
18
|
+
ctx.log(` ${SEPARATOR}\n`);
|
|
19
|
+
const inputs = await ctx.collectDomainInputs(yamlConfig, flags.yes);
|
|
20
|
+
const { dnsRecords, dnsGuideUrl } = await ctx.registerDomainAndSave(yamlConfig, inputs, flags.json);
|
|
21
|
+
if (flags.json) {
|
|
22
|
+
ctx.log(JSON.stringify({ dnsRecords, domain: inputs.domain }, null, 2));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
ctx.showDnsRecords(dnsRecords, dnsGuideUrl, flags.json);
|
|
26
|
+
if (!flags.yes) {
|
|
27
|
+
await promptAndVerify(ctx, inputs.domain);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function showDomainStatus(ctx, opts) {
|
|
31
|
+
const response = await ctx.spinner(' Loading domain status...', opts.json, () => ctx.get(API_ENDPOINTS.DOMAIN_STATUS, {
|
|
32
|
+
domain: opts.domain,
|
|
33
|
+
}));
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
ctx.onApiError(response);
|
|
36
|
+
}
|
|
37
|
+
const { data } = response;
|
|
38
|
+
if (opts.json) {
|
|
39
|
+
ctx.log(JSON.stringify(data, null, 2));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
ctx.log(`\n Domain: ${chalk.bold(data.domain || 'not configured')}`);
|
|
43
|
+
ctx.log(` Status: ${data.verified ? chalk.green('✓ verified') : chalk.red('✗ not verified')}`);
|
|
44
|
+
ctx.log(` Bounce rate: ${data.bounceRate ?? 'N/A'}%`);
|
|
45
|
+
ctx.log(` Spam rate: ${data.spamRate ?? 'N/A'}%\n`);
|
|
46
|
+
}
|
|
47
|
+
export function getDomainOrError(ctx, yamlConfig) {
|
|
48
|
+
const domain = yamlConfig.project?.domain;
|
|
49
|
+
if (!domain) {
|
|
50
|
+
ctx.error(ERRORS.DOMAIN_NOT_CONFIGURED);
|
|
51
|
+
}
|
|
52
|
+
return domain;
|
|
53
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ApiResponse } from '../../api-client.js';
|
|
2
|
+
import type { MailmodoYaml } from '../../yaml-config.js';
|
|
3
|
+
export interface DomainVerifyResponse {
|
|
4
|
+
dkim: boolean;
|
|
5
|
+
dmarc: boolean;
|
|
6
|
+
dnsGuideUrl?: string;
|
|
7
|
+
returnPath: boolean;
|
|
8
|
+
domainStatus: string;
|
|
9
|
+
}
|
|
10
|
+
export interface DomainStatusResponse {
|
|
11
|
+
bounceRate: number;
|
|
12
|
+
domain: string;
|
|
13
|
+
spamRate: number;
|
|
14
|
+
status: string;
|
|
15
|
+
verified: boolean;
|
|
16
|
+
}
|
|
17
|
+
export type DomainFlags = {
|
|
18
|
+
json: boolean;
|
|
19
|
+
yes: boolean;
|
|
20
|
+
};
|
|
21
|
+
export type DomainCtx = {
|
|
22
|
+
collectDomainInputs(yaml: MailmodoYaml, skip: boolean): Promise<{
|
|
23
|
+
address: string;
|
|
24
|
+
domain: string;
|
|
25
|
+
fromEmail: string;
|
|
26
|
+
fromName: string;
|
|
27
|
+
replyTo: string;
|
|
28
|
+
}>;
|
|
29
|
+
error(msg: string): never;
|
|
30
|
+
get<T>(path: string, params?: Record<string, string>): Promise<ApiResponse<T>>;
|
|
31
|
+
log(msg?: string): void;
|
|
32
|
+
onApiError(resp: {
|
|
33
|
+
error?: string;
|
|
34
|
+
status: number;
|
|
35
|
+
}): never;
|
|
36
|
+
registerDomainAndSave(yaml: MailmodoYaml, inputs: {
|
|
37
|
+
address: string;
|
|
38
|
+
domain: string;
|
|
39
|
+
fromEmail: string;
|
|
40
|
+
fromName?: string;
|
|
41
|
+
replyTo?: string;
|
|
42
|
+
}, json: boolean): Promise<{
|
|
43
|
+
dnsGuideUrl?: string;
|
|
44
|
+
dnsRecords: Array<{
|
|
45
|
+
host: string;
|
|
46
|
+
type: string;
|
|
47
|
+
value: string;
|
|
48
|
+
}>;
|
|
49
|
+
}>;
|
|
50
|
+
showDnsRecords(records: Array<{
|
|
51
|
+
host: string;
|
|
52
|
+
type: string;
|
|
53
|
+
value: string;
|
|
54
|
+
}>, guideUrl: string | undefined, json: boolean): void;
|
|
55
|
+
spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
|
|
56
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { API_ENDPOINTS } from '../../constants.js';
|
|
3
|
+
import { INFO } from '../../messages.js';
|
|
4
|
+
function logDkimMistakes(ctx) {
|
|
5
|
+
ctx.log(`\n DKIM common mistakes:`);
|
|
6
|
+
ctx.log(` - Using CNAME instead of TXT record type`);
|
|
7
|
+
ctx.log(` - Including the full domain in the Host field`);
|
|
8
|
+
ctx.log(` - Cloudflare: proxy must be OFF (grey cloud, not orange)`);
|
|
9
|
+
}
|
|
10
|
+
function logReturnPathMistakes(ctx) {
|
|
11
|
+
ctx.log(`\n Return Path common mistakes:`);
|
|
12
|
+
ctx.log(` - Missing or incorrect CNAME for mm-bounce subdomain`);
|
|
13
|
+
ctx.log(` - Cloudflare: proxy must be OFF (grey cloud, not orange)`);
|
|
14
|
+
}
|
|
15
|
+
function logVerifyFailure(ctx, dkim, returnPath, dnsGuideUrl) {
|
|
16
|
+
ctx.log(`\n ${INFO.DNS_RECORDS_FAILED}`);
|
|
17
|
+
if (!dkim) {
|
|
18
|
+
logDkimMistakes(ctx);
|
|
19
|
+
}
|
|
20
|
+
if (!returnPath) {
|
|
21
|
+
logReturnPathMistakes(ctx);
|
|
22
|
+
}
|
|
23
|
+
ctx.log(`\n ${INFO.DNS_FIX_AND_VERIFY}`);
|
|
24
|
+
if (dnsGuideUrl) {
|
|
25
|
+
ctx.log(` Help: ${chalk.cyan(dnsGuideUrl)}\n`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function verifyDomain(ctx, opts) {
|
|
29
|
+
const response = await ctx.spinner(' Checking DNS...', opts.json, () => ctx.get(API_ENDPOINTS.DOMAIN_VERIFY, {
|
|
30
|
+
domain: opts.domain,
|
|
31
|
+
}));
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
ctx.onApiError(response);
|
|
34
|
+
}
|
|
35
|
+
const { dkim, dmarc, dnsGuideUrl, returnPath, domainStatus } = response.data;
|
|
36
|
+
if (opts.json) {
|
|
37
|
+
ctx.log(JSON.stringify({ dkim, dmarc, returnPath, domainStatus }, null, 2));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
ctx.log(` DKIM ${dkim ? chalk.green('✓') : chalk.red('✗ Not found')}`);
|
|
41
|
+
ctx.log(` DMARC ${dmarc ? chalk.green('✓') : chalk.red('✗ Not found')}`);
|
|
42
|
+
ctx.log(` Return Path ${returnPath ? chalk.green('✓') : chalk.red('✗ Not found')}`);
|
|
43
|
+
const allPassed = domainStatus === 'VERIFIED';
|
|
44
|
+
if (allPassed) {
|
|
45
|
+
ctx.log(`\n ${chalk.green('✓')} Domain verified.\n`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
logVerifyFailure(ctx, dkim, returnPath, dnsGuideUrl);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { EmailConfig } from '../../yaml-config.js';
|
|
2
|
+
import type { EditResponse } from './types.js';
|
|
3
|
+
export declare function stripHtml(html: string): string;
|
|
4
|
+
export declare function truncate(text: string, max: number): string;
|
|
5
|
+
export declare function showFieldDiff(log: (msg?: string) => void, label: string, oldVal: string | undefined, newVal: string | undefined): boolean;
|
|
6
|
+
export declare function showHtmlChange(log: (msg?: string) => void, oldHtml: null | string, newHtml: string | undefined): boolean;
|
|
7
|
+
export declare function buildDiffPreview(email: EmailConfig, updated: EditResponse, templateHtml: null | string): Record<string, unknown>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export function stripHtml(html) {
|
|
3
|
+
return html
|
|
4
|
+
.replaceAll(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
5
|
+
.replaceAll(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
6
|
+
.replaceAll(/<[^>]+>/g, ' ')
|
|
7
|
+
.replaceAll(' ', ' ')
|
|
8
|
+
.replaceAll('&', '&')
|
|
9
|
+
.replaceAll('<', '<')
|
|
10
|
+
.replaceAll('>', '>')
|
|
11
|
+
.replaceAll(/\s+/g, ' ')
|
|
12
|
+
.trim();
|
|
13
|
+
}
|
|
14
|
+
export function truncate(text, max) {
|
|
15
|
+
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
16
|
+
}
|
|
17
|
+
export function showFieldDiff(log, label, oldVal, newVal) {
|
|
18
|
+
if (!newVal || oldVal === newVal)
|
|
19
|
+
return false;
|
|
20
|
+
log(`\n ${label}:`);
|
|
21
|
+
if (oldVal)
|
|
22
|
+
log(` ${chalk.red(`- ${oldVal}`)}`);
|
|
23
|
+
log(` ${chalk.green(`+ ${newVal}`)}`);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
export function showHtmlChange(log, oldHtml, newHtml) {
|
|
27
|
+
if (!newHtml || oldHtml === newHtml)
|
|
28
|
+
return false;
|
|
29
|
+
log(`\n HTML Body:`);
|
|
30
|
+
const MAX = 500;
|
|
31
|
+
if (oldHtml) {
|
|
32
|
+
log(` ${chalk.red(`- ${truncate(stripHtml(oldHtml), MAX)}`)}`);
|
|
33
|
+
}
|
|
34
|
+
log(` ${chalk.green(`+ ${truncate(stripHtml(newHtml), MAX)}`)}`);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
export function buildDiffPreview(email, updated, templateHtml) {
|
|
38
|
+
const subjectChanged = Boolean(updated.subject) && updated.subject !== email.subject;
|
|
39
|
+
const previewChanged = Boolean(updated.previewText) && updated.previewText !== email.previewText;
|
|
40
|
+
const htmlChanged = Boolean(updated.html) && updated.html !== templateHtml;
|
|
41
|
+
const diff = {};
|
|
42
|
+
diff.subject = subjectChanged
|
|
43
|
+
? { new: updated.subject, old: email.subject }
|
|
44
|
+
: { unchanged: true, value: email.subject };
|
|
45
|
+
if (email.previewText || updated.previewText) {
|
|
46
|
+
diff.previewText = previewChanged
|
|
47
|
+
? { new: updated.previewText, old: email.previewText }
|
|
48
|
+
: { unchanged: true, value: email.previewText };
|
|
49
|
+
}
|
|
50
|
+
if (templateHtml || updated.html) {
|
|
51
|
+
const oldText = templateHtml
|
|
52
|
+
? truncate(stripHtml(templateHtml), 500)
|
|
53
|
+
: null;
|
|
54
|
+
const newText = updated.html
|
|
55
|
+
? truncate(stripHtml(updated.html), 500)
|
|
56
|
+
: null;
|
|
57
|
+
diff.html = htmlChanged
|
|
58
|
+
? { new: newText, old: oldText }
|
|
59
|
+
: { unchanged: true, value: oldText };
|
|
60
|
+
}
|
|
61
|
+
if (updated.ctaText) {
|
|
62
|
+
diff.ctaText = { new: updated.ctaText };
|
|
63
|
+
}
|
|
64
|
+
return { diff };
|
|
65
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { EmailConfig } from '../../yaml-config.js';
|
|
2
|
+
import type { EditResponse } from './types.js';
|
|
3
|
+
type Log = (msg?: string) => void;
|
|
4
|
+
export declare function showChangeSummary(log: Log, email: EmailConfig, updated: EditResponse, templateHtml: null | string): void;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { showFieldDiff, showHtmlChange, stripHtml, truncate } from './diff.js';
|
|
3
|
+
function showUnchangedField(log, label, value) {
|
|
4
|
+
if (!value)
|
|
5
|
+
return;
|
|
6
|
+
log(`\n ${label}:`);
|
|
7
|
+
log(` ${chalk.dim(value)}`);
|
|
8
|
+
}
|
|
9
|
+
function showUnchangedHtml(log, templateHtml) {
|
|
10
|
+
if (!templateHtml)
|
|
11
|
+
return;
|
|
12
|
+
log(`\n HTML Body:`);
|
|
13
|
+
log(` ${chalk.dim(truncate(stripHtml(templateHtml), 500))}`);
|
|
14
|
+
}
|
|
15
|
+
function showSuggestedChanges(log, content, changed) {
|
|
16
|
+
const { email, templateHtml, updated } = content;
|
|
17
|
+
log('\n Suggested Changes:');
|
|
18
|
+
if (changed.subject)
|
|
19
|
+
showFieldDiff(log, 'Subject', email.subject, updated.subject);
|
|
20
|
+
if (changed.preview)
|
|
21
|
+
showFieldDiff(log, 'Preview Text', email.previewText, updated.previewText);
|
|
22
|
+
if (changed.html)
|
|
23
|
+
showHtmlChange(log, templateHtml, updated.html);
|
|
24
|
+
if (changed.cta)
|
|
25
|
+
showFieldDiff(log, 'CTA Text', undefined, updated.ctaText);
|
|
26
|
+
if (!changed.subject && !changed.preview && !changed.html && !changed.cta) {
|
|
27
|
+
log(`\n ${chalk.dim('No changes detected.')}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function showUnchanged(log, email, templateHtml, changed) {
|
|
31
|
+
const hasContent = !changed.subject ||
|
|
32
|
+
(!changed.preview && Boolean(email.previewText)) ||
|
|
33
|
+
(!changed.html && Boolean(templateHtml));
|
|
34
|
+
if (!hasContent)
|
|
35
|
+
return;
|
|
36
|
+
log('\n Unchanged:');
|
|
37
|
+
if (!changed.subject)
|
|
38
|
+
showUnchangedField(log, 'Subject', email.subject);
|
|
39
|
+
if (!changed.preview)
|
|
40
|
+
showUnchangedField(log, 'Preview Text', email.previewText);
|
|
41
|
+
if (!changed.html)
|
|
42
|
+
showUnchangedHtml(log, templateHtml);
|
|
43
|
+
}
|
|
44
|
+
export function showChangeSummary(log, email, updated, templateHtml) {
|
|
45
|
+
const changed = {
|
|
46
|
+
cta: Boolean(updated.ctaText),
|
|
47
|
+
html: Boolean(updated.html) && updated.html !== templateHtml,
|
|
48
|
+
preview: Boolean(updated.previewText) && updated.previewText !== email.previewText,
|
|
49
|
+
subject: Boolean(updated.subject) && updated.subject !== email.subject,
|
|
50
|
+
};
|
|
51
|
+
showSuggestedChanges(log, { email, templateHtml, updated }, changed);
|
|
52
|
+
showUnchanged(log, email, templateHtml, changed);
|
|
53
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EditContext, EditCtx, EditFlags, EditResponse } from './types.js';
|
|
2
|
+
export declare function askChangeDescription(): Promise<string>;
|
|
3
|
+
export declare function promptEditAction(): Promise<'accept' | 'retry' | 'skip'>;
|
|
4
|
+
export declare function handleUserAction(ctx: EditCtx, editCtx: EditContext, opts: {
|
|
5
|
+
flags: EditFlags;
|
|
6
|
+
updated: EditResponse;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
export declare function runEditStep(ctx: EditCtx, editCtx: EditContext, changeDescription: string, flags: EditFlags): Promise<void>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { input, select } from '@inquirer/prompts';
|
|
2
|
+
import { API_ENDPOINTS } from '../../constants.js';
|
|
3
|
+
import { buildDiffPreview } from './diff.js';
|
|
4
|
+
import { showChangeSummary } from './display.js';
|
|
5
|
+
import { finalizeEdit } from './persist.js';
|
|
6
|
+
export async function askChangeDescription() {
|
|
7
|
+
return input({
|
|
8
|
+
message: 'What do you want to change?',
|
|
9
|
+
validate: (value) => (value?.trim() ? true : 'Please describe the change'),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export async function promptEditAction() {
|
|
13
|
+
return select({
|
|
14
|
+
message: 'Accept, try again, or skip?',
|
|
15
|
+
choices: [
|
|
16
|
+
{ name: 'Accept', value: 'accept' },
|
|
17
|
+
{ name: 'Try again', value: 'retry' },
|
|
18
|
+
{ name: 'Skip', value: 'skip' },
|
|
19
|
+
],
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function buildEditApiBody(changeDescription, email, templateHtml) {
|
|
23
|
+
return {
|
|
24
|
+
changeRequest: changeDescription,
|
|
25
|
+
currentEmail: {
|
|
26
|
+
condition: email.condition,
|
|
27
|
+
goal: email.goal,
|
|
28
|
+
html: templateHtml,
|
|
29
|
+
id: email.id,
|
|
30
|
+
previewText: email.previewText,
|
|
31
|
+
subject: email.subject,
|
|
32
|
+
trigger: email.trigger,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export async function handleUserAction(ctx, editCtx, opts) {
|
|
37
|
+
const { flags, updated } = opts;
|
|
38
|
+
ctx.log('');
|
|
39
|
+
const action = await promptEditAction();
|
|
40
|
+
if (action === 'skip') {
|
|
41
|
+
ctx.log('\n Changes discarded.\n');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (action === 'retry') {
|
|
45
|
+
const newChange = await askChangeDescription();
|
|
46
|
+
await runEditStep(ctx, editCtx, newChange, flags);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
await finalizeEdit(ctx, editCtx, { flags, updated });
|
|
50
|
+
}
|
|
51
|
+
export async function runEditStep(ctx, editCtx, changeDescription, flags) {
|
|
52
|
+
const body = buildEditApiBody(changeDescription, editCtx.email, editCtx.templateHtml);
|
|
53
|
+
const response = await ctx.spinner(' Applying AI edits...', flags.json, () => ctx.post(API_ENDPOINTS.EDIT, body));
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
ctx.handleAiQuotaError(response, 'edit');
|
|
56
|
+
ctx.onApiError(response);
|
|
57
|
+
}
|
|
58
|
+
const updated = response.data;
|
|
59
|
+
if (flags.json) {
|
|
60
|
+
ctx.log(JSON.stringify(buildDiffPreview(editCtx.email, updated, editCtx.templateHtml), null, 2));
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
showChangeSummary(ctx.log, editCtx.email, updated, editCtx.templateHtml);
|
|
64
|
+
}
|
|
65
|
+
if (flags.yes) {
|
|
66
|
+
await finalizeEdit(ctx, editCtx, { flags, updated });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
await handleUserAction(ctx, editCtx, { flags, updated });
|
|
70
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { confirm } from '@inquirer/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { saveTemplate, saveYaml, } from '../../yaml-config.js';
|
|
4
|
+
function applyEmailChanges(email, updated) {
|
|
5
|
+
if (updated.subject)
|
|
6
|
+
email.subject = updated.subject;
|
|
7
|
+
if (updated.previewText)
|
|
8
|
+
email.previewText = updated.previewText;
|
|
9
|
+
if (updated.ctaText)
|
|
10
|
+
email.ctaText = updated.ctaText;
|
|
11
|
+
}
|
|
12
|
+
async function persistChanges(ctx, editCtx, updated) {
|
|
13
|
+
const updatedYaml = {
|
|
14
|
+
...editCtx.yamlConfig,
|
|
15
|
+
emails: [...editCtx.yamlConfig.emails],
|
|
16
|
+
};
|
|
17
|
+
updatedYaml.emails[editCtx.emailIndex] = editCtx.email;
|
|
18
|
+
await saveYaml(updatedYaml);
|
|
19
|
+
if (updated.html) {
|
|
20
|
+
await saveTemplate(editCtx.templateFilename, updated.html);
|
|
21
|
+
}
|
|
22
|
+
await ctx.syncYaml();
|
|
23
|
+
await ctx.syncTemplate(editCtx.email.id);
|
|
24
|
+
}
|
|
25
|
+
function logJsonResult(ctx, email, updated, oldSubject, oldPreviewText) {
|
|
26
|
+
ctx.log(JSON.stringify({
|
|
27
|
+
diff: {
|
|
28
|
+
previewText: updated.previewText && updated.previewText !== oldPreviewText
|
|
29
|
+
? { new: updated.previewText, old: oldPreviewText }
|
|
30
|
+
: undefined,
|
|
31
|
+
subject: oldSubject === email.subject
|
|
32
|
+
? undefined
|
|
33
|
+
: { new: email.subject, old: oldSubject },
|
|
34
|
+
},
|
|
35
|
+
email,
|
|
36
|
+
status: 'updated',
|
|
37
|
+
}, null, 2));
|
|
38
|
+
}
|
|
39
|
+
async function handleAcceptOutput(ctx, email) {
|
|
40
|
+
ctx.log(`\n Updated ${chalk.green('mailmodo.yaml')}`);
|
|
41
|
+
const shouldPreview = await confirm({
|
|
42
|
+
default: true,
|
|
43
|
+
message: 'Preview the change?',
|
|
44
|
+
});
|
|
45
|
+
if (shouldPreview) {
|
|
46
|
+
await ctx.runCommand('preview', [email.id]);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
ctx.log(` Run: ${chalk.cyan(`mailmodo preview ${email.id}`)}\n`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export async function finalizeEdit(ctx, editCtx, opts) {
|
|
53
|
+
const { flags, updated } = opts;
|
|
54
|
+
const oldSubject = editCtx.email.subject;
|
|
55
|
+
const oldPreviewText = editCtx.email.previewText;
|
|
56
|
+
applyEmailChanges(editCtx.email, updated);
|
|
57
|
+
await persistChanges(ctx, editCtx, updated);
|
|
58
|
+
if (flags.json) {
|
|
59
|
+
logJsonResult(ctx, editCtx.email, updated, oldSubject, oldPreviewText);
|
|
60
|
+
}
|
|
61
|
+
else if (flags.yes) {
|
|
62
|
+
ctx.log(`\n Updated ${chalk.green('mailmodo.yaml')}\n`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
await handleAcceptOutput(ctx, editCtx.email);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ApiResponse } from '../../api-client.js';
|
|
2
|
+
import type { EmailConfig, MailmodoYaml } from '../../yaml-config.js';
|
|
3
|
+
export interface EditResponse {
|
|
4
|
+
ctaText?: string;
|
|
5
|
+
html?: string;
|
|
6
|
+
previewText?: string;
|
|
7
|
+
subject?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface EditContext {
|
|
10
|
+
email: EmailConfig;
|
|
11
|
+
emailIndex: number;
|
|
12
|
+
templateFilename: string;
|
|
13
|
+
templateHtml: null | string;
|
|
14
|
+
yamlConfig: MailmodoYaml;
|
|
15
|
+
}
|
|
16
|
+
export type EditFlags = {
|
|
17
|
+
json: boolean;
|
|
18
|
+
yes: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type EditCtx = {
|
|
21
|
+
error(msg: string): never;
|
|
22
|
+
exit(code?: number): never;
|
|
23
|
+
handleAiQuotaError(resp: {
|
|
24
|
+
data?: unknown;
|
|
25
|
+
error?: string;
|
|
26
|
+
status: number;
|
|
27
|
+
}, feature: 'edit'): void;
|
|
28
|
+
log(msg?: string): void;
|
|
29
|
+
onApiError(resp: {
|
|
30
|
+
error?: string;
|
|
31
|
+
status: number;
|
|
32
|
+
}): never;
|
|
33
|
+
post<T>(path: string, body?: unknown): Promise<ApiResponse<T>>;
|
|
34
|
+
runCommand(id: string, argv: string[]): Promise<void>;
|
|
35
|
+
spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
|
|
36
|
+
syncTemplate(emailId: string): Promise<void>;
|
|
37
|
+
syncYaml(): Promise<void>;
|
|
38
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import open from 'open';
|
|
5
|
+
function trySpawnEditor(editor, filePath) {
|
|
6
|
+
const [cmd, args] = process.platform === 'win32'
|
|
7
|
+
? ['cmd.exe', ['/c', editor, filePath]]
|
|
8
|
+
: [editor, [filePath]];
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const child = spawn(cmd, [...args], { stdio: 'ignore' });
|
|
11
|
+
child.on('error', () => {
|
|
12
|
+
resolve(false);
|
|
13
|
+
});
|
|
14
|
+
child.on('close', (code) => {
|
|
15
|
+
resolve(code === 0);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export async function openTemplateInEditor(ctx, template) {
|
|
20
|
+
const templatePath = join(process.cwd(), template);
|
|
21
|
+
ctx.log(`\n Opening ${template}...\n`);
|
|
22
|
+
const editor = process.env.VISUAL || process.env.EDITOR;
|
|
23
|
+
if (editor) {
|
|
24
|
+
const [cmd, ...editorArgs] = editor.trim().split(/\s+/);
|
|
25
|
+
const launched = await new Promise((resolve) => {
|
|
26
|
+
const child = spawn(cmd, [...editorArgs, templatePath], {
|
|
27
|
+
stdio: 'inherit',
|
|
28
|
+
});
|
|
29
|
+
child.on('error', () => resolve(false));
|
|
30
|
+
child.on('close', (code) => resolve(code === 0));
|
|
31
|
+
});
|
|
32
|
+
if (launched)
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (await trySpawnEditor('code', templatePath))
|
|
36
|
+
return;
|
|
37
|
+
try {
|
|
38
|
+
await open(templatePath);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
ctx.log(` ${chalk.dim(`Could not open editor. Open the file manually: ${templatePath}`)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { EmailConfig } from '../../yaml-config.js';
|
|
2
|
+
import type { EmailsCtx } from './types.js';
|
|
3
|
+
export declare function renderEmailTable(ctx: EmailsCtx, emails: EmailConfig[]): void;
|
|
4
|
+
export declare function renderEmailDetail(ctx: EmailsCtx, email: EmailConfig): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export function renderEmailTable(ctx, emails) {
|
|
3
|
+
ctx.log(`\n ${chalk.bold(String(emails.length))} emails configured in mailmodo.yaml:\n`);
|
|
4
|
+
const idColW = Math.max(...emails.map((e) => e.id.length), 'ID'.length) + 2;
|
|
5
|
+
const triggerColW = Math.max(...emails.map((e) => e.trigger.length), 'Trigger'.length) + 2;
|
|
6
|
+
const delayColW = Math.max(...emails.map((e) => String(e.delay).length), 'Delay'.length) + 2;
|
|
7
|
+
const hasConditions = emails.some((e) => e.condition);
|
|
8
|
+
ctx.log(` ${chalk.bold('ID'.padEnd(idColW))}${chalk.bold('Trigger'.padEnd(triggerColW))}${chalk.bold('Delay'.padEnd(delayColW))}${hasConditions ? chalk.bold('Condition') : ''}`);
|
|
9
|
+
ctx.log(` ${'─'.repeat(idColW + triggerColW + delayColW + (hasConditions ? 'Condition'.length : 0))}`);
|
|
10
|
+
for (const email of emails) {
|
|
11
|
+
const id = chalk.cyan(email.id.padEnd(idColW));
|
|
12
|
+
const trigger = email.trigger.padEnd(triggerColW);
|
|
13
|
+
const delay = String(email.delay).padEnd(delayColW);
|
|
14
|
+
const condition = email.condition ? chalk.dim(email.condition) : '';
|
|
15
|
+
ctx.log(` ${id}${trigger}${delay}${condition}`);
|
|
16
|
+
}
|
|
17
|
+
ctx.log('');
|
|
18
|
+
}
|
|
19
|
+
export function renderEmailDetail(ctx, email) {
|
|
20
|
+
ctx.log('');
|
|
21
|
+
ctx.log(` ${chalk.bold('ID:')} ${email.id}`);
|
|
22
|
+
ctx.log(` ${chalk.bold('Trigger:')} ${email.trigger}`);
|
|
23
|
+
ctx.log(` ${chalk.bold('Delay:')} ${email.delay === 0 || email.delay === '0' ? '0 (immediate)' : email.delay}`);
|
|
24
|
+
ctx.log(` ${chalk.bold('Subject:')} ${email.subject}`);
|
|
25
|
+
ctx.log(` ${chalk.bold('Template:')} ${email.template}`);
|
|
26
|
+
if (email.style) {
|
|
27
|
+
ctx.log(` ${chalk.bold('Style:')} ${email.style}`);
|
|
28
|
+
}
|
|
29
|
+
if (email.condition) {
|
|
30
|
+
ctx.log(` ${chalk.bold('Condition:')} ${email.condition}`);
|
|
31
|
+
}
|
|
32
|
+
if (email.goal) {
|
|
33
|
+
ctx.log(` ${chalk.bold('Goal:')} ${email.goal}`);
|
|
34
|
+
}
|
|
35
|
+
ctx.log('');
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|