@leadcms/sdk 3.2.0 → 3.3.1
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 +15 -3
- package/dist/cli/bin/generate-env.d.ts +1 -1
- package/dist/cli/bin/generate-env.d.ts.map +1 -1
- package/dist/cli/bin/generate-env.js +2 -2
- package/dist/cli/bin/generate-env.js.map +1 -1
- package/dist/cli/bin/init.d.ts +1 -1
- package/dist/cli/bin/init.d.ts.map +1 -1
- package/dist/cli/bin/init.js +5 -2
- package/dist/cli/bin/init.js.map +1 -1
- package/dist/cli/bin/pull-comments.js +3 -1
- package/dist/cli/bin/pull-comments.js.map +1 -1
- package/dist/cli/bin/pull-content.js +2 -1
- package/dist/cli/bin/pull-content.js.map +1 -1
- package/dist/cli/bin/pull-media.js +3 -1
- package/dist/cli/bin/pull-media.js.map +1 -1
- package/dist/cli/bin/watch.d.ts +1 -1
- package/dist/cli/bin/watch.d.ts.map +1 -1
- package/dist/cli/bin/watch.js +2 -2
- package/dist/cli/bin/watch.js.map +1 -1
- package/dist/cli/index.js +7 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/content-merge.d.ts +80 -0
- package/dist/lib/content-merge.d.ts.map +1 -0
- package/dist/lib/content-merge.js +350 -0
- package/dist/lib/content-merge.js.map +1 -0
- package/dist/scripts/fetch-leadcms-content.d.ts +1 -0
- package/dist/scripts/fetch-leadcms-content.d.ts.map +1 -1
- package/dist/scripts/fetch-leadcms-content.js +106 -13
- package/dist/scripts/fetch-leadcms-content.js.map +1 -1
- package/dist/scripts/generate-env-js.d.ts +5 -0
- package/dist/scripts/generate-env-js.d.ts.map +1 -1
- package/dist/scripts/generate-env-js.js +12 -10
- package/dist/scripts/generate-env-js.js.map +1 -1
- package/dist/scripts/init-leadcms.d.ts +4 -0
- package/dist/scripts/init-leadcms.d.ts.map +1 -1
- package/dist/scripts/init-leadcms.js +13 -22
- package/dist/scripts/init-leadcms.js.map +1 -1
- package/dist/scripts/pull-all.d.ts +16 -1
- package/dist/scripts/pull-all.d.ts.map +1 -1
- package/dist/scripts/pull-all.js +49 -24
- package/dist/scripts/pull-all.js.map +1 -1
- package/dist/scripts/pull-comments.d.ts +5 -1
- package/dist/scripts/pull-comments.d.ts.map +1 -1
- package/dist/scripts/pull-comments.js +8 -1
- package/dist/scripts/pull-comments.js.map +1 -1
- package/dist/scripts/pull-content.d.ts +2 -0
- package/dist/scripts/pull-content.d.ts.map +1 -1
- package/dist/scripts/pull-content.js +7 -1
- package/dist/scripts/pull-content.js.map +1 -1
- package/dist/scripts/pull-media.d.ts +5 -1
- package/dist/scripts/pull-media.d.ts.map +1 -1
- package/dist/scripts/pull-media.js +8 -1
- package/dist/scripts/pull-media.js.map +1 -1
- package/dist/scripts/sse-watcher.d.ts +2 -0
- package/dist/scripts/sse-watcher.d.ts.map +1 -1
- package/dist/scripts/sse-watcher.js +2 -1
- package/dist/scripts/sse-watcher.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three-way merge utilities for LeadCMS content
|
|
3
|
+
*
|
|
4
|
+
* Uses the node-diff3 library to perform git-style three-way merges between
|
|
5
|
+
* a base version, a local version, and a remote version of content files.
|
|
6
|
+
*
|
|
7
|
+
* For JSON content, a structural (field-level) merge is used instead of
|
|
8
|
+
* line-based diff to avoid false conflicts from adjacent line changes.
|
|
9
|
+
*
|
|
10
|
+
* The base version comes from the server via the sync API (the state of the
|
|
11
|
+
* content at the time of the client's last sync token). This avoids needing
|
|
12
|
+
* any local storage of base snapshots.
|
|
13
|
+
*/
|
|
14
|
+
import { diff3Merge } from 'node-diff3';
|
|
15
|
+
/**
|
|
16
|
+
* Fields that are controlled by the server and should always take the remote
|
|
17
|
+
* value during merge, even if they differ between base and local.
|
|
18
|
+
* These fields are set/updated by the server automatically and should never
|
|
19
|
+
* be treated as meaningful local edits.
|
|
20
|
+
*/
|
|
21
|
+
const SERVER_CONTROLLED_FIELDS = new Set([
|
|
22
|
+
'updatedAt',
|
|
23
|
+
'createdAt',
|
|
24
|
+
]);
|
|
25
|
+
/**
|
|
26
|
+
* Regex to match a YAML frontmatter line for a server-controlled field.
|
|
27
|
+
* Matches lines like:
|
|
28
|
+
* updatedAt: "2026-02-01T00:00:00Z"
|
|
29
|
+
* createdAt: "2026-01-01T00:00:00Z"
|
|
30
|
+
* updatedAt: 2026-02-01T00:00:00Z
|
|
31
|
+
*/
|
|
32
|
+
const SERVER_CONTROLLED_YAML_LINE = /^\s*(updatedAt|createdAt)\s*:/;
|
|
33
|
+
/**
|
|
34
|
+
* Perform a three-way merge between base, local, and remote content.
|
|
35
|
+
*
|
|
36
|
+
* This works like git merge:
|
|
37
|
+
* - Changes that don't overlap are merged automatically
|
|
38
|
+
* - Changes that modify the same lines produce conflict markers
|
|
39
|
+
* - Server-controlled fields (updatedAt, createdAt) in YAML frontmatter
|
|
40
|
+
* are auto-resolved to the remote value, never producing conflicts
|
|
41
|
+
*
|
|
42
|
+
* For JSON content, prefer threeWayMergeJson() which does structural merging
|
|
43
|
+
* and avoids false conflicts from adjacent line changes.
|
|
44
|
+
*
|
|
45
|
+
* @param base - The original content at the time of last sync (from server's baseItems)
|
|
46
|
+
* @param local - The current local file content (possibly user-modified)
|
|
47
|
+
* @param remote - The current remote content (fetched from server)
|
|
48
|
+
* @returns MergeResult with merged content and conflict information
|
|
49
|
+
*/
|
|
50
|
+
export function threeWayMerge(base, local, remote) {
|
|
51
|
+
const baseLines = base.split('\n');
|
|
52
|
+
const localLines = local.split('\n');
|
|
53
|
+
const remoteLines = remote.split('\n');
|
|
54
|
+
const regions = diff3Merge(localLines, baseLines, remoteLines);
|
|
55
|
+
let conflictCount = 0;
|
|
56
|
+
const resultLines = [];
|
|
57
|
+
for (const region of regions) {
|
|
58
|
+
if ('ok' in region && region.ok) {
|
|
59
|
+
resultLines.push(...region.ok);
|
|
60
|
+
}
|
|
61
|
+
else if ('conflict' in region && region.conflict) {
|
|
62
|
+
const localConflictLines = region.conflict.a;
|
|
63
|
+
const remoteConflictLines = region.conflict.b;
|
|
64
|
+
// Try to auto-resolve server-controlled fields within the conflict
|
|
65
|
+
const resolved = resolveServerControlledConflict(localConflictLines, remoteConflictLines);
|
|
66
|
+
if (resolved.remainingConflict) {
|
|
67
|
+
// There are still real conflicts after extracting server-controlled fields
|
|
68
|
+
conflictCount++;
|
|
69
|
+
resultLines.push(...resolved.resolvedLines);
|
|
70
|
+
resultLines.push('<<<<<<< local');
|
|
71
|
+
resultLines.push(...resolved.remainingConflict.local);
|
|
72
|
+
resultLines.push('=======');
|
|
73
|
+
resultLines.push(...resolved.remainingConflict.remote);
|
|
74
|
+
resultLines.push('>>>>>>> remote');
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// All lines in this conflict were server-controlled → fully auto-resolved
|
|
78
|
+
resultLines.push(...resolved.resolvedLines);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const merged = resultLines.join('\n');
|
|
83
|
+
return {
|
|
84
|
+
success: conflictCount === 0,
|
|
85
|
+
merged,
|
|
86
|
+
hasConflicts: conflictCount > 0,
|
|
87
|
+
conflictCount,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Attempt to auto-resolve server-controlled fields within a conflict region.
|
|
92
|
+
*
|
|
93
|
+
* For each line in the conflict, if it's a server-controlled YAML field
|
|
94
|
+
* (updatedAt, createdAt), take the remote version. Non-server-controlled
|
|
95
|
+
* lines remain as conflicts.
|
|
96
|
+
*
|
|
97
|
+
* Returns:
|
|
98
|
+
* - resolvedLines: lines that were auto-resolved (server-controlled)
|
|
99
|
+
* - remainingConflict: null if fully resolved, or { local, remote } with
|
|
100
|
+
* the non-server-controlled lines that still conflict
|
|
101
|
+
*/
|
|
102
|
+
function resolveServerControlledConflict(localLines, remoteLines) {
|
|
103
|
+
const resolvedLines = [];
|
|
104
|
+
const remainingLocal = [];
|
|
105
|
+
const remainingRemote = [];
|
|
106
|
+
// Separate server-controlled from non-server-controlled lines on each side
|
|
107
|
+
const localServerControlled = [];
|
|
108
|
+
const localOther = [];
|
|
109
|
+
for (const line of localLines) {
|
|
110
|
+
if (SERVER_CONTROLLED_YAML_LINE.test(line)) {
|
|
111
|
+
localServerControlled.push(line);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
localOther.push(line);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const remoteServerControlled = [];
|
|
118
|
+
const remoteOther = [];
|
|
119
|
+
for (const line of remoteLines) {
|
|
120
|
+
if (SERVER_CONTROLLED_YAML_LINE.test(line)) {
|
|
121
|
+
remoteServerControlled.push(line);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
remoteOther.push(line);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Auto-resolve server-controlled fields: always take remote version
|
|
128
|
+
resolvedLines.push(...remoteServerControlled);
|
|
129
|
+
// Check if there are remaining non-server-controlled lines
|
|
130
|
+
if (localOther.length === 0 && remoteOther.length === 0) {
|
|
131
|
+
// Entire conflict was server-controlled → fully resolved
|
|
132
|
+
return { resolvedLines, remainingConflict: null };
|
|
133
|
+
}
|
|
134
|
+
// There are real conflicting lines remaining
|
|
135
|
+
return {
|
|
136
|
+
resolvedLines,
|
|
137
|
+
remainingConflict: { local: localOther, remote: remoteOther },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Perform a structural three-way merge on JSON content.
|
|
142
|
+
*
|
|
143
|
+
* Instead of doing a line-based diff (which produces false conflicts for
|
|
144
|
+
* adjacent field changes), this parses the JSON and merges field-by-field:
|
|
145
|
+
*
|
|
146
|
+
* - Fields changed only locally → keep local value
|
|
147
|
+
* - Fields changed only remotely → take remote value
|
|
148
|
+
* - Fields changed identically on both sides → take either (same value)
|
|
149
|
+
* - Fields changed differently on both sides → conflict
|
|
150
|
+
* - Server-controlled fields (updatedAt, createdAt) → always take remote
|
|
151
|
+
*
|
|
152
|
+
* For nested objects, the merge recurses into each level.
|
|
153
|
+
*
|
|
154
|
+
* @param base - The base JSON content (from server's baseItems, transformed to local format)
|
|
155
|
+
* @param local - The current local JSON file content
|
|
156
|
+
* @param remote - The current remote JSON content (transformed to local format)
|
|
157
|
+
* @returns MergeResult with the merged JSON string
|
|
158
|
+
*/
|
|
159
|
+
export function threeWayMergeJson(base, local, remote) {
|
|
160
|
+
let baseObj;
|
|
161
|
+
let localObj;
|
|
162
|
+
let remoteObj;
|
|
163
|
+
try {
|
|
164
|
+
baseObj = JSON.parse(base);
|
|
165
|
+
localObj = JSON.parse(local);
|
|
166
|
+
remoteObj = JSON.parse(remote);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// If any version is not valid JSON, fall back to line-based merge
|
|
170
|
+
return threeWayMerge(base, local, remote);
|
|
171
|
+
}
|
|
172
|
+
const { value: mergedObj, conflicted, conflictCount } = mergeValues(baseObj, localObj, remoteObj);
|
|
173
|
+
const merged = JSON.stringify(mergedObj, null, 2);
|
|
174
|
+
return {
|
|
175
|
+
success: !conflicted,
|
|
176
|
+
merged,
|
|
177
|
+
hasConflicts: conflicted,
|
|
178
|
+
conflictCount,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Recursively merge three values (base, local, remote).
|
|
183
|
+
* Returns the merged value and whether any conflicts were found.
|
|
184
|
+
*/
|
|
185
|
+
function mergeValues(base, local, remote) {
|
|
186
|
+
// If both local and remote are objects (not arrays), merge field by field
|
|
187
|
+
if (isPlainObject(base) && isPlainObject(local) && isPlainObject(remote)) {
|
|
188
|
+
return mergeObjects(base, local, remote);
|
|
189
|
+
}
|
|
190
|
+
// For non-object values (primitives, arrays, etc.): compare as JSON strings
|
|
191
|
+
const baseStr = JSON.stringify(base);
|
|
192
|
+
const localStr = JSON.stringify(local);
|
|
193
|
+
const remoteStr = JSON.stringify(remote);
|
|
194
|
+
if (baseStr === localStr && baseStr === remoteStr) {
|
|
195
|
+
// No changes
|
|
196
|
+
return { value: local, conflicted: false, conflictCount: 0 };
|
|
197
|
+
}
|
|
198
|
+
if (baseStr === localStr) {
|
|
199
|
+
// Only remote changed → take remote
|
|
200
|
+
return { value: remote, conflicted: false, conflictCount: 0 };
|
|
201
|
+
}
|
|
202
|
+
if (baseStr === remoteStr) {
|
|
203
|
+
// Only local changed → keep local
|
|
204
|
+
return { value: local, conflicted: false, conflictCount: 0 };
|
|
205
|
+
}
|
|
206
|
+
if (localStr === remoteStr) {
|
|
207
|
+
// Both changed identically → take either
|
|
208
|
+
return { value: local, conflicted: false, conflictCount: 0 };
|
|
209
|
+
}
|
|
210
|
+
// Both changed differently → conflict
|
|
211
|
+
// Use a special marker object that will be serialized with conflict info
|
|
212
|
+
return {
|
|
213
|
+
value: {
|
|
214
|
+
'<<<<<<< local': local,
|
|
215
|
+
'=======': '---',
|
|
216
|
+
'>>>>>>> remote': remote,
|
|
217
|
+
},
|
|
218
|
+
conflicted: true,
|
|
219
|
+
conflictCount: 1,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Merge three plain objects field by field.
|
|
224
|
+
*/
|
|
225
|
+
function mergeObjects(base, local, remote) {
|
|
226
|
+
const allKeys = new Set([
|
|
227
|
+
...Object.keys(base),
|
|
228
|
+
...Object.keys(local),
|
|
229
|
+
...Object.keys(remote),
|
|
230
|
+
]);
|
|
231
|
+
const merged = {};
|
|
232
|
+
let hasConflict = false;
|
|
233
|
+
let totalConflicts = 0;
|
|
234
|
+
for (const key of allKeys) {
|
|
235
|
+
const inBase = key in base;
|
|
236
|
+
const inLocal = key in local;
|
|
237
|
+
const inRemote = key in remote;
|
|
238
|
+
// Server-controlled fields: always take remote value
|
|
239
|
+
if (SERVER_CONTROLLED_FIELDS.has(key)) {
|
|
240
|
+
if (inRemote) {
|
|
241
|
+
merged[key] = remote[key];
|
|
242
|
+
}
|
|
243
|
+
else if (inLocal) {
|
|
244
|
+
merged[key] = local[key];
|
|
245
|
+
}
|
|
246
|
+
// If only in base, it was deleted from both → omit
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (inBase && inLocal && inRemote) {
|
|
250
|
+
// Key exists in all three — merge the values
|
|
251
|
+
const result = mergeValues(base[key], local[key], remote[key]);
|
|
252
|
+
merged[key] = result.value;
|
|
253
|
+
if (result.conflicted) {
|
|
254
|
+
hasConflict = true;
|
|
255
|
+
totalConflicts += result.conflictCount;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else if (!inBase && inLocal && inRemote) {
|
|
259
|
+
// Key added by both sides
|
|
260
|
+
const result = mergeValues(undefined, local[key], remote[key]);
|
|
261
|
+
merged[key] = result.value;
|
|
262
|
+
if (result.conflicted) {
|
|
263
|
+
hasConflict = true;
|
|
264
|
+
totalConflicts += result.conflictCount;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else if (!inBase && inLocal && !inRemote) {
|
|
268
|
+
// Key added only locally → keep
|
|
269
|
+
merged[key] = local[key];
|
|
270
|
+
}
|
|
271
|
+
else if (!inBase && !inLocal && inRemote) {
|
|
272
|
+
// Key added only remotely → take
|
|
273
|
+
merged[key] = remote[key];
|
|
274
|
+
}
|
|
275
|
+
else if (inBase && !inLocal && inRemote) {
|
|
276
|
+
// Key deleted locally — check if remote also changed it
|
|
277
|
+
const baseStr = JSON.stringify(base[key]);
|
|
278
|
+
const remoteStr = JSON.stringify(remote[key]);
|
|
279
|
+
if (baseStr === remoteStr) {
|
|
280
|
+
// Remote didn't change it, local deleted it → keep deleted (omit)
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
// Remote changed it but local deleted it → conflict, prefer remote
|
|
284
|
+
merged[key] = remote[key];
|
|
285
|
+
hasConflict = true;
|
|
286
|
+
totalConflicts++;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else if (inBase && inLocal && !inRemote) {
|
|
290
|
+
// Key deleted remotely — check if local also changed it
|
|
291
|
+
const baseStr = JSON.stringify(base[key]);
|
|
292
|
+
const localStr = JSON.stringify(local[key]);
|
|
293
|
+
if (baseStr === localStr) {
|
|
294
|
+
// Local didn't change it, remote deleted it → keep deleted (omit)
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
// Local changed it but remote deleted it → conflict, prefer local
|
|
298
|
+
merged[key] = local[key];
|
|
299
|
+
hasConflict = true;
|
|
300
|
+
totalConflicts++;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else if (inBase && !inLocal && !inRemote) {
|
|
304
|
+
// Deleted by both sides → omit
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return { value: merged, conflicted: hasConflict, conflictCount: totalConflicts };
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Check if a value is a plain object (not array, null, Date, etc.)
|
|
311
|
+
*/
|
|
312
|
+
function isPlainObject(value) {
|
|
313
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Determine whether a local file has been modified compared to the base version.
|
|
317
|
+
*
|
|
318
|
+
* This is used to decide whether three-way merge is needed:
|
|
319
|
+
* - If local === base → local is unmodified, safe to overwrite with remote
|
|
320
|
+
* - If local !== base → local was modified, need three-way merge
|
|
321
|
+
*
|
|
322
|
+
* Uses timestamp normalization to avoid false positives from precision differences
|
|
323
|
+
* (e.g. server returns 7 decimal places but local file has 6).
|
|
324
|
+
*
|
|
325
|
+
* @param base - The base content (from server's baseItems, transformed to local format)
|
|
326
|
+
* @param local - The current local file content
|
|
327
|
+
* @returns true if local content differs from base
|
|
328
|
+
*/
|
|
329
|
+
export function isLocallyModified(base, local) {
|
|
330
|
+
return normalizeForMergeComparison(base) !== normalizeForMergeComparison(local);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Normalize content for merge comparison.
|
|
334
|
+
* Handles trivial whitespace and timestamp precision differences that shouldn't
|
|
335
|
+
* trigger a merge.
|
|
336
|
+
*/
|
|
337
|
+
function normalizeForMergeComparison(content) {
|
|
338
|
+
return content
|
|
339
|
+
.replace(/\r\n/g, '\n')
|
|
340
|
+
.replace(/\s+\n/g, '\n')
|
|
341
|
+
// Normalize ISO timestamp precision: truncate fractional seconds to 6 decimal
|
|
342
|
+
// places (microsecond precision) then strip trailing zeros.
|
|
343
|
+
// e.g. "2026-02-13T10:32:20.2939836Z" → "2026-02-13T10:32:20.293983Z"
|
|
344
|
+
// This prevents false diffs from servers returning 7-digit precision while
|
|
345
|
+
// local serializers use 6-digit precision.
|
|
346
|
+
.replace(/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{1,6})\d*Z/g, '$1Z')
|
|
347
|
+
.replace(/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+?)0+Z/g, '$1Z')
|
|
348
|
+
.trimEnd();
|
|
349
|
+
}
|
|
350
|
+
//# sourceMappingURL=content-merge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-merge.js","sourceRoot":"","sources":["../../src/lib/content-merge.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-leadcms-content.d.ts","sourceRoot":"","sources":["../../src/scripts/fetch-leadcms-content.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC;AAIvB,OAAO,EAQL,WAAW,EACZ,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"fetch-leadcms-content.d.ts","sourceRoot":"","sources":["../../src/scripts/fetch-leadcms-content.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC;AAIvB,OAAO,EAQL,WAAW,EACZ,MAAM,sBAAsB,CAAC;AAa9B,UAAU,SAAS;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,UAAU,gBAAgB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAQD,UAAU,iBAAiB;IACzB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACvC,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,UAAU,eAAe;IACvB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;CACvB;AAuSD;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;AAEnD;;;;GAIG;AACH,iBAAe,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAqCvE;AAED;;;;GAIG;AACH,iBAAS,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAU7D;AAED;;;GAGG;AACH,iBAAe,sBAAsB,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAezF;AAED;;;;;;GAMG;AACH,iBAAe,wBAAwB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BjF;AAED,iBAAe,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CA4QnC;AAGD,OAAO,EAAE,IAAI,IAAI,mBAAmB,EAAE,CAAC;AAGvC,OAAO,EAAE,wBAAwB,EAAE,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,CAAC;AAMzE,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,SAAS,EAAE,gBAAgB,EAAE,CAAC"}
|
|
@@ -3,7 +3,9 @@ import fs from "fs/promises";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import axios from "axios";
|
|
5
5
|
import { downloadMediaFileDirect, leadCMSUrl, leadCMSApiKey, defaultLanguage, CONTENT_DIR, MEDIA_DIR, fetchContentTypes, } from "./leadcms-helpers.js";
|
|
6
|
-
import { saveContentFile } from "../lib/content-transformation.js";
|
|
6
|
+
import { saveContentFile, transformRemoteToLocalFormat } from "../lib/content-transformation.js";
|
|
7
|
+
import { threeWayMerge, threeWayMergeJson, isLocallyModified } from "../lib/content-merge.js";
|
|
8
|
+
import { getConfig } from "../lib/config.js";
|
|
7
9
|
// Add axios request/response interceptors for debugging
|
|
8
10
|
axios.interceptors.request.use((config) => {
|
|
9
11
|
console.log(`[AXIOS REQUEST] ${config.method?.toUpperCase()} ${config.url}`);
|
|
@@ -112,6 +114,7 @@ async function fetchContentSync(syncToken) {
|
|
|
112
114
|
console.log(`[FETCH_CONTENT_SYNC] Fetching public content (no authentication)`);
|
|
113
115
|
let allItems = [];
|
|
114
116
|
let allDeleted = [];
|
|
117
|
+
let allBaseItems = {};
|
|
115
118
|
let token = syncToken || "";
|
|
116
119
|
let nextSyncToken = undefined;
|
|
117
120
|
let page = 0;
|
|
@@ -119,6 +122,10 @@ async function fetchContentSync(syncToken) {
|
|
|
119
122
|
const url = new URL("/api/content/sync", leadCMSUrl);
|
|
120
123
|
url.searchParams.set("filter[limit]", "100");
|
|
121
124
|
url.searchParams.set("syncToken", token);
|
|
125
|
+
// Request base versions for three-way merge when doing incremental sync
|
|
126
|
+
if (syncToken) {
|
|
127
|
+
url.searchParams.set("includeBase", "true");
|
|
128
|
+
}
|
|
122
129
|
console.log(`[FETCH_CONTENT_SYNC] Page ${page}, URL: ${url.toString()}`);
|
|
123
130
|
try {
|
|
124
131
|
// SECURITY: Never send API key for read operations
|
|
@@ -135,6 +142,11 @@ async function fetchContentSync(syncToken) {
|
|
|
135
142
|
if (data.deleted && Array.isArray(data.deleted)) {
|
|
136
143
|
allDeleted.push(...data.deleted);
|
|
137
144
|
}
|
|
145
|
+
// Collect base items for three-way merge support
|
|
146
|
+
if (data.baseItems && typeof data.baseItems === 'object') {
|
|
147
|
+
Object.assign(allBaseItems, data.baseItems);
|
|
148
|
+
console.log(`[FETCH_CONTENT_SYNC] Page ${page} - Got ${Object.keys(data.baseItems).length} base items for merge`);
|
|
149
|
+
}
|
|
138
150
|
const newSyncToken = res.headers["x-next-sync-token"] || token;
|
|
139
151
|
console.log(`[FETCH_CONTENT_SYNC] Next sync token: ${newSyncToken}`);
|
|
140
152
|
if (!newSyncToken || newSyncToken === token) {
|
|
@@ -151,10 +163,11 @@ async function fetchContentSync(syncToken) {
|
|
|
151
163
|
throw error;
|
|
152
164
|
}
|
|
153
165
|
}
|
|
154
|
-
console.log(`[FETCH_CONTENT_SYNC] Completed - Total items: ${allItems.length}, deleted: ${allDeleted.length}`);
|
|
166
|
+
console.log(`[FETCH_CONTENT_SYNC] Completed - Total items: ${allItems.length}, deleted: ${allDeleted.length}, base items: ${Object.keys(allBaseItems).length}`);
|
|
155
167
|
return {
|
|
156
168
|
items: allItems,
|
|
157
169
|
deleted: allDeleted,
|
|
170
|
+
baseItems: allBaseItems,
|
|
158
171
|
nextSyncToken: nextSyncToken || token,
|
|
159
172
|
};
|
|
160
173
|
}
|
|
@@ -380,18 +393,18 @@ async function main() {
|
|
|
380
393
|
const typeMap = await fetchContentTypes();
|
|
381
394
|
const { token: lastSyncToken, migrated: contentTokenMigrated } = await readSyncToken();
|
|
382
395
|
const { token: lastMediaSyncToken, migrated: mediaTokenMigrated } = await readMediaSyncToken();
|
|
383
|
-
let items = [], deleted = [], nextSyncToken = "";
|
|
396
|
+
let items = [], deleted = [], baseItems = {}, nextSyncToken = "";
|
|
384
397
|
let mediaItems = [], mediaDeleted = [], nextMediaSyncToken = "";
|
|
385
398
|
// Sync content (only if supported)
|
|
386
399
|
if (contentSupported) {
|
|
387
400
|
try {
|
|
388
401
|
if (lastSyncToken) {
|
|
389
402
|
console.log(`Syncing content from LeadCMS using sync token: ${lastSyncToken}`);
|
|
390
|
-
({ items, deleted, nextSyncToken } = await fetchContentSync(lastSyncToken));
|
|
403
|
+
({ items, deleted, baseItems, nextSyncToken } = await fetchContentSync(lastSyncToken));
|
|
391
404
|
}
|
|
392
405
|
else {
|
|
393
406
|
console.log("No content sync token found. Doing full fetch from LeadCMS...");
|
|
394
|
-
({ items, deleted, nextSyncToken } = await fetchContentSync(undefined));
|
|
407
|
+
({ items, deleted, baseItems, nextSyncToken } = await fetchContentSync(undefined));
|
|
395
408
|
}
|
|
396
409
|
}
|
|
397
410
|
catch (error) {
|
|
@@ -430,23 +443,103 @@ async function main() {
|
|
|
430
443
|
const contentIdIndex = (items.length > 0 || deleted.length > 0)
|
|
431
444
|
? await buildContentIdIndex(CONTENT_DIR)
|
|
432
445
|
: new Map();
|
|
433
|
-
// Save content files
|
|
446
|
+
// Save content files (with three-way merge support)
|
|
447
|
+
const hasBaseItems = Object.keys(baseItems).length > 0;
|
|
448
|
+
let mergedCount = 0;
|
|
449
|
+
let conflictCount = 0;
|
|
450
|
+
let overwrittenCount = 0;
|
|
451
|
+
let newCount = 0;
|
|
452
|
+
// Build a ContentTypeMap for transformation
|
|
453
|
+
const contentTypeMap = {};
|
|
454
|
+
for (const [key, value] of Object.entries(typeMap)) {
|
|
455
|
+
contentTypeMap[key] = value === 'JSON' ? 'JSON' : 'MDX';
|
|
456
|
+
}
|
|
434
457
|
for (const content of items) {
|
|
435
458
|
if (content && typeof content === "object") {
|
|
459
|
+
const idStr = content.id != null ? String(content.id) : undefined;
|
|
460
|
+
// Determine the file path where this content would be saved
|
|
461
|
+
const contentConfig = getConfig();
|
|
462
|
+
const contentLanguage = content.language || contentConfig.defaultLanguage;
|
|
463
|
+
let targetContentDir = CONTENT_DIR;
|
|
464
|
+
if (contentLanguage !== contentConfig.defaultLanguage) {
|
|
465
|
+
targetContentDir = path.join(CONTENT_DIR, contentLanguage);
|
|
466
|
+
}
|
|
467
|
+
const contentType = contentTypeMap[content.type] || 'MDX';
|
|
468
|
+
const extension = contentType === 'MDX' ? '.mdx' : '.json';
|
|
469
|
+
const expectedPath = path.join(targetContentDir, `${content.slug}${extension}`);
|
|
470
|
+
// Check if local file exists before deleting old paths
|
|
471
|
+
let localContent = null;
|
|
472
|
+
try {
|
|
473
|
+
localContent = await fs.readFile(expectedPath, 'utf8');
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
// File does not exist — this is new content
|
|
477
|
+
}
|
|
436
478
|
// Before saving, remove any existing file with the same id.
|
|
437
479
|
// This handles slug renames, type changes (MDX↔JSON), and language
|
|
438
480
|
// changes — the old file at the previous path is cleaned up before the
|
|
439
481
|
// new version is written at the (potentially different) new path.
|
|
440
|
-
if (
|
|
441
|
-
await deleteContentFilesById(contentIdIndex,
|
|
482
|
+
if (idStr != null) {
|
|
483
|
+
await deleteContentFilesById(contentIdIndex, idStr);
|
|
484
|
+
}
|
|
485
|
+
// Try three-way merge if we have a base version and local file exists
|
|
486
|
+
const baseContent = idStr ? baseItems[idStr] : undefined;
|
|
487
|
+
if (localContent && baseContent && hasBaseItems) {
|
|
488
|
+
// Transform both base and remote to local file format for comparison
|
|
489
|
+
const baseTransformed = await transformRemoteToLocalFormat(baseContent, contentTypeMap);
|
|
490
|
+
const remoteTransformed = await transformRemoteToLocalFormat(content, contentTypeMap);
|
|
491
|
+
if (!isLocallyModified(baseTransformed, localContent)) {
|
|
492
|
+
// Local file is unchanged from base — safe to overwrite with remote
|
|
493
|
+
await saveContentFile({ content, typeMap, contentDir: CONTENT_DIR });
|
|
494
|
+
overwrittenCount++;
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
// Local file was modified — perform three-way merge
|
|
498
|
+
// Use structural merge for JSON (avoids false conflicts from adjacent lines)
|
|
499
|
+
// and line-based merge for MDX
|
|
500
|
+
const mergeResult = contentType === 'JSON'
|
|
501
|
+
? threeWayMergeJson(baseTransformed, localContent, remoteTransformed)
|
|
502
|
+
: threeWayMerge(baseTransformed, localContent, remoteTransformed);
|
|
503
|
+
if (mergeResult.success) {
|
|
504
|
+
// Clean merge — write the merged result
|
|
505
|
+
await fs.mkdir(path.dirname(expectedPath), { recursive: true });
|
|
506
|
+
await fs.writeFile(expectedPath, mergeResult.merged, 'utf8');
|
|
507
|
+
console.log(`🔀 Auto-merged: ${content.slug} (local + remote changes combined)`);
|
|
508
|
+
mergedCount++;
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
// Conflicts — write merged content with conflict markers
|
|
512
|
+
await fs.mkdir(path.dirname(expectedPath), { recursive: true });
|
|
513
|
+
await fs.writeFile(expectedPath, mergeResult.merged, 'utf8');
|
|
514
|
+
console.warn(`⚠️ Conflict in: ${content.slug} (${mergeResult.conflictCount} conflict(s) — manual resolution needed)`);
|
|
515
|
+
conflictCount++;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
// No base available or file is new — overwrite (current behavior)
|
|
521
|
+
await saveContentFile({ content, typeMap, contentDir: CONTENT_DIR });
|
|
522
|
+
if (localContent) {
|
|
523
|
+
overwrittenCount++;
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
newCount++;
|
|
527
|
+
}
|
|
442
528
|
}
|
|
443
|
-
await saveContentFile({
|
|
444
|
-
content,
|
|
445
|
-
typeMap,
|
|
446
|
-
contentDir: CONTENT_DIR,
|
|
447
|
-
});
|
|
448
529
|
}
|
|
449
530
|
}
|
|
531
|
+
// Print merge summary
|
|
532
|
+
if (hasBaseItems && items.length > 0) {
|
|
533
|
+
console.log(`\n📊 Content sync summary:`);
|
|
534
|
+
if (newCount > 0)
|
|
535
|
+
console.log(` ✨ New: ${newCount}`);
|
|
536
|
+
if (overwrittenCount > 0)
|
|
537
|
+
console.log(` 📝 Updated (no local changes): ${overwrittenCount}`);
|
|
538
|
+
if (mergedCount > 0)
|
|
539
|
+
console.log(` 🔀 Auto-merged: ${mergedCount}`);
|
|
540
|
+
if (conflictCount > 0)
|
|
541
|
+
console.log(` ⚠️ Conflicts (need manual resolution): ${conflictCount}`);
|
|
542
|
+
}
|
|
450
543
|
// Remove deleted content files from all language directories
|
|
451
544
|
for (const id of deleted) {
|
|
452
545
|
await deleteContentFilesById(contentIdIndex, String(id));
|