@mailmodo/cli 0.0.54 → 0.0.55-beta.pr57.93

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 (136) hide show
  1. package/dist/commands/billing/index.d.ts +1 -11
  2. package/dist/commands/billing/index.js +28 -181
  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.d.ts +1 -32
  6. package/dist/commands/deploy/index.js +52 -303
  7. package/dist/commands/deployments/index.d.ts +1 -4
  8. package/dist/commands/deployments/index.js +11 -52
  9. package/dist/commands/domain/index.d.ts +1 -14
  10. package/dist/commands/domain/index.js +19 -100
  11. package/dist/commands/edit/index.d.ts +2 -20
  12. package/dist/commands/edit/index.js +35 -244
  13. package/dist/commands/emails/index.d.ts +1 -2
  14. package/dist/commands/emails/index.js +26 -91
  15. package/dist/commands/init/index.d.ts +1 -2
  16. package/dist/commands/init/index.js +43 -179
  17. package/dist/commands/login/index.d.ts +2 -0
  18. package/dist/commands/login/index.js +35 -64
  19. package/dist/commands/logs/index.d.ts +1 -8
  20. package/dist/commands/logs/index.js +12 -55
  21. package/dist/commands/preview/index.d.ts +1 -19
  22. package/dist/commands/preview/index.js +40 -210
  23. package/dist/commands/sdk/index.d.ts +1 -3
  24. package/dist/commands/sdk/index.js +14 -46
  25. package/dist/commands/settings/index.d.ts +1 -22
  26. package/dist/commands/settings/index.js +35 -241
  27. package/dist/commands/status/index.d.ts +1 -0
  28. package/dist/commands/status/index.js +13 -39
  29. package/dist/lib/api-client.d.ts +5 -0
  30. package/dist/lib/api-client.js +45 -0
  31. package/dist/lib/base-command.d.ts +25 -1
  32. package/dist/lib/base-command.js +91 -5
  33. package/dist/lib/commands/billing/checkout-status.d.ts +3 -0
  34. package/dist/lib/commands/billing/checkout-status.js +63 -0
  35. package/dist/lib/commands/billing/format.d.ts +7 -0
  36. package/dist/lib/commands/billing/format.js +63 -0
  37. package/dist/lib/commands/billing/purchase-cap.d.ts +7 -0
  38. package/dist/lib/commands/billing/purchase-cap.js +57 -0
  39. package/dist/lib/commands/billing/types.d.ts +72 -0
  40. package/dist/lib/commands/billing/types.js +1 -0
  41. package/dist/lib/commands/contacts/actions.d.ts +3 -0
  42. package/dist/lib/commands/contacts/actions.js +49 -0
  43. package/dist/lib/commands/contacts/export-delete.d.ts +9 -0
  44. package/dist/lib/commands/contacts/export-delete.js +51 -0
  45. package/dist/lib/commands/contacts/types.d.ts +35 -0
  46. package/dist/lib/commands/contacts/types.js +1 -0
  47. package/dist/lib/commands/deploy/domain-setup.d.ts +8 -0
  48. package/dist/lib/commands/deploy/domain-setup.js +82 -0
  49. package/dist/lib/commands/deploy/output.d.ts +5 -0
  50. package/dist/lib/commands/deploy/output.js +61 -0
  51. package/dist/lib/commands/deploy/payload.d.ts +41 -0
  52. package/dist/lib/commands/deploy/payload.js +95 -0
  53. package/dist/lib/commands/deploy/sequence-status.d.ts +3 -0
  54. package/dist/lib/commands/deploy/sequence-status.js +56 -0
  55. package/dist/lib/commands/deploy/types.d.ts +88 -0
  56. package/dist/lib/commands/deploy/types.js +1 -0
  57. package/dist/lib/commands/deployments/output.d.ts +2 -0
  58. package/dist/lib/commands/deployments/output.js +68 -0
  59. package/dist/lib/commands/deployments/types.d.ts +24 -0
  60. package/dist/lib/commands/deployments/types.js +1 -0
  61. package/dist/lib/commands/domain/setup.d.ts +8 -0
  62. package/dist/lib/commands/domain/setup.js +53 -0
  63. package/dist/lib/commands/domain/types.d.ts +56 -0
  64. package/dist/lib/commands/domain/types.js +1 -0
  65. package/dist/lib/commands/domain/verify.d.ts +5 -0
  66. package/dist/lib/commands/domain/verify.js +50 -0
  67. package/dist/lib/commands/edit/diff.d.ts +7 -0
  68. package/dist/lib/commands/edit/diff.js +65 -0
  69. package/dist/lib/commands/edit/display.d.ts +5 -0
  70. package/dist/lib/commands/edit/display.js +53 -0
  71. package/dist/lib/commands/edit/flow.d.ts +8 -0
  72. package/dist/lib/commands/edit/flow.js +70 -0
  73. package/dist/lib/commands/edit/persist.d.ts +5 -0
  74. package/dist/lib/commands/edit/persist.js +65 -0
  75. package/dist/lib/commands/edit/types.d.ts +37 -0
  76. package/dist/lib/commands/edit/types.js +1 -0
  77. package/dist/lib/commands/emails/editor.d.ts +2 -0
  78. package/dist/lib/commands/emails/editor.js +43 -0
  79. package/dist/lib/commands/emails/output.d.ts +4 -0
  80. package/dist/lib/commands/emails/output.js +36 -0
  81. package/dist/lib/commands/emails/types.d.ts +3 -0
  82. package/dist/lib/commands/emails/types.js +1 -0
  83. package/dist/lib/commands/init/analysis.d.ts +3 -0
  84. package/dist/lib/commands/init/analysis.js +69 -0
  85. package/dist/lib/commands/init/output.d.ts +12 -0
  86. package/dist/lib/commands/init/output.js +39 -0
  87. package/dist/lib/commands/init/payload.d.ts +8 -0
  88. package/dist/lib/commands/init/payload.js +78 -0
  89. package/dist/lib/commands/init/types.d.ts +57 -0
  90. package/dist/lib/commands/init/types.js +1 -0
  91. package/dist/lib/commands/login/output.d.ts +8 -0
  92. package/dist/lib/commands/login/output.js +53 -0
  93. package/dist/lib/commands/login/types.d.ts +19 -0
  94. package/dist/lib/commands/login/types.js +1 -0
  95. package/dist/lib/commands/logs/output.d.ts +2 -0
  96. package/dist/lib/commands/logs/output.js +52 -0
  97. package/dist/lib/commands/logs/types.d.ts +23 -0
  98. package/dist/lib/commands/logs/types.js +1 -0
  99. package/dist/lib/commands/preview/actions.d.ts +11 -0
  100. package/dist/lib/commands/preview/actions.js +43 -0
  101. package/dist/lib/commands/preview/render.d.ts +3 -0
  102. package/dist/lib/commands/preview/render.js +30 -0
  103. package/dist/lib/commands/preview/server.d.ts +8 -0
  104. package/dist/lib/commands/preview/server.js +63 -0
  105. package/dist/lib/commands/preview/types.d.ts +19 -0
  106. package/dist/lib/commands/preview/types.js +1 -0
  107. package/dist/lib/commands/preview/wrapper-html.d.ts +2 -0
  108. package/dist/lib/commands/preview/wrapper-html.js +35 -0
  109. package/dist/lib/commands/sdk/output.d.ts +2 -0
  110. package/dist/lib/commands/sdk/output.js +42 -0
  111. package/dist/lib/commands/sdk/types.d.ts +21 -0
  112. package/dist/lib/commands/sdk/types.js +1 -0
  113. package/dist/lib/commands/settings/actions.d.ts +10 -0
  114. package/dist/lib/commands/settings/actions.js +56 -0
  115. package/dist/lib/commands/settings/display.d.ts +15 -0
  116. package/dist/lib/commands/settings/display.js +69 -0
  117. package/dist/lib/commands/settings/logo-domain.d.ts +3 -0
  118. package/dist/lib/commands/settings/logo-domain.js +47 -0
  119. package/dist/lib/commands/settings/prompt.d.ts +2 -0
  120. package/dist/lib/commands/settings/prompt.js +82 -0
  121. package/dist/lib/commands/settings/types.d.ts +65 -0
  122. package/dist/lib/commands/settings/types.js +1 -0
  123. package/dist/lib/commands/status/output.d.ts +2 -0
  124. package/dist/lib/commands/status/output.js +49 -0
  125. package/dist/lib/commands/status/types.d.ts +28 -0
  126. package/dist/lib/commands/status/types.js +1 -0
  127. package/dist/lib/constants.d.ts +1 -0
  128. package/dist/lib/constants.js +1 -0
  129. package/dist/lib/messages.d.ts +22 -0
  130. package/dist/lib/messages.js +22 -0
  131. package/dist/lib/templates/missing-templates.d.ts +5 -0
  132. package/dist/lib/templates/missing-templates.js +61 -0
  133. package/dist/lib/templates/types.d.ts +13 -0
  134. package/dist/lib/templates/types.js +1 -0
  135. package/oclif.manifest.json +40 -40
  136. package/package.json +1 -1
@@ -0,0 +1,63 @@
1
+ import chalk from 'chalk';
2
+ import open from 'open';
3
+ import { FREE_TIER } from '../../base-command.js';
4
+ import { API_ENDPOINTS } from '../../constants.js';
5
+ import { INFO } from '../../messages.js';
6
+ import { formatAutoCharge, formatCap, formatCurrency, formatPaymentMethod, formatUsageBlock, } from './format.js';
7
+ export async function startCheckout(ctx, jsonOutput) {
8
+ const response = await ctx.spinner(' Creating checkout session...', jsonOutput, () => ctx.post(API_ENDPOINTS.BILLING_CHECKOUT));
9
+ if (!response.ok) {
10
+ ctx.onApiError(response);
11
+ }
12
+ const { checkoutUrl } = response.data;
13
+ if (jsonOutput) {
14
+ ctx.log(JSON.stringify({ checkoutUrl }, null, 2));
15
+ return;
16
+ }
17
+ ctx.log(`\n ${chalk.bold('Stripe Checkout')} — add or update your payment method.`);
18
+ ctx.log(` ${chalk.dim(checkoutUrl)}\n`);
19
+ if (!process.env.CI) {
20
+ try {
21
+ await open(checkoutUrl);
22
+ ctx.log(` ${INFO.BROWSER_OPENING}\n`);
23
+ }
24
+ catch {
25
+ ctx.log(` ${INFO.BROWSER_OPEN_FAILED}\n`);
26
+ }
27
+ }
28
+ }
29
+ function logStatusFields(ctx, data) {
30
+ ctx.log('');
31
+ ctx.log(` Tier: ${data.tier}`);
32
+ ctx.log(` Payment: ${formatPaymentMethod(data)}`);
33
+ ctx.log(` Auto-charge: ${formatAutoCharge(data)}`);
34
+ if (data.tier !== FREE_TIER) {
35
+ ctx.log(` Monthly cap: ${formatCap(data.cap)}`);
36
+ }
37
+ ctx.log(` Total spent: ${formatCurrency(data.totalSpent, data.spentCurrency)}`);
38
+ if (data.activeBlocks.length === 0) {
39
+ ctx.log(' Active blocks: none');
40
+ }
41
+ else {
42
+ ctx.log(' Active blocks:');
43
+ for (const block of data.activeBlocks) {
44
+ ctx.log(` - ${formatUsageBlock(block)}`);
45
+ }
46
+ }
47
+ ctx.log('');
48
+ }
49
+ export async function showStatus(ctx, jsonOutput, statusOnly) {
50
+ const response = await ctx.spinner(' Loading billing status...', jsonOutput, () => ctx.get(API_ENDPOINTS.BILLING_STATUS));
51
+ if (!response.ok) {
52
+ ctx.onApiError(response);
53
+ }
54
+ const { data } = response;
55
+ if (jsonOutput) {
56
+ ctx.log(JSON.stringify(data, null, 2));
57
+ return;
58
+ }
59
+ logStatusFields(ctx, data);
60
+ if (!data.hasPaymentMethod && !statusOnly) {
61
+ await startCheckout(ctx, jsonOutput);
62
+ }
63
+ }
@@ -0,0 +1,7 @@
1
+ import type { BillingCap, BillingStatusResponse, BillingUsageBlock } from './types.js';
2
+ export declare function pluralize(word: string, count: number): string;
3
+ export declare function formatAutoCharge(data: BillingStatusResponse): string;
4
+ export declare function formatCap(cap: BillingCap): string;
5
+ export declare function formatCurrency(amount: number | string, currency: string): string;
6
+ export declare function formatPaymentMethod(data: BillingStatusResponse): string;
7
+ export declare function formatUsageBlock(block: BillingUsageBlock): string;
@@ -0,0 +1,63 @@
1
+ export function pluralize(word, count) {
2
+ return count === 1 ? word : `${word}s`;
3
+ }
4
+ export function formatAutoCharge(data) {
5
+ if (!data.autoChargeEnabled) {
6
+ return 'disabled';
7
+ }
8
+ if (typeof data.autoChargeBlockCount !== 'number') {
9
+ return 'enabled';
10
+ }
11
+ return `enabled (${data.autoChargeBlockCount} ${pluralize('block', data.autoChargeBlockCount)})`;
12
+ }
13
+ export function formatCap(cap) {
14
+ const hasBlocks = typeof cap.inBlocks === 'number';
15
+ const hasEmails = typeof cap.inEmails === 'number';
16
+ if (hasBlocks && hasEmails) {
17
+ return `${cap.inBlocks} ${pluralize('block', cap.inBlocks)} (${cap.inEmails.toLocaleString()} ${pluralize('email', cap.inEmails)})`;
18
+ }
19
+ if (hasBlocks) {
20
+ return `${cap.inBlocks} ${pluralize('block', cap.inBlocks)}`;
21
+ }
22
+ if (hasEmails) {
23
+ return `${cap.inEmails.toLocaleString()} ${pluralize('email', cap.inEmails)}`;
24
+ }
25
+ return 'not set';
26
+ }
27
+ export function formatCurrency(amount, currency) {
28
+ const numericAmount = typeof amount === 'number' ? amount : Number.parseFloat(amount);
29
+ const normalizedCurrency = currency.toUpperCase();
30
+ if (Number.isFinite(numericAmount)) {
31
+ try {
32
+ return new Intl.NumberFormat('en-US', {
33
+ currency: normalizedCurrency,
34
+ style: 'currency',
35
+ }).format(numericAmount);
36
+ }
37
+ catch {
38
+ return `${numericAmount.toFixed(2)} ${normalizedCurrency}`;
39
+ }
40
+ }
41
+ return `${String(amount)} ${normalizedCurrency}`;
42
+ }
43
+ export function formatPaymentMethod(data) {
44
+ if (!data.hasPaymentMethod) {
45
+ return 'No payment method on file';
46
+ }
47
+ const primaryMethod = data.paymentMethod[0];
48
+ if (!primaryMethod) {
49
+ return 'Payment method on file';
50
+ }
51
+ const brand = primaryMethod.brand || 'Card';
52
+ return primaryMethod.last4
53
+ ? `${brand} ending ${primaryMethod.last4}`
54
+ : `${brand} on file`;
55
+ }
56
+ export function formatUsageBlock(block) {
57
+ const allowance = block.blockAllowance ?? Math.max(block.blocksCount * block.blockSize, 0);
58
+ const used = Math.max(block.emailsSent ?? 0, 0);
59
+ const activationSuffix = block.activatedAt
60
+ ? `, activated at ${new Date(block.activatedAt).toLocaleDateString('en-US')}`
61
+ : '';
62
+ return `${block.type} block (${used + allowance}): ${used} ${pluralize('email', used)} sent, ${allowance} ${pluralize('email', allowance)} remaining${activationSuffix}`;
63
+ }
@@ -0,0 +1,7 @@
1
+ import type { BillingCtx } from './types.js';
2
+ export declare function purchaseBlocks(ctx: BillingCtx, blocksCount: number, jsonOutput: boolean): Promise<void>;
3
+ export declare function setCap(ctx: BillingCtx, opts: {
4
+ autoChargeBlockCount?: number;
5
+ cap: number;
6
+ json: boolean;
7
+ }): Promise<void>;
@@ -0,0 +1,57 @@
1
+ import chalk from 'chalk';
2
+ import { FREE_TIER } from '../../base-command.js';
3
+ import { API_ENDPOINTS } from '../../constants.js';
4
+ import { loadYaml, saveYaml } from '../../yaml-config.js';
5
+ import { pluralize } from './format.js';
6
+ async function persistMonthlyCap(ctx, capBlocks) {
7
+ const yamlConfig = await loadYaml();
8
+ if (!yamlConfig)
9
+ return;
10
+ if (yamlConfig.project.monthlyCap === capBlocks)
11
+ return;
12
+ yamlConfig.project.monthlyCap = capBlocks;
13
+ await saveYaml(yamlConfig);
14
+ await ctx.syncYaml();
15
+ }
16
+ export async function purchaseBlocks(ctx, blocksCount, jsonOutput) {
17
+ if (blocksCount < 1) {
18
+ ctx.error('Purchase block count must be at least 1 block.');
19
+ }
20
+ const response = await ctx.spinner(' Initiating block purchase...', jsonOutput, () => ctx.post(API_ENDPOINTS.BILLING_PURCHASE, {
21
+ blocksCount,
22
+ }));
23
+ if (!response.ok) {
24
+ ctx.onApiError(response);
25
+ }
26
+ if (jsonOutput) {
27
+ ctx.log(JSON.stringify(response.data, null, 2));
28
+ return;
29
+ }
30
+ ctx.log(`\n ${chalk.green('✓')} ${response.data.message}`);
31
+ ctx.log(` Blocks: ${blocksCount} ${pluralize('block', blocksCount)}`);
32
+ ctx.log(` Payment intent:${` ${chalk.dim(response.data.paymentIntentId)}`}`);
33
+ ctx.log('');
34
+ }
35
+ export async function setCap(ctx, opts) {
36
+ ctx.validateBillingCapInputs({
37
+ autoChargeBlockCount: opts.autoChargeBlockCount,
38
+ cap: opts.cap,
39
+ });
40
+ const tier = await ctx.fetchBillingTier();
41
+ if (tier === FREE_TIER) {
42
+ ctx.warnFreeTierCapBlocked(opts.json);
43
+ return;
44
+ }
45
+ const data = await ctx.applyBillingCap(opts);
46
+ await persistMonthlyCap(ctx, data.capBlocks);
47
+ if (opts.json) {
48
+ ctx.log(JSON.stringify(data, null, 2));
49
+ return;
50
+ }
51
+ ctx.log(`\n ${chalk.green('✓')} ${data.message}`);
52
+ ctx.log(` Monthly cap: ${chalk.bold(String(data.capBlocks))} ${pluralize('block', data.capBlocks)} (${data.capEmails.toLocaleString()} emails)`);
53
+ if (data.autoChargeBlockCount !== undefined) {
54
+ ctx.log(` Auto-charge: ${data.autoChargeBlockCount} ${pluralize('block', data.autoChargeBlockCount)}`);
55
+ }
56
+ ctx.log('');
57
+ }
@@ -0,0 +1,72 @@
1
+ import type { ApiResponse } from '../../api-client.js';
2
+ import type { BillingCapUpdateResult } from '../../base-command.js';
3
+ export type { BillingCapUpdateResult } from '../../base-command.js';
4
+ export interface BillingCap {
5
+ capUpdatedAt: null | string;
6
+ inBlocks: null | number;
7
+ inEmails: null | number;
8
+ }
9
+ export interface BillingPaymentMethod {
10
+ brand?: string;
11
+ last4?: string;
12
+ type?: string;
13
+ }
14
+ export interface BillingUsageBlock {
15
+ activatedAt?: string;
16
+ blockAllowance?: number;
17
+ blockSize: number;
18
+ blocksCount: number;
19
+ emailsSent: number;
20
+ id: string;
21
+ status: string;
22
+ type: string;
23
+ }
24
+ export interface BillingStatusResponse {
25
+ activeBlocks: BillingUsageBlock[];
26
+ autoChargeBlockCount: null | number;
27
+ autoChargeEnabled: boolean;
28
+ cap: BillingCap;
29
+ hasPaymentMethod: boolean;
30
+ paymentMethod: BillingPaymentMethod[];
31
+ spentCurrency: string;
32
+ tier: string;
33
+ totalSpent: number | string;
34
+ }
35
+ export interface CheckoutResponse {
36
+ checkoutUrl: string;
37
+ }
38
+ export interface BillingPurchaseResponse {
39
+ message: string;
40
+ paymentIntentId: string;
41
+ }
42
+ export type BillingFlags = {
43
+ 'auto-charge-block-count'?: number;
44
+ cap?: number;
45
+ checkout: boolean;
46
+ json: boolean;
47
+ purchase?: number;
48
+ status: boolean;
49
+ };
50
+ export type BillingCtx = {
51
+ applyBillingCap(opts: {
52
+ autoChargeBlockCount?: number;
53
+ cap: number;
54
+ json: boolean;
55
+ }): Promise<BillingCapUpdateResult>;
56
+ error(msg: string): never;
57
+ fetchBillingTier(): Promise<null | string>;
58
+ get<T>(path: string, params?: Record<string, string>): Promise<ApiResponse<T>>;
59
+ log(msg?: string): void;
60
+ onApiError(resp: {
61
+ error?: string;
62
+ status: number;
63
+ }): never;
64
+ post<T>(path: string, body?: unknown): Promise<ApiResponse<T>>;
65
+ spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
66
+ syncYaml(): Promise<void>;
67
+ validateBillingCapInputs(opts: {
68
+ autoChargeBlockCount?: number;
69
+ cap: number;
70
+ }): void;
71
+ warnFreeTierCapBlocked(json: boolean): void;
72
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -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 {};
@@ -0,0 +1,8 @@
1
+ import type { MailmodoYaml } from '../../yaml-config.js';
2
+ import type { DeployCtx, DeployFlags, ValidateResponse } from './types.js';
3
+ export declare function validateDeploySequence(ctx: DeployCtx, payload: object, flags: {
4
+ json: boolean;
5
+ }): Promise<ValidateResponse>;
6
+ export declare function verifyDomain(ctx: DeployCtx, jsonOutput: boolean, domain: string): Promise<boolean>;
7
+ export declare function runDomainSetup(ctx: DeployCtx, yamlConfig: MailmodoYaml, flags: DeployFlags): Promise<boolean>;
8
+ export declare function ensureDomainReady(ctx: DeployCtx, yamlConfig: MailmodoYaml, flags: DeployFlags): Promise<boolean>;
@@ -0,0 +1,82 @@
1
+ import { confirm, input } from '@inquirer/prompts';
2
+ import chalk from 'chalk';
3
+ import { API_ENDPOINTS } from '../../constants.js';
4
+ import { ERRORS, INFO, PROMPTS } from '../../messages.js';
5
+ export async function validateDeploySequence(ctx, payload, flags) {
6
+ const res = await ctx.spinner(' Validating sequence...', flags.json, () => ctx.post(API_ENDPOINTS.SEQUENCES_VALIDATE, payload));
7
+ if (!res.ok) {
8
+ if (res.data.error === 'senderDomainNotFound')
9
+ ctx.error(ERRORS.DOMAIN_NOT_REGISTERED);
10
+ if (res.data.error === 'senderDomainNotVerified')
11
+ ctx.error(ERRORS.DOMAIN_NOT_VERIFIED);
12
+ ctx.onApiError(res);
13
+ }
14
+ return res.data;
15
+ }
16
+ export async function verifyDomain(ctx, jsonOutput, domain) {
17
+ const res = await ctx.spinner(' Checking DNS...', jsonOutput, () => ctx.get(API_ENDPOINTS.DOMAIN_VERIFY, { domain }));
18
+ if (!res.ok)
19
+ ctx.onApiError(res);
20
+ const { dkim, dmarc, dnsGuideUrl, domainStatus, returnPath } = res.data;
21
+ const allPassed = domainStatus === 'VERIFIED';
22
+ if (!jsonOutput) {
23
+ ctx.log(` DKIM ${dkim ? chalk.green('✓') : chalk.red('✗')}`);
24
+ ctx.log(` DMARC ${dmarc ? chalk.green('✓') : chalk.red('✗')}`);
25
+ ctx.log(` Return-Path ${returnPath ? chalk.green('✓') : chalk.red('✗')}`);
26
+ if (allPassed) {
27
+ ctx.log(`\n ${chalk.green('Domain verified.')} Continuing deploy...\n`);
28
+ }
29
+ else {
30
+ ctx.log(`\n ${INFO.DNS_RECORDS_FAILED}\n ${INFO.DNS_FIX_AND_VERIFY}`);
31
+ if (dnsGuideUrl)
32
+ ctx.log(` Help: ${chalk.cyan(dnsGuideUrl)}\n`);
33
+ }
34
+ }
35
+ return allPassed;
36
+ }
37
+ export async function runDomainSetup(ctx, yamlConfig, flags) {
38
+ const inputs = await ctx.collectDomainInputs(yamlConfig, flags.yes);
39
+ const { dnsRecords, dnsGuideUrl } = await ctx.registerDomainAndSave(yamlConfig, inputs, flags.json);
40
+ ctx.showDnsRecords(dnsRecords, dnsGuideUrl, flags.json);
41
+ if (flags.yes)
42
+ return verifyDomain(ctx, flags.json, inputs.domain);
43
+ const action = await input({
44
+ default: '',
45
+ message: PROMPTS.ENTER_AFTER_RECORDS,
46
+ });
47
+ if (action.toLowerCase() === 'skip') {
48
+ ctx.log(`\n ${INFO.SEQUENCES_NOT_DEPLOYED}\n ${INFO.DOMAIN_NOT_DEPLOYED_HINT}\n`);
49
+ return false;
50
+ }
51
+ return verifyDomain(ctx, flags.json, inputs.domain);
52
+ }
53
+ export async function ensureDomainReady(ctx, yamlConfig, flags) {
54
+ const check = await ctx.spinner(' Checking domain verification...', flags.json, () => ctx.get(API_ENDPOINTS.DOMAIN_VERIFY, {
55
+ domain: yamlConfig.project?.domain || '',
56
+ }));
57
+ if (!check.ok)
58
+ ctx.onApiError(check);
59
+ if (check.data?.domainStatus === 'VERIFIED')
60
+ return true;
61
+ if (yamlConfig.project?.domain) {
62
+ if (!flags.json)
63
+ ctx.log(`\n ${INFO.DOMAIN_PENDING_VERIFICATION}\n`);
64
+ return false;
65
+ }
66
+ if (!flags.json) {
67
+ ctx.log(`\n No sending domain set up yet.`);
68
+ ctx.log(` You need a verified domain before sending emails.`);
69
+ ctx.log(` This is a one-time setup. Takes about 5 minutes.\n`);
70
+ }
71
+ if (!flags.yes) {
72
+ const go = await confirm({
73
+ default: true,
74
+ message: 'Set up your sending domain now?',
75
+ });
76
+ if (!go) {
77
+ ctx.log(`\n ${INFO.SEQUENCES_NOT_DEPLOYED}\n Emails will not send until your domain is verified.\n ${INFO.DOMAIN_NOT_DEPLOYED_HINT}\n`);
78
+ return false;
79
+ }
80
+ }
81
+ return runDomainSetup(ctx, yamlConfig, flags);
82
+ }
@@ -0,0 +1,5 @@
1
+ import type { MailmodoYaml } from '../../yaml-config.js';
2
+ import type { DeployCtx, SdkSnippet, ValidateResponse } from './types.js';
3
+ export declare function logDiff(ctx: DeployCtx, diff: NonNullable<ValidateResponse['diff']>): void;
4
+ export declare function logPreDeploySummary(ctx: DeployCtx, yamlConfig: MailmodoYaml, validateResult: ValidateResponse, jsonOutput: boolean): void;
5
+ export declare function logDeploySuccessInstructions(ctx: DeployCtx, sdkSnippet: SdkSnippet): void;
@@ -0,0 +1,61 @@
1
+ import chalk from 'chalk';
2
+ import { SDK_IMPORT_SNIPPET, SDK_INSTALL_COMMAND } from '../../constants.js';
3
+ import { DEPLOY, SEPARATOR } from '../../messages.js';
4
+ export function logDiff(ctx, diff) {
5
+ if (!diff.hasChanges) {
6
+ ctx.log(` ${DEPLOY.NO_CHANGES}`);
7
+ return;
8
+ }
9
+ ctx.log(` ${DEPLOY.CHANGES_HEADER}`);
10
+ for (const email of diff.added) {
11
+ ctx.log(` ${chalk.green('+')} ${email.id.padEnd(24)} ${email.trigger || ''}`);
12
+ }
13
+ for (const email of diff.removed) {
14
+ ctx.log(` ${chalk.red('-')} ${email.id.padEnd(24)} ${email.trigger || ''}`);
15
+ }
16
+ for (const email of diff.modified) {
17
+ ctx.log(` ${chalk.yellow('~')} ${email.id.padEnd(24)} ${email.changedFields?.join(', ') || ''}`);
18
+ }
19
+ if (diff.unchanged.length > 0)
20
+ ctx.log(` ${chalk.dim(`∙ ${diff.unchanged.length} unchanged`)}`);
21
+ }
22
+ export function logPreDeploySummary(ctx, yamlConfig, validateResult, jsonOutput) {
23
+ if (jsonOutput)
24
+ return;
25
+ ctx.log(`\n ${chalk.green('✓')} Domain: ${yamlConfig.project?.domain || 'verified'}\n`);
26
+ if (!validateResult.existingDeployment || !validateResult.diff) {
27
+ ctx.log(` ${DEPLOY.DEPLOYING_HEADER}`);
28
+ for (const email of yamlConfig.emails) {
29
+ ctx.log(` ${chalk.green('+')} ${email.id.padEnd(24)} ${email.trigger}`);
30
+ }
31
+ }
32
+ else {
33
+ logDiff(ctx, validateResult.diff);
34
+ }
35
+ ctx.log('');
36
+ }
37
+ export function logDeploySuccessInstructions(ctx, sdkSnippet) {
38
+ ctx.log(` ${DEPLOY.SUCCESS}\n`);
39
+ ctx.log(` ${SEPARATOR}`);
40
+ ctx.log(` ${DEPLOY.SDK_ONBOARDING_HEADER}`);
41
+ ctx.log(` ${SEPARATOR}\n`);
42
+ ctx.log(` ${chalk.cyan(sdkSnippet.install ?? SDK_INSTALL_COMMAND)}\n`);
43
+ ctx.log(` ${chalk.dim(SDK_IMPORT_SNIPPET)}\n`);
44
+ if (sdkSnippet.examples) {
45
+ ctx.log(` ${DEPLOY.SDK_EXAMPLE_COMMENT}`);
46
+ ctx.log(` ${chalk.dim(sdkSnippet.examples.track)}`);
47
+ ctx.log(` ${chalk.dim(sdkSnippet.examples.identify)}\n`);
48
+ }
49
+ const trackCalls = [...new Set(sdkSnippet.trackCalls ?? [])];
50
+ for (const call of trackCalls)
51
+ ctx.log(` ${chalk.dim(call)}`);
52
+ if (trackCalls.length > 0)
53
+ ctx.log('');
54
+ const identifyCalls = [...new Set(sdkSnippet.identifyCalls ?? [])];
55
+ for (const call of identifyCalls)
56
+ ctx.log(` ${chalk.dim(call)}`);
57
+ if (identifyCalls.length > 0)
58
+ ctx.log('');
59
+ ctx.log(` ${DEPLOY.SDK_DOCS_HINT}\n`);
60
+ ctx.log(` ${SEPARATOR}\n`);
61
+ }
@@ -0,0 +1,41 @@
1
+ import { type EmailConfig, type MailmodoYaml, type ProjectConfig } from '../../yaml-config.js';
2
+ import type { DeployCtx } from './types.js';
3
+ export declare function mapEmailToPayload(email: EmailConfig): {
4
+ condition: string | null;
5
+ ctaText: string;
6
+ delay: string | number;
7
+ goal: string;
8
+ id: string;
9
+ isReminder: boolean;
10
+ previewText: string;
11
+ priority: string;
12
+ subject: string;
13
+ trigger: string;
14
+ };
15
+ export declare function buildProjectPayload(project: ProjectConfig, monthlyCap: number | undefined): {
16
+ webhookUrl?: string | undefined;
17
+ product: {
18
+ businessType: string;
19
+ description: string;
20
+ pricingModel: string;
21
+ productName: string;
22
+ saasModel: string;
23
+ targetUser: string;
24
+ url: string;
25
+ };
26
+ senderDetails: {
27
+ address: string;
28
+ domain: string;
29
+ fromEmail: string;
30
+ fromName: string;
31
+ replyTo: string;
32
+ };
33
+ monthlyCap?: number | undefined;
34
+ brand: {
35
+ colors: string[];
36
+ logoUrl: string;
37
+ };
38
+ emailStyle: "branded" | "plain";
39
+ };
40
+ export declare function buildRegeneratePayload(yamlConfig: MailmodoYaml, missingIds: string[]): Record<string, unknown>;
41
+ export declare function buildDeployPayload(ctx: DeployCtx, yamlConfig: MailmodoYaml): Promise<object>;