@mailmodo/cli 0.0.56-beta.pr58.99 → 0.0.56
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/deploy/index.js +3 -8
- package/dist/commands/edit/index.js +1 -4
- package/dist/commands/init/index.js +0 -1
- package/dist/commands/preview/index.js +0 -2
- package/dist/lib/base-command.d.ts +2 -33
- package/dist/lib/base-command.js +5 -102
- package/dist/lib/commands/edit/diff.js +2 -2
- package/dist/lib/commands/edit/persist.js +4 -6
- package/dist/lib/commands/edit/types.d.ts +0 -1
- package/dist/lib/commands/emails/editor.js +1 -1
- package/dist/lib/commands/init/analysis.js +1 -5
- package/dist/lib/commands/preview/types.d.ts +0 -3
- package/dist/lib/constants.d.ts +2 -3
- package/dist/lib/constants.js +5 -4
- package/dist/lib/messages.d.ts +0 -1
- package/dist/lib/messages.js +0 -21
- package/dist/lib/templates/missing-templates.d.ts +1 -15
- package/dist/lib/templates/missing-templates.js +22 -34
- package/dist/lib/templates/types.d.ts +0 -3
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/dist/lib/templates/regenerate.d.ts +0 -10
- package/dist/lib/templates/regenerate.js +0 -29
- package/dist/lib/templates/sync.d.ts +0 -33
- package/dist/lib/templates/sync.js +0 -106
|
@@ -3,7 +3,7 @@ import { confirm } from '@inquirer/prompts';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
5
|
import { API_ENDPOINTS } from '../../lib/constants.js';
|
|
6
|
-
import { MISSING_TEMPLATES
|
|
6
|
+
import { MISSING_TEMPLATES } from '../../lib/messages.js';
|
|
7
7
|
import { buildDeployPayload } from '../../lib/commands/deploy/payload.js';
|
|
8
8
|
import { logDeploySuccessInstructions, logPreDeploySummary, } from '../../lib/commands/deploy/output.js';
|
|
9
9
|
import { pauseSequence, resumeSequence, } from '../../lib/commands/deploy/sequence-status.js';
|
|
@@ -44,10 +44,8 @@ export default class Deploy extends BaseCommand {
|
|
|
44
44
|
const yamlConfig = await this.ensureYaml();
|
|
45
45
|
const missingIds = getMissingTemplateIds(yamlConfig);
|
|
46
46
|
if (missingIds.length > 0) {
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
ctx.log(`\n ${chalk.green('✓')} ${restoredFromServerHint(missingIds)}\n`);
|
|
50
|
-
else if (result === 'regenerated')
|
|
47
|
+
const regenerated = await handleMissingTemplates(ctx, yamlConfig, missingIds, baseFlags);
|
|
48
|
+
if (regenerated)
|
|
51
49
|
ctx.log(`\n ${chalk.green('✓')} ${MISSING_TEMPLATES.REVIEW_HINT}\n`);
|
|
52
50
|
return;
|
|
53
51
|
}
|
|
@@ -71,7 +69,6 @@ export default class Deploy extends BaseCommand {
|
|
|
71
69
|
if (!response.ok)
|
|
72
70
|
ctx.onApiError(response);
|
|
73
71
|
await ctx.syncYaml();
|
|
74
|
-
await ctx.syncTemplates(yamlConfig);
|
|
75
72
|
if (flags.json) {
|
|
76
73
|
this.log(JSON.stringify({
|
|
77
74
|
deployed: response.data.deployed,
|
|
@@ -89,7 +86,6 @@ export default class Deploy extends BaseCommand {
|
|
|
89
86
|
collectDomainInputs: (yaml, skip) => this.collectDomainSetupInputs(yaml, skip),
|
|
90
87
|
error: (msg) => this.error(msg),
|
|
91
88
|
exit: (code) => this.exit(code),
|
|
92
|
-
fetchTemplate: (emailId) => this.getTemplateFromServer(emailId),
|
|
93
89
|
get: (path, params) => this.apiClient.get(path, params),
|
|
94
90
|
getBillingCap: async () => {
|
|
95
91
|
const s = await this.fetchBillingStatus();
|
|
@@ -101,7 +97,6 @@ export default class Deploy extends BaseCommand {
|
|
|
101
97
|
registerDomainAndSave: (yaml, inputs, json) => this.registerDomain(yaml, inputs, json),
|
|
102
98
|
showDnsRecords: (records, url, json) => this.logDnsRecords(records, url, json),
|
|
103
99
|
spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
|
|
104
|
-
syncTemplates: (yaml) => this.syncTemplatesToServer(yaml),
|
|
105
100
|
syncYaml: () => this.syncYamlToServer(),
|
|
106
101
|
};
|
|
107
102
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
3
|
-
import { getTemplateFilename, loadTemplate
|
|
3
|
+
import { getTemplateFilename, loadTemplate } from '../../lib/yaml-config.js';
|
|
4
4
|
import { handleMissingTemplates } from '../../lib/templates/missing-templates.js';
|
|
5
5
|
import { askChangeDescription, runEditStep, } from '../../lib/commands/edit/flow.js';
|
|
6
6
|
export default class Edit extends BaseCommand {
|
|
@@ -63,7 +63,6 @@ export default class Edit extends BaseCommand {
|
|
|
63
63
|
post: (path, body) => this.apiClient.post(path, body),
|
|
64
64
|
runCommand: (id, argv) => this.config.runCommand(id, argv),
|
|
65
65
|
spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
|
|
66
|
-
syncTemplate: (emailId) => this.syncTemplateToServer(emailId),
|
|
67
66
|
syncYaml: () => this.syncYamlToServer(),
|
|
68
67
|
};
|
|
69
68
|
}
|
|
@@ -71,12 +70,10 @@ export default class Edit extends BaseCommand {
|
|
|
71
70
|
return {
|
|
72
71
|
error: (msg) => this.error(msg),
|
|
73
72
|
exit: (code) => this.exit(code),
|
|
74
|
-
fetchTemplate: (emailId) => this.getTemplateFromServer(emailId),
|
|
75
73
|
log: (msg) => this.log(msg),
|
|
76
74
|
onApiError: (r) => this.handleApiError(r),
|
|
77
75
|
post: (path, body) => this.apiClient.post(path, body),
|
|
78
76
|
spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
|
|
79
|
-
syncTemplates: (yaml) => this.syncTemplatesToServer(yaml),
|
|
80
77
|
syncYaml: () => this.syncYamlToServer(),
|
|
81
78
|
};
|
|
82
79
|
}
|
|
@@ -39,7 +39,6 @@ export default class Init extends BaseCommand {
|
|
|
39
39
|
await applyMonthlyCap(ctx, yamlConfig);
|
|
40
40
|
await saveAllTemplates(analysisPayload.recommendedEmails, generatedEmails);
|
|
41
41
|
await ctx.syncYaml();
|
|
42
|
-
await this.syncTemplatesToServer(yamlConfig);
|
|
43
42
|
logInitSuccess(ctx, {
|
|
44
43
|
brand: analysisPayload.brand,
|
|
45
44
|
emailConfigs,
|
|
@@ -82,12 +82,10 @@ export default class Preview extends BaseCommand {
|
|
|
82
82
|
return {
|
|
83
83
|
error: (msg) => this.error(msg),
|
|
84
84
|
exit: (code) => this.exit(code),
|
|
85
|
-
fetchTemplate: (emailId) => this.getTemplateFromServer(emailId),
|
|
86
85
|
log: (msg) => this.log(msg),
|
|
87
86
|
onApiError: (r) => this.handleApiError(r),
|
|
88
87
|
post: (path, body) => this.apiClient.post(path, body),
|
|
89
88
|
spinner: (text, json, work) => this.withApiSpinner({ json, text }, work),
|
|
90
|
-
syncTemplates: (yaml) => this.syncTemplatesToServer(yaml),
|
|
91
89
|
syncYaml: () => this.syncYamlToServer(),
|
|
92
90
|
};
|
|
93
91
|
}
|
|
@@ -80,11 +80,8 @@ export declare abstract class BaseCommand extends Command {
|
|
|
80
80
|
* successfully written, `false` otherwise (file already present, server 404,
|
|
81
81
|
* or any network error). Silent — never throws.
|
|
82
82
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* gets both their config and templates back without having to run init again.
|
|
86
|
-
*
|
|
87
|
-
* Used by `mailmodo login` right after the API key is validated.
|
|
83
|
+
* Used by `mailmodo login` right after the API key is validated so a returning
|
|
84
|
+
* user automatically gets their config back without having to run `init` again.
|
|
88
85
|
*/
|
|
89
86
|
protected recoverYamlAfterLogin(client: ApiClient): Promise<boolean>;
|
|
90
87
|
/**
|
|
@@ -93,34 +90,6 @@ export declare abstract class BaseCommand extends Command {
|
|
|
93
90
|
* always succeeds regardless of sync failures.
|
|
94
91
|
*/
|
|
95
92
|
protected syncYamlToServer(): Promise<void>;
|
|
96
|
-
/**
|
|
97
|
-
* Bulk-uploads all template HTML files referenced in the YAML to the server
|
|
98
|
-
* as a backup. Best-effort: silently ignores all errors so the originating
|
|
99
|
-
* command always succeeds regardless of sync failures.
|
|
100
|
-
* Called after init, deploy, and AI regeneration.
|
|
101
|
-
*/
|
|
102
|
-
protected syncTemplatesToServer(yaml: MailmodoYaml): Promise<void>;
|
|
103
|
-
/**
|
|
104
|
-
* Uploads a single template's HTML files to the server for incremental sync.
|
|
105
|
-
* Best-effort: silently ignores all errors. Called after the edit command
|
|
106
|
-
* applies changes to a specific template.
|
|
107
|
-
*/
|
|
108
|
-
protected syncTemplateToServer(emailId: string): Promise<void>;
|
|
109
|
-
/**
|
|
110
|
-
* Fetches a single backed-up template from the server and writes it to the
|
|
111
|
-
* local mailmodo/ folder. Returns true if the template was successfully
|
|
112
|
-
* restored, false if it is not backed up or on any error.
|
|
113
|
-
* Used as the `ctx.fetchTemplate` bridge passed to handleMissingTemplates.
|
|
114
|
-
*/
|
|
115
|
-
protected getTemplateFromServer(emailId: string): Promise<boolean>;
|
|
116
|
-
/**
|
|
117
|
-
* Silently restores any template files missing from disk by fetching them
|
|
118
|
-
* from the server. Uses the bulk endpoint when all templates are missing
|
|
119
|
-
* (single round-trip), or individual per-ID requests when only a subset is
|
|
120
|
-
* missing to avoid overwriting files that are already present. Best-effort —
|
|
121
|
-
* never throws.
|
|
122
|
-
*/
|
|
123
|
-
private fetchMissingTemplates;
|
|
124
93
|
/**
|
|
125
94
|
* Handles a failed API response by mapping HTTP status codes to
|
|
126
95
|
* user-friendly error messages and exiting the process.
|
package/dist/lib/base-command.js
CHANGED
|
@@ -8,8 +8,6 @@ import ora from 'ora';
|
|
|
8
8
|
import { ApiClient } from './api-client.js';
|
|
9
9
|
import { loadConfig } from './config.js';
|
|
10
10
|
import { API_ENDPOINTS, IS_DEV_MODE, YAML_FILE } from './constants.js';
|
|
11
|
-
import { syncTemplatesToServer, syncTemplateToServer, fetchTemplatesFromServer, fetchTemplateFromServer, } from './templates/sync.js';
|
|
12
|
-
import { getMissingTemplateIds } from './templates/missing-templates.js';
|
|
13
11
|
import { ERRORS, INFO, PROMPTS, quotaExhaustedMessage, recordLabel, VALIDATION, } from './messages.js';
|
|
14
12
|
import { loadYaml, saveYaml } from './yaml-config.js';
|
|
15
13
|
export const FREE_TIER = 'free';
|
|
@@ -139,22 +137,13 @@ export class BaseCommand extends Command {
|
|
|
139
137
|
* successfully written, `false` otherwise (file already present, server 404,
|
|
140
138
|
* or any network error). Silent — never throws.
|
|
141
139
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* gets both their config and templates back without having to run init again.
|
|
145
|
-
*
|
|
146
|
-
* Used by `mailmodo login` right after the API key is validated.
|
|
140
|
+
* Used by `mailmodo login` right after the API key is validated so a returning
|
|
141
|
+
* user automatically gets their config back without having to run `init` again.
|
|
147
142
|
*/
|
|
148
143
|
async recoverYamlAfterLogin(client) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
// Always check for missing templates, whether YAML was just restored or already present
|
|
154
|
-
const yaml = await loadYaml();
|
|
155
|
-
if (yaml)
|
|
156
|
-
await this.fetchMissingTemplates(client, yaml);
|
|
157
|
-
return yamlRestored;
|
|
144
|
+
if (existsSync(join(process.cwd(), YAML_FILE)))
|
|
145
|
+
return false;
|
|
146
|
+
return this.fetchAndWriteYaml(client);
|
|
158
147
|
}
|
|
159
148
|
/**
|
|
160
149
|
* Uploads the current local mailmodo.yaml to the server as a backup.
|
|
@@ -184,92 +173,6 @@ export class BaseCommand extends Command {
|
|
|
184
173
|
// Silently ignore — local file remains authoritative
|
|
185
174
|
}
|
|
186
175
|
}
|
|
187
|
-
/**
|
|
188
|
-
* Bulk-uploads all template HTML files referenced in the YAML to the server
|
|
189
|
-
* as a backup. Best-effort: silently ignores all errors so the originating
|
|
190
|
-
* command always succeeds regardless of sync failures.
|
|
191
|
-
* Called after init, deploy, and AI regeneration.
|
|
192
|
-
*/
|
|
193
|
-
async syncTemplatesToServer(yaml) {
|
|
194
|
-
try {
|
|
195
|
-
let client = this.apiClient;
|
|
196
|
-
if (!client) {
|
|
197
|
-
const envKey = process.env.MAILMODO_API_KEY;
|
|
198
|
-
const apiKey = envKey ?? (await loadConfig())?.apiKey;
|
|
199
|
-
if (!apiKey)
|
|
200
|
-
return;
|
|
201
|
-
client = new ApiClient(apiKey);
|
|
202
|
-
}
|
|
203
|
-
await syncTemplatesToServer(client, yaml);
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
// Silently ignore — local files remain authoritative
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Uploads a single template's HTML files to the server for incremental sync.
|
|
211
|
-
* Best-effort: silently ignores all errors. Called after the edit command
|
|
212
|
-
* applies changes to a specific template.
|
|
213
|
-
*/
|
|
214
|
-
async syncTemplateToServer(emailId) {
|
|
215
|
-
try {
|
|
216
|
-
let client = this.apiClient;
|
|
217
|
-
if (!client) {
|
|
218
|
-
const envKey = process.env.MAILMODO_API_KEY;
|
|
219
|
-
const apiKey = envKey ?? (await loadConfig())?.apiKey;
|
|
220
|
-
if (!apiKey)
|
|
221
|
-
return;
|
|
222
|
-
client = new ApiClient(apiKey);
|
|
223
|
-
}
|
|
224
|
-
await syncTemplateToServer(client, emailId);
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
// Silently ignore — local files remain authoritative
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Fetches a single backed-up template from the server and writes it to the
|
|
232
|
-
* local mailmodo/ folder. Returns true if the template was successfully
|
|
233
|
-
* restored, false if it is not backed up or on any error.
|
|
234
|
-
* Used as the `ctx.fetchTemplate` bridge passed to handleMissingTemplates.
|
|
235
|
-
*/
|
|
236
|
-
async getTemplateFromServer(emailId) {
|
|
237
|
-
try {
|
|
238
|
-
let client = this.apiClient;
|
|
239
|
-
if (!client) {
|
|
240
|
-
const envKey = process.env.MAILMODO_API_KEY;
|
|
241
|
-
const apiKey = envKey ?? (await loadConfig())?.apiKey;
|
|
242
|
-
if (!apiKey)
|
|
243
|
-
return false;
|
|
244
|
-
client = new ApiClient(apiKey);
|
|
245
|
-
}
|
|
246
|
-
return fetchTemplateFromServer(client, emailId);
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
return false;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Silently restores any template files missing from disk by fetching them
|
|
254
|
-
* from the server. Uses the bulk endpoint when all templates are missing
|
|
255
|
-
* (single round-trip), or individual per-ID requests when only a subset is
|
|
256
|
-
* missing to avoid overwriting files that are already present. Best-effort —
|
|
257
|
-
* never throws.
|
|
258
|
-
*/
|
|
259
|
-
async fetchMissingTemplates(client, yaml) {
|
|
260
|
-
try {
|
|
261
|
-
const missingIds = getMissingTemplateIds(yaml);
|
|
262
|
-
if (missingIds.length === 0)
|
|
263
|
-
return;
|
|
264
|
-
// Use bulk endpoint when every template is missing; per-ID otherwise
|
|
265
|
-
await (missingIds.length === yaml.emails.length
|
|
266
|
-
? fetchTemplatesFromServer(client, yaml)
|
|
267
|
-
: Promise.all(missingIds.map((id) => fetchTemplateFromServer(client, id))));
|
|
268
|
-
}
|
|
269
|
-
catch {
|
|
270
|
-
// best-effort, silently ignore
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
176
|
/**
|
|
274
177
|
* Handles a failed API response by mapping HTTP status codes to
|
|
275
178
|
* user-friendly error messages and exiting the process.
|
|
@@ -42,12 +42,12 @@ export function buildDiffPreview(email, updated, templateHtml) {
|
|
|
42
42
|
diff.subject = subjectChanged
|
|
43
43
|
? { new: updated.subject, old: email.subject }
|
|
44
44
|
: { unchanged: true, value: email.subject };
|
|
45
|
-
if (email.previewText
|
|
45
|
+
if (email.previewText ?? updated.previewText) {
|
|
46
46
|
diff.previewText = previewChanged
|
|
47
47
|
? { new: updated.previewText, old: email.previewText }
|
|
48
48
|
: { unchanged: true, value: email.previewText };
|
|
49
49
|
}
|
|
50
|
-
if (templateHtml
|
|
50
|
+
if (templateHtml ?? updated.html) {
|
|
51
51
|
const oldText = templateHtml
|
|
52
52
|
? truncate(stripHtml(templateHtml), 500)
|
|
53
53
|
: null;
|
|
@@ -20,13 +20,12 @@ async function persistChanges(ctx, editCtx, updated) {
|
|
|
20
20
|
await saveTemplate(editCtx.templateFilename, updated.html);
|
|
21
21
|
}
|
|
22
22
|
await ctx.syncYaml();
|
|
23
|
-
await ctx.syncTemplate(editCtx.email.id);
|
|
24
23
|
}
|
|
25
|
-
function logJsonResult(ctx, email, updated, oldSubject
|
|
24
|
+
function logJsonResult(ctx, email, updated, oldSubject) {
|
|
26
25
|
ctx.log(JSON.stringify({
|
|
27
26
|
diff: {
|
|
28
|
-
previewText: updated.previewText && updated.previewText !==
|
|
29
|
-
? { new: updated.previewText, old:
|
|
27
|
+
previewText: updated.previewText && updated.previewText !== email.previewText
|
|
28
|
+
? { new: updated.previewText, old: email.previewText }
|
|
30
29
|
: undefined,
|
|
31
30
|
subject: oldSubject === email.subject
|
|
32
31
|
? undefined
|
|
@@ -52,11 +51,10 @@ async function handleAcceptOutput(ctx, email) {
|
|
|
52
51
|
export async function finalizeEdit(ctx, editCtx, opts) {
|
|
53
52
|
const { flags, updated } = opts;
|
|
54
53
|
const oldSubject = editCtx.email.subject;
|
|
55
|
-
const oldPreviewText = editCtx.email.previewText;
|
|
56
54
|
applyEmailChanges(editCtx.email, updated);
|
|
57
55
|
await persistChanges(ctx, editCtx, updated);
|
|
58
56
|
if (flags.json) {
|
|
59
|
-
logJsonResult(ctx, editCtx.email, updated, oldSubject
|
|
57
|
+
logJsonResult(ctx, editCtx.email, updated, oldSubject);
|
|
60
58
|
}
|
|
61
59
|
else if (flags.yes) {
|
|
62
60
|
ctx.log(`\n Updated ${chalk.green('mailmodo.yaml')}\n`);
|
|
@@ -33,6 +33,5 @@ export type EditCtx = {
|
|
|
33
33
|
post<T>(path: string, body?: unknown): Promise<ApiResponse<T>>;
|
|
34
34
|
runCommand(id: string, argv: string[]): Promise<void>;
|
|
35
35
|
spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
|
|
36
|
-
syncTemplate(emailId: string): Promise<void>;
|
|
37
36
|
syncYaml(): Promise<void>;
|
|
38
37
|
};
|
|
@@ -27,7 +27,7 @@ export async function openTemplateInEditor(ctx, template) {
|
|
|
27
27
|
stdio: 'inherit',
|
|
28
28
|
});
|
|
29
29
|
child.on('error', () => resolve(false));
|
|
30
|
-
child.on('close', (
|
|
30
|
+
child.on('close', () => resolve(true));
|
|
31
31
|
});
|
|
32
32
|
if (launched)
|
|
33
33
|
return;
|
|
@@ -4,12 +4,8 @@ import { API_ENDPOINTS } from '../../constants.js';
|
|
|
4
4
|
import { isValidUrl } from '../../utils.js';
|
|
5
5
|
import { logAnalysisSummary } from './output.js';
|
|
6
6
|
export async function promptProductUrl(flagUrl) {
|
|
7
|
-
if (flagUrl)
|
|
8
|
-
if (!isValidUrl(flagUrl)) {
|
|
9
|
-
throw new Error(`Invalid URL: "${flagUrl}". Please enter a valid URL (e.g., https://myapp.com)`);
|
|
10
|
-
}
|
|
7
|
+
if (flagUrl)
|
|
11
8
|
return flagUrl;
|
|
12
|
-
}
|
|
13
9
|
return input({
|
|
14
10
|
message: 'What is your product URL?',
|
|
15
11
|
validate(value) {
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { ApiResponse } from '../../api-client.js';
|
|
2
|
-
import type { MailmodoYaml } from '../../yaml-config.js';
|
|
3
2
|
export type PreviewCtx = {
|
|
4
3
|
error(msg: string): never;
|
|
5
4
|
exit(code?: number): never;
|
|
6
|
-
fetchTemplate(emailId: string): Promise<boolean>;
|
|
7
5
|
log(msg?: string): void;
|
|
8
6
|
onApiError(resp: {
|
|
9
7
|
error?: string;
|
|
@@ -11,7 +9,6 @@ export type PreviewCtx = {
|
|
|
11
9
|
}): never;
|
|
12
10
|
post<T>(path: string, body?: unknown): Promise<ApiResponse<T>>;
|
|
13
11
|
spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
|
|
14
|
-
syncTemplates(yaml: MailmodoYaml): Promise<void>;
|
|
15
12
|
syncYaml(): Promise<void>;
|
|
16
13
|
};
|
|
17
14
|
export type PreviewEmail = {
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
* when this is true so end users never see internal request metadata.
|
|
5
5
|
*/
|
|
6
6
|
export declare const IS_DEV_MODE: boolean;
|
|
7
|
-
export declare const API_BASE_URL
|
|
7
|
+
export declare const API_BASE_URL = "https://app-vertex-debug.azurewebsites.net";
|
|
8
8
|
export declare const API_ENDPOINTS: Readonly<{
|
|
9
9
|
ANALYTICS: "/analytics";
|
|
10
10
|
ANALYZE: "/analyze";
|
|
11
11
|
ASSETS_LOGO: "/assets/logo";
|
|
12
|
-
ASSETS_TEMPLATES: "/assets/templates";
|
|
13
12
|
ASSETS_YAML: "/assets/yaml";
|
|
14
13
|
AUTH_VALIDATE: "/auth/validate";
|
|
15
14
|
BILLING_CAP: "/billing/cap";
|
|
@@ -31,7 +30,7 @@ export declare const API_ENDPOINTS: Readonly<{
|
|
|
31
30
|
SEQUENCES_SDK: "/sequences/sdk";
|
|
32
31
|
SEQUENCES_VALIDATE: "/sequences/validate";
|
|
33
32
|
}>;
|
|
34
|
-
export declare const LOGIN_URL
|
|
33
|
+
export declare const LOGIN_URL = "https://app-vertex-debug.azurewebsites.net/signup.html";
|
|
35
34
|
export declare const PREVIEW_PORT = 3421;
|
|
36
35
|
export declare const DEFAULT_BRAND_COLOR = "#1A56DB";
|
|
37
36
|
export declare const TEMPLATES_DIR = "mailmodo";
|
package/dist/lib/constants.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/** Set by `bin/dev.js` when running the CLI locally (tsx bootstrap). */
|
|
2
2
|
const DEV_API_BASE_URL = 'https://app-vertex-debug.azurewebsites.net';
|
|
3
|
-
const PRODUCTION_API_BASE_URL = 'https://api.mailmodo.dev';
|
|
4
3
|
/**
|
|
5
4
|
* True when the CLI is running in a dev/debug context. Verbose request
|
|
6
5
|
* diagnostics (URL, status, response body, error code) are only surfaced
|
|
7
6
|
* when this is true so end users never see internal request metadata.
|
|
8
7
|
*/
|
|
9
8
|
export const IS_DEV_MODE = Boolean(process.env.MAILMODO_DEV_TSX || process.env.MAILMODO_DEBUG);
|
|
9
|
+
// const PRODUCTION_API_BASE_URL = 'https://api.mailmodo.com';
|
|
10
|
+
const PRODUCTION_API_BASE_URL = 'https://app-vertex-debug.azurewebsites.net';
|
|
10
11
|
export const API_BASE_URL = process.env.MAILMODO_DEV_TSX
|
|
11
12
|
? DEV_API_BASE_URL
|
|
12
13
|
: PRODUCTION_API_BASE_URL;
|
|
@@ -14,7 +15,6 @@ export const API_ENDPOINTS = Object.freeze({
|
|
|
14
15
|
ANALYTICS: '/analytics',
|
|
15
16
|
ANALYZE: '/analyze',
|
|
16
17
|
ASSETS_LOGO: '/assets/logo',
|
|
17
|
-
ASSETS_TEMPLATES: '/assets/templates',
|
|
18
18
|
ASSETS_YAML: '/assets/yaml',
|
|
19
19
|
AUTH_VALIDATE: '/auth/validate',
|
|
20
20
|
BILLING_CAP: '/billing/cap',
|
|
@@ -36,8 +36,9 @@ export const API_ENDPOINTS = Object.freeze({
|
|
|
36
36
|
SEQUENCES_SDK: '/sequences/sdk',
|
|
37
37
|
SEQUENCES_VALIDATE: '/sequences/validate',
|
|
38
38
|
});
|
|
39
|
-
const DEV_LOGIN_URL = 'https://app-vertex-debug.azurewebsites.net/signup';
|
|
40
|
-
const PRODUCTION_LOGIN_URL = 'https://
|
|
39
|
+
const DEV_LOGIN_URL = 'https://app-vertex-debug.azurewebsites.net/signup.html';
|
|
40
|
+
// const PRODUCTION_LOGIN_URL = 'https://mailmodo.com/cli';
|
|
41
|
+
const PRODUCTION_LOGIN_URL = 'https://app-vertex-debug.azurewebsites.net/signup.html';
|
|
41
42
|
export const LOGIN_URL = process.env.MAILMODO_DEV_TSX
|
|
42
43
|
? DEV_LOGIN_URL
|
|
43
44
|
: PRODUCTION_LOGIN_URL;
|
package/dist/lib/messages.d.ts
CHANGED
|
@@ -63,7 +63,6 @@ export declare const DEPLOY: {
|
|
|
63
63
|
readonly SDK_ONBOARDING_HEADER: string;
|
|
64
64
|
readonly SUCCESS: `${string} Emails are live.`;
|
|
65
65
|
};
|
|
66
|
-
export declare function restoredFromServerHint(ids: string[]): string;
|
|
67
66
|
export declare function pauseSuccess(sequenceId: string): string;
|
|
68
67
|
export declare function pauseAlready(sequenceId: string): string;
|
|
69
68
|
export declare function resumeSuccess(sequenceId: string): string;
|
package/dist/lib/messages.js
CHANGED
|
@@ -63,27 +63,6 @@ export const DEPLOY = {
|
|
|
63
63
|
SDK_ONBOARDING_HEADER: chalk.bold('ADD THIS TO YOUR APP (one-time only):'),
|
|
64
64
|
SUCCESS: `${chalk.green('Deployed.')} Emails are live.`,
|
|
65
65
|
};
|
|
66
|
-
export function restoredFromServerHint(ids) {
|
|
67
|
-
if (ids.length === 1) {
|
|
68
|
-
const [id] = ids;
|
|
69
|
-
return (`Template ${chalk.cyan(id)} was not found locally and has been refreshed from the server.\n` +
|
|
70
|
-
` Review it with ${chalk.cyan(`mailmodo preview ${id}`)}, then run ${chalk.cyan('mailmodo deploy')} again.`);
|
|
71
|
-
}
|
|
72
|
-
const header = 'Template ID';
|
|
73
|
-
const colWidth = Math.max(header.length, ...ids.map((id) => id.length));
|
|
74
|
-
const pad = (s) => s.padEnd(colWidth);
|
|
75
|
-
const hr = '─'.repeat(colWidth + 2);
|
|
76
|
-
const table = [
|
|
77
|
-
` ┌${hr}┐`,
|
|
78
|
-
` │ ${chalk.bold(pad(header))} │`,
|
|
79
|
-
` ├${hr}┤`,
|
|
80
|
-
...ids.map((id) => ` │ ${chalk.cyan(pad(id))} │`),
|
|
81
|
-
` └${hr}┘`,
|
|
82
|
-
].join('\n');
|
|
83
|
-
return (`${ids.length} templates were not found locally and have been refreshed from the server:\n\n` +
|
|
84
|
-
`${table}\n\n` +
|
|
85
|
-
` Review each with ${chalk.cyan('mailmodo preview <id>')}, then run ${chalk.cyan('mailmodo deploy')} again.`);
|
|
86
|
-
}
|
|
87
66
|
export function pauseSuccess(sequenceId) {
|
|
88
67
|
return `Sequence ${chalk.cyan(sequenceId)} paused. Run ${chalk.cyan(`mailmodo deploy --resume ${sequenceId}`)} to resume.`;
|
|
89
68
|
}
|
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
import { type MailmodoYaml } from '../yaml-config.js';
|
|
2
2
|
import type { DeployFlags } from '../commands/deploy/types.js';
|
|
3
3
|
import type { RegenCtx } from './types.js';
|
|
4
|
-
/**
|
|
5
|
-
* Returns the IDs of emails whose template file does not exist on disk.
|
|
6
|
-
* Resolves the correct filename for each email by accounting for per-email
|
|
7
|
-
* and project-level style settings (branded vs. plain).
|
|
8
|
-
*/
|
|
9
4
|
export declare function getMissingTemplateIds(yamlConfig: MailmodoYaml): string[];
|
|
10
|
-
|
|
11
|
-
* Handles the case where one or more template files are missing before a
|
|
12
|
-
* command can proceed. First attempts a silent server restore for all missing
|
|
13
|
-
* IDs; if the server has backups for all of them the command continues without
|
|
14
|
-
* any user interaction. Only falls through to an interactive prompt (or an
|
|
15
|
-
* immediate error in --json / --yes mode) for files the server also does not have.
|
|
16
|
-
* Returns true if the situation was resolved (restored or regenerated), false
|
|
17
|
-
* if the user chose to abort.
|
|
18
|
-
*/
|
|
19
|
-
export declare function handleMissingTemplates(ctx: RegenCtx, yamlConfig: MailmodoYaml, missingIds: string[], flags: DeployFlags): Promise<'regenerated' | 'restored' | false>;
|
|
5
|
+
export declare function handleMissingTemplates(ctx: RegenCtx, yamlConfig: MailmodoYaml, missingIds: string[], flags: DeployFlags): Promise<boolean>;
|
|
@@ -2,56 +2,44 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { select } from '@inquirer/prompts';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
-
import { TEMPLATES_DIR } from '../constants.js';
|
|
5
|
+
import { API_ENDPOINTS, TEMPLATES_DIR } from '../constants.js';
|
|
6
6
|
import { MISSING_TEMPLATES } from '../messages.js';
|
|
7
|
-
import { getTemplateFilename } from '../yaml-config.js';
|
|
8
|
-
import {
|
|
9
|
-
/**
|
|
10
|
-
* Returns the IDs of emails whose template file does not exist on disk.
|
|
11
|
-
* Resolves the correct filename for each email by accounting for per-email
|
|
12
|
-
* and project-level style settings (branded vs. plain).
|
|
13
|
-
*/
|
|
7
|
+
import { getTemplateFilename, saveTemplate, } from '../yaml-config.js';
|
|
8
|
+
import { buildRegeneratePayload } from '../commands/deploy/payload.js';
|
|
14
9
|
export function getMissingTemplateIds(yamlConfig) {
|
|
15
10
|
return yamlConfig.emails
|
|
16
11
|
.filter((e) => !existsSync(join(process.cwd(), TEMPLATES_DIR, getTemplateFilename(e.id, e.style, yamlConfig.project?.emailStyle))))
|
|
17
12
|
.map((e) => e.id);
|
|
18
13
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
14
|
+
async function regenerateMissingTemplates(ctx, yamlConfig, missingIds, flags) {
|
|
15
|
+
const response = await ctx.spinner(` ${MISSING_TEMPLATES.REGENERATE_SPINNER}`, flags.json, () => ctx.post(API_ENDPOINTS.GENERATE, buildRegeneratePayload(yamlConfig, missingIds)));
|
|
16
|
+
if (!response.ok)
|
|
17
|
+
ctx.onApiError(response);
|
|
18
|
+
const saves = [];
|
|
19
|
+
for (const email of response.data?.emails ?? []) {
|
|
20
|
+
if (!/^[\w-]+$/.test(email.id))
|
|
21
|
+
continue;
|
|
22
|
+
if (email.html)
|
|
23
|
+
saves.push(saveTemplate(`${email.id}.html`, email.html));
|
|
24
|
+
if (email.plainHtml)
|
|
25
|
+
saves.push(saveTemplate(`${email.id}_plain.html`, email.plainHtml));
|
|
26
|
+
}
|
|
27
|
+
await Promise.all(saves);
|
|
28
|
+
await ctx.syncYaml();
|
|
28
29
|
}
|
|
29
|
-
/**
|
|
30
|
-
* Handles the case where one or more template files are missing before a
|
|
31
|
-
* command can proceed. First attempts a silent server restore for all missing
|
|
32
|
-
* IDs; if the server has backups for all of them the command continues without
|
|
33
|
-
* any user interaction. Only falls through to an interactive prompt (or an
|
|
34
|
-
* immediate error in --json / --yes mode) for files the server also does not have.
|
|
35
|
-
* Returns true if the situation was resolved (restored or regenerated), false
|
|
36
|
-
* if the user chose to abort.
|
|
37
|
-
*/
|
|
38
30
|
export async function handleMissingTemplates(ctx, yamlConfig, missingIds, flags) {
|
|
39
|
-
// Try to silently recover from server before interrupting the user
|
|
40
|
-
const stillMissing = await silentlyRestoreFromServer(ctx, missingIds);
|
|
41
|
-
if (stillMissing.length === 0)
|
|
42
|
-
return 'restored';
|
|
43
31
|
if (flags.json) {
|
|
44
32
|
ctx.log(JSON.stringify({
|
|
45
33
|
error: 'missing_templates',
|
|
46
34
|
message: MISSING_TEMPLATES.YES_ERROR,
|
|
47
|
-
missingTemplates:
|
|
35
|
+
missingTemplates: missingIds.map((id) => `${id}.html`),
|
|
48
36
|
}, null, 2));
|
|
49
37
|
ctx.exit(1);
|
|
50
38
|
}
|
|
51
39
|
if (flags.yes)
|
|
52
40
|
ctx.error(MISSING_TEMPLATES.YES_ERROR);
|
|
53
41
|
ctx.log(`\n ${MISSING_TEMPLATES.HEADER}`);
|
|
54
|
-
for (const id of
|
|
42
|
+
for (const id of missingIds)
|
|
55
43
|
ctx.log(` ${chalk.red('✗')} mailmodo/${id}.html`);
|
|
56
44
|
ctx.log(`\n ${MISSING_TEMPLATES.REGENERATE_NOTE}\n`);
|
|
57
45
|
const action = await select({
|
|
@@ -68,6 +56,6 @@ export async function handleMissingTemplates(ctx, yamlConfig, missingIds, flags)
|
|
|
68
56
|
ctx.log(`\n ${MISSING_TEMPLATES.ABORT_HINT}\n`);
|
|
69
57
|
return false;
|
|
70
58
|
}
|
|
71
|
-
await regenerateMissingTemplates(ctx, yamlConfig,
|
|
72
|
-
return
|
|
59
|
+
await regenerateMissingTemplates(ctx, yamlConfig, missingIds, flags);
|
|
60
|
+
return true;
|
|
73
61
|
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { ApiResponse } from '../api-client.js';
|
|
2
|
-
import type { MailmodoYaml } from '../yaml-config.js';
|
|
3
2
|
export type RegenCtx = {
|
|
4
3
|
error(msg: string): never;
|
|
5
4
|
exit(code?: number): never;
|
|
6
|
-
fetchTemplate(emailId: string): Promise<boolean>;
|
|
7
5
|
log(msg?: string): void;
|
|
8
6
|
onApiError(resp: {
|
|
9
7
|
error?: string;
|
|
@@ -11,6 +9,5 @@ export type RegenCtx = {
|
|
|
11
9
|
}): never;
|
|
12
10
|
post<T = Record<string, unknown>>(path: string, body?: Record<string, unknown> | unknown): Promise<ApiResponse<T>>;
|
|
13
11
|
spinner<T>(text: string, json: boolean, work: () => Promise<T>): Promise<T>;
|
|
14
|
-
syncTemplates(yaml: MailmodoYaml): Promise<void>;
|
|
15
12
|
syncYaml(): Promise<void>;
|
|
16
13
|
};
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { type MailmodoYaml } from '../yaml-config.js';
|
|
2
|
-
import type { DeployFlags } from '../commands/deploy/types.js';
|
|
3
|
-
import type { RegenCtx } from './types.js';
|
|
4
|
-
/**
|
|
5
|
-
* Calls POST /email/generate with the missing email IDs, writes the returned
|
|
6
|
-
* HTML files to disk, then syncs both the YAML and all templates to the server.
|
|
7
|
-
* Called when the user chooses "Re-generate via AI" from the missing-template
|
|
8
|
-
* prompt, or when no server backup exists to restore from.
|
|
9
|
-
*/
|
|
10
|
-
export declare function regenerateMissingTemplates(ctx: RegenCtx, yamlConfig: MailmodoYaml, missingIds: string[], flags: DeployFlags): Promise<void>;
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { API_ENDPOINTS } from '../constants.js';
|
|
2
|
-
import { saveTemplate } from '../yaml-config.js';
|
|
3
|
-
import { buildRegeneratePayload } from '../commands/deploy/payload.js';
|
|
4
|
-
import { MISSING_TEMPLATES } from '../messages.js';
|
|
5
|
-
/**
|
|
6
|
-
* Calls POST /email/generate with the missing email IDs, writes the returned
|
|
7
|
-
* HTML files to disk, then syncs both the YAML and all templates to the server.
|
|
8
|
-
* Called when the user chooses "Re-generate via AI" from the missing-template
|
|
9
|
-
* prompt, or when no server backup exists to restore from.
|
|
10
|
-
*/
|
|
11
|
-
export async function regenerateMissingTemplates(ctx, yamlConfig, missingIds, flags) {
|
|
12
|
-
const response = await ctx.spinner(` ${MISSING_TEMPLATES.REGENERATE_SPINNER}`, flags.json, () => ctx.post(API_ENDPOINTS.GENERATE, buildRegeneratePayload(yamlConfig, missingIds)));
|
|
13
|
-
if (!response.ok)
|
|
14
|
-
ctx.onApiError(response);
|
|
15
|
-
const saves = [];
|
|
16
|
-
for (const email of response.data?.emails ?? []) {
|
|
17
|
-
// Guard against path traversal: only allow alphanumeric, dash, and underscore IDs
|
|
18
|
-
if (!/^[\w-]+$/.test(email.id))
|
|
19
|
-
continue;
|
|
20
|
-
if (email.html)
|
|
21
|
-
saves.push(saveTemplate(`${email.id}.html`, email.html));
|
|
22
|
-
if (email.plainHtml)
|
|
23
|
-
saves.push(saveTemplate(`${email.id}_plain.html`, email.plainHtml));
|
|
24
|
-
}
|
|
25
|
-
await Promise.all(saves);
|
|
26
|
-
await ctx.syncYaml();
|
|
27
|
-
// Back up the freshly generated templates so the server reflects the new state
|
|
28
|
-
await ctx.syncTemplates(yamlConfig);
|
|
29
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { ApiClient } from '../api-client.js';
|
|
2
|
-
import { type MailmodoYaml } from '../yaml-config.js';
|
|
3
|
-
/**
|
|
4
|
-
* Bulk-uploads all template HTML files referenced in the YAML to the server
|
|
5
|
-
* as a backup. Only files present on disk are sent; any extra files in the
|
|
6
|
-
* mailmodo/ folder that are not listed in the YAML are intentionally skipped.
|
|
7
|
-
* All files are read in parallel before building the multipart payload.
|
|
8
|
-
*/
|
|
9
|
-
export declare function syncTemplatesToServer(client: ApiClient, yaml: MailmodoYaml): Promise<void>;
|
|
10
|
-
/**
|
|
11
|
-
* Uploads a single template's HTML files to the server for incremental sync.
|
|
12
|
-
* Used by the edit command after a change so only the modified template is
|
|
13
|
-
* re-uploaded instead of the full set. The branded HTML (`html` field) is
|
|
14
|
-
* required by the API; the plain variant (`plainHtml`) is uploaded only if
|
|
15
|
-
* its file exists on disk.
|
|
16
|
-
*/
|
|
17
|
-
export declare function syncTemplateToServer(client: ApiClient, emailId: string): Promise<void>;
|
|
18
|
-
/**
|
|
19
|
-
* Downloads all backed-up templates from the server and writes them to the
|
|
20
|
-
* local mailmodo/ folder. Only templates whose emailId appears in the YAML
|
|
21
|
-
* are written to disk — extra templates stored on the server (e.g. from a
|
|
22
|
-
* previous project state) are intentionally ignored to keep the local folder
|
|
23
|
-
* in sync with the current YAML.
|
|
24
|
-
* Returns true if at least one file was written, false otherwise.
|
|
25
|
-
*/
|
|
26
|
-
export declare function fetchTemplatesFromServer(client: ApiClient, yaml: MailmodoYaml): Promise<boolean>;
|
|
27
|
-
/**
|
|
28
|
-
* Downloads a single backed-up template from the server by emailId and writes
|
|
29
|
-
* it to the local mailmodo/ folder. Returns true if the template was found and
|
|
30
|
-
* written successfully, false if it has not been backed up or on any error.
|
|
31
|
-
* The plain HTML variant is only written if the server returned a non-empty value.
|
|
32
|
-
*/
|
|
33
|
-
export declare function fetchTemplateFromServer(client: ApiClient, emailId: string): Promise<boolean>;
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { readFile } from 'node:fs/promises';
|
|
4
|
-
import { API_ENDPOINTS, TEMPLATES_DIR } from '../constants.js';
|
|
5
|
-
import { saveTemplate } from '../yaml-config.js';
|
|
6
|
-
/**
|
|
7
|
-
* Bulk-uploads all template HTML files referenced in the YAML to the server
|
|
8
|
-
* as a backup. Only files present on disk are sent; any extra files in the
|
|
9
|
-
* mailmodo/ folder that are not listed in the YAML are intentionally skipped.
|
|
10
|
-
* All files are read in parallel before building the multipart payload.
|
|
11
|
-
*/
|
|
12
|
-
export async function syncTemplatesToServer(client, yaml) {
|
|
13
|
-
// Collect filenames for both branded and plain variants, filtered to those that exist on disk
|
|
14
|
-
const filenames = yaml.emails
|
|
15
|
-
.flatMap((email) => [`${email.id}.html`, `${email.id}_plain.html`])
|
|
16
|
-
.filter((f) => existsSync(join(process.cwd(), TEMPLATES_DIR, f)));
|
|
17
|
-
if (filenames.length === 0)
|
|
18
|
-
return;
|
|
19
|
-
// Read all files in parallel to avoid sequential await-in-loop
|
|
20
|
-
const files = await Promise.all(filenames.map(async (f) => ({
|
|
21
|
-
content: await readFile(join(process.cwd(), TEMPLATES_DIR, f), 'utf8'),
|
|
22
|
-
name: f,
|
|
23
|
-
})));
|
|
24
|
-
// API expects multipart fields keyed by the template filename (e.g. abc123.html)
|
|
25
|
-
const formData = new FormData();
|
|
26
|
-
for (const { name, content } of files) {
|
|
27
|
-
formData.append(name, new Blob([content], { type: 'text/html' }), name);
|
|
28
|
-
}
|
|
29
|
-
await client.postFormData(API_ENDPOINTS.ASSETS_TEMPLATES, formData);
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Uploads a single template's HTML files to the server for incremental sync.
|
|
33
|
-
* Used by the edit command after a change so only the modified template is
|
|
34
|
-
* re-uploaded instead of the full set. The branded HTML (`html` field) is
|
|
35
|
-
* required by the API; the plain variant (`plainHtml`) is uploaded only if
|
|
36
|
-
* its file exists on disk.
|
|
37
|
-
*/
|
|
38
|
-
export async function syncTemplateToServer(client, emailId) {
|
|
39
|
-
const htmlPath = join(process.cwd(), TEMPLATES_DIR, `${emailId}.html`);
|
|
40
|
-
// API requires the branded HTML field; skip entirely if the file is absent
|
|
41
|
-
if (!existsSync(htmlPath))
|
|
42
|
-
return;
|
|
43
|
-
const formData = new FormData();
|
|
44
|
-
const html = await readFile(htmlPath, 'utf8');
|
|
45
|
-
formData.append('html', new Blob([html], { type: 'text/html' }), `${emailId}.html`);
|
|
46
|
-
const plainPath = join(process.cwd(), TEMPLATES_DIR, `${emailId}_plain.html`);
|
|
47
|
-
if (existsSync(plainPath)) {
|
|
48
|
-
const plainHtml = await readFile(plainPath, 'utf8');
|
|
49
|
-
formData.append('plainHtml', new Blob([plainHtml], { type: 'text/html' }), `${emailId}_plain.html`);
|
|
50
|
-
}
|
|
51
|
-
await client.postFormData(`${API_ENDPOINTS.ASSETS_TEMPLATES}/${emailId}`, formData);
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Downloads all backed-up templates from the server and writes them to the
|
|
55
|
-
* local mailmodo/ folder. Only templates whose emailId appears in the YAML
|
|
56
|
-
* are written to disk — extra templates stored on the server (e.g. from a
|
|
57
|
-
* previous project state) are intentionally ignored to keep the local folder
|
|
58
|
-
* in sync with the current YAML.
|
|
59
|
-
* Returns true if at least one file was written, false otherwise.
|
|
60
|
-
*/
|
|
61
|
-
export async function fetchTemplatesFromServer(client, yaml) {
|
|
62
|
-
try {
|
|
63
|
-
const resp = await client.get(API_ENDPOINTS.ASSETS_TEMPLATES);
|
|
64
|
-
if (!resp.ok || !resp.data?.templates?.length)
|
|
65
|
-
return false;
|
|
66
|
-
// Build a set of valid IDs from the YAML to filter out stale server entries
|
|
67
|
-
const validIds = new Set(yaml.emails.map((e) => e.id));
|
|
68
|
-
const saves = [];
|
|
69
|
-
for (const t of resp.data.templates) {
|
|
70
|
-
if (!validIds.has(t.emailId))
|
|
71
|
-
continue;
|
|
72
|
-
// The API returns an empty string when a variant was never stored; skip those
|
|
73
|
-
if (t.html)
|
|
74
|
-
saves.push(saveTemplate(`${t.emailId}.html`, t.html));
|
|
75
|
-
if (t.plainHtml)
|
|
76
|
-
saves.push(saveTemplate(`${t.emailId}_plain.html`, t.plainHtml));
|
|
77
|
-
}
|
|
78
|
-
await Promise.all(saves);
|
|
79
|
-
return saves.length > 0;
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Downloads a single backed-up template from the server by emailId and writes
|
|
87
|
-
* it to the local mailmodo/ folder. Returns true if the template was found and
|
|
88
|
-
* written successfully, false if it has not been backed up or on any error.
|
|
89
|
-
* The plain HTML variant is only written if the server returned a non-empty value.
|
|
90
|
-
*/
|
|
91
|
-
export async function fetchTemplateFromServer(client, emailId) {
|
|
92
|
-
try {
|
|
93
|
-
const resp = await client.get(`${API_ENDPOINTS.ASSETS_TEMPLATES}/${emailId}`);
|
|
94
|
-
// A missing or empty html field means the template was never backed up
|
|
95
|
-
if (!resp.ok || !resp.data?.html)
|
|
96
|
-
return false;
|
|
97
|
-
await saveTemplate(`${emailId}.html`, resp.data.html);
|
|
98
|
-
// plainHtml is empty string when no plain variant is stored; skip in that case
|
|
99
|
-
if (resp.data.plainHtml)
|
|
100
|
-
await saveTemplate(`${emailId}_plain.html`, resp.data.plainHtml);
|
|
101
|
-
return true;
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
}
|