@mailmodo/cli 0.0.54-beta.pr56.91 → 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.
Files changed (125) hide show
  1. package/dist/commands/billing/index.d.ts +1 -11
  2. package/dist/commands/billing/index.js +28 -184
  3. package/dist/commands/contacts/index.d.ts +1 -19
  4. package/dist/commands/contacts/index.js +21 -114
  5. package/dist/commands/deploy/index.js +4 -4
  6. package/dist/commands/deployments/index.d.ts +1 -4
  7. package/dist/commands/deployments/index.js +11 -52
  8. package/dist/commands/domain/index.d.ts +1 -14
  9. package/dist/commands/domain/index.js +19 -100
  10. package/dist/commands/edit/index.d.ts +2 -20
  11. package/dist/commands/edit/index.js +30 -258
  12. package/dist/commands/emails/index.d.ts +1 -2
  13. package/dist/commands/emails/index.js +26 -91
  14. package/dist/commands/init/index.d.ts +1 -3
  15. package/dist/commands/init/index.js +41 -199
  16. package/dist/commands/login/index.d.ts +2 -0
  17. package/dist/commands/login/index.js +32 -76
  18. package/dist/commands/logs/index.d.ts +1 -8
  19. package/dist/commands/logs/index.js +12 -55
  20. package/dist/commands/preview/index.d.ts +1 -19
  21. package/dist/commands/preview/index.js +30 -212
  22. package/dist/commands/sdk/index.d.ts +1 -3
  23. package/dist/commands/sdk/index.js +14 -46
  24. package/dist/commands/settings/index.d.ts +1 -22
  25. package/dist/commands/settings/index.js +34 -246
  26. package/dist/commands/status/index.d.ts +1 -0
  27. package/dist/commands/status/index.js +13 -39
  28. package/dist/lib/commands/billing/checkout-status.d.ts +3 -0
  29. package/dist/lib/commands/billing/checkout-status.js +63 -0
  30. package/dist/lib/commands/billing/format.d.ts +7 -0
  31. package/dist/lib/commands/billing/format.js +63 -0
  32. package/dist/lib/commands/billing/purchase-cap.d.ts +7 -0
  33. package/dist/lib/commands/billing/purchase-cap.js +57 -0
  34. package/dist/lib/commands/billing/types.d.ts +72 -0
  35. package/dist/lib/commands/contacts/actions.d.ts +3 -0
  36. package/dist/lib/commands/contacts/actions.js +49 -0
  37. package/dist/lib/commands/contacts/export-delete.d.ts +9 -0
  38. package/dist/lib/commands/contacts/export-delete.js +51 -0
  39. package/dist/lib/commands/contacts/types.d.ts +35 -0
  40. package/dist/lib/commands/contacts/types.js +1 -0
  41. package/dist/lib/{deploy → commands/deploy}/domain-setup.d.ts +1 -1
  42. package/dist/lib/{deploy → commands/deploy}/domain-setup.js +2 -2
  43. package/dist/lib/{deploy → commands/deploy}/output.d.ts +1 -1
  44. package/dist/lib/{deploy → commands/deploy}/output.js +2 -2
  45. package/dist/lib/{deploy → commands/deploy}/payload.d.ts +1 -1
  46. package/dist/lib/{deploy → commands/deploy}/payload.js +2 -2
  47. package/dist/lib/{deploy → commands/deploy}/sequence-status.js +2 -2
  48. package/dist/lib/{deploy → commands/deploy}/types.d.ts +4 -4
  49. package/dist/lib/commands/deploy/types.js +1 -0
  50. package/dist/lib/commands/deployments/output.d.ts +2 -0
  51. package/dist/lib/commands/deployments/output.js +68 -0
  52. package/dist/lib/commands/deployments/types.d.ts +24 -0
  53. package/dist/lib/commands/deployments/types.js +1 -0
  54. package/dist/lib/commands/domain/setup.d.ts +8 -0
  55. package/dist/lib/commands/domain/setup.js +53 -0
  56. package/dist/lib/commands/domain/types.d.ts +56 -0
  57. package/dist/lib/commands/domain/types.js +1 -0
  58. package/dist/lib/commands/domain/verify.d.ts +5 -0
  59. package/dist/lib/commands/domain/verify.js +50 -0
  60. package/dist/lib/commands/edit/diff.d.ts +7 -0
  61. package/dist/lib/commands/edit/diff.js +65 -0
  62. package/dist/lib/commands/edit/display.d.ts +5 -0
  63. package/dist/lib/commands/edit/display.js +53 -0
  64. package/dist/lib/commands/edit/flow.d.ts +8 -0
  65. package/dist/lib/commands/edit/flow.js +70 -0
  66. package/dist/lib/commands/edit/persist.d.ts +5 -0
  67. package/dist/lib/commands/edit/persist.js +65 -0
  68. package/dist/lib/commands/edit/types.d.ts +37 -0
  69. package/dist/lib/commands/edit/types.js +1 -0
  70. package/dist/lib/commands/emails/editor.d.ts +2 -0
  71. package/dist/lib/commands/emails/editor.js +43 -0
  72. package/dist/lib/commands/emails/output.d.ts +4 -0
  73. package/dist/lib/commands/emails/output.js +36 -0
  74. package/dist/lib/commands/emails/types.d.ts +3 -0
  75. package/dist/lib/commands/emails/types.js +1 -0
  76. package/dist/lib/commands/init/analysis.d.ts +3 -0
  77. package/dist/lib/commands/init/analysis.js +69 -0
  78. package/dist/lib/commands/init/output.d.ts +12 -0
  79. package/dist/lib/commands/init/output.js +39 -0
  80. package/dist/lib/commands/init/payload.d.ts +8 -0
  81. package/dist/lib/commands/init/payload.js +78 -0
  82. package/dist/lib/commands/init/types.d.ts +57 -0
  83. package/dist/lib/commands/init/types.js +1 -0
  84. package/dist/lib/commands/login/output.d.ts +8 -0
  85. package/dist/lib/commands/login/output.js +53 -0
  86. package/dist/lib/commands/login/types.d.ts +19 -0
  87. package/dist/lib/commands/login/types.js +1 -0
  88. package/dist/lib/commands/logs/output.d.ts +2 -0
  89. package/dist/lib/commands/logs/output.js +52 -0
  90. package/dist/lib/commands/logs/types.d.ts +23 -0
  91. package/dist/lib/commands/logs/types.js +1 -0
  92. package/dist/lib/commands/preview/actions.d.ts +11 -0
  93. package/dist/lib/commands/preview/actions.js +43 -0
  94. package/dist/lib/commands/preview/render.d.ts +3 -0
  95. package/dist/lib/commands/preview/render.js +30 -0
  96. package/dist/lib/commands/preview/server.d.ts +8 -0
  97. package/dist/lib/commands/preview/server.js +63 -0
  98. package/dist/lib/commands/preview/types.d.ts +19 -0
  99. package/dist/lib/commands/preview/types.js +1 -0
  100. package/dist/lib/commands/preview/wrapper-html.d.ts +2 -0
  101. package/dist/lib/commands/preview/wrapper-html.js +35 -0
  102. package/dist/lib/commands/sdk/output.d.ts +2 -0
  103. package/dist/lib/commands/sdk/output.js +42 -0
  104. package/dist/lib/commands/sdk/types.d.ts +21 -0
  105. package/dist/lib/commands/sdk/types.js +1 -0
  106. package/dist/lib/commands/settings/actions.d.ts +10 -0
  107. package/dist/lib/commands/settings/actions.js +56 -0
  108. package/dist/lib/commands/settings/display.d.ts +15 -0
  109. package/dist/lib/commands/settings/display.js +69 -0
  110. package/dist/lib/commands/settings/logo-domain.d.ts +3 -0
  111. package/dist/lib/commands/settings/logo-domain.js +47 -0
  112. package/dist/lib/commands/settings/prompt.d.ts +2 -0
  113. package/dist/lib/commands/settings/prompt.js +82 -0
  114. package/dist/lib/commands/settings/types.d.ts +65 -0
  115. package/dist/lib/commands/settings/types.js +1 -0
  116. package/dist/lib/commands/status/output.d.ts +2 -0
  117. package/dist/lib/commands/status/output.js +49 -0
  118. package/dist/lib/commands/status/types.d.ts +28 -0
  119. package/dist/lib/commands/status/types.js +1 -0
  120. package/dist/lib/templates/missing-templates.d.ts +1 -1
  121. package/dist/lib/templates/missing-templates.js +1 -1
  122. package/oclif.manifest.json +48 -48
  123. package/package.json +1 -1
  124. /package/dist/lib/{deploy → commands/billing}/types.js +0 -0
  125. /package/dist/lib/{deploy → commands/deploy}/sequence-status.d.ts +0 -0
@@ -0,0 +1,3 @@
1
+ import type { ContactsCtx } from './types.js';
2
+ export declare function showSummary(ctx: ContactsCtx, json: boolean): Promise<void>;
3
+ export declare function searchContact(ctx: ContactsCtx, email: string, json: boolean): Promise<void>;
@@ -0,0 +1,49 @@
1
+ import chalk from 'chalk';
2
+ import { API_ENDPOINTS } from '../../constants.js';
3
+ export async function showSummary(ctx, json) {
4
+ const response = await ctx.spinner(' Loading contacts...', json, () => ctx.get(API_ENDPOINTS.CONTACTS));
5
+ if (!response.ok) {
6
+ ctx.onApiError(response);
7
+ }
8
+ const { data } = response;
9
+ if (json) {
10
+ ctx.log(JSON.stringify(data, null, 2));
11
+ return;
12
+ }
13
+ ctx.log(`\n Total: ${chalk.bold(String(data.total ?? 0))} ` +
14
+ `Active: ${chalk.green(String(data.active ?? 0))} ` +
15
+ `Unsubscribed: ${chalk.yellow(String(data.unsubscribed ?? 0))} ` +
16
+ `Bounced: ${chalk.red(String(data.bounced ?? 0))}\n`);
17
+ }
18
+ export async function searchContact(ctx, email, json) {
19
+ const encodedEmail = encodeURIComponent(email);
20
+ const response = await ctx.spinner(' Looking up contact...', json, () => ctx.get(`${API_ENDPOINTS.CONTACTS}/${encodedEmail}`));
21
+ if (!response.ok) {
22
+ if (response.status === 404) {
23
+ if (json) {
24
+ ctx.log(JSON.stringify({ email, error: 'Contact not found' }, null, 2));
25
+ }
26
+ else {
27
+ ctx.log(`\n Contact '${email}' not found.\n`);
28
+ }
29
+ return;
30
+ }
31
+ ctx.onApiError(response);
32
+ }
33
+ const contact = response.data;
34
+ if (json) {
35
+ ctx.log(JSON.stringify(contact, null, 2));
36
+ return;
37
+ }
38
+ ctx.log(`\n ${chalk.bold('Email:')} ${contact.email}`);
39
+ ctx.log(` ${chalk.bold('Status:')} ${contact.status}`);
40
+ ctx.log(` ${chalk.bold('Added:')} ${contact.added}`);
41
+ ctx.log(` ${chalk.bold('Next email:')} ${contact.nextEmail || 'None'}`);
42
+ if (contact.properties && Object.keys(contact.properties).length > 0) {
43
+ ctx.log(` ${chalk.bold('Properties:')}`);
44
+ for (const [key, value] of Object.entries(contact.properties)) {
45
+ ctx.log(` ${key}: ${String(value)}`);
46
+ }
47
+ }
48
+ ctx.log('');
49
+ }
@@ -0,0 +1,9 @@
1
+ import type { ContactsCtx } from './types.js';
2
+ export declare function exportContacts(ctx: ContactsCtx, opts: {
3
+ json: boolean;
4
+ }): Promise<void>;
5
+ export declare function deleteContact(ctx: ContactsCtx, opts: {
6
+ email: string;
7
+ json: boolean;
8
+ skipConfirm: boolean;
9
+ }): Promise<void>;
@@ -0,0 +1,51 @@
1
+ import { confirm } from '@inquirer/prompts';
2
+ import chalk from 'chalk';
3
+ import { writeFile } from 'node:fs/promises';
4
+ import { resolve } from 'node:path';
5
+ import { API_ENDPOINTS } from '../../constants.js';
6
+ export async function exportContacts(ctx, opts) {
7
+ const { json } = opts;
8
+ const response = await ctx.spinner(' Preparing contact export...', json, () => ctx.get(API_ENDPOINTS.CONTACTS_EXPORT));
9
+ if (!response.ok) {
10
+ ctx.onApiError(response);
11
+ }
12
+ if (json) {
13
+ ctx.log(JSON.stringify(response.data, null, 2));
14
+ return;
15
+ }
16
+ ctx.log(`\n ${chalk.green('✓')} Contact export started.`);
17
+ const { downloadUrl, status } = response.data;
18
+ if (!downloadUrl) {
19
+ ctx.log(`\n Export status: ${status ?? 'unknown'}. No download URL yet.\n`);
20
+ return;
21
+ }
22
+ const fileResult = await ctx.spinner(' Downloading CSV file...', json, () => ctx.getPublicFile(downloadUrl.trim()));
23
+ if (!fileResult.ok) {
24
+ ctx.error(`Download failed: ${fileResult.status} ${fileResult.error ?? ''}\n URL: ${fileResult.debug.fullUrl}`);
25
+ }
26
+ const outputPath = resolve('contacts.csv');
27
+ await writeFile(outputPath, Buffer.from(fileResult.body));
28
+ ctx.log(`\n ${chalk.green('✓')} Contact export saved to ${chalk.cyan(outputPath)}\n`);
29
+ }
30
+ export async function deleteContact(ctx, opts) {
31
+ const { email, json, skipConfirm } = opts;
32
+ if (!skipConfirm) {
33
+ const confirmed = await confirm({
34
+ default: false,
35
+ message: `Permanently delete ${email} and all their data? This cannot be undone.`,
36
+ });
37
+ if (!confirmed) {
38
+ ctx.log('\n Delete cancelled.\n');
39
+ return;
40
+ }
41
+ }
42
+ const response = await ctx.spinner(' Deleting contact...', json, () => ctx.delete(`${API_ENDPOINTS.CONTACTS}/${encodeURIComponent(email)}`));
43
+ if (!response.ok) {
44
+ ctx.onApiError(response);
45
+ }
46
+ if (json) {
47
+ ctx.log(JSON.stringify({ deleted: true, email }, null, 2));
48
+ return;
49
+ }
50
+ ctx.log(`\n ${chalk.green('✓')} Contact ${email} permanently deleted.\n`);
51
+ }
@@ -0,0 +1,35 @@
1
+ import type { ApiResponse } from '../../api-client.js';
2
+ import type { FileFetchResult } from '../../fetch-file.js';
3
+ export interface ContactSummaryResponse {
4
+ active: number;
5
+ bounced: number;
6
+ total: number;
7
+ unsubscribed: number;
8
+ }
9
+ export interface ContactDetailResponse {
10
+ added: string;
11
+ email: string;
12
+ nextEmail: null | string;
13
+ properties: Record<string, unknown>;
14
+ status: string;
15
+ }
16
+ export interface ContactExportResponse {
17
+ downloadUrl: string;
18
+ status: string;
19
+ }
20
+ export type ContactsFlags = {
21
+ json: boolean;
22
+ yes: boolean;
23
+ };
24
+ export type ContactsCtx = {
25
+ delete<T = Record<string, unknown>>(path: string): Promise<ApiResponse<T>>;
26
+ error(msg: string): never;
27
+ get<T>(path: string, params?: Record<string, string>): Promise<ApiResponse<T>>;
28
+ getPublicFile(url: string): Promise<FileFetchResult>;
29
+ log(msg?: string): void;
30
+ onApiError(resp: {
31
+ error?: string;
32
+ status: number;
33
+ }): never;
34
+ spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
35
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- import type { MailmodoYaml } from '../yaml-config.js';
1
+ import type { MailmodoYaml } from '../../yaml-config.js';
2
2
  import type { DeployCtx, DeployFlags, ValidateResponse } from './types.js';
3
3
  export declare function validateDeploySequence(ctx: DeployCtx, payload: object, flags: {
4
4
  json: boolean;
@@ -1,7 +1,7 @@
1
1
  import { confirm, input } from '@inquirer/prompts';
2
2
  import chalk from 'chalk';
3
- import { API_ENDPOINTS } from '../constants.js';
4
- import { ERRORS, INFO, PROMPTS } from '../messages.js';
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 '../yaml-config.js';
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 '../constants.js';
3
- import { DEPLOY, SEPARATOR } from '../messages.js';
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 '../yaml-config.js';
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 '../yaml-config.js';
2
- import { DEFAULT_BRAND_COLOR } from '../constants.js';
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 '../constants.js';
3
- import { INFO, pauseAlready, pauseSuccess, PROMPTS, resumeAlready, resumeSuccess, } from '../messages.js';
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 '../api-client.js';
2
- import type { MailmodoYaml } from '../yaml-config.js';
3
- import type { RegenCtx } from '../templates/types.js';
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 '../templates/types.js';
88
+ export { type RegenCtx } from '../../templates/types.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { DeploymentsCtx, DeploymentsResponse } from './types.js';
2
+ export declare function renderDeploymentsTable(ctx: DeploymentsCtx, data: DeploymentsResponse): void;
@@ -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,5 @@
1
+ import type { DomainCtx } from './types.js';
2
+ export declare function verifyDomain(ctx: DomainCtx, opts: {
3
+ domain: string;
4
+ json: boolean;
5
+ }): Promise<void>;
@@ -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('&nbsp;', ' ')
8
+ .replaceAll('&amp;', '&')
9
+ .replaceAll('&lt;', '<')
10
+ .replaceAll('&gt;', '>')
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
+ }