@mailmodo/cli 0.0.55 → 0.0.56-beta.pr58.101
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 +51 -200
- 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 +38 -10
- package/dist/lib/base-command.js +171 -18
- 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/dist/lib/yaml-config.d.ts +1 -0
- package/dist/lib/yaml-config.js +8 -0
- package/oclif.manifest.json +100 -100
- 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,38 @@
|
|
|
1
|
+
import type { ApiResponse } from '../../api-client.js';
|
|
2
|
+
import type { EmailConfig, MailmodoYaml } from '../../yaml-config.js';
|
|
3
|
+
export interface EditResponse {
|
|
4
|
+
ctaText?: string;
|
|
5
|
+
html?: string;
|
|
6
|
+
previewText?: string;
|
|
7
|
+
subject?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface EditContext {
|
|
10
|
+
email: EmailConfig;
|
|
11
|
+
emailIndex: number;
|
|
12
|
+
templateFilename: string;
|
|
13
|
+
templateHtml: null | string;
|
|
14
|
+
yamlConfig: MailmodoYaml;
|
|
15
|
+
}
|
|
16
|
+
export type EditFlags = {
|
|
17
|
+
json: boolean;
|
|
18
|
+
yes: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type EditCtx = {
|
|
21
|
+
error(msg: string): never;
|
|
22
|
+
exit(code?: number): never;
|
|
23
|
+
handleAiQuotaError(resp: {
|
|
24
|
+
data?: unknown;
|
|
25
|
+
error?: string;
|
|
26
|
+
status: number;
|
|
27
|
+
}, feature: 'edit'): void;
|
|
28
|
+
log(msg?: string): void;
|
|
29
|
+
onApiError(resp: {
|
|
30
|
+
error?: string;
|
|
31
|
+
status: number;
|
|
32
|
+
}): never;
|
|
33
|
+
post<T>(path: string, body?: unknown): Promise<ApiResponse<T>>;
|
|
34
|
+
runCommand(id: string, argv: string[]): Promise<void>;
|
|
35
|
+
spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
|
|
36
|
+
syncTemplate(emailId: string): Promise<void>;
|
|
37
|
+
syncYaml(): Promise<void>;
|
|
38
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import open from 'open';
|
|
5
|
+
function trySpawnEditor(editor, filePath) {
|
|
6
|
+
const [cmd, args] = process.platform === 'win32'
|
|
7
|
+
? ['cmd.exe', ['/c', editor, filePath]]
|
|
8
|
+
: [editor, [filePath]];
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const child = spawn(cmd, [...args], { stdio: 'ignore' });
|
|
11
|
+
child.on('error', () => {
|
|
12
|
+
resolve(false);
|
|
13
|
+
});
|
|
14
|
+
child.on('close', (code) => {
|
|
15
|
+
resolve(code === 0);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export async function openTemplateInEditor(ctx, template) {
|
|
20
|
+
const templatePath = join(process.cwd(), template);
|
|
21
|
+
ctx.log(`\n Opening ${template}...\n`);
|
|
22
|
+
const editor = process.env.VISUAL || process.env.EDITOR;
|
|
23
|
+
if (editor) {
|
|
24
|
+
const [cmd, ...editorArgs] = editor.trim().split(/\s+/);
|
|
25
|
+
const launched = await new Promise((resolve) => {
|
|
26
|
+
const child = spawn(cmd, [...editorArgs, templatePath], {
|
|
27
|
+
stdio: 'inherit',
|
|
28
|
+
});
|
|
29
|
+
child.on('error', () => resolve(false));
|
|
30
|
+
child.on('close', (code) => resolve(code === 0));
|
|
31
|
+
});
|
|
32
|
+
if (launched)
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (await trySpawnEditor('code', templatePath))
|
|
36
|
+
return;
|
|
37
|
+
try {
|
|
38
|
+
await open(templatePath);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
ctx.log(` ${chalk.dim(`Could not open editor. Open the file manually: ${templatePath}`)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { EmailConfig } from '../../yaml-config.js';
|
|
2
|
+
import type { EmailsCtx } from './types.js';
|
|
3
|
+
export declare function renderEmailTable(ctx: EmailsCtx, emails: EmailConfig[]): void;
|
|
4
|
+
export declare function renderEmailDetail(ctx: EmailsCtx, email: EmailConfig): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export function renderEmailTable(ctx, emails) {
|
|
3
|
+
ctx.log(`\n ${chalk.bold(String(emails.length))} emails configured in mailmodo.yaml:\n`);
|
|
4
|
+
const idColW = Math.max(...emails.map((e) => e.id.length), 'ID'.length) + 2;
|
|
5
|
+
const triggerColW = Math.max(...emails.map((e) => e.trigger.length), 'Trigger'.length) + 2;
|
|
6
|
+
const delayColW = Math.max(...emails.map((e) => String(e.delay).length), 'Delay'.length) + 2;
|
|
7
|
+
const hasConditions = emails.some((e) => e.condition);
|
|
8
|
+
ctx.log(` ${chalk.bold('ID'.padEnd(idColW))}${chalk.bold('Trigger'.padEnd(triggerColW))}${chalk.bold('Delay'.padEnd(delayColW))}${hasConditions ? chalk.bold('Condition') : ''}`);
|
|
9
|
+
ctx.log(` ${'─'.repeat(idColW + triggerColW + delayColW + (hasConditions ? 'Condition'.length : 0))}`);
|
|
10
|
+
for (const email of emails) {
|
|
11
|
+
const id = chalk.cyan(email.id.padEnd(idColW));
|
|
12
|
+
const trigger = email.trigger.padEnd(triggerColW);
|
|
13
|
+
const delay = String(email.delay).padEnd(delayColW);
|
|
14
|
+
const condition = email.condition ? chalk.dim(email.condition) : '';
|
|
15
|
+
ctx.log(` ${id}${trigger}${delay}${condition}`);
|
|
16
|
+
}
|
|
17
|
+
ctx.log('');
|
|
18
|
+
}
|
|
19
|
+
export function renderEmailDetail(ctx, email) {
|
|
20
|
+
ctx.log('');
|
|
21
|
+
ctx.log(` ${chalk.bold('ID:')} ${email.id}`);
|
|
22
|
+
ctx.log(` ${chalk.bold('Trigger:')} ${email.trigger}`);
|
|
23
|
+
ctx.log(` ${chalk.bold('Delay:')} ${email.delay === 0 || email.delay === '0' ? '0 (immediate)' : email.delay}`);
|
|
24
|
+
ctx.log(` ${chalk.bold('Subject:')} ${email.subject}`);
|
|
25
|
+
ctx.log(` ${chalk.bold('Template:')} ${email.template}`);
|
|
26
|
+
if (email.style) {
|
|
27
|
+
ctx.log(` ${chalk.bold('Style:')} ${email.style}`);
|
|
28
|
+
}
|
|
29
|
+
if (email.condition) {
|
|
30
|
+
ctx.log(` ${chalk.bold('Condition:')} ${email.condition}`);
|
|
31
|
+
}
|
|
32
|
+
if (email.goal) {
|
|
33
|
+
ctx.log(` ${chalk.bold('Goal:')} ${email.goal}`);
|
|
34
|
+
}
|
|
35
|
+
ctx.log('');
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
}
|