@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,73 @@
|
|
|
1
|
+
import { editor, input, select } from '@inquirer/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { API_ENDPOINTS } from '../../constants.js';
|
|
4
|
+
import { isValidUrl } from '../../utils.js';
|
|
5
|
+
import { logAnalysisSummary } from './output.js';
|
|
6
|
+
export async function promptProductUrl(flagUrl) {
|
|
7
|
+
if (flagUrl) {
|
|
8
|
+
if (!isValidUrl(flagUrl)) {
|
|
9
|
+
throw new Error(`Invalid URL: "${flagUrl}". Please enter a valid URL (e.g., https://myapp.com)`);
|
|
10
|
+
}
|
|
11
|
+
return flagUrl;
|
|
12
|
+
}
|
|
13
|
+
return input({
|
|
14
|
+
message: 'What is your product URL?',
|
|
15
|
+
validate(value) {
|
|
16
|
+
if (!value?.trim())
|
|
17
|
+
return 'URL is required';
|
|
18
|
+
if (isValidUrl(value))
|
|
19
|
+
return true;
|
|
20
|
+
return 'Please enter a valid URL (e.g., https://myapp.com)';
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export async function analyzeProduct(ctx, productUrl, flags) {
|
|
25
|
+
const response = await ctx.spinner(' Analyzing your product — scraping homepage, pricing, features', flags.json, () => ctx.post(API_ENDPOINTS.ANALYZE, { url: productUrl }));
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
ctx.onAiQuotaError(response, 'init');
|
|
28
|
+
ctx.onApiError(response);
|
|
29
|
+
}
|
|
30
|
+
const analysis = response.data;
|
|
31
|
+
const shouldShow = !flags.json || !flags.yes;
|
|
32
|
+
if (shouldShow) {
|
|
33
|
+
const logLine = flags.json && !flags.yes ? ctx.logToStderr : ctx.log;
|
|
34
|
+
logAnalysisSummary(analysis, logLine);
|
|
35
|
+
}
|
|
36
|
+
if (flags.yes)
|
|
37
|
+
return analysis;
|
|
38
|
+
return promptConfirmAnalysis(ctx, analysis);
|
|
39
|
+
}
|
|
40
|
+
async function promptConfirmAnalysis(ctx, analysis) {
|
|
41
|
+
const userAction = await select({
|
|
42
|
+
choices: [
|
|
43
|
+
{ name: 'Yes - continue with this result', value: 'yes' },
|
|
44
|
+
{ name: 'No - stop here', value: 'no' },
|
|
45
|
+
{ name: 'Edit - update the result before continuing', value: 'edit' },
|
|
46
|
+
],
|
|
47
|
+
message: 'Does this look right?',
|
|
48
|
+
});
|
|
49
|
+
if (userAction === 'no') {
|
|
50
|
+
ctx.log(`\n Stopped. Run ${chalk.cyan('mailmodo init')} again when you are ready.\n`);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (userAction === 'edit') {
|
|
54
|
+
const edited = await editor({
|
|
55
|
+
default: JSON.stringify(analysis, null, 2),
|
|
56
|
+
message: 'Edit the analysis JSON. Save and close to continue.',
|
|
57
|
+
postfix: '.json',
|
|
58
|
+
validate(value) {
|
|
59
|
+
if (!value?.trim())
|
|
60
|
+
return 'Edited analysis cannot be empty';
|
|
61
|
+
try {
|
|
62
|
+
JSON.parse(value);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return 'Please provide valid JSON';
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
return JSON.parse(edited);
|
|
71
|
+
}
|
|
72
|
+
return analysis;
|
|
73
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EmailConfig, MailmodoYaml } from '../../yaml-config.js';
|
|
2
|
+
import type { AnalysisResult, InitCtx, InitFlags } from './types.js';
|
|
3
|
+
type SuccessOpts = {
|
|
4
|
+
brand: AnalysisResult['brand'];
|
|
5
|
+
emailConfigs: EmailConfig[];
|
|
6
|
+
emailStyle: string | undefined;
|
|
7
|
+
json: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function logAnalysisSummary(analysis: AnalysisResult, logLine: (message?: string) => void): void;
|
|
10
|
+
export declare function confirmOverwrite(ctx: InitCtx, flags: InitFlags, existing: MailmodoYaml | null): Promise<boolean>;
|
|
11
|
+
export declare function logInitSuccess(ctx: InitCtx, opts: SuccessOpts): void;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { confirm } from '@inquirer/prompts';
|
|
3
|
+
export function logAnalysisSummary(analysis, logLine) {
|
|
4
|
+
logLine(` Detected:`);
|
|
5
|
+
logLine(` - Type: ${analysis.businessType} — ${analysis.description}`);
|
|
6
|
+
logLine(` - Model: ${analysis.pricingModel}`);
|
|
7
|
+
logLine(` - Users: ${analysis.targetUser}`);
|
|
8
|
+
if (analysis.events?.length) {
|
|
9
|
+
logLine(` - Events: ${analysis.events.join(', ')}`);
|
|
10
|
+
}
|
|
11
|
+
logLine('');
|
|
12
|
+
}
|
|
13
|
+
export async function confirmOverwrite(ctx, flags, existing) {
|
|
14
|
+
if (!existing || flags.yes)
|
|
15
|
+
return true;
|
|
16
|
+
ctx.log(`\n ${chalk.yellow('⚠')} ${chalk.bold('mailmodo.yaml already exists.')}`);
|
|
17
|
+
ctx.log(` Running init will overwrite your current project configuration and all email templates.\n`);
|
|
18
|
+
const proceed = await confirm({
|
|
19
|
+
default: false,
|
|
20
|
+
message: 'Overwrite existing configuration and templates?',
|
|
21
|
+
});
|
|
22
|
+
if (!proceed) {
|
|
23
|
+
ctx.log(`\n Init cancelled. Run ${chalk.cyan('mailmodo edit')} to modify individual emails.\n`);
|
|
24
|
+
}
|
|
25
|
+
return proceed;
|
|
26
|
+
}
|
|
27
|
+
export function logInitSuccess(ctx, opts) {
|
|
28
|
+
if (opts.json) {
|
|
29
|
+
ctx.log(JSON.stringify({
|
|
30
|
+
brandDetected: opts.brand,
|
|
31
|
+
emails: opts.emailConfigs,
|
|
32
|
+
emailsCreated: opts.emailConfigs.length,
|
|
33
|
+
style: opts.emailStyle,
|
|
34
|
+
}, null, 2));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
ctx.log(` Created ${chalk.green('mailmodo.yaml')} + ${chalk.green(String(opts.emailConfigs.length))} email templates in ${chalk.green('/mailmodo')}\n`);
|
|
38
|
+
ctx.log(` Run ${chalk.cyan("'mailmodo emails'")} to review.\n`);
|
|
39
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type EmailConfig, type MailmodoYaml } from '../../yaml-config.js';
|
|
2
|
+
import type { AnalysisResult, GeneratedEmail, InitCtx } from './types.js';
|
|
3
|
+
export declare function buildEmailConfig(rec: AnalysisResult['recommendedEmails'][number], generated: GeneratedEmail | undefined, productName: string): EmailConfig;
|
|
4
|
+
export declare function buildEmailConfigs(analysisPayload: AnalysisResult, generatedEmails: GeneratedEmail[]): EmailConfig[];
|
|
5
|
+
export declare function buildYamlConfig(analysisPayload: AnalysisResult, emailConfigs: EmailConfig[], productUrl: string): MailmodoYaml;
|
|
6
|
+
export declare function preserveUserFields(config: MailmodoYaml, existing: MailmodoYaml): void;
|
|
7
|
+
export declare function applyMonthlyCap(ctx: InitCtx, yamlConfig: MailmodoYaml): Promise<void>;
|
|
8
|
+
export declare function saveAllTemplates(recommendedEmails: AnalysisResult['recommendedEmails'], generatedEmails: GeneratedEmail[]): Promise<void>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { DEFAULT_BRAND_COLOR } from '../../constants.js';
|
|
2
|
+
import { saveTemplate, saveYaml, } from '../../yaml-config.js';
|
|
3
|
+
import { normalizeTrigger } from '../../utils.js';
|
|
4
|
+
export function buildEmailConfig(rec, generated, productName) {
|
|
5
|
+
return {
|
|
6
|
+
delay: rec.delay || '0',
|
|
7
|
+
id: rec.id,
|
|
8
|
+
trigger: normalizeTrigger(rec.trigger, productName),
|
|
9
|
+
...(rec.condition ? { condition: rec.condition } : {}),
|
|
10
|
+
subject: generated?.subject || `Email for ${rec.id}`,
|
|
11
|
+
template: `mailmodo/${rec.id}.html`,
|
|
12
|
+
...(generated?.previewText ? { previewText: generated.previewText } : {}),
|
|
13
|
+
...(generated?.ctaText ? { ctaText: generated.ctaText } : {}),
|
|
14
|
+
goal: rec.goal,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function buildEmailConfigs(analysisPayload, generatedEmails) {
|
|
18
|
+
return analysisPayload.recommendedEmails.map((rec, index) => buildEmailConfig(rec, generatedEmails[index], analysisPayload.productName));
|
|
19
|
+
}
|
|
20
|
+
export function buildYamlConfig(analysisPayload, emailConfigs, productUrl) {
|
|
21
|
+
return {
|
|
22
|
+
emails: emailConfigs,
|
|
23
|
+
project: {
|
|
24
|
+
brandColor: analysisPayload.brand?.color || DEFAULT_BRAND_COLOR,
|
|
25
|
+
description: analysisPayload.description,
|
|
26
|
+
emailStyle: 'plain',
|
|
27
|
+
fromEmail: '',
|
|
28
|
+
fromName: `Team ${analysisPayload.productName}`,
|
|
29
|
+
logoUrl: analysisPayload.brand?.logoUrl || '',
|
|
30
|
+
name: analysisPayload.productName,
|
|
31
|
+
pricingModel: analysisPayload.pricingModel,
|
|
32
|
+
replyTo: '',
|
|
33
|
+
saasModel: analysisPayload.saasModel,
|
|
34
|
+
targetUser: analysisPayload.targetUser,
|
|
35
|
+
type: analysisPayload.businessType,
|
|
36
|
+
url: productUrl,
|
|
37
|
+
webhookUrl: '',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function preserveUserFields(config, existing) {
|
|
42
|
+
const p = existing.project;
|
|
43
|
+
if (p.fromEmail)
|
|
44
|
+
config.project.fromEmail = p.fromEmail;
|
|
45
|
+
if (p.replyTo)
|
|
46
|
+
config.project.replyTo = p.replyTo;
|
|
47
|
+
if (p.fromName)
|
|
48
|
+
config.project.fromName = p.fromName;
|
|
49
|
+
if (p.webhookUrl)
|
|
50
|
+
config.project.webhookUrl = p.webhookUrl;
|
|
51
|
+
if (p.emailStyle)
|
|
52
|
+
config.project.emailStyle = p.emailStyle;
|
|
53
|
+
if (p.domain)
|
|
54
|
+
config.project.domain = p.domain;
|
|
55
|
+
if (p.address)
|
|
56
|
+
config.project.address = p.address;
|
|
57
|
+
if (p.logoFile)
|
|
58
|
+
config.project.logoFile = p.logoFile;
|
|
59
|
+
}
|
|
60
|
+
export async function applyMonthlyCap(ctx, yamlConfig) {
|
|
61
|
+
const monthlyCap = await ctx.getBillingCap();
|
|
62
|
+
if (monthlyCap !== null && monthlyCap !== undefined) {
|
|
63
|
+
yamlConfig.project.monthlyCap = monthlyCap;
|
|
64
|
+
await saveYaml(yamlConfig);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export async function saveAllTemplates(recommendedEmails, generatedEmails) {
|
|
68
|
+
const saves = recommendedEmails.flatMap((rec, index) => {
|
|
69
|
+
const gen = generatedEmails[index];
|
|
70
|
+
const result = [];
|
|
71
|
+
if (gen?.html)
|
|
72
|
+
result.push(saveTemplate(`${rec.id}.html`, gen.html));
|
|
73
|
+
if (gen?.plainHtml)
|
|
74
|
+
result.push(saveTemplate(`${rec.id}_plain.html`, gen.plainHtml));
|
|
75
|
+
return result;
|
|
76
|
+
});
|
|
77
|
+
await Promise.all(saves);
|
|
78
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ApiResponse } from '../../api-client.js';
|
|
2
|
+
import type { AiQuotaFeature } from '../../base-command.js';
|
|
3
|
+
export interface AnalysisResult {
|
|
4
|
+
brand: {
|
|
5
|
+
color: string;
|
|
6
|
+
logoUrl: string;
|
|
7
|
+
};
|
|
8
|
+
businessType: string;
|
|
9
|
+
description: string;
|
|
10
|
+
events: string[];
|
|
11
|
+
pricingModel: string;
|
|
12
|
+
productName: string;
|
|
13
|
+
recommendedEmails: Array<{
|
|
14
|
+
condition: null | string;
|
|
15
|
+
delay: string;
|
|
16
|
+
goal: string;
|
|
17
|
+
id: string;
|
|
18
|
+
isReminder: boolean;
|
|
19
|
+
priority: string;
|
|
20
|
+
trigger: string;
|
|
21
|
+
}>;
|
|
22
|
+
saasModel: string;
|
|
23
|
+
targetUser: string;
|
|
24
|
+
}
|
|
25
|
+
export interface GeneratedEmail {
|
|
26
|
+
ctaText: string;
|
|
27
|
+
html: string;
|
|
28
|
+
id: string;
|
|
29
|
+
plainHtml: string;
|
|
30
|
+
previewText: string;
|
|
31
|
+
subject: string;
|
|
32
|
+
}
|
|
33
|
+
export interface GenerateResult {
|
|
34
|
+
emails: GeneratedEmail[];
|
|
35
|
+
}
|
|
36
|
+
export type InitFlags = {
|
|
37
|
+
json: boolean;
|
|
38
|
+
yes: boolean;
|
|
39
|
+
};
|
|
40
|
+
export type InitCtx = {
|
|
41
|
+
get<T>(path: string, params?: Record<string, string>): Promise<ApiResponse<T>>;
|
|
42
|
+
getBillingCap(): Promise<number | undefined>;
|
|
43
|
+
log(msg?: string): void;
|
|
44
|
+
logToStderr(msg?: string): void;
|
|
45
|
+
onAiQuotaError(resp: {
|
|
46
|
+
data?: unknown;
|
|
47
|
+
error?: string;
|
|
48
|
+
status: number;
|
|
49
|
+
}, feature: AiQuotaFeature): void;
|
|
50
|
+
onApiError(resp: {
|
|
51
|
+
error?: string;
|
|
52
|
+
status: number;
|
|
53
|
+
}): never;
|
|
54
|
+
post<T>(path: string, body?: unknown): Promise<ApiResponse<T>>;
|
|
55
|
+
spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
|
|
56
|
+
syncYaml(): Promise<void>;
|
|
57
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LoginCtx, ValidateResponse } from './types.js';
|
|
2
|
+
import type { MailmodoConfig } from '../../config.js';
|
|
3
|
+
export declare function logAlreadyLoggedIn(ctx: LoginCtx, existing: MailmodoConfig, opts: {
|
|
4
|
+
json: boolean;
|
|
5
|
+
}): void;
|
|
6
|
+
export declare function logLoginSuccess(ctx: LoginCtx, data: Pick<ValidateResponse, 'email' | 'paidEmailsRemaining' | 'plan' | 'totalFreeRemaining'>, opts: {
|
|
7
|
+
json: boolean;
|
|
8
|
+
}): void;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export function logAlreadyLoggedIn(ctx, existing, opts) {
|
|
3
|
+
if (opts.json) {
|
|
4
|
+
ctx.log(JSON.stringify({
|
|
5
|
+
email: existing.email ?? null,
|
|
6
|
+
status: 'already_logged_in',
|
|
7
|
+
totalFreeRemaining: existing.totalFreeRemaining ?? null,
|
|
8
|
+
}, null, 2));
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
ctx.log('\n You are already logged in.\n');
|
|
12
|
+
const emailDisplay = existing.email?.trim()
|
|
13
|
+
? chalk.green(existing.email.trim())
|
|
14
|
+
: chalk.dim('(unknown)');
|
|
15
|
+
ctx.log(` Email: ${emailDisplay}\n`);
|
|
16
|
+
ctx.log(` ${chalk.dim('1.')} Run ${chalk.cyan('mailmodo init')} to generate an email sequence.`);
|
|
17
|
+
ctx.log(` ${chalk.dim('2.')} Run ${chalk.cyan('mailmodo logout')} to log in with another account.\n`);
|
|
18
|
+
}
|
|
19
|
+
export function logLoginSuccess(ctx, data, opts) {
|
|
20
|
+
const { email, plan, totalFreeRemaining, paidEmailsRemaining } = data;
|
|
21
|
+
if (opts.json) {
|
|
22
|
+
ctx.log(JSON.stringify({
|
|
23
|
+
email,
|
|
24
|
+
paidEmailsRemaining,
|
|
25
|
+
plan,
|
|
26
|
+
status: 'authenticated',
|
|
27
|
+
totalFreeRemaining,
|
|
28
|
+
}, null, 2));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
ctx.log(`\n Logged in as ${chalk.green(email)}`);
|
|
32
|
+
if (plan === 'free') {
|
|
33
|
+
ctx.log(` Free tier: ${chalk.cyan(String(totalFreeRemaining))} emails remaining`);
|
|
34
|
+
ctx.log(' No credit card required.\n');
|
|
35
|
+
}
|
|
36
|
+
if (plan === 'paid') {
|
|
37
|
+
ctx.log(` Current paid block: ${chalk.cyan(String(paidEmailsRemaining))} emails remaining\n`);
|
|
38
|
+
}
|
|
39
|
+
ctx.log(` Next: Run ${chalk.cyan("'mailmodo init'")} to generate your email sequences.\n`);
|
|
40
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ValidateResponse {
|
|
2
|
+
createdOn: string;
|
|
3
|
+
email: string;
|
|
4
|
+
id: string;
|
|
5
|
+
isPaidUser: boolean;
|
|
6
|
+
isVerified: boolean;
|
|
7
|
+
paidEmailsRemaining: number;
|
|
8
|
+
plan: string;
|
|
9
|
+
totalEmailsSent: number;
|
|
10
|
+
totalFreeRemaining: number;
|
|
11
|
+
}
|
|
12
|
+
export type LoginCtx = {
|
|
13
|
+
log(msg?: string): void;
|
|
14
|
+
onApiError(resp: {
|
|
15
|
+
error?: string;
|
|
16
|
+
status: number;
|
|
17
|
+
}): never;
|
|
18
|
+
spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
|
|
19
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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,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(' ', ' ')
|
|
24
|
+
.replaceAll('&', '&')
|
|
25
|
+
.replaceAll('<', '<')
|
|
26
|
+
.replaceAll('>', '>')
|
|
27
|
+
.replaceAll('"', '"')
|
|
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,22 @@
|
|
|
1
|
+
import type { ApiResponse } from '../../api-client.js';
|
|
2
|
+
import type { MailmodoYaml } from '../../yaml-config.js';
|
|
3
|
+
export type PreviewCtx = {
|
|
4
|
+
error(msg: string): never;
|
|
5
|
+
exit(code?: number): never;
|
|
6
|
+
fetchTemplate(emailId: string): Promise<boolean>;
|
|
7
|
+
log(msg?: string): void;
|
|
8
|
+
onApiError(resp: {
|
|
9
|
+
error?: string;
|
|
10
|
+
status: number;
|
|
11
|
+
}): never;
|
|
12
|
+
post<T>(path: string, body?: unknown): Promise<ApiResponse<T>>;
|
|
13
|
+
spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
|
|
14
|
+
syncTemplates(yaml: MailmodoYaml): Promise<void>;
|
|
15
|
+
syncYaml(): Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
export type PreviewEmail = {
|
|
18
|
+
id: string;
|
|
19
|
+
previewText?: string;
|
|
20
|
+
style?: string;
|
|
21
|
+
subject: string;
|
|
22
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
}
|