@mailmodo/cli 0.0.55 → 0.0.56-beta.pr58.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/billing/index.d.ts +1 -11
- package/dist/commands/billing/index.js +28 -184
- package/dist/commands/contacts/index.d.ts +1 -19
- package/dist/commands/contacts/index.js +21 -114
- package/dist/commands/deploy/index.js +12 -7
- package/dist/commands/deployments/index.d.ts +1 -4
- package/dist/commands/deployments/index.js +11 -52
- package/dist/commands/domain/index.d.ts +1 -14
- package/dist/commands/domain/index.js +19 -100
- package/dist/commands/edit/index.d.ts +2 -20
- package/dist/commands/edit/index.js +33 -258
- package/dist/commands/emails/index.d.ts +1 -2
- package/dist/commands/emails/index.js +26 -91
- package/dist/commands/init/index.d.ts +1 -3
- package/dist/commands/init/index.js +47 -199
- package/dist/commands/login/index.d.ts +2 -0
- package/dist/commands/login/index.js +32 -79
- package/dist/commands/logs/index.d.ts +1 -8
- package/dist/commands/logs/index.js +12 -55
- package/dist/commands/preview/index.d.ts +1 -19
- package/dist/commands/preview/index.js +32 -212
- package/dist/commands/sdk/index.d.ts +1 -3
- package/dist/commands/sdk/index.js +14 -46
- package/dist/commands/settings/index.d.ts +1 -22
- package/dist/commands/settings/index.js +34 -246
- package/dist/commands/status/index.d.ts +1 -0
- package/dist/commands/status/index.js +13 -39
- package/dist/lib/base-command.d.ts +35 -10
- package/dist/lib/base-command.js +169 -17
- package/dist/lib/commands/billing/checkout-status.d.ts +3 -0
- package/dist/lib/commands/billing/checkout-status.js +63 -0
- package/dist/lib/commands/billing/format.d.ts +7 -0
- package/dist/lib/commands/billing/format.js +63 -0
- package/dist/lib/commands/billing/purchase-cap.d.ts +7 -0
- package/dist/lib/commands/billing/purchase-cap.js +57 -0
- package/dist/lib/commands/billing/types.d.ts +72 -0
- package/dist/lib/commands/contacts/actions.d.ts +3 -0
- package/dist/lib/commands/contacts/actions.js +49 -0
- package/dist/lib/commands/contacts/export-delete.d.ts +9 -0
- package/dist/lib/commands/contacts/export-delete.js +51 -0
- package/dist/lib/commands/contacts/types.d.ts +35 -0
- package/dist/lib/commands/contacts/types.js +1 -0
- package/dist/lib/{deploy → commands/deploy}/domain-setup.d.ts +1 -1
- package/dist/lib/{deploy → commands/deploy}/domain-setup.js +2 -2
- package/dist/lib/{deploy → commands/deploy}/output.d.ts +1 -1
- package/dist/lib/{deploy → commands/deploy}/output.js +2 -2
- package/dist/lib/{deploy → commands/deploy}/payload.d.ts +1 -1
- package/dist/lib/{deploy → commands/deploy}/payload.js +2 -2
- package/dist/lib/{deploy → commands/deploy}/sequence-status.js +2 -2
- package/dist/lib/{deploy → commands/deploy}/types.d.ts +4 -4
- package/dist/lib/commands/deploy/types.js +1 -0
- package/dist/lib/commands/deployments/output.d.ts +2 -0
- package/dist/lib/commands/deployments/output.js +68 -0
- package/dist/lib/commands/deployments/types.d.ts +24 -0
- package/dist/lib/commands/deployments/types.js +1 -0
- package/dist/lib/commands/domain/setup.d.ts +8 -0
- package/dist/lib/commands/domain/setup.js +53 -0
- package/dist/lib/commands/domain/types.d.ts +56 -0
- package/dist/lib/commands/domain/types.js +1 -0
- package/dist/lib/commands/domain/verify.d.ts +5 -0
- package/dist/lib/commands/domain/verify.js +50 -0
- package/dist/lib/commands/edit/diff.d.ts +7 -0
- package/dist/lib/commands/edit/diff.js +65 -0
- package/dist/lib/commands/edit/display.d.ts +5 -0
- package/dist/lib/commands/edit/display.js +53 -0
- package/dist/lib/commands/edit/flow.d.ts +8 -0
- package/dist/lib/commands/edit/flow.js +70 -0
- package/dist/lib/commands/edit/persist.d.ts +5 -0
- package/dist/lib/commands/edit/persist.js +67 -0
- package/dist/lib/commands/edit/types.d.ts +38 -0
- package/dist/lib/commands/edit/types.js +1 -0
- package/dist/lib/commands/emails/editor.d.ts +2 -0
- package/dist/lib/commands/emails/editor.js +43 -0
- package/dist/lib/commands/emails/output.d.ts +4 -0
- package/dist/lib/commands/emails/output.js +36 -0
- package/dist/lib/commands/emails/types.d.ts +3 -0
- package/dist/lib/commands/emails/types.js +1 -0
- package/dist/lib/commands/init/analysis.d.ts +3 -0
- package/dist/lib/commands/init/analysis.js +73 -0
- package/dist/lib/commands/init/output.d.ts +12 -0
- package/dist/lib/commands/init/output.js +39 -0
- package/dist/lib/commands/init/payload.d.ts +8 -0
- package/dist/lib/commands/init/payload.js +78 -0
- package/dist/lib/commands/init/types.d.ts +57 -0
- package/dist/lib/commands/init/types.js +1 -0
- package/dist/lib/commands/login/output.d.ts +8 -0
- package/dist/lib/commands/login/output.js +40 -0
- package/dist/lib/commands/login/types.d.ts +19 -0
- package/dist/lib/commands/login/types.js +1 -0
- package/dist/lib/commands/logs/output.d.ts +2 -0
- package/dist/lib/commands/logs/output.js +52 -0
- package/dist/lib/commands/logs/types.d.ts +23 -0
- package/dist/lib/commands/logs/types.js +1 -0
- package/dist/lib/commands/preview/actions.d.ts +11 -0
- package/dist/lib/commands/preview/actions.js +43 -0
- package/dist/lib/commands/preview/render.d.ts +3 -0
- package/dist/lib/commands/preview/render.js +30 -0
- package/dist/lib/commands/preview/server.d.ts +8 -0
- package/dist/lib/commands/preview/server.js +63 -0
- package/dist/lib/commands/preview/types.d.ts +22 -0
- package/dist/lib/commands/preview/types.js +1 -0
- package/dist/lib/commands/preview/wrapper-html.d.ts +2 -0
- package/dist/lib/commands/preview/wrapper-html.js +35 -0
- package/dist/lib/commands/sdk/output.d.ts +2 -0
- package/dist/lib/commands/sdk/output.js +42 -0
- package/dist/lib/commands/sdk/types.d.ts +21 -0
- package/dist/lib/commands/sdk/types.js +1 -0
- package/dist/lib/commands/settings/actions.d.ts +10 -0
- package/dist/lib/commands/settings/actions.js +56 -0
- package/dist/lib/commands/settings/display.d.ts +15 -0
- package/dist/lib/commands/settings/display.js +69 -0
- package/dist/lib/commands/settings/logo-domain.d.ts +3 -0
- package/dist/lib/commands/settings/logo-domain.js +47 -0
- package/dist/lib/commands/settings/prompt.d.ts +2 -0
- package/dist/lib/commands/settings/prompt.js +82 -0
- package/dist/lib/commands/settings/types.d.ts +65 -0
- package/dist/lib/commands/settings/types.js +1 -0
- package/dist/lib/commands/status/output.d.ts +2 -0
- package/dist/lib/commands/status/output.js +49 -0
- package/dist/lib/commands/status/types.d.ts +28 -0
- package/dist/lib/commands/status/types.js +1 -0
- package/dist/lib/constants.d.ts +3 -2
- package/dist/lib/constants.js +4 -5
- package/dist/lib/messages.d.ts +11 -0
- package/dist/lib/messages.js +31 -0
- package/dist/lib/templates/missing-templates.d.ts +16 -2
- package/dist/lib/templates/missing-templates.js +34 -22
- package/dist/lib/templates/regenerate.d.ts +10 -0
- package/dist/lib/templates/regenerate.js +29 -0
- package/dist/lib/templates/sync.d.ts +33 -0
- package/dist/lib/templates/sync.js +106 -0
- package/dist/lib/templates/types.d.ts +3 -0
- package/oclif.manifest.json +54 -54
- package/package.json +1 -1
- /package/dist/lib/{deploy → commands/billing}/types.js +0 -0
- /package/dist/lib/{deploy → commands/deploy}/sequence-status.d.ts +0 -0
|
@@ -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,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,82 @@
|
|
|
1
|
+
import { input, select } from '@inquirer/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { FREE_TIER } from '../../base-command.js';
|
|
4
|
+
import { INFO } from '../../messages.js';
|
|
5
|
+
import { settingKeyToProp } from '../../utils.js';
|
|
6
|
+
import { saveYaml } from '../../yaml-config.js';
|
|
7
|
+
import { applyMonthlyCapChange } from './actions.js';
|
|
8
|
+
import { SETUP_HINTS } from './display.js';
|
|
9
|
+
import { handleDomainChange, handleLogoUpload } from './logo-domain.js';
|
|
10
|
+
async function handleUnknownEditKey(ctx, yamlConfig, editKey) {
|
|
11
|
+
if (editKey === 'logo_file') {
|
|
12
|
+
await handleLogoUpload(ctx, yamlConfig);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const editPropKey = settingKeyToProp(editKey);
|
|
16
|
+
const hint = SETUP_HINTS[editPropKey];
|
|
17
|
+
if (hint) {
|
|
18
|
+
ctx.log(`\n ${editKey} is not configured yet. Run ${chalk.cyan(hint)} to set it up.\n`);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
ctx.log(`\n Unknown setting: ${editKey}\n`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function dispatchEditKey(ctx, yamlConfig, editKey, tier) {
|
|
25
|
+
if (editKey === 'logo_file') {
|
|
26
|
+
await handleLogoUpload(ctx, yamlConfig);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (editKey === 'domain') {
|
|
30
|
+
await handleDomainChange(ctx, yamlConfig);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (editKey === 'monthly_cap') {
|
|
34
|
+
const newValue = await input({ message: 'New monthly cap (blocks):' });
|
|
35
|
+
await applyMonthlyCapChange(ctx, yamlConfig, {
|
|
36
|
+
isJson: false,
|
|
37
|
+
knownTier: tier,
|
|
38
|
+
rawValue: newValue,
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (editKey === 'email_style') {
|
|
43
|
+
const style = await select({
|
|
44
|
+
choices: [
|
|
45
|
+
{ name: 'plain', value: 'plain' },
|
|
46
|
+
{ name: 'branded', value: 'branded' },
|
|
47
|
+
],
|
|
48
|
+
message: 'Email style:',
|
|
49
|
+
});
|
|
50
|
+
yamlConfig.project.emailStyle = style;
|
|
51
|
+
await saveYaml(yamlConfig);
|
|
52
|
+
await ctx.syncYaml();
|
|
53
|
+
ctx.log(`\n ${chalk.green('✓')} email_style updated to ${chalk.cyan(style)}`);
|
|
54
|
+
ctx.log(` ${INFO.DEPLOY_TO_APPLY}\n`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const editPropKey = settingKeyToProp(editKey);
|
|
58
|
+
const newValue = await input({ message: `New value for ${editKey}:` });
|
|
59
|
+
yamlConfig.project[editPropKey] = newValue;
|
|
60
|
+
await saveYaml(yamlConfig);
|
|
61
|
+
await ctx.syncYaml();
|
|
62
|
+
ctx.log(`\n ${chalk.green('✓')} Updated. ${INFO.DEPLOY_TO_APPLY}\n`);
|
|
63
|
+
}
|
|
64
|
+
export async function promptEditSetting(ctx, yamlConfig, tier) {
|
|
65
|
+
const { project } = yamlConfig;
|
|
66
|
+
const editKey = await input({
|
|
67
|
+
default: 'n',
|
|
68
|
+
message: "Edit a setting? (key or 'n'):",
|
|
69
|
+
});
|
|
70
|
+
if (editKey === 'n')
|
|
71
|
+
return;
|
|
72
|
+
if (editKey === 'monthly_cap' && tier === FREE_TIER) {
|
|
73
|
+
ctx.warnFreeTierCapBlocked(false);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const editPropKey = settingKeyToProp(editKey);
|
|
77
|
+
if (!(editPropKey in project)) {
|
|
78
|
+
await handleUnknownEditKey(ctx, yamlConfig, editKey);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await dispatchEditKey(ctx, yamlConfig, editKey, tier);
|
|
82
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ApiResponse } from '../../api-client.js';
|
|
2
|
+
import type { BillingCapUpdateResult } from '../../base-command.js';
|
|
3
|
+
import type { MailmodoYaml } from '../../yaml-config.js';
|
|
4
|
+
export type SettingsYaml = MailmodoYaml;
|
|
5
|
+
export type SettingsCtx = {
|
|
6
|
+
applyBillingCap(opts: {
|
|
7
|
+
cap: number;
|
|
8
|
+
json: boolean;
|
|
9
|
+
}): Promise<BillingCapUpdateResult>;
|
|
10
|
+
collectDomainInputs(yaml: MailmodoYaml, skip: boolean): Promise<{
|
|
11
|
+
address: string;
|
|
12
|
+
domain: string;
|
|
13
|
+
fromEmail: string;
|
|
14
|
+
fromName: string;
|
|
15
|
+
replyTo: string;
|
|
16
|
+
}>;
|
|
17
|
+
ensureAuth(): Promise<void>;
|
|
18
|
+
error(msg: string): never;
|
|
19
|
+
fetchBillingStatus(): Promise<null | {
|
|
20
|
+
cap?: {
|
|
21
|
+
inBlocks: null | number;
|
|
22
|
+
};
|
|
23
|
+
tier?: string;
|
|
24
|
+
}>;
|
|
25
|
+
fetchBillingTier(): Promise<null | string>;
|
|
26
|
+
get<T>(path: string, params?: Record<string, string>): Promise<ApiResponse<T>>;
|
|
27
|
+
log(msg?: string): void;
|
|
28
|
+
onApiError(resp: {
|
|
29
|
+
error?: string;
|
|
30
|
+
status: number;
|
|
31
|
+
}): never;
|
|
32
|
+
postFormData<T>(path: string, formData: FormData): Promise<ApiResponse<T>>;
|
|
33
|
+
registerDomainAndSave(yaml: MailmodoYaml, inputs: {
|
|
34
|
+
address: string;
|
|
35
|
+
domain: string;
|
|
36
|
+
fromEmail: string;
|
|
37
|
+
fromName?: string;
|
|
38
|
+
replyTo?: string;
|
|
39
|
+
}, json: boolean): Promise<{
|
|
40
|
+
dnsGuideUrl?: string;
|
|
41
|
+
dnsRecords: Array<{
|
|
42
|
+
host: string;
|
|
43
|
+
type: string;
|
|
44
|
+
value: string;
|
|
45
|
+
}>;
|
|
46
|
+
}>;
|
|
47
|
+
showDnsRecords(records: Array<{
|
|
48
|
+
host: string;
|
|
49
|
+
type: string;
|
|
50
|
+
value: string;
|
|
51
|
+
}>, guideUrl: string | undefined, json: boolean): void;
|
|
52
|
+
spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
|
|
53
|
+
syncYaml(): Promise<void>;
|
|
54
|
+
warnFreeTierCapBlocked(json: boolean): void;
|
|
55
|
+
};
|
|
56
|
+
export interface LogoUploadResponse {
|
|
57
|
+
url: string;
|
|
58
|
+
}
|
|
59
|
+
export interface DomainStatusResponse {
|
|
60
|
+
bounceRate: number;
|
|
61
|
+
domain: string;
|
|
62
|
+
spamRate: number;
|
|
63
|
+
status: string;
|
|
64
|
+
verified: boolean;
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
function logMetricRow(ctx, metric) {
|
|
3
|
+
const id = (metric.emailId || '').padEnd(30);
|
|
4
|
+
const sent = String(metric.sent ?? 0).padEnd(7);
|
|
5
|
+
const openRate = (metric.open || '0%').padEnd(7);
|
|
6
|
+
const clickRate = (metric.click || '0%').padEnd(8);
|
|
7
|
+
const convRate = metric.conv || '0%';
|
|
8
|
+
ctx.log(` ${id}${sent}${openRate}${clickRate}${convRate}`);
|
|
9
|
+
}
|
|
10
|
+
function logEmailsTable(ctx, emails) {
|
|
11
|
+
ctx.log(`\n ${chalk.bold('Last 7 days')}${''.padEnd(20)}Sent Open Click Conv`);
|
|
12
|
+
ctx.log(` ${'─'.repeat(62)}`);
|
|
13
|
+
if (emails?.length) {
|
|
14
|
+
for (const metric of emails) {
|
|
15
|
+
logMetricRow(ctx, metric);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
ctx.log(` ${chalk.dim('No data yet. Deploy emails first.')}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function logQuota(ctx, quota) {
|
|
23
|
+
if (quota.plan === 'free') {
|
|
24
|
+
ctx.log(` Free tier remaining: ${chalk.cyan(String(quota.freeRemaining))} emails`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (quota.plan === 'paid') {
|
|
28
|
+
if (quota.currentBlockEmailsRemaining !== null &&
|
|
29
|
+
quota.currentBlockEmailsRemaining !== undefined) {
|
|
30
|
+
const { blockSize } = quota;
|
|
31
|
+
const sent = blockSize - quota.currentBlockEmailsRemaining;
|
|
32
|
+
const remaining = quota.currentBlockEmailsRemaining;
|
|
33
|
+
ctx.log(` Current paid block (${blockSize} emails) : ${chalk.cyan(`${sent} emails sent, ${remaining} emails remaining`)}`);
|
|
34
|
+
}
|
|
35
|
+
ctx.log(` Blocks used: ${quota.blocksUsed}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function logStatusOutput(ctx, data) {
|
|
39
|
+
const { emails, monthlySent, quota } = data;
|
|
40
|
+
logEmailsTable(ctx, emails);
|
|
41
|
+
ctx.log('');
|
|
42
|
+
if (monthlySent !== null && monthlySent !== undefined) {
|
|
43
|
+
ctx.log(` Emails sent this month: ${chalk.bold(String(monthlySent))}`);
|
|
44
|
+
}
|
|
45
|
+
if (quota) {
|
|
46
|
+
logQuota(ctx, quota);
|
|
47
|
+
}
|
|
48
|
+
ctx.log('');
|
|
49
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ApiResponse } from '../../api-client.js';
|
|
2
|
+
export interface EmailMetric {
|
|
3
|
+
click: string;
|
|
4
|
+
conv: string;
|
|
5
|
+
emailId: string;
|
|
6
|
+
open: string;
|
|
7
|
+
sent: number;
|
|
8
|
+
}
|
|
9
|
+
export interface AnalyticsResponse {
|
|
10
|
+
emails: EmailMetric[];
|
|
11
|
+
monthlySent: number;
|
|
12
|
+
quota: {
|
|
13
|
+
blockSize: number;
|
|
14
|
+
blocksUsed: number;
|
|
15
|
+
currentBlockEmailsRemaining: number;
|
|
16
|
+
freeRemaining: number;
|
|
17
|
+
plan: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export type StatusCtx = {
|
|
21
|
+
get<T = Record<string, unknown>>(path: string, params?: Record<string, string>): Promise<ApiResponse<T>>;
|
|
22
|
+
log(msg?: string): void;
|
|
23
|
+
onApiError(resp: {
|
|
24
|
+
error?: string;
|
|
25
|
+
status: number;
|
|
26
|
+
}): never;
|
|
27
|
+
spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
|
|
28
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* when this is true so end users never see internal request metadata.
|
|
5
5
|
*/
|
|
6
6
|
export declare const IS_DEV_MODE: boolean;
|
|
7
|
-
export declare const API_BASE_URL
|
|
7
|
+
export declare const API_BASE_URL: string;
|
|
8
8
|
export declare const API_ENDPOINTS: Readonly<{
|
|
9
9
|
ANALYTICS: "/analytics";
|
|
10
10
|
ANALYZE: "/analyze";
|
|
11
11
|
ASSETS_LOGO: "/assets/logo";
|
|
12
|
+
ASSETS_TEMPLATES: "/assets/templates";
|
|
12
13
|
ASSETS_YAML: "/assets/yaml";
|
|
13
14
|
AUTH_VALIDATE: "/auth/validate";
|
|
14
15
|
BILLING_CAP: "/billing/cap";
|
|
@@ -30,7 +31,7 @@ export declare const API_ENDPOINTS: Readonly<{
|
|
|
30
31
|
SEQUENCES_SDK: "/sequences/sdk";
|
|
31
32
|
SEQUENCES_VALIDATE: "/sequences/validate";
|
|
32
33
|
}>;
|
|
33
|
-
export declare const LOGIN_URL
|
|
34
|
+
export declare const LOGIN_URL: string;
|
|
34
35
|
export declare const PREVIEW_PORT = 3421;
|
|
35
36
|
export declare const DEFAULT_BRAND_COLOR = "#1A56DB";
|
|
36
37
|
export declare const TEMPLATES_DIR = "mailmodo";
|
package/dist/lib/constants.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/** Set by `bin/dev.js` when running the CLI locally (tsx bootstrap). */
|
|
2
2
|
const DEV_API_BASE_URL = 'https://app-vertex-debug.azurewebsites.net';
|
|
3
|
+
const PRODUCTION_API_BASE_URL = 'https://api.mailmodo.dev';
|
|
3
4
|
/**
|
|
4
5
|
* True when the CLI is running in a dev/debug context. Verbose request
|
|
5
6
|
* diagnostics (URL, status, response body, error code) are only surfaced
|
|
6
7
|
* when this is true so end users never see internal request metadata.
|
|
7
8
|
*/
|
|
8
9
|
export const IS_DEV_MODE = Boolean(process.env.MAILMODO_DEV_TSX || process.env.MAILMODO_DEBUG);
|
|
9
|
-
// const PRODUCTION_API_BASE_URL = 'https://api.mailmodo.com';
|
|
10
|
-
const PRODUCTION_API_BASE_URL = 'https://app-vertex-debug.azurewebsites.net';
|
|
11
10
|
export const API_BASE_URL = process.env.MAILMODO_DEV_TSX
|
|
12
11
|
? DEV_API_BASE_URL
|
|
13
12
|
: PRODUCTION_API_BASE_URL;
|
|
@@ -15,6 +14,7 @@ export const API_ENDPOINTS = Object.freeze({
|
|
|
15
14
|
ANALYTICS: '/analytics',
|
|
16
15
|
ANALYZE: '/analyze',
|
|
17
16
|
ASSETS_LOGO: '/assets/logo',
|
|
17
|
+
ASSETS_TEMPLATES: '/assets/templates',
|
|
18
18
|
ASSETS_YAML: '/assets/yaml',
|
|
19
19
|
AUTH_VALIDATE: '/auth/validate',
|
|
20
20
|
BILLING_CAP: '/billing/cap',
|
|
@@ -36,9 +36,8 @@ export const API_ENDPOINTS = Object.freeze({
|
|
|
36
36
|
SEQUENCES_SDK: '/sequences/sdk',
|
|
37
37
|
SEQUENCES_VALIDATE: '/sequences/validate',
|
|
38
38
|
});
|
|
39
|
-
const DEV_LOGIN_URL = 'https://app-vertex-debug.azurewebsites.net/signup
|
|
40
|
-
|
|
41
|
-
const PRODUCTION_LOGIN_URL = 'https://app-vertex-debug.azurewebsites.net/signup.html';
|
|
39
|
+
const DEV_LOGIN_URL = 'https://app-vertex-debug.azurewebsites.net/signup';
|
|
40
|
+
const PRODUCTION_LOGIN_URL = 'https://app.mailmodo.dev/signup';
|
|
42
41
|
export const LOGIN_URL = process.env.MAILMODO_DEV_TSX
|
|
43
42
|
? DEV_LOGIN_URL
|
|
44
43
|
: PRODUCTION_LOGIN_URL;
|
package/dist/lib/messages.d.ts
CHANGED
|
@@ -13,6 +13,16 @@ export declare const PROMPTS: {
|
|
|
13
13
|
readonly REPLY_TO: "Reply-to address (optional, press Enter to use sender email):";
|
|
14
14
|
readonly SENDER_EMAIL: "Sender email address:";
|
|
15
15
|
};
|
|
16
|
+
export declare const BLANK_DIR: {
|
|
17
|
+
readonly CHOICE_FRESH: "Start fresh — create a new project here";
|
|
18
|
+
readonly CHOICE_RESTORE: "Restore project files from server";
|
|
19
|
+
readonly CHOICE_RESTORE_INIT: "Restore existing project files from server";
|
|
20
|
+
readonly CHOICE_SKIP: "Skip — I'll navigate to my project directory manually";
|
|
21
|
+
readonly PROMPT: "No mailmodo.yaml or templates found in this directory.";
|
|
22
|
+
readonly PROMPT_INIT: "A project was found on the server. What would you like to do?";
|
|
23
|
+
readonly RESTORED_INIT: `Project restored from server. Run ${string} to re-deploy, or ${string} to review your emails.`;
|
|
24
|
+
readonly SKIP_HINT: `Navigate to your project directory and run your command there, or run ${string} to start a new project here.`;
|
|
25
|
+
};
|
|
16
26
|
export declare const MISSING_TEMPLATES: {
|
|
17
27
|
readonly ABORT_HINT: `Restore the missing files from version control, then run ${string} again.`;
|
|
18
28
|
readonly CHOICE_ABORT: "Abort (restore from version control)";
|
|
@@ -63,6 +73,7 @@ export declare const DEPLOY: {
|
|
|
63
73
|
readonly SDK_ONBOARDING_HEADER: string;
|
|
64
74
|
readonly SUCCESS: `${string} Emails are live.`;
|
|
65
75
|
};
|
|
76
|
+
export declare function restoredFromServerHint(ids: string[]): string;
|
|
66
77
|
export declare function pauseSuccess(sequenceId: string): string;
|
|
67
78
|
export declare function pauseAlready(sequenceId: string): string;
|
|
68
79
|
export declare function resumeSuccess(sequenceId: string): string;
|
package/dist/lib/messages.js
CHANGED
|
@@ -14,6 +14,16 @@ export const PROMPTS = {
|
|
|
14
14
|
REPLY_TO: 'Reply-to address (optional, press Enter to use sender email):',
|
|
15
15
|
SENDER_EMAIL: 'Sender email address:',
|
|
16
16
|
};
|
|
17
|
+
export const BLANK_DIR = {
|
|
18
|
+
CHOICE_FRESH: 'Start fresh — create a new project here',
|
|
19
|
+
CHOICE_RESTORE: 'Restore project files from server',
|
|
20
|
+
CHOICE_RESTORE_INIT: 'Restore existing project files from server',
|
|
21
|
+
CHOICE_SKIP: "Skip — I'll navigate to my project directory manually",
|
|
22
|
+
PROMPT: 'No mailmodo.yaml or templates found in this directory.',
|
|
23
|
+
PROMPT_INIT: 'A project was found on the server. What would you like to do?',
|
|
24
|
+
RESTORED_INIT: `Project restored from server. Run ${chalk.cyan('mailmodo deploy')} to re-deploy, or ${chalk.cyan('mailmodo emails')} to review your emails.`,
|
|
25
|
+
SKIP_HINT: `Navigate to your project directory and run your command there, or run ${chalk.cyan('mailmodo init')} to start a new project here.`,
|
|
26
|
+
};
|
|
17
27
|
export const MISSING_TEMPLATES = {
|
|
18
28
|
ABORT_HINT: `Restore the missing files from version control, then run ${chalk.cyan('mailmodo deploy')} again.`,
|
|
19
29
|
CHOICE_ABORT: 'Abort (restore from version control)',
|
|
@@ -63,6 +73,27 @@ export const DEPLOY = {
|
|
|
63
73
|
SDK_ONBOARDING_HEADER: chalk.bold('ADD THIS TO YOUR APP (one-time only):'),
|
|
64
74
|
SUCCESS: `${chalk.green('Deployed.')} Emails are live.`,
|
|
65
75
|
};
|
|
76
|
+
export function restoredFromServerHint(ids) {
|
|
77
|
+
if (ids.length === 1) {
|
|
78
|
+
const [id] = ids;
|
|
79
|
+
return (`Template ${chalk.cyan(id)} was not found locally and has been refreshed from the server.\n` +
|
|
80
|
+
` Review it with ${chalk.cyan(`mailmodo preview ${id}`)}, then run ${chalk.cyan('mailmodo deploy')} again.`);
|
|
81
|
+
}
|
|
82
|
+
const header = 'Template ID';
|
|
83
|
+
const colWidth = Math.max(header.length, ...ids.map((id) => id.length));
|
|
84
|
+
const pad = (s) => s.padEnd(colWidth);
|
|
85
|
+
const hr = '─'.repeat(colWidth + 2);
|
|
86
|
+
const table = [
|
|
87
|
+
` ┌${hr}┐`,
|
|
88
|
+
` │ ${chalk.bold(pad(header))} │`,
|
|
89
|
+
` ├${hr}┤`,
|
|
90
|
+
...ids.map((id) => ` │ ${chalk.cyan(pad(id))} │`),
|
|
91
|
+
` └${hr}┘`,
|
|
92
|
+
].join('\n');
|
|
93
|
+
return (`${ids.length} templates were not found locally and have been refreshed from the server:\n\n` +
|
|
94
|
+
`${table}\n\n` +
|
|
95
|
+
` Review each with ${chalk.cyan('mailmodo preview <id>')}, then run ${chalk.cyan('mailmodo deploy')} again.`);
|
|
96
|
+
}
|
|
66
97
|
export function pauseSuccess(sequenceId) {
|
|
67
98
|
return `Sequence ${chalk.cyan(sequenceId)} paused. Run ${chalk.cyan(`mailmodo deploy --resume ${sequenceId}`)} to resume.`;
|
|
68
99
|
}
|