@leadcms/sdk 3.3.4 ā 3.3.7
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/README.md +3 -0
- package/dist/cli/bin/pull-all.js +9 -2
- package/dist/cli/bin/pull-all.js.map +1 -1
- package/dist/cli/bin/pull-comments.js +9 -2
- package/dist/cli/bin/pull-comments.js.map +1 -1
- package/dist/cli/bin/pull-content.js +9 -2
- package/dist/cli/bin/pull-content.js.map +1 -1
- package/dist/cli/bin/pull-email-templates.d.ts +6 -0
- package/dist/cli/bin/pull-email-templates.d.ts.map +1 -0
- package/dist/cli/bin/pull-email-templates.js +24 -0
- package/dist/cli/bin/pull-email-templates.js.map +1 -0
- package/dist/cli/bin/pull-media.js +9 -2
- package/dist/cli/bin/pull-media.js.map +1 -1
- package/dist/cli/bin/push-all.js +17 -4
- package/dist/cli/bin/push-all.js.map +1 -1
- package/dist/cli/bin/push-content.js +9 -2
- package/dist/cli/bin/push-content.js.map +1 -1
- package/dist/cli/bin/push-email-templates.d.ts +6 -0
- package/dist/cli/bin/push-email-templates.d.ts.map +1 -0
- package/dist/cli/bin/push-email-templates.js +21 -0
- package/dist/cli/bin/push-email-templates.js.map +1 -0
- package/dist/cli/bin/push-media.js +9 -2
- package/dist/cli/bin/push-media.js.map +1 -1
- package/dist/cli/bin/push.js +2 -0
- package/dist/cli/bin/push.js.map +1 -1
- package/dist/cli/bin/status-all.d.ts +1 -1
- package/dist/cli/bin/status-all.js +255 -31
- package/dist/cli/bin/status-all.js.map +1 -1
- package/dist/cli/bin/status-content.js +9 -2
- package/dist/cli/bin/status-content.js.map +1 -1
- package/dist/cli/bin/status-email-templates.d.ts +6 -0
- package/dist/cli/bin/status-email-templates.d.ts.map +1 -0
- package/dist/cli/bin/status-email-templates.js +29 -0
- package/dist/cli/bin/status-email-templates.js.map +1 -0
- package/dist/cli/bin/status-media.js +9 -2
- package/dist/cli/bin/status-media.js.map +1 -1
- package/dist/cli/bin/status.js +2 -0
- package/dist/cli/bin/status.js.map +1 -1
- package/dist/cli/bin/watch.js +2 -0
- package/dist/cli/bin/watch.js.map +1 -1
- package/dist/cli/index.js +27 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/lib/cms-config-types.d.ts +4 -0
- package/dist/lib/cms-config-types.d.ts.map +1 -1
- package/dist/lib/cms-config-types.js +6 -0
- package/dist/lib/cms-config-types.js.map +1 -1
- package/dist/lib/cms.d.ts.map +1 -1
- package/dist/lib/cms.js +1 -0
- package/dist/lib/cms.js.map +1 -1
- package/dist/lib/config.d.ts +2 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +5 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/content-merge.d.ts +2 -2
- package/dist/lib/content-merge.d.ts.map +1 -1
- package/dist/lib/content-merge.js +39 -35
- package/dist/lib/content-merge.js.map +1 -1
- package/dist/lib/content-transformation.d.ts +1 -1
- package/dist/lib/content-transformation.d.ts.map +1 -1
- package/dist/lib/content-transformation.js +8 -2
- package/dist/lib/content-transformation.js.map +1 -1
- package/dist/lib/data-service.d.ts +56 -0
- package/dist/lib/data-service.d.ts.map +1 -1
- package/dist/lib/data-service.js +246 -36
- package/dist/lib/data-service.js.map +1 -1
- package/dist/lib/email-template-transformation.d.ts +36 -0
- package/dist/lib/email-template-transformation.d.ts.map +1 -0
- package/dist/lib/email-template-transformation.js +88 -0
- package/dist/lib/email-template-transformation.js.map +1 -0
- package/dist/lib/logger.d.ts +63 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +87 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/spinner.d.ts +25 -0
- package/dist/lib/spinner.d.ts.map +1 -0
- package/dist/lib/spinner.js +80 -0
- package/dist/lib/spinner.js.map +1 -0
- package/dist/scripts/fetch-leadcms-comments.d.ts.map +1 -1
- package/dist/scripts/fetch-leadcms-comments.js +22 -21
- package/dist/scripts/fetch-leadcms-comments.js.map +1 -1
- package/dist/scripts/fetch-leadcms-content.d.ts.map +1 -1
- package/dist/scripts/fetch-leadcms-content.js +46 -45
- package/dist/scripts/fetch-leadcms-content.js.map +1 -1
- package/dist/scripts/fetch-leadcms-email-templates.d.ts +15 -0
- package/dist/scripts/fetch-leadcms-email-templates.d.ts.map +1 -0
- package/dist/scripts/fetch-leadcms-email-templates.js +272 -0
- package/dist/scripts/fetch-leadcms-email-templates.js.map +1 -0
- package/dist/scripts/init-leadcms.d.ts.map +1 -1
- package/dist/scripts/init-leadcms.js +22 -3
- package/dist/scripts/init-leadcms.js.map +1 -1
- package/dist/scripts/leadcms-helpers.d.ts +1 -0
- package/dist/scripts/leadcms-helpers.d.ts.map +1 -1
- package/dist/scripts/leadcms-helpers.js +7 -5
- package/dist/scripts/leadcms-helpers.js.map +1 -1
- package/dist/scripts/pull-all.d.ts +5 -1
- package/dist/scripts/pull-all.d.ts.map +1 -1
- package/dist/scripts/pull-all.js +38 -17
- package/dist/scripts/pull-all.js.map +1 -1
- package/dist/scripts/pull-comments.d.ts.map +1 -1
- package/dist/scripts/pull-comments.js +3 -2
- package/dist/scripts/pull-comments.js.map +1 -1
- package/dist/scripts/pull-content.d.ts.map +1 -1
- package/dist/scripts/pull-content.js +3 -2
- package/dist/scripts/pull-content.js.map +1 -1
- package/dist/scripts/pull-email-templates.d.ts +13 -0
- package/dist/scripts/pull-email-templates.d.ts.map +1 -0
- package/dist/scripts/pull-email-templates.js +79 -0
- package/dist/scripts/pull-email-templates.js.map +1 -0
- package/dist/scripts/pull-media.d.ts.map +1 -1
- package/dist/scripts/pull-media.js +3 -2
- package/dist/scripts/pull-media.js.map +1 -1
- package/dist/scripts/push-email-templates.d.ts +50 -0
- package/dist/scripts/push-email-templates.d.ts.map +1 -0
- package/dist/scripts/push-email-templates.js +687 -0
- package/dist/scripts/push-email-templates.js.map +1 -0
- package/dist/scripts/push-leadcms-content.d.ts +45 -1
- package/dist/scripts/push-leadcms-content.d.ts.map +1 -1
- package/dist/scripts/push-leadcms-content.js +112 -28
- package/dist/scripts/push-leadcms-content.js.map +1 -1
- package/dist/scripts/push-media.d.ts +2 -0
- package/dist/scripts/push-media.d.ts.map +1 -1
- package/dist/scripts/push-media.js +8 -5
- package/dist/scripts/push-media.js.map +1 -1
- package/dist/scripts/sse-watcher.d.ts.map +1 -1
- package/dist/scripts/sse-watcher.js +72 -71
- package/dist/scripts/sse-watcher.js.map +1 -1
- package/leadcms.config.json.sample +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import * as Diff from "diff";
|
|
5
|
+
import { EMAIL_TEMPLATES_DIR, defaultLanguage, } from "./leadcms-helpers.js";
|
|
6
|
+
import { parseEmailTemplateFileContent, transformEmailTemplateRemoteToLocalFormat, formatEmailTemplateForApi, } from "../lib/email-template-transformation.js";
|
|
7
|
+
import { hasContentDifferences, normalizeContentForComparison } from "../lib/content-transformation.js";
|
|
8
|
+
import { threeWayMerge, isLocallyModified } from "../lib/content-merge.js";
|
|
9
|
+
import { leadCMSDataService } from "../lib/data-service.js";
|
|
10
|
+
import { fetchEmailTemplateSync } from "./fetch-leadcms-email-templates.js";
|
|
11
|
+
import { colorConsole, statusColors, diffColors } from "../lib/console-colors.js";
|
|
12
|
+
import { logger } from "../lib/logger.js";
|
|
13
|
+
function slugifySegment(value) {
|
|
14
|
+
return value
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.trim()
|
|
17
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
18
|
+
.replace(/^-+|-+$/g, "");
|
|
19
|
+
}
|
|
20
|
+
async function isLocaleDirectory(dirPath, parentDir) {
|
|
21
|
+
if (parentDir !== EMAIL_TEMPLATES_DIR) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const dirName = path.basename(dirPath);
|
|
25
|
+
return /^[a-z]{2}(-[A-Z]{2})?$/.test(dirName);
|
|
26
|
+
}
|
|
27
|
+
async function readLocalEmailTemplates() {
|
|
28
|
+
const localTemplates = [];
|
|
29
|
+
async function walkDirectory(dir, locale = defaultLanguage, baseDir = EMAIL_TEMPLATES_DIR) {
|
|
30
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const fullPath = path.join(dir, entry.name);
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
if (entry.name !== defaultLanguage && await isLocaleDirectory(fullPath, dir)) {
|
|
35
|
+
await walkDirectory(fullPath, entry.name, fullPath);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
await walkDirectory(fullPath, locale, baseDir);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else if (entry.isFile() && entry.name.endsWith('.html')) {
|
|
42
|
+
const content = await fs.readFile(fullPath, 'utf8');
|
|
43
|
+
const parsed = parseEmailTemplateFileContent(content);
|
|
44
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
45
|
+
const relativeDir = path.dirname(relativePath);
|
|
46
|
+
const groupFolder = relativeDir === '.' ? 'ungrouped' : relativeDir.split(path.sep)[0];
|
|
47
|
+
const baseName = path.basename(fullPath, '.html');
|
|
48
|
+
const metadata = { ...parsed.metadata };
|
|
49
|
+
if (!metadata.name) {
|
|
50
|
+
metadata.name = baseName;
|
|
51
|
+
}
|
|
52
|
+
if (!metadata.language) {
|
|
53
|
+
metadata.language = locale;
|
|
54
|
+
}
|
|
55
|
+
localTemplates.push({
|
|
56
|
+
filePath: fullPath,
|
|
57
|
+
locale,
|
|
58
|
+
groupFolder,
|
|
59
|
+
metadata,
|
|
60
|
+
body: parsed.body,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
await walkDirectory(EMAIL_TEMPLATES_DIR);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code === 'ENOENT') {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
return localTemplates;
|
|
75
|
+
}
|
|
76
|
+
function normalizeGroupKey(name) {
|
|
77
|
+
return slugifySegment(name);
|
|
78
|
+
}
|
|
79
|
+
function resolveEmailGroupId(template, groupIndex) {
|
|
80
|
+
// 1. If metadata already has a numeric emailGroupId, use it directly
|
|
81
|
+
const existing = template.metadata.emailGroupId;
|
|
82
|
+
if (existing != null) {
|
|
83
|
+
return Number(existing);
|
|
84
|
+
}
|
|
85
|
+
// 2. If metadata has a groupName, look it up in the index
|
|
86
|
+
const groupName = template.metadata.groupName;
|
|
87
|
+
if (groupName) {
|
|
88
|
+
const groupKey = normalizeGroupKey(groupName);
|
|
89
|
+
if (groupKey) {
|
|
90
|
+
const candidates = groupIndex.get(groupKey) || [];
|
|
91
|
+
const localeMatch = candidates.find(group => group.language === template.locale);
|
|
92
|
+
const match = localeMatch || candidates[0];
|
|
93
|
+
if (match?.id != null) {
|
|
94
|
+
return Number(match.id);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
// 3. Fall back to folder name
|
|
100
|
+
const groupKey = normalizeGroupKey(template.groupFolder);
|
|
101
|
+
if (!groupKey) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const candidates = groupIndex.get(groupKey) || [];
|
|
105
|
+
const localeMatch = candidates.find(group => group.language === template.locale);
|
|
106
|
+
const match = localeMatch || candidates[0];
|
|
107
|
+
return match?.id != null ? Number(match.id) : null;
|
|
108
|
+
}
|
|
109
|
+
function buildGroupIndex(groups) {
|
|
110
|
+
const index = new Map();
|
|
111
|
+
for (const group of groups) {
|
|
112
|
+
const name = group.name || '';
|
|
113
|
+
const key = normalizeGroupKey(name);
|
|
114
|
+
if (!key) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const existing = index.get(key) || [];
|
|
118
|
+
existing.push(group);
|
|
119
|
+
index.set(key, existing);
|
|
120
|
+
}
|
|
121
|
+
return index;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Resolve the effective group name for a template:
|
|
125
|
+
* 1. metadata.groupName (from frontmatter)
|
|
126
|
+
* 2. groupFolder (from directory structure)
|
|
127
|
+
* 3. null if ungrouped
|
|
128
|
+
*/
|
|
129
|
+
function getEffectiveGroupName(template) {
|
|
130
|
+
if (template.metadata.groupName) {
|
|
131
|
+
return template.metadata.groupName;
|
|
132
|
+
}
|
|
133
|
+
if (template.groupFolder && template.groupFolder !== 'ungrouped') {
|
|
134
|
+
return template.groupFolder;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Find email groups that are referenced by local templates but don't exist remotely.
|
|
140
|
+
* Creates them automatically (like content type auto-creation).
|
|
141
|
+
*/
|
|
142
|
+
async function createMissingEmailGroups(localTemplates, groupIndex, dryRun = false) {
|
|
143
|
+
// Collect unique missing group names from local templates
|
|
144
|
+
const missingGroups = new Map();
|
|
145
|
+
for (const template of localTemplates) {
|
|
146
|
+
const groupName = getEffectiveGroupName(template);
|
|
147
|
+
if (!groupName)
|
|
148
|
+
continue;
|
|
149
|
+
const groupKey = normalizeGroupKey(groupName);
|
|
150
|
+
if (!groupKey)
|
|
151
|
+
continue;
|
|
152
|
+
if (groupIndex.has(groupKey))
|
|
153
|
+
continue;
|
|
154
|
+
if (missingGroups.has(groupKey))
|
|
155
|
+
continue;
|
|
156
|
+
missingGroups.set(groupKey, {
|
|
157
|
+
name: groupName,
|
|
158
|
+
language: template.locale || defaultLanguage,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (missingGroups.size === 0)
|
|
162
|
+
return [];
|
|
163
|
+
const created = [];
|
|
164
|
+
colorConsole.warn(`\nā ļø Missing email groups in remote LeadCMS: ${[...missingGroups.values()].map(g => colorConsole.highlight(g.name)).join(', ')}`);
|
|
165
|
+
for (const [key, { name, language }] of missingGroups) {
|
|
166
|
+
if (dryRun) {
|
|
167
|
+
colorConsole.progress(`š” [DRY RUN] Would create email group: ${colorConsole.highlight(name)} (${language})`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const newGroup = await leadCMSDataService.createEmailGroup({ name, language });
|
|
172
|
+
colorConsole.success(`ā
Created email group: ${colorConsole.highlight(name)} (ID: ${newGroup.id})`);
|
|
173
|
+
created.push(newGroup);
|
|
174
|
+
// Add to the index so subsequent templates can resolve
|
|
175
|
+
const existing = groupIndex.get(key) || [];
|
|
176
|
+
existing.push(newGroup);
|
|
177
|
+
groupIndex.set(key, existing);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
colorConsole.error(`ā Failed to create email group '${name}': ${error.message}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return created;
|
|
184
|
+
}
|
|
185
|
+
function getRemoteMatch(local, remoteTemplates) {
|
|
186
|
+
const localId = local.metadata.id != null ? Number(local.metadata.id) : undefined;
|
|
187
|
+
if (localId != null) {
|
|
188
|
+
return remoteTemplates.find(template => template.id === localId);
|
|
189
|
+
}
|
|
190
|
+
const name = local.metadata.name;
|
|
191
|
+
const emailGroupId = local.metadata.emailGroupId;
|
|
192
|
+
const language = local.metadata.language || local.locale;
|
|
193
|
+
return remoteTemplates.find(template => template.name === name &&
|
|
194
|
+
(template.language || defaultLanguage) === language &&
|
|
195
|
+
template.emailGroupId === emailGroupId);
|
|
196
|
+
}
|
|
197
|
+
function filterUndefinedValues(obj) {
|
|
198
|
+
const output = {};
|
|
199
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
200
|
+
if (value !== undefined) {
|
|
201
|
+
output[key] = value;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return output;
|
|
205
|
+
}
|
|
206
|
+
async function hasTemplateChanges(local, remote) {
|
|
207
|
+
const localContent = await fs.readFile(local.filePath, 'utf8');
|
|
208
|
+
const remoteTransformed = transformEmailTemplateRemoteToLocalFormat(remote);
|
|
209
|
+
return hasContentDifferences(localContent, remoteTransformed);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Update a local email template file with data from the API response.
|
|
213
|
+
* Ensures updatedAt, id, and other metadata stay in sync after push.
|
|
214
|
+
*/
|
|
215
|
+
async function updateLocalFileFromResponse(local, response, emailGroups) {
|
|
216
|
+
try {
|
|
217
|
+
// Enrich the response with group name if missing
|
|
218
|
+
if (response.emailGroupId != null && !response.emailGroup?.name) {
|
|
219
|
+
const group = emailGroups.find(g => Number(g.id) === Number(response.emailGroupId));
|
|
220
|
+
if (group) {
|
|
221
|
+
response.emailGroup = { id: group.id, name: group.name, language: group.language };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const transformed = transformEmailTemplateRemoteToLocalFormat(response);
|
|
225
|
+
await fs.writeFile(local.filePath, transformed, 'utf8');
|
|
226
|
+
logger.verbose(`[PUSH] Updated local file: ${local.filePath}`);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
logger.verbose(`[PUSH] Failed to update local file ${local.filePath}: ${error.message}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Enrich remote templates with email group data.
|
|
234
|
+
* The list/sync APIs often return emailGroup: null even when emailGroupId is set.
|
|
235
|
+
*/
|
|
236
|
+
function enrichRemoteTemplatesWithGroups(remoteTemplates, emailGroups) {
|
|
237
|
+
const groupById = new Map(emailGroups.map(g => [Number(g.id), g]));
|
|
238
|
+
for (const template of remoteTemplates) {
|
|
239
|
+
if (template.emailGroupId != null && !template.emailGroup?.name) {
|
|
240
|
+
const group = groupById.get(Number(template.emailGroupId));
|
|
241
|
+
if (group) {
|
|
242
|
+
template.emailGroup = { id: group.id, name: group.name, language: group.language };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Fetch base items from the sync endpoint for three-way merge.
|
|
249
|
+
* Base items represent the server's state at the time of the last pull sync token.
|
|
250
|
+
* Returns an empty record if no sync token exists or the fetch fails.
|
|
251
|
+
*/
|
|
252
|
+
async function fetchBaseItemsForMerge(emailGroups) {
|
|
253
|
+
try {
|
|
254
|
+
const syncTokenPath = path.join(EMAIL_TEMPLATES_DIR, '.sync-token');
|
|
255
|
+
let syncToken;
|
|
256
|
+
try {
|
|
257
|
+
syncToken = (await fs.readFile(syncTokenPath, 'utf8')).trim() || undefined;
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return {};
|
|
261
|
+
}
|
|
262
|
+
if (!syncToken)
|
|
263
|
+
return {};
|
|
264
|
+
const { baseItems } = await fetchEmailTemplateSync(syncToken);
|
|
265
|
+
// Enrich base items with email group data
|
|
266
|
+
const groupById = new Map(emailGroups.map(g => [Number(g.id), g]));
|
|
267
|
+
for (const base of Object.values(baseItems)) {
|
|
268
|
+
if (base.emailGroupId != null && !base.emailGroup) {
|
|
269
|
+
const group = groupById.get(Number(base.emailGroupId));
|
|
270
|
+
if (group) {
|
|
271
|
+
base.emailGroup = { id: group.id, name: group.name, language: group.language };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return baseItems;
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
logger.verbose(`[PUSH] Could not fetch base items for merge: ${error.message}`);
|
|
279
|
+
return {};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Attempt a three-way merge between base (from last pull), local (current file),
|
|
284
|
+
* and remote (current server state).
|
|
285
|
+
*
|
|
286
|
+
* Returns { canMerge: false } if no base version is available.
|
|
287
|
+
* Returns { canMerge: true, localUnmodified: true } if local hasn't changed since last pull.
|
|
288
|
+
* Returns { canMerge: true, merged, hasConflicts, conflictCount } with merge result.
|
|
289
|
+
*/
|
|
290
|
+
async function attemptAutoMerge(local, remote, baseItems) {
|
|
291
|
+
const remoteId = remote.id != null ? String(remote.id) : undefined;
|
|
292
|
+
if (!remoteId)
|
|
293
|
+
return { canMerge: false };
|
|
294
|
+
const baseItem = baseItems[remoteId];
|
|
295
|
+
if (!baseItem)
|
|
296
|
+
return { canMerge: false };
|
|
297
|
+
const baseTransformed = transformEmailTemplateRemoteToLocalFormat(baseItem);
|
|
298
|
+
let localContent;
|
|
299
|
+
try {
|
|
300
|
+
localContent = await fs.readFile(local.filePath, 'utf8');
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return { canMerge: false };
|
|
304
|
+
}
|
|
305
|
+
const remoteTransformed = transformEmailTemplateRemoteToLocalFormat(remote);
|
|
306
|
+
if (!isLocallyModified(baseTransformed, localContent)) {
|
|
307
|
+
// Local file is identical to what was pulled ā no local edits to merge
|
|
308
|
+
return { canMerge: true, localUnmodified: true, merged: remoteTransformed };
|
|
309
|
+
}
|
|
310
|
+
const mergeResult = threeWayMerge(baseTransformed, localContent, remoteTransformed);
|
|
311
|
+
return {
|
|
312
|
+
canMerge: true,
|
|
313
|
+
merged: mergeResult.merged,
|
|
314
|
+
hasConflicts: mergeResult.hasConflicts,
|
|
315
|
+
conflictCount: mergeResult.conflictCount,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
async function buildEmailTemplateStatus(options = {}) {
|
|
319
|
+
const { showDelete } = options;
|
|
320
|
+
const operations = [];
|
|
321
|
+
const localTemplates = await readLocalEmailTemplates();
|
|
322
|
+
const remoteTemplates = await leadCMSDataService.getAllEmailTemplates();
|
|
323
|
+
const emailGroups = await leadCMSDataService.getAllEmailGroups();
|
|
324
|
+
const groupIndex = buildGroupIndex(emailGroups);
|
|
325
|
+
// Enrich remote templates with group names for accurate comparison
|
|
326
|
+
enrichRemoteTemplatesWithGroups(remoteTemplates, emailGroups);
|
|
327
|
+
// Fetch base items for three-way auto-merge detection
|
|
328
|
+
const baseItems = await fetchBaseItemsForMerge(emailGroups);
|
|
329
|
+
for (const local of localTemplates) {
|
|
330
|
+
const resolvedGroupId = resolveEmailGroupId(local, groupIndex);
|
|
331
|
+
if (resolvedGroupId != null) {
|
|
332
|
+
local.metadata.emailGroupId = resolvedGroupId;
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
const groupName = getEffectiveGroupName(local);
|
|
336
|
+
if (groupName) {
|
|
337
|
+
operations.push({
|
|
338
|
+
type: 'conflict',
|
|
339
|
+
local,
|
|
340
|
+
reason: `Email group '${groupName}' not found remotely (will be created on push)`,
|
|
341
|
+
});
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const match = getRemoteMatch(local, remoteTemplates);
|
|
346
|
+
const payload = filterUndefinedValues(formatEmailTemplateForApi({
|
|
347
|
+
metadata: local.metadata,
|
|
348
|
+
body: local.body,
|
|
349
|
+
}));
|
|
350
|
+
const requiredFields = ['name', 'subject', 'fromEmail', 'fromName', 'language', 'emailGroupId'];
|
|
351
|
+
const missingFields = requiredFields.filter(field => payload[field] === undefined || payload[field] === null || payload[field] === '');
|
|
352
|
+
if (missingFields.length > 0) {
|
|
353
|
+
operations.push({
|
|
354
|
+
type: 'conflict',
|
|
355
|
+
local,
|
|
356
|
+
reason: `Missing required fields: ${missingFields.join(', ')}`,
|
|
357
|
+
});
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (!match) {
|
|
361
|
+
operations.push({ type: 'create', local });
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const localUpdated = local.metadata.updatedAt ? new Date(local.metadata.updatedAt) : new Date(0);
|
|
365
|
+
const remoteUpdated = match?.updatedAt ? new Date(match.updatedAt) : new Date(0);
|
|
366
|
+
if (remoteUpdated > localUpdated) {
|
|
367
|
+
// Attempt three-way auto-merge instead of immediately flagging as conflict
|
|
368
|
+
const mergeAttempt = await attemptAutoMerge(local, match, baseItems);
|
|
369
|
+
if (mergeAttempt.canMerge && mergeAttempt.localUnmodified) {
|
|
370
|
+
// Local hasn't changed since last pull ā nothing to push
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
else if (mergeAttempt.canMerge && !mergeAttempt.hasConflicts) {
|
|
374
|
+
// Auto-merge would succeed ā show as update
|
|
375
|
+
operations.push({ type: 'update', local, remote: match, reason: 'auto-merged' });
|
|
376
|
+
}
|
|
377
|
+
else if (mergeAttempt.canMerge && mergeAttempt.hasConflicts) {
|
|
378
|
+
operations.push({
|
|
379
|
+
type: 'conflict',
|
|
380
|
+
local,
|
|
381
|
+
remote: match,
|
|
382
|
+
reason: `Auto-merge has ${mergeAttempt.conflictCount} conflict(s) ā resolve manually`,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
operations.push({
|
|
387
|
+
type: 'conflict',
|
|
388
|
+
local,
|
|
389
|
+
remote: match,
|
|
390
|
+
reason: 'Remote email template updated after local changes',
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const hasChanges = await hasTemplateChanges(local, match);
|
|
396
|
+
if (hasChanges) {
|
|
397
|
+
operations.push({ type: 'update', local, remote: match });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (showDelete) {
|
|
401
|
+
const localIds = new Set(localTemplates
|
|
402
|
+
.map(template => template.metadata.id)
|
|
403
|
+
.filter(id => id != null)
|
|
404
|
+
.map(id => Number(id)));
|
|
405
|
+
for (const remote of remoteTemplates) {
|
|
406
|
+
if (remote.id == null) {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (!localIds.has(Number(remote.id))) {
|
|
410
|
+
operations.push({ type: 'delete', remote });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
operations,
|
|
416
|
+
totalLocal: localTemplates.length,
|
|
417
|
+
totalRemote: remoteTemplates.length,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function getRemoteGroupLabel(remote) {
|
|
421
|
+
if (remote.emailGroup?.name) {
|
|
422
|
+
return slugifySegment(remote.emailGroup.name) || remote.emailGroup.name;
|
|
423
|
+
}
|
|
424
|
+
return 'ungrouped';
|
|
425
|
+
}
|
|
426
|
+
export async function statusEmailTemplates(options = {}) {
|
|
427
|
+
const { targetId, showDetailedPreview } = options;
|
|
428
|
+
const result = await buildEmailTemplateStatus(options);
|
|
429
|
+
let { operations } = result;
|
|
430
|
+
// Filter by target ID if specified
|
|
431
|
+
if (targetId) {
|
|
432
|
+
operations = operations.filter(op => {
|
|
433
|
+
const localId = op.local?.metadata?.id?.toString();
|
|
434
|
+
const remoteId = op.remote?.id?.toString();
|
|
435
|
+
return localId === targetId || remoteId === targetId;
|
|
436
|
+
});
|
|
437
|
+
if (operations.length === 0) {
|
|
438
|
+
colorConsole.important('\nš LeadCMS Email Template Status');
|
|
439
|
+
colorConsole.log('');
|
|
440
|
+
colorConsole.log(`ā No email template found with ID ${targetId}`);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
colorConsole.important('\nš LeadCMS Email Template Status');
|
|
445
|
+
colorConsole.log('');
|
|
446
|
+
const syncableChanges = operations.length;
|
|
447
|
+
if (syncableChanges === 0) {
|
|
448
|
+
colorConsole.success('ā
No changes detected. Email templates are in sync!');
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const label = targetId ? `Status for template ID ${targetId}:` : `Changes to be synced (${syncableChanges} files):`;
|
|
452
|
+
console.log(label);
|
|
453
|
+
console.log('');
|
|
454
|
+
const sortOps = (ops) => {
|
|
455
|
+
return ops.sort((a, b) => {
|
|
456
|
+
const aLocale = a.local?.locale || a.remote?.language || defaultLanguage;
|
|
457
|
+
const bLocale = b.local?.locale || b.remote?.language || defaultLanguage;
|
|
458
|
+
if (aLocale !== bLocale) {
|
|
459
|
+
return aLocale.localeCompare(bLocale);
|
|
460
|
+
}
|
|
461
|
+
const aName = a.local?.metadata?.name || a.remote?.name || '';
|
|
462
|
+
const bName = b.local?.metadata?.name || b.remote?.name || '';
|
|
463
|
+
return aName.localeCompare(bName);
|
|
464
|
+
});
|
|
465
|
+
};
|
|
466
|
+
const createOps = sortOps(operations.filter(op => op.type === 'create'));
|
|
467
|
+
const updateOps = sortOps(operations.filter(op => op.type === 'update'));
|
|
468
|
+
const conflictOps = sortOps(operations.filter(op => op.type === 'conflict'));
|
|
469
|
+
const deleteOps = sortOps(operations.filter(op => op.type === 'delete'));
|
|
470
|
+
async function printDiffPreview(op) {
|
|
471
|
+
if (!showDetailedPreview && !targetId)
|
|
472
|
+
return;
|
|
473
|
+
if (!op.local?.filePath)
|
|
474
|
+
return;
|
|
475
|
+
try {
|
|
476
|
+
const localContent = await fs.readFile(op.local.filePath, 'utf8');
|
|
477
|
+
const remoteTransformed = op.remote
|
|
478
|
+
? transformEmailTemplateRemoteToLocalFormat(op.remote)
|
|
479
|
+
: '';
|
|
480
|
+
if (!hasContentDifferences(localContent, remoteTransformed)) {
|
|
481
|
+
colorConsole.log(' No content changes detected');
|
|
482
|
+
colorConsole.log('');
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
// Normalize both sides so timestamp precision differences don't appear in the diff
|
|
486
|
+
const normalizedLocal = normalizeContentForComparison(localContent);
|
|
487
|
+
const normalizedRemote = normalizeContentForComparison(remoteTransformed);
|
|
488
|
+
const diff = Diff.diffLines(normalizedRemote, normalizedLocal);
|
|
489
|
+
let addedLines = 0;
|
|
490
|
+
let removedLines = 0;
|
|
491
|
+
colorConsole.info(' Content diff preview:');
|
|
492
|
+
let previewLines = 0;
|
|
493
|
+
const maxPreviewLines = 10;
|
|
494
|
+
for (const part of diff) {
|
|
495
|
+
const lines = part.value.split('\n').filter((line) => line.trim() !== '');
|
|
496
|
+
if (part.added) {
|
|
497
|
+
addedLines += lines.length;
|
|
498
|
+
if (previewLines < maxPreviewLines) {
|
|
499
|
+
for (const line of lines.slice(0, Math.min(lines.length, maxPreviewLines - previewLines))) {
|
|
500
|
+
colorConsole.log(` ${diffColors.added(`+ ${line}`)}`);
|
|
501
|
+
previewLines++;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
else if (part.removed) {
|
|
506
|
+
removedLines += lines.length;
|
|
507
|
+
if (previewLines < maxPreviewLines) {
|
|
508
|
+
for (const line of lines.slice(0, Math.min(lines.length, maxPreviewLines - previewLines))) {
|
|
509
|
+
colorConsole.log(` ${diffColors.removed(`- ${line}`)}`);
|
|
510
|
+
previewLines++;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (previewLines >= maxPreviewLines)
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
if (previewLines >= maxPreviewLines && (addedLines + removedLines > previewLines)) {
|
|
518
|
+
colorConsole.gray(` ... (${addedLines + removedLines - previewLines} more changes)`);
|
|
519
|
+
}
|
|
520
|
+
colorConsole.log(` ${colorConsole.green(`+${addedLines}`)} / ${colorConsole.red(`-${removedLines}`)} lines`);
|
|
521
|
+
colorConsole.log('');
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
logger.verbose(`[DIFF] Failed to generate diff for ${op.local?.metadata?.name}: ${error.message}`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
for (const op of createOps) {
|
|
528
|
+
const groupLabel = (op.local?.groupFolder || 'ungrouped').padEnd(12);
|
|
529
|
+
const localeLabel = `[${op.local?.locale || defaultLanguage}]`.padEnd(6);
|
|
530
|
+
const nameLabel = op.local?.metadata?.name || 'unknown';
|
|
531
|
+
colorConsole.log(` ${statusColors.created('new file:')} ${groupLabel} ${localeLabel} ${colorConsole.highlight(nameLabel)}`);
|
|
532
|
+
await printDiffPreview(op);
|
|
533
|
+
}
|
|
534
|
+
for (const op of updateOps) {
|
|
535
|
+
const groupLabel = (op.local?.groupFolder || 'ungrouped').padEnd(12);
|
|
536
|
+
const localeLabel = `[${op.local?.locale || defaultLanguage}]`.padEnd(6);
|
|
537
|
+
const nameLabel = op.local?.metadata?.name || op.remote?.name || 'unknown';
|
|
538
|
+
const idLabel = op.remote?.id ? `(ID: ${op.remote.id})` : '';
|
|
539
|
+
const mergeHint = op.reason === 'auto-merged' ? colorConsole.gray('(auto-merged) ') : '';
|
|
540
|
+
colorConsole.log(` ${statusColors.modified('modified:')} ${groupLabel} ${localeLabel} ${colorConsole.highlight(nameLabel)} ${mergeHint}${colorConsole.gray(idLabel)}`);
|
|
541
|
+
await printDiffPreview(op);
|
|
542
|
+
}
|
|
543
|
+
for (const op of conflictOps) {
|
|
544
|
+
const groupLabel = (op.local?.groupFolder || getRemoteGroupLabel(op.remote || {})).padEnd(12);
|
|
545
|
+
const localeLabel = `[${op.local?.locale || op.remote?.language || defaultLanguage}]`.padEnd(6);
|
|
546
|
+
const nameLabel = op.local?.metadata?.name || op.remote?.name || 'unknown';
|
|
547
|
+
const reason = op.reason ? `(${op.reason})` : '';
|
|
548
|
+
colorConsole.log(` ${statusColors.conflict('conflict:')} ${groupLabel} ${localeLabel} ${colorConsole.highlight(nameLabel)} ${colorConsole.gray(reason)}`);
|
|
549
|
+
await printDiffPreview(op);
|
|
550
|
+
}
|
|
551
|
+
for (const op of deleteOps) {
|
|
552
|
+
const groupLabel = getRemoteGroupLabel(op.remote || {}).padEnd(12);
|
|
553
|
+
const localeLabel = `[${op.remote?.language || defaultLanguage}]`.padEnd(6);
|
|
554
|
+
const nameLabel = op.remote?.name || 'unknown';
|
|
555
|
+
const idLabel = op.remote?.id ? `(ID: ${op.remote.id})` : '';
|
|
556
|
+
colorConsole.log(` ${statusColors.conflict('deleted:')} ${groupLabel} ${localeLabel} ${colorConsole.highlight(nameLabel)} ${colorConsole.gray(idLabel)}`);
|
|
557
|
+
await printDiffPreview(op);
|
|
558
|
+
}
|
|
559
|
+
const summary = operations.reduce((acc, op) => {
|
|
560
|
+
acc[op.type] += 1;
|
|
561
|
+
return acc;
|
|
562
|
+
}, { create: 0, update: 0, delete: 0, conflict: 0, skip: 0 });
|
|
563
|
+
console.log(`\nš Email Templates Summary:`);
|
|
564
|
+
if (summary.create === 0 && summary.update === 0 && summary.conflict === 0 && summary.delete === 0) {
|
|
565
|
+
console.log(`\nā
No changes detected. Email templates are in sync!`);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (summary.create > 0)
|
|
569
|
+
console.log(` ⨠New: ${summary.create}`);
|
|
570
|
+
if (summary.update > 0)
|
|
571
|
+
console.log(` š Updated: ${summary.update}`);
|
|
572
|
+
if (summary.conflict > 0)
|
|
573
|
+
console.log(` ā ļø Conflicts: ${summary.conflict}`);
|
|
574
|
+
if (summary.delete > 0)
|
|
575
|
+
console.log(` šļø Deletes: ${summary.delete}`);
|
|
576
|
+
if (summary.skip > 0)
|
|
577
|
+
console.log(` ā Unchanged: ${summary.skip}`);
|
|
578
|
+
if (summary.conflict > 0) {
|
|
579
|
+
const conflicts = operations.filter(op => op.type === 'conflict');
|
|
580
|
+
for (const conflict of conflicts) {
|
|
581
|
+
console.warn(` - ${conflict.local?.metadata?.name || conflict.remote?.id}: ${conflict.reason || 'Conflict'}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// Export for unified status renderer
|
|
586
|
+
export { buildEmailTemplateStatus, getRemoteGroupLabel };
|
|
587
|
+
export async function pushEmailTemplates(options = {}) {
|
|
588
|
+
const { force, dryRun, allowDelete } = options;
|
|
589
|
+
const localTemplates = await readLocalEmailTemplates();
|
|
590
|
+
const remoteTemplates = await leadCMSDataService.getAllEmailTemplates();
|
|
591
|
+
const emailGroups = await leadCMSDataService.getAllEmailGroups();
|
|
592
|
+
const groupIndex = buildGroupIndex(emailGroups);
|
|
593
|
+
// Enrich remote templates with group names for accurate comparison
|
|
594
|
+
enrichRemoteTemplatesWithGroups(remoteTemplates, emailGroups);
|
|
595
|
+
// Auto-create missing email groups (like content type auto-creation)
|
|
596
|
+
await createMissingEmailGroups(localTemplates, groupIndex, dryRun);
|
|
597
|
+
// Fetch base items for three-way auto-merge
|
|
598
|
+
const baseItems = await fetchBaseItemsForMerge(emailGroups);
|
|
599
|
+
for (const local of localTemplates) {
|
|
600
|
+
const resolvedGroupId = resolveEmailGroupId(local, groupIndex);
|
|
601
|
+
if (resolvedGroupId != null) {
|
|
602
|
+
local.metadata.emailGroupId = resolvedGroupId;
|
|
603
|
+
}
|
|
604
|
+
const match = getRemoteMatch(local, remoteTemplates);
|
|
605
|
+
const localUpdated = local.metadata.updatedAt ? new Date(local.metadata.updatedAt) : new Date(0);
|
|
606
|
+
const remoteUpdated = match?.updatedAt ? new Date(match.updatedAt) : new Date(0);
|
|
607
|
+
let autoMerged = false;
|
|
608
|
+
if (match && !force && remoteUpdated > localUpdated) {
|
|
609
|
+
// Attempt three-way auto-merge instead of immediately skipping as conflict
|
|
610
|
+
const mergeAttempt = await attemptAutoMerge(local, match, baseItems);
|
|
611
|
+
if (mergeAttempt.canMerge && mergeAttempt.localUnmodified) {
|
|
612
|
+
// Local hasn't changed since last pull ā nothing to push
|
|
613
|
+
logger.verbose(`[PUSH] Skipping ${local.metadata.name} ā no local changes to push`);
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
else if (mergeAttempt.canMerge && !mergeAttempt.hasConflicts) {
|
|
617
|
+
// Auto-merge succeeded ā update local data from merged content
|
|
618
|
+
const parsed = parseEmailTemplateFileContent(mergeAttempt.merged);
|
|
619
|
+
local.metadata = { ...parsed.metadata, emailGroupId: local.metadata.emailGroupId };
|
|
620
|
+
local.body = parsed.body;
|
|
621
|
+
autoMerged = true;
|
|
622
|
+
}
|
|
623
|
+
else if (mergeAttempt.canMerge && mergeAttempt.hasConflicts) {
|
|
624
|
+
console.warn(`ā ļø Auto-merge has ${mergeAttempt.conflictCount} conflict(s): ${local.metadata.name || match.id} ā skipping (use --force to override)`);
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
console.warn(`ā ļø Remote email template updated after local changes: ${local.metadata.name || match.id}`);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const payload = filterUndefinedValues(formatEmailTemplateForApi({
|
|
633
|
+
metadata: local.metadata,
|
|
634
|
+
body: local.body,
|
|
635
|
+
}));
|
|
636
|
+
const requiredFields = ['name', 'subject', 'fromEmail', 'fromName', 'language', 'emailGroupId'];
|
|
637
|
+
const missingFields = requiredFields.filter(field => payload[field] === undefined || payload[field] === null || payload[field] === '');
|
|
638
|
+
if (missingFields.length > 0) {
|
|
639
|
+
console.warn(`ā ļø Skipping ${local.filePath} - missing required fields: ${missingFields.join(', ')}`);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (!match) {
|
|
643
|
+
if (dryRun) {
|
|
644
|
+
console.log(`š” [DRY RUN] Create email template: ${payload.name}`);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
const created = await leadCMSDataService.createEmailTemplate(payload);
|
|
648
|
+
console.log(`ā
Created email template: ${payload.name}`);
|
|
649
|
+
await updateLocalFileFromResponse(local, created, emailGroups);
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
// Only check hasChanges for non-merged items (merged content is known to differ)
|
|
653
|
+
if (!autoMerged) {
|
|
654
|
+
const hasChanges = await hasTemplateChanges(local, match);
|
|
655
|
+
if (!hasChanges) {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (dryRun) {
|
|
660
|
+
const label = autoMerged ? '[DRY RUN] Auto-merge + update' : '[DRY RUN] Update';
|
|
661
|
+
console.log(`š” ${label} email template: ${payload.name} (ID ${match.id})`);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
const updated = await leadCMSDataService.updateEmailTemplate(Number(match.id), payload);
|
|
665
|
+
const label = autoMerged ? 'š Auto-merged and updated' : 'ā
Updated';
|
|
666
|
+
console.log(`${label} email template: ${payload.name}`);
|
|
667
|
+
await updateLocalFileFromResponse(local, updated, emailGroups);
|
|
668
|
+
}
|
|
669
|
+
if (!allowDelete) {
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const localIds = new Set(localTemplates.map(template => template.metadata.id).filter(id => id != null).map(id => Number(id)));
|
|
673
|
+
for (const remote of remoteTemplates) {
|
|
674
|
+
if (remote.id == null) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
if (!localIds.has(Number(remote.id))) {
|
|
678
|
+
if (dryRun) {
|
|
679
|
+
console.log(`š” [DRY RUN] Delete email template: ${remote.name || remote.id}`);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
await leadCMSDataService.deleteEmailTemplate(Number(remote.id));
|
|
683
|
+
console.log(`šļø Deleted email template: ${remote.name || remote.id}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
//# sourceMappingURL=push-email-templates.js.map
|