@mailmodo/cli 0.0.54 → 0.0.55-beta.pr57.92
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 -181
- package/dist/commands/contacts/index.d.ts +1 -19
- package/dist/commands/contacts/index.js +21 -114
- package/dist/commands/deploy/index.d.ts +1 -32
- package/dist/commands/deploy/index.js +52 -303
- 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 +35 -244
- 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 -2
- package/dist/commands/init/index.js +43 -179
- package/dist/commands/login/index.d.ts +2 -0
- package/dist/commands/login/index.js +35 -64
- 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 +40 -210
- 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 +35 -241
- package/dist/commands/status/index.d.ts +1 -0
- package/dist/commands/status/index.js +13 -39
- package/dist/lib/api-client.d.ts +5 -0
- package/dist/lib/api-client.js +45 -0
- package/dist/lib/base-command.d.ts +25 -1
- package/dist/lib/base-command.js +91 -5
- 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/billing/types.js +1 -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/commands/deploy/domain-setup.d.ts +8 -0
- package/dist/lib/commands/deploy/domain-setup.js +82 -0
- package/dist/lib/commands/deploy/output.d.ts +5 -0
- package/dist/lib/commands/deploy/output.js +61 -0
- package/dist/lib/commands/deploy/payload.d.ts +41 -0
- package/dist/lib/commands/deploy/payload.js +95 -0
- package/dist/lib/commands/deploy/sequence-status.d.ts +3 -0
- package/dist/lib/commands/deploy/sequence-status.js +56 -0
- package/dist/lib/commands/deploy/types.d.ts +88 -0
- 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 +65 -0
- package/dist/lib/commands/edit/types.d.ts +37 -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 +69 -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 +53 -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 +19 -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 +1 -0
- package/dist/lib/constants.js +1 -0
- package/dist/lib/messages.d.ts +22 -0
- package/dist/lib/messages.js +22 -0
- package/dist/lib/templates/missing-templates.d.ts +5 -0
- package/dist/lib/templates/missing-templates.js +61 -0
- package/dist/lib/templates/types.d.ts +13 -0
- package/dist/lib/templates/types.js +1 -0
- package/oclif.manifest.json +66 -66
- package/package.json +1 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { loadTemplate, } from '../../yaml-config.js';
|
|
2
|
+
import { DEFAULT_BRAND_COLOR } from '../../constants.js';
|
|
3
|
+
export function mapEmailToPayload(email) {
|
|
4
|
+
return {
|
|
5
|
+
condition: email.condition || null,
|
|
6
|
+
ctaText: email.ctaText || '',
|
|
7
|
+
delay: email.delay,
|
|
8
|
+
goal: email.goal || '',
|
|
9
|
+
id: email.id,
|
|
10
|
+
isReminder: false,
|
|
11
|
+
previewText: email.previewText || '',
|
|
12
|
+
priority: 'medium',
|
|
13
|
+
subject: email.subject,
|
|
14
|
+
trigger: email.trigger,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function buildBrandSection(project) {
|
|
18
|
+
return {
|
|
19
|
+
colors: [project?.brandColor || DEFAULT_BRAND_COLOR],
|
|
20
|
+
logoUrl: project?.logoUrl || '',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function buildProductSection(project) {
|
|
24
|
+
return {
|
|
25
|
+
businessType: project?.type || '',
|
|
26
|
+
description: project?.description || '',
|
|
27
|
+
pricingModel: project?.pricingModel || '',
|
|
28
|
+
productName: project?.name || '',
|
|
29
|
+
saasModel: project?.saasModel || '',
|
|
30
|
+
targetUser: project?.targetUser || '',
|
|
31
|
+
url: project?.url || '',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function buildSenderSection(project) {
|
|
35
|
+
return {
|
|
36
|
+
address: project?.address || '',
|
|
37
|
+
domain: project?.domain || '',
|
|
38
|
+
fromEmail: project?.fromEmail || '',
|
|
39
|
+
fromName: project?.fromName || '',
|
|
40
|
+
replyTo: project?.replyTo || project?.fromEmail || '',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function buildProjectPayload(project, monthlyCap) {
|
|
44
|
+
return {
|
|
45
|
+
brand: buildBrandSection(project),
|
|
46
|
+
emailStyle: project?.emailStyle || 'branded',
|
|
47
|
+
...(monthlyCap === undefined ? {} : { monthlyCap }),
|
|
48
|
+
product: buildProductSection(project),
|
|
49
|
+
senderDetails: buildSenderSection(project),
|
|
50
|
+
...(project?.webhookUrl ? { webhookUrl: project.webhookUrl } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function buildRegeneratePayload(yamlConfig, missingIds) {
|
|
54
|
+
const { emails, project } = yamlConfig;
|
|
55
|
+
const idSet = new Set(missingIds);
|
|
56
|
+
const targets = emails.filter((e) => idSet.has(e.id));
|
|
57
|
+
return {
|
|
58
|
+
brand: {
|
|
59
|
+
color: project.brandColor || DEFAULT_BRAND_COLOR,
|
|
60
|
+
logoUrl: project.logoUrl || '',
|
|
61
|
+
},
|
|
62
|
+
businessType: project.type || '',
|
|
63
|
+
description: project.description || '',
|
|
64
|
+
events: [...new Set(targets.map((e) => e.trigger))],
|
|
65
|
+
pricingModel: project.pricingModel || '',
|
|
66
|
+
productName: project.name || '',
|
|
67
|
+
recommendedEmails: targets.map((e) => ({
|
|
68
|
+
condition: e.condition || null,
|
|
69
|
+
delay: String(e.delay ?? '0'),
|
|
70
|
+
goal: e.goal || '',
|
|
71
|
+
id: e.id,
|
|
72
|
+
isReminder: false,
|
|
73
|
+
priority: 'medium',
|
|
74
|
+
trigger: e.trigger,
|
|
75
|
+
})),
|
|
76
|
+
saasModel: project.saasModel || '',
|
|
77
|
+
targetUser: project.targetUser || '',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export async function buildDeployPayload(ctx, yamlConfig) {
|
|
81
|
+
const [emailsWithHtml, monthlyCap] = await Promise.all([
|
|
82
|
+
Promise.all(yamlConfig.emails.map(async (email) => {
|
|
83
|
+
const html = (await loadTemplate(`${email.id}.html`)) || '';
|
|
84
|
+
const plainHtml = (await loadTemplate(`${email.id}_plain.html`)) || html;
|
|
85
|
+
return { ...mapEmailToPayload(email), html, plainHtml };
|
|
86
|
+
})),
|
|
87
|
+
yamlConfig.project.monthlyCap === undefined
|
|
88
|
+
? ctx.getBillingCap()
|
|
89
|
+
: Promise.resolve(yamlConfig.project.monthlyCap),
|
|
90
|
+
]);
|
|
91
|
+
return {
|
|
92
|
+
...buildProjectPayload(yamlConfig.project, monthlyCap),
|
|
93
|
+
emails: emailsWithHtml,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { DeployCtx, DeployFlags } from './types.js';
|
|
2
|
+
export declare function pauseSequence(ctx: DeployCtx, sequenceId: string, flags: DeployFlags): Promise<void>;
|
|
3
|
+
export declare function resumeSequence(ctx: DeployCtx, sequenceId: string, flags: DeployFlags): Promise<void>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { confirm } from '@inquirer/prompts';
|
|
2
|
+
import { API_ENDPOINTS } from '../../constants.js';
|
|
3
|
+
import { INFO, pauseAlready, pauseSuccess, PROMPTS, resumeAlready, resumeSuccess, } from '../../messages.js';
|
|
4
|
+
function sequenceStatusPath(sequenceId) {
|
|
5
|
+
return `${API_ENDPOINTS.SEQUENCES}/${encodeURIComponent(sequenceId)}/status`;
|
|
6
|
+
}
|
|
7
|
+
async function updateSequenceStatus(ctx, opts) {
|
|
8
|
+
const response = await ctx.spinner(opts.spinnerText, opts.flags.json, () => ctx.post(sequenceStatusPath(opts.sequenceId), {
|
|
9
|
+
status: opts.status,
|
|
10
|
+
}));
|
|
11
|
+
if (!response.ok)
|
|
12
|
+
ctx.onApiError(response);
|
|
13
|
+
return response.data;
|
|
14
|
+
}
|
|
15
|
+
export async function pauseSequence(ctx, sequenceId, flags) {
|
|
16
|
+
if (!flags.yes) {
|
|
17
|
+
const confirmed = await confirm({
|
|
18
|
+
default: false,
|
|
19
|
+
message: PROMPTS.PAUSE_CONFIRM,
|
|
20
|
+
});
|
|
21
|
+
if (!confirmed) {
|
|
22
|
+
ctx.log(`\n ${INFO.PAUSE_CANCELLED}\n`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const data = await updateSequenceStatus(ctx, {
|
|
27
|
+
flags,
|
|
28
|
+
sequenceId,
|
|
29
|
+
spinnerText: ' Pausing sequence...',
|
|
30
|
+
status: 'paused',
|
|
31
|
+
});
|
|
32
|
+
if (flags.json) {
|
|
33
|
+
ctx.log(JSON.stringify(data, null, 2));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const msg = data.alreadyInStatus
|
|
37
|
+
? pauseAlready(data.sequenceId || sequenceId)
|
|
38
|
+
: pauseSuccess(data.sequenceId || sequenceId);
|
|
39
|
+
ctx.log(`\n ${msg}\n`);
|
|
40
|
+
}
|
|
41
|
+
export async function resumeSequence(ctx, sequenceId, flags) {
|
|
42
|
+
const data = await updateSequenceStatus(ctx, {
|
|
43
|
+
flags,
|
|
44
|
+
sequenceId,
|
|
45
|
+
spinnerText: ' Resuming sequence...',
|
|
46
|
+
status: 'active',
|
|
47
|
+
});
|
|
48
|
+
if (flags.json) {
|
|
49
|
+
ctx.log(JSON.stringify(data, null, 2));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const msg = data.alreadyInStatus
|
|
53
|
+
? resumeAlready(data.sequenceId || sequenceId)
|
|
54
|
+
: resumeSuccess(data.sequenceId || sequenceId);
|
|
55
|
+
ctx.log(`\n ${msg}\n`);
|
|
56
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
export interface EmailDiffEntry {
|
|
5
|
+
changedFields?: string[];
|
|
6
|
+
id: string;
|
|
7
|
+
subject?: string;
|
|
8
|
+
trigger?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ValidateResponse {
|
|
11
|
+
diff: null | {
|
|
12
|
+
added: EmailDiffEntry[];
|
|
13
|
+
hasChanges: boolean;
|
|
14
|
+
modified: EmailDiffEntry[];
|
|
15
|
+
removed: EmailDiffEntry[];
|
|
16
|
+
unchanged: EmailDiffEntry[];
|
|
17
|
+
};
|
|
18
|
+
error: null | string;
|
|
19
|
+
existingDeployment: boolean;
|
|
20
|
+
isValid: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface SdkSnippet {
|
|
23
|
+
examples: {
|
|
24
|
+
identify: string;
|
|
25
|
+
track: string;
|
|
26
|
+
};
|
|
27
|
+
identifyCalls: string[];
|
|
28
|
+
install: string;
|
|
29
|
+
trackCalls: string[];
|
|
30
|
+
}
|
|
31
|
+
export interface DeployResponse {
|
|
32
|
+
deployed: boolean;
|
|
33
|
+
diff: {
|
|
34
|
+
added: EmailDiffEntry[];
|
|
35
|
+
hasChanges: boolean;
|
|
36
|
+
modified: EmailDiffEntry[];
|
|
37
|
+
removed: EmailDiffEntry[];
|
|
38
|
+
unchanged: EmailDiffEntry[];
|
|
39
|
+
};
|
|
40
|
+
emailsLive: number;
|
|
41
|
+
sdkSnippet: SdkSnippet;
|
|
42
|
+
sequenceId: string;
|
|
43
|
+
}
|
|
44
|
+
export type DeployFlags = {
|
|
45
|
+
json: boolean;
|
|
46
|
+
yes: boolean;
|
|
47
|
+
};
|
|
48
|
+
export type DeployCtx = RegenCtx & {
|
|
49
|
+
collectDomainInputs(yaml: MailmodoYaml, skip: boolean): Promise<{
|
|
50
|
+
address: string;
|
|
51
|
+
domain: string;
|
|
52
|
+
fromEmail: string;
|
|
53
|
+
fromName: string;
|
|
54
|
+
replyTo: string;
|
|
55
|
+
}>;
|
|
56
|
+
error(msg: string): never;
|
|
57
|
+
exit(code?: number): never;
|
|
58
|
+
get<T = Record<string, unknown>>(path: string, params?: Record<string, string>): Promise<ApiResponse<T>>;
|
|
59
|
+
getBillingCap(): Promise<number | undefined>;
|
|
60
|
+
log(msg?: string): void;
|
|
61
|
+
onApiError(resp: {
|
|
62
|
+
error?: string;
|
|
63
|
+
status: number;
|
|
64
|
+
}): never;
|
|
65
|
+
post<T = Record<string, unknown>>(path: string, body?: Record<string, unknown> | unknown): Promise<ApiResponse<T>>;
|
|
66
|
+
registerDomainAndSave(yaml: MailmodoYaml, inputs: {
|
|
67
|
+
address: string;
|
|
68
|
+
domain: string;
|
|
69
|
+
fromEmail: string;
|
|
70
|
+
fromName?: string;
|
|
71
|
+
replyTo?: string;
|
|
72
|
+
}, json: boolean): Promise<{
|
|
73
|
+
dnsGuideUrl?: string;
|
|
74
|
+
dnsRecords: Array<{
|
|
75
|
+
host: string;
|
|
76
|
+
type: string;
|
|
77
|
+
value: string;
|
|
78
|
+
}>;
|
|
79
|
+
}>;
|
|
80
|
+
showDnsRecords(records: Array<{
|
|
81
|
+
host: string;
|
|
82
|
+
type: string;
|
|
83
|
+
value: string;
|
|
84
|
+
}>, guideUrl: string | undefined, json: boolean): void;
|
|
85
|
+
spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
|
|
86
|
+
syncYaml(): Promise<void>;
|
|
87
|
+
};
|
|
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>;
|