@mailmodo/cli 0.0.52 → 0.0.54-beta.pr56.86
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 +1 -0
- package/dist/commands/deploy/index.d.ts +1 -32
- package/dist/commands/deploy/index.js +49 -304
- package/dist/commands/edit/index.js +1 -0
- package/dist/commands/init/index.js +3 -9
- package/dist/commands/login/index.js +17 -2
- package/dist/commands/sdk/index.d.ts +14 -0
- package/dist/commands/sdk/index.js +74 -0
- package/dist/commands/settings/index.js +7 -10
- package/dist/lib/api-client.d.ts +5 -0
- package/dist/lib/api-client.js +45 -0
- package/dist/lib/base-command.d.ts +24 -1
- package/dist/lib/base-command.js +84 -5
- package/dist/lib/constants.d.ts +5 -0
- package/dist/lib/constants.js +5 -0
- package/dist/lib/deploy/domain-setup.d.ts +8 -0
- package/dist/lib/deploy/domain-setup.js +80 -0
- package/dist/lib/deploy/missing-templates.d.ts +4 -0
- package/dist/lib/deploy/missing-templates.js +57 -0
- package/dist/lib/deploy/output.d.ts +5 -0
- package/dist/lib/deploy/output.js +61 -0
- package/dist/lib/deploy/payload.d.ts +41 -0
- package/dist/lib/deploy/payload.js +95 -0
- package/dist/lib/deploy/sequence-status.d.ts +3 -0
- package/dist/lib/deploy/sequence-status.js +56 -0
- package/dist/lib/deploy/types.d.ts +86 -0
- package/dist/lib/deploy/types.js +1 -0
- package/dist/lib/messages.d.ts +9 -0
- package/dist/lib/messages.js +9 -0
- package/dist/lib/utils.d.ts +11 -0
- package/dist/lib/utils.js +40 -0
- package/oclif.manifest.json +48 -1
- package/package.json +1 -1
|
@@ -176,6 +176,7 @@ export default class Billing extends BaseCommand {
|
|
|
176
176
|
return;
|
|
177
177
|
yamlConfig.project.monthlyCap = capBlocks;
|
|
178
178
|
await saveYaml(yamlConfig);
|
|
179
|
+
await this.syncYamlToServer();
|
|
179
180
|
}
|
|
180
181
|
pluralize(word, count) {
|
|
181
182
|
return count === 1 ? word : `${word}s`;
|
|
@@ -8,37 +8,6 @@ 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;
|
|
12
11
|
run(): Promise<void>;
|
|
13
|
-
private
|
|
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;
|
|
12
|
+
private makeCtx;
|
|
44
13
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
-
import { confirm
|
|
3
|
-
import chalk from 'chalk';
|
|
2
|
+
import { confirm } from '@inquirer/prompts';
|
|
4
3
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
|
-
import { API_ENDPOINTS
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
5
|
+
import { buildDeployPayload } from '../../lib/deploy/payload.js';
|
|
6
|
+
import { logDeploySuccessInstructions, logPreDeploySummary, } from '../../lib/deploy/output.js';
|
|
7
|
+
import { pauseSequence, resumeSequence, } from '../../lib/deploy/sequence-status.js';
|
|
8
|
+
import { ensureDomainReady, validateDeploySequence, } from '../../lib/deploy/domain-setup.js';
|
|
9
|
+
import { getMissingTemplateIds, handleMissingTemplates, } from '../../lib/deploy/missing-templates.js';
|
|
8
10
|
export default class Deploy extends BaseCommand {
|
|
9
11
|
static description = 'Deploy, pause, or resume an email sequence';
|
|
10
12
|
static examples = [
|
|
@@ -24,37 +26,45 @@ export default class Deploy extends BaseCommand {
|
|
|
24
26
|
exclusive: ['pause'],
|
|
25
27
|
}),
|
|
26
28
|
};
|
|
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
|
-
}
|
|
32
29
|
async run() {
|
|
33
30
|
const { flags } = await this.parse(Deploy);
|
|
34
31
|
await this.ensureAuth();
|
|
32
|
+
const ctx = this.makeCtx();
|
|
35
33
|
const baseFlags = { json: flags.json, yes: flags.yes };
|
|
36
34
|
if (flags.pause) {
|
|
37
|
-
await
|
|
35
|
+
await pauseSequence(ctx, flags.pause, baseFlags);
|
|
38
36
|
return;
|
|
39
37
|
}
|
|
40
38
|
if (flags.resume) {
|
|
41
|
-
await
|
|
39
|
+
await resumeSequence(ctx, flags.resume, baseFlags);
|
|
42
40
|
return;
|
|
43
41
|
}
|
|
44
42
|
const yamlConfig = await this.ensureYaml();
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
43
|
+
const missingIds = getMissingTemplateIds(yamlConfig);
|
|
44
|
+
if (missingIds.length > 0) {
|
|
45
|
+
await handleMissingTemplates(ctx, yamlConfig, missingIds, baseFlags);
|
|
47
46
|
return;
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
const confirmed = await this.confirmDeploy(yamlConfig, flags);
|
|
52
|
-
if (!confirmed)
|
|
47
|
+
}
|
|
48
|
+
const domainReady = await ensureDomainReady(ctx, yamlConfig, baseFlags);
|
|
49
|
+
if (!domainReady)
|
|
53
50
|
return;
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
const payload = await buildDeployPayload(ctx, yamlConfig);
|
|
52
|
+
const validateResult = await validateDeploySequence(ctx, payload, baseFlags);
|
|
53
|
+
logPreDeploySummary(ctx, yamlConfig, validateResult, flags.json);
|
|
54
|
+
if (!flags.yes) {
|
|
55
|
+
const proceed = await confirm({
|
|
56
|
+
default: true,
|
|
57
|
+
message: `Deploy ${yamlConfig.emails.length} emails?`,
|
|
58
|
+
});
|
|
59
|
+
if (!proceed) {
|
|
60
|
+
this.log('\n Deploy cancelled.\n');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
57
63
|
}
|
|
64
|
+
const response = await ctx.spinner(' Deploying email sequences...', flags.json, () => ctx.post(API_ENDPOINTS.SEQUENCES_DEPLOY, payload));
|
|
65
|
+
if (!response.ok)
|
|
66
|
+
ctx.onApiError(response);
|
|
67
|
+
await ctx.syncYaml();
|
|
58
68
|
if (flags.json) {
|
|
59
69
|
this.log(JSON.stringify({
|
|
60
70
|
deployed: response.data.deployed,
|
|
@@ -65,290 +75,25 @@ export default class Deploy extends BaseCommand {
|
|
|
65
75
|
}, null, 2));
|
|
66
76
|
return;
|
|
67
77
|
}
|
|
68
|
-
|
|
78
|
+
logDeploySuccessInstructions(ctx, response.data.sdkSnippet);
|
|
69
79
|
}
|
|
70
|
-
|
|
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) {
|
|
80
|
+
makeCtx() {
|
|
172
81
|
return {
|
|
173
|
-
|
|
174
|
-
|
|
82
|
+
collectDomainInputs: (yaml, skip) => this.collectDomainSetupInputs(yaml, skip),
|
|
83
|
+
error: (msg) => this.error(msg),
|
|
84
|
+
exit: (code) => this.exit(code),
|
|
85
|
+
get: (path, params) => this.apiClient.get(path, params),
|
|
86
|
+
getBillingCap: async () => {
|
|
87
|
+
const s = await this.fetchBillingStatus();
|
|
88
|
+
return s?.cap?.inBlocks ?? undefined;
|
|
89
|
+
},
|
|
90
|
+
log: (msg) => this.log(msg),
|
|
91
|
+
onApiError: (r) => this.handleApiError(r),
|
|
92
|
+
post: (path, body) => this.apiClient.post(path, body),
|
|
93
|
+
registerDomainAndSave: (yaml, inputs, json) => this.registerDomain(yaml, inputs, json),
|
|
94
|
+
showDnsRecords: (records, url, json) => this.logDnsRecords(records, url, json),
|
|
95
|
+
spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
|
|
96
|
+
syncYaml: () => this.syncYamlToServer(),
|
|
175
97
|
};
|
|
176
98
|
}
|
|
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 ?? 'npm install @mailmodo/sdk')}\n`);
|
|
290
|
-
this.log(` ${chalk.dim("import { track, identify } from '@mailmodo/sdk'")}\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
|
-
}
|
|
354
99
|
}
|
|
@@ -125,6 +125,7 @@ export default class Edit extends BaseCommand {
|
|
|
125
125
|
if (updated.html) {
|
|
126
126
|
await saveTemplate(ctx.templateFilename, updated.html);
|
|
127
127
|
}
|
|
128
|
+
await this.syncYamlToServer();
|
|
128
129
|
}
|
|
129
130
|
logJsonResult(email, updated, oldSubject) {
|
|
130
131
|
this.log(JSON.stringify({
|
|
@@ -4,14 +4,7 @@ import chalk from 'chalk';
|
|
|
4
4
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
5
|
import { API_ENDPOINTS, DEFAULT_BRAND_COLOR } from '../../lib/constants.js';
|
|
6
6
|
import { loadYaml, saveTemplate, saveYaml, } from '../../lib/yaml-config.js';
|
|
7
|
-
|
|
8
|
-
try {
|
|
9
|
-
return Boolean(new URL(value));
|
|
10
|
-
}
|
|
11
|
-
catch {
|
|
12
|
-
return false;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
7
|
+
import { isValidUrl, normalizeTrigger } from '../../lib/utils.js';
|
|
15
8
|
/**
|
|
16
9
|
* Prints the human-readable analysis summary using the provided line writer.
|
|
17
10
|
* Use stderr when `--json` is set so stdout stays free for machine-readable JSON.
|
|
@@ -124,7 +117,7 @@ export default class Init extends BaseCommand {
|
|
|
124
117
|
return {
|
|
125
118
|
delay: rec.delay || '0',
|
|
126
119
|
id: rec.id,
|
|
127
|
-
trigger: rec.trigger,
|
|
120
|
+
trigger: normalizeTrigger(rec.trigger, analysisPayload.productName),
|
|
128
121
|
...(rec.condition ? { condition: rec.condition } : {}),
|
|
129
122
|
subject: generated?.subject || `Email for ${rec.id}`,
|
|
130
123
|
template: `mailmodo/${rec.id}.html`,
|
|
@@ -168,6 +161,7 @@ export default class Init extends BaseCommand {
|
|
|
168
161
|
return saves;
|
|
169
162
|
});
|
|
170
163
|
await Promise.all(templateSaves);
|
|
164
|
+
await this.syncYamlToServer();
|
|
171
165
|
if (flags.json) {
|
|
172
166
|
this.log(JSON.stringify({
|
|
173
167
|
brandDetected: analysisPayload.brand,
|
|
@@ -27,11 +27,14 @@ 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);
|
|
30
32
|
if (flags.json) {
|
|
31
33
|
this.log(JSON.stringify({
|
|
32
34
|
email: existing.email ?? null,
|
|
33
35
|
status: 'already_logged_in',
|
|
34
36
|
totalFreeRemaining: existing.totalFreeRemaining ?? null,
|
|
37
|
+
yamlRestored,
|
|
35
38
|
}, null, 2));
|
|
36
39
|
return;
|
|
37
40
|
}
|
|
@@ -40,7 +43,12 @@ export default class Login extends BaseCommand {
|
|
|
40
43
|
? chalk.green(existing.email.trim())
|
|
41
44
|
: chalk.dim('(unknown)');
|
|
42
45
|
this.log(` Email: ${emailDisplay}\n`);
|
|
43
|
-
|
|
46
|
+
if (yamlRestored) {
|
|
47
|
+
this.log(` ${INFO.YAML_RESTORED_ON_LOGIN}`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
this.log(` ${chalk.dim('1.')} Run ${chalk.cyan('mailmodo init')} to generate an email sequence.`);
|
|
51
|
+
}
|
|
44
52
|
this.log(` ${chalk.dim('2.')} Run ${chalk.cyan('mailmodo logout')} to log in with another account.\n`);
|
|
45
53
|
return;
|
|
46
54
|
}
|
|
@@ -79,6 +87,7 @@ export default class Login extends BaseCommand {
|
|
|
79
87
|
email,
|
|
80
88
|
totalFreeRemaining,
|
|
81
89
|
});
|
|
90
|
+
const yamlRestored = await this.recoverYamlAfterLogin(client);
|
|
82
91
|
if (flags.json) {
|
|
83
92
|
this.log(JSON.stringify({
|
|
84
93
|
email,
|
|
@@ -86,6 +95,7 @@ export default class Login extends BaseCommand {
|
|
|
86
95
|
totalFreeRemaining,
|
|
87
96
|
paidEmailsRemaining,
|
|
88
97
|
status: 'authenticated',
|
|
98
|
+
yamlRestored,
|
|
89
99
|
}, null, 2));
|
|
90
100
|
return;
|
|
91
101
|
}
|
|
@@ -97,6 +107,11 @@ export default class Login extends BaseCommand {
|
|
|
97
107
|
if (plan === 'paid') {
|
|
98
108
|
this.log(` Current paid block: ${chalk.cyan(String(paidEmailsRemaining))} emails remaining\n`);
|
|
99
109
|
}
|
|
100
|
-
|
|
110
|
+
if (yamlRestored) {
|
|
111
|
+
this.log(` ${INFO.YAML_RESTORED_ON_LOGIN}\n`);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
this.log(` Next: Run ${chalk.cyan("'mailmodo init'")} to generate your email sequences.\n`);
|
|
115
|
+
}
|
|
101
116
|
}
|
|
102
117
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
2
|
+
export default class Sdk extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
'sequence-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
private renderSnippets;
|
|
12
|
+
private renderSequenceBlock;
|
|
13
|
+
private renderCallBlock;
|
|
14
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { BaseCommand } from '../../lib/base-command.js';
|
|
4
|
+
import { API_ENDPOINTS, SDK_IMPORT_SNIPPET, SDK_INSTALL_COMMAND, } from '../../lib/constants.js';
|
|
5
|
+
import { SEPARATOR } from '../../lib/messages.js';
|
|
6
|
+
export default class Sdk extends BaseCommand {
|
|
7
|
+
static description = 'Show the SDK track() / identify() reference for deployed sequences';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> sdk',
|
|
10
|
+
'<%= config.bin %> sdk --sequence-id a1b2c3d4',
|
|
11
|
+
'<%= config.bin %> sdk --json',
|
|
12
|
+
];
|
|
13
|
+
static flags = {
|
|
14
|
+
...BaseCommand.baseFlags,
|
|
15
|
+
'sequence-id': Flags.string({
|
|
16
|
+
description: 'Limit output to a single active sequence by ID (default: all active sequences)',
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
async run() {
|
|
20
|
+
const { flags } = await this.parse(Sdk);
|
|
21
|
+
await this.ensureAuth();
|
|
22
|
+
const params = flags['sequence-id']
|
|
23
|
+
? { sequenceId: flags['sequence-id'] }
|
|
24
|
+
: undefined;
|
|
25
|
+
const response = await this.withApiSpinner({ json: flags.json, text: ' Loading SDK reference...' }, () => this.apiClient.get(API_ENDPOINTS.SEQUENCES_SDK, params));
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
this.handleApiError(response);
|
|
28
|
+
}
|
|
29
|
+
if (flags.json) {
|
|
30
|
+
this.log(JSON.stringify(response.data, null, 2));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.renderSnippets(response.data);
|
|
34
|
+
}
|
|
35
|
+
renderSnippets(data) {
|
|
36
|
+
const snippets = data.sdkSnippets ?? [];
|
|
37
|
+
if (snippets.length === 0) {
|
|
38
|
+
this.log(`\n ${chalk.dim('No active deployed sequences.')}`);
|
|
39
|
+
this.log(` Run ${chalk.cyan('mailmodo deploy')} to deploy one.\n`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
this.log(`\n ${chalk.bold(String(snippets.length))} active ${snippets.length === 1 ? 'sequence' : 'sequences'}:\n`);
|
|
43
|
+
this.log(` ${SEPARATOR}`);
|
|
44
|
+
this.log(` ${chalk.bold('SDK EVENT REFERENCE')}`);
|
|
45
|
+
this.log(` ${SEPARATOR}\n`);
|
|
46
|
+
this.log(` ${chalk.cyan(SDK_INSTALL_COMMAND)}\n`);
|
|
47
|
+
this.log(` ${chalk.dim(SDK_IMPORT_SNIPPET)}\n`);
|
|
48
|
+
for (const [index, snippet] of snippets.entries()) {
|
|
49
|
+
this.renderSequenceBlock(snippet);
|
|
50
|
+
if (index < snippets.length - 1)
|
|
51
|
+
this.log('');
|
|
52
|
+
}
|
|
53
|
+
this.log(` ${SEPARATOR}\n`);
|
|
54
|
+
}
|
|
55
|
+
renderSequenceBlock(snippet) {
|
|
56
|
+
const productName = snippet.productName || 'Unnamed sequence';
|
|
57
|
+
this.log(` ${chalk.bold(productName)} ${chalk.dim(`(${snippet.sequenceId})`)}`);
|
|
58
|
+
const trackCalls = [...new Set(snippet.sdkSnippet?.trackCalls ?? [])];
|
|
59
|
+
const identifyCalls = [...new Set(snippet.sdkSnippet?.identifyCalls ?? [])];
|
|
60
|
+
this.renderCallBlock('// track() calls', trackCalls);
|
|
61
|
+
this.renderCallBlock('// identify() calls', identifyCalls);
|
|
62
|
+
if (trackCalls.length === 0 && identifyCalls.length === 0) {
|
|
63
|
+
this.log(` ${chalk.dim('No track() or identify() calls available.')}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
renderCallBlock(label, calls) {
|
|
67
|
+
if (calls.length === 0)
|
|
68
|
+
return;
|
|
69
|
+
this.log(` ${chalk.dim(label)}`);
|
|
70
|
+
for (const call of calls) {
|
|
71
|
+
this.log(` ${chalk.dim(call)}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|