@mailmodo/cli 0.0.8 → 0.0.9-beta.pr11.15
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.js +3 -3
- package/dist/commands/contacts/index.js +4 -4
- package/dist/commands/deploy/index.d.ts +17 -0
- package/dist/commands/deploy/index.js +37 -17
- package/dist/commands/domain/index.js +4 -7
- package/dist/commands/edit/index.js +2 -2
- package/dist/commands/init/index.js +34 -16
- package/dist/commands/login/index.js +26 -5
- package/dist/commands/logout/index.d.ts +10 -0
- package/dist/commands/logout/index.js +28 -0
- package/dist/commands/logs/index.js +1 -1
- package/dist/commands/preview/index.js +2 -2
- package/dist/commands/settings/index.d.ts +1 -0
- package/dist/commands/settings/index.js +4 -3
- package/dist/commands/status/index.js +1 -1
- package/dist/lib/api-client.d.ts +16 -13
- package/dist/lib/api-client.js +52 -7
- package/dist/lib/base-command.d.ts +30 -2
- package/dist/lib/base-command.js +66 -4
- package/dist/lib/config.d.ts +5 -0
- package/dist/lib/config.js +15 -2
- package/dist/lib/constants.d.ts +17 -17
- package/dist/lib/constants.js +23 -18
- package/oclif.manifest.json +84 -46
- package/package.json +3 -2
|
@@ -35,7 +35,7 @@ export default class Billing extends BaseCommand {
|
|
|
35
35
|
* opens Stripe Checkout if no card is on file.
|
|
36
36
|
*/
|
|
37
37
|
async showStatus(jsonOutput) {
|
|
38
|
-
const response = await this.apiClient.get(API_ENDPOINTS.BILLING_STATUS);
|
|
38
|
+
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Loading billing status...' }, () => this.apiClient.get(API_ENDPOINTS.BILLING_STATUS));
|
|
39
39
|
if (!response.ok) {
|
|
40
40
|
this.handleApiError(response);
|
|
41
41
|
}
|
|
@@ -84,9 +84,9 @@ export default class Billing extends BaseCommand {
|
|
|
84
84
|
* @param {boolean} jsonOutput - Whether to output JSON instead of formatted text.
|
|
85
85
|
*/
|
|
86
86
|
async setCap(cap, jsonOutput) {
|
|
87
|
-
const response = await this.apiClient.patch(API_ENDPOINTS.BILLING_CAP, {
|
|
87
|
+
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Updating spending cap...' }, () => this.apiClient.patch(API_ENDPOINTS.BILLING_CAP, {
|
|
88
88
|
cap,
|
|
89
|
-
});
|
|
89
|
+
}));
|
|
90
90
|
if (!response.ok) {
|
|
91
91
|
this.handleApiError(response);
|
|
92
92
|
}
|
|
@@ -43,7 +43,7 @@ export default class Contacts extends BaseCommand {
|
|
|
43
43
|
* Displays aggregate contact counts: total, active, unsubscribed, bounced.
|
|
44
44
|
*/
|
|
45
45
|
async showSummary(jsonOutput) {
|
|
46
|
-
const response = await this.apiClient.get(API_ENDPOINTS.CONTACTS);
|
|
46
|
+
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Loading contacts...' }, () => this.apiClient.get(API_ENDPOINTS.CONTACTS));
|
|
47
47
|
if (!response.ok) {
|
|
48
48
|
this.handleApiError(response);
|
|
49
49
|
}
|
|
@@ -62,7 +62,7 @@ export default class Contacts extends BaseCommand {
|
|
|
62
62
|
* status, properties, and pending emails.
|
|
63
63
|
*/
|
|
64
64
|
async searchContact(email, jsonOutput) {
|
|
65
|
-
const response = await this.apiClient.get(`${API_ENDPOINTS.CONTACTS}/${encodeURIComponent(email)}`);
|
|
65
|
+
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Looking up contact...' }, () => this.apiClient.get(`${API_ENDPOINTS.CONTACTS}/${encodeURIComponent(email)}`));
|
|
66
66
|
if (!response.ok) {
|
|
67
67
|
if (response.status === 404) {
|
|
68
68
|
if (jsonOutput) {
|
|
@@ -97,7 +97,7 @@ export default class Contacts extends BaseCommand {
|
|
|
97
97
|
* for the generated file.
|
|
98
98
|
*/
|
|
99
99
|
async exportContacts(jsonOutput) {
|
|
100
|
-
const response = await this.apiClient.get(API_ENDPOINTS.CONTACTS_EXPORT);
|
|
100
|
+
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Preparing contact export...' }, () => this.apiClient.get(API_ENDPOINTS.CONTACTS_EXPORT));
|
|
101
101
|
if (!response.ok) {
|
|
102
102
|
this.handleApiError(response);
|
|
103
103
|
}
|
|
@@ -126,7 +126,7 @@ export default class Contacts extends BaseCommand {
|
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
-
const response = await this.apiClient.delete(`${API_ENDPOINTS.CONTACTS}/${encodeURIComponent(email)}`);
|
|
129
|
+
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Deleting contact...' }, () => this.apiClient.delete(`${API_ENDPOINTS.CONTACTS}/${encodeURIComponent(email)}`));
|
|
130
130
|
if (!response.ok) {
|
|
131
131
|
this.handleApiError(response);
|
|
132
132
|
}
|
|
@@ -6,7 +6,24 @@ export default class Deploy extends BaseCommand {
|
|
|
6
6
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
7
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* Fetches current DNS verification status for the deploy flow.
|
|
11
|
+
*
|
|
12
|
+
* @param jsonOutput - When true, spinner uses stderr for stdout-safe JSON runs.
|
|
13
|
+
*/
|
|
14
|
+
private fetchDomainVerifyForDeploy;
|
|
9
15
|
run(): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Lists emails about to be deployed (skipped when `--json` is set).
|
|
18
|
+
*
|
|
19
|
+
* @param yamlConfig - Loaded project YAML.
|
|
20
|
+
* @param jsonOutput - When true, skip human-readable output.
|
|
21
|
+
*/
|
|
22
|
+
private logPreDeploySummary;
|
|
23
|
+
/**
|
|
24
|
+
* Prints the post-deploy success message and SDK install snippet for interactive runs.
|
|
25
|
+
*/
|
|
26
|
+
private logDeploySuccessInstructions;
|
|
10
27
|
/**
|
|
11
28
|
* Interactive domain setup flow. Collects domain, sender email, and business
|
|
12
29
|
* address from the user, then calls the API to get DNS records to configure.
|
|
@@ -12,11 +12,19 @@ export default class Deploy extends BaseCommand {
|
|
|
12
12
|
static flags = {
|
|
13
13
|
...BaseCommand.baseFlags,
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* Fetches current DNS verification status for the deploy flow.
|
|
17
|
+
*
|
|
18
|
+
* @param jsonOutput - When true, spinner uses stderr for stdout-safe JSON runs.
|
|
19
|
+
*/
|
|
20
|
+
fetchDomainVerifyForDeploy(jsonOutput) {
|
|
21
|
+
return this.withApiSpinner({ json: jsonOutput, text: ' Checking domain verification...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY));
|
|
22
|
+
}
|
|
15
23
|
async run() {
|
|
16
24
|
const { flags } = await this.parse(Deploy);
|
|
17
25
|
await this.ensureAuth();
|
|
18
26
|
const yamlConfig = await this.ensureYaml();
|
|
19
|
-
const domainVerify = await this.
|
|
27
|
+
const domainVerify = await this.fetchDomainVerifyForDeploy(flags.json);
|
|
20
28
|
const domainVerified = domainVerify.ok &&
|
|
21
29
|
domainVerify.data?.spf === 'pass' &&
|
|
22
30
|
domainVerify.data?.dkim === 'pass' &&
|
|
@@ -44,14 +52,7 @@ export default class Deploy extends BaseCommand {
|
|
|
44
52
|
if (!completed)
|
|
45
53
|
return;
|
|
46
54
|
}
|
|
47
|
-
|
|
48
|
-
this.log(`\n ${chalk.green('✓')} Domain: ${yamlConfig.project?.domain || 'verified'}\n`);
|
|
49
|
-
this.log(` Deploying:`);
|
|
50
|
-
for (const email of yamlConfig.emails) {
|
|
51
|
-
this.log(` ${chalk.green('+')} ${email.id.padEnd(24)} ${email.trigger}`);
|
|
52
|
-
}
|
|
53
|
-
this.log('');
|
|
54
|
-
}
|
|
55
|
+
this.logPreDeploySummary(yamlConfig, flags.json);
|
|
55
56
|
if (!flags.yes) {
|
|
56
57
|
const proceed = await confirm({
|
|
57
58
|
default: true,
|
|
@@ -62,10 +63,10 @@ export default class Deploy extends BaseCommand {
|
|
|
62
63
|
return;
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
|
-
const response = await this.apiClient.post(API_ENDPOINTS.SEQUENCES, {
|
|
66
|
+
const response = await this.withApiSpinner({ json: flags.json, text: ' Deploying email sequences...' }, () => this.apiClient.post(API_ENDPOINTS.SEQUENCES, {
|
|
66
67
|
emails: yamlConfig.emails,
|
|
67
68
|
project: yamlConfig.project,
|
|
68
|
-
});
|
|
69
|
+
}));
|
|
69
70
|
if (!response.ok) {
|
|
70
71
|
this.handleApiError(response);
|
|
71
72
|
}
|
|
@@ -77,6 +78,28 @@ export default class Deploy extends BaseCommand {
|
|
|
77
78
|
}, null, 2));
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
81
|
+
this.logDeploySuccessInstructions();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Lists emails about to be deployed (skipped when `--json` is set).
|
|
85
|
+
*
|
|
86
|
+
* @param yamlConfig - Loaded project YAML.
|
|
87
|
+
* @param jsonOutput - When true, skip human-readable output.
|
|
88
|
+
*/
|
|
89
|
+
logPreDeploySummary(yamlConfig, jsonOutput) {
|
|
90
|
+
if (jsonOutput)
|
|
91
|
+
return;
|
|
92
|
+
this.log(`\n ${chalk.green('✓')} Domain: ${yamlConfig.project?.domain || 'verified'}\n`);
|
|
93
|
+
this.log(` Deploying:`);
|
|
94
|
+
for (const email of yamlConfig.emails) {
|
|
95
|
+
this.log(` ${chalk.green('+')} ${email.id.padEnd(24)} ${email.trigger}`);
|
|
96
|
+
}
|
|
97
|
+
this.log('');
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Prints the post-deploy success message and SDK install snippet for interactive runs.
|
|
101
|
+
*/
|
|
102
|
+
logDeploySuccessInstructions() {
|
|
80
103
|
this.log(` ${chalk.green('Deployed.')} Emails are live.\n`);
|
|
81
104
|
this.log(` ${'─'.repeat(53)}`);
|
|
82
105
|
this.log(` ${chalk.bold('ADD THIS TO YOUR APP (one-time only):')}`);
|
|
@@ -125,11 +148,11 @@ export default class Deploy extends BaseCommand {
|
|
|
125
148
|
validate: (v) => (v?.trim() ? true : 'Address is required'),
|
|
126
149
|
});
|
|
127
150
|
}
|
|
128
|
-
const domainResponse = await this.apiClient.post(API_ENDPOINTS.DOMAIN, {
|
|
151
|
+
const domainResponse = await this.withApiSpinner({ json: flags.json, text: ' Configuring domain...' }, () => this.apiClient.post(API_ENDPOINTS.DOMAIN, {
|
|
129
152
|
address,
|
|
130
153
|
domain,
|
|
131
154
|
fromEmail: senderEmail,
|
|
132
|
-
});
|
|
155
|
+
}));
|
|
133
156
|
if (!domainResponse.ok) {
|
|
134
157
|
this.handleApiError(domainResponse);
|
|
135
158
|
}
|
|
@@ -171,10 +194,7 @@ export default class Deploy extends BaseCommand {
|
|
|
171
194
|
* @returns {Promise<boolean>} true if all records pass.
|
|
172
195
|
*/
|
|
173
196
|
async verifyDomain(jsonOutput) {
|
|
174
|
-
|
|
175
|
-
this.log(`\n Checking DNS...`);
|
|
176
|
-
}
|
|
177
|
-
const verify = await this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY);
|
|
197
|
+
const verify = await this.withApiSpinner({ json: jsonOutput, text: ' Checking DNS...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY));
|
|
178
198
|
if (!verify.ok) {
|
|
179
199
|
this.handleApiError(verify);
|
|
180
200
|
}
|
|
@@ -72,11 +72,11 @@ export default class Domain extends BaseCommand {
|
|
|
72
72
|
validate: (v) => (v?.trim() ? true : 'Address is required'),
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
|
-
const response = await this.apiClient.post(API_ENDPOINTS.DOMAIN, {
|
|
75
|
+
const response = await this.withApiSpinner({ json: flags.json, text: ' Configuring domain...' }, () => this.apiClient.post(API_ENDPOINTS.DOMAIN, {
|
|
76
76
|
address,
|
|
77
77
|
domain,
|
|
78
78
|
fromEmail: senderEmail,
|
|
79
|
-
});
|
|
79
|
+
}));
|
|
80
80
|
if (!response.ok) {
|
|
81
81
|
this.handleApiError(response);
|
|
82
82
|
}
|
|
@@ -112,10 +112,7 @@ export default class Domain extends BaseCommand {
|
|
|
112
112
|
* Calls the domain verification API and displays pass/fail for each DNS record.
|
|
113
113
|
*/
|
|
114
114
|
async verifyDomain(jsonOutput) {
|
|
115
|
-
|
|
116
|
-
this.log(`\n Checking DNS...`);
|
|
117
|
-
}
|
|
118
|
-
const response = await this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY);
|
|
115
|
+
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Checking DNS...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY));
|
|
119
116
|
if (!response.ok) {
|
|
120
117
|
this.handleApiError(response);
|
|
121
118
|
}
|
|
@@ -148,7 +145,7 @@ export default class Domain extends BaseCommand {
|
|
|
148
145
|
* bounce rate, and spam complaint rate.
|
|
149
146
|
*/
|
|
150
147
|
async showDomainStatus(jsonOutput) {
|
|
151
|
-
const response = await this.apiClient.get(API_ENDPOINTS.DOMAIN_STATUS);
|
|
148
|
+
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Loading domain status...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_STATUS));
|
|
152
149
|
if (!response.ok) {
|
|
153
150
|
this.handleApiError(response);
|
|
154
151
|
}
|
|
@@ -39,7 +39,7 @@ export default class Edit extends BaseCommand {
|
|
|
39
39
|
validate: (value) => value?.trim() ? true : 'Please describe the change',
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
|
-
const response = await this.apiClient.post(API_ENDPOINTS.EDIT, {
|
|
42
|
+
const response = await this.withApiSpinner({ json: flags.json, text: ' Applying AI edits...' }, () => this.apiClient.post(API_ENDPOINTS.EDIT, {
|
|
43
43
|
changeRequest: changeDescription,
|
|
44
44
|
currentEmail: {
|
|
45
45
|
condition: email.condition,
|
|
@@ -50,7 +50,7 @@ export default class Edit extends BaseCommand {
|
|
|
50
50
|
subject: email.subject,
|
|
51
51
|
trigger: email.trigger,
|
|
52
52
|
},
|
|
53
|
-
});
|
|
53
|
+
}));
|
|
54
54
|
if (!response.ok) {
|
|
55
55
|
this.handleApiError(response);
|
|
56
56
|
}
|
|
@@ -12,6 +12,23 @@ function isValidUrl(value) {
|
|
|
12
12
|
return false;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Prints the human-readable analysis summary using the provided line writer.
|
|
17
|
+
* Use stderr when `--json` is set so stdout stays free for machine-readable JSON.
|
|
18
|
+
*
|
|
19
|
+
* @param analysis - Parsed analyze API payload.
|
|
20
|
+
* @param logLine - Line writer (typically bound `this.log` or `this.logToStderr`).
|
|
21
|
+
*/
|
|
22
|
+
function logAnalysisSummary(analysis, logLine) {
|
|
23
|
+
logLine(` Detected:`);
|
|
24
|
+
logLine(` - Type: ${analysis.businessType} — ${analysis.description}`);
|
|
25
|
+
logLine(` - Model: ${analysis.pricingModel}`);
|
|
26
|
+
logLine(` - Users: ${analysis.targetUser}`);
|
|
27
|
+
if (analysis.events?.length) {
|
|
28
|
+
logLine(` - Events: ${analysis.events.join(', ')}`);
|
|
29
|
+
}
|
|
30
|
+
logLine('');
|
|
31
|
+
}
|
|
15
32
|
export default class Init extends BaseCommand {
|
|
16
33
|
static description = 'Analyze your product and generate email sequences';
|
|
17
34
|
static examples = [
|
|
@@ -38,22 +55,22 @@ export default class Init extends BaseCommand {
|
|
|
38
55
|
},
|
|
39
56
|
});
|
|
40
57
|
}
|
|
41
|
-
this.
|
|
42
|
-
|
|
43
|
-
|
|
58
|
+
const analysisResponse = await this.withApiSpinner({
|
|
59
|
+
json: flags.json,
|
|
60
|
+
text: ' Analyzing your product — scraping homepage, pricing, features',
|
|
61
|
+
}, () => this.apiClient.post(API_ENDPOINTS.ANALYZE, {
|
|
62
|
+
url: productUrl,
|
|
63
|
+
}));
|
|
44
64
|
if (!analysisResponse.ok) {
|
|
45
65
|
this.handleApiError(analysisResponse);
|
|
46
66
|
}
|
|
47
67
|
const analysis = analysisResponse.data;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.log(` - Events: ${analysis.events.join(', ')}`);
|
|
55
|
-
}
|
|
56
|
-
this.log('');
|
|
68
|
+
const shouldShowAnalysisSummary = !flags.json || !flags.yes;
|
|
69
|
+
if (shouldShowAnalysisSummary) {
|
|
70
|
+
const logSummary = flags.json && !flags.yes
|
|
71
|
+
? this.logToStderr.bind(this)
|
|
72
|
+
: this.log.bind(this);
|
|
73
|
+
logAnalysisSummary(analysis, logSummary);
|
|
57
74
|
}
|
|
58
75
|
let analysisPayload = analysis;
|
|
59
76
|
if (!flags.yes) {
|
|
@@ -89,12 +106,13 @@ export default class Init extends BaseCommand {
|
|
|
89
106
|
analysisPayload = JSON.parse(editedAnalysis);
|
|
90
107
|
}
|
|
91
108
|
}
|
|
92
|
-
this.
|
|
93
|
-
|
|
94
|
-
|
|
109
|
+
const generateResponse = await this.withApiSpinner({
|
|
110
|
+
json: flags.json,
|
|
111
|
+
text: ' Generating email templates...',
|
|
112
|
+
}, () => this.apiClient.post(API_ENDPOINTS.GENERATE, {
|
|
95
113
|
analysis: analysisPayload,
|
|
96
114
|
productUrl,
|
|
97
|
-
});
|
|
115
|
+
}));
|
|
98
116
|
if (!generateResponse.ok) {
|
|
99
117
|
this.handleApiError(generateResponse);
|
|
100
118
|
}
|
|
@@ -3,7 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import open from 'open';
|
|
4
4
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
5
|
import { ApiClient } from '../../lib/api-client.js';
|
|
6
|
-
import { saveConfig } from '../../lib/config.js';
|
|
6
|
+
import { loadConfig, saveConfig } from '../../lib/config.js';
|
|
7
7
|
import { API_ENDPOINTS, SIGNUP_URL } from '../../lib/constants.js';
|
|
8
8
|
export default class Login extends BaseCommand {
|
|
9
9
|
static description = 'Authenticate with Mailmodo using your API key';
|
|
@@ -16,7 +16,30 @@ export default class Login extends BaseCommand {
|
|
|
16
16
|
};
|
|
17
17
|
async run() {
|
|
18
18
|
const { flags } = await this.parse(Login);
|
|
19
|
-
|
|
19
|
+
const envKey = process.env.MAILMODO_API_KEY;
|
|
20
|
+
if (!envKey) {
|
|
21
|
+
const existing = await loadConfig();
|
|
22
|
+
if (existing?.apiKey) {
|
|
23
|
+
if (flags.json) {
|
|
24
|
+
this.log(JSON.stringify({
|
|
25
|
+
accountName: existing.accountName ?? null,
|
|
26
|
+
email: existing.email ?? null,
|
|
27
|
+
freeRemaining: existing.freeRemaining ?? null,
|
|
28
|
+
status: 'already_logged_in',
|
|
29
|
+
}, null, 2));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.log('\n You are already logged in.\n');
|
|
33
|
+
const emailDisplay = existing.email?.trim()
|
|
34
|
+
? chalk.green(existing.email.trim())
|
|
35
|
+
: chalk.dim('(unknown)');
|
|
36
|
+
this.log(` Email: ${emailDisplay}\n`);
|
|
37
|
+
this.log(` ${chalk.dim('1.')} Run ${chalk.cyan('mailmodo init')} to generate an email sequence.`);
|
|
38
|
+
this.log(` ${chalk.dim('2.')} Run ${chalk.cyan('mailmodo logout')} to log in with another account.\n`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
let apiKey = envKey;
|
|
20
43
|
if (apiKey) {
|
|
21
44
|
this.log('Detected MAILMODO_API_KEY from environment.');
|
|
22
45
|
}
|
|
@@ -34,15 +57,13 @@ export default class Login extends BaseCommand {
|
|
|
34
57
|
validate(value) {
|
|
35
58
|
if (!value?.trim())
|
|
36
59
|
return 'API key is required';
|
|
37
|
-
if (!value.startsWith('mm_'))
|
|
38
|
-
return 'Invalid API key format. Keys start with mm_';
|
|
39
60
|
return true;
|
|
40
61
|
},
|
|
41
62
|
});
|
|
42
63
|
}
|
|
43
64
|
const trimmedKey = apiKey.trim();
|
|
44
65
|
const client = new ApiClient(trimmedKey);
|
|
45
|
-
const response = await client.
|
|
66
|
+
const response = await this.withApiSpinner({ json: flags.json, text: ' Validating API key...' }, () => client.post(API_ENDPOINTS.AUTH_VALIDATE, {}));
|
|
46
67
|
if (!response.ok) {
|
|
47
68
|
this.handleApiError(response);
|
|
48
69
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
2
|
+
export default class Logout extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
};
|
|
9
|
+
run(): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
3
|
+
import { clearConfig, loadConfig } from '../../lib/config.js';
|
|
4
|
+
export default class Logout extends BaseCommand {
|
|
5
|
+
static description = 'Sign out by removing saved credentials from this machine';
|
|
6
|
+
static examples = ['<%= config.bin %> logout'];
|
|
7
|
+
static flags = {
|
|
8
|
+
...BaseCommand.baseFlags,
|
|
9
|
+
};
|
|
10
|
+
async run() {
|
|
11
|
+
const { flags } = await this.parse(Logout);
|
|
12
|
+
const config = await loadConfig();
|
|
13
|
+
if (!config?.apiKey) {
|
|
14
|
+
if (flags.json) {
|
|
15
|
+
this.log(JSON.stringify({ status: 'not_logged_in' }, null, 2));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
this.log('You are not logged in.');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
await clearConfig();
|
|
22
|
+
if (flags.json) {
|
|
23
|
+
this.log(JSON.stringify({ status: 'logged_out' }, null, 2));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
this.log(`\n Signed out. Run ${chalk.cyan('mailmodo login')} to authenticate again.\n`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -26,7 +26,7 @@ export default class Logs extends BaseCommand {
|
|
|
26
26
|
params.email = flags.email;
|
|
27
27
|
if (flags.failed)
|
|
28
28
|
params.failed = 'true';
|
|
29
|
-
const response = await this.apiClient.get(API_ENDPOINTS.LOGS, params);
|
|
29
|
+
const response = await this.withApiSpinner({ json: flags.json, text: ' Loading email logs...' }, () => this.apiClient.get(API_ENDPOINTS.LOGS, params));
|
|
30
30
|
if (!response.ok) {
|
|
31
31
|
this.handleApiError(response);
|
|
32
32
|
}
|
|
@@ -125,9 +125,9 @@ export default class Preview extends BaseCommand {
|
|
|
125
125
|
*/
|
|
126
126
|
async sendTestEmail(emailId, toAddress, jsonOutput) {
|
|
127
127
|
await this.ensureAuth();
|
|
128
|
-
const response = await this.apiClient.post(`${API_ENDPOINTS.PREVIEW}/${emailId}/send`, {
|
|
128
|
+
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Sending test email...' }, () => this.apiClient.post(`${API_ENDPOINTS.PREVIEW}/${emailId}/send`, {
|
|
129
129
|
to: toAddress,
|
|
130
|
-
});
|
|
130
|
+
}));
|
|
131
131
|
if (!response.ok) {
|
|
132
132
|
this.handleApiError(response);
|
|
133
133
|
}
|
|
@@ -14,6 +14,7 @@ export default class Settings extends BaseCommand {
|
|
|
14
14
|
* and logoUrl in the project config.
|
|
15
15
|
*
|
|
16
16
|
* @param {import('../../lib/yaml-config.js').MailmodoYaml} yamlConfig - The full YAML config to update and save.
|
|
17
|
+
* @param {boolean} jsonOutput - When true, spinner uses stderr so stdout stays clean.
|
|
17
18
|
*/
|
|
18
19
|
private handleLogoUpload;
|
|
19
20
|
}
|
|
@@ -89,7 +89,7 @@ export default class Settings extends BaseCommand {
|
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
91
|
if (editKey === 'logo_file') {
|
|
92
|
-
await this.handleLogoUpload(yamlConfig);
|
|
92
|
+
await this.handleLogoUpload(yamlConfig, flags.json);
|
|
93
93
|
return;
|
|
94
94
|
}
|
|
95
95
|
if (editKey === 'email_style') {
|
|
@@ -122,8 +122,9 @@ export default class Settings extends BaseCommand {
|
|
|
122
122
|
* and logoUrl in the project config.
|
|
123
123
|
*
|
|
124
124
|
* @param {import('../../lib/yaml-config.js').MailmodoYaml} yamlConfig - The full YAML config to update and save.
|
|
125
|
+
* @param {boolean} jsonOutput - When true, spinner uses stderr so stdout stays clean.
|
|
125
126
|
*/
|
|
126
|
-
async handleLogoUpload(yamlConfig) {
|
|
127
|
+
async handleLogoUpload(yamlConfig, jsonOutput) {
|
|
127
128
|
const logoPath = await input({ message: 'Path to logo file:' });
|
|
128
129
|
const resolvedPath = resolve(logoPath);
|
|
129
130
|
if (!existsSync(resolvedPath)) {
|
|
@@ -134,7 +135,7 @@ export default class Settings extends BaseCommand {
|
|
|
134
135
|
const fileBuffer = await readFile(resolvedPath);
|
|
135
136
|
const formData = new FormData();
|
|
136
137
|
formData.append('logo', new Blob([new Uint8Array(fileBuffer)]), logoPath.split(/[/\\]/).pop() || 'logo.png');
|
|
137
|
-
const response = await this.apiClient.postFormData(API_ENDPOINTS.ASSETS_LOGO, formData);
|
|
138
|
+
const response = await this.withApiSpinner({ json: jsonOutput, text: ' Uploading logo...' }, () => this.apiClient.postFormData(API_ENDPOINTS.ASSETS_LOGO, formData));
|
|
138
139
|
if (!response.ok) {
|
|
139
140
|
this.handleApiError(response);
|
|
140
141
|
}
|
|
@@ -13,7 +13,7 @@ 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 response = await this.apiClient.get(API_ENDPOINTS.ANALYTICS);
|
|
16
|
+
const response = await this.withApiSpinner({ json: flags.json, text: ' Loading analytics...' }, () => this.apiClient.get(API_ENDPOINTS.ANALYTICS));
|
|
17
17
|
if (!response.ok) {
|
|
18
18
|
this.handleApiError(response);
|
|
19
19
|
}
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request context attached to API responses for troubleshooting failed calls.
|
|
3
|
+
* Only the resolved URL is stored; it encodes origin, path, and query string.
|
|
4
|
+
*/
|
|
5
|
+
export interface ApiRequestDebugInfo {
|
|
6
|
+
/** Node/DNS/network error code when `fetch` throws before an HTTP response. */
|
|
7
|
+
causeCode?: string;
|
|
8
|
+
/** Fully resolved request URL (origin, path, and query string). */
|
|
9
|
+
fullUrl: string;
|
|
10
|
+
/** Truncated JSON summary of a non-empty error response body, when available. */
|
|
11
|
+
responseSummary?: string;
|
|
12
|
+
}
|
|
1
13
|
export interface ApiResponse<T = Record<string, unknown>> {
|
|
2
14
|
data: T;
|
|
15
|
+
/** Populated for tracing; especially useful when `ok` is false. */
|
|
16
|
+
debug?: ApiRequestDebugInfo;
|
|
3
17
|
error?: string;
|
|
4
18
|
ok: boolean;
|
|
5
19
|
status: number;
|
|
6
20
|
}
|
|
7
|
-
/**
|
|
8
|
-
* HTTP client for the Mailmodo CLI API.
|
|
9
|
-
* Wraps the native fetch API with Bearer token authentication,
|
|
10
|
-
* consistent error handling, and typed responses.
|
|
11
|
-
*
|
|
12
|
-
* All requests include:
|
|
13
|
-
* - Authorization header with the user's API key
|
|
14
|
-
* - Content-Type: application/json
|
|
15
|
-
* - User-Agent: @mailmodo/cli
|
|
16
|
-
*
|
|
17
|
-
* Network errors (ECONNREFUSED, ENOTFOUND) return a friendly message
|
|
18
|
-
* indicating the API may not be available, rather than a raw stack trace.
|
|
19
|
-
*/
|
|
20
21
|
export declare class ApiClient {
|
|
21
22
|
private apiKey;
|
|
22
23
|
private baseUrl;
|
|
23
24
|
constructor(apiKey: string);
|
|
25
|
+
private resolveUrl;
|
|
26
|
+
private requestDebug;
|
|
24
27
|
/**
|
|
25
28
|
* Sends an HTTP request to the Mailmodo API and returns a typed response.
|
|
26
29
|
*
|
package/dist/lib/api-client.js
CHANGED
|
@@ -12,6 +12,22 @@ import { API_BASE_URL } from './constants.js';
|
|
|
12
12
|
* Network errors (ECONNREFUSED, ENOTFOUND) return a friendly message
|
|
13
13
|
* indicating the API may not be available, rather than a raw stack trace.
|
|
14
14
|
*/
|
|
15
|
+
const RESPONSE_BODY_DEBUG_MAX = 800;
|
|
16
|
+
function summarizeResponseBody(data) {
|
|
17
|
+
if (data === null || data === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
try {
|
|
20
|
+
const s = JSON.stringify(data);
|
|
21
|
+
if (s === '{}' || s === '[]')
|
|
22
|
+
return undefined;
|
|
23
|
+
return s.length > RESPONSE_BODY_DEBUG_MAX
|
|
24
|
+
? `${s.slice(0, RESPONSE_BODY_DEBUG_MAX)}…`
|
|
25
|
+
: s;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
15
31
|
export class ApiClient {
|
|
16
32
|
apiKey;
|
|
17
33
|
baseUrl;
|
|
@@ -19,6 +35,18 @@ export class ApiClient {
|
|
|
19
35
|
this.baseUrl = API_BASE_URL;
|
|
20
36
|
this.apiKey = apiKey;
|
|
21
37
|
}
|
|
38
|
+
resolveUrl(path, params) {
|
|
39
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
40
|
+
if (params) {
|
|
41
|
+
for (const [key, value] of Object.entries(params)) {
|
|
42
|
+
url.searchParams.set(key, value);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return url;
|
|
46
|
+
}
|
|
47
|
+
requestDebug(url) {
|
|
48
|
+
return { fullUrl: url.toString() };
|
|
49
|
+
}
|
|
22
50
|
/**
|
|
23
51
|
* Sends an HTTP request to the Mailmodo API and returns a typed response.
|
|
24
52
|
*
|
|
@@ -31,12 +59,8 @@ export class ApiClient {
|
|
|
31
59
|
* data (parsed JSON body), and optional error (string message on failure).
|
|
32
60
|
*/
|
|
33
61
|
async request(method, path, body, params) {
|
|
34
|
-
const url =
|
|
35
|
-
|
|
36
|
-
for (const [key, value] of Object.entries(params)) {
|
|
37
|
-
url.searchParams.set(key, value);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
62
|
+
const url = this.resolveUrl(path, params);
|
|
63
|
+
const debug = this.requestDebug(url);
|
|
40
64
|
const headers = {
|
|
41
65
|
Authorization: `Bearer ${this.apiKey}`,
|
|
42
66
|
'Content-Type': 'application/json',
|
|
@@ -51,8 +75,13 @@ export class ApiClient {
|
|
|
51
75
|
const data = await response.json().catch(() => ({}));
|
|
52
76
|
if (!response.ok) {
|
|
53
77
|
const errorData = data;
|
|
78
|
+
const summary = summarizeResponseBody(data);
|
|
54
79
|
return {
|
|
55
80
|
data: data,
|
|
81
|
+
debug: {
|
|
82
|
+
...debug,
|
|
83
|
+
responseSummary: summary,
|
|
84
|
+
},
|
|
56
85
|
error: errorData?.message ||
|
|
57
86
|
errorData?.error ||
|
|
58
87
|
`Request failed with status ${response.status}`,
|
|
@@ -65,8 +94,13 @@ export class ApiClient {
|
|
|
65
94
|
catch (error) {
|
|
66
95
|
const err = error;
|
|
67
96
|
const isConnectionError = err?.cause?.code === 'ECONNREFUSED' || err?.cause?.code === 'ENOTFOUND';
|
|
97
|
+
const causeCode = err?.cause?.code;
|
|
68
98
|
return {
|
|
69
99
|
data: {},
|
|
100
|
+
debug: {
|
|
101
|
+
...debug,
|
|
102
|
+
...(causeCode ? { causeCode } : {}),
|
|
103
|
+
},
|
|
70
104
|
error: isConnectionError
|
|
71
105
|
? 'Cannot connect to Mailmodo API. The API service may not be available yet.'
|
|
72
106
|
: err?.message || 'An unexpected network error occurred.',
|
|
@@ -88,7 +122,8 @@ export class ApiClient {
|
|
|
88
122
|
return this.request('POST', path, body);
|
|
89
123
|
}
|
|
90
124
|
async postFormData(path, formData) {
|
|
91
|
-
const url =
|
|
125
|
+
const url = this.resolveUrl(path);
|
|
126
|
+
const debug = this.requestDebug(url);
|
|
92
127
|
try {
|
|
93
128
|
const response = await fetch(url.toString(), {
|
|
94
129
|
body: formData,
|
|
@@ -101,8 +136,13 @@ export class ApiClient {
|
|
|
101
136
|
const data = await response.json().catch(() => ({}));
|
|
102
137
|
if (!response.ok) {
|
|
103
138
|
const errorData = data;
|
|
139
|
+
const summary = summarizeResponseBody(data);
|
|
104
140
|
return {
|
|
105
141
|
data: data,
|
|
142
|
+
debug: {
|
|
143
|
+
...debug,
|
|
144
|
+
responseSummary: summary,
|
|
145
|
+
},
|
|
106
146
|
error: errorData?.message ||
|
|
107
147
|
errorData?.error ||
|
|
108
148
|
`Upload failed with status ${response.status}`,
|
|
@@ -114,8 +154,13 @@ export class ApiClient {
|
|
|
114
154
|
}
|
|
115
155
|
catch (error) {
|
|
116
156
|
const err = error;
|
|
157
|
+
const causeCode = err?.cause?.code;
|
|
117
158
|
return {
|
|
118
159
|
data: {},
|
|
160
|
+
debug: {
|
|
161
|
+
...debug,
|
|
162
|
+
...(causeCode ? { causeCode } : {}),
|
|
163
|
+
},
|
|
119
164
|
error: err?.message || 'File upload failed.',
|
|
120
165
|
ok: false,
|
|
121
166
|
status: 0,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
import { ApiClient } from './api-client.js';
|
|
2
|
+
import { ApiClient, type ApiRequestDebugInfo } from './api-client.js';
|
|
3
3
|
import { type MailmodoConfig } from './config.js';
|
|
4
4
|
import { type MailmodoYaml } from './yaml-config.js';
|
|
5
5
|
/**
|
|
@@ -22,6 +22,22 @@ export declare abstract class BaseCommand extends Command {
|
|
|
22
22
|
* @returns {Promise<MailmodoConfig>} The resolved configuration containing the API key.
|
|
23
23
|
*/
|
|
24
24
|
protected ensureAuth(): Promise<MailmodoConfig>;
|
|
25
|
+
/**
|
|
26
|
+
* Runs an async function (typically an API call) while showing a cyan ora spinner.
|
|
27
|
+
* When `options.json` is true, the leading blank line is written to stderr so stdout stays
|
|
28
|
+
* free for machine-readable JSON (e.g. piping to `jq`).
|
|
29
|
+
*
|
|
30
|
+
* @template T - Resolved type of the work promise (often the API client response).
|
|
31
|
+
* @param {{ json: boolean; text: string }} options - Spinner options.
|
|
32
|
+
* @param {boolean} options.json - If true, stderr for the blank line before the spinner.
|
|
33
|
+
* @param {string} options.text - Spinner label (often two leading spaces).
|
|
34
|
+
* @param {() => Promise<T>} work - Zero-argument async function that performs the request.
|
|
35
|
+
* @returns {Promise<T>} The fulfilled value from `work`.
|
|
36
|
+
*/
|
|
37
|
+
protected withApiSpinner<T>(options: {
|
|
38
|
+
json: boolean;
|
|
39
|
+
text: string;
|
|
40
|
+
}, work: () => Promise<T>): Promise<T>;
|
|
25
41
|
/**
|
|
26
42
|
* Loads and returns the mailmodo.yaml configuration from the current directory.
|
|
27
43
|
* Exits with an error if the file is not found, directing the user to run init.
|
|
@@ -34,12 +50,24 @@ export declare abstract class BaseCommand extends Command {
|
|
|
34
50
|
* Handles a failed API response by mapping HTTP status codes to
|
|
35
51
|
* user-friendly error messages and exiting the process.
|
|
36
52
|
*
|
|
37
|
-
* @param {{ status: number; error?: string }} response - The API response object with ok=false.
|
|
53
|
+
* @param {{ status: number; error?: string; debug?: ApiRequestDebugInfo }} response - The API response object with ok=false.
|
|
38
54
|
* Status 401 prompts re-authentication, 429 indicates rate limiting,
|
|
39
55
|
* all others display the server's error message or a generic fallback.
|
|
56
|
+
* When `debug` is present, extra lines list Full URL, Status, optional Response
|
|
57
|
+
* body summary, and optional Error Code (network failures).
|
|
40
58
|
*/
|
|
41
59
|
protected handleApiError(response: {
|
|
60
|
+
debug?: ApiRequestDebugInfo;
|
|
42
61
|
error?: string;
|
|
43
62
|
status: number;
|
|
44
63
|
}): never;
|
|
64
|
+
/**
|
|
65
|
+
* Builds the terminal error string for a failed API call, appending request
|
|
66
|
+
* metadata when {@link ApiRequestDebugInfo} is available.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} message - Primary error text (HTTP message or generic).
|
|
69
|
+
* @param {{ status: number; debug?: ApiRequestDebugInfo }} response - Failed API response.
|
|
70
|
+
* @returns {string} Message plus indented Request details for troubleshooting.
|
|
71
|
+
*/
|
|
72
|
+
private formatApiFailure;
|
|
45
73
|
}
|
package/dist/lib/base-command.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
3
4
|
import { ApiClient } from './api-client.js';
|
|
4
5
|
import { loadConfig } from './config.js';
|
|
5
6
|
import { loadYaml } from './yaml-config.js';
|
|
@@ -39,6 +40,36 @@ export class BaseCommand extends Command {
|
|
|
39
40
|
this.apiClient = new ApiClient(config.apiKey);
|
|
40
41
|
return config;
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Runs an async function (typically an API call) while showing a cyan ora spinner.
|
|
45
|
+
* When `options.json` is true, the leading blank line is written to stderr so stdout stays
|
|
46
|
+
* free for machine-readable JSON (e.g. piping to `jq`).
|
|
47
|
+
*
|
|
48
|
+
* @template T - Resolved type of the work promise (often the API client response).
|
|
49
|
+
* @param {{ json: boolean; text: string }} options - Spinner options.
|
|
50
|
+
* @param {boolean} options.json - If true, stderr for the blank line before the spinner.
|
|
51
|
+
* @param {string} options.text - Spinner label (often two leading spaces).
|
|
52
|
+
* @param {() => Promise<T>} work - Zero-argument async function that performs the request.
|
|
53
|
+
* @returns {Promise<T>} The fulfilled value from `work`.
|
|
54
|
+
*/
|
|
55
|
+
async withApiSpinner(options, work) {
|
|
56
|
+
if (options.json) {
|
|
57
|
+
this.logToStderr('');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.log('');
|
|
61
|
+
}
|
|
62
|
+
const spinner = ora({
|
|
63
|
+
color: 'cyan',
|
|
64
|
+
text: options.text,
|
|
65
|
+
}).start();
|
|
66
|
+
try {
|
|
67
|
+
return await work();
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
spinner.stop();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
42
73
|
/**
|
|
43
74
|
* Loads and returns the mailmodo.yaml configuration from the current directory.
|
|
44
75
|
* Exits with an error if the file is not found, directing the user to run init.
|
|
@@ -57,17 +88,48 @@ export class BaseCommand extends Command {
|
|
|
57
88
|
* Handles a failed API response by mapping HTTP status codes to
|
|
58
89
|
* user-friendly error messages and exiting the process.
|
|
59
90
|
*
|
|
60
|
-
* @param {{ status: number; error?: string }} response - The API response object with ok=false.
|
|
91
|
+
* @param {{ status: number; error?: string; debug?: ApiRequestDebugInfo }} response - The API response object with ok=false.
|
|
61
92
|
* Status 401 prompts re-authentication, 429 indicates rate limiting,
|
|
62
93
|
* all others display the server's error message or a generic fallback.
|
|
94
|
+
* When `debug` is present, extra lines list Full URL, Status, optional Response
|
|
95
|
+
* body summary, and optional Error Code (network failures).
|
|
63
96
|
*/
|
|
64
97
|
handleApiError(response) {
|
|
65
98
|
if (response.status === 401) {
|
|
66
|
-
this.error(`Invalid API key. Run ${chalk.cyan('mailmodo login')} to re-authenticate
|
|
99
|
+
this.error(this.formatApiFailure(`Invalid API key. Run ${chalk.cyan('mailmodo login')} to re-authenticate.`, response));
|
|
67
100
|
}
|
|
68
101
|
if (response.status === 429) {
|
|
69
|
-
this.error('Rate limit exceeded. Please try again later.');
|
|
102
|
+
this.error(this.formatApiFailure('Rate limit exceeded. Please try again later.', response));
|
|
103
|
+
}
|
|
104
|
+
this.error(this.formatApiFailure(response.error || 'An unexpected API error occurred.', response));
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Builds the terminal error string for a failed API call, appending request
|
|
108
|
+
* metadata when {@link ApiRequestDebugInfo} is available.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} message - Primary error text (HTTP message or generic).
|
|
111
|
+
* @param {{ status: number; debug?: ApiRequestDebugInfo }} response - Failed API response.
|
|
112
|
+
* @returns {string} Message plus indented Request details for troubleshooting.
|
|
113
|
+
*/
|
|
114
|
+
formatApiFailure(message, response) {
|
|
115
|
+
const { debug, status } = response;
|
|
116
|
+
if (!debug) {
|
|
117
|
+
return message;
|
|
70
118
|
}
|
|
71
|
-
|
|
119
|
+
return [
|
|
120
|
+
message,
|
|
121
|
+
'',
|
|
122
|
+
chalk.dim('Request:'),
|
|
123
|
+
chalk.dim(` URL: ${debug.fullUrl}`),
|
|
124
|
+
status > 0
|
|
125
|
+
? chalk.dim(` Status: ${String(status)}`)
|
|
126
|
+
: chalk.dim(' Status: (No HTTP response — network or client error)'),
|
|
127
|
+
...(debug.responseSummary
|
|
128
|
+
? [chalk.dim(` Response: ${debug.responseSummary}`)]
|
|
129
|
+
: []),
|
|
130
|
+
...(debug.causeCode
|
|
131
|
+
? [chalk.dim(` Error Code: ${debug.causeCode}`)]
|
|
132
|
+
: []),
|
|
133
|
+
].join('\n');
|
|
72
134
|
}
|
|
73
135
|
}
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ export declare function loadConfig(): Promise<MailmodoConfig | null>;
|
|
|
22
22
|
* at minimum an apiKey. Optional fields: email, accountName, freeRemaining.
|
|
23
23
|
*/
|
|
24
24
|
export declare function saveConfig(config: MailmodoConfig): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Deletes the saved CLI config file (~/.mailmodo/config), removing the stored API key.
|
|
27
|
+
* No-op if the file does not exist.
|
|
28
|
+
*/
|
|
29
|
+
export declare function clearConfig(): Promise<void>;
|
|
25
30
|
/**
|
|
26
31
|
* Returns the absolute path to the Mailmodo config directory (~/.mailmodo).
|
|
27
32
|
*
|
package/dist/lib/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
const CONFIG_DIR = join(homedir(), '.mailmodo');
|
|
@@ -13,7 +13,6 @@ const CONFIG_FILE = join(CONFIG_DIR, 'config');
|
|
|
13
13
|
* or null if the config file does not exist or is corrupted.
|
|
14
14
|
*/
|
|
15
15
|
export async function loadConfig() {
|
|
16
|
-
console.log('Loading config from', CONFIG_FILE);
|
|
17
16
|
if (!existsSync(CONFIG_FILE))
|
|
18
17
|
return null;
|
|
19
18
|
try {
|
|
@@ -38,6 +37,20 @@ export async function saveConfig(config) {
|
|
|
38
37
|
}
|
|
39
38
|
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
40
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Deletes the saved CLI config file (~/.mailmodo/config), removing the stored API key.
|
|
42
|
+
* No-op if the file does not exist.
|
|
43
|
+
*/
|
|
44
|
+
export async function clearConfig() {
|
|
45
|
+
if (!existsSync(CONFIG_FILE))
|
|
46
|
+
return;
|
|
47
|
+
try {
|
|
48
|
+
await unlink(CONFIG_FILE);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Ignore missing file or permission errors; caller can treat as best-effort sign-out.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
41
54
|
/**
|
|
42
55
|
* Returns the absolute path to the Mailmodo config directory (~/.mailmodo).
|
|
43
56
|
*
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
export declare const API_BASE_URL: string;
|
|
2
2
|
export declare const API_ENDPOINTS: Readonly<{
|
|
3
|
-
ANALYTICS: "/analytics";
|
|
4
|
-
ANALYZE: "/analyze";
|
|
5
|
-
ASSETS_LOGO: "/assets/logo";
|
|
6
|
-
AUTH_VALIDATE: "/auth/validate";
|
|
7
|
-
BILLING_CAP: "/billing/cap";
|
|
8
|
-
BILLING_STATUS: "/billing/status";
|
|
9
|
-
CONTACTS: "/contacts";
|
|
10
|
-
CONTACTS_EXPORT: "/contacts/export";
|
|
11
|
-
DOMAIN: "/domain";
|
|
12
|
-
DOMAIN_STATUS: "/domain/status";
|
|
13
|
-
DOMAIN_VERIFY: "/domain/verify";
|
|
14
|
-
EDIT: "/edit";
|
|
15
|
-
EVENTS: "/events";
|
|
16
|
-
GENERATE: "/generate";
|
|
17
|
-
LOGS: "/logs";
|
|
18
|
-
PREVIEW: "/preview";
|
|
19
|
-
SEQUENCES: "/sequences";
|
|
3
|
+
ANALYTICS: "/api/analytics";
|
|
4
|
+
ANALYZE: "/api/analyze";
|
|
5
|
+
ASSETS_LOGO: "/api/assets/logo";
|
|
6
|
+
AUTH_VALIDATE: "/api/auth/validate";
|
|
7
|
+
BILLING_CAP: "/api/billing/cap";
|
|
8
|
+
BILLING_STATUS: "/api/billing/status";
|
|
9
|
+
CONTACTS: "/api/contacts";
|
|
10
|
+
CONTACTS_EXPORT: "/api/contacts/export";
|
|
11
|
+
DOMAIN: "/api/domain";
|
|
12
|
+
DOMAIN_STATUS: "/api/domain/status";
|
|
13
|
+
DOMAIN_VERIFY: "/api/domain/verify";
|
|
14
|
+
EDIT: "/api/edit";
|
|
15
|
+
EVENTS: "/api/events";
|
|
16
|
+
GENERATE: "/api/generate";
|
|
17
|
+
LOGS: "/api/logs";
|
|
18
|
+
PREVIEW: "/api/preview";
|
|
19
|
+
SEQUENCES: "/api/sequences";
|
|
20
20
|
}>;
|
|
21
21
|
export declare const SIGNUP_URL = "https://mailmodo.com/cli";
|
|
22
22
|
export declare const DOCS_URL = "https://mailmodo.com/docs/cli";
|
package/dist/lib/constants.js
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
/** Set by `bin/dev.js` when running the CLI locally (tsx bootstrap). */
|
|
2
|
+
const DEV_API_BASE_URL = 'https://app-vertex-debug.azurewebsites.net';
|
|
3
|
+
const PRODUCTION_API_BASE_URL = 'https://api.mailmodo.com';
|
|
4
|
+
export const API_BASE_URL = process.env.MAILMODO_DEV_TSX
|
|
5
|
+
? DEV_API_BASE_URL
|
|
6
|
+
: PRODUCTION_API_BASE_URL;
|
|
2
7
|
export const API_ENDPOINTS = Object.freeze({
|
|
3
|
-
ANALYTICS: '/analytics',
|
|
4
|
-
ANALYZE: '/analyze',
|
|
5
|
-
ASSETS_LOGO: '/assets/logo',
|
|
6
|
-
AUTH_VALIDATE: '/auth/validate',
|
|
7
|
-
BILLING_CAP: '/billing/cap',
|
|
8
|
-
BILLING_STATUS: '/billing/status',
|
|
9
|
-
CONTACTS: '/contacts',
|
|
10
|
-
CONTACTS_EXPORT: '/contacts/export',
|
|
11
|
-
DOMAIN: '/domain',
|
|
12
|
-
DOMAIN_STATUS: '/domain/status',
|
|
13
|
-
DOMAIN_VERIFY: '/domain/verify',
|
|
14
|
-
EDIT: '/edit',
|
|
15
|
-
EVENTS: '/events',
|
|
16
|
-
GENERATE: '/generate',
|
|
17
|
-
LOGS: '/logs',
|
|
18
|
-
PREVIEW: '/preview',
|
|
19
|
-
SEQUENCES: '/sequences',
|
|
8
|
+
ANALYTICS: '/api/analytics',
|
|
9
|
+
ANALYZE: '/api/analyze',
|
|
10
|
+
ASSETS_LOGO: '/api/assets/logo',
|
|
11
|
+
AUTH_VALIDATE: '/api/auth/validate',
|
|
12
|
+
BILLING_CAP: '/api/billing/cap',
|
|
13
|
+
BILLING_STATUS: '/api/billing/status',
|
|
14
|
+
CONTACTS: '/api/contacts',
|
|
15
|
+
CONTACTS_EXPORT: '/api/contacts/export',
|
|
16
|
+
DOMAIN: '/api/domain',
|
|
17
|
+
DOMAIN_STATUS: '/api/domain/status',
|
|
18
|
+
DOMAIN_VERIFY: '/api/domain/verify',
|
|
19
|
+
EDIT: '/api/edit',
|
|
20
|
+
EVENTS: '/api/events',
|
|
21
|
+
GENERATE: '/api/generate',
|
|
22
|
+
LOGS: '/api/logs',
|
|
23
|
+
PREVIEW: '/api/preview',
|
|
24
|
+
SEQUENCES: '/api/sequences',
|
|
20
25
|
});
|
|
21
26
|
export const SIGNUP_URL = 'https://mailmodo.com/cli';
|
|
22
27
|
export const DOCS_URL = 'https://mailmodo.com/docs/cli';
|
package/oclif.manifest.json
CHANGED
|
@@ -153,14 +153,13 @@
|
|
|
153
153
|
"index.js"
|
|
154
154
|
]
|
|
155
155
|
},
|
|
156
|
-
"
|
|
156
|
+
"emails": {
|
|
157
157
|
"aliases": [],
|
|
158
158
|
"args": {},
|
|
159
|
-
"description": "
|
|
159
|
+
"description": "List and view configured email sequences",
|
|
160
160
|
"examples": [
|
|
161
|
-
"<%= config.bin %>
|
|
162
|
-
"<%= config.bin %>
|
|
163
|
-
"<%= config.bin %> domain --status"
|
|
161
|
+
"<%= config.bin %> emails",
|
|
162
|
+
"<%= config.bin %> emails --json"
|
|
164
163
|
],
|
|
165
164
|
"flags": {
|
|
166
165
|
"json": {
|
|
@@ -175,23 +174,11 @@
|
|
|
175
174
|
"name": "yes",
|
|
176
175
|
"allowNo": false,
|
|
177
176
|
"type": "boolean"
|
|
178
|
-
},
|
|
179
|
-
"status": {
|
|
180
|
-
"description": "Show domain health status",
|
|
181
|
-
"name": "status",
|
|
182
|
-
"allowNo": false,
|
|
183
|
-
"type": "boolean"
|
|
184
|
-
},
|
|
185
|
-
"verify": {
|
|
186
|
-
"description": "Verify DNS records",
|
|
187
|
-
"name": "verify",
|
|
188
|
-
"allowNo": false,
|
|
189
|
-
"type": "boolean"
|
|
190
177
|
}
|
|
191
178
|
},
|
|
192
179
|
"hasDynamicHelp": false,
|
|
193
180
|
"hiddenAliases": [],
|
|
194
|
-
"id": "
|
|
181
|
+
"id": "emails",
|
|
195
182
|
"pluginAlias": "@mailmodo/cli",
|
|
196
183
|
"pluginName": "@mailmodo/cli",
|
|
197
184
|
"pluginType": "core",
|
|
@@ -201,17 +188,23 @@
|
|
|
201
188
|
"relativePath": [
|
|
202
189
|
"dist",
|
|
203
190
|
"commands",
|
|
204
|
-
"
|
|
191
|
+
"emails",
|
|
205
192
|
"index.js"
|
|
206
193
|
]
|
|
207
194
|
},
|
|
208
|
-
"
|
|
195
|
+
"edit": {
|
|
209
196
|
"aliases": [],
|
|
210
|
-
"args": {
|
|
211
|
-
|
|
197
|
+
"args": {
|
|
198
|
+
"id": {
|
|
199
|
+
"description": "Email ID to edit",
|
|
200
|
+
"name": "id",
|
|
201
|
+
"required": true
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
"description": "Edit an email using AI-assisted natural language changes",
|
|
212
205
|
"examples": [
|
|
213
|
-
"<%= config.bin %>
|
|
214
|
-
"<%= config.bin %>
|
|
206
|
+
"<%= config.bin %> edit welcome",
|
|
207
|
+
"<%= config.bin %> edit welcome --change \"make subject more urgent\" --yes"
|
|
215
208
|
],
|
|
216
209
|
"flags": {
|
|
217
210
|
"json": {
|
|
@@ -226,11 +219,18 @@
|
|
|
226
219
|
"name": "yes",
|
|
227
220
|
"allowNo": false,
|
|
228
221
|
"type": "boolean"
|
|
222
|
+
},
|
|
223
|
+
"change": {
|
|
224
|
+
"description": "Natural language description of the change",
|
|
225
|
+
"name": "change",
|
|
226
|
+
"hasDynamicHelp": false,
|
|
227
|
+
"multiple": false,
|
|
228
|
+
"type": "option"
|
|
229
229
|
}
|
|
230
230
|
},
|
|
231
231
|
"hasDynamicHelp": false,
|
|
232
232
|
"hiddenAliases": [],
|
|
233
|
-
"id": "
|
|
233
|
+
"id": "edit",
|
|
234
234
|
"pluginAlias": "@mailmodo/cli",
|
|
235
235
|
"pluginName": "@mailmodo/cli",
|
|
236
236
|
"pluginType": "core",
|
|
@@ -240,23 +240,18 @@
|
|
|
240
240
|
"relativePath": [
|
|
241
241
|
"dist",
|
|
242
242
|
"commands",
|
|
243
|
-
"
|
|
243
|
+
"edit",
|
|
244
244
|
"index.js"
|
|
245
245
|
]
|
|
246
246
|
},
|
|
247
|
-
"
|
|
247
|
+
"domain": {
|
|
248
248
|
"aliases": [],
|
|
249
|
-
"args": {
|
|
250
|
-
|
|
251
|
-
"description": "Email ID to edit",
|
|
252
|
-
"name": "id",
|
|
253
|
-
"required": true
|
|
254
|
-
}
|
|
255
|
-
},
|
|
256
|
-
"description": "Edit an email using AI-assisted natural language changes",
|
|
249
|
+
"args": {},
|
|
250
|
+
"description": "Set up and verify your sending domain",
|
|
257
251
|
"examples": [
|
|
258
|
-
"<%= config.bin %>
|
|
259
|
-
"<%= config.bin %>
|
|
252
|
+
"<%= config.bin %> domain",
|
|
253
|
+
"<%= config.bin %> domain --verify",
|
|
254
|
+
"<%= config.bin %> domain --status"
|
|
260
255
|
],
|
|
261
256
|
"flags": {
|
|
262
257
|
"json": {
|
|
@@ -272,17 +267,22 @@
|
|
|
272
267
|
"allowNo": false,
|
|
273
268
|
"type": "boolean"
|
|
274
269
|
},
|
|
275
|
-
"
|
|
276
|
-
"description": "
|
|
277
|
-
"name": "
|
|
278
|
-
"
|
|
279
|
-
"
|
|
280
|
-
|
|
270
|
+
"status": {
|
|
271
|
+
"description": "Show domain health status",
|
|
272
|
+
"name": "status",
|
|
273
|
+
"allowNo": false,
|
|
274
|
+
"type": "boolean"
|
|
275
|
+
},
|
|
276
|
+
"verify": {
|
|
277
|
+
"description": "Verify DNS records",
|
|
278
|
+
"name": "verify",
|
|
279
|
+
"allowNo": false,
|
|
280
|
+
"type": "boolean"
|
|
281
281
|
}
|
|
282
282
|
},
|
|
283
283
|
"hasDynamicHelp": false,
|
|
284
284
|
"hiddenAliases": [],
|
|
285
|
-
"id": "
|
|
285
|
+
"id": "domain",
|
|
286
286
|
"pluginAlias": "@mailmodo/cli",
|
|
287
287
|
"pluginName": "@mailmodo/cli",
|
|
288
288
|
"pluginType": "core",
|
|
@@ -292,7 +292,7 @@
|
|
|
292
292
|
"relativePath": [
|
|
293
293
|
"dist",
|
|
294
294
|
"commands",
|
|
295
|
-
"
|
|
295
|
+
"domain",
|
|
296
296
|
"index.js"
|
|
297
297
|
]
|
|
298
298
|
},
|
|
@@ -435,6 +435,44 @@
|
|
|
435
435
|
"index.js"
|
|
436
436
|
]
|
|
437
437
|
},
|
|
438
|
+
"logout": {
|
|
439
|
+
"aliases": [],
|
|
440
|
+
"args": {},
|
|
441
|
+
"description": "Sign out by removing saved credentials from this machine",
|
|
442
|
+
"examples": [
|
|
443
|
+
"<%= config.bin %> logout"
|
|
444
|
+
],
|
|
445
|
+
"flags": {
|
|
446
|
+
"json": {
|
|
447
|
+
"description": "Output as JSON",
|
|
448
|
+
"name": "json",
|
|
449
|
+
"allowNo": false,
|
|
450
|
+
"type": "boolean"
|
|
451
|
+
},
|
|
452
|
+
"yes": {
|
|
453
|
+
"char": "y",
|
|
454
|
+
"description": "Skip confirmation prompts",
|
|
455
|
+
"name": "yes",
|
|
456
|
+
"allowNo": false,
|
|
457
|
+
"type": "boolean"
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
"hasDynamicHelp": false,
|
|
461
|
+
"hiddenAliases": [],
|
|
462
|
+
"id": "logout",
|
|
463
|
+
"pluginAlias": "@mailmodo/cli",
|
|
464
|
+
"pluginName": "@mailmodo/cli",
|
|
465
|
+
"pluginType": "core",
|
|
466
|
+
"strict": true,
|
|
467
|
+
"enableJsonFlag": false,
|
|
468
|
+
"isESM": true,
|
|
469
|
+
"relativePath": [
|
|
470
|
+
"dist",
|
|
471
|
+
"commands",
|
|
472
|
+
"logout",
|
|
473
|
+
"index.js"
|
|
474
|
+
]
|
|
475
|
+
},
|
|
438
476
|
"preview": {
|
|
439
477
|
"aliases": [],
|
|
440
478
|
"args": {
|
|
@@ -580,5 +618,5 @@
|
|
|
580
618
|
]
|
|
581
619
|
}
|
|
582
620
|
},
|
|
583
|
-
"version": "0.0.
|
|
621
|
+
"version": "0.0.9-beta.pr11.15"
|
|
584
622
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mailmodo/cli",
|
|
3
3
|
"description": "Email lifecycle automation for the AI-native builder generation.",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.9-beta.pr11.15",
|
|
5
5
|
"author": "provishalk",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mailmodo": "bin/run.js"
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"@oclif/plugin-plugins": "^5",
|
|
15
15
|
"chalk": "^5.6.2",
|
|
16
16
|
"js-yaml": "^4.1.1",
|
|
17
|
-
"open": "^11.0.0"
|
|
17
|
+
"open": "^11.0.0",
|
|
18
|
+
"ora": "^9.3.0"
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|
|
20
21
|
"@eslint/compat": "^1",
|