@mailmodo/cli 0.0.55 → 0.0.56-beta.pr58.101
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 +51 -200
- 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 +38 -10
- package/dist/lib/base-command.js +171 -18
- 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/dist/lib/yaml-config.d.ts +1 -0
- package/dist/lib/yaml-config.js +8 -0
- package/oclif.manifest.json +100 -100
- 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,7 +1,7 @@
|
|
|
1
1
|
import { confirm, input } from '@inquirer/prompts';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { API_ENDPOINTS } from '
|
|
4
|
-
import { ERRORS, INFO, PROMPTS } from '
|
|
3
|
+
import { API_ENDPOINTS } from '../../constants.js';
|
|
4
|
+
import { ERRORS, INFO, PROMPTS } from '../../messages.js';
|
|
5
5
|
export async function validateDeploySequence(ctx, payload, flags) {
|
|
6
6
|
const res = await ctx.spinner(' Validating sequence...', flags.json, () => ctx.post(API_ENDPOINTS.SEQUENCES_VALIDATE, payload));
|
|
7
7
|
if (!res.ok) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MailmodoYaml } from '
|
|
1
|
+
import type { MailmodoYaml } from '../../yaml-config.js';
|
|
2
2
|
import type { DeployCtx, SdkSnippet, ValidateResponse } from './types.js';
|
|
3
3
|
export declare function logDiff(ctx: DeployCtx, diff: NonNullable<ValidateResponse['diff']>): void;
|
|
4
4
|
export declare function logPreDeploySummary(ctx: DeployCtx, yamlConfig: MailmodoYaml, validateResult: ValidateResponse, jsonOutput: boolean): void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { SDK_IMPORT_SNIPPET, SDK_INSTALL_COMMAND } from '
|
|
3
|
-
import { DEPLOY, SEPARATOR } from '
|
|
2
|
+
import { SDK_IMPORT_SNIPPET, SDK_INSTALL_COMMAND } from '../../constants.js';
|
|
3
|
+
import { DEPLOY, SEPARATOR } from '../../messages.js';
|
|
4
4
|
export function logDiff(ctx, diff) {
|
|
5
5
|
if (!diff.hasChanges) {
|
|
6
6
|
ctx.log(` ${DEPLOY.NO_CHANGES}`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type EmailConfig, type MailmodoYaml, type ProjectConfig } from '
|
|
1
|
+
import { type EmailConfig, type MailmodoYaml, type ProjectConfig } from '../../yaml-config.js';
|
|
2
2
|
import type { DeployCtx } from './types.js';
|
|
3
3
|
export declare function mapEmailToPayload(email: EmailConfig): {
|
|
4
4
|
condition: string | null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { loadTemplate, } from '
|
|
2
|
-
import { DEFAULT_BRAND_COLOR } from '
|
|
1
|
+
import { loadTemplate, } from '../../yaml-config.js';
|
|
2
|
+
import { DEFAULT_BRAND_COLOR } from '../../constants.js';
|
|
3
3
|
export function mapEmailToPayload(email) {
|
|
4
4
|
return {
|
|
5
5
|
condition: email.condition || null,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { confirm } from '@inquirer/prompts';
|
|
2
|
-
import { API_ENDPOINTS } from '
|
|
3
|
-
import { INFO, pauseAlready, pauseSuccess, PROMPTS, resumeAlready, resumeSuccess, } from '
|
|
2
|
+
import { API_ENDPOINTS } from '../../constants.js';
|
|
3
|
+
import { INFO, pauseAlready, pauseSuccess, PROMPTS, resumeAlready, resumeSuccess, } from '../../messages.js';
|
|
4
4
|
function sequenceStatusPath(sequenceId) {
|
|
5
5
|
return `${API_ENDPOINTS.SEQUENCES}/${encodeURIComponent(sequenceId)}/status`;
|
|
6
6
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ApiResponse } from '
|
|
2
|
-
import type { MailmodoYaml } from '
|
|
3
|
-
import type { RegenCtx } from '
|
|
1
|
+
import type { ApiResponse } from '../../api-client.js';
|
|
2
|
+
import type { MailmodoYaml } from '../../yaml-config.js';
|
|
3
|
+
import type { RegenCtx } from '../../templates/types.js';
|
|
4
4
|
export interface EmailDiffEntry {
|
|
5
5
|
changedFields?: string[];
|
|
6
6
|
id: string;
|
|
@@ -85,4 +85,4 @@ export type DeployCtx = RegenCtx & {
|
|
|
85
85
|
spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
|
|
86
86
|
syncYaml(): Promise<void>;
|
|
87
87
|
};
|
|
88
|
-
export { type RegenCtx } from '
|
|
88
|
+
export { type RegenCtx } from '../../templates/types.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
function statusColor(status) {
|
|
3
|
+
if (status === 'active')
|
|
4
|
+
return chalk.green;
|
|
5
|
+
if (status === 'paused')
|
|
6
|
+
return chalk.yellow;
|
|
7
|
+
return chalk.white;
|
|
8
|
+
}
|
|
9
|
+
function colWidth(rows, key, header) {
|
|
10
|
+
return Math.max(...rows.map((r) => r[key].length), header.length) + 2;
|
|
11
|
+
}
|
|
12
|
+
function formatDate(iso) {
|
|
13
|
+
if (!iso)
|
|
14
|
+
return '';
|
|
15
|
+
const parsed = new Date(iso);
|
|
16
|
+
if (Number.isNaN(parsed.getTime()))
|
|
17
|
+
return iso;
|
|
18
|
+
return parsed.toISOString().slice(0, 10);
|
|
19
|
+
}
|
|
20
|
+
function logEmptyState(ctx) {
|
|
21
|
+
ctx.log(`\n ${chalk.dim('No deployed sequences yet.')}`);
|
|
22
|
+
ctx.log(` Run ${chalk.cyan('mailmodo deploy')} to deploy one.\n`);
|
|
23
|
+
}
|
|
24
|
+
function logTableHeader(ctx, count, widths) {
|
|
25
|
+
ctx.log(`\n ${chalk.bold(String(count))} deployed ${count === 1 ? 'sequence' : 'sequences'}:\n`);
|
|
26
|
+
ctx.log(` ${chalk.bold('Product'.padEnd(widths.product))}` +
|
|
27
|
+
`${chalk.bold('Status'.padEnd(widths.status))}` +
|
|
28
|
+
`${chalk.bold('Emails'.padEnd(widths.emails))}` +
|
|
29
|
+
`${chalk.bold('Sequence ID'.padEnd(widths.sequenceId))}` +
|
|
30
|
+
`${chalk.bold('Updated')}`);
|
|
31
|
+
ctx.log(` ${'─'.repeat(widths.product + widths.status + widths.emails + widths.sequenceId + widths.updated)}`);
|
|
32
|
+
}
|
|
33
|
+
function logTableRows(ctx, rows, widths) {
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
const status = statusColor(row.status)(row.status.padEnd(widths.status));
|
|
36
|
+
ctx.log(` ${row.product.padEnd(widths.product)}` +
|
|
37
|
+
`${status}` +
|
|
38
|
+
`${row.emails.padEnd(widths.emails)}` +
|
|
39
|
+
`${chalk.cyan(row.sequenceId.padEnd(widths.sequenceId))}` +
|
|
40
|
+
`${chalk.dim(row.updated)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function renderDeploymentsTable(ctx, data) {
|
|
44
|
+
const sequences = data.sequences ?? [];
|
|
45
|
+
if (sequences.length === 0) {
|
|
46
|
+
logEmptyState(ctx);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const rows = sequences.map((seq) => ({
|
|
50
|
+
emails: String(seq.emailCount ?? 0),
|
|
51
|
+
product: seq.productName ?? '',
|
|
52
|
+
sequenceId: seq.sequenceId ?? '',
|
|
53
|
+
status: seq.status ?? '',
|
|
54
|
+
updated: formatDate(seq.updatedAt),
|
|
55
|
+
}));
|
|
56
|
+
const widths = {
|
|
57
|
+
emails: colWidth(rows, 'emails', 'Emails'),
|
|
58
|
+
product: colWidth(rows, 'product', 'Product'),
|
|
59
|
+
sequenceId: colWidth(rows, 'sequenceId', 'Sequence ID'),
|
|
60
|
+
status: colWidth(rows, 'status', 'Status'),
|
|
61
|
+
updated: colWidth(rows, 'updated', 'Updated'),
|
|
62
|
+
};
|
|
63
|
+
logTableHeader(ctx, sequences.length, widths);
|
|
64
|
+
logTableRows(ctx, rows, widths);
|
|
65
|
+
ctx.log('');
|
|
66
|
+
ctx.log(` Pause: ${chalk.cyan('mailmodo deploy --pause <sequence-id>')}`);
|
|
67
|
+
ctx.log(` Resume: ${chalk.cyan('mailmodo deploy --resume <sequence-id>')}\n`);
|
|
68
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ApiResponse } from '../../api-client.js';
|
|
2
|
+
export interface Deployment {
|
|
3
|
+
createdAt: string;
|
|
4
|
+
emailCount: number;
|
|
5
|
+
productName: string;
|
|
6
|
+
senderDomain: string;
|
|
7
|
+
sequenceId: string;
|
|
8
|
+
status: 'active' | 'paused';
|
|
9
|
+
triggers: string[];
|
|
10
|
+
updatedAt: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DeploymentsResponse {
|
|
13
|
+
sequences: Deployment[];
|
|
14
|
+
total: number;
|
|
15
|
+
}
|
|
16
|
+
export type DeploymentsCtx = {
|
|
17
|
+
get<T = Record<string, unknown>>(path: string, params?: Record<string, string>): Promise<ApiResponse<T>>;
|
|
18
|
+
log(msg?: string): void;
|
|
19
|
+
onApiError(resp: {
|
|
20
|
+
error?: string;
|
|
21
|
+
status: number;
|
|
22
|
+
}): never;
|
|
23
|
+
spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
|
|
24
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DomainCtx, DomainFlags } from './types.js';
|
|
2
|
+
import type { MailmodoYaml } from '../../yaml-config.js';
|
|
3
|
+
export declare function setupDomain(ctx: DomainCtx, yamlConfig: MailmodoYaml, flags: DomainFlags): Promise<void>;
|
|
4
|
+
export declare function showDomainStatus(ctx: DomainCtx, opts: {
|
|
5
|
+
domain: string;
|
|
6
|
+
json: boolean;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
export declare function getDomainOrError(ctx: DomainCtx, yamlConfig: MailmodoYaml): string;
|
|
@@ -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
|
+
}
|