@leadcms/sdk 1.3.0-pre → 2.1.0

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.
Files changed (56) hide show
  1. package/README.md +82 -3
  2. package/dist/cli/index.js +55 -62
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2 -18
  7. package/dist/index.js.map +1 -1
  8. package/dist/lib/cms.d.ts +1 -1
  9. package/dist/lib/cms.d.ts.map +1 -1
  10. package/dist/lib/cms.js +49 -72
  11. package/dist/lib/cms.js.map +1 -1
  12. package/dist/lib/config.d.ts +0 -8
  13. package/dist/lib/config.d.ts.map +1 -1
  14. package/dist/lib/config.js +13 -40
  15. package/dist/lib/config.js.map +1 -1
  16. package/dist/lib/console-colors.d.ts +49 -0
  17. package/dist/lib/console-colors.d.ts.map +1 -0
  18. package/dist/lib/console-colors.js +121 -0
  19. package/dist/lib/console-colors.js.map +1 -0
  20. package/dist/lib/content-transformation.d.ts +90 -0
  21. package/dist/lib/content-transformation.d.ts.map +1 -0
  22. package/dist/lib/content-transformation.js +335 -0
  23. package/dist/lib/content-transformation.js.map +1 -0
  24. package/dist/lib/data-service.d.ts +97 -0
  25. package/dist/lib/data-service.d.ts.map +1 -0
  26. package/dist/lib/data-service.js +389 -0
  27. package/dist/lib/data-service.js.map +1 -0
  28. package/dist/scripts/fetch-leadcms-content.d.ts +19 -0
  29. package/dist/scripts/fetch-leadcms-content.d.ts.map +1 -0
  30. package/dist/scripts/fetch-leadcms-content.js +301 -0
  31. package/dist/scripts/fetch-leadcms-content.js.map +1 -0
  32. package/dist/scripts/generate-env-js.d.ts +2 -0
  33. package/dist/scripts/generate-env-js.d.ts.map +1 -0
  34. package/dist/scripts/generate-env-js.js +22 -0
  35. package/dist/scripts/generate-env-js.js.map +1 -0
  36. package/dist/scripts/leadcms-helpers.d.ts +25 -0
  37. package/dist/scripts/leadcms-helpers.d.ts.map +1 -0
  38. package/dist/scripts/leadcms-helpers.js +78 -0
  39. package/dist/scripts/leadcms-helpers.js.map +1 -0
  40. package/dist/scripts/push-leadcms-content.d.ts +50 -0
  41. package/dist/scripts/push-leadcms-content.d.ts.map +1 -0
  42. package/dist/scripts/push-leadcms-content.js +1022 -0
  43. package/dist/scripts/push-leadcms-content.js.map +1 -0
  44. package/dist/scripts/sse-watcher.d.ts +20 -0
  45. package/dist/scripts/sse-watcher.d.ts.map +1 -0
  46. package/dist/scripts/sse-watcher.js +268 -0
  47. package/dist/scripts/sse-watcher.js.map +1 -0
  48. package/dist/scripts/status-leadcms-content.d.ts +4 -0
  49. package/dist/scripts/status-leadcms-content.d.ts.map +1 -0
  50. package/dist/scripts/status-leadcms-content.js +36 -0
  51. package/dist/scripts/status-leadcms-content.js.map +1 -0
  52. package/package.json +14 -12
  53. package/dist/scripts/fetch-leadcms-content.mjs +0 -367
  54. package/dist/scripts/generate-env-js.mjs +0 -24
  55. package/dist/scripts/leadcms-helpers.mjs +0 -208
  56. package/dist/scripts/sse-watcher.mjs +0 -300
@@ -0,0 +1,1022 @@
1
+ import "dotenv/config";
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+ import readline from "readline";
5
+ import matter from "gray-matter";
6
+ import * as Diff from "diff";
7
+ import { defaultLanguage, CONTENT_DIR, } from "./leadcms-helpers.js";
8
+ import { leadCMSDataService } from "../lib/data-service.js";
9
+ import { transformRemoteToLocalFormat, transformRemoteForComparison, hasContentDifferences, replaceLocalMediaPaths } from "../lib/content-transformation.js";
10
+ import { colorConsole, statusColors, diffColors } from '../lib/console-colors.js';
11
+ // Create readline interface for user prompts
12
+ const rl = readline.createInterface({
13
+ input: process.stdin,
14
+ output: process.stdout
15
+ });
16
+ // Promisify readline question
17
+ function question(prompt) {
18
+ return new Promise((resolve) => {
19
+ rl.question(prompt, resolve);
20
+ });
21
+ }
22
+ /**
23
+ * Check if a directory is a locale directory
24
+ * Only immediate children of CONTENT_DIR with 2-5 letter language codes are considered locales
25
+ */
26
+ async function isLocaleDirectory(dirPath, parentDir) {
27
+ try {
28
+ // Only consider directories that are immediate children of CONTENT_DIR
29
+ if (parentDir !== CONTENT_DIR) {
30
+ return false;
31
+ }
32
+ const dirName = path.basename(dirPath);
33
+ // Check if it matches language code pattern (2-5 letters, optionally with region codes)
34
+ // Examples: en, da, ru, en-US, pt-BR, zh-CN
35
+ const isLanguageCode = /^[a-z]{2}(-[A-Z]{2})?$/.test(dirName);
36
+ return isLanguageCode;
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ } /**
42
+ * Read and parse all local content files
43
+ */
44
+ async function readLocalContent() {
45
+ console.log(`[LOCAL] Reading content from: ${CONTENT_DIR}`);
46
+ const localContent = [];
47
+ async function walkDirectory(dir, locale = defaultLanguage, baseContentDir = CONTENT_DIR) {
48
+ try {
49
+ const entries = await fs.readdir(dir, { withFileTypes: true });
50
+ for (const entry of entries) {
51
+ const fullPath = path.join(dir, entry.name);
52
+ if (entry.isDirectory()) {
53
+ // Check if this is a locale directory (only immediate children of content dir)
54
+ if (entry.name !== defaultLanguage && await isLocaleDirectory(fullPath, dir)) {
55
+ // This is a language directory
56
+ await walkDirectory(fullPath, entry.name, fullPath);
57
+ }
58
+ else {
59
+ // Regular directory, keep current locale and baseContentDir
60
+ await walkDirectory(fullPath, locale, baseContentDir);
61
+ }
62
+ }
63
+ else if (entry.isFile() && (entry.name.endsWith('.mdx') || entry.name.endsWith('.json'))) {
64
+ try {
65
+ const content = await parseContentFile(fullPath, locale, baseContentDir);
66
+ if (content) {
67
+ localContent.push(content);
68
+ }
69
+ }
70
+ catch (error) {
71
+ console.warn(`[LOCAL] Failed to parse ${fullPath}:`, error.message);
72
+ }
73
+ }
74
+ }
75
+ }
76
+ catch (error) {
77
+ console.warn(`[LOCAL] Failed to read directory ${dir}:`, error.message);
78
+ }
79
+ }
80
+ await walkDirectory(CONTENT_DIR);
81
+ console.log(`[LOCAL] Found ${localContent.length} local content files`);
82
+ return localContent;
83
+ }
84
+ /**
85
+ * Parse a single content file (MDX or JSON)
86
+ */
87
+ async function parseContentFile(filePath, locale, baseContentDir = CONTENT_DIR) {
88
+ const fileContent = await fs.readFile(filePath, 'utf-8');
89
+ const ext = path.extname(filePath);
90
+ const basename = path.basename(filePath, ext);
91
+ let metadata;
92
+ let body = '';
93
+ if (ext === '.mdx') {
94
+ const parsed = matter(fileContent);
95
+ metadata = parsed.data;
96
+ body = parsed.content.trim();
97
+ }
98
+ else if (ext === '.json') {
99
+ const jsonData = JSON.parse(fileContent);
100
+ body = jsonData.body || '';
101
+ metadata = { ...jsonData };
102
+ delete metadata.body;
103
+ }
104
+ else {
105
+ return null;
106
+ }
107
+ // Calculate slug from relative path within the content directory
108
+ // This includes subdirectories like blog/, docs/, legal/ as part of the slug
109
+ const relativePath = path.relative(baseContentDir, filePath);
110
+ const relativeDir = path.dirname(relativePath);
111
+ let slug;
112
+ if (relativeDir === '.' || relativeDir === '') {
113
+ // File is directly in the content/locale directory
114
+ slug = basename;
115
+ }
116
+ else {
117
+ // File is in a subdirectory, include the path
118
+ slug = path.join(relativeDir, basename).replace(/\\/g, '/'); // Normalize to forward slashes
119
+ }
120
+ return {
121
+ filePath,
122
+ slug,
123
+ locale,
124
+ type: metadata.type,
125
+ metadata,
126
+ body,
127
+ isLocal: true
128
+ };
129
+ }
130
+ /**
131
+ * Get all unique content types from local content
132
+ */
133
+ function getLocalContentTypes(localContent) {
134
+ const types = new Set();
135
+ for (const content of localContent) {
136
+ if (content.type) {
137
+ types.add(content.type);
138
+ }
139
+ }
140
+ return types;
141
+ }
142
+ /**
143
+ * Fetch all remote content using sync API without token (full fetch)
144
+ */
145
+ async function fetchRemoteContent() {
146
+ const allItems = await leadCMSDataService.getAllContent();
147
+ // Ensure we have an array
148
+ if (!Array.isArray(allItems)) {
149
+ console.warn(`[${leadCMSDataService.isMockMode() ? 'MOCK' : 'REMOTE'}] Retrieved invalid data (not an array):`, typeof allItems);
150
+ return [];
151
+ }
152
+ console.log(`[${leadCMSDataService.isMockMode() ? 'MOCK' : 'REMOTE'}] Retrieved ${allItems.length} content items`);
153
+ return allItems.map(item => ({ ...item, isLocal: false }));
154
+ }
155
+ /**
156
+ * Compare local and remote content by transforming remote to local format
157
+ * Returns true if there are meaningful differences in content
158
+ * This new approach compares normalized file content directly instead of parsed objects
159
+ */
160
+ async function hasActualContentChanges(local, remote, typeMap) {
161
+ try {
162
+ // Read the local file content as-is
163
+ const localFileContent = await fs.readFile(local.filePath, 'utf-8');
164
+ // Transform remote content for comparison, only including fields that exist in local content
165
+ // This prevents false positives when remote has additional fields like updatedAt
166
+ const transformedRemoteContent = await transformRemoteForComparison(remote, localFileContent, typeMap);
167
+ // Compare the raw file contents using shared normalization logic
168
+ const hasFileContentChanges = hasContentDifferences(localFileContent, transformedRemoteContent);
169
+ return hasFileContentChanges;
170
+ }
171
+ catch (error) {
172
+ console.warn(`[COMPARE] Failed to compare content for ${local.slug}:`, error.message);
173
+ // Fallback to true to err on the side of showing changes
174
+ return true;
175
+ }
176
+ }
177
+ /**
178
+ * Match local content with remote content
179
+ */
180
+ async function matchContent(localContent, remoteContent, typeMap) {
181
+ const operations = {
182
+ create: [],
183
+ update: [],
184
+ rename: [],
185
+ typeChange: [],
186
+ conflict: []
187
+ };
188
+ for (const local of localContent) {
189
+ let match = undefined;
190
+ // First try to match by ID if local content has one
191
+ if (local.metadata.id) {
192
+ match = remoteContent.find(remote => remote.id === local.metadata.id);
193
+ }
194
+ // If no ID match, try to match by current filename slug and locale
195
+ if (!match) {
196
+ match = remoteContent.find(remote => remote.slug === local.slug &&
197
+ (remote.language || defaultLanguage) === local.locale);
198
+ }
199
+ // If still no match, try by the slug in metadata (could be old slug for renames)
200
+ if (!match && local.metadata.slug && local.metadata.slug !== local.slug) {
201
+ match = remoteContent.find(remote => remote.slug === local.metadata.slug &&
202
+ (remote.language || defaultLanguage) === local.locale);
203
+ }
204
+ // If still no match, try by title and locale (if title exists)
205
+ if (!match && local.metadata.title) {
206
+ match = remoteContent.find(remote => remote.title === local.metadata.title &&
207
+ (remote.language || defaultLanguage) === local.locale);
208
+ }
209
+ if (match) {
210
+ // Check for conflicts by comparing updatedAt timestamps from content metadata
211
+ const localUpdated = local.metadata.updatedAt ? new Date(local.metadata.updatedAt) : new Date(0);
212
+ const remoteUpdated = match.updatedAt ? new Date(match.updatedAt) : new Date(0);
213
+ // Detect different types of changes
214
+ const slugChanged = match.slug !== local.slug;
215
+ const typeChanged = match.type !== local.type;
216
+ if (remoteUpdated > localUpdated) {
217
+ let conflictReason = 'Remote content was updated after local content';
218
+ if (slugChanged && typeChanged) {
219
+ conflictReason = 'Both slug and content type changed remotely';
220
+ }
221
+ else if (slugChanged) {
222
+ conflictReason = 'Slug changed remotely after local changes';
223
+ }
224
+ else if (typeChanged) {
225
+ conflictReason = 'Content type changed remotely after local changes';
226
+ }
227
+ operations.conflict.push({
228
+ local,
229
+ remote: match,
230
+ reason: conflictReason
231
+ });
232
+ }
233
+ else if (slugChanged && typeChanged) {
234
+ // Both slug and type changed - this is a complex update
235
+ operations.typeChange.push({
236
+ local,
237
+ remote: match,
238
+ oldSlug: match.slug,
239
+ oldType: match.type,
240
+ newType: local.type
241
+ });
242
+ }
243
+ else if (slugChanged) {
244
+ // Slug changed - this is a rename
245
+ operations.rename.push({
246
+ local,
247
+ remote: match,
248
+ oldSlug: match.slug
249
+ });
250
+ }
251
+ else if (typeChanged) {
252
+ // Content type changed
253
+ operations.typeChange.push({
254
+ local,
255
+ remote: match,
256
+ oldType: match.type,
257
+ newType: local.type
258
+ });
259
+ }
260
+ else {
261
+ // Check if content actually changed by comparing all fields
262
+ const hasContentChanges = await hasActualContentChanges(local, match, typeMap);
263
+ if (hasContentChanges) {
264
+ // Regular update - content modified but slug and type same
265
+ operations.update.push({
266
+ local,
267
+ remote: match
268
+ });
269
+ }
270
+ // If no content changes, don't add to any operation (content is in sync)
271
+ }
272
+ }
273
+ else {
274
+ // No match found, this is a new content item
275
+ operations.create.push({
276
+ local
277
+ });
278
+ }
279
+ }
280
+ return operations;
281
+ }
282
+ /**
283
+ * Validate that all required content types exist remotely
284
+ */
285
+ async function validateContentTypes(localTypes, remoteTypeMap, dryRun = false) {
286
+ const missingTypes = [];
287
+ for (const type of localTypes) {
288
+ if (!remoteTypeMap[type]) {
289
+ missingTypes.push(type);
290
+ }
291
+ }
292
+ if (missingTypes.length > 0) {
293
+ colorConsole.error(`\n❌ Missing content types in remote LeadCMS: ${colorConsole.highlight(missingTypes.join(', '))}`);
294
+ colorConsole.warn(`\nYou need to create these content types in your LeadCMS instance before pushing content.`);
295
+ if (dryRun) {
296
+ colorConsole.info('\n🧪 In dry run mode - showing what content type creation would look like:');
297
+ for (const type of missingTypes) {
298
+ colorConsole.progress(`\n📋 CREATE CONTENT TYPE (Dry Run):`);
299
+ colorConsole.log(`\n${colorConsole.cyan('POST')} ${colorConsole.highlight('/api/content-types')}`);
300
+ colorConsole.log(`${colorConsole.gray('Content-Type:')} application/json`);
301
+ colorConsole.log(`\n${colorConsole.gray('Request Body:')}`);
302
+ const sampleContentTypeData = {
303
+ uid: type,
304
+ name: type.charAt(0).toUpperCase() + type.slice(1),
305
+ format: 'MDX',
306
+ supportsCoverImage: false,
307
+ supportsComments: false
308
+ };
309
+ colorConsole.log(JSON.stringify(sampleContentTypeData, null, 2));
310
+ colorConsole.success(`✅ Would create content type: ${colorConsole.highlight(type)}`);
311
+ }
312
+ return; // Skip interactive creation in dry run mode
313
+ }
314
+ const createChoice = await question('\nWould you like me to create these content types automatically? (y/N): ');
315
+ if (createChoice.toLowerCase() === 'y' || createChoice.toLowerCase() === 'yes') {
316
+ for (const type of missingTypes) {
317
+ await createContentTypeInteractive(type, dryRun);
318
+ }
319
+ }
320
+ else {
321
+ colorConsole.info('\nPlease create the missing content types manually in your LeadCMS instance and try again.');
322
+ process.exit(1);
323
+ }
324
+ }
325
+ }
326
+ /**
327
+ * Create a content type in remote LeadCMS
328
+ */
329
+ async function createContentTypeInteractive(typeName, dryRun = false) {
330
+ colorConsole.progress(`\n📝 Creating content type: ${colorConsole.highlight(typeName)}`);
331
+ const format = await question(`What format should '${colorConsole.highlight(typeName)}' use? (MDX/JSON) [MDX]: `) || 'MDX';
332
+ const supportsCoverImage = await question(`Should '${colorConsole.highlight(typeName)}' support cover images? (y/N): `);
333
+ const supportsComments = await question(`Should '${colorConsole.highlight(typeName)}' support comments? (y/N): `);
334
+ const contentTypeData = {
335
+ uid: typeName,
336
+ name: typeName.charAt(0).toUpperCase() + typeName.slice(1),
337
+ format: format.toUpperCase(),
338
+ supportsCoverImage: supportsCoverImage.toLowerCase() === 'y',
339
+ supportsComments: supportsComments.toLowerCase() === 'y'
340
+ };
341
+ if (dryRun) {
342
+ colorConsole.progress(`\n📋 CREATE CONTENT TYPE (Dry Run):`);
343
+ colorConsole.log(`\n${colorConsole.cyan('POST')} ${colorConsole.highlight('/api/content-types')}`);
344
+ colorConsole.log(`${colorConsole.gray('Content-Type:')} application/json`);
345
+ colorConsole.log(`\n${colorConsole.gray('Request Body:')}`);
346
+ colorConsole.log(JSON.stringify(contentTypeData, null, 2));
347
+ colorConsole.success(`✅ Would create content type: ${colorConsole.highlight(typeName)}`);
348
+ }
349
+ else {
350
+ try {
351
+ await leadCMSDataService.createContentType(contentTypeData);
352
+ colorConsole.success(`✅ Created content type: ${colorConsole.highlight(typeName)}`);
353
+ }
354
+ catch (error) {
355
+ colorConsole.error(`❌ Failed to create content type '${colorConsole.highlight(typeName)}':`, error.message);
356
+ throw error;
357
+ }
358
+ }
359
+ }
360
+ /**
361
+ * Filter content operations to only include specific content by ID or slug
362
+ */
363
+ function filterContentOperations(operations, targetId, targetSlug) {
364
+ if (!targetId && !targetSlug) {
365
+ return operations; // No filtering needed
366
+ }
367
+ const matchesTarget = (op) => {
368
+ if (targetId) {
369
+ // Check if local content has the target ID
370
+ if (op.local.metadata.id?.toString() === targetId)
371
+ return true;
372
+ // Check if remote content has the target ID
373
+ if (op.remote?.id?.toString() === targetId)
374
+ return true;
375
+ }
376
+ if (targetSlug) {
377
+ // Check if local content has the target slug
378
+ if (op.local.slug === targetSlug)
379
+ return true;
380
+ // Check if remote content has the target slug
381
+ if (op.remote?.slug === targetSlug)
382
+ return true;
383
+ // Check if this is a rename and the old slug matches
384
+ if (op.oldSlug === targetSlug)
385
+ return true;
386
+ }
387
+ return false;
388
+ };
389
+ return {
390
+ create: operations.create.filter(matchesTarget),
391
+ update: operations.update.filter(matchesTarget),
392
+ rename: operations.rename.filter(matchesTarget),
393
+ typeChange: operations.typeChange.filter(matchesTarget),
394
+ conflict: operations.conflict.filter(matchesTarget)
395
+ };
396
+ }
397
+ /**
398
+ * Display detailed diff for a single content item
399
+ */
400
+ async function displayDetailedDiff(operation, operationType, typeMap) {
401
+ const { local, remote } = operation;
402
+ console.log(`\n📄 Detailed Changes for: ${local.slug} [${local.locale}]`);
403
+ console.log(` Operation: ${operationType}`);
404
+ console.log(` Content Type: ${local.type}`);
405
+ if (remote?.id) {
406
+ console.log(` Remote ID: ${remote.id}`);
407
+ }
408
+ console.log('');
409
+ // Compare content using the new transformation approach
410
+ console.log('\n📝 Content Changes:');
411
+ try {
412
+ // Read local file content as-is
413
+ const localFileContent = await fs.readFile(local.filePath, 'utf-8');
414
+ // Transform remote content to local format for comparison
415
+ const transformedRemoteContent = remote ? await transformRemoteToLocalFormat(remote, typeMap) : '';
416
+ if (localFileContent.trim() === transformedRemoteContent.trim()) {
417
+ console.log(' No content changes detected');
418
+ }
419
+ else {
420
+ // Use line-by-line diff for detailed comparison
421
+ const diff = Diff.diffLines(transformedRemoteContent, localFileContent);
422
+ let addedLines = 0;
423
+ let removedLines = 0;
424
+ let unchangedLines = 0;
425
+ // Show diff preview and count changes
426
+ colorConsole.info(' Content diff preview:');
427
+ let previewLines = 0;
428
+ const maxPreviewLines = 10;
429
+ for (const part of diff) {
430
+ // Count non-empty lines only for more accurate statistics
431
+ const lines = part.value.split('\n').filter((line) => line.trim() !== '');
432
+ if (part.added) {
433
+ addedLines += lines.length;
434
+ if (previewLines < maxPreviewLines) {
435
+ for (const line of lines.slice(0, Math.min(lines.length, maxPreviewLines - previewLines))) {
436
+ colorConsole.log(` ${diffColors.added(`+ ${line}`)}`);
437
+ previewLines++;
438
+ }
439
+ }
440
+ }
441
+ else if (part.removed) {
442
+ removedLines += lines.length;
443
+ if (previewLines < maxPreviewLines) {
444
+ for (const line of lines.slice(0, Math.min(lines.length, maxPreviewLines - previewLines))) {
445
+ colorConsole.log(` ${diffColors.removed(`- ${line}`)}`);
446
+ previewLines++;
447
+ }
448
+ }
449
+ }
450
+ else {
451
+ unchangedLines += lines.length;
452
+ }
453
+ if (previewLines >= maxPreviewLines)
454
+ break;
455
+ }
456
+ if (previewLines >= maxPreviewLines && (addedLines + removedLines > previewLines)) {
457
+ colorConsole.gray(` ... (${addedLines + removedLines - previewLines} more changes)`);
458
+ }
459
+ const summaryText = `\n 📊 Change Summary: ${colorConsole.green(`+${addedLines} lines added`)}, ${colorConsole.red(`-${removedLines} lines removed`)}, ${unchangedLines} lines unchanged`;
460
+ colorConsole.log(summaryText);
461
+ }
462
+ }
463
+ catch (error) {
464
+ console.warn(`[DIFF] Failed to generate detailed diff for ${local.slug}:`, error.message);
465
+ console.log(' Unable to show content comparison');
466
+ }
467
+ console.log('');
468
+ }
469
+ /**
470
+ * Display status/preview of changes
471
+ */
472
+ async function displayStatus(operations, isStatusOnly = false, isSingleFile = false, showDetailedPreview = false, typeMap) {
473
+ if (isSingleFile) {
474
+ colorConsole.important('\n📄 LeadCMS File Status');
475
+ }
476
+ else {
477
+ colorConsole.important('\n📊 LeadCMS Status');
478
+ }
479
+ colorConsole.log('');
480
+ // Summary line like git
481
+ const totalChanges = operations.create.length + operations.update.length + operations.rename.length + operations.typeChange.length + operations.conflict.length;
482
+ if (totalChanges === 0) {
483
+ if (isSingleFile) {
484
+ colorConsole.success('✅ File is in sync with remote content!');
485
+ }
486
+ else {
487
+ colorConsole.success('✅ No changes detected. Everything is in sync!');
488
+ }
489
+ return;
490
+ }
491
+ // For single file mode, show detailed diff information
492
+ if (isSingleFile) {
493
+ // Show detailed diff for each operation
494
+ for (const op of operations.create) {
495
+ await displayDetailedDiff(op, 'New file', typeMap);
496
+ }
497
+ for (const op of operations.update) {
498
+ await displayDetailedDiff(op, 'Modified', typeMap);
499
+ }
500
+ for (const op of operations.rename) {
501
+ await displayDetailedDiff(op, `Renamed (${op.oldSlug} → ${op.local.slug})`, typeMap);
502
+ }
503
+ for (const op of operations.typeChange) {
504
+ await displayDetailedDiff(op, `Type changed (${op.oldType} → ${op.newType})`, typeMap);
505
+ }
506
+ for (const op of operations.conflict) {
507
+ await displayDetailedDiff(op, `Conflict: ${op.reason}`, typeMap);
508
+ }
509
+ return;
510
+ }
511
+ // Changes to be synced (like git's "Changes to be committed")
512
+ if (operations.create.length > 0 || operations.update.length > 0 || operations.rename.length > 0 || operations.typeChange.length > 0) {
513
+ const syncableChanges = operations.create.length + operations.update.length + operations.rename.length + operations.typeChange.length;
514
+ console.log(`Changes to be synced (${syncableChanges} files):`);
515
+ if (!isStatusOnly) {
516
+ console.log(' (use "leadcms status" to see sync status)');
517
+ }
518
+ console.log('');
519
+ // Helper function to sort operations by locale (ASC) then slug (ASC)
520
+ const sortOperations = (ops) => {
521
+ return ops.sort((a, b) => {
522
+ // First sort by locale
523
+ if (a.local.locale !== b.local.locale) {
524
+ return a.local.locale.localeCompare(b.local.locale);
525
+ }
526
+ // Then sort by slug within the same locale
527
+ return a.local.slug.localeCompare(b.local.slug);
528
+ });
529
+ };
530
+ // New content
531
+ for (const op of sortOperations([...operations.create])) {
532
+ const typeLabel = op.local.type.padEnd(12);
533
+ const localeLabel = `[${op.local.locale}]`.padEnd(6);
534
+ colorConsole.log(` ${statusColors.created('new file:')} ${typeLabel} ${localeLabel} ${colorConsole.highlight(op.local.slug)}`);
535
+ }
536
+ // Modified content
537
+ for (const op of sortOperations([...operations.update])) {
538
+ const typeLabel = op.local.type.padEnd(12);
539
+ const localeLabel = `[${op.local.locale}]`.padEnd(6);
540
+ const idLabel = op.remote?.id ? `(ID: ${op.remote.id})` : '';
541
+ colorConsole.log(` ${statusColors.modified('modified:')} ${typeLabel} ${localeLabel} ${colorConsole.highlight(op.local.slug)} ${colorConsole.gray(idLabel)}`);
542
+ }
543
+ // Renamed content (slug changed)
544
+ for (const op of sortOperations([...operations.rename])) {
545
+ const typeLabel = op.local.type.padEnd(12);
546
+ const localeLabel = `[${op.local.locale}]`.padEnd(6);
547
+ const idLabel = op.remote?.id ? `(ID: ${op.remote.id})` : '';
548
+ colorConsole.log(` ${statusColors.renamed('renamed:')} ${typeLabel} ${localeLabel} ${colorConsole.gray(op.oldSlug || 'unknown')} -> ${colorConsole.highlight(op.local.slug)} ${colorConsole.gray(idLabel)}`);
549
+ }
550
+ // Type changed content
551
+ for (const op of sortOperations([...operations.typeChange])) {
552
+ const typeLabel = op.local.type.padEnd(12);
553
+ const localeLabel = `[${op.local.locale}]`.padEnd(6);
554
+ const idLabel = op.remote?.id ? `(ID: ${op.remote.id})` : '';
555
+ const typeChangeLabel = `(${colorConsole.gray(op.oldType || 'unknown')} -> ${colorConsole.highlight(op.newType || 'unknown')})`;
556
+ colorConsole.log(` ${statusColors.typeChange('type change:')}${typeLabel} ${localeLabel} ${colorConsole.highlight(op.local.slug)} ${typeChangeLabel} ${colorConsole.gray(idLabel)}`);
557
+ }
558
+ // Show detailed previews if requested (and not in single file mode which already shows them)
559
+ if (showDetailedPreview && !isSingleFile) {
560
+ console.log('');
561
+ console.log('📋 Detailed Change Previews:');
562
+ console.log('');
563
+ // Show detailed diff for each operation (same as single file mode)
564
+ for (const op of sortOperations([...operations.create])) {
565
+ await displayDetailedDiff(op, 'New file', typeMap);
566
+ }
567
+ for (const op of sortOperations([...operations.update])) {
568
+ await displayDetailedDiff(op, 'Modified', typeMap);
569
+ }
570
+ for (const op of sortOperations([...operations.rename])) {
571
+ await displayDetailedDiff(op, `Renamed (${op.oldSlug} → ${op.local.slug})`, typeMap);
572
+ }
573
+ for (const op of sortOperations([...operations.typeChange])) {
574
+ await displayDetailedDiff(op, `Type changed (${op.oldType} → ${op.newType})`, typeMap);
575
+ }
576
+ }
577
+ console.log('');
578
+ } // Conflicts (like git's merge conflicts)
579
+ if (operations.conflict.length > 0) {
580
+ colorConsole.warn(`⚠️ Unmerged conflicts (${operations.conflict.length} files):`);
581
+ colorConsole.info(' (use "leadcms pull" to merge remote changes)');
582
+ colorConsole.log('');
583
+ // Sort conflicts by locale then slug as well
584
+ const sortedConflicts = [...operations.conflict].sort((a, b) => {
585
+ if (a.local.locale !== b.local.locale) {
586
+ return a.local.locale.localeCompare(b.local.locale);
587
+ }
588
+ return a.local.slug.localeCompare(b.local.slug);
589
+ });
590
+ for (const op of sortedConflicts) {
591
+ const typeLabel = op.local.type.padEnd(12);
592
+ const localeLabel = `[${op.local.locale}]`.padEnd(6);
593
+ colorConsole.log(` ${statusColors.conflict('conflict:')} ${typeLabel} ${localeLabel} ${colorConsole.highlight(op.local.slug)}`);
594
+ colorConsole.log(` ${colorConsole.gray(op.reason || 'Unknown conflict')}`);
595
+ }
596
+ colorConsole.log('');
597
+ // Show detailed previews for conflicts if requested (and not in single file mode)
598
+ if (showDetailedPreview && !isSingleFile && operations.conflict.length > 0) {
599
+ console.log('');
600
+ console.log('📋 Detailed Conflict Previews:');
601
+ console.log('');
602
+ for (const op of sortedConflicts) {
603
+ await displayDetailedDiff(op, `Conflict: ${op.reason}`, typeMap);
604
+ }
605
+ }
606
+ if (!isStatusOnly) {
607
+ colorConsole.important('💡 To resolve conflicts:');
608
+ colorConsole.info(' • Run "leadcms pull" to fetch latest changes');
609
+ colorConsole.info(' • Resolve conflicts in local files');
610
+ colorConsole.info(' • Run "leadcms push" again');
611
+ colorConsole.warn(' • Or use "leadcms push --force" to override remote changes (⚠️ data loss risk)');
612
+ colorConsole.log('');
613
+ }
614
+ }
615
+ }
616
+ /**
617
+ * Display what API calls would be made without executing them
618
+ */
619
+ async function showDryRunOperations(operations) {
620
+ // Check if there are any operations to show
621
+ const totalOperations = operations.create.length + operations.update.length +
622
+ operations.rename.length + operations.typeChange.length;
623
+ if (totalOperations === 0) {
624
+ return; // Don't show dry run preview if there are no operations
625
+ }
626
+ colorConsole.important('\n🧪 Dry Run Mode - API Calls Preview');
627
+ colorConsole.info('The following API calls would be made:\n');
628
+ // Create operations
629
+ if (operations.create.length > 0) {
630
+ colorConsole.progress(`\n📤 CREATE Operations (${operations.create.length}):`);
631
+ for (const op of operations.create) {
632
+ const contentData = formatContentForAPI(op.local);
633
+ colorConsole.log(`\n${colorConsole.cyan('POST')} ${colorConsole.highlight('/api/content')}`);
634
+ colorConsole.log(`${colorConsole.gray('Content-Type:')} application/json`);
635
+ colorConsole.log(`\n${colorConsole.gray('Request Body:')}`);
636
+ colorConsole.log(JSON.stringify(contentData, null, 2));
637
+ }
638
+ }
639
+ // Update operations
640
+ if (operations.update.length > 0) {
641
+ colorConsole.progress(`\n🔄 UPDATE Operations (${operations.update.length}):`);
642
+ for (const op of operations.update) {
643
+ if (op.remote?.id) {
644
+ const contentData = formatContentForAPI(op.local);
645
+ colorConsole.log(`\n${colorConsole.yellow('PUT')} ${colorConsole.highlight(`/api/content/${op.remote.id}`)}`);
646
+ colorConsole.log(`${colorConsole.gray('Content-Type:')} application/json`);
647
+ colorConsole.log(`\n${colorConsole.gray('Request Body:')}`);
648
+ colorConsole.log(JSON.stringify(contentData, null, 2));
649
+ }
650
+ }
651
+ }
652
+ // Rename operations (implemented as updates)
653
+ if (operations.rename.length > 0) {
654
+ colorConsole.progress(`\n📝 RENAME Operations (${operations.rename.length}):`);
655
+ for (const op of operations.rename) {
656
+ if (op.remote?.id) {
657
+ const contentData = formatContentForAPI(op.local);
658
+ colorConsole.log(`\n${colorConsole.yellow('PUT')} ${colorConsole.highlight(`/api/content/${op.remote.id}`)}`);
659
+ colorConsole.log(`${colorConsole.gray('Content-Type:')} application/json`);
660
+ colorConsole.log(`${colorConsole.gray('Note:')} Renaming ${colorConsole.gray(op.oldSlug || 'unknown')} → ${colorConsole.highlight(op.local.slug)}`);
661
+ colorConsole.log(`\n${colorConsole.gray('Request Body:')}`);
662
+ colorConsole.log(JSON.stringify(contentData, null, 2));
663
+ }
664
+ }
665
+ }
666
+ // Type change operations (implemented as updates)
667
+ if (operations.typeChange.length > 0) {
668
+ colorConsole.progress(`\n🔀 TYPE CHANGE Operations (${operations.typeChange.length}):`);
669
+ for (const op of operations.typeChange) {
670
+ if (op.remote?.id) {
671
+ const contentData = formatContentForAPI(op.local);
672
+ colorConsole.log(`\n${colorConsole.yellow('PUT')} ${colorConsole.highlight(`/api/content/${op.remote.id}`)}`);
673
+ colorConsole.log(`${colorConsole.gray('Content-Type:')} application/json`);
674
+ colorConsole.log(`${colorConsole.gray('Note:')} Type change ${colorConsole.gray(op.oldType || 'unknown')} → ${colorConsole.highlight(op.newType || 'unknown')}`);
675
+ colorConsole.log(`\n${colorConsole.gray('Request Body:')}`);
676
+ colorConsole.log(JSON.stringify(contentData, null, 2));
677
+ }
678
+ }
679
+ }
680
+ colorConsole.log('\n');
681
+ colorConsole.important('💡 No actual API calls were made. Use without --dry-run to execute.');
682
+ }
683
+ /**
684
+ * Main function for push command
685
+ */
686
+ async function pushMain(options = {}) {
687
+ const { statusOnly = false, force = false, targetId, targetSlug, showDetailedPreview = false, dryRun = false } = options;
688
+ try {
689
+ const isSingleFileMode = !!(targetId || targetSlug);
690
+ const actionDescription = statusOnly ? 'status check' : 'push';
691
+ const targetDescription = targetId ? `ID ${targetId}` : targetSlug ? `slug "${targetSlug}"` : 'all content';
692
+ console.log(`[PUSH] Starting ${actionDescription} for ${targetDescription}...`);
693
+ // Read local content
694
+ const localContent = await readLocalContent();
695
+ if (localContent.length === 0) {
696
+ console.log('📂 No local content found. Nothing to sync.');
697
+ return;
698
+ }
699
+ // Fetch remote content types for content transformation
700
+ const remoteTypes = await leadCMSDataService.getContentTypes();
701
+ const remoteTypeMap = {};
702
+ remoteTypes.forEach(type => {
703
+ remoteTypeMap[type.uid] = type.format;
704
+ });
705
+ // Filter local content if targeting specific content
706
+ let filteredLocalContent = localContent;
707
+ if (isSingleFileMode) {
708
+ filteredLocalContent = localContent.filter(content => {
709
+ if (targetId && content.metadata.id?.toString() === targetId)
710
+ return true;
711
+ if (targetSlug && content.slug === targetSlug)
712
+ return true;
713
+ return false;
714
+ });
715
+ if (filteredLocalContent.length === 0) {
716
+ console.log(`❌ No local content found with ${targetId ? `ID ${targetId}` : `slug "${targetSlug}"`}`);
717
+ return;
718
+ }
719
+ console.log(`[LOCAL] Found ${filteredLocalContent.length} matching local file(s)`);
720
+ }
721
+ else {
722
+ // Get local content types and validate them
723
+ const localTypes = getLocalContentTypes(localContent);
724
+ console.log(`[LOCAL] Found content types: ${Array.from(localTypes).join(', ')}`);
725
+ await validateContentTypes(localTypes, remoteTypeMap, dryRun);
726
+ }
727
+ // Fetch remote content for comparison
728
+ const remoteContent = await fetchRemoteContent();
729
+ // Match local vs remote content with type mapping for proper content transformation
730
+ const operations = await matchContent(filteredLocalContent, remoteContent, remoteTypeMap);
731
+ // Filter operations if targeting specific content
732
+ const finalOperations = isSingleFileMode ?
733
+ filterContentOperations(operations, targetId, targetSlug) :
734
+ operations;
735
+ // Check if we found the target content
736
+ if (isSingleFileMode) {
737
+ const totalChanges = finalOperations.create.length + finalOperations.update.length +
738
+ finalOperations.rename.length + finalOperations.typeChange.length +
739
+ finalOperations.conflict.length;
740
+ if (totalChanges === 0 && filteredLocalContent.length > 0) {
741
+ // We have local content but no operations - it's in sync
742
+ console.log(`✅ Content with ${targetId ? `ID ${targetId}` : `slug "${targetSlug}"`} is in sync`);
743
+ }
744
+ else if (totalChanges === 0) {
745
+ console.log(`❌ No content found with ${targetId ? `ID ${targetId}` : `slug "${targetSlug}"`} in remote or local`);
746
+ return;
747
+ }
748
+ }
749
+ // Display status
750
+ await displayStatus(finalOperations, statusOnly, isSingleFileMode, showDetailedPreview, remoteTypeMap);
751
+ // If status only, we're done
752
+ if (statusOnly) {
753
+ return;
754
+ }
755
+ // If dry run mode, show API calls without executing
756
+ if (dryRun) {
757
+ await showDryRunOperations(finalOperations);
758
+ return;
759
+ }
760
+ // Handle conflicts
761
+ if (finalOperations.conflict.length > 0 && !force) {
762
+ console.log('\n❌ Cannot proceed due to conflicts. Use --force to override or resolve conflicts first.');
763
+ return;
764
+ }
765
+ const totalChanges = finalOperations.create.length + finalOperations.update.length + finalOperations.rename.length + finalOperations.typeChange.length;
766
+ if (totalChanges === 0) {
767
+ if (isSingleFileMode) {
768
+ console.log('✅ File is already in sync.');
769
+ }
770
+ else {
771
+ console.log('✅ Nothing to sync.');
772
+ }
773
+ return;
774
+ }
775
+ // Confirm changes
776
+ const itemDescription = isSingleFileMode ? 'file change' : 'changes';
777
+ const confirmMsg = `\nProceed with syncing ${totalChanges} ${itemDescription} to LeadCMS? (y/N): `;
778
+ const confirmation = await question(confirmMsg);
779
+ if (confirmation.toLowerCase() !== 'y' && confirmation.toLowerCase() !== 'yes') {
780
+ console.log('🚫 Push cancelled.');
781
+ return;
782
+ }
783
+ // Execute the sync
784
+ await executePush(finalOperations, { force });
785
+ colorConsole.success('\n🎉 Content push completed successfully!');
786
+ }
787
+ catch (error) {
788
+ const operation = statusOnly ? 'Status check' : 'Push';
789
+ console.error(`❌ ${operation} failed:`, error.message);
790
+ process.exit(1);
791
+ }
792
+ finally {
793
+ rl.close();
794
+ }
795
+ }
796
+ /**
797
+ * Execute the actual push operations
798
+ */
799
+ async function executePush(operations, options = {}) {
800
+ const { force = false } = options;
801
+ // Handle force updates for conflicts
802
+ if (force && operations.conflict.length > 0) {
803
+ console.log(`\n🔄 Force updating ${operations.conflict.length} conflicted items...`);
804
+ for (const conflict of operations.conflict) {
805
+ operations.update.push({
806
+ local: conflict.local,
807
+ remote: conflict.remote
808
+ });
809
+ }
810
+ }
811
+ // Use individual operations
812
+ await executeIndividualOperations(operations, { force });
813
+ }
814
+ /**
815
+ * Execute operations individually (one by one)
816
+ */
817
+ async function executeIndividualOperations(operations, options = {}) {
818
+ const { force = false } = options;
819
+ let successful = 0;
820
+ let failed = 0;
821
+ // Create new content
822
+ if (operations.create.length > 0) {
823
+ console.log(`\n🆕 Creating ${operations.create.length} new items...`);
824
+ for (const op of operations.create) {
825
+ try {
826
+ const result = await leadCMSDataService.createContent(formatContentForAPI(op.local));
827
+ if (result) {
828
+ await updateLocalMetadata(op.local, result);
829
+ successful++;
830
+ colorConsole.success(`✅ Created: ${colorConsole.highlight(`${op.local.type}/${op.local.slug}`)}`);
831
+ }
832
+ else {
833
+ failed++;
834
+ colorConsole.error(`❌ Failed to create: ${colorConsole.highlight(`${op.local.type}/${op.local.slug}`)}`);
835
+ }
836
+ }
837
+ catch (error) {
838
+ failed++;
839
+ colorConsole.error(`❌ Failed to create ${colorConsole.highlight(`${op.local.type}/${op.local.slug}`)}:`, error.message);
840
+ }
841
+ }
842
+ }
843
+ // Update existing content
844
+ if (operations.update.length > 0) {
845
+ console.log(`\n🔄 Updating ${operations.update.length} existing items...`);
846
+ for (const op of operations.update) {
847
+ try {
848
+ if (op.remote?.id) {
849
+ const result = await leadCMSDataService.updateContent(op.remote.id, formatContentForAPI(op.local));
850
+ if (result) {
851
+ await updateLocalMetadata(op.local, result);
852
+ successful++;
853
+ colorConsole.success(`✅ Updated: ${colorConsole.highlight(`${op.local.type}/${op.local.slug}`)}`);
854
+ }
855
+ else {
856
+ failed++;
857
+ colorConsole.error(`❌ Failed to update: ${colorConsole.highlight(`${op.local.type}/${op.local.slug}`)}`);
858
+ }
859
+ }
860
+ else {
861
+ failed++;
862
+ colorConsole.error(`❌ Failed to update ${colorConsole.highlight(`${op.local.type}/${op.local.slug}`)}: No remote ID`);
863
+ }
864
+ }
865
+ catch (error) {
866
+ failed++;
867
+ console.log(`❌ Failed to update ${op.local.type}/${op.local.slug}:`, error.message);
868
+ }
869
+ }
870
+ }
871
+ // Handle renamed content (slug changed)
872
+ if (operations.rename.length > 0) {
873
+ console.log(`\n📝 Renaming ${operations.rename.length} items...`);
874
+ for (const op of operations.rename) {
875
+ try {
876
+ if (op.remote?.id) {
877
+ const result = await leadCMSDataService.updateContent(op.remote.id, formatContentForAPI(op.local));
878
+ if (result) {
879
+ await updateLocalMetadata(op.local, result);
880
+ successful++;
881
+ console.log(`✅ Renamed: ${op.oldSlug} -> ${op.local.slug}`);
882
+ }
883
+ else {
884
+ failed++;
885
+ console.log(`❌ Failed to rename: ${op.oldSlug} -> ${op.local.slug}`);
886
+ }
887
+ }
888
+ else {
889
+ failed++;
890
+ console.log(`❌ Failed to rename ${op.oldSlug}: No remote ID`);
891
+ }
892
+ }
893
+ catch (error) {
894
+ failed++;
895
+ console.log(`❌ Failed to rename ${op.oldSlug}:`, error.message);
896
+ }
897
+ }
898
+ }
899
+ // Handle content type changes
900
+ if (operations.typeChange.length > 0) {
901
+ console.log(`\n🔄 Changing content types for ${operations.typeChange.length} items...`);
902
+ for (const op of operations.typeChange) {
903
+ try {
904
+ if (op.remote?.id) {
905
+ const result = await leadCMSDataService.updateContent(op.remote.id, formatContentForAPI(op.local));
906
+ if (result) {
907
+ await updateLocalMetadata(op.local, result);
908
+ successful++;
909
+ console.log(`✅ Type changed: ${op.local.slug} (${op.oldType} -> ${op.newType})`);
910
+ }
911
+ else {
912
+ failed++;
913
+ console.log(`❌ Failed to change type: ${op.local.slug} (${op.oldType} -> ${op.newType})`);
914
+ }
915
+ }
916
+ else {
917
+ failed++;
918
+ console.log(`❌ Failed to change type for ${op.local.slug}: No remote ID`);
919
+ }
920
+ }
921
+ catch (error) {
922
+ failed++;
923
+ console.log(`❌ Failed to change type for ${op.local.slug}:`, error.message);
924
+ }
925
+ }
926
+ }
927
+ console.log(`\n📊 Results: ${successful} successful, ${failed} failed`);
928
+ // If any updates were successful, automatically pull latest changes to sync local store
929
+ if (successful > 0) {
930
+ console.log(`\n🔄 Syncing latest changes from LeadCMS to local store...`);
931
+ try {
932
+ const { fetchLeadCMSContent } = await import('./fetch-leadcms-content.js');
933
+ await fetchLeadCMSContent();
934
+ console.log('✅ Local content store synchronized with latest changes');
935
+ }
936
+ catch (error) {
937
+ console.warn('⚠️ Failed to automatically sync local content:', error.message);
938
+ console.log('💡 You may want to manually run the pull command to sync latest changes');
939
+ }
940
+ }
941
+ }
942
+ /**
943
+ * Format local content for API submission
944
+ */
945
+ function formatContentForAPI(localContent) {
946
+ const contentData = {
947
+ slug: localContent.slug,
948
+ type: localContent.type,
949
+ language: localContent.locale,
950
+ body: localContent.body,
951
+ ...localContent.metadata
952
+ };
953
+ // Preserve the file-based slug (from localContent.slug) over metadata slug
954
+ // This is crucial for rename operations where the file has been renamed
955
+ // but the frontmatter still contains the old slug
956
+ if (localContent.slug !== localContent.metadata?.slug) {
957
+ contentData.slug = localContent.slug;
958
+ }
959
+ // Remove local-only fields
960
+ delete contentData.filePath;
961
+ delete contentData.isLocal;
962
+ // Apply backward URL transformation: convert /media/ paths back to /api/media/ for API
963
+ return replaceLocalMediaPaths(contentData);
964
+ }
965
+ /**
966
+ * Update local file with metadata from LeadCMS response
967
+ */
968
+ async function updateLocalMetadata(localContent, remoteResponse) {
969
+ const { filePath } = localContent;
970
+ const ext = path.extname(filePath);
971
+ try {
972
+ if (ext === '.mdx') {
973
+ const fileContent = await fs.readFile(filePath, 'utf-8');
974
+ const parsed = matter(fileContent);
975
+ // Update metadata with response data (only non-system fields)
976
+ parsed.data.id = remoteResponse.id;
977
+ // Do not add system fields (createdAt, updatedAt, publishedAt) to local files
978
+ // Rebuild the file
979
+ const newContent = matter.stringify(parsed.content, parsed.data);
980
+ await fs.writeFile(filePath, newContent, 'utf-8');
981
+ }
982
+ else if (ext === '.json') {
983
+ const jsonData = JSON.parse(await fs.readFile(filePath, 'utf-8'));
984
+ // Update metadata (only non-system fields)
985
+ jsonData.id = remoteResponse.id;
986
+ // Do not add system fields (createdAt, updatedAt, publishedAt) to local files
987
+ await fs.writeFile(filePath, JSON.stringify(jsonData, null, 2), 'utf-8');
988
+ }
989
+ }
990
+ catch (error) {
991
+ console.warn(`Failed to update local metadata for ${filePath}:`, error.message);
992
+ }
993
+ }
994
+ // Export functions for CLI usage
995
+ export { pushMain as pushLeadCMSContent };
996
+ // Export internal functions for testing
997
+ export { hasActualContentChanges };
998
+ // Re-export the new comparison function for consistency
999
+ export { transformRemoteForComparison } from "../lib/content-transformation.js";
1000
+ // Handle direct script execution only in ESM environment
1001
+ if (typeof import.meta !== 'undefined' && process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
1002
+ const args = process.argv.slice(2);
1003
+ const statusOnly = args.includes('--status');
1004
+ const force = args.includes('--force');
1005
+ const dryRun = args.includes('--dry-run');
1006
+ // Parse target ID or slug
1007
+ let targetId;
1008
+ let targetSlug;
1009
+ const idIndex = args.findIndex(arg => arg === '--id');
1010
+ if (idIndex !== -1 && args[idIndex + 1]) {
1011
+ targetId = args[idIndex + 1];
1012
+ }
1013
+ const slugIndex = args.findIndex(arg => arg === '--slug');
1014
+ if (slugIndex !== -1 && args[slugIndex + 1]) {
1015
+ targetSlug = args[slugIndex + 1];
1016
+ }
1017
+ pushMain({ statusOnly, force, targetId, targetSlug, dryRun }).catch((error) => {
1018
+ console.error('Error running LeadCMS push:', error.message);
1019
+ process.exit(1);
1020
+ });
1021
+ }
1022
+ //# sourceMappingURL=push-leadcms-content.js.map