@leadcms/sdk 3.3.12 → 3.5.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 (165) hide show
  1. package/README.md +2 -1
  2. package/dist/cli/bin/content-status-args.d.ts +7 -0
  3. package/dist/cli/bin/content-status-args.d.ts.map +1 -0
  4. package/dist/cli/bin/content-status-args.js +27 -0
  5. package/dist/cli/bin/content-status-args.js.map +1 -0
  6. package/dist/cli/bin/login.js +1 -1
  7. package/dist/cli/bin/login.js.map +1 -1
  8. package/dist/cli/bin/pull-all.js +4 -2
  9. package/dist/cli/bin/pull-all.js.map +1 -1
  10. package/dist/cli/bin/pull-comments.js +3 -1
  11. package/dist/cli/bin/pull-comments.js.map +1 -1
  12. package/dist/cli/bin/pull-content.js +6 -2
  13. package/dist/cli/bin/pull-content.js.map +1 -1
  14. package/dist/cli/bin/pull-email-templates.js +3 -1
  15. package/dist/cli/bin/pull-email-templates.js.map +1 -1
  16. package/dist/cli/bin/pull-media.js +3 -1
  17. package/dist/cli/bin/pull-media.js.map +1 -1
  18. package/dist/cli/bin/pull-settings.js +2 -0
  19. package/dist/cli/bin/pull-settings.js.map +1 -1
  20. package/dist/cli/bin/push-all.js +12 -1
  21. package/dist/cli/bin/push-all.js.map +1 -1
  22. package/dist/cli/bin/push-comments.d.ts +6 -0
  23. package/dist/cli/bin/push-comments.d.ts.map +1 -0
  24. package/dist/cli/bin/push-comments.js +35 -0
  25. package/dist/cli/bin/push-comments.js.map +1 -0
  26. package/dist/cli/bin/push-content.js +7 -4
  27. package/dist/cli/bin/push-content.js.map +1 -1
  28. package/dist/cli/bin/push-email-templates.js +3 -1
  29. package/dist/cli/bin/push-email-templates.js.map +1 -1
  30. package/dist/cli/bin/push-media.js +2 -0
  31. package/dist/cli/bin/push-media.js.map +1 -1
  32. package/dist/cli/bin/push-settings.js +2 -0
  33. package/dist/cli/bin/push-settings.js.map +1 -1
  34. package/dist/cli/bin/push.js +5 -3
  35. package/dist/cli/bin/push.js.map +1 -1
  36. package/dist/cli/bin/remote-flag.d.ts +17 -0
  37. package/dist/cli/bin/remote-flag.d.ts.map +1 -0
  38. package/dist/cli/bin/remote-flag.js +50 -0
  39. package/dist/cli/bin/remote-flag.js.map +1 -0
  40. package/dist/cli/bin/remote.d.ts +14 -0
  41. package/dist/cli/bin/remote.d.ts.map +1 -0
  42. package/dist/cli/bin/remote.js +291 -0
  43. package/dist/cli/bin/remote.js.map +1 -0
  44. package/dist/cli/bin/status-all.js +67 -4
  45. package/dist/cli/bin/status-all.js.map +1 -1
  46. package/dist/cli/bin/status-comments.d.ts +6 -0
  47. package/dist/cli/bin/status-comments.d.ts.map +1 -0
  48. package/dist/cli/bin/status-comments.js +30 -0
  49. package/dist/cli/bin/status-comments.js.map +1 -0
  50. package/dist/cli/bin/status-content.js +6 -2
  51. package/dist/cli/bin/status-content.js.map +1 -1
  52. package/dist/cli/bin/status-email-templates.js +3 -1
  53. package/dist/cli/bin/status-email-templates.js.map +1 -1
  54. package/dist/cli/bin/status-media.js +2 -0
  55. package/dist/cli/bin/status-media.js.map +1 -1
  56. package/dist/cli/bin/status-settings.js +2 -0
  57. package/dist/cli/bin/status-settings.js.map +1 -1
  58. package/dist/cli/bin/status.js +5 -2
  59. package/dist/cli/bin/status.js.map +1 -1
  60. package/dist/cli/bin/watch.js +6 -3
  61. package/dist/cli/bin/watch.js.map +1 -1
  62. package/dist/cli/index.js +65 -10
  63. package/dist/cli/index.js.map +1 -1
  64. package/dist/index.js +0 -1
  65. package/dist/index.js.map +1 -1
  66. package/dist/lib/auth.d.ts +1 -1
  67. package/dist/lib/auth.d.ts.map +1 -1
  68. package/dist/lib/auth.js +5 -5
  69. package/dist/lib/auth.js.map +1 -1
  70. package/dist/lib/cms.d.ts +7 -7
  71. package/dist/lib/cms.d.ts.map +1 -1
  72. package/dist/lib/cms.js +41 -7
  73. package/dist/lib/cms.js.map +1 -1
  74. package/dist/lib/comment-types.d.ts +4 -0
  75. package/dist/lib/comment-types.d.ts.map +1 -1
  76. package/dist/lib/config.d.ts +6 -1
  77. package/dist/lib/config.d.ts.map +1 -1
  78. package/dist/lib/config.js +37 -8
  79. package/dist/lib/config.js.map +1 -1
  80. package/dist/lib/content-merge.d.ts.map +1 -1
  81. package/dist/lib/content-merge.js +102 -19
  82. package/dist/lib/content-merge.js.map +1 -1
  83. package/dist/lib/content-transformation.d.ts +5 -0
  84. package/dist/lib/content-transformation.d.ts.map +1 -1
  85. package/dist/lib/content-transformation.js +49 -2
  86. package/dist/lib/content-transformation.js.map +1 -1
  87. package/dist/lib/data-service.d.ts +47 -1
  88. package/dist/lib/data-service.d.ts.map +1 -1
  89. package/dist/lib/data-service.js +143 -0
  90. package/dist/lib/data-service.js.map +1 -1
  91. package/dist/lib/remote-context.d.ts +98 -0
  92. package/dist/lib/remote-context.d.ts.map +1 -0
  93. package/dist/lib/remote-context.js +297 -0
  94. package/dist/lib/remote-context.js.map +1 -0
  95. package/dist/scripts/init-leadcms.d.ts.map +1 -1
  96. package/dist/scripts/init-leadcms.js +127 -15
  97. package/dist/scripts/init-leadcms.js.map +1 -1
  98. package/dist/scripts/leadcms-helpers.d.ts +16 -5
  99. package/dist/scripts/leadcms-helpers.d.ts.map +1 -1
  100. package/dist/scripts/leadcms-helpers.js +31 -8
  101. package/dist/scripts/leadcms-helpers.js.map +1 -1
  102. package/dist/scripts/login-leadcms.d.ts +4 -1
  103. package/dist/scripts/login-leadcms.d.ts.map +1 -1
  104. package/dist/scripts/login-leadcms.js +45 -6
  105. package/dist/scripts/login-leadcms.js.map +1 -1
  106. package/dist/scripts/pull-all.d.ts +14 -6
  107. package/dist/scripts/pull-all.d.ts.map +1 -1
  108. package/dist/scripts/pull-all.js +86 -33
  109. package/dist/scripts/pull-all.js.map +1 -1
  110. package/dist/scripts/pull-comments.d.ts +3 -0
  111. package/dist/scripts/pull-comments.d.ts.map +1 -1
  112. package/dist/scripts/pull-comments.js +4 -4
  113. package/dist/scripts/pull-comments.js.map +1 -1
  114. package/dist/scripts/pull-content.d.ts +14 -0
  115. package/dist/scripts/pull-content.d.ts.map +1 -1
  116. package/dist/scripts/pull-content.js +85 -8
  117. package/dist/scripts/pull-content.js.map +1 -1
  118. package/dist/scripts/pull-email-templates.d.ts +3 -0
  119. package/dist/scripts/pull-email-templates.d.ts.map +1 -1
  120. package/dist/scripts/pull-email-templates.js +4 -4
  121. package/dist/scripts/pull-email-templates.js.map +1 -1
  122. package/dist/scripts/{fetch-leadcms-comments.d.ts → pull-leadcms-comments.d.ts} +10 -4
  123. package/dist/scripts/pull-leadcms-comments.d.ts.map +1 -0
  124. package/dist/scripts/{fetch-leadcms-comments.js → pull-leadcms-comments.js} +62 -32
  125. package/dist/scripts/pull-leadcms-comments.js.map +1 -0
  126. package/dist/scripts/{fetch-leadcms-content.d.ts → pull-leadcms-content.d.ts} +13 -19
  127. package/dist/scripts/pull-leadcms-content.d.ts.map +1 -0
  128. package/dist/scripts/{fetch-leadcms-content.js → pull-leadcms-content.js} +121 -250
  129. package/dist/scripts/pull-leadcms-content.js.map +1 -0
  130. package/dist/scripts/{fetch-leadcms-email-templates.d.ts → pull-leadcms-email-templates.d.ts} +5 -4
  131. package/dist/scripts/pull-leadcms-email-templates.d.ts.map +1 -0
  132. package/dist/scripts/{fetch-leadcms-email-templates.js → pull-leadcms-email-templates.js} +89 -11
  133. package/dist/scripts/pull-leadcms-email-templates.js.map +1 -0
  134. package/dist/scripts/pull-leadcms-media.d.ts +32 -0
  135. package/dist/scripts/pull-leadcms-media.d.ts.map +1 -0
  136. package/dist/scripts/pull-leadcms-media.js +229 -0
  137. package/dist/scripts/pull-leadcms-media.js.map +1 -0
  138. package/dist/scripts/pull-media.d.ts +4 -1
  139. package/dist/scripts/pull-media.d.ts.map +1 -1
  140. package/dist/scripts/pull-media.js +4 -4
  141. package/dist/scripts/pull-media.js.map +1 -1
  142. package/dist/scripts/push-comments.d.ts +48 -0
  143. package/dist/scripts/push-comments.d.ts.map +1 -0
  144. package/dist/scripts/push-comments.js +433 -0
  145. package/dist/scripts/push-comments.js.map +1 -0
  146. package/dist/scripts/push-email-templates.d.ts +6 -1
  147. package/dist/scripts/push-email-templates.d.ts.map +1 -1
  148. package/dist/scripts/push-email-templates.js +96 -21
  149. package/dist/scripts/push-email-templates.js.map +1 -1
  150. package/dist/scripts/push-leadcms-content.d.ts +24 -8
  151. package/dist/scripts/push-leadcms-content.d.ts.map +1 -1
  152. package/dist/scripts/push-leadcms-content.js +192 -56
  153. package/dist/scripts/push-leadcms-content.js.map +1 -1
  154. package/dist/scripts/sse-watcher.d.ts +9 -8
  155. package/dist/scripts/sse-watcher.d.ts.map +1 -1
  156. package/dist/scripts/sse-watcher.js +59 -52
  157. package/dist/scripts/sse-watcher.js.map +1 -1
  158. package/leadcms.config.json.sample +12 -1
  159. package/package.json +2 -3
  160. package/dist/scripts/fetch-leadcms-comments.d.ts.map +0 -1
  161. package/dist/scripts/fetch-leadcms-comments.js.map +0 -1
  162. package/dist/scripts/fetch-leadcms-content.d.ts.map +0 -1
  163. package/dist/scripts/fetch-leadcms-content.js.map +0 -1
  164. package/dist/scripts/fetch-leadcms-email-templates.d.ts.map +0 -1
  165. package/dist/scripts/fetch-leadcms-email-templates.js.map +0 -1
@@ -6,10 +6,60 @@ import matter from "gray-matter";
6
6
  import * as Diff from "diff";
7
7
  import { defaultLanguage, CONTENT_DIR, } from "./leadcms-helpers.js";
8
8
  import { leadCMSDataService } from "../lib/data-service.js";
9
- import { transformRemoteToLocalFormat, transformRemoteForComparison, hasContentDifferences } from "../lib/content-transformation.js";
9
+ import { transformRemoteToLocalFormat, transformRemoteForComparison, hasContentDifferences, stripTimestampMetadata } from "../lib/content-transformation.js";
10
10
  import { formatContentForAPI } from '../lib/content-api-formatting.js';
11
11
  import { colorConsole, statusColors, diffColors } from '../lib/console-colors.js';
12
12
  import { logger } from '../lib/logger.js';
13
+ const CONTENT_STATUS_ALIASES = {
14
+ create: 'create',
15
+ created: 'create',
16
+ new: 'create',
17
+ update: 'update',
18
+ updated: 'update',
19
+ modify: 'update',
20
+ modified: 'update',
21
+ change: 'update',
22
+ changed: 'update',
23
+ rename: 'rename',
24
+ renamed: 'rename',
25
+ typechange: 'typeChange',
26
+ 'type-change': 'typeChange',
27
+ typechanged: 'typeChange',
28
+ 'type-changed': 'typeChange',
29
+ conflict: 'conflict',
30
+ conflicts: 'conflict',
31
+ delete: 'delete',
32
+ deleted: 'delete',
33
+ remove: 'delete',
34
+ removed: 'delete'
35
+ };
36
+ function normalizeStatusAlias(value) {
37
+ return value.trim().toLowerCase().replace(/[_\s]+/g, '-');
38
+ }
39
+ export function normalizeContentStatusFilters(statusFilters) {
40
+ if (!statusFilters || statusFilters.length === 0) {
41
+ return undefined;
42
+ }
43
+ const normalized = new Set();
44
+ const invalid = [];
45
+ for (const rawFilter of statusFilters) {
46
+ const filter = normalizeStatusAlias(rawFilter);
47
+ if (!filter) {
48
+ continue;
49
+ }
50
+ const operationKey = CONTENT_STATUS_ALIASES[filter];
51
+ if (operationKey) {
52
+ normalized.add(operationKey);
53
+ }
54
+ else {
55
+ invalid.push(rawFilter);
56
+ }
57
+ }
58
+ if (invalid.length > 0) {
59
+ throw new Error(`Unsupported content status filter: ${invalid.join(', ')}`);
60
+ }
61
+ return normalized.size > 0 ? Array.from(normalized) : undefined;
62
+ }
13
63
  // Create readline interface for user prompts
14
64
  const rl = readline.createInterface({
15
65
  input: process.stdin,
@@ -166,10 +216,10 @@ async function fetchRemoteContent() {
166
216
  async function hasActualContentChanges(local, remote, typeMap) {
167
217
  try {
168
218
  // Read the local file content as-is
169
- const localFileContent = await fs.readFile(local.filePath, 'utf-8');
219
+ const localFileContent = stripTimestampMetadata(await fs.readFile(local.filePath, 'utf-8'));
170
220
  // Transform remote content for comparison, only including fields that exist in local content
171
221
  // This prevents false positives when remote has additional fields like updatedAt
172
- const transformedRemoteContent = await transformRemoteForComparison(remote, localFileContent, typeMap);
222
+ const transformedRemoteContent = stripTimestampMetadata(await transformRemoteForComparison(remote, localFileContent, typeMap));
173
223
  // Compare the raw file contents using shared normalization logic
174
224
  const hasFileContentChanges = hasContentDifferences(localFileContent, transformedRemoteContent);
175
225
  return hasFileContentChanges;
@@ -183,7 +233,7 @@ async function hasActualContentChanges(local, remote, typeMap) {
183
233
  /**
184
234
  * Match local content with remote content
185
235
  */
186
- async function matchContent(localContent, remoteContent, typeMap, allowDelete = false) {
236
+ async function matchContent(localContent, remoteContent, typeMap, allowDelete = false, metadataMap) {
187
237
  const operations = {
188
238
  create: [],
189
239
  update: [],
@@ -194,9 +244,14 @@ async function matchContent(localContent, remoteContent, typeMap, allowDelete =
194
244
  };
195
245
  for (const local of localContent) {
196
246
  let match = undefined;
197
- // First try to match by ID if local content has one
198
- if (local.metadata.id) {
199
- match = remoteContent.find(remote => remote.id === local.metadata.id);
247
+ // First try to match by ID — use remote-specific id-map when available,
248
+ // falling back to frontmatter ID for single-remote backward compatibility
249
+ const remoteId = metadataMap
250
+ ? (await import('../lib/remote-context.js')).lookupRemoteId(metadataMap, local.locale, local.slug)
251
+ : undefined;
252
+ const matchId = remoteId ?? local.metadata.id;
253
+ if (matchId) {
254
+ match = remoteContent.find(remote => remote.id === matchId);
200
255
  }
201
256
  // If no ID match, try to match by current filename slug and locale
202
257
  if (!match) {
@@ -214,8 +269,12 @@ async function matchContent(localContent, remoteContent, typeMap, allowDelete =
214
269
  (remote.language || defaultLanguage) === local.locale);
215
270
  }
216
271
  if (match) {
217
- // Check for conflicts by comparing updatedAt timestamps from content metadata
218
- const localUpdated = local.metadata.updatedAt ? new Date(local.metadata.updatedAt) : new Date(0);
272
+ // Check for conflicts by comparing updatedAt timestamps.
273
+ // Use remote-specific metadata-map when available, falling back to frontmatter.
274
+ const localUpdatedStr = metadataMap
275
+ ? (await import('../lib/remote-context.js')).getMetadataForContent(metadataMap, local.locale, local.slug)?.updatedAt
276
+ : local.metadata.updatedAt;
277
+ const localUpdated = localUpdatedStr ? new Date(localUpdatedStr) : new Date(0);
219
278
  const remoteUpdated = match.updatedAt ? new Date(match.updatedAt) : new Date(0);
220
279
  // Detect different types of changes
221
280
  const slugChanged = match.slug !== local.slug;
@@ -286,8 +345,24 @@ async function matchContent(localContent, remoteContent, typeMap, allowDelete =
286
345
  }
287
346
  // Check for deleted content (remote but not local) if deletion is allowed
288
347
  if (allowDelete) {
289
- // Create a set of local content IDs and slugs for quick lookup
290
- const localIds = new Set(localContent.map(c => c.metadata.id).filter(id => id));
348
+ // Build a set of IDs from the remote-specific id-map (or frontmatter for single-remote)
349
+ const localIds = new Set();
350
+ if (metadataMap) {
351
+ // Multi-remote: use the target remote's metadata values
352
+ for (const slugMap of Object.values(metadataMap.content)) {
353
+ for (const entry of Object.values(slugMap)) {
354
+ if (entry.id != null)
355
+ localIds.add(entry.id);
356
+ }
357
+ }
358
+ }
359
+ else {
360
+ // Single-remote: use frontmatter IDs
361
+ for (const c of localContent) {
362
+ if (c.metadata.id != null)
363
+ localIds.add(c.metadata.id);
364
+ }
365
+ }
291
366
  const localSlugs = new Map();
292
367
  for (const local of localContent) {
293
368
  const key = `${local.slug}:${local.locale}`;
@@ -460,11 +535,15 @@ async function createContentTypeInteractive(typeName, localContent, dryRun = fal
460
535
  /**
461
536
  * Filter content operations to only include specific content by ID or slug
462
537
  */
463
- function filterContentOperations(operations, targetId, targetSlug) {
464
- if (!targetId && !targetSlug) {
538
+ function filterContentOperations(operations, targetId, targetSlug, statusFilters) {
539
+ const normalizedStatusFilters = normalizeContentStatusFilters(statusFilters);
540
+ if (!targetId && !targetSlug && !normalizedStatusFilters) {
465
541
  return operations; // No filtering needed
466
542
  }
467
543
  const matchesTarget = (op) => {
544
+ if (!targetId && !targetSlug) {
545
+ return true;
546
+ }
468
547
  if (targetId) {
469
548
  // Check if local content has the target ID
470
549
  if (op.local.metadata.id?.toString() === targetId)
@@ -486,19 +565,25 @@ function filterContentOperations(operations, targetId, targetSlug) {
486
565
  }
487
566
  return false;
488
567
  };
568
+ const includesStatus = (operationType) => {
569
+ if (!normalizedStatusFilters || normalizedStatusFilters.length === 0) {
570
+ return true;
571
+ }
572
+ return normalizedStatusFilters.includes(operationType);
573
+ };
489
574
  return {
490
- create: operations.create.filter(matchesTarget),
491
- update: operations.update.filter(matchesTarget),
492
- rename: operations.rename.filter(matchesTarget),
493
- typeChange: operations.typeChange.filter(matchesTarget),
494
- conflict: operations.conflict.filter(matchesTarget),
495
- delete: operations.delete.filter(matchesTarget)
575
+ create: includesStatus('create') ? operations.create.filter(matchesTarget) : [],
576
+ update: includesStatus('update') ? operations.update.filter(matchesTarget) : [],
577
+ rename: includesStatus('rename') ? operations.rename.filter(matchesTarget) : [],
578
+ typeChange: includesStatus('typeChange') ? operations.typeChange.filter(matchesTarget) : [],
579
+ conflict: includesStatus('conflict') ? operations.conflict.filter(matchesTarget) : [],
580
+ delete: includesStatus('delete') ? operations.delete.filter(matchesTarget) : []
496
581
  };
497
582
  }
498
583
  /**
499
584
  * Display detailed diff for a single content item
500
585
  */
501
- async function displayDetailedDiff(operation, operationType, typeMap) {
586
+ async function displayDetailedDiff(operation, operationType, typeMap, options = {}) {
502
587
  const { local, remote } = operation;
503
588
  console.log(`\nšŸ“„ Detailed Changes for: ${local.slug} [${local.locale}]`);
504
589
  console.log(` Operation: ${operationType}`);
@@ -511,9 +596,9 @@ async function displayDetailedDiff(operation, operationType, typeMap) {
511
596
  console.log('\nšŸ“ Content Changes:');
512
597
  try {
513
598
  // Read local file content as-is
514
- const localFileContent = await fs.readFile(local.filePath, 'utf-8');
599
+ const localFileContent = stripTimestampMetadata(await fs.readFile(local.filePath, 'utf-8'));
515
600
  // Transform remote content to local format for comparison
516
- const transformedRemoteContent = remote ? await transformRemoteToLocalFormat(remote, typeMap) : '';
601
+ const transformedRemoteContent = remote ? stripTimestampMetadata(await transformRemoteToLocalFormat(remote, typeMap)) : '';
517
602
  if (localFileContent.trim() === transformedRemoteContent.trim()) {
518
603
  console.log(' No content changes detected');
519
604
  }
@@ -526,7 +611,8 @@ async function displayDetailedDiff(operation, operationType, typeMap) {
526
611
  // Show diff preview and count changes
527
612
  colorConsole.info(' Content diff preview:');
528
613
  let previewLines = 0;
529
- const maxPreviewLines = 10;
614
+ const shouldLimitPreviewLines = options.limitPreviewLines !== false;
615
+ const maxPreviewLines = shouldLimitPreviewLines ? 10 : Number.POSITIVE_INFINITY;
530
616
  for (const part of diff) {
531
617
  // Count non-empty lines only for more accurate statistics
532
618
  const lines = part.value.split('\n').filter((line) => line.trim() !== '');
@@ -551,10 +637,10 @@ async function displayDetailedDiff(operation, operationType, typeMap) {
551
637
  else {
552
638
  unchangedLines += lines.length;
553
639
  }
554
- if (previewLines >= maxPreviewLines)
640
+ if (shouldLimitPreviewLines && previewLines >= maxPreviewLines)
555
641
  break;
556
642
  }
557
- if (previewLines >= maxPreviewLines && (addedLines + removedLines > previewLines)) {
643
+ if (shouldLimitPreviewLines && previewLines >= maxPreviewLines && (addedLines + removedLines > previewLines)) {
558
644
  colorConsole.gray(` ... (${addedLines + removedLines - previewLines} more changes)`);
559
645
  }
560
646
  const summaryText = `\n šŸ“Š Change Summary: ${colorConsole.green(`+${addedLines} lines added`)}, ${colorConsole.red(`-${removedLines} lines removed`)}, ${unchangedLines} lines unchanged`;
@@ -570,7 +656,7 @@ async function displayDetailedDiff(operation, operationType, typeMap) {
570
656
  /**
571
657
  * Display status/preview of changes
572
658
  */
573
- async function displayStatus(operations, isStatusOnly = false, isSingleFile = false, showDetailedPreview = false, typeMap, showDelete = false) {
659
+ async function displayStatus(operations, isStatusOnly = false, isSingleFile = false, showDetailedPreview = false, typeMap, showDelete = false, remoteName) {
574
660
  if (isSingleFile) {
575
661
  colorConsole.important('\nšŸ“„ LeadCMS File Status');
576
662
  }
@@ -593,19 +679,19 @@ async function displayStatus(operations, isStatusOnly = false, isSingleFile = fa
593
679
  if (isSingleFile) {
594
680
  // Show detailed diff for each operation
595
681
  for (const op of operations.create) {
596
- await displayDetailedDiff(op, 'New file', typeMap);
682
+ await displayDetailedDiff(op, 'New file', typeMap, { limitPreviewLines: false });
597
683
  }
598
684
  for (const op of operations.update) {
599
- await displayDetailedDiff(op, 'Modified', typeMap);
685
+ await displayDetailedDiff(op, 'Modified', typeMap, { limitPreviewLines: false });
600
686
  }
601
687
  for (const op of operations.rename) {
602
- await displayDetailedDiff(op, `Renamed (${op.oldSlug} → ${op.local.slug})`, typeMap);
688
+ await displayDetailedDiff(op, `Renamed (${op.oldSlug} → ${op.local.slug})`, typeMap, { limitPreviewLines: false });
603
689
  }
604
690
  for (const op of operations.typeChange) {
605
- await displayDetailedDiff(op, `Type changed (${op.oldType} → ${op.newType})`, typeMap);
691
+ await displayDetailedDiff(op, `Type changed (${op.oldType} → ${op.newType})`, typeMap, { limitPreviewLines: false });
606
692
  }
607
693
  for (const op of operations.conflict) {
608
- await displayDetailedDiff(op, `Conflict: ${op.reason}`, typeMap);
694
+ await displayDetailedDiff(op, `Conflict: ${op.reason}`, typeMap, { limitPreviewLines: false });
609
695
  }
610
696
  return;
611
697
  }
@@ -688,7 +774,8 @@ async function displayStatus(operations, isStatusOnly = false, isSingleFile = fa
688
774
  } // Conflicts (like git's merge conflicts)
689
775
  if (operations.conflict.length > 0) {
690
776
  colorConsole.warn(`āš ļø Unmerged conflicts (${operations.conflict.length} files):`);
691
- colorConsole.info(' (use "leadcms pull" to merge remote changes)');
777
+ const remoteFlag = remoteName ? ` -r ${remoteName}` : '';
778
+ colorConsole.info(` (use "leadcms pull-content${remoteFlag}" to merge remote changes)`);
692
779
  colorConsole.log('');
693
780
  // Sort conflicts by locale then slug as well
694
781
  const sortedConflicts = [...operations.conflict].sort((a, b) => {
@@ -716,10 +803,10 @@ async function displayStatus(operations, isStatusOnly = false, isSingleFile = fa
716
803
  }
717
804
  if (!isStatusOnly) {
718
805
  colorConsole.important('šŸ’” To resolve conflicts:');
719
- colorConsole.info(' • Run "leadcms pull" to fetch latest changes');
806
+ colorConsole.info(` • Run "leadcms pull-content${remoteFlag}" to pull latest changes`);
720
807
  colorConsole.info(' • Resolve conflicts in local files');
721
- colorConsole.info(' • Run "leadcms push" again');
722
- colorConsole.warn(' • Or use "leadcms push --force" to override remote changes (āš ļø data loss risk)');
808
+ colorConsole.info(` • Run "leadcms push-content${remoteFlag}" again`);
809
+ colorConsole.warn(` • Or use "leadcms push-content${remoteFlag} --force" to override remote changes (āš ļø data loss risk)`);
723
810
  colorConsole.log('');
724
811
  }
725
812
  }
@@ -805,16 +892,22 @@ async function showDryRunOperations(operations) {
805
892
  * Main function for push command
806
893
  */
807
894
  async function pushMain(options = {}) {
808
- const { statusOnly = false, force = false, targetId, targetSlug, showDetailedPreview = false, dryRun = false, allowDelete = false, showDelete = false } = options;
895
+ const { statusOnly = false, targetId, targetSlug, statusFilter, showDetailedPreview = false, dryRun = false, allowDelete = false, showDelete = false, remoteContext: remoteCtx } = options;
896
+ let force = options.force ?? false;
809
897
  try {
898
+ // Configure data service for the target remote (multi-remote support)
899
+ if (remoteCtx) {
900
+ leadCMSDataService.configureForRemote(remoteCtx.url, remoteCtx.apiKey);
901
+ logger.verbose(`[PUSH] Using remote "${remoteCtx.name}" (${remoteCtx.url})`);
902
+ }
810
903
  const isSingleFileMode = !!(targetId || targetSlug);
811
904
  const actionDescription = statusOnly ? 'status check' : 'push';
812
905
  const targetDescription = targetId ? `ID ${targetId}` : targetSlug ? `slug "${targetSlug}"` : 'all content';
813
906
  logger.verbose(`[PUSH] Starting ${actionDescription} for ${targetDescription}...`);
814
907
  // Check for API key if not in status-only mode
815
908
  if (!statusOnly && !dryRun) {
816
- const config = await import('../lib/config.js').then(m => m.getConfig());
817
- if (!config.apiKey) {
909
+ const hasApiKey = remoteCtx ? !!remoteCtx.apiKey : !!(await import('../lib/config.js').then(m => m.getConfig())).apiKey;
910
+ if (!hasApiKey) {
818
911
  console.log('\nāŒ Cannot push changes: No API key configured (anonymous mode)');
819
912
  console.log('\nšŸ’” To push changes, you need to authenticate:');
820
913
  console.log(' • Set LEADCMS_API_KEY in your .env file');
@@ -873,15 +966,21 @@ async function pushMain(options = {}) {
873
966
  }
874
967
  // Fetch remote content for comparison
875
968
  const remoteContent = await fetchRemoteContent();
969
+ // Load per-remote metadata-map for multi-remote support
970
+ let metadataMap;
971
+ if (remoteCtx) {
972
+ const rc = await import('../lib/remote-context.js');
973
+ metadataMap = await rc.readMetadataMap(remoteCtx);
974
+ }
876
975
  // Match local vs remote content with type mapping for proper content transformation
877
976
  // In status mode, enable deletion detection if showDelete is true (for display purposes)
878
977
  // In push mode, only detect deletions if allowDelete is true (for execution)
879
978
  const shouldDetectDeletions = statusOnly ? (showDelete || allowDelete) : allowDelete;
880
- const operations = await matchContent(filteredLocalContent, remoteContent, remoteTypeMap, shouldDetectDeletions);
979
+ const operations = await matchContent(filteredLocalContent, remoteContent, remoteTypeMap, shouldDetectDeletions, metadataMap);
881
980
  // Filter operations if targeting specific content
882
- const finalOperations = isSingleFileMode ?
883
- filterContentOperations(operations, targetId, targetSlug) :
884
- operations;
981
+ const finalOperations = (isSingleFileMode || (statusFilter && statusFilter.length > 0))
982
+ ? filterContentOperations(operations, targetId, targetSlug, statusFilter)
983
+ : operations;
885
984
  // Check if we found the target content
886
985
  if (isSingleFileMode) {
887
986
  const totalChanges = countPushChanges(finalOperations, true);
@@ -905,7 +1004,7 @@ async function pushMain(options = {}) {
905
1004
  }
906
1005
  }
907
1006
  // Display status
908
- await displayStatus(finalOperations, statusOnly, isSingleFileMode, showDetailedPreview, remoteTypeMap, statusOnly ? showDelete : true);
1007
+ await displayStatus(finalOperations, statusOnly, isSingleFileMode, showDetailedPreview, remoteTypeMap, statusOnly ? showDelete : true, remoteCtx?.name);
909
1008
  // If status only, we're done
910
1009
  if (statusOnly) {
911
1010
  return;
@@ -939,7 +1038,7 @@ async function pushMain(options = {}) {
939
1038
  return;
940
1039
  }
941
1040
  // Execute the sync
942
- const results = await executePush(finalOperations, { force });
1041
+ const results = await executePush(finalOperations, { force, remoteCtx });
943
1042
  // Display final message based on results
944
1043
  if (results.failed === 0) {
945
1044
  colorConsole.success('\nšŸŽ‰ Content push completed successfully!');
@@ -964,7 +1063,7 @@ async function pushMain(options = {}) {
964
1063
  * Execute the actual push operations
965
1064
  */
966
1065
  async function executePush(operations, options = {}) {
967
- const { force = false } = options;
1066
+ const { force = false, remoteCtx } = options;
968
1067
  // Handle force updates for conflicts
969
1068
  if (force && operations.conflict.length > 0) {
970
1069
  console.log(`\nšŸ”„ Force updating ${operations.conflict.length} conflicted items...`);
@@ -976,13 +1075,13 @@ async function executePush(operations, options = {}) {
976
1075
  }
977
1076
  }
978
1077
  // Use individual operations
979
- return await executeIndividualOperations(operations, { force });
1078
+ return await executeIndividualOperations(operations, { force, remoteCtx });
980
1079
  }
981
1080
  /**
982
1081
  * Execute operations individually (one by one)
983
1082
  */
984
1083
  async function executeIndividualOperations(operations, options = {}) {
985
- const { force = false } = options;
1084
+ const { force = false, remoteCtx } = options;
986
1085
  let successful = 0;
987
1086
  let failed = 0;
988
1087
  // Create new content
@@ -992,7 +1091,7 @@ async function executeIndividualOperations(operations, options = {}) {
992
1091
  try {
993
1092
  const result = await leadCMSDataService.createContent(formatContentForAPI(op.local));
994
1093
  if (result) {
995
- await updateLocalMetadata(op.local, result);
1094
+ await updateLocalMetadata(op.local, result, remoteCtx);
996
1095
  successful++;
997
1096
  colorConsole.success(`āœ… Created: ${colorConsole.highlight(`${op.local.type}/${op.local.slug}`)}`);
998
1097
  }
@@ -1015,7 +1114,7 @@ async function executeIndividualOperations(operations, options = {}) {
1015
1114
  if (op.remote?.id) {
1016
1115
  const result = await leadCMSDataService.updateContent(op.remote.id, formatContentForAPI(op.local));
1017
1116
  if (result) {
1018
- await updateLocalMetadata(op.local, result);
1117
+ await updateLocalMetadata(op.local, result, remoteCtx);
1019
1118
  successful++;
1020
1119
  colorConsole.success(`āœ… Updated: ${colorConsole.highlight(`${op.local.type}/${op.local.slug}`)}`);
1021
1120
  }
@@ -1043,7 +1142,7 @@ async function executeIndividualOperations(operations, options = {}) {
1043
1142
  if (op.remote?.id) {
1044
1143
  const result = await leadCMSDataService.updateContent(op.remote.id, formatContentForAPI(op.local));
1045
1144
  if (result) {
1046
- await updateLocalMetadata(op.local, result);
1145
+ await updateLocalMetadata(op.local, result, remoteCtx);
1047
1146
  successful++;
1048
1147
  colorConsole.success(`āœ… Renamed: ${op.oldSlug} -> ${op.local.slug}`);
1049
1148
  }
@@ -1071,7 +1170,7 @@ async function executeIndividualOperations(operations, options = {}) {
1071
1170
  if (op.remote?.id) {
1072
1171
  const result = await leadCMSDataService.updateContent(op.remote.id, formatContentForAPI(op.local));
1073
1172
  if (result) {
1074
- await updateLocalMetadata(op.local, result);
1173
+ await updateLocalMetadata(op.local, result, remoteCtx);
1075
1174
  successful++;
1076
1175
  colorConsole.success(`āœ… Type changed: ${op.local.slug} (${op.oldType} -> ${op.newType})`);
1077
1176
  }
@@ -1117,8 +1216,10 @@ async function executeIndividualOperations(operations, options = {}) {
1117
1216
  if (successful > 0) {
1118
1217
  logger.info(`\nšŸ”„ Syncing latest changes from LeadCMS to local store...`);
1119
1218
  try {
1120
- const { fetchLeadCMSContent } = await import('./fetch-leadcms-content.js');
1121
- await fetchLeadCMSContent();
1219
+ const { pullLeadCMSContent } = await import('./pull-leadcms-content.js');
1220
+ await pullLeadCMSContent({ remoteContext: remoteCtx });
1221
+ const { pullLeadCMSMedia } = await import('./pull-leadcms-media.js');
1222
+ await pullLeadCMSMedia({ remoteContext: remoteCtx });
1122
1223
  console.log('āœ… Local content store synchronized with latest changes');
1123
1224
  }
1124
1225
  catch (error) {
@@ -1132,11 +1233,35 @@ async function executeIndividualOperations(operations, options = {}) {
1132
1233
  * Format local content for API submission
1133
1234
  */
1134
1235
  /**
1135
- * Update local file with metadata from LeadCMS response
1236
+ * Update local file with metadata from LeadCMS response.
1237
+ * In multi-remote mode, saves IDs and timestamps to per-remote map files.
1238
+ * Frontmatter is only updated for the default remote (or in single-remote mode).
1136
1239
  */
1137
- async function updateLocalMetadata(localContent, remoteResponse) {
1240
+ async function updateLocalMetadata(localContent, remoteResponse, remoteCtx) {
1138
1241
  const { filePath } = localContent;
1139
1242
  const ext = path.extname(filePath);
1243
+ // Per-remote metadata updates
1244
+ if (remoteCtx) {
1245
+ try {
1246
+ const rc = await import('../lib/remote-context.js');
1247
+ const metaMap = await rc.readMetadataMap(remoteCtx);
1248
+ if (remoteResponse.id != null) {
1249
+ rc.setRemoteId(metaMap, localContent.locale, localContent.slug, remoteResponse.id);
1250
+ }
1251
+ rc.setMetadataForContent(metaMap, localContent.locale, localContent.slug, {
1252
+ createdAt: remoteResponse.createdAt,
1253
+ updatedAt: remoteResponse.updatedAt,
1254
+ });
1255
+ await rc.writeMetadataMap(remoteCtx, metaMap);
1256
+ }
1257
+ catch (error) {
1258
+ console.warn(`Failed to update remote map files for ${filePath}:`, error.message);
1259
+ }
1260
+ }
1261
+ // Only update frontmatter in single-remote mode or for the default remote
1262
+ if (remoteCtx && !remoteCtx.isDefault) {
1263
+ return;
1264
+ }
1140
1265
  try {
1141
1266
  if (ext === '.mdx') {
1142
1267
  const fileContent = await fs.readFile(filePath, 'utf-8');
@@ -1182,6 +1307,7 @@ export { analyzeContentTypeFromFiles };
1182
1307
  export { isYes };
1183
1308
  export { normalizeFormat };
1184
1309
  export { fetchRemoteContent };
1310
+ export { displayDetailedDiff };
1185
1311
  // Re-export formatContentForAPI from utility module for testing
1186
1312
  export { formatContentForAPI } from '../lib/content-api-formatting.js';
1187
1313
  // Re-export the new comparison function for consistency
@@ -1191,6 +1317,10 @@ export { transformRemoteForComparison } from "../lib/content-transformation.js";
1191
1317
  * Used by the unified status-all renderer.
1192
1318
  */
1193
1319
  export async function getContentStatusData(options = {}) {
1320
+ // Configure data service for the target remote (multi-remote support)
1321
+ if (options.remoteContext) {
1322
+ leadCMSDataService.configureForRemote(options.remoteContext.url, options.remoteContext.apiKey);
1323
+ }
1194
1324
  const localContent = await readLocalContent();
1195
1325
  if (localContent.length === 0) {
1196
1326
  return {
@@ -1208,7 +1338,13 @@ export async function getContentStatusData(options = {}) {
1208
1338
  logger.verbose('[STATUS] Warning: content types response was not an array, proceeding without type map');
1209
1339
  }
1210
1340
  const remoteContent = await fetchRemoteContent();
1211
- const operations = await matchContent(localContent, remoteContent, remoteTypeMap, options.showDelete || false);
1341
+ // Load per-remote maps for multi-remote support
1342
+ let metadataMap;
1343
+ if (options.remoteContext) {
1344
+ const rc = await import('../lib/remote-context.js');
1345
+ metadataMap = await rc.readMetadataMap(options.remoteContext);
1346
+ }
1347
+ const operations = await matchContent(localContent, remoteContent, remoteTypeMap, options.showDelete || false, metadataMap);
1212
1348
  return {
1213
1349
  operations,
1214
1350
  totalLocal: localContent.length,