@mailmodo/cli 0.0.54-beta.pr56.91 → 0.0.54
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 +6 -9
- package/dist/commands/deploy/index.d.ts +32 -1
- package/dist/commands/deploy/index.js +303 -52
- package/dist/commands/edit/index.js +0 -19
- package/dist/commands/init/index.d.ts +0 -1
- package/dist/commands/init/index.js +3 -25
- package/dist/commands/login/index.js +3 -18
- package/dist/commands/preview/index.js +12 -24
- package/dist/commands/settings/index.js +0 -6
- package/dist/lib/api-client.d.ts +0 -5
- package/dist/lib/api-client.js +0 -45
- package/dist/lib/base-command.d.ts +1 -25
- package/dist/lib/base-command.js +5 -91
- package/dist/lib/constants.d.ts +0 -1
- package/dist/lib/constants.js +0 -1
- package/dist/lib/messages.d.ts +0 -22
- package/dist/lib/messages.js +0 -22
- package/oclif.manifest.json +60 -60
- package/package.json +1 -1
- package/dist/lib/deploy/domain-setup.d.ts +0 -8
- package/dist/lib/deploy/domain-setup.js +0 -82
- package/dist/lib/deploy/output.d.ts +0 -5
- package/dist/lib/deploy/output.js +0 -61
- package/dist/lib/deploy/payload.d.ts +0 -41
- package/dist/lib/deploy/payload.js +0 -95
- package/dist/lib/deploy/sequence-status.d.ts +0 -3
- package/dist/lib/deploy/sequence-status.js +0 -56
- package/dist/lib/deploy/types.d.ts +0 -88
- package/dist/lib/deploy/types.js +0 -1
- package/dist/lib/templates/missing-templates.d.ts +0 -5
- package/dist/lib/templates/missing-templates.js +0 -61
- package/dist/lib/templates/types.d.ts +0 -13
- package/dist/lib/templates/types.js +0 -1
|
@@ -68,14 +68,12 @@ export default class Billing extends BaseCommand {
|
|
|
68
68
|
}
|
|
69
69
|
this.log(`\n ${chalk.bold('Stripe Checkout')} — add or update your payment method.`);
|
|
70
70
|
this.log(` ${chalk.dim(checkoutUrl)}\n`);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.log(` ${INFO.BROWSER_OPEN_FAILED}\n`);
|
|
78
|
-
}
|
|
71
|
+
try {
|
|
72
|
+
await open(checkoutUrl);
|
|
73
|
+
this.log(` ${INFO.BROWSER_OPENING}\n`);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
this.log(` ${INFO.BROWSER_OPEN_FAILED}\n`);
|
|
79
77
|
}
|
|
80
78
|
}
|
|
81
79
|
async showStatus(jsonOutput, statusOnly) {
|
|
@@ -178,7 +176,6 @@ export default class Billing extends BaseCommand {
|
|
|
178
176
|
return;
|
|
179
177
|
yamlConfig.project.monthlyCap = capBlocks;
|
|
180
178
|
await saveYaml(yamlConfig);
|
|
181
|
-
await this.syncYamlToServer();
|
|
182
179
|
}
|
|
183
180
|
pluralize(word, count) {
|
|
184
181
|
return count === 1 ? word : `${word}s`;
|
|
@@ -8,6 +8,37 @@ export default class Deploy extends BaseCommand {
|
|
|
8
8
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
9
|
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
10
|
};
|
|
11
|
+
private fetchDomainVerifyForDeploy;
|
|
11
12
|
run(): Promise<void>;
|
|
12
|
-
private
|
|
13
|
+
private validateSequence;
|
|
14
|
+
/**
|
|
15
|
+
* Calls `POST /sequences/{id}/status` with `{ status: "paused" }` and prints
|
|
16
|
+
* a confirmation-aware success/no-op message. Skips the prompt entirely when
|
|
17
|
+
* `--yes` is set so the command stays scriptable. `--json` always emits the
|
|
18
|
+
* raw server payload (sequenceId, status, alreadyInStatus).
|
|
19
|
+
*/
|
|
20
|
+
private pauseSequence;
|
|
21
|
+
/**
|
|
22
|
+
* Calls `POST /sequences/{id}/status` with `{ status: "active" }`. No prompt
|
|
23
|
+
* — resuming is the safe direction (it does not start sends that weren't
|
|
24
|
+
* already queued). `--json` always emits the raw server payload (sequenceId,
|
|
25
|
+
* status, alreadyInStatus).
|
|
26
|
+
*/
|
|
27
|
+
private resumeSequence;
|
|
28
|
+
private updateSequenceStatus;
|
|
29
|
+
private sequenceStatusPath;
|
|
30
|
+
private buildDeployPayload;
|
|
31
|
+
private resolveMonthlyCapForDeploy;
|
|
32
|
+
private mapEmailToPayload;
|
|
33
|
+
private buildBrandSection;
|
|
34
|
+
private buildProductSection;
|
|
35
|
+
private buildSenderSection;
|
|
36
|
+
private buildProjectPayload;
|
|
37
|
+
private confirmDeploy;
|
|
38
|
+
private ensureDomainReady;
|
|
39
|
+
private logPreDeploySummary;
|
|
40
|
+
private logDiff;
|
|
41
|
+
private logDeploySuccessInstructions;
|
|
42
|
+
private runDomainSetup;
|
|
43
|
+
private verifyDomain;
|
|
13
44
|
}
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
-
import { confirm } from '@inquirer/prompts';
|
|
2
|
+
import { confirm, input } from '@inquirer/prompts';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
|
-
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { logDeploySuccessInstructions, logPreDeploySummary, } from '../../lib/deploy/output.js';
|
|
9
|
-
import { pauseSequence, resumeSequence, } from '../../lib/deploy/sequence-status.js';
|
|
10
|
-
import { ensureDomainReady, validateDeploySequence, } from '../../lib/deploy/domain-setup.js';
|
|
11
|
-
import { getMissingTemplateIds, handleMissingTemplates, } from '../../lib/templates/missing-templates.js';
|
|
5
|
+
import { API_ENDPOINTS, DEFAULT_BRAND_COLOR, SDK_IMPORT_SNIPPET, SDK_INSTALL_COMMAND, } from '../../lib/constants.js';
|
|
6
|
+
import { ERRORS, INFO, pauseAlready, pauseSuccess, PROMPTS, resumeAlready, resumeSuccess, SEPARATOR, } from '../../lib/messages.js';
|
|
7
|
+
import { loadTemplate, } from '../../lib/yaml-config.js';
|
|
12
8
|
export default class Deploy extends BaseCommand {
|
|
13
9
|
static description = 'Deploy, pause, or resume an email sequence';
|
|
14
10
|
static examples = [
|
|
@@ -28,47 +24,37 @@ export default class Deploy extends BaseCommand {
|
|
|
28
24
|
exclusive: ['pause'],
|
|
29
25
|
}),
|
|
30
26
|
};
|
|
27
|
+
fetchDomainVerifyForDeploy(jsonOutput, domain) {
|
|
28
|
+
return this.withApiSpinner({ json: jsonOutput, text: ' Checking domain verification...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY, {
|
|
29
|
+
domain: domain || '',
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
31
32
|
async run() {
|
|
32
33
|
const { flags } = await this.parse(Deploy);
|
|
33
34
|
await this.ensureAuth();
|
|
34
|
-
const ctx = this.makeCtx();
|
|
35
35
|
const baseFlags = { json: flags.json, yes: flags.yes };
|
|
36
36
|
if (flags.pause) {
|
|
37
|
-
await pauseSequence(
|
|
37
|
+
await this.pauseSequence(flags.pause, baseFlags);
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
40
|
if (flags.resume) {
|
|
41
|
-
await resumeSequence(
|
|
41
|
+
await this.resumeSequence(flags.resume, baseFlags);
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
const yamlConfig = await this.ensureYaml();
|
|
45
|
-
const
|
|
46
|
-
if (missingIds.length > 0) {
|
|
47
|
-
const regenerated = await handleMissingTemplates(ctx, yamlConfig, missingIds, baseFlags);
|
|
48
|
-
if (regenerated)
|
|
49
|
-
ctx.log(`\n ${chalk.green('✓')} ${MISSING_TEMPLATES.REVIEW_HINT}\n`);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
const domainReady = await ensureDomainReady(ctx, yamlConfig, baseFlags);
|
|
45
|
+
const domainReady = await this.ensureDomainReady(yamlConfig, flags);
|
|
53
46
|
if (!domainReady)
|
|
54
47
|
return;
|
|
55
|
-
const payload = await buildDeployPayload(
|
|
56
|
-
const validateResult = await
|
|
57
|
-
logPreDeploySummary(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this.log('\n Deploy cancelled.\n');
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
48
|
+
const payload = await this.buildDeployPayload(yamlConfig);
|
|
49
|
+
const validateResult = await this.validateSequence(payload, flags);
|
|
50
|
+
this.logPreDeploySummary(yamlConfig, validateResult, flags.json);
|
|
51
|
+
const confirmed = await this.confirmDeploy(yamlConfig, flags);
|
|
52
|
+
if (!confirmed)
|
|
53
|
+
return;
|
|
54
|
+
const response = await this.withApiSpinner({ json: flags.json, text: ' Deploying email sequences...' }, () => this.apiClient.post(API_ENDPOINTS.SEQUENCES_DEPLOY, payload));
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
this.handleApiError(response);
|
|
67
57
|
}
|
|
68
|
-
const response = await ctx.spinner(' Deploying email sequences...', flags.json, () => ctx.post(API_ENDPOINTS.SEQUENCES_DEPLOY, payload));
|
|
69
|
-
if (!response.ok)
|
|
70
|
-
ctx.onApiError(response);
|
|
71
|
-
await ctx.syncYaml();
|
|
72
58
|
if (flags.json) {
|
|
73
59
|
this.log(JSON.stringify({
|
|
74
60
|
deployed: response.data.deployed,
|
|
@@ -79,25 +65,290 @@ export default class Deploy extends BaseCommand {
|
|
|
79
65
|
}, null, 2));
|
|
80
66
|
return;
|
|
81
67
|
}
|
|
82
|
-
logDeploySuccessInstructions(
|
|
68
|
+
this.logDeploySuccessInstructions(response.data.sdkSnippet);
|
|
83
69
|
}
|
|
84
|
-
|
|
70
|
+
async validateSequence(payload, flags) {
|
|
71
|
+
const response = await this.withApiSpinner({ json: flags.json, text: ' Validating sequence...' }, () => this.apiClient.post(API_ENDPOINTS.SEQUENCES_VALIDATE, payload));
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
if (response.data.error === 'senderDomainNotFound') {
|
|
74
|
+
this.error(ERRORS.DOMAIN_NOT_REGISTERED);
|
|
75
|
+
}
|
|
76
|
+
if (response.data.error === 'senderDomainNotVerified') {
|
|
77
|
+
this.error(ERRORS.DOMAIN_NOT_VERIFIED);
|
|
78
|
+
}
|
|
79
|
+
this.handleApiError(response);
|
|
80
|
+
}
|
|
81
|
+
return response.data;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Calls `POST /sequences/{id}/status` with `{ status: "paused" }` and prints
|
|
85
|
+
* a confirmation-aware success/no-op message. Skips the prompt entirely when
|
|
86
|
+
* `--yes` is set so the command stays scriptable. `--json` always emits the
|
|
87
|
+
* raw server payload (sequenceId, status, alreadyInStatus).
|
|
88
|
+
*/
|
|
89
|
+
async pauseSequence(sequenceId, flags) {
|
|
90
|
+
if (!flags.yes) {
|
|
91
|
+
const confirmed = await confirm({
|
|
92
|
+
default: false,
|
|
93
|
+
message: PROMPTS.PAUSE_CONFIRM,
|
|
94
|
+
});
|
|
95
|
+
if (!confirmed) {
|
|
96
|
+
this.log(`\n ${INFO.PAUSE_CANCELLED}\n`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const data = await this.updateSequenceStatus(sequenceId, 'paused', flags, ' Pausing sequence...');
|
|
101
|
+
if (flags.json) {
|
|
102
|
+
this.log(JSON.stringify(data, null, 2));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const message = data.alreadyInStatus
|
|
106
|
+
? pauseAlready(data.sequenceId || sequenceId)
|
|
107
|
+
: pauseSuccess(data.sequenceId || sequenceId);
|
|
108
|
+
this.log(`\n ${message}\n`);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Calls `POST /sequences/{id}/status` with `{ status: "active" }`. No prompt
|
|
112
|
+
* — resuming is the safe direction (it does not start sends that weren't
|
|
113
|
+
* already queued). `--json` always emits the raw server payload (sequenceId,
|
|
114
|
+
* status, alreadyInStatus).
|
|
115
|
+
*/
|
|
116
|
+
async resumeSequence(sequenceId, flags) {
|
|
117
|
+
const data = await this.updateSequenceStatus(sequenceId, 'active', flags, ' Resuming sequence...');
|
|
118
|
+
if (flags.json) {
|
|
119
|
+
this.log(JSON.stringify(data, null, 2));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const message = data.alreadyInStatus
|
|
123
|
+
? resumeAlready(data.sequenceId || sequenceId)
|
|
124
|
+
: resumeSuccess(data.sequenceId || sequenceId);
|
|
125
|
+
this.log(`\n ${message}\n`);
|
|
126
|
+
}
|
|
127
|
+
async updateSequenceStatus(sequenceId, status, flags, spinnerText) {
|
|
128
|
+
const response = await this.withApiSpinner({ json: flags.json, text: spinnerText }, () => this.apiClient.post(this.sequenceStatusPath(sequenceId), { status }));
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
this.handleApiError(response);
|
|
131
|
+
}
|
|
132
|
+
return response.data;
|
|
133
|
+
}
|
|
134
|
+
sequenceStatusPath(sequenceId) {
|
|
135
|
+
return `${API_ENDPOINTS.SEQUENCES}/${encodeURIComponent(sequenceId)}/status`;
|
|
136
|
+
}
|
|
137
|
+
async buildDeployPayload(yamlConfig) {
|
|
138
|
+
const [emailsWithHtml, monthlyCap] = await Promise.all([
|
|
139
|
+
Promise.all(yamlConfig.emails.map(async (email) => {
|
|
140
|
+
const html = (await loadTemplate(`${email.id}.html`)) || '';
|
|
141
|
+
const plainHtml = (await loadTemplate(`${email.id}_plain.html`)) || html;
|
|
142
|
+
return { ...this.mapEmailToPayload(email), html, plainHtml };
|
|
143
|
+
})),
|
|
144
|
+
this.resolveMonthlyCapForDeploy(yamlConfig.project.monthlyCap),
|
|
145
|
+
]);
|
|
146
|
+
return {
|
|
147
|
+
...this.buildProjectPayload(yamlConfig.project, monthlyCap),
|
|
148
|
+
emails: emailsWithHtml,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
async resolveMonthlyCapForDeploy(yamlMonthlyCap) {
|
|
152
|
+
if (yamlMonthlyCap !== undefined)
|
|
153
|
+
return yamlMonthlyCap;
|
|
154
|
+
const billingStatus = await this.fetchBillingStatus();
|
|
155
|
+
return billingStatus?.cap?.inBlocks ?? undefined;
|
|
156
|
+
}
|
|
157
|
+
mapEmailToPayload(email) {
|
|
158
|
+
return {
|
|
159
|
+
condition: email.condition || null,
|
|
160
|
+
ctaText: email.ctaText || '',
|
|
161
|
+
delay: email.delay,
|
|
162
|
+
goal: email.goal || '',
|
|
163
|
+
id: email.id,
|
|
164
|
+
isReminder: false,
|
|
165
|
+
previewText: email.previewText || '',
|
|
166
|
+
priority: 'medium',
|
|
167
|
+
subject: email.subject,
|
|
168
|
+
trigger: email.trigger,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
buildBrandSection(project) {
|
|
85
172
|
return {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
exit: (code) => this.exit(code),
|
|
89
|
-
get: (path, params) => this.apiClient.get(path, params),
|
|
90
|
-
getBillingCap: async () => {
|
|
91
|
-
const s = await this.fetchBillingStatus();
|
|
92
|
-
return s?.cap?.inBlocks ?? undefined;
|
|
93
|
-
},
|
|
94
|
-
log: (msg) => this.log(msg),
|
|
95
|
-
onApiError: (r) => this.handleApiError(r),
|
|
96
|
-
post: (path, body) => this.apiClient.post(path, body),
|
|
97
|
-
registerDomainAndSave: (yaml, inputs, json) => this.registerDomain(yaml, inputs, json),
|
|
98
|
-
showDnsRecords: (records, url, json) => this.logDnsRecords(records, url, json),
|
|
99
|
-
spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
|
|
100
|
-
syncYaml: () => this.syncYamlToServer(),
|
|
173
|
+
colors: [project?.brandColor || DEFAULT_BRAND_COLOR],
|
|
174
|
+
logoUrl: project?.logoUrl || '',
|
|
101
175
|
};
|
|
102
176
|
}
|
|
177
|
+
buildProductSection(project) {
|
|
178
|
+
return {
|
|
179
|
+
businessType: project?.type || '',
|
|
180
|
+
description: project?.description || '',
|
|
181
|
+
pricingModel: project?.pricingModel || '',
|
|
182
|
+
productName: project?.name || '',
|
|
183
|
+
saasModel: project?.saasModel || '',
|
|
184
|
+
targetUser: project?.targetUser || '',
|
|
185
|
+
url: project?.url || '',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
buildSenderSection(project) {
|
|
189
|
+
return {
|
|
190
|
+
address: project?.address || '',
|
|
191
|
+
domain: project?.domain || '',
|
|
192
|
+
fromEmail: project?.fromEmail || '',
|
|
193
|
+
fromName: project?.fromName || '',
|
|
194
|
+
replyTo: project?.replyTo || project?.fromEmail || '',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
buildProjectPayload(project, monthlyCap) {
|
|
198
|
+
return {
|
|
199
|
+
brand: this.buildBrandSection(project),
|
|
200
|
+
emailStyle: project?.emailStyle || 'branded',
|
|
201
|
+
...(monthlyCap === undefined ? {} : { monthlyCap }),
|
|
202
|
+
product: this.buildProductSection(project),
|
|
203
|
+
senderDetails: this.buildSenderSection(project),
|
|
204
|
+
...(project?.webhookUrl ? { webhookUrl: project.webhookUrl } : {}),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
async confirmDeploy(yamlConfig, flags) {
|
|
208
|
+
if (flags.yes)
|
|
209
|
+
return true;
|
|
210
|
+
const proceed = await confirm({
|
|
211
|
+
default: true,
|
|
212
|
+
message: `Deploy ${yamlConfig.emails.length} emails?`,
|
|
213
|
+
});
|
|
214
|
+
if (!proceed) {
|
|
215
|
+
this.log('\n Deploy cancelled.\n');
|
|
216
|
+
}
|
|
217
|
+
return proceed;
|
|
218
|
+
}
|
|
219
|
+
async ensureDomainReady(yamlConfig, flags) {
|
|
220
|
+
const domainVerify = await this.fetchDomainVerifyForDeploy(flags.json, yamlConfig.project?.domain);
|
|
221
|
+
if (domainVerify.ok && domainVerify.data?.domainStatus === 'VERIFIED') {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
if (yamlConfig.project?.domain) {
|
|
225
|
+
if (!flags.json) {
|
|
226
|
+
this.log(`\n ${INFO.DOMAIN_PENDING_VERIFICATION}\n`);
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
if (!flags.json) {
|
|
231
|
+
this.log(`\n No sending domain set up yet.`);
|
|
232
|
+
this.log(` You need a verified domain before sending emails.`);
|
|
233
|
+
this.log(` This is a one-time setup. Takes about 5 minutes.\n`);
|
|
234
|
+
}
|
|
235
|
+
if (!flags.yes) {
|
|
236
|
+
const setupNow = await confirm({
|
|
237
|
+
default: true,
|
|
238
|
+
message: 'Set up your sending domain now?',
|
|
239
|
+
});
|
|
240
|
+
if (!setupNow) {
|
|
241
|
+
this.log(`\n ${INFO.SEQUENCES_NOT_DEPLOYED}`);
|
|
242
|
+
this.log(` Emails will not send until your domain is verified.`);
|
|
243
|
+
this.log(` ${INFO.DOMAIN_NOT_DEPLOYED_HINT}\n`);
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return this.runDomainSetup(yamlConfig, flags);
|
|
248
|
+
}
|
|
249
|
+
logPreDeploySummary(yamlConfig, validateResult, jsonOutput) {
|
|
250
|
+
if (jsonOutput)
|
|
251
|
+
return;
|
|
252
|
+
this.log(`\n ${chalk.green('✓')} Domain: ${yamlConfig.project?.domain || 'verified'}\n`);
|
|
253
|
+
if (!validateResult.existingDeployment || !validateResult.diff) {
|
|
254
|
+
this.log(` Deploying:`);
|
|
255
|
+
for (const email of yamlConfig.emails) {
|
|
256
|
+
this.log(` ${chalk.green('+')} ${email.id.padEnd(24)} ${email.trigger}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
this.logDiff(validateResult.diff);
|
|
261
|
+
}
|
|
262
|
+
this.log('');
|
|
263
|
+
}
|
|
264
|
+
logDiff(diff) {
|
|
265
|
+
if (!diff.hasChanges) {
|
|
266
|
+
this.log(` No changes from last deployment.`);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
this.log(` Changes vs. last deployment:`);
|
|
270
|
+
for (const email of diff.added) {
|
|
271
|
+
this.log(` ${chalk.green('+')} ${email.id.padEnd(24)} ${email.trigger || ''}`);
|
|
272
|
+
}
|
|
273
|
+
for (const email of diff.removed) {
|
|
274
|
+
this.log(` ${chalk.red('-')} ${email.id.padEnd(24)} ${email.trigger || ''}`);
|
|
275
|
+
}
|
|
276
|
+
for (const email of diff.modified) {
|
|
277
|
+
const fields = email.changedFields?.join(', ') || '';
|
|
278
|
+
this.log(` ${chalk.yellow('~')} ${email.id.padEnd(24)} ${fields}`);
|
|
279
|
+
}
|
|
280
|
+
if (diff.unchanged.length > 0) {
|
|
281
|
+
this.log(` ${chalk.dim(`∙ ${diff.unchanged.length} unchanged`)}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
logDeploySuccessInstructions(sdkSnippet) {
|
|
285
|
+
this.log(` ${chalk.green('Deployed.')} Emails are live.\n`);
|
|
286
|
+
this.log(` ${SEPARATOR}`);
|
|
287
|
+
this.log(` ${chalk.bold('ADD THIS TO YOUR APP (one-time only):')}`);
|
|
288
|
+
this.log(` ${SEPARATOR}\n`);
|
|
289
|
+
this.log(` ${chalk.cyan(sdkSnippet.install ?? SDK_INSTALL_COMMAND)}\n`);
|
|
290
|
+
this.log(` ${chalk.dim(SDK_IMPORT_SNIPPET)}\n`);
|
|
291
|
+
if (sdkSnippet.examples) {
|
|
292
|
+
this.log(` ${chalk.dim('// Example usage:')}`);
|
|
293
|
+
this.log(` ${chalk.dim(sdkSnippet.examples.track)}`);
|
|
294
|
+
this.log(` ${chalk.dim(sdkSnippet.examples.identify)}\n`);
|
|
295
|
+
}
|
|
296
|
+
const trackCalls = [...new Set(sdkSnippet.trackCalls ?? [])];
|
|
297
|
+
for (const call of trackCalls) {
|
|
298
|
+
this.log(` ${chalk.dim(call)}`);
|
|
299
|
+
}
|
|
300
|
+
if (trackCalls.length > 0)
|
|
301
|
+
this.log('');
|
|
302
|
+
const identifyCalls = [...new Set(sdkSnippet.identifyCalls ?? [])];
|
|
303
|
+
for (const call of identifyCalls) {
|
|
304
|
+
this.log(` ${chalk.dim(call)}`);
|
|
305
|
+
}
|
|
306
|
+
if (identifyCalls.length > 0)
|
|
307
|
+
this.log('');
|
|
308
|
+
this.log(` Full SDK docs: ${chalk.cyan('mailmodo.com/docs/sdk')}\n`);
|
|
309
|
+
this.log(` ${SEPARATOR}\n`);
|
|
310
|
+
}
|
|
311
|
+
async runDomainSetup(yamlConfig, flags) {
|
|
312
|
+
const inputs = await this.collectDomainSetupInputs(yamlConfig, flags.yes);
|
|
313
|
+
const { dnsRecords, dnsGuideUrl } = await this.registerDomain(yamlConfig, inputs, flags.json);
|
|
314
|
+
this.logDnsRecords(dnsRecords, dnsGuideUrl, flags.json);
|
|
315
|
+
if (flags.yes) {
|
|
316
|
+
return this.verifyDomain(flags.json, inputs.domain);
|
|
317
|
+
}
|
|
318
|
+
const action = await input({
|
|
319
|
+
default: '',
|
|
320
|
+
message: PROMPTS.ENTER_AFTER_RECORDS,
|
|
321
|
+
});
|
|
322
|
+
if (action.toLowerCase() === 'skip') {
|
|
323
|
+
this.log(`\n ${INFO.SEQUENCES_NOT_DEPLOYED}`);
|
|
324
|
+
this.log(` ${INFO.DOMAIN_NOT_DEPLOYED_HINT}\n`);
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
return this.verifyDomain(flags.json, inputs.domain);
|
|
328
|
+
}
|
|
329
|
+
async verifyDomain(jsonOutput, domain) {
|
|
330
|
+
const verify = await this.withApiSpinner({ json: jsonOutput, text: ' Checking DNS...' }, () => this.apiClient.get(API_ENDPOINTS.DOMAIN_VERIFY, {
|
|
331
|
+
domain,
|
|
332
|
+
}));
|
|
333
|
+
if (!verify.ok) {
|
|
334
|
+
this.handleApiError(verify);
|
|
335
|
+
}
|
|
336
|
+
const { dkim, dmarc, dnsGuideUrl, domainStatus, returnPath } = verify.data;
|
|
337
|
+
const allPassed = domainStatus === 'VERIFIED';
|
|
338
|
+
if (!jsonOutput) {
|
|
339
|
+
this.log(` DKIM ${dkim ? chalk.green('✓') : chalk.red('✗')}`);
|
|
340
|
+
this.log(` DMARC ${dmarc ? chalk.green('✓') : chalk.red('✗')}`);
|
|
341
|
+
this.log(` Return-Path ${returnPath ? chalk.green('✓') : chalk.red('✗')}`);
|
|
342
|
+
if (allPassed) {
|
|
343
|
+
this.log(`\n ${chalk.green('Domain verified.')} Continuing deploy...\n`);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
this.log(`\n ${INFO.DNS_RECORDS_FAILED}`);
|
|
347
|
+
this.log(`\n ${INFO.DNS_FIX_AND_VERIFY}`);
|
|
348
|
+
if (dnsGuideUrl)
|
|
349
|
+
this.log(` Help: ${chalk.cyan(dnsGuideUrl)}\n`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return allPassed;
|
|
353
|
+
}
|
|
103
354
|
}
|
|
@@ -4,7 +4,6 @@ import chalk from 'chalk';
|
|
|
4
4
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
5
|
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
6
6
|
import { loadTemplate, getTemplateFilename, saveTemplate, saveYaml, } from '../../lib/yaml-config.js';
|
|
7
|
-
import { handleMissingTemplates } from '../../lib/templates/missing-templates.js';
|
|
8
7
|
export default class Edit extends BaseCommand {
|
|
9
8
|
static args = {
|
|
10
9
|
id: Args.string({
|
|
@@ -44,23 +43,6 @@ export default class Edit extends BaseCommand {
|
|
|
44
43
|
json: flags.json ?? false,
|
|
45
44
|
yes: flags.yes ?? false,
|
|
46
45
|
};
|
|
47
|
-
if (!ctx.templateHtml) {
|
|
48
|
-
const regenCtx = {
|
|
49
|
-
error: (msg) => this.error(msg),
|
|
50
|
-
exit: (code) => this.exit(code),
|
|
51
|
-
log: (msg) => this.log(msg),
|
|
52
|
-
onApiError: (r) => this.handleApiError(r),
|
|
53
|
-
post: (path, body) => this.apiClient.post(path, body),
|
|
54
|
-
spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
|
|
55
|
-
syncYaml: () => this.syncYamlToServer(),
|
|
56
|
-
};
|
|
57
|
-
const regenerated = await handleMissingTemplates(regenCtx, yamlConfig, [email.id], editFlags);
|
|
58
|
-
if (!regenerated)
|
|
59
|
-
return;
|
|
60
|
-
ctx.templateHtml = await loadTemplate(templateFilename);
|
|
61
|
-
if (!ctx.templateHtml)
|
|
62
|
-
this.error('Template regeneration failed.');
|
|
63
|
-
}
|
|
64
46
|
const initialChange = flags.change?.trim() || (await this.askChangeDescription());
|
|
65
47
|
await this.runEditStep(ctx, initialChange, editFlags);
|
|
66
48
|
}
|
|
@@ -143,7 +125,6 @@ export default class Edit extends BaseCommand {
|
|
|
143
125
|
if (updated.html) {
|
|
144
126
|
await saveTemplate(ctx.templateFilename, updated.html);
|
|
145
127
|
}
|
|
146
|
-
await this.syncYamlToServer();
|
|
147
128
|
}
|
|
148
129
|
logJsonResult(email, updated, oldSubject) {
|
|
149
130
|
this.log(JSON.stringify({
|
|
@@ -35,8 +35,7 @@ export default class Init extends BaseCommand {
|
|
|
35
35
|
async run() {
|
|
36
36
|
const { flags } = await this.parse(Init);
|
|
37
37
|
await this.ensureAuth();
|
|
38
|
-
|
|
39
|
-
if (!(await this.confirmOverwriteIfNeeded(flags, existing)))
|
|
38
|
+
if (!(await this.confirmOverwriteIfNeeded(flags)))
|
|
40
39
|
return;
|
|
41
40
|
let productUrl = flags.url;
|
|
42
41
|
if (!productUrl) {
|
|
@@ -148,8 +147,6 @@ export default class Init extends BaseCommand {
|
|
|
148
147
|
webhookUrl: '',
|
|
149
148
|
},
|
|
150
149
|
};
|
|
151
|
-
if (existing)
|
|
152
|
-
this.preserveUserFields(yamlConfig, existing);
|
|
153
150
|
await saveYaml(yamlConfig);
|
|
154
151
|
await this.persistMonthlyCap(yamlConfig);
|
|
155
152
|
const templateSaves = analysisPayload.recommendedEmails.flatMap((rec, index) => {
|
|
@@ -164,7 +161,6 @@ export default class Init extends BaseCommand {
|
|
|
164
161
|
return saves;
|
|
165
162
|
});
|
|
166
163
|
await Promise.all(templateSaves);
|
|
167
|
-
await this.syncYamlToServer();
|
|
168
164
|
if (flags.json) {
|
|
169
165
|
this.log(JSON.stringify({
|
|
170
166
|
brandDetected: analysisPayload.brand,
|
|
@@ -185,26 +181,8 @@ export default class Init extends BaseCommand {
|
|
|
185
181
|
await saveYaml(yamlConfig);
|
|
186
182
|
}
|
|
187
183
|
}
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
if (p.fromEmail)
|
|
191
|
-
config.project.fromEmail = p.fromEmail;
|
|
192
|
-
if (p.replyTo)
|
|
193
|
-
config.project.replyTo = p.replyTo;
|
|
194
|
-
if (p.fromName)
|
|
195
|
-
config.project.fromName = p.fromName;
|
|
196
|
-
if (p.webhookUrl)
|
|
197
|
-
config.project.webhookUrl = p.webhookUrl;
|
|
198
|
-
if (p.emailStyle)
|
|
199
|
-
config.project.emailStyle = p.emailStyle;
|
|
200
|
-
if (p.domain)
|
|
201
|
-
config.project.domain = p.domain;
|
|
202
|
-
if (p.address)
|
|
203
|
-
config.project.address = p.address;
|
|
204
|
-
if (p.logoFile)
|
|
205
|
-
config.project.logoFile = p.logoFile;
|
|
206
|
-
}
|
|
207
|
-
async confirmOverwriteIfNeeded(flags, existing) {
|
|
184
|
+
async confirmOverwriteIfNeeded(flags) {
|
|
185
|
+
const existing = await loadYaml();
|
|
208
186
|
if (!existing)
|
|
209
187
|
return true;
|
|
210
188
|
if (flags.yes)
|
|
@@ -27,14 +27,11 @@ export default class Login extends BaseCommand {
|
|
|
27
27
|
if (!envKey) {
|
|
28
28
|
const existing = await loadConfig();
|
|
29
29
|
if (existing?.apiKey) {
|
|
30
|
-
const existingClient = new ApiClient(existing.apiKey);
|
|
31
|
-
const yamlRestored = await this.recoverYamlAfterLogin(existingClient);
|
|
32
30
|
if (flags.json) {
|
|
33
31
|
this.log(JSON.stringify({
|
|
34
32
|
email: existing.email ?? null,
|
|
35
33
|
status: 'already_logged_in',
|
|
36
34
|
totalFreeRemaining: existing.totalFreeRemaining ?? null,
|
|
37
|
-
yamlRestored,
|
|
38
35
|
}, null, 2));
|
|
39
36
|
return;
|
|
40
37
|
}
|
|
@@ -43,13 +40,8 @@ export default class Login extends BaseCommand {
|
|
|
43
40
|
? chalk.green(existing.email.trim())
|
|
44
41
|
: chalk.dim('(unknown)');
|
|
45
42
|
this.log(` Email: ${emailDisplay}\n`);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
this.log(` ${chalk.dim('1.')} Run ${chalk.cyan('mailmodo init')} to generate an email sequence.`);
|
|
51
|
-
}
|
|
52
|
-
this.log(` ${chalk.dim(yamlRestored ? '1.' : '2.')} Run ${chalk.cyan('mailmodo logout')} to log in with another account.\n`);
|
|
43
|
+
this.log(` ${chalk.dim('1.')} Run ${chalk.cyan('mailmodo init')} to generate an email sequence.`);
|
|
44
|
+
this.log(` ${chalk.dim('2.')} Run ${chalk.cyan('mailmodo logout')} to log in with another account.\n`);
|
|
53
45
|
return;
|
|
54
46
|
}
|
|
55
47
|
}
|
|
@@ -87,7 +79,6 @@ export default class Login extends BaseCommand {
|
|
|
87
79
|
email,
|
|
88
80
|
totalFreeRemaining,
|
|
89
81
|
});
|
|
90
|
-
const yamlRestored = await this.recoverYamlAfterLogin(client);
|
|
91
82
|
if (flags.json) {
|
|
92
83
|
this.log(JSON.stringify({
|
|
93
84
|
email,
|
|
@@ -95,7 +86,6 @@ export default class Login extends BaseCommand {
|
|
|
95
86
|
totalFreeRemaining,
|
|
96
87
|
paidEmailsRemaining,
|
|
97
88
|
status: 'authenticated',
|
|
98
|
-
yamlRestored,
|
|
99
89
|
}, null, 2));
|
|
100
90
|
return;
|
|
101
91
|
}
|
|
@@ -107,11 +97,6 @@ export default class Login extends BaseCommand {
|
|
|
107
97
|
if (plan === 'paid') {
|
|
108
98
|
this.log(` Current paid block: ${chalk.cyan(String(paidEmailsRemaining))} emails remaining\n`);
|
|
109
99
|
}
|
|
110
|
-
|
|
111
|
-
this.log(` ${INFO.YAML_RESTORED_ON_LOGIN}\n`);
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
this.log(` Next: Run ${chalk.cyan("'mailmodo init'")} to generate your email sequences.\n`);
|
|
115
|
-
}
|
|
100
|
+
this.log(` Next: Run ${chalk.cyan("'mailmodo init'")} to generate your email sequences.\n`);
|
|
116
101
|
}
|
|
117
102
|
}
|