@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.
- package/actions/publish.js +19 -4
- package/actions/pull.js +90 -4
- package/package.json +1 -1
- package/utils/assetPaths.js +14 -14
package/actions/publish.js
CHANGED
|
@@ -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:
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
package/utils/assetPaths.js
CHANGED
|
@@ -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
|
|
15
|
+
* and normalizing with forward slashes (preserves original casing).
|
|
16
16
|
*
|
|
17
|
-
* @param {string} localPath - Local file path (e.g., "src/Assets/
|
|
18
|
-
* @returns {string} API path (e.g., "/contents/assets/
|
|
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/
|
|
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
|
|
38
|
-
let dirPath = path.dirname(apiPath).replace(/\\/g, '/')
|
|
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/
|
|
75
|
-
* @returns {string} API path (e.g., "/contents/assets/
|
|
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/
|
|
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
|
|
95
|
-
apiPath = apiPath.replace(/\\/g, '/')
|
|
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 === '') {
|