@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,26 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
+
import { input, select } from '@inquirer/prompts';
|
|
2
3
|
import chalk from 'chalk';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
3
7
|
import { BaseCommand, FREE_TIER } from '../../lib/base-command.js';
|
|
8
|
+
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
9
|
+
import { INFO } from '../../lib/messages.js';
|
|
10
|
+
import { settingKeyToProp } from '../../lib/utils.js';
|
|
4
11
|
import { saveYaml } from '../../lib/yaml-config.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
12
|
+
const SETTINGS_GROUPS = Object.freeze({
|
|
13
|
+
billing: ['monthly_cap'],
|
|
14
|
+
brand: ['email_style', 'brand_color', 'logo_url', 'logo_file'],
|
|
15
|
+
domain: ['domain', 'address'],
|
|
16
|
+
identity: ['from_name', 'from_email', 'reply_to'],
|
|
17
|
+
integrations: ['webhook_url'],
|
|
18
|
+
});
|
|
19
|
+
const SETUP_HINTS = {
|
|
20
|
+
address: "'mailmodo domain'",
|
|
21
|
+
domain: "'mailmodo domain'",
|
|
22
|
+
monthlyCap: "'mailmodo billing --cap <n>'",
|
|
23
|
+
};
|
|
8
24
|
export default class Settings extends BaseCommand {
|
|
9
25
|
static description = 'View and update project settings';
|
|
10
26
|
static examples = [
|
|
@@ -19,18 +35,14 @@ export default class Settings extends BaseCommand {
|
|
|
19
35
|
async run() {
|
|
20
36
|
const { flags } = await this.parse(Settings);
|
|
21
37
|
const yamlConfig = await this.ensureYaml();
|
|
22
|
-
const ctx = this.makeCtx();
|
|
23
38
|
if (flags.set) {
|
|
24
|
-
await applySetFlag(
|
|
25
|
-
isJson: flags.json ?? false,
|
|
26
|
-
setFlag: flags.set,
|
|
27
|
-
});
|
|
39
|
+
await this.applySetFlag(flags.set, yamlConfig, flags.json ?? false);
|
|
28
40
|
return;
|
|
29
41
|
}
|
|
30
42
|
const [domainVerified, billingStatus] = await Promise.all([
|
|
31
43
|
flags.json
|
|
32
44
|
? Promise.resolve(null)
|
|
33
|
-
: fetchDomainVerified(
|
|
45
|
+
: this.fetchDomainVerified(yamlConfig.project.domain),
|
|
34
46
|
this.fetchBillingStatus(),
|
|
35
47
|
]);
|
|
36
48
|
const tier = billingStatus?.tier ?? null;
|
|
@@ -48,35 +60,235 @@ export default class Settings extends BaseCommand {
|
|
|
48
60
|
for (const [group, keys] of Object.entries(SETTINGS_GROUPS)) {
|
|
49
61
|
if (group === 'billing' && tier === FREE_TIER)
|
|
50
62
|
continue;
|
|
51
|
-
displaySettingsGroup(
|
|
52
|
-
domainVerified,
|
|
53
|
-
keys,
|
|
54
|
-
project: yamlConfig.project,
|
|
55
|
-
});
|
|
63
|
+
this.displaySettingsGroup(group, keys, yamlConfig.project, domainVerified);
|
|
56
64
|
}
|
|
57
65
|
if (!flags.yes) {
|
|
58
|
-
await promptEditSetting(
|
|
66
|
+
await this.promptEditSetting(yamlConfig, tier);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async applySetFlag(setFlag, yamlConfig, isJson) {
|
|
70
|
+
const { project } = yamlConfig;
|
|
71
|
+
const eqIndex = setFlag.indexOf('=');
|
|
72
|
+
if (eqIndex === -1) {
|
|
73
|
+
this.error('Invalid format. Use --set key=value (e.g., --set brand_color=#0F3460)');
|
|
74
|
+
}
|
|
75
|
+
const key = setFlag.slice(0, eqIndex).trim();
|
|
76
|
+
const propKey = settingKeyToProp(key);
|
|
77
|
+
const value = setFlag.slice(eqIndex + 1).trim();
|
|
78
|
+
if (!(propKey in project) && key !== 'logo_file') {
|
|
79
|
+
this.error(`Unknown setting: ${key}`);
|
|
80
|
+
}
|
|
81
|
+
if (propKey === 'monthlyCap') {
|
|
82
|
+
await this.applyMonthlyCapChange(yamlConfig, value, isJson, null);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
project[propKey] = value;
|
|
86
|
+
await saveYaml(yamlConfig);
|
|
87
|
+
await this.syncYamlToServer();
|
|
88
|
+
if (isJson) {
|
|
89
|
+
this.log(JSON.stringify({ [propKey]: value, status: 'updated' }, null, 2));
|
|
90
|
+
return;
|
|
59
91
|
}
|
|
92
|
+
this.log(`\n ${chalk.green('✓')} ${key} updated to ${chalk.cyan(value)}`);
|
|
93
|
+
this.log(` ${INFO.DEPLOY_TO_APPLY}\n`);
|
|
60
94
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
95
|
+
async applyMonthlyCapChange(yamlConfig, rawValue, isJson, knownTier) {
|
|
96
|
+
const parsed = Number(rawValue);
|
|
97
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
98
|
+
this.error('monthly_cap must be a positive integer (blocks).');
|
|
99
|
+
}
|
|
100
|
+
await this.ensureAuth();
|
|
101
|
+
const tier = knownTier ?? (await this.fetchBillingTier());
|
|
102
|
+
if (tier === FREE_TIER) {
|
|
103
|
+
this.warnFreeTierCapBlocked(isJson);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const data = await this.applyBillingCap({ cap: parsed, json: isJson });
|
|
107
|
+
yamlConfig.project.monthlyCap = data.capBlocks;
|
|
108
|
+
await saveYaml(yamlConfig);
|
|
109
|
+
await this.syncYamlToServer();
|
|
110
|
+
if (isJson) {
|
|
111
|
+
this.log(JSON.stringify({ monthlyCap: data.capBlocks, status: 'updated' }, null, 2));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.log(`\n ${chalk.green('✓')} monthly_cap updated to ${chalk.cyan(String(data.capBlocks))} (${data.capEmails.toLocaleString()} emails)\n`);
|
|
115
|
+
}
|
|
116
|
+
displaySettingsGroup(group, keys, project, domainVerified) {
|
|
117
|
+
const availableKeys = keys.filter((key) => {
|
|
118
|
+
if (group === 'brand' && key === 'logo_file')
|
|
119
|
+
return true;
|
|
120
|
+
return settingKeyToProp(key) in project;
|
|
121
|
+
});
|
|
122
|
+
const groupTitle = ` ${chalk.bold(group.charAt(0).toUpperCase() + group.slice(1))}`;
|
|
123
|
+
if (availableKeys.length === 0) {
|
|
124
|
+
const hint = SETUP_HINTS[settingKeyToProp(keys[0])];
|
|
125
|
+
if (hint) {
|
|
126
|
+
this.log(groupTitle);
|
|
127
|
+
this.log(` ${'─'.repeat(49)}`);
|
|
128
|
+
this.log(` ${chalk.dim(`Run ${hint} to configure.`)}`);
|
|
129
|
+
this.log('');
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.log(groupTitle);
|
|
134
|
+
this.log(` ${'─'.repeat(49)}`);
|
|
135
|
+
for (const key of availableKeys) {
|
|
136
|
+
const propKey = settingKeyToProp(key);
|
|
137
|
+
const value = project[propKey];
|
|
138
|
+
let displayValue = value ? String(value) : chalk.dim('(not set)');
|
|
139
|
+
if (key === 'domain' && value && domainVerified === true) {
|
|
140
|
+
displayValue += ` ${chalk.green('✓ verified')}`;
|
|
141
|
+
}
|
|
142
|
+
else if (key === 'domain' && value && domainVerified === false) {
|
|
143
|
+
displayValue += ` ${chalk.red('✗ not verified')}`;
|
|
144
|
+
}
|
|
145
|
+
this.log(` ${key.padEnd(16)} ${displayValue}`);
|
|
146
|
+
}
|
|
147
|
+
const missingKeys = keys.filter((key) => !(settingKeyToProp(key) in project) &&
|
|
148
|
+
!(group === 'brand' && key === 'logo_file'));
|
|
149
|
+
for (const key of missingKeys) {
|
|
150
|
+
const hint = SETUP_HINTS[settingKeyToProp(key)];
|
|
151
|
+
if (hint) {
|
|
152
|
+
this.log(` ${key.padEnd(16)} ${chalk.dim(`(run ${hint} to set up)`)}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
this.log('');
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Prompts the user to pick a setting key to edit and dispatches
|
|
159
|
+
* to the appropriate handler for that key.
|
|
160
|
+
*/
|
|
161
|
+
async promptEditSetting(yamlConfig, tier) {
|
|
162
|
+
const { project } = yamlConfig;
|
|
163
|
+
const editKey = await input({
|
|
164
|
+
default: 'n',
|
|
165
|
+
message: "Edit a setting? (key or 'n'):",
|
|
166
|
+
});
|
|
167
|
+
if (editKey === 'n')
|
|
168
|
+
return;
|
|
169
|
+
if (editKey === 'monthly_cap' && tier === FREE_TIER) {
|
|
170
|
+
this.warnFreeTierCapBlocked(false);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const editPropKey = settingKeyToProp(editKey);
|
|
174
|
+
if (!(editPropKey in project)) {
|
|
175
|
+
if (editKey === 'logo_file') {
|
|
176
|
+
await this.handleLogoUpload(yamlConfig);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const hint = SETUP_HINTS[editPropKey];
|
|
180
|
+
if (hint) {
|
|
181
|
+
this.log(`\n ${editKey} is not configured yet. Run ${chalk.cyan(hint)} to set it up.\n`);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
this.log(`\n Unknown setting: ${editKey}\n`);
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (editKey === 'logo_file') {
|
|
189
|
+
await this.handleLogoUpload(yamlConfig);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (editKey === 'domain') {
|
|
193
|
+
await this.handleDomainChange(yamlConfig);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (editKey === 'monthly_cap') {
|
|
197
|
+
const newValue = await input({
|
|
198
|
+
message: 'New monthly cap (blocks):',
|
|
199
|
+
});
|
|
200
|
+
await this.applyMonthlyCapChange(yamlConfig, newValue, false, tier);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (editKey === 'email_style') {
|
|
204
|
+
const style = await select({
|
|
205
|
+
choices: [
|
|
206
|
+
{ name: 'plain', value: 'plain' },
|
|
207
|
+
{ name: 'branded', value: 'branded' },
|
|
208
|
+
],
|
|
209
|
+
message: 'Email style:',
|
|
210
|
+
});
|
|
211
|
+
project.emailStyle = style;
|
|
212
|
+
await saveYaml(yamlConfig);
|
|
213
|
+
await this.syncYamlToServer();
|
|
214
|
+
this.log(`\n ${chalk.green('✓')} email_style updated to ${chalk.cyan(style)}`);
|
|
215
|
+
this.log(` ${INFO.DEPLOY_TO_APPLY}\n`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const newValue = await input({
|
|
219
|
+
message: `New value for ${editKey}:`,
|
|
220
|
+
});
|
|
221
|
+
project[editPropKey] = newValue;
|
|
222
|
+
await saveYaml(yamlConfig);
|
|
223
|
+
await this.syncYamlToServer();
|
|
224
|
+
this.log(`\n ${chalk.green('✓')} Updated. ${INFO.DEPLOY_TO_APPLY}\n`);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Fetches the domain verification status from the API.
|
|
228
|
+
* Returns true/false for verified/unverified, or null if unavailable.
|
|
229
|
+
*/
|
|
230
|
+
async fetchDomainVerified(domain) {
|
|
231
|
+
if (!domain)
|
|
232
|
+
return null;
|
|
233
|
+
try {
|
|
234
|
+
await this.ensureAuth();
|
|
235
|
+
const response = await this.apiClient.get(API_ENDPOINTS.DOMAIN_STATUS, { domain });
|
|
236
|
+
if (!response.ok) {
|
|
237
|
+
this.log(` ${chalk.dim('Could not fetch domain status. Run')} ${chalk.cyan("'mailmodo domain --status'")} ${chalk.dim('to check manually.')}`);
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
return response.data?.verified === true;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
this.log(` ${chalk.dim('Could not reach API for domain status. Skipping verification check.')}`);
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async handleDomainChange(yamlConfig) {
|
|
248
|
+
await this.ensureAuth();
|
|
249
|
+
const inputs = await this.collectDomainSetupInputs(yamlConfig, false);
|
|
250
|
+
const { dnsRecords, dnsGuideUrl } = await this.registerDomain(yamlConfig, inputs, false);
|
|
251
|
+
this.log(`\n Domain and sender details updated. You will need to re-verify.`);
|
|
252
|
+
this.logDnsRecords(dnsRecords, dnsGuideUrl, false);
|
|
253
|
+
this.log(` Run ${chalk.cyan("'mailmodo domain --verify'")} once records are added.`);
|
|
254
|
+
this.log(` Emails will not send until the new domain is verified.`);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Handles the logo file upload flow: validates the local file exists,
|
|
258
|
+
* reads it, uploads to Mailmodo CDN via API, and updates both logoFile
|
|
259
|
+
* and logoUrl in the project config.
|
|
260
|
+
*
|
|
261
|
+
* @param {import('../../lib/yaml-config.js').MailmodoYaml} yamlConfig - The full YAML config to update and save.
|
|
262
|
+
*/
|
|
263
|
+
async handleLogoUpload(yamlConfig) {
|
|
264
|
+
const logoPath = await input({ message: 'Path to logo file:' });
|
|
265
|
+
const resolvedPath = resolve(logoPath);
|
|
266
|
+
if (!existsSync(resolvedPath)) {
|
|
267
|
+
this.log(`\n File not found: ${resolvedPath}\n`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
await this.ensureAuth();
|
|
271
|
+
const fileBuffer = await readFile(resolvedPath);
|
|
272
|
+
const ext = resolvedPath.split('.').pop()?.toLowerCase();
|
|
273
|
+
const mimeTypes = {
|
|
274
|
+
png: 'image/png',
|
|
275
|
+
jpg: 'image/jpeg',
|
|
276
|
+
jpeg: 'image/jpeg',
|
|
277
|
+
svg: 'image/svg+xml',
|
|
80
278
|
};
|
|
279
|
+
const mimeType = mimeTypes[ext ?? ''] ?? 'application/octet-stream';
|
|
280
|
+
const formData = new FormData();
|
|
281
|
+
formData.append('logo', new Blob([new Uint8Array(fileBuffer)], { type: mimeType }), logoPath.split(/[/\\]/).pop() || 'logo.png');
|
|
282
|
+
const response = await this.withApiSpinner({ json: false, text: ' Uploading logo file...' }, () => this.apiClient.postFormData(API_ENDPOINTS.ASSETS_LOGO, formData));
|
|
283
|
+
if (!response.ok) {
|
|
284
|
+
this.handleApiError(response);
|
|
285
|
+
}
|
|
286
|
+
yamlConfig.project.logoUrl = response.data?.url || '';
|
|
287
|
+
yamlConfig.project.logoFile = logoPath;
|
|
288
|
+
await saveYaml(yamlConfig);
|
|
289
|
+
await this.syncYamlToServer();
|
|
290
|
+
this.log(`\n Logo uploaded and hosted at:`);
|
|
291
|
+
this.log(` ${chalk.cyan(String(response.data?.url))}`);
|
|
292
|
+
this.log(` Run ${chalk.cyan("'mailmodo deploy'")} to apply to all branded emails.\n`);
|
|
81
293
|
}
|
|
82
294
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
1
2
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
2
3
|
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
3
|
-
import { logStatusOutput } from '../../lib/commands/status/output.js';
|
|
4
4
|
export default class Status extends BaseCommand {
|
|
5
5
|
static description = 'View email performance metrics and quota usage';
|
|
6
6
|
static examples = [
|
|
@@ -13,23 +13,49 @@ export default class Status extends BaseCommand {
|
|
|
13
13
|
async run() {
|
|
14
14
|
const { flags } = await this.parse(Status);
|
|
15
15
|
await this.ensureAuth();
|
|
16
|
-
const
|
|
17
|
-
const response = await ctx.spinner(' Loading analytics...', flags.json, () => ctx.get(API_ENDPOINTS.ANALYTICS));
|
|
16
|
+
const response = await this.withApiSpinner({ json: flags.json, text: ' Loading analytics...' }, () => this.apiClient.get(API_ENDPOINTS.ANALYTICS));
|
|
18
17
|
if (!response.ok) {
|
|
19
|
-
|
|
18
|
+
this.handleApiError(response);
|
|
20
19
|
}
|
|
20
|
+
const { emails, monthlySent, quota } = response.data;
|
|
21
21
|
if (flags.json) {
|
|
22
22
|
this.log(JSON.stringify(response.data, null, 2));
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
this.log(`\n ${chalk.bold('Last 7 days')}${''.padEnd(20)}Sent Open Click Conv`);
|
|
26
|
+
this.log(` ${'─'.repeat(62)}`);
|
|
27
|
+
if (emails?.length) {
|
|
28
|
+
for (const metric of emails) {
|
|
29
|
+
const id = (metric.emailId || '').padEnd(30);
|
|
30
|
+
const sent = String(metric.sent ?? 0).padEnd(7);
|
|
31
|
+
const openRate = (metric.open || '0%').padEnd(7);
|
|
32
|
+
const clickRate = (metric.click || '0%').padEnd(8);
|
|
33
|
+
const convRate = metric.conv || '0%';
|
|
34
|
+
this.log(` ${id}${sent}${openRate}${clickRate}${convRate}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
this.log(` ${chalk.dim('No data yet. Deploy emails first.')}`);
|
|
39
|
+
}
|
|
40
|
+
this.log('');
|
|
41
|
+
if (monthlySent !== null && monthlySent !== undefined) {
|
|
42
|
+
this.log(` Emails sent this month: ${chalk.bold(String(monthlySent))}`);
|
|
43
|
+
}
|
|
44
|
+
if (quota) {
|
|
45
|
+
if (quota.plan === 'free') {
|
|
46
|
+
this.log(` Free tier remaining: ${chalk.cyan(String(quota.freeRemaining))} emails`);
|
|
47
|
+
}
|
|
48
|
+
else if (quota.plan === 'paid') {
|
|
49
|
+
if (quota.currentBlockEmailsRemaining !== null &&
|
|
50
|
+
quota.currentBlockEmailsRemaining !== undefined) {
|
|
51
|
+
const { blockSize } = quota;
|
|
52
|
+
const sent = blockSize - quota.currentBlockEmailsRemaining;
|
|
53
|
+
const remaining = quota.currentBlockEmailsRemaining;
|
|
54
|
+
this.log(` Current paid block (${blockSize} emails) : ${chalk.cyan(`${sent} emails sent, ${remaining} emails remaining`)}`);
|
|
55
|
+
}
|
|
56
|
+
this.log(` Blocks used: ${quota.blocksUsed}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
this.log('');
|
|
34
60
|
}
|
|
35
61
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MailmodoYaml } from '
|
|
1
|
+
import type { MailmodoYaml } from '../yaml-config.js';
|
|
2
2
|
import type { DeployCtx, DeployFlags, ValidateResponse } from './types.js';
|
|
3
3
|
export declare function validateDeploySequence(ctx: DeployCtx, payload: object, flags: {
|
|
4
4
|
json: boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { confirm, input } from '@inquirer/prompts';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { API_ENDPOINTS } from '
|
|
4
|
-
import { ERRORS, INFO, PROMPTS } from '
|
|
3
|
+
import { API_ENDPOINTS } from '../constants.js';
|
|
4
|
+
import { ERRORS, INFO, PROMPTS } from '../messages.js';
|
|
5
5
|
export async function validateDeploySequence(ctx, payload, flags) {
|
|
6
6
|
const res = await ctx.spinner(' Validating sequence...', flags.json, () => ctx.post(API_ENDPOINTS.SEQUENCES_VALIDATE, payload));
|
|
7
7
|
if (!res.ok) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MailmodoYaml } from '
|
|
1
|
+
import type { MailmodoYaml } from '../yaml-config.js';
|
|
2
2
|
import type { DeployCtx, SdkSnippet, ValidateResponse } from './types.js';
|
|
3
3
|
export declare function logDiff(ctx: DeployCtx, diff: NonNullable<ValidateResponse['diff']>): void;
|
|
4
4
|
export declare function logPreDeploySummary(ctx: DeployCtx, yamlConfig: MailmodoYaml, validateResult: ValidateResponse, jsonOutput: boolean): void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { SDK_IMPORT_SNIPPET, SDK_INSTALL_COMMAND } from '
|
|
3
|
-
import { DEPLOY, SEPARATOR } from '
|
|
2
|
+
import { SDK_IMPORT_SNIPPET, SDK_INSTALL_COMMAND } from '../constants.js';
|
|
3
|
+
import { DEPLOY, SEPARATOR } from '../messages.js';
|
|
4
4
|
export function logDiff(ctx, diff) {
|
|
5
5
|
if (!diff.hasChanges) {
|
|
6
6
|
ctx.log(` ${DEPLOY.NO_CHANGES}`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type EmailConfig, type MailmodoYaml, type ProjectConfig } from '
|
|
1
|
+
import { type EmailConfig, type MailmodoYaml, type ProjectConfig } from '../yaml-config.js';
|
|
2
2
|
import type { DeployCtx } from './types.js';
|
|
3
3
|
export declare function mapEmailToPayload(email: EmailConfig): {
|
|
4
4
|
condition: string | null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { loadTemplate, } from '
|
|
2
|
-
import { DEFAULT_BRAND_COLOR } from '
|
|
1
|
+
import { loadTemplate, } from '../yaml-config.js';
|
|
2
|
+
import { DEFAULT_BRAND_COLOR } from '../constants.js';
|
|
3
3
|
export function mapEmailToPayload(email) {
|
|
4
4
|
return {
|
|
5
5
|
condition: email.condition || null,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { confirm } from '@inquirer/prompts';
|
|
2
|
-
import { API_ENDPOINTS } from '
|
|
3
|
-
import { INFO, pauseAlready, pauseSuccess, PROMPTS, resumeAlready, resumeSuccess, } from '
|
|
2
|
+
import { API_ENDPOINTS } from '../constants.js';
|
|
3
|
+
import { INFO, pauseAlready, pauseSuccess, PROMPTS, resumeAlready, resumeSuccess, } from '../messages.js';
|
|
4
4
|
function sequenceStatusPath(sequenceId) {
|
|
5
5
|
return `${API_ENDPOINTS.SEQUENCES}/${encodeURIComponent(sequenceId)}/status`;
|
|
6
6
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ApiResponse } from '
|
|
2
|
-
import type { MailmodoYaml } from '
|
|
3
|
-
import type { RegenCtx } from '
|
|
1
|
+
import type { ApiResponse } from '../api-client.js';
|
|
2
|
+
import type { MailmodoYaml } from '../yaml-config.js';
|
|
3
|
+
import type { RegenCtx } from '../templates/types.js';
|
|
4
4
|
export interface EmailDiffEntry {
|
|
5
5
|
changedFields?: string[];
|
|
6
6
|
id: string;
|
|
@@ -85,4 +85,4 @@ export type DeployCtx = RegenCtx & {
|
|
|
85
85
|
spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
|
|
86
86
|
syncYaml(): Promise<void>;
|
|
87
87
|
};
|
|
88
|
-
export { type RegenCtx } from '
|
|
88
|
+
export { type RegenCtx } from '../templates/types.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type MailmodoYaml } from '../yaml-config.js';
|
|
2
|
-
import type { DeployFlags } from '../
|
|
2
|
+
import type { DeployFlags } from '../deploy/types.js';
|
|
3
3
|
import type { RegenCtx } from './types.js';
|
|
4
4
|
export declare function getMissingTemplateIds(yamlConfig: MailmodoYaml): string[];
|
|
5
5
|
export declare function handleMissingTemplates(ctx: RegenCtx, yamlConfig: MailmodoYaml, missingIds: string[], flags: DeployFlags): Promise<boolean>;
|
|
@@ -5,7 +5,7 @@ import chalk from 'chalk';
|
|
|
5
5
|
import { API_ENDPOINTS, TEMPLATES_DIR } from '../constants.js';
|
|
6
6
|
import { MISSING_TEMPLATES } from '../messages.js';
|
|
7
7
|
import { getTemplateFilename, saveTemplate, } from '../yaml-config.js';
|
|
8
|
-
import { buildRegeneratePayload } from '../
|
|
8
|
+
import { buildRegeneratePayload } from '../deploy/payload.js';
|
|
9
9
|
export function getMissingTemplateIds(yamlConfig) {
|
|
10
10
|
return yamlConfig.emails
|
|
11
11
|
.filter((e) => !existsSync(join(process.cwd(), TEMPLATES_DIR, getTemplateFilename(e.id, e.style, yamlConfig.project?.emailStyle))))
|