@mailmodo/cli 0.0.54 → 0.0.55-beta.pr57.92
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 -181
- package/dist/commands/contacts/index.d.ts +1 -19
- package/dist/commands/contacts/index.js +21 -114
- package/dist/commands/deploy/index.d.ts +1 -32
- package/dist/commands/deploy/index.js +52 -303
- 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 +35 -244
- 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 -2
- package/dist/commands/init/index.js +43 -179
- package/dist/commands/login/index.d.ts +2 -0
- package/dist/commands/login/index.js +35 -64
- 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 +40 -210
- 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 +35 -241
- package/dist/commands/status/index.d.ts +1 -0
- package/dist/commands/status/index.js +13 -39
- package/dist/lib/api-client.d.ts +5 -0
- package/dist/lib/api-client.js +45 -0
- package/dist/lib/base-command.d.ts +25 -1
- package/dist/lib/base-command.js +91 -5
- 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/billing/types.js +1 -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/commands/deploy/domain-setup.d.ts +8 -0
- package/dist/lib/commands/deploy/domain-setup.js +82 -0
- package/dist/lib/commands/deploy/output.d.ts +5 -0
- package/dist/lib/commands/deploy/output.js +61 -0
- package/dist/lib/commands/deploy/payload.d.ts +41 -0
- package/dist/lib/commands/deploy/payload.js +95 -0
- package/dist/lib/commands/deploy/sequence-status.d.ts +3 -0
- package/dist/lib/commands/deploy/sequence-status.js +56 -0
- package/dist/lib/commands/deploy/types.d.ts +88 -0
- 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 +65 -0
- package/dist/lib/commands/edit/types.d.ts +37 -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 +69 -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 +53 -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 +19 -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 +1 -0
- package/dist/lib/constants.js +1 -0
- package/dist/lib/messages.d.ts +22 -0
- package/dist/lib/messages.js +22 -0
- package/dist/lib/templates/missing-templates.d.ts +5 -0
- package/dist/lib/templates/missing-templates.js +61 -0
- package/dist/lib/templates/types.d.ts +13 -0
- package/dist/lib/templates/types.js +1 -0
- package/oclif.manifest.json +66 -66
- package/package.json +1 -1
|
@@ -1,26 +1,10 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
-
import { input, select } from '@inquirer/prompts';
|
|
3
2
|
import chalk from 'chalk';
|
|
4
|
-
import { existsSync } from 'node:fs';
|
|
5
|
-
import { readFile } from 'node:fs/promises';
|
|
6
|
-
import { resolve } from 'node:path';
|
|
7
3
|
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';
|
|
11
4
|
import { saveYaml } from '../../lib/yaml-config.js';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
};
|
|
5
|
+
import { applySetFlag } from '../../lib/commands/settings/actions.js';
|
|
6
|
+
import { displaySettingsGroup, fetchDomainVerified, SETTINGS_GROUPS, } from '../../lib/commands/settings/display.js';
|
|
7
|
+
import { promptEditSetting } from '../../lib/commands/settings/prompt.js';
|
|
24
8
|
export default class Settings extends BaseCommand {
|
|
25
9
|
static description = 'View and update project settings';
|
|
26
10
|
static examples = [
|
|
@@ -35,14 +19,18 @@ export default class Settings extends BaseCommand {
|
|
|
35
19
|
async run() {
|
|
36
20
|
const { flags } = await this.parse(Settings);
|
|
37
21
|
const yamlConfig = await this.ensureYaml();
|
|
22
|
+
const ctx = this.makeCtx();
|
|
38
23
|
if (flags.set) {
|
|
39
|
-
await
|
|
24
|
+
await applySetFlag(ctx, yamlConfig, {
|
|
25
|
+
isJson: flags.json ?? false,
|
|
26
|
+
setFlag: flags.set,
|
|
27
|
+
});
|
|
40
28
|
return;
|
|
41
29
|
}
|
|
42
30
|
const [domainVerified, billingStatus] = await Promise.all([
|
|
43
31
|
flags.json
|
|
44
32
|
? Promise.resolve(null)
|
|
45
|
-
:
|
|
33
|
+
: fetchDomainVerified(ctx, yamlConfig.project.domain),
|
|
46
34
|
this.fetchBillingStatus(),
|
|
47
35
|
]);
|
|
48
36
|
const tier = billingStatus?.tier ?? null;
|
|
@@ -50,6 +38,7 @@ export default class Settings extends BaseCommand {
|
|
|
50
38
|
if (capFromApi !== null) {
|
|
51
39
|
yamlConfig.project.monthlyCap = capFromApi;
|
|
52
40
|
await saveYaml(yamlConfig);
|
|
41
|
+
await this.syncYamlToServer();
|
|
53
42
|
}
|
|
54
43
|
if (flags.json) {
|
|
55
44
|
this.log(JSON.stringify({ settings: yamlConfig.project }, null, 2));
|
|
@@ -59,230 +48,35 @@ export default class Settings extends BaseCommand {
|
|
|
59
48
|
for (const [group, keys] of Object.entries(SETTINGS_GROUPS)) {
|
|
60
49
|
if (group === 'billing' && tier === FREE_TIER)
|
|
61
50
|
continue;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
async applySetFlag(setFlag, yamlConfig, isJson) {
|
|
69
|
-
const { project } = yamlConfig;
|
|
70
|
-
const eqIndex = setFlag.indexOf('=');
|
|
71
|
-
if (eqIndex === -1) {
|
|
72
|
-
this.error('Invalid format. Use --set key=value (e.g., --set brand_color=#0F3460)');
|
|
73
|
-
}
|
|
74
|
-
const key = setFlag.slice(0, eqIndex).trim();
|
|
75
|
-
const propKey = settingKeyToProp(key);
|
|
76
|
-
const value = setFlag.slice(eqIndex + 1).trim();
|
|
77
|
-
if (!(propKey in project) && key !== 'logo_file') {
|
|
78
|
-
this.error(`Unknown setting: ${key}`);
|
|
79
|
-
}
|
|
80
|
-
if (propKey === 'monthlyCap') {
|
|
81
|
-
await this.applyMonthlyCapChange(yamlConfig, value, isJson, null);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
project[propKey] = value;
|
|
85
|
-
await saveYaml(yamlConfig);
|
|
86
|
-
if (isJson) {
|
|
87
|
-
this.log(JSON.stringify({ [propKey]: value, status: 'updated' }, null, 2));
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
this.log(`\n ${chalk.green('✓')} ${key} updated to ${chalk.cyan(value)}`);
|
|
91
|
-
this.log(` ${INFO.DEPLOY_TO_APPLY}\n`);
|
|
92
|
-
}
|
|
93
|
-
async applyMonthlyCapChange(yamlConfig, rawValue, isJson, knownTier) {
|
|
94
|
-
const parsed = Number(rawValue);
|
|
95
|
-
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
96
|
-
this.error('monthly_cap must be a positive integer (blocks).');
|
|
97
|
-
}
|
|
98
|
-
await this.ensureAuth();
|
|
99
|
-
const tier = knownTier ?? (await this.fetchBillingTier());
|
|
100
|
-
if (tier === FREE_TIER) {
|
|
101
|
-
this.warnFreeTierCapBlocked(isJson);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
const data = await this.applyBillingCap({ cap: parsed, json: isJson });
|
|
105
|
-
yamlConfig.project.monthlyCap = data.capBlocks;
|
|
106
|
-
await saveYaml(yamlConfig);
|
|
107
|
-
if (isJson) {
|
|
108
|
-
this.log(JSON.stringify({ monthlyCap: data.capBlocks, status: 'updated' }, null, 2));
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
this.log(`\n ${chalk.green('✓')} monthly_cap updated to ${chalk.cyan(String(data.capBlocks))} (${data.capEmails.toLocaleString()} emails)\n`);
|
|
112
|
-
}
|
|
113
|
-
displaySettingsGroup(group, keys, project, domainVerified) {
|
|
114
|
-
const availableKeys = keys.filter((key) => {
|
|
115
|
-
if (group === 'brand' && key === 'logo_file')
|
|
116
|
-
return true;
|
|
117
|
-
return settingKeyToProp(key) in project;
|
|
118
|
-
});
|
|
119
|
-
const groupTitle = ` ${chalk.bold(group.charAt(0).toUpperCase() + group.slice(1))}`;
|
|
120
|
-
if (availableKeys.length === 0) {
|
|
121
|
-
const hint = SETUP_HINTS[settingKeyToProp(keys[0])];
|
|
122
|
-
if (hint) {
|
|
123
|
-
this.log(groupTitle);
|
|
124
|
-
this.log(` ${'─'.repeat(49)}`);
|
|
125
|
-
this.log(` ${chalk.dim(`Run ${hint} to configure.`)}`);
|
|
126
|
-
this.log('');
|
|
127
|
-
}
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
this.log(groupTitle);
|
|
131
|
-
this.log(` ${'─'.repeat(49)}`);
|
|
132
|
-
for (const key of availableKeys) {
|
|
133
|
-
const propKey = settingKeyToProp(key);
|
|
134
|
-
const value = project[propKey];
|
|
135
|
-
let displayValue = value ? String(value) : chalk.dim('(not set)');
|
|
136
|
-
if (key === 'domain' && value && domainVerified === true) {
|
|
137
|
-
displayValue += ` ${chalk.green('✓ verified')}`;
|
|
138
|
-
}
|
|
139
|
-
else if (key === 'domain' && value && domainVerified === false) {
|
|
140
|
-
displayValue += ` ${chalk.red('✗ not verified')}`;
|
|
141
|
-
}
|
|
142
|
-
this.log(` ${key.padEnd(16)} ${displayValue}`);
|
|
143
|
-
}
|
|
144
|
-
const missingKeys = keys.filter((key) => !(settingKeyToProp(key) in project) &&
|
|
145
|
-
!(group === 'brand' && key === 'logo_file'));
|
|
146
|
-
for (const key of missingKeys) {
|
|
147
|
-
const hint = SETUP_HINTS[settingKeyToProp(key)];
|
|
148
|
-
if (hint) {
|
|
149
|
-
this.log(` ${key.padEnd(16)} ${chalk.dim(`(run ${hint} to set up)`)}`);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
this.log('');
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Prompts the user to pick a setting key to edit and dispatches
|
|
156
|
-
* to the appropriate handler for that key.
|
|
157
|
-
*/
|
|
158
|
-
async promptEditSetting(yamlConfig, tier) {
|
|
159
|
-
const { project } = yamlConfig;
|
|
160
|
-
const editKey = await input({
|
|
161
|
-
default: 'n',
|
|
162
|
-
message: "Edit a setting? (key or 'n'):",
|
|
163
|
-
});
|
|
164
|
-
if (editKey === 'n')
|
|
165
|
-
return;
|
|
166
|
-
if (editKey === 'monthly_cap' && tier === FREE_TIER) {
|
|
167
|
-
this.warnFreeTierCapBlocked(false);
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
const editPropKey = settingKeyToProp(editKey);
|
|
171
|
-
if (!(editPropKey in project)) {
|
|
172
|
-
if (editKey === 'logo_file') {
|
|
173
|
-
await this.handleLogoUpload(yamlConfig);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
const hint = SETUP_HINTS[editPropKey];
|
|
177
|
-
if (hint) {
|
|
178
|
-
this.log(`\n ${editKey} is not configured yet. Run ${chalk.cyan(hint)} to set it up.\n`);
|
|
179
|
-
}
|
|
180
|
-
else {
|
|
181
|
-
this.log(`\n Unknown setting: ${editKey}\n`);
|
|
182
|
-
}
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
if (editKey === 'logo_file') {
|
|
186
|
-
await this.handleLogoUpload(yamlConfig);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
if (editKey === 'domain') {
|
|
190
|
-
await this.handleDomainChange(yamlConfig);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
if (editKey === 'monthly_cap') {
|
|
194
|
-
const newValue = await input({
|
|
195
|
-
message: 'New monthly cap (blocks):',
|
|
196
|
-
});
|
|
197
|
-
await this.applyMonthlyCapChange(yamlConfig, newValue, false, tier);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
if (editKey === 'email_style') {
|
|
201
|
-
const style = await select({
|
|
202
|
-
choices: [
|
|
203
|
-
{ name: 'plain', value: 'plain' },
|
|
204
|
-
{ name: 'branded', value: 'branded' },
|
|
205
|
-
],
|
|
206
|
-
message: 'Email style:',
|
|
51
|
+
displaySettingsGroup(ctx, group, {
|
|
52
|
+
domainVerified,
|
|
53
|
+
keys,
|
|
54
|
+
project: yamlConfig.project,
|
|
207
55
|
});
|
|
208
|
-
project.emailStyle = style;
|
|
209
|
-
await saveYaml(yamlConfig);
|
|
210
|
-
this.log(`\n ${chalk.green('✓')} email_style updated to ${chalk.cyan(style)}`);
|
|
211
|
-
this.log(` ${INFO.DEPLOY_TO_APPLY}\n`);
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
const newValue = await input({
|
|
215
|
-
message: `New value for ${editKey}:`,
|
|
216
|
-
});
|
|
217
|
-
project[editPropKey] = newValue;
|
|
218
|
-
await saveYaml(yamlConfig);
|
|
219
|
-
this.log(`\n ${chalk.green('✓')} Updated. ${INFO.DEPLOY_TO_APPLY}\n`);
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Fetches the domain verification status from the API.
|
|
223
|
-
* Returns true/false for verified/unverified, or null if unavailable.
|
|
224
|
-
*/
|
|
225
|
-
async fetchDomainVerified(domain) {
|
|
226
|
-
if (!domain)
|
|
227
|
-
return null;
|
|
228
|
-
try {
|
|
229
|
-
await this.ensureAuth();
|
|
230
|
-
const response = await this.apiClient.get(API_ENDPOINTS.DOMAIN_STATUS, { domain });
|
|
231
|
-
if (!response.ok) {
|
|
232
|
-
this.log(` ${chalk.dim('Could not fetch domain status. Run')} ${chalk.cyan("'mailmodo domain --status'")} ${chalk.dim('to check manually.')}`);
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
return response.data?.verified === true;
|
|
236
56
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return null;
|
|
57
|
+
if (!flags.yes) {
|
|
58
|
+
await promptEditSetting(ctx, yamlConfig, tier);
|
|
240
59
|
}
|
|
241
60
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (!existsSync(resolvedPath)) {
|
|
262
|
-
this.log(`\n File not found: ${resolvedPath}\n`);
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
await this.ensureAuth();
|
|
266
|
-
const fileBuffer = await readFile(resolvedPath);
|
|
267
|
-
const ext = resolvedPath.split('.').pop()?.toLowerCase();
|
|
268
|
-
const mimeTypes = {
|
|
269
|
-
png: 'image/png',
|
|
270
|
-
jpg: 'image/jpeg',
|
|
271
|
-
jpeg: 'image/jpeg',
|
|
272
|
-
svg: 'image/svg+xml',
|
|
61
|
+
makeCtx() {
|
|
62
|
+
return {
|
|
63
|
+
applyBillingCap: (opts) => this.applyBillingCap(opts),
|
|
64
|
+
collectDomainInputs: (yaml, skip) => this.collectDomainSetupInputs(yaml, skip),
|
|
65
|
+
ensureAuth: async () => {
|
|
66
|
+
await this.ensureAuth();
|
|
67
|
+
},
|
|
68
|
+
error: (msg) => this.error(msg),
|
|
69
|
+
fetchBillingStatus: () => this.fetchBillingStatus(),
|
|
70
|
+
fetchBillingTier: () => this.fetchBillingTier(),
|
|
71
|
+
get: (path, params) => this.apiClient.get(path, params),
|
|
72
|
+
log: (msg) => this.log(msg),
|
|
73
|
+
onApiError: (r) => this.handleApiError(r),
|
|
74
|
+
postFormData: (path, formData) => this.apiClient.postFormData(path, formData),
|
|
75
|
+
registerDomainAndSave: (yaml, inputs, json) => this.registerDomain(yaml, inputs, json),
|
|
76
|
+
showDnsRecords: (records, url, json) => this.logDnsRecords(records, url, json),
|
|
77
|
+
spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
|
|
78
|
+
syncYaml: () => this.syncYamlToServer(),
|
|
79
|
+
warnFreeTierCapBlocked: (json) => this.warnFreeTierCapBlocked(json),
|
|
273
80
|
};
|
|
274
|
-
const mimeType = mimeTypes[ext ?? ''] ?? 'application/octet-stream';
|
|
275
|
-
const formData = new FormData();
|
|
276
|
-
formData.append('logo', new Blob([new Uint8Array(fileBuffer)], { type: mimeType }), logoPath.split(/[/\\]/).pop() || 'logo.png');
|
|
277
|
-
const response = await this.withApiSpinner({ json: false, text: ' Uploading logo file...' }, () => this.apiClient.postFormData(API_ENDPOINTS.ASSETS_LOGO, formData));
|
|
278
|
-
if (!response.ok) {
|
|
279
|
-
this.handleApiError(response);
|
|
280
|
-
}
|
|
281
|
-
yamlConfig.project.logoUrl = response.data?.url || '';
|
|
282
|
-
yamlConfig.project.logoFile = logoPath;
|
|
283
|
-
await saveYaml(yamlConfig);
|
|
284
|
-
this.log(`\n Logo uploaded and hosted at:`);
|
|
285
|
-
this.log(` ${chalk.cyan(String(response.data?.url))}`);
|
|
286
|
-
this.log(` Run ${chalk.cyan("'mailmodo deploy'")} to apply to all branded emails.\n`);
|
|
287
81
|
}
|
|
288
82
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
1
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
3
2
|
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,49 +13,23 @@ 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
|
|
16
|
+
const ctx = this.makeCtx();
|
|
17
|
+
const response = await ctx.spinner(' Loading analytics...', flags.json, () => ctx.get(API_ENDPOINTS.ANALYTICS));
|
|
17
18
|
if (!response.ok) {
|
|
18
|
-
|
|
19
|
+
ctx.onApiError(response);
|
|
19
20
|
}
|
|
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
|
-
|
|
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('');
|
|
25
|
+
logStatusOutput(ctx, response.data);
|
|
26
|
+
}
|
|
27
|
+
makeCtx() {
|
|
28
|
+
return {
|
|
29
|
+
get: (path, params) => this.apiClient.get(path, params),
|
|
30
|
+
log: (msg) => this.log(msg),
|
|
31
|
+
onApiError: (r) => this.handleApiError(r),
|
|
32
|
+
spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
|
|
33
|
+
};
|
|
60
34
|
}
|
|
61
35
|
}
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -46,6 +46,11 @@ export declare class ApiClient {
|
|
|
46
46
|
* Bearer auth as other requests; does not parse JSON.
|
|
47
47
|
*/
|
|
48
48
|
getFile(url: string): Promise<FileFetchResult>;
|
|
49
|
+
/**
|
|
50
|
+
* GET an endpoint and return the raw response body as a string (e.g. YAML files).
|
|
51
|
+
* Uses Bearer auth; does not parse JSON.
|
|
52
|
+
*/
|
|
53
|
+
getRawText(path: string): Promise<ApiResponse<string>>;
|
|
49
54
|
/**
|
|
50
55
|
* GET an external URL (e.g. blob storage) without auth headers.
|
|
51
56
|
*/
|
package/dist/lib/api-client.js
CHANGED
|
@@ -135,6 +135,51 @@ export class ApiClient {
|
|
|
135
135
|
async getFile(url) {
|
|
136
136
|
return fetchFileWithBearerAuth(url, this.apiKey);
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* GET an endpoint and return the raw response body as a string (e.g. YAML files).
|
|
140
|
+
* Uses Bearer auth; does not parse JSON.
|
|
141
|
+
*/
|
|
142
|
+
async getRawText(path) {
|
|
143
|
+
const url = this.resolveUrl(path);
|
|
144
|
+
const debug = this.requestDebug(url);
|
|
145
|
+
try {
|
|
146
|
+
const response = await fetch(url.toString(), {
|
|
147
|
+
headers: {
|
|
148
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
149
|
+
'User-Agent': '@mailmodo/cli',
|
|
150
|
+
},
|
|
151
|
+
method: 'GET',
|
|
152
|
+
});
|
|
153
|
+
const text = await response.text().catch(() => '');
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
return {
|
|
156
|
+
data: '',
|
|
157
|
+
debug: {
|
|
158
|
+
...debug,
|
|
159
|
+
...(text ? { responseSummary: text.slice(0, 200) } : {}),
|
|
160
|
+
},
|
|
161
|
+
error: text || `Request failed with status ${response.status}`,
|
|
162
|
+
ok: false,
|
|
163
|
+
status: response.status,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return { data: text, ok: true, status: response.status };
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const err = error;
|
|
170
|
+
const isConnectionError = err?.cause?.code === 'ECONNREFUSED' || err?.cause?.code === 'ENOTFOUND';
|
|
171
|
+
const causeCode = err?.cause?.code;
|
|
172
|
+
return {
|
|
173
|
+
data: '',
|
|
174
|
+
debug: { ...debug, ...(causeCode ? { causeCode } : {}) },
|
|
175
|
+
error: isConnectionError
|
|
176
|
+
? 'Cannot connect to Mailmodo API. The API service may not be available yet.'
|
|
177
|
+
: err?.message || 'An unexpected network error occurred.',
|
|
178
|
+
ok: false,
|
|
179
|
+
status: 0,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
138
183
|
/**
|
|
139
184
|
* GET an external URL (e.g. blob storage) without auth headers.
|
|
140
185
|
*/
|
|
@@ -60,12 +60,36 @@ export declare abstract class BaseCommand extends Command {
|
|
|
60
60
|
}, work: () => Promise<T>): Promise<T>;
|
|
61
61
|
/**
|
|
62
62
|
* Loads and returns the mailmodo.yaml configuration from the current directory.
|
|
63
|
-
*
|
|
63
|
+
* If the file is not found locally and the API client is available, attempts to
|
|
64
|
+
* restore it from the server. Exits with an error if the file cannot be found
|
|
65
|
+
* or restored, directing the user to run init.
|
|
64
66
|
*
|
|
65
67
|
* @returns {Promise<MailmodoYaml>} The parsed mailmodo.yaml containing project
|
|
66
68
|
* settings and all email sequence definitions.
|
|
67
69
|
*/
|
|
68
70
|
protected ensureYaml(): Promise<MailmodoYaml>;
|
|
71
|
+
private fetchAndWriteYaml;
|
|
72
|
+
/**
|
|
73
|
+
* Attempts to fetch mailmodo.yaml from the server and save it locally.
|
|
74
|
+
* Returns null silently on any failure so callers can fall through to an error.
|
|
75
|
+
*/
|
|
76
|
+
private restoreYamlFromServer;
|
|
77
|
+
/**
|
|
78
|
+
* If `mailmodo.yaml` is absent from the current directory, attempts to restore
|
|
79
|
+
* it from the server using the given client. Returns `true` if the file was
|
|
80
|
+
* successfully written, `false` otherwise (file already present, server 404,
|
|
81
|
+
* or any network error). Silent — never throws.
|
|
82
|
+
*
|
|
83
|
+
* Used by `mailmodo login` right after the API key is validated so a returning
|
|
84
|
+
* user automatically gets their config back without having to run `init` again.
|
|
85
|
+
*/
|
|
86
|
+
protected recoverYamlAfterLogin(client: ApiClient): Promise<boolean>;
|
|
87
|
+
/**
|
|
88
|
+
* Uploads the current local mailmodo.yaml to the server as a backup.
|
|
89
|
+
* Best-effort: silently ignores all errors so the originating command
|
|
90
|
+
* always succeeds regardless of sync failures.
|
|
91
|
+
*/
|
|
92
|
+
protected syncYamlToServer(): Promise<void>;
|
|
69
93
|
/**
|
|
70
94
|
* Handles a failed API response by mapping HTTP status codes to
|
|
71
95
|
* user-friendly error messages and exiting the process.
|
package/dist/lib/base-command.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
1
4
|
import { input } from '@inquirer/prompts';
|
|
2
5
|
import { Command, Flags } from '@oclif/core';
|
|
3
6
|
import chalk from 'chalk';
|
|
4
7
|
import ora from 'ora';
|
|
5
8
|
import { ApiClient } from './api-client.js';
|
|
6
9
|
import { loadConfig } from './config.js';
|
|
7
|
-
import { API_ENDPOINTS, IS_DEV_MODE } from './constants.js';
|
|
10
|
+
import { API_ENDPOINTS, IS_DEV_MODE, YAML_FILE } from './constants.js';
|
|
8
11
|
import { ERRORS, INFO, PROMPTS, quotaExhaustedMessage, recordLabel, VALIDATION, } from './messages.js';
|
|
9
12
|
import { loadYaml, saveYaml } from './yaml-config.js';
|
|
10
13
|
export const FREE_TIER = 'free';
|
|
@@ -76,17 +79,99 @@ export class BaseCommand extends Command {
|
|
|
76
79
|
}
|
|
77
80
|
/**
|
|
78
81
|
* Loads and returns the mailmodo.yaml configuration from the current directory.
|
|
79
|
-
*
|
|
82
|
+
* If the file is not found locally and the API client is available, attempts to
|
|
83
|
+
* restore it from the server. Exits with an error if the file cannot be found
|
|
84
|
+
* or restored, directing the user to run init.
|
|
80
85
|
*
|
|
81
86
|
* @returns {Promise<MailmodoYaml>} The parsed mailmodo.yaml containing project
|
|
82
87
|
* settings and all email sequence definitions.
|
|
83
88
|
*/
|
|
84
89
|
async ensureYaml() {
|
|
85
90
|
const config = await loadYaml();
|
|
86
|
-
if (
|
|
87
|
-
|
|
91
|
+
if (config)
|
|
92
|
+
return config;
|
|
93
|
+
const restored = await this.restoreYamlFromServer();
|
|
94
|
+
if (restored)
|
|
95
|
+
return restored;
|
|
96
|
+
this.error(ERRORS.NO_YAML);
|
|
97
|
+
}
|
|
98
|
+
async fetchAndWriteYaml(client) {
|
|
99
|
+
try {
|
|
100
|
+
const response = await client.getRawText(API_ENDPOINTS.ASSETS_YAML);
|
|
101
|
+
if (!response.ok || !response.data)
|
|
102
|
+
return false;
|
|
103
|
+
await writeFile(join(process.cwd(), YAML_FILE), response.data);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Attempts to fetch mailmodo.yaml from the server and save it locally.
|
|
112
|
+
* Returns null silently on any failure so callers can fall through to an error.
|
|
113
|
+
*/
|
|
114
|
+
async restoreYamlFromServer() {
|
|
115
|
+
try {
|
|
116
|
+
let client = this.apiClient;
|
|
117
|
+
if (!client) {
|
|
118
|
+
const envKey = process.env.MAILMODO_API_KEY;
|
|
119
|
+
const apiKey = envKey ?? (await loadConfig())?.apiKey;
|
|
120
|
+
if (!apiKey)
|
|
121
|
+
return null;
|
|
122
|
+
client = new ApiClient(apiKey);
|
|
123
|
+
}
|
|
124
|
+
const written = await this.fetchAndWriteYaml(client);
|
|
125
|
+
if (!written)
|
|
126
|
+
return null;
|
|
127
|
+
this.logToStderr(INFO.YAML_RESTORED_FROM_SERVER);
|
|
128
|
+
return loadYaml();
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* If `mailmodo.yaml` is absent from the current directory, attempts to restore
|
|
136
|
+
* it from the server using the given client. Returns `true` if the file was
|
|
137
|
+
* successfully written, `false` otherwise (file already present, server 404,
|
|
138
|
+
* or any network error). Silent — never throws.
|
|
139
|
+
*
|
|
140
|
+
* Used by `mailmodo login` right after the API key is validated so a returning
|
|
141
|
+
* user automatically gets their config back without having to run `init` again.
|
|
142
|
+
*/
|
|
143
|
+
async recoverYamlAfterLogin(client) {
|
|
144
|
+
if (existsSync(join(process.cwd(), YAML_FILE)))
|
|
145
|
+
return false;
|
|
146
|
+
return this.fetchAndWriteYaml(client);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Uploads the current local mailmodo.yaml to the server as a backup.
|
|
150
|
+
* Best-effort: silently ignores all errors so the originating command
|
|
151
|
+
* always succeeds regardless of sync failures.
|
|
152
|
+
*/
|
|
153
|
+
async syncYamlToServer() {
|
|
154
|
+
try {
|
|
155
|
+
let client = this.apiClient;
|
|
156
|
+
if (!client) {
|
|
157
|
+
const envKey = process.env.MAILMODO_API_KEY;
|
|
158
|
+
const apiKey = envKey ?? (await loadConfig())?.apiKey;
|
|
159
|
+
if (!apiKey)
|
|
160
|
+
return;
|
|
161
|
+
client = new ApiClient(apiKey);
|
|
162
|
+
}
|
|
163
|
+
const filePath = join(process.cwd(), YAML_FILE);
|
|
164
|
+
if (!existsSync(filePath))
|
|
165
|
+
return;
|
|
166
|
+
const content = await readFile(filePath, 'utf8');
|
|
167
|
+
const blob = new Blob([content], { type: 'application/yaml' });
|
|
168
|
+
const formData = new FormData();
|
|
169
|
+
formData.append('yaml', blob, YAML_FILE);
|
|
170
|
+
await client.postFormData(API_ENDPOINTS.ASSETS_YAML, formData);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Silently ignore — local file remains authoritative
|
|
88
174
|
}
|
|
89
|
-
return config;
|
|
90
175
|
}
|
|
91
176
|
/**
|
|
92
177
|
* Handles a failed API response by mapping HTTP status codes to
|
|
@@ -276,6 +361,7 @@ export class BaseCommand extends Command {
|
|
|
276
361
|
if (inputs.replyTo)
|
|
277
362
|
yamlConfig.project.replyTo = inputs.replyTo;
|
|
278
363
|
await saveYaml(yamlConfig);
|
|
364
|
+
await this.syncYamlToServer();
|
|
279
365
|
return {
|
|
280
366
|
dnsGuideUrl: response.data?.dnsGuideUrl,
|
|
281
367
|
dnsRecords: response.data?.dnsRecords || [],
|