@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,52 @@
1
+ import chalk from 'chalk';
2
+ function statusColor(status) {
3
+ switch (status) {
4
+ case 'bounced':
5
+ case 'complained':
6
+ case 'failed': {
7
+ return chalk.red;
8
+ }
9
+ case 'clicked':
10
+ case 'opened':
11
+ case 'sent': {
12
+ return chalk.green;
13
+ }
14
+ case 'skipped': {
15
+ return chalk.yellow;
16
+ }
17
+ default: {
18
+ return chalk.white;
19
+ }
20
+ }
21
+ }
22
+ function renderEntries(ctx, entries) {
23
+ for (const entry of entries) {
24
+ const time = (entry.timestamp || '').padEnd(18);
25
+ const templateId = (entry.emailId || '').padEnd(24);
26
+ const colorFn = statusColor(entry.status);
27
+ const status = colorFn((entry.status || '').padEnd(10));
28
+ const contact = entry.contact || '';
29
+ ctx.log(` ${time}${templateId}${status}${contact}`);
30
+ if (entry.reason) {
31
+ const label = entry.status === 'skipped' ? 'condition not met' : 'reason';
32
+ ctx.log(` ${' '.repeat(52)}${chalk.dim(`(${label}: ${entry.reason})`)}`);
33
+ }
34
+ }
35
+ }
36
+ export function renderLogs(ctx, data) {
37
+ const { entries, limit, page, total } = data;
38
+ ctx.log(`\n ${'Time'.padEnd(18)}${'Email'.padEnd(24)}${'Status'.padEnd(10)}Contact`);
39
+ ctx.log(` ${'─'.repeat(68)}`);
40
+ if (entries?.length) {
41
+ renderEntries(ctx, entries);
42
+ const totalPages = Math.ceil(total / limit);
43
+ ctx.log(`\n Page ${page} of ${totalPages} · ${total} total entries`);
44
+ if (page < totalPages) {
45
+ ctx.log(` ${chalk.dim(`Next: --page ${page + 1}`)}`);
46
+ }
47
+ }
48
+ else {
49
+ ctx.log(` ${chalk.dim('No log entries found.')}`);
50
+ }
51
+ ctx.log('');
52
+ }
@@ -0,0 +1,23 @@
1
+ import type { ApiResponse } from '../../api-client.js';
2
+ export interface LogEntry {
3
+ contact: string;
4
+ emailId: string;
5
+ reason?: string;
6
+ status: string;
7
+ timestamp: string;
8
+ }
9
+ export interface LogsResponse {
10
+ entries: LogEntry[];
11
+ limit: number;
12
+ page: number;
13
+ total: number;
14
+ }
15
+ export type LogsCtx = {
16
+ get<T = Record<string, unknown>>(path: string, params?: Record<string, string>): Promise<ApiResponse<T>>;
17
+ log(msg?: string): void;
18
+ onApiError(resp: {
19
+ error?: string;
20
+ status: number;
21
+ }): never;
22
+ spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
23
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import type { PreviewCtx, PreviewEmail } from './types.js';
2
+ export declare function renderText(ctx: PreviewCtx, email: PreviewEmail, opts: {
3
+ jsonOutput: boolean;
4
+ sampleData: Record<string, string>;
5
+ templateHtml: string;
6
+ }): Promise<void>;
7
+ export declare function sendTestEmail(ctx: PreviewCtx, email: PreviewEmail, html: string, opts: {
8
+ domain: string | undefined;
9
+ jsonOutput: boolean;
10
+ toAddress: string;
11
+ }): Promise<void>;
@@ -0,0 +1,43 @@
1
+ import chalk from 'chalk';
2
+ import { API_ENDPOINTS } from '../../constants.js';
3
+ import { htmlToText, renderTemplate } from './render.js';
4
+ export async function renderText(ctx, email, opts) {
5
+ const { jsonOutput, sampleData, templateHtml } = opts;
6
+ const plainText = htmlToText(renderTemplate(templateHtml, sampleData));
7
+ if (jsonOutput) {
8
+ ctx.log(JSON.stringify({
9
+ body: plainText,
10
+ id: email.id,
11
+ previewText: email.previewText,
12
+ subject: email.subject,
13
+ }, null, 2));
14
+ return;
15
+ }
16
+ ctx.log(`\n ${chalk.bold('SUBJECT:')} ${email.subject}`);
17
+ if (email.previewText) {
18
+ ctx.log(` ${chalk.bold('PREVIEW:')} ${email.previewText}`);
19
+ }
20
+ ctx.log(`\n${plainText}\n`);
21
+ }
22
+ export async function sendTestEmail(ctx, email, html, opts) {
23
+ const { domain, jsonOutput, toAddress } = opts;
24
+ const response = await ctx.spinner(' Sending test email...', jsonOutput, () => ctx.post(`${API_ENDPOINTS.PREVIEW}/send`, {
25
+ domain,
26
+ html,
27
+ subject: email.subject,
28
+ to: toAddress,
29
+ }));
30
+ if (!response.ok) {
31
+ ctx.onApiError(response);
32
+ }
33
+ const { note, sentTo, sentVia, status } = response.data;
34
+ if (jsonOutput) {
35
+ ctx.log(JSON.stringify({ note, sentTo, sentVia, status }, null, 2));
36
+ return;
37
+ }
38
+ ctx.log(`\n ${chalk.green('✓')} Test email sent to ${chalk.cyan(sentTo)} via ${chalk.cyan(sentVia)}.`);
39
+ if (note) {
40
+ ctx.log(` ${chalk.dim(note)}`);
41
+ }
42
+ ctx.log('');
43
+ }
@@ -0,0 +1,3 @@
1
+ export declare const SAMPLE_DATA: Record<string, string>;
2
+ export declare function renderTemplate(html: string, data: Record<string, string>): string;
3
+ export declare function htmlToText(html: string): string;
@@ -0,0 +1,30 @@
1
+ /* eslint-disable camelcase */
2
+ export const SAMPLE_DATA = Object.freeze({
3
+ app_url: 'https://yourapp.com',
4
+ cta_url: 'https://yourapp.com/action',
5
+ first_name: 'Sarah',
6
+ product_name: 'YourApp',
7
+ unsubscribe_url: '#',
8
+ });
9
+ /* eslint-enable camelcase */
10
+ export function renderTemplate(html, data) {
11
+ return html.replaceAll(/\{\{(\w+)\}\}/g, (match, key) => data[key] || match);
12
+ }
13
+ export function htmlToText(html) {
14
+ return html
15
+ .replaceAll(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
16
+ .replaceAll(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
17
+ .replaceAll(/<br\s*\/?>/gi, '\n')
18
+ .replaceAll(/<\/p>/gi, '\n\n')
19
+ .replaceAll(/<\/div>/gi, '\n')
20
+ .replaceAll(/<\/tr>/gi, '\n')
21
+ .replaceAll(/<\/li>/gi, '\n')
22
+ .replaceAll(/<[^>]+>/g, '')
23
+ .replaceAll('&nbsp;', ' ')
24
+ .replaceAll('&amp;', '&')
25
+ .replaceAll('&lt;', '<')
26
+ .replaceAll('&gt;', '>')
27
+ .replaceAll('&quot;', '"')
28
+ .replaceAll(/\n{3,}/g, '\n\n')
29
+ .trim();
30
+ }
@@ -0,0 +1,8 @@
1
+ import type { PreviewCtx, PreviewEmail } from './types.js';
2
+ export declare function findAvailablePort(startPort: number, endPort?: number): Promise<number>;
3
+ export declare function startPreviewServer(ctx: PreviewCtx, email: PreviewEmail, opts: {
4
+ effectiveStyle: 'branded' | 'plain';
5
+ jsonOutput: boolean;
6
+ rendered: string;
7
+ sampleData: Record<string, string>;
8
+ }): Promise<void>;
@@ -0,0 +1,63 @@
1
+ import { createServer } from 'node:http';
2
+ import chalk from 'chalk';
3
+ import open from 'open';
4
+ import { PREVIEW_PORT } from '../../constants.js';
5
+ import { INFO } from '../../messages.js';
6
+ import { buildWrapperHtml } from './wrapper-html.js';
7
+ export async function findAvailablePort(startPort, endPort = startPort + 10) {
8
+ if (startPort > endPort) {
9
+ throw new Error(`No available port found starting from port ${endPort - 10}`);
10
+ }
11
+ const available = await new Promise((resolve) => {
12
+ const probe = createServer();
13
+ probe.once('error', () => resolve(false));
14
+ probe.once('listening', () => probe.close(() => resolve(true)));
15
+ probe.listen(startPort);
16
+ });
17
+ return available ? startPort : findAvailablePort(startPort + 1, endPort);
18
+ }
19
+ export async function startPreviewServer(ctx, email, opts) {
20
+ const { effectiveStyle, jsonOutput, rendered, sampleData } = opts;
21
+ const wrapperHtml = buildWrapperHtml(email, rendered, sampleData, effectiveStyle);
22
+ const port = await findAvailablePort(PREVIEW_PORT);
23
+ if (!jsonOutput && port !== PREVIEW_PORT) {
24
+ ctx.log(`\n ${chalk.yellow('!')} Port ${PREVIEW_PORT} is already in use. Opening preview on port ${chalk.cyan(String(port))}.`);
25
+ }
26
+ if (jsonOutput) {
27
+ ctx.log(JSON.stringify({
28
+ id: email.id,
29
+ style: effectiveStyle,
30
+ url: `http://localhost:${port}`,
31
+ }, null, 2));
32
+ }
33
+ const server = createServer((_req, res) => {
34
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
35
+ res.end(wrapperHtml);
36
+ });
37
+ await new Promise((resolve) => {
38
+ server.listen(port, () => resolve());
39
+ });
40
+ const url = `http://localhost:${port}`;
41
+ if (!jsonOutput) {
42
+ ctx.log(`\n Style: ${chalk.cyan(effectiveStyle)}`);
43
+ ctx.log(` Preview server at ${chalk.cyan(url)}`);
44
+ ctx.log(` ${INFO.BROWSER_OPENING}\n`);
45
+ ctx.log(` ${chalk.dim('Press Ctrl+C to stop the preview server.')}\n`);
46
+ }
47
+ try {
48
+ await open(url);
49
+ }
50
+ catch {
51
+ if (!jsonOutput) {
52
+ ctx.log(` ${INFO.BROWSER_OPEN_FAILED}`);
53
+ }
54
+ }
55
+ await new Promise((resolve) => {
56
+ const shutdown = () => {
57
+ server.closeAllConnections?.();
58
+ server.close(() => resolve());
59
+ };
60
+ process.once('SIGINT', shutdown);
61
+ process.once('SIGTERM', shutdown);
62
+ });
63
+ }
@@ -0,0 +1,19 @@
1
+ import type { ApiResponse } from '../../api-client.js';
2
+ export type PreviewCtx = {
3
+ error(msg: string): never;
4
+ exit(code?: number): never;
5
+ log(msg?: string): void;
6
+ onApiError(resp: {
7
+ error?: string;
8
+ status: number;
9
+ }): never;
10
+ post<T>(path: string, body?: unknown): Promise<ApiResponse<T>>;
11
+ spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
12
+ syncYaml(): Promise<void>;
13
+ };
14
+ export type PreviewEmail = {
15
+ id: string;
16
+ previewText?: string;
17
+ style?: string;
18
+ subject: string;
19
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { PreviewEmail } from './types.js';
2
+ export declare function buildWrapperHtml(email: PreviewEmail, rendered: string, sampleData: Record<string, string>, effectiveStyle: 'branded' | 'plain'): string;
@@ -0,0 +1,35 @@
1
+ export function buildWrapperHtml(email, rendered, sampleData, effectiveStyle) {
2
+ return `<!DOCTYPE html>
3
+ <html>
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <title>Preview: ${email.subject}</title>
7
+ <style>
8
+ body { margin: 0; padding: 2rem; background: #f5f5f5; font-family: system-ui; }
9
+ .preview-bar { background: #1a1a2e; color: #fff; padding: 0.75rem 1.5rem; border-radius: 0.5rem;
10
+ margin-bottom: 1.5rem; display: flex; justify-content: space-between; align-items: center; }
11
+ .preview-bar h3 { margin: 0; font-size: 0.875rem; }
12
+ .preview-bar span { font-size: 0.75rem; opacity: 0.7; }
13
+ .email-frame { background: #fff; max-width: 40rem; margin: 0 auto; border-radius: 0.5rem;
14
+ box-shadow: 0 0.125rem 0.5rem rgba(0,0,0,0.1); overflow: hidden; }
15
+ .email-header { padding: 1rem 1.5rem; border-bottom: 1px solid #eee; }
16
+ .email-header .subject { font-weight: 600; font-size: 1rem; }
17
+ .email-header .meta { font-size: 0.75rem; color: #666; margin-top: 0.25rem; }
18
+ .email-body { padding: 1.5rem; }
19
+ </style>
20
+ </head>
21
+ <body>
22
+ <div class="preview-bar">
23
+ <h3>Mailmodo Preview — ${email.id}</h3>
24
+ <span>Style: ${effectiveStyle} · Press Ctrl+C in terminal to stop</span>
25
+ </div>
26
+ <div class="email-frame">
27
+ <div class="email-header">
28
+ <div class="subject">${email.subject}</div>
29
+ <div class="meta">To: ${sampleData.first_name} · From: ${sampleData.product_name}</div>
30
+ </div>
31
+ <div class="email-body">${rendered}</div>
32
+ </div>
33
+ </body>
34
+ </html>`;
35
+ }
@@ -0,0 +1,2 @@
1
+ import type { SdkCtx, SdkSnippetsResponse } from './types.js';
2
+ export declare function renderSdkSnippets(ctx: SdkCtx, data: SdkSnippetsResponse): void;
@@ -0,0 +1,42 @@
1
+ import chalk from 'chalk';
2
+ import { SDK_IMPORT_SNIPPET, SDK_INSTALL_COMMAND } from '../../constants.js';
3
+ import { SEPARATOR } from '../../messages.js';
4
+ function renderCallBlock(ctx, label, calls) {
5
+ if (calls.length === 0)
6
+ return;
7
+ ctx.log(` ${chalk.dim(label)}`);
8
+ for (const call of calls) {
9
+ ctx.log(` ${chalk.dim(call)}`);
10
+ }
11
+ }
12
+ function renderSequenceBlock(ctx, snippet) {
13
+ const productName = snippet.productName || 'Unnamed sequence';
14
+ ctx.log(` ${chalk.bold(productName)} ${chalk.dim(`(${snippet.sequenceId})`)}`);
15
+ const trackCalls = [...new Set(snippet.sdkSnippet?.trackCalls ?? [])];
16
+ const identifyCalls = [...new Set(snippet.sdkSnippet?.identifyCalls ?? [])];
17
+ renderCallBlock(ctx, '// track() calls', trackCalls);
18
+ renderCallBlock(ctx, '// identify() calls', identifyCalls);
19
+ if (trackCalls.length === 0 && identifyCalls.length === 0) {
20
+ ctx.log(` ${chalk.dim('No track() or identify() calls available.')}`);
21
+ }
22
+ }
23
+ export function renderSdkSnippets(ctx, data) {
24
+ const snippets = data.sdkSnippets ?? [];
25
+ if (snippets.length === 0) {
26
+ ctx.log(`\n ${chalk.dim('No active deployed sequences.')}`);
27
+ ctx.log(` Run ${chalk.cyan('mailmodo deploy')} to deploy one.\n`);
28
+ return;
29
+ }
30
+ ctx.log(`\n ${chalk.bold(String(snippets.length))} active ${snippets.length === 1 ? 'sequence' : 'sequences'}:\n`);
31
+ ctx.log(` ${SEPARATOR}`);
32
+ ctx.log(` ${chalk.bold('SDK EVENT REFERENCE')}`);
33
+ ctx.log(` ${SEPARATOR}\n`);
34
+ ctx.log(` ${chalk.cyan(SDK_INSTALL_COMMAND)}\n`);
35
+ ctx.log(` ${chalk.dim(SDK_IMPORT_SNIPPET)}\n`);
36
+ for (const [index, snippet] of snippets.entries()) {
37
+ renderSequenceBlock(ctx, snippet);
38
+ if (index < snippets.length - 1)
39
+ ctx.log('');
40
+ }
41
+ ctx.log(` ${SEPARATOR}\n`);
42
+ }
@@ -0,0 +1,21 @@
1
+ import type { ApiResponse } from '../../api-client.js';
2
+ export interface SdkSnippetEntry {
3
+ productName: string;
4
+ sdkSnippet: {
5
+ identifyCalls: string[];
6
+ trackCalls: string[];
7
+ };
8
+ sequenceId: string;
9
+ }
10
+ export interface SdkSnippetsResponse {
11
+ sdkSnippets: SdkSnippetEntry[];
12
+ }
13
+ export type SdkCtx = {
14
+ get<T = Record<string, unknown>>(path: string, params?: Record<string, string>): Promise<ApiResponse<T>>;
15
+ log(msg?: string): void;
16
+ onApiError(resp: {
17
+ error?: string;
18
+ status: number;
19
+ }): never;
20
+ spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
21
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import type { SettingsCtx, SettingsYaml } from './types.js';
2
+ export declare function applyMonthlyCapChange(ctx: SettingsCtx, yamlConfig: SettingsYaml, opts: {
3
+ isJson: boolean;
4
+ knownTier: null | string;
5
+ rawValue: string;
6
+ }): Promise<void>;
7
+ export declare function applySetFlag(ctx: SettingsCtx, yamlConfig: SettingsYaml, opts: {
8
+ isJson: boolean;
9
+ setFlag: string;
10
+ }): Promise<void>;
@@ -0,0 +1,56 @@
1
+ import chalk from 'chalk';
2
+ import { FREE_TIER } from '../../base-command.js';
3
+ import { INFO } from '../../messages.js';
4
+ import { settingKeyToProp } from '../../utils.js';
5
+ import { saveYaml } from '../../yaml-config.js';
6
+ export async function applyMonthlyCapChange(ctx, yamlConfig, opts) {
7
+ const parsed = Number(opts.rawValue);
8
+ if (!Number.isInteger(parsed) || parsed < 1) {
9
+ ctx.error('monthly_cap must be a positive integer (blocks).');
10
+ }
11
+ await ctx.ensureAuth();
12
+ const tier = opts.knownTier ?? (await ctx.fetchBillingTier());
13
+ if (tier === FREE_TIER) {
14
+ ctx.warnFreeTierCapBlocked(opts.isJson);
15
+ return;
16
+ }
17
+ const data = await ctx.applyBillingCap({ cap: parsed, json: opts.isJson });
18
+ yamlConfig.project.monthlyCap = data.capBlocks;
19
+ await saveYaml(yamlConfig);
20
+ await ctx.syncYaml();
21
+ if (opts.isJson) {
22
+ ctx.log(JSON.stringify({ monthlyCap: data.capBlocks, status: 'updated' }, null, 2));
23
+ return;
24
+ }
25
+ ctx.log(`\n ${chalk.green('✓')} monthly_cap updated to ${chalk.cyan(String(data.capBlocks))} (${data.capEmails.toLocaleString()} emails)\n`);
26
+ }
27
+ export async function applySetFlag(ctx, yamlConfig, opts) {
28
+ const { project } = yamlConfig;
29
+ const eqIndex = opts.setFlag.indexOf('=');
30
+ if (eqIndex === -1) {
31
+ ctx.error('Invalid format. Use --set key=value (e.g., --set brand_color=#0F3460)');
32
+ }
33
+ const key = opts.setFlag.slice(0, eqIndex).trim();
34
+ const propKey = settingKeyToProp(key);
35
+ const value = opts.setFlag.slice(eqIndex + 1).trim();
36
+ if (!(propKey in project) && key !== 'logo_file') {
37
+ ctx.error(`Unknown setting: ${key}`);
38
+ }
39
+ if (propKey === 'monthlyCap') {
40
+ await applyMonthlyCapChange(ctx, yamlConfig, {
41
+ isJson: opts.isJson,
42
+ knownTier: null,
43
+ rawValue: value,
44
+ });
45
+ return;
46
+ }
47
+ project[propKey] = value;
48
+ await saveYaml(yamlConfig);
49
+ await ctx.syncYaml();
50
+ if (opts.isJson) {
51
+ ctx.log(JSON.stringify({ [propKey]: value, status: 'updated' }, null, 2));
52
+ return;
53
+ }
54
+ ctx.log(`\n ${chalk.green('✓')} ${key} updated to ${chalk.cyan(value)}`);
55
+ ctx.log(` ${INFO.DEPLOY_TO_APPLY}\n`);
56
+ }
@@ -0,0 +1,15 @@
1
+ import type { SettingsCtx } from './types.js';
2
+ export declare const SETTINGS_GROUPS: Readonly<{
3
+ billing: string[];
4
+ brand: string[];
5
+ domain: string[];
6
+ identity: string[];
7
+ integrations: string[];
8
+ }>;
9
+ export declare const SETUP_HINTS: Record<string, string>;
10
+ export declare function fetchDomainVerified(ctx: SettingsCtx, domain: string | undefined): Promise<boolean | null>;
11
+ export declare function displaySettingsGroup(ctx: SettingsCtx, group: string, opts: {
12
+ domainVerified: boolean | null;
13
+ keys: string[];
14
+ project: Record<string, unknown>;
15
+ }): void;
@@ -0,0 +1,69 @@
1
+ import chalk from 'chalk';
2
+ import { API_ENDPOINTS } from '../../constants.js';
3
+ import { settingKeyToProp } from '../../utils.js';
4
+ export const SETTINGS_GROUPS = Object.freeze({
5
+ billing: ['monthly_cap'],
6
+ brand: ['email_style', 'brand_color', 'logo_url', 'logo_file'],
7
+ domain: ['domain', 'address'],
8
+ identity: ['from_name', 'from_email', 'reply_to'],
9
+ integrations: ['webhook_url'],
10
+ });
11
+ export const SETUP_HINTS = {
12
+ address: "'mailmodo domain'",
13
+ domain: "'mailmodo domain'",
14
+ monthlyCap: "'mailmodo billing --cap <n>'",
15
+ };
16
+ export async function fetchDomainVerified(ctx, domain) {
17
+ if (!domain)
18
+ return null;
19
+ try {
20
+ await ctx.ensureAuth();
21
+ const response = await ctx.get(API_ENDPOINTS.DOMAIN_STATUS, { domain });
22
+ if (!response.ok) {
23
+ ctx.log(` ${chalk.dim('Could not fetch domain status. Run')} ${chalk.cyan("'mailmodo domain --status'")} ${chalk.dim('to check manually.')}`);
24
+ return null;
25
+ }
26
+ return response.data?.verified === true;
27
+ }
28
+ catch {
29
+ ctx.log(` ${chalk.dim('Could not reach API for domain status. Skipping verification check.')}`);
30
+ return null;
31
+ }
32
+ }
33
+ function domainDisplay(key, value, domainVerified) {
34
+ let v = value ? String(value) : chalk.dim('(not set)');
35
+ if (key === 'domain' && value && domainVerified === true)
36
+ v += ` ${chalk.green('✓ verified')}`;
37
+ if (key === 'domain' && value && domainVerified === false)
38
+ v += ` ${chalk.red('✗ not verified')}`;
39
+ return v;
40
+ }
41
+ export function displaySettingsGroup(ctx, group, opts) {
42
+ const { domainVerified, keys, project } = opts;
43
+ const availableKeys = keys.filter((key) => (group === 'brand' && key === 'logo_file') ||
44
+ settingKeyToProp(key) in project);
45
+ const groupTitle = ` ${chalk.bold(group.charAt(0).toUpperCase() + group.slice(1))}`;
46
+ if (availableKeys.length === 0) {
47
+ const hint = SETUP_HINTS[settingKeyToProp(keys[0])];
48
+ if (hint) {
49
+ ctx.log(groupTitle);
50
+ ctx.log(` ${'─'.repeat(49)}`);
51
+ ctx.log(` ${chalk.dim(`Run ${hint} to configure.`)}`);
52
+ ctx.log('');
53
+ }
54
+ return;
55
+ }
56
+ ctx.log(groupTitle);
57
+ ctx.log(` ${'─'.repeat(49)}`);
58
+ for (const key of availableKeys) {
59
+ ctx.log(` ${key.padEnd(16)} ${domainDisplay(key, project[settingKeyToProp(key)], domainVerified)}`);
60
+ }
61
+ const missingKeys = keys.filter((key) => !(settingKeyToProp(key) in project) &&
62
+ !(group === 'brand' && key === 'logo_file'));
63
+ for (const key of missingKeys) {
64
+ const hint = SETUP_HINTS[settingKeyToProp(key)];
65
+ if (hint)
66
+ ctx.log(` ${key.padEnd(16)} ${chalk.dim(`(run ${hint} to set up)`)}`);
67
+ }
68
+ ctx.log('');
69
+ }
@@ -0,0 +1,3 @@
1
+ import type { SettingsCtx, SettingsYaml } from './types.js';
2
+ export declare function handleLogoUpload(ctx: SettingsCtx, yamlConfig: SettingsYaml): Promise<void>;
3
+ export declare function handleDomainChange(ctx: SettingsCtx, yamlConfig: SettingsYaml): Promise<void>;
@@ -0,0 +1,47 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+ import { input } from '@inquirer/prompts';
5
+ import chalk from 'chalk';
6
+ import { API_ENDPOINTS } from '../../constants.js';
7
+ import { saveYaml } from '../../yaml-config.js';
8
+ const MIME_TYPES = {
9
+ jpeg: 'image/jpeg',
10
+ jpg: 'image/jpeg',
11
+ png: 'image/png',
12
+ svg: 'image/svg+xml',
13
+ };
14
+ export async function handleLogoUpload(ctx, yamlConfig) {
15
+ const logoPath = await input({ message: 'Path to logo file:' });
16
+ const resolvedPath = resolve(logoPath);
17
+ if (!existsSync(resolvedPath)) {
18
+ ctx.log(`\n File not found: ${resolvedPath}\n`);
19
+ return;
20
+ }
21
+ await ctx.ensureAuth();
22
+ const fileBuffer = await readFile(resolvedPath);
23
+ const ext = resolvedPath.split('.').pop()?.toLowerCase();
24
+ const mimeType = MIME_TYPES[ext ?? ''] ?? 'application/octet-stream';
25
+ const formData = new FormData();
26
+ formData.append('logo', new Blob([new Uint8Array(fileBuffer)], { type: mimeType }), logoPath.split(/[/\\]/).pop() || 'logo.png');
27
+ const response = await ctx.spinner(' Uploading logo file...', false, () => ctx.postFormData(API_ENDPOINTS.ASSETS_LOGO, formData));
28
+ if (!response.ok) {
29
+ ctx.onApiError(response);
30
+ }
31
+ yamlConfig.project.logoUrl = response.data?.url || '';
32
+ yamlConfig.project.logoFile = logoPath;
33
+ await saveYaml(yamlConfig);
34
+ await ctx.syncYaml();
35
+ ctx.log(`\n Logo uploaded and hosted at:`);
36
+ ctx.log(` ${chalk.cyan(String(response.data?.url))}`);
37
+ ctx.log(` Run ${chalk.cyan("'mailmodo deploy'")} to apply to all branded emails.\n`);
38
+ }
39
+ export async function handleDomainChange(ctx, yamlConfig) {
40
+ await ctx.ensureAuth();
41
+ const inputs = await ctx.collectDomainInputs(yamlConfig, false);
42
+ const { dnsRecords, dnsGuideUrl } = await ctx.registerDomainAndSave(yamlConfig, inputs, false);
43
+ ctx.log(`\n Domain and sender details updated. You will need to re-verify.`);
44
+ ctx.showDnsRecords(dnsRecords, dnsGuideUrl, false);
45
+ ctx.log(` Run ${chalk.cyan("'mailmodo domain --verify'")} once records are added.`);
46
+ ctx.log(` Emails will not send until the new domain is verified.`);
47
+ }
@@ -0,0 +1,2 @@
1
+ import type { SettingsCtx, SettingsYaml } from './types.js';
2
+ export declare function promptEditSetting(ctx: SettingsCtx, yamlConfig: SettingsYaml, tier: null | string): Promise<void>;