@magentrix-corp/magentrix-cli 1.1.3 → 1.1.5

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.
@@ -670,27 +670,42 @@ export const runPublish = async (options = {}) => {
670
670
  });
671
671
  }
672
672
 
673
- // Step 9: Filter out redundant file deletions
673
+ // Step 9: Filter out redundant file and folder deletions
674
674
  const foldersBeingDeleted = actionQueue
675
675
  .filter(a => a.action === 'delete_folder')
676
676
  .map(a => a.folderPath);
677
677
 
678
678
  const filteredActionQueue = actionQueue.filter(action => {
679
+ // Filter out files inside folders being deleted
679
680
  if (action.action === 'delete_static_asset' && action.filePath) {
680
- // Check if this file's directory is inside any folder being deleted
681
681
  const fileDir = path.dirname(action.filePath);
682
682
  for (const deletedFolder of foldersBeingDeleted) {
683
- // Use path.normalize to handle trailing slashes and ensure proper comparison
684
683
  const normalizedFileDir = path.normalize(fileDir);
685
684
  const normalizedDeletedFolder = path.normalize(deletedFolder);
686
685
 
687
- // Check if file is inside the deleted folder (or is the folder itself)
688
686
  if (normalizedFileDir === normalizedDeletedFolder ||
689
687
  normalizedFileDir.startsWith(normalizedDeletedFolder + path.sep)) {
690
688
  return false; // Skip - covered by folder deletion
691
689
  }
692
690
  }
693
691
  }
692
+
693
+ // Filter out child folders inside folders being deleted
694
+ if (action.action === 'delete_folder' && action.folderPath) {
695
+ for (const deletedFolder of foldersBeingDeleted) {
696
+ // Don't compare a folder to itself
697
+ if (action.folderPath === deletedFolder) continue;
698
+
699
+ const normalizedChildFolder = path.normalize(action.folderPath);
700
+ const normalizedParentFolder = path.normalize(deletedFolder);
701
+
702
+ // Check if this folder is inside another folder being deleted
703
+ if (normalizedChildFolder.startsWith(normalizedParentFolder + path.sep)) {
704
+ return false; // Skip - covered by parent folder deletion
705
+ }
706
+ }
707
+ }
708
+
694
709
  return true;
695
710
  });
696
711
 
package/actions/pull.js CHANGED
@@ -5,7 +5,7 @@ import fs from "fs";
5
5
  import { withSpinner } from "../utils/spinner.js";
6
6
  import { EXPORT_ROOT, TYPE_DIR_MAP } from "../vars/global.js";
7
7
  import { mapRecordToFile, writeRecords } from "../utils/cli/writeRecords.js";
8
- import { updateBase } from "../utils/updateFileBase.js";
8
+ import { updateBase, removeFromBase } from "../utils/updateFileBase.js";
9
9
  import { compareAllFilesAndLogStatus, promptConflictResolution, showCurrentConflicts } from "../utils/cli/helpers/compare.js";
10
10
  import path from "path";
11
11
  import { compareLocalAndRemote } from "../utils/compare.js";
@@ -94,7 +94,14 @@ export const pull = async () => {
94
94
  }
95
95
  ];
96
96
 
97
- // Step 3: Download records in parallel with spinner
97
+ // Step 3: Load existing base.json to detect deletions
98
+ const hits = await config.searchObject({}, { filename: "base.json", global: false });
99
+ const cachedResults = hits?.[0]?.value || {};
100
+ const cachedAssets = Object.values(cachedResults).filter(c =>
101
+ c.type === 'File' || c.type === 'Folder'
102
+ );
103
+
104
+ // Step 4: Download records in parallel with spinner
98
105
  const [activeClassResult, activePageResult, assets] = await withSpinner("Downloading files...", async () => {
99
106
  const meqlResults = await Promise.all(
100
107
  queries.map(q => meqlQuery(instanceUrl, token.value, q.query))
@@ -108,7 +115,52 @@ export const pull = async () => {
108
115
  ]
109
116
  });
110
117
 
111
- // Update assets base
118
+ // Collect all server asset paths
119
+ const serverAssetPaths = new Set();
120
+ const collectServerPaths = (records) => {
121
+ for (const record of records) {
122
+ const fullPath = path.join(EXPORT_ROOT, record?.Path);
123
+ serverAssetPaths.add(path.normalize(fullPath));
124
+
125
+ if (record?.Type === 'Folder' && record?.Children?.length > 0) {
126
+ collectServerPaths(record.Children);
127
+ }
128
+ }
129
+ };
130
+ collectServerPaths(assets.tree);
131
+
132
+ // Step 5: Detect and delete assets that were on server before but are now gone
133
+ const assetsToDelete = [];
134
+ for (const cached of cachedAssets) {
135
+ const cachedPath = path.normalize(cached.filePath || cached.lastKnownPath);
136
+
137
+ // If this asset was in base.json but not returned from server, it was deleted
138
+ if (!serverAssetPaths.has(cachedPath)) {
139
+ assetsToDelete.push(cachedPath);
140
+ }
141
+ }
142
+
143
+ // Delete local files/folders that were deleted on server
144
+ for (const assetPath of assetsToDelete) {
145
+ try {
146
+ if (fs.existsSync(assetPath)) {
147
+ const stats = fs.statSync(assetPath);
148
+ if (stats.isDirectory()) {
149
+ fs.rmSync(assetPath, { recursive: true, force: true });
150
+ console.log(chalk.gray(` 🗑️ Removed deleted folder: ${path.relative(process.cwd(), assetPath)}`));
151
+ } else {
152
+ fs.unlinkSync(assetPath);
153
+ console.log(chalk.gray(` 🗑️ Removed deleted file: ${path.relative(process.cwd(), assetPath)}`));
154
+ }
155
+ }
156
+ // Remove from base.json
157
+ removeFromBase(assetPath);
158
+ } catch (err) {
159
+ console.warn(chalk.yellow(` ⚠️ Could not delete ${assetPath}: ${err.message}`));
160
+ }
161
+ }
162
+
163
+ // Step 6: Update assets base
112
164
  const processAssets = (records) => {
113
165
  for (const record of records) {
114
166
  if (record?.Type === 'Folder') {
@@ -139,7 +191,7 @@ export const pull = async () => {
139
191
 
140
192
  processAssets(assets.tree);
141
193
 
142
- // Check for conflicts and have user select conflict resolution
194
+ // Step 7: Handle code entity (ActiveClass, ActivePage) deletions
143
195
  const activeClassRecords = (activeClassResult.Records || []).map(record => {
144
196
  record.Content = record.Body;
145
197
  delete record.Body;
@@ -149,6 +201,40 @@ export const pull = async () => {
149
201
  const activePageRecords = (activePageResult.Records || []);
150
202
  const allRecords = [...activeClassRecords, ...activePageRecords].map(mapRecordToFile);
151
203
 
204
+ // Get all server record IDs
205
+ const serverRecordIds = new Set([
206
+ ...activeClassRecords.map(r => r.Id),
207
+ ...activePageRecords.map(r => r.Id)
208
+ ]);
209
+
210
+ // Find code entities in base.json that are no longer on server
211
+ const cachedCodeEntities = Object.values(cachedResults).filter(c =>
212
+ c.type !== 'File' && c.type !== 'Folder' && c.recordId
213
+ );
214
+
215
+ const codeEntitiesToDelete = [];
216
+ for (const cached of cachedCodeEntities) {
217
+ // If this code entity was in base.json but not returned from server, it was deleted
218
+ if (!serverRecordIds.has(cached.recordId)) {
219
+ codeEntitiesToDelete.push(cached);
220
+ }
221
+ }
222
+
223
+ // Delete local code entity files that were deleted on server
224
+ for (const entity of codeEntitiesToDelete) {
225
+ const entityPath = entity.filePath || entity.lastKnownPath;
226
+ try {
227
+ if (entityPath && fs.existsSync(entityPath)) {
228
+ fs.unlinkSync(entityPath);
229
+ console.log(chalk.gray(` 🗑️ Removed deleted ${entity.type}: ${path.relative(process.cwd(), entityPath)}`));
230
+ }
231
+ // Remove from base.json
232
+ removeFromBase(entity.recordId);
233
+ } catch (err) {
234
+ console.warn(chalk.yellow(` ⚠️ Could not delete ${entityPath}: ${err.message}`));
235
+ }
236
+ }
237
+
152
238
  const issues = [];
153
239
  for (const record of allRecords) {
154
240
  if (record?.error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magentrix-corp/magentrix-cli",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "CLI tool for synchronizing local files with Magentrix cloud platform",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -12,13 +12,13 @@ import { EXPORT_ROOT } from '../vars/global.js';
12
12
 
13
13
  /**
14
14
  * Convert a local asset path to an API path by adding the 'contents' prefix
15
- * and normalizing to lowercase with forward slashes.
15
+ * and normalizing with forward slashes (preserves original casing).
16
16
  *
17
- * @param {string} localPath - Local file path (e.g., "src/Assets/images/logo.png")
18
- * @returns {string} API path (e.g., "/contents/assets/images")
17
+ * @param {string} localPath - Local file path (e.g., "src/Assets/Images/Logo.png")
18
+ * @returns {string} API path (e.g., "/contents/assets/Images")
19
19
  *
20
20
  * @example
21
- * toApiPath("src/Assets/images/logo.png") // "/contents/assets/images"
21
+ * toApiPath("src/Assets/Images/Logo.png") // "/contents/assets/Images"
22
22
  * toApiPath("src/Assets") // "/contents/assets"
23
23
  */
24
24
  export const toApiPath = (localPath) => {
@@ -31,11 +31,11 @@ export const toApiPath = (localPath) => {
31
31
  const normalized = path.normalize(localPath);
32
32
  const relative = normalized.replace(new RegExp(`^${EXPORT_ROOT}[\\\\/]?`), '');
33
33
 
34
- // Replace 'Assets' with 'contents/assets'
34
+ // Replace 'Assets' with 'contents/assets' (case insensitive search, but replace with lowercase)
35
35
  const apiPath = relative.replace(/^Assets/i, 'contents/assets');
36
36
 
37
- // Normalize to forward slashes and lowercase, remove filename
38
- let dirPath = path.dirname(apiPath).replace(/\\/g, '/').toLowerCase();
37
+ // Normalize to forward slashes (preserve casing!), remove filename
38
+ let dirPath = path.dirname(apiPath).replace(/\\/g, '/');
39
39
 
40
40
  // Handle edge case where dirname returns '.' for root
41
41
  if (dirPath === '.') {
@@ -69,13 +69,13 @@ export const toLocalPath = (apiPath) => {
69
69
 
70
70
  /**
71
71
  * Convert a local folder path to an API path (keeps the folder, doesn't extract parent).
72
- * Similar to toApiPath but for folders.
72
+ * Similar to toApiPath but for folders (preserves original casing).
73
73
  *
74
- * @param {string} localFolderPath - Local folder path (e.g., "src/Assets/images")
75
- * @returns {string} API path (e.g., "/contents/assets/images")
74
+ * @param {string} localFolderPath - Local folder path (e.g., "src/Assets/Images")
75
+ * @returns {string} API path (e.g., "/contents/assets/Images")
76
76
  *
77
77
  * @example
78
- * toApiFolderPath("src/Assets/images") // "/contents/assets/images"
78
+ * toApiFolderPath("src/Assets/Images") // "/contents/assets/Images"
79
79
  * toApiFolderPath("src/Assets") // "/contents/assets"
80
80
  */
81
81
  export const toApiFolderPath = (localFolderPath) => {
@@ -88,11 +88,11 @@ export const toApiFolderPath = (localFolderPath) => {
88
88
  const normalized = path.normalize(localFolderPath);
89
89
  const relative = normalized.replace(new RegExp(`^${EXPORT_ROOT}[\\\\/]?`), '');
90
90
 
91
- // Replace 'Assets' with 'contents/assets'
91
+ // Replace 'Assets' with 'contents/assets' (case insensitive search, but replace with lowercase)
92
92
  let apiPath = relative.replace(/^Assets/i, 'contents/assets');
93
93
 
94
- // Normalize to forward slashes and lowercase (but don't get dirname!)
95
- apiPath = apiPath.replace(/\\/g, '/').toLowerCase();
94
+ // Normalize to forward slashes (preserve casing!)
95
+ apiPath = apiPath.replace(/\\/g, '/');
96
96
 
97
97
  // Handle edge case where path is just 'Assets'
98
98
  if (apiPath === 'contents/assets' || apiPath === '') {