@mailmodo/cli 0.0.55-beta.pr57.93 → 0.0.55
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 +11 -1
- package/dist/commands/billing/index.js +184 -28
- package/dist/commands/contacts/index.d.ts +19 -1
- package/dist/commands/contacts/index.js +114 -21
- package/dist/commands/deploy/index.js +4 -4
- package/dist/commands/deployments/index.d.ts +4 -1
- package/dist/commands/deployments/index.js +52 -11
- package/dist/commands/domain/index.d.ts +14 -1
- package/dist/commands/domain/index.js +100 -19
- package/dist/commands/edit/index.d.ts +20 -2
- package/dist/commands/edit/index.js +258 -30
- package/dist/commands/emails/index.d.ts +2 -1
- package/dist/commands/emails/index.js +91 -26
- package/dist/commands/init/index.d.ts +3 -1
- package/dist/commands/init/index.js +199 -41
- package/dist/commands/login/index.d.ts +0 -2
- package/dist/commands/login/index.js +76 -32
- package/dist/commands/logs/index.d.ts +8 -1
- package/dist/commands/logs/index.js +55 -12
- package/dist/commands/preview/index.d.ts +19 -1
- package/dist/commands/preview/index.js +212 -30
- package/dist/commands/sdk/index.d.ts +3 -1
- package/dist/commands/sdk/index.js +46 -14
- package/dist/commands/settings/index.d.ts +22 -1
- package/dist/commands/settings/index.js +246 -34
- package/dist/commands/status/index.d.ts +0 -1
- package/dist/commands/status/index.js +39 -13
- package/dist/lib/{commands/deploy → deploy}/domain-setup.d.ts +1 -1
- package/dist/lib/{commands/deploy → deploy}/domain-setup.js +2 -2
- package/dist/lib/{commands/deploy → deploy}/output.d.ts +1 -1
- package/dist/lib/{commands/deploy → deploy}/output.js +2 -2
- package/dist/lib/{commands/deploy → deploy}/payload.d.ts +1 -1
- package/dist/lib/{commands/deploy → deploy}/payload.js +2 -2
- package/dist/lib/{commands/deploy → deploy}/sequence-status.js +2 -2
- package/dist/lib/{commands/deploy → deploy}/types.d.ts +4 -4
- package/dist/lib/templates/missing-templates.d.ts +1 -1
- package/dist/lib/templates/missing-templates.js +1 -1
- package/oclif.manifest.json +54 -54
- package/package.json +1 -1
- package/dist/lib/commands/billing/checkout-status.d.ts +0 -3
- package/dist/lib/commands/billing/checkout-status.js +0 -63
- package/dist/lib/commands/billing/format.d.ts +0 -7
- package/dist/lib/commands/billing/format.js +0 -63
- package/dist/lib/commands/billing/purchase-cap.d.ts +0 -7
- package/dist/lib/commands/billing/purchase-cap.js +0 -57
- package/dist/lib/commands/billing/types.d.ts +0 -72
- package/dist/lib/commands/contacts/actions.d.ts +0 -3
- package/dist/lib/commands/contacts/actions.js +0 -49
- package/dist/lib/commands/contacts/export-delete.d.ts +0 -9
- package/dist/lib/commands/contacts/export-delete.js +0 -51
- package/dist/lib/commands/contacts/types.d.ts +0 -35
- package/dist/lib/commands/contacts/types.js +0 -1
- package/dist/lib/commands/deploy/types.js +0 -1
- package/dist/lib/commands/deployments/output.d.ts +0 -2
- package/dist/lib/commands/deployments/output.js +0 -68
- package/dist/lib/commands/deployments/types.d.ts +0 -24
- package/dist/lib/commands/deployments/types.js +0 -1
- package/dist/lib/commands/domain/setup.d.ts +0 -8
- package/dist/lib/commands/domain/setup.js +0 -53
- package/dist/lib/commands/domain/types.d.ts +0 -56
- package/dist/lib/commands/domain/types.js +0 -1
- package/dist/lib/commands/domain/verify.d.ts +0 -5
- package/dist/lib/commands/domain/verify.js +0 -50
- package/dist/lib/commands/edit/diff.d.ts +0 -7
- package/dist/lib/commands/edit/diff.js +0 -65
- package/dist/lib/commands/edit/display.d.ts +0 -5
- package/dist/lib/commands/edit/display.js +0 -53
- package/dist/lib/commands/edit/flow.d.ts +0 -8
- package/dist/lib/commands/edit/flow.js +0 -70
- package/dist/lib/commands/edit/persist.d.ts +0 -5
- package/dist/lib/commands/edit/persist.js +0 -65
- package/dist/lib/commands/edit/types.d.ts +0 -37
- package/dist/lib/commands/edit/types.js +0 -1
- package/dist/lib/commands/emails/editor.d.ts +0 -2
- package/dist/lib/commands/emails/editor.js +0 -43
- package/dist/lib/commands/emails/output.d.ts +0 -4
- package/dist/lib/commands/emails/output.js +0 -36
- package/dist/lib/commands/emails/types.d.ts +0 -3
- package/dist/lib/commands/emails/types.js +0 -1
- package/dist/lib/commands/init/analysis.d.ts +0 -3
- package/dist/lib/commands/init/analysis.js +0 -69
- package/dist/lib/commands/init/output.d.ts +0 -12
- package/dist/lib/commands/init/output.js +0 -39
- package/dist/lib/commands/init/payload.d.ts +0 -8
- package/dist/lib/commands/init/payload.js +0 -78
- package/dist/lib/commands/init/types.d.ts +0 -57
- package/dist/lib/commands/init/types.js +0 -1
- package/dist/lib/commands/login/output.d.ts +0 -8
- package/dist/lib/commands/login/output.js +0 -53
- package/dist/lib/commands/login/types.d.ts +0 -19
- package/dist/lib/commands/login/types.js +0 -1
- package/dist/lib/commands/logs/output.d.ts +0 -2
- package/dist/lib/commands/logs/output.js +0 -52
- package/dist/lib/commands/logs/types.d.ts +0 -23
- package/dist/lib/commands/logs/types.js +0 -1
- package/dist/lib/commands/preview/actions.d.ts +0 -11
- package/dist/lib/commands/preview/actions.js +0 -43
- package/dist/lib/commands/preview/render.d.ts +0 -3
- package/dist/lib/commands/preview/render.js +0 -30
- package/dist/lib/commands/preview/server.d.ts +0 -8
- package/dist/lib/commands/preview/server.js +0 -63
- package/dist/lib/commands/preview/types.d.ts +0 -19
- package/dist/lib/commands/preview/types.js +0 -1
- package/dist/lib/commands/preview/wrapper-html.d.ts +0 -2
- package/dist/lib/commands/preview/wrapper-html.js +0 -35
- package/dist/lib/commands/sdk/output.d.ts +0 -2
- package/dist/lib/commands/sdk/output.js +0 -42
- package/dist/lib/commands/sdk/types.d.ts +0 -21
- package/dist/lib/commands/sdk/types.js +0 -1
- package/dist/lib/commands/settings/actions.d.ts +0 -10
- package/dist/lib/commands/settings/actions.js +0 -56
- package/dist/lib/commands/settings/display.d.ts +0 -15
- package/dist/lib/commands/settings/display.js +0 -69
- package/dist/lib/commands/settings/logo-domain.d.ts +0 -3
- package/dist/lib/commands/settings/logo-domain.js +0 -47
- package/dist/lib/commands/settings/prompt.d.ts +0 -2
- package/dist/lib/commands/settings/prompt.js +0 -82
- package/dist/lib/commands/settings/types.d.ts +0 -65
- package/dist/lib/commands/settings/types.js +0 -1
- package/dist/lib/commands/status/output.d.ts +0 -2
- package/dist/lib/commands/status/output.js +0 -49
- package/dist/lib/commands/status/types.d.ts +0 -28
- package/dist/lib/commands/status/types.js +0 -1
- /package/dist/lib/{commands/deploy → deploy}/sequence-status.d.ts +0 -0
- /package/dist/lib/{commands/billing → deploy}/types.js +0 -0
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
+
import { confirm, editor, input, select } from '@inquirer/prompts';
|
|
3
|
+
import chalk from 'chalk';
|
|
2
4
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
3
|
-
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
4
|
-
import { loadYaml, saveYaml } from '../../lib/yaml-config.js';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import { API_ENDPOINTS, DEFAULT_BRAND_COLOR } from '../../lib/constants.js';
|
|
6
|
+
import { loadYaml, saveTemplate, saveYaml, } from '../../lib/yaml-config.js';
|
|
7
|
+
import { isValidUrl, normalizeTrigger } from '../../lib/utils.js';
|
|
8
|
+
/**
|
|
9
|
+
* Prints the human-readable analysis summary using the provided line writer.
|
|
10
|
+
* Use stderr when `--json` is set so stdout stays free for machine-readable JSON.
|
|
11
|
+
*
|
|
12
|
+
* @param analysis - Parsed analyze API payload.
|
|
13
|
+
* @param logLine - Line writer (typically bound `this.log` or `this.logToStderr`).
|
|
14
|
+
*/
|
|
15
|
+
function logAnalysisSummary(analysis, logLine) {
|
|
16
|
+
logLine(` Detected:`);
|
|
17
|
+
logLine(` - Type: ${analysis.businessType} — ${analysis.description}`);
|
|
18
|
+
logLine(` - Model: ${analysis.pricingModel}`);
|
|
19
|
+
logLine(` - Users: ${analysis.targetUser}`);
|
|
20
|
+
if (analysis.events?.length) {
|
|
21
|
+
logLine(` - Events: ${analysis.events.join(', ')}`);
|
|
22
|
+
}
|
|
23
|
+
logLine('');
|
|
24
|
+
}
|
|
8
25
|
export default class Init extends BaseCommand {
|
|
9
26
|
static description = 'Analyze your product and generate email sequences';
|
|
10
27
|
static examples = [
|
|
@@ -18,48 +35,189 @@ export default class Init extends BaseCommand {
|
|
|
18
35
|
async run() {
|
|
19
36
|
const { flags } = await this.parse(Init);
|
|
20
37
|
await this.ensureAuth();
|
|
21
|
-
const ctx = this.makeCtx();
|
|
22
|
-
const baseFlags = { json: flags.json, yes: flags.yes };
|
|
23
38
|
const existing = await loadYaml();
|
|
24
|
-
if (!(await
|
|
39
|
+
if (!(await this.confirmOverwriteIfNeeded(flags, existing)))
|
|
25
40
|
return;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
let productUrl = flags.url;
|
|
42
|
+
if (!productUrl) {
|
|
43
|
+
productUrl = await input({
|
|
44
|
+
message: 'What is your product URL?',
|
|
45
|
+
validate(value) {
|
|
46
|
+
if (!value?.trim())
|
|
47
|
+
return 'URL is required';
|
|
48
|
+
if (isValidUrl(value))
|
|
49
|
+
return true;
|
|
50
|
+
return 'Please enter a valid URL (e.g., https://myapp.com)';
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const analysisResponse = await this.withApiSpinner({
|
|
55
|
+
json: flags.json,
|
|
56
|
+
text: ' Analyzing your product — scraping homepage, pricing, features',
|
|
57
|
+
}, () => this.apiClient.post(API_ENDPOINTS.ANALYZE, {
|
|
58
|
+
url: productUrl,
|
|
59
|
+
}));
|
|
60
|
+
if (!analysisResponse.ok) {
|
|
61
|
+
this.handleAiQuotaError(analysisResponse, 'init');
|
|
62
|
+
this.handleApiError(analysisResponse);
|
|
63
|
+
}
|
|
64
|
+
const analysis = analysisResponse.data;
|
|
65
|
+
const shouldShowAnalysisSummary = !flags.json || !flags.yes;
|
|
66
|
+
if (shouldShowAnalysisSummary) {
|
|
67
|
+
const logSummary = flags.json && !flags.yes
|
|
68
|
+
? this.logToStderr.bind(this)
|
|
69
|
+
: this.log.bind(this);
|
|
70
|
+
logAnalysisSummary(analysis, logSummary);
|
|
71
|
+
}
|
|
72
|
+
let analysisPayload = analysis;
|
|
73
|
+
if (!flags.yes) {
|
|
74
|
+
const userAction = await select({
|
|
75
|
+
choices: [
|
|
76
|
+
{ name: 'Yes - continue with this result', value: 'yes' },
|
|
77
|
+
{ name: 'No - stop here', value: 'no' },
|
|
78
|
+
{ name: 'Edit - update the result before continuing', value: 'edit' },
|
|
79
|
+
],
|
|
80
|
+
message: 'Does this look right?',
|
|
81
|
+
});
|
|
82
|
+
if (userAction === 'no') {
|
|
83
|
+
this.log(`\n Stopped. Run ${chalk.cyan('mailmodo init')} again when you are ready.\n`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (userAction === 'edit') {
|
|
87
|
+
const editedAnalysis = await editor({
|
|
88
|
+
default: JSON.stringify(analysis, null, 2),
|
|
89
|
+
message: 'Edit the analysis JSON. Save and close to continue.',
|
|
90
|
+
postfix: '.json',
|
|
91
|
+
validate(value) {
|
|
92
|
+
if (!value?.trim())
|
|
93
|
+
return 'Edited analysis cannot be empty';
|
|
94
|
+
try {
|
|
95
|
+
JSON.parse(value);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return 'Please provide valid JSON';
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
analysisPayload = JSON.parse(editedAnalysis);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const generateResponse = await this.withApiSpinner({
|
|
107
|
+
json: flags.json,
|
|
108
|
+
text: ' Generating email templates...',
|
|
109
|
+
}, () => this.apiClient.post(API_ENDPOINTS.GENERATE, {
|
|
110
|
+
...analysisPayload,
|
|
111
|
+
}));
|
|
112
|
+
if (!generateResponse.ok) {
|
|
113
|
+
this.handleApiError(generateResponse);
|
|
114
|
+
}
|
|
33
115
|
const generatedEmails = generateResponse.data?.emails || [];
|
|
34
|
-
const emailConfigs =
|
|
35
|
-
|
|
116
|
+
const emailConfigs = analysisPayload.recommendedEmails.map((rec, index) => {
|
|
117
|
+
const generated = generatedEmails[index];
|
|
118
|
+
return {
|
|
119
|
+
delay: rec.delay || '0',
|
|
120
|
+
id: rec.id,
|
|
121
|
+
trigger: normalizeTrigger(rec.trigger, analysisPayload.productName),
|
|
122
|
+
...(rec.condition ? { condition: rec.condition } : {}),
|
|
123
|
+
subject: generated?.subject || `Email for ${rec.id}`,
|
|
124
|
+
template: `mailmodo/${rec.id}.html`,
|
|
125
|
+
...(generated?.previewText
|
|
126
|
+
? { previewText: generated.previewText }
|
|
127
|
+
: {}),
|
|
128
|
+
...(generated?.ctaText ? { ctaText: generated.ctaText } : {}),
|
|
129
|
+
goal: rec.goal,
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
const yamlConfig = {
|
|
133
|
+
emails: emailConfigs,
|
|
134
|
+
project: {
|
|
135
|
+
brandColor: analysisPayload.brand?.color || DEFAULT_BRAND_COLOR,
|
|
136
|
+
description: analysisPayload.description,
|
|
137
|
+
emailStyle: 'plain',
|
|
138
|
+
fromEmail: '',
|
|
139
|
+
fromName: `Team ${analysisPayload.productName}`,
|
|
140
|
+
logoUrl: analysisPayload.brand?.logoUrl || '',
|
|
141
|
+
name: analysisPayload.productName,
|
|
142
|
+
pricingModel: analysisPayload.pricingModel,
|
|
143
|
+
replyTo: '',
|
|
144
|
+
saasModel: analysisPayload.saasModel,
|
|
145
|
+
targetUser: analysisPayload.targetUser,
|
|
146
|
+
type: analysisPayload.businessType,
|
|
147
|
+
url: productUrl,
|
|
148
|
+
webhookUrl: '',
|
|
149
|
+
},
|
|
150
|
+
};
|
|
36
151
|
if (existing)
|
|
37
|
-
preserveUserFields(yamlConfig, existing);
|
|
152
|
+
this.preserveUserFields(yamlConfig, existing);
|
|
38
153
|
await saveYaml(yamlConfig);
|
|
39
|
-
await
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
154
|
+
await this.persistMonthlyCap(yamlConfig);
|
|
155
|
+
const templateSaves = analysisPayload.recommendedEmails.flatMap((rec, index) => {
|
|
156
|
+
const generated = generatedEmails[index];
|
|
157
|
+
const saves = [];
|
|
158
|
+
if (generated?.html) {
|
|
159
|
+
saves.push(saveTemplate(`${rec.id}.html`, generated.html));
|
|
160
|
+
}
|
|
161
|
+
if (generated?.plainHtml) {
|
|
162
|
+
saves.push(saveTemplate(`${rec.id}_plain.html`, generated.plainHtml));
|
|
163
|
+
}
|
|
164
|
+
return saves;
|
|
47
165
|
});
|
|
166
|
+
await Promise.all(templateSaves);
|
|
167
|
+
await this.syncYamlToServer();
|
|
168
|
+
if (flags.json) {
|
|
169
|
+
this.log(JSON.stringify({
|
|
170
|
+
brandDetected: analysisPayload.brand,
|
|
171
|
+
emails: emailConfigs,
|
|
172
|
+
emailsCreated: emailConfigs.length,
|
|
173
|
+
style: yamlConfig.project.emailStyle,
|
|
174
|
+
}, null, 2));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
this.log(` Created ${chalk.green('mailmodo.yaml')} + ${chalk.green(String(emailConfigs.length))} email templates in ${chalk.green('/mailmodo')}\n`);
|
|
178
|
+
this.log(` Run ${chalk.cyan("'mailmodo emails'")} to review.\n`);
|
|
48
179
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
180
|
+
async persistMonthlyCap(yamlConfig) {
|
|
181
|
+
const billingStatus = await this.fetchBillingStatus();
|
|
182
|
+
const monthlyCap = billingStatus?.cap?.inBlocks;
|
|
183
|
+
if (monthlyCap !== null && monthlyCap !== undefined) {
|
|
184
|
+
yamlConfig.project.monthlyCap = monthlyCap;
|
|
185
|
+
await saveYaml(yamlConfig);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
preserveUserFields(config, existing) {
|
|
189
|
+
const p = existing.project;
|
|
190
|
+
if (p.fromEmail)
|
|
191
|
+
config.project.fromEmail = p.fromEmail;
|
|
192
|
+
if (p.replyTo)
|
|
193
|
+
config.project.replyTo = p.replyTo;
|
|
194
|
+
if (p.fromName)
|
|
195
|
+
config.project.fromName = p.fromName;
|
|
196
|
+
if (p.webhookUrl)
|
|
197
|
+
config.project.webhookUrl = p.webhookUrl;
|
|
198
|
+
if (p.emailStyle)
|
|
199
|
+
config.project.emailStyle = p.emailStyle;
|
|
200
|
+
if (p.domain)
|
|
201
|
+
config.project.domain = p.domain;
|
|
202
|
+
if (p.address)
|
|
203
|
+
config.project.address = p.address;
|
|
204
|
+
if (p.logoFile)
|
|
205
|
+
config.project.logoFile = p.logoFile;
|
|
206
|
+
}
|
|
207
|
+
async confirmOverwriteIfNeeded(flags, existing) {
|
|
208
|
+
if (!existing)
|
|
209
|
+
return true;
|
|
210
|
+
if (flags.yes)
|
|
211
|
+
return true;
|
|
212
|
+
this.log(`\n ${chalk.yellow('⚠')} ${chalk.bold('mailmodo.yaml already exists.')}`);
|
|
213
|
+
this.log(` Running init will overwrite your current project configuration and all email templates.\n`);
|
|
214
|
+
const proceed = await confirm({
|
|
215
|
+
default: false,
|
|
216
|
+
message: 'Overwrite existing configuration and templates?',
|
|
217
|
+
});
|
|
218
|
+
if (!proceed) {
|
|
219
|
+
this.log(`\n Init cancelled. Run ${chalk.cyan('mailmodo edit')} to modify individual emails.\n`);
|
|
220
|
+
}
|
|
221
|
+
return proceed;
|
|
64
222
|
}
|
|
65
223
|
}
|
|
@@ -6,7 +6,6 @@ import { ApiClient } from '../../lib/api-client.js';
|
|
|
6
6
|
import { loadConfig, saveConfig } from '../../lib/config.js';
|
|
7
7
|
import { API_ENDPOINTS, LOGIN_URL } from '../../lib/constants.js';
|
|
8
8
|
import { INFO } from '../../lib/messages.js';
|
|
9
|
-
import { logAlreadyLoggedIn, logLoginSuccess, } from '../../lib/commands/login/output.js';
|
|
10
9
|
export default class Login extends BaseCommand {
|
|
11
10
|
static description = 'Authenticate with Mailmodo using your API key';
|
|
12
11
|
static examples = process.platform === 'win32'
|
|
@@ -19,55 +18,100 @@ export default class Login extends BaseCommand {
|
|
|
19
18
|
'<%= config.bin %> login',
|
|
20
19
|
'MAILMODO_API_KEY=YOUR_API_KEY <%= config.bin %> login',
|
|
21
20
|
];
|
|
22
|
-
static flags = {
|
|
21
|
+
static flags = {
|
|
22
|
+
...BaseCommand.baseFlags,
|
|
23
|
+
};
|
|
23
24
|
async run() {
|
|
24
25
|
const { flags } = await this.parse(Login);
|
|
25
|
-
const ctx = this.makeCtx();
|
|
26
26
|
const envKey = process.env.MAILMODO_API_KEY;
|
|
27
27
|
if (!envKey) {
|
|
28
28
|
const existing = await loadConfig();
|
|
29
29
|
if (existing?.apiKey) {
|
|
30
30
|
const existingClient = new ApiClient(existing.apiKey);
|
|
31
31
|
const yamlRestored = await this.recoverYamlAfterLogin(existingClient);
|
|
32
|
-
|
|
32
|
+
if (flags.json) {
|
|
33
|
+
this.log(JSON.stringify({
|
|
34
|
+
email: existing.email ?? null,
|
|
35
|
+
status: 'already_logged_in',
|
|
36
|
+
totalFreeRemaining: existing.totalFreeRemaining ?? null,
|
|
37
|
+
yamlRestored,
|
|
38
|
+
}, null, 2));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
this.log('\n You are already logged in.\n');
|
|
42
|
+
const emailDisplay = existing.email?.trim()
|
|
43
|
+
? chalk.green(existing.email.trim())
|
|
44
|
+
: chalk.dim('(unknown)');
|
|
45
|
+
this.log(` Email: ${emailDisplay}\n`);
|
|
46
|
+
if (yamlRestored) {
|
|
47
|
+
this.log(` ${INFO.YAML_RESTORED_ON_LOGIN}`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
this.log(` ${chalk.dim('1.')} Run ${chalk.cyan('mailmodo init')} to generate an email sequence.`);
|
|
51
|
+
}
|
|
52
|
+
this.log(` ${chalk.dim(yamlRestored ? '1.' : '2.')} Run ${chalk.cyan('mailmodo logout')} to log in with another account.\n`);
|
|
33
53
|
return;
|
|
34
54
|
}
|
|
35
55
|
}
|
|
36
|
-
|
|
56
|
+
let apiKey = envKey;
|
|
57
|
+
if (apiKey) {
|
|
58
|
+
this.log('Detected MAILMODO_API_KEY from environment.');
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
this.log(`\n Get your free API key at: ${chalk.cyan(LOGIN_URL)}\n`);
|
|
62
|
+
try {
|
|
63
|
+
await open(LOGIN_URL);
|
|
64
|
+
this.log(` ${INFO.BROWSER_OPENING}\n`);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
this.log(` ${INFO.BROWSER_OPEN_FAILED}\n`);
|
|
68
|
+
}
|
|
69
|
+
apiKey = await input({
|
|
70
|
+
message: 'Paste your API key:',
|
|
71
|
+
validate(value) {
|
|
72
|
+
if (!value?.trim())
|
|
73
|
+
return 'API key is required';
|
|
74
|
+
return true;
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
37
78
|
const trimmedKey = apiKey.trim();
|
|
38
79
|
const client = new ApiClient(trimmedKey);
|
|
39
|
-
const response = await
|
|
80
|
+
const response = await this.withApiSpinner({ json: flags.json, text: ' Validating API key...' }, () => client.get(API_ENDPOINTS.AUTH_VALIDATE));
|
|
40
81
|
if (!response.ok) {
|
|
41
|
-
|
|
82
|
+
this.handleApiError(response);
|
|
42
83
|
}
|
|
43
84
|
const { email, totalFreeRemaining, paidEmailsRemaining, plan } = response.data;
|
|
44
|
-
await saveConfig({
|
|
85
|
+
await saveConfig({
|
|
86
|
+
apiKey: trimmedKey,
|
|
87
|
+
email,
|
|
88
|
+
totalFreeRemaining,
|
|
89
|
+
});
|
|
45
90
|
const yamlRestored = await this.recoverYamlAfterLogin(client);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this.log(`\n Get your free API key at: ${chalk.cyan(LOGIN_URL)}\n`);
|
|
57
|
-
try {
|
|
58
|
-
await open(LOGIN_URL);
|
|
59
|
-
this.log(` ${INFO.BROWSER_OPENING}\n`);
|
|
91
|
+
if (flags.json) {
|
|
92
|
+
this.log(JSON.stringify({
|
|
93
|
+
email,
|
|
94
|
+
plan,
|
|
95
|
+
totalFreeRemaining,
|
|
96
|
+
paidEmailsRemaining,
|
|
97
|
+
status: 'authenticated',
|
|
98
|
+
yamlRestored,
|
|
99
|
+
}, null, 2));
|
|
100
|
+
return;
|
|
60
101
|
}
|
|
61
|
-
|
|
62
|
-
|
|
102
|
+
this.log(`\n Logged in as ${chalk.green(email)}`);
|
|
103
|
+
if (plan === 'free') {
|
|
104
|
+
this.log(` Free tier: ${chalk.cyan(String(totalFreeRemaining))} emails remaining`);
|
|
105
|
+
this.log(' No credit card required.\n');
|
|
106
|
+
}
|
|
107
|
+
if (plan === 'paid') {
|
|
108
|
+
this.log(` Current paid block: ${chalk.cyan(String(paidEmailsRemaining))} emails remaining\n`);
|
|
109
|
+
}
|
|
110
|
+
if (yamlRestored) {
|
|
111
|
+
this.log(` ${INFO.YAML_RESTORED_ON_LOGIN}\n`);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
this.log(` Next: Run ${chalk.cyan("'mailmodo init'")} to generate your email sequences.\n`);
|
|
63
115
|
}
|
|
64
|
-
return input({
|
|
65
|
-
message: 'Paste your API key:',
|
|
66
|
-
validate(value) {
|
|
67
|
-
if (!value?.trim())
|
|
68
|
-
return 'API key is required';
|
|
69
|
-
return true;
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
116
|
}
|
|
73
117
|
}
|
|
@@ -11,5 +11,12 @@ export default class Logs extends BaseCommand {
|
|
|
11
11
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
12
|
};
|
|
13
13
|
run(): Promise<void>;
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Returns the appropriate chalk color function based on the delivery status.
|
|
16
|
+
* Green for sent/opened/clicked, red for bounced/failed, yellow for skipped.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} status - The delivery event status string.
|
|
19
|
+
* @returns {Function} A chalk color function to wrap the status display text.
|
|
20
|
+
*/
|
|
21
|
+
private statusColor;
|
|
15
22
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
3
4
|
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
4
|
-
import { renderLogs } from '../../lib/commands/logs/output.js';
|
|
5
5
|
export default class Logs extends BaseCommand {
|
|
6
6
|
static description = 'View email send logs and delivery events';
|
|
7
7
|
static examples = [
|
|
@@ -29,7 +29,6 @@ export default class Logs extends BaseCommand {
|
|
|
29
29
|
async run() {
|
|
30
30
|
const { flags } = await this.parse(Logs);
|
|
31
31
|
await this.ensureAuth();
|
|
32
|
-
const ctx = this.makeCtx();
|
|
33
32
|
const params = {
|
|
34
33
|
limit: String(flags.limit),
|
|
35
34
|
page: String(flags.page),
|
|
@@ -38,22 +37,66 @@ export default class Logs extends BaseCommand {
|
|
|
38
37
|
params.email = flags.email;
|
|
39
38
|
if (flags.failed)
|
|
40
39
|
params.failed = 'true';
|
|
41
|
-
const response = await
|
|
40
|
+
const response = await this.withApiSpinner({ json: flags.json, text: ' Loading email logs...' }, () => this.apiClient.get(API_ENDPOINTS.LOGS, params));
|
|
42
41
|
if (!response.ok) {
|
|
43
|
-
|
|
42
|
+
this.handleApiError(response);
|
|
44
43
|
}
|
|
44
|
+
const { entries, limit, page, total } = response.data;
|
|
45
45
|
if (flags.json) {
|
|
46
46
|
this.log(JSON.stringify(response.data, null, 2));
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
this.log(`\n ${'Time'.padEnd(18)}${'Email'.padEnd(24)}${'Status'.padEnd(10)}Contact`);
|
|
50
|
+
this.log(` ${'─'.repeat(68)}`);
|
|
51
|
+
if (entries?.length) {
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const time = (entry.timestamp || '').padEnd(18);
|
|
54
|
+
const templateId = (entry.emailId || '').padEnd(24);
|
|
55
|
+
const statusColor = this.statusColor(entry.status);
|
|
56
|
+
const status = statusColor((entry.status || '').padEnd(10));
|
|
57
|
+
const contact = entry.contact || '';
|
|
58
|
+
this.log(` ${time}${templateId}${status}${contact}`);
|
|
59
|
+
if (entry.reason) {
|
|
60
|
+
const label = entry.status === 'skipped' ? 'condition not met' : 'reason';
|
|
61
|
+
this.log(` ${' '.repeat(52)}${chalk.dim(`(${label}: ${entry.reason})`)}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const totalPages = Math.ceil(total / limit);
|
|
65
|
+
this.log(`\n Page ${page} of ${totalPages} · ${total} total entries`);
|
|
66
|
+
if (page < totalPages) {
|
|
67
|
+
this.log(` ${chalk.dim(`Next: --page ${page + 1}`)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
this.log(` ${chalk.dim('No log entries found.')}`);
|
|
72
|
+
}
|
|
73
|
+
this.log('');
|
|
50
74
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Returns the appropriate chalk color function based on the delivery status.
|
|
77
|
+
* Green for sent/opened/clicked, red for bounced/failed, yellow for skipped.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} status - The delivery event status string.
|
|
80
|
+
* @returns {Function} A chalk color function to wrap the status display text.
|
|
81
|
+
*/
|
|
82
|
+
statusColor(status) {
|
|
83
|
+
switch (status) {
|
|
84
|
+
case 'bounced':
|
|
85
|
+
case 'complained':
|
|
86
|
+
case 'failed': {
|
|
87
|
+
return chalk.red;
|
|
88
|
+
}
|
|
89
|
+
case 'clicked':
|
|
90
|
+
case 'opened':
|
|
91
|
+
case 'sent': {
|
|
92
|
+
return chalk.green;
|
|
93
|
+
}
|
|
94
|
+
case 'skipped': {
|
|
95
|
+
return chalk.yellow;
|
|
96
|
+
}
|
|
97
|
+
default: {
|
|
98
|
+
return chalk.white;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
58
101
|
}
|
|
59
102
|
}
|
|
@@ -12,5 +12,23 @@ export default class Preview extends BaseCommand {
|
|
|
12
12
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
13
|
};
|
|
14
14
|
run(): Promise<void>;
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Renders a plain text version of the email to stdout. Used by AI agents
|
|
17
|
+
* and CI pipelines that cannot open a browser.
|
|
18
|
+
*/
|
|
19
|
+
private renderText;
|
|
20
|
+
/**
|
|
21
|
+
* Calls the API to send a test email to the specified address.
|
|
22
|
+
* Before domain verification, tests send via the mailmodo.com domain.
|
|
23
|
+
*/
|
|
24
|
+
private sendTestEmail;
|
|
25
|
+
/**
|
|
26
|
+
* Probes ports starting at startPort and returns the first one not in use.
|
|
27
|
+
*/
|
|
28
|
+
private findAvailablePort;
|
|
29
|
+
/**
|
|
30
|
+
* Starts a local HTTP server to serve the rendered email template,
|
|
31
|
+
* then opens the user's default browser to view it.
|
|
32
|
+
*/
|
|
33
|
+
private startPreviewServer;
|
|
16
34
|
}
|