@magentrix-corp/magentrix-cli 1.0.1 → 1.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.
- package/README.md +99 -1
- package/actions/autopublish.v2.js +2 -2
- package/actions/create.js +180 -64
- package/actions/publish.js +348 -253
- package/actions/pull.js +53 -8
- package/actions/setup.js +34 -7
- package/bin/magentrix.js +61 -2
- package/package.json +3 -2
- package/utils/assetPaths.js +138 -0
- package/utils/cacher.js +5 -2
- package/utils/cli/helpers/compare.js +4 -2
- package/utils/downloadAssets.js +14 -8
- package/utils/magentrix/api/assets.js +21 -1
- package/utils/magentrix/api/retrieveEntity.js +55 -1
- package/utils/updateFileBase.js +8 -3
- package/vars/global.js +1 -0
package/actions/publish.js
CHANGED
|
@@ -11,196 +11,114 @@ import {
|
|
|
11
11
|
EXPORT_ROOT,
|
|
12
12
|
TYPE_DIR_MAP,
|
|
13
13
|
} from "../vars/global.js";
|
|
14
|
-
import {
|
|
15
|
-
getFileTag,
|
|
16
|
-
} from "../utils/filetag.js";
|
|
14
|
+
import { getFileTag, setFileTag } from "../utils/filetag.js";
|
|
17
15
|
import { sha256 } from "../utils/hash.js";
|
|
18
16
|
import { createEntity } from "../utils/magentrix/api/createEntity.js";
|
|
19
17
|
import { updateEntity } from "../utils/magentrix/api/updateEntity.js";
|
|
20
18
|
import { deleteEntity } from "../utils/magentrix/api/deleteEntity.js";
|
|
21
|
-
import { setFileTag } from "../utils/filetag.js";
|
|
22
19
|
import { removeFromBase, updateBase } from "../utils/updateFileBase.js";
|
|
23
|
-
import { deleteAsset, uploadAsset } from "../utils/magentrix/api/assets.js";
|
|
24
|
-
import {
|
|
20
|
+
import { deleteAsset, uploadAsset, createFolder } from "../utils/magentrix/api/assets.js";
|
|
21
|
+
import { toApiPath, toApiFolderPath } from "../utils/assetPaths.js";
|
|
25
22
|
|
|
26
23
|
const config = new Config();
|
|
27
24
|
|
|
28
|
-
/*
|
|
25
|
+
/* ==================== UTILITY FUNCTIONS ==================== */
|
|
29
26
|
|
|
30
27
|
/**
|
|
31
|
-
*
|
|
28
|
+
* Infers Magentrix entity metadata from a file path by walking up the directory tree.
|
|
29
|
+
* @param {string} filePath - The file path to analyze
|
|
30
|
+
* @returns {Object} { type, entity, contentField } for Magentrix API
|
|
32
31
|
*/
|
|
33
32
|
const inferMeta = (filePath) => {
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
type
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
let currentPath = path.dirname(filePath);
|
|
34
|
+
const exportRootAbs = path.resolve(EXPORT_ROOT);
|
|
35
|
+
|
|
36
|
+
while (currentPath.startsWith(exportRootAbs)) {
|
|
37
|
+
const folderName = path.basename(currentPath);
|
|
38
|
+
const type = Object.keys(TYPE_DIR_MAP).find(
|
|
39
|
+
(k) => TYPE_DIR_MAP[k].directory === folderName
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (type) {
|
|
43
|
+
return {
|
|
44
|
+
type,
|
|
45
|
+
entity: ENTITY_TYPE_MAP[type],
|
|
46
|
+
contentField: ENTITY_FIELD_MAP[type],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const parentPath = path.dirname(currentPath);
|
|
51
|
+
if (parentPath === currentPath) break; // Reached root
|
|
52
|
+
currentPath = parentPath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { type: undefined, entity: undefined, contentField: undefined };
|
|
43
56
|
};
|
|
44
57
|
|
|
45
58
|
/**
|
|
46
|
-
* Safely
|
|
59
|
+
* Safely reads file content and computes hash.
|
|
60
|
+
* @param {string} filePath - Path to the file
|
|
61
|
+
* @returns {Object|null} { content, hash } or null on error
|
|
47
62
|
*/
|
|
48
63
|
const readFileSafe = (filePath) => {
|
|
49
64
|
try {
|
|
50
65
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
51
66
|
return { content, hash: sha256(content) };
|
|
52
67
|
} catch (err) {
|
|
53
|
-
console.error(
|
|
54
|
-
|
|
55
|
-
" Unable to read file " +
|
|
56
|
-
chalk.white.bold(filePath)
|
|
57
|
-
);
|
|
58
|
-
console.error(
|
|
59
|
-
chalk.gray("→ Please verify the file exists and you have read permissions.")
|
|
60
|
-
);
|
|
68
|
+
console.error(chalk.red.bold("Error:") + " Unable to read file " + chalk.white.bold(filePath));
|
|
69
|
+
console.error(chalk.gray("→ Please verify the file exists and you have read permissions."));
|
|
61
70
|
return null;
|
|
62
71
|
}
|
|
63
72
|
};
|
|
64
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Formats multiline error messages with proper indentation.
|
|
76
|
+
*/
|
|
65
77
|
const formatMultilineError = (err) => {
|
|
66
|
-
// Ensure it's a string and split into lines
|
|
67
78
|
const lines = String(err).split(/\r?\n/);
|
|
68
|
-
// First line gets the bullet, rest get indentation
|
|
69
79
|
return lines
|
|
70
80
|
.map((line, i) => {
|
|
71
|
-
const prefix = i === 0 ? `${chalk.redBright(' •')} ` : ' ';
|
|
81
|
+
const prefix = i === 0 ? `${chalk.redBright(' •')} ` : ' ';
|
|
72
82
|
return prefix + chalk.whiteBright(line);
|
|
73
83
|
})
|
|
74
84
|
.join('\n');
|
|
75
85
|
};
|
|
76
86
|
|
|
77
|
-
const getFolderFromPath = (filePath) => {
|
|
78
|
-
const fileFolderPath = filePath
|
|
79
|
-
.split("/")
|
|
80
|
-
.filter((x, i) => !(x === EXPORT_ROOT && i === 0))
|
|
81
|
-
.slice(0, -1)
|
|
82
|
-
.join("/");
|
|
83
|
-
|
|
84
|
-
return fileFolderPath;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/* ---------- Network operations ---------- */
|
|
88
|
-
|
|
89
87
|
/**
|
|
90
|
-
*
|
|
91
|
-
* Updates cache after successful operations.
|
|
88
|
+
* Recursively walks a directory and returns all subdirectory paths.
|
|
92
89
|
*/
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// Execute all operations in parallel
|
|
97
|
-
const results = await Promise.allSettled(
|
|
98
|
-
actionQueue.map(async (action, index) => {
|
|
99
|
-
try {
|
|
100
|
-
let result;
|
|
101
|
-
switch (action.action) {
|
|
102
|
-
case "create":
|
|
103
|
-
result = await handleCreateAction(instanceUrl, token.value, action);
|
|
104
|
-
break;
|
|
105
|
-
case "update":
|
|
106
|
-
result = await handleUpdateAction(instanceUrl, token.value, action);
|
|
107
|
-
break;
|
|
108
|
-
case "delete":
|
|
109
|
-
result = await handleDeleteAction(instanceUrl, token.value, action);
|
|
110
|
-
break;
|
|
111
|
-
case "create_static_asset":
|
|
112
|
-
result = await handleCreateStaticAssetAction(instanceUrl, token.value, action);
|
|
113
|
-
break;
|
|
114
|
-
case "delete_static_asset":
|
|
115
|
-
result = await handleDeleteStaticAssetAction(instanceUrl, token.value, action);
|
|
116
|
-
break;
|
|
117
|
-
default:
|
|
118
|
-
throw new Error(`Unknown action: ${action.action}`);
|
|
119
|
-
}
|
|
120
|
-
return { index, action, result, success: true };
|
|
121
|
-
} catch (error) {
|
|
122
|
-
return { index, action, error: error.message, success: false };
|
|
123
|
-
}
|
|
124
|
-
})
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
// Process results
|
|
128
|
-
let successCount = 0;
|
|
129
|
-
let errorCount = 0;
|
|
130
|
-
const errors = [];
|
|
90
|
+
const walkFolders = (dir) => {
|
|
91
|
+
if (!fs.existsSync(dir)) return [];
|
|
131
92
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const { index, action, success, error, result: operationResult } = result.value;
|
|
135
|
-
if (success) {
|
|
136
|
-
successCount++;
|
|
137
|
-
console.log(
|
|
138
|
-
chalk.green(`✓ [${index + 1}]`) +
|
|
139
|
-
` ${chalk.yellow(action.action.toUpperCase())} ${chalk.cyan(action.type || action?.names?.join(", ") || path.basename(action.filePath) || '')} ` +
|
|
140
|
-
(operationResult?.recordId ? chalk.magenta(operationResult.recordId) : "")
|
|
141
|
-
);
|
|
93
|
+
const folders = [];
|
|
94
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
142
95
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// Enhanced error display matching autopublish.js style
|
|
150
|
-
console.log();
|
|
151
|
-
console.log(chalk.bgRed.bold.white(' ✖ Operation Failed '));
|
|
152
|
-
console.log(chalk.redBright('─'.repeat(48)));
|
|
153
|
-
console.log(
|
|
154
|
-
chalk.red.bold(`[${index + 1}] ${action.action.toUpperCase()} ${action.type || action?.names?.join(", ") || ''} (${action.filePath || action.folder}):`)
|
|
155
|
-
);
|
|
156
|
-
// console.log(`${chalk.redBright(' •')} ${chalk.whiteBright(error)}`);
|
|
157
|
-
console.log(formatMultilineError(error));
|
|
158
|
-
console.log(chalk.redBright('─'.repeat(48)));
|
|
159
|
-
}
|
|
160
|
-
} else {
|
|
161
|
-
// Promise rejection (shouldn't happen with allSettled, but just in case)
|
|
162
|
-
errorCount++;
|
|
163
|
-
console.log();
|
|
164
|
-
console.log(chalk.bgRed.bold.white(' ✖ Unexpected Error '));
|
|
165
|
-
console.log(chalk.redBright('─'.repeat(48)));
|
|
166
|
-
console.log(`${chalk.redBright(' •')} ${chalk.whiteBright(result.reason)}`);
|
|
167
|
-
console.log(chalk.redBright('─'.repeat(48)));
|
|
96
|
+
for (const item of items) {
|
|
97
|
+
if (item.isDirectory()) {
|
|
98
|
+
const fullPath = path.join(dir, item.name);
|
|
99
|
+
folders.push(fullPath);
|
|
100
|
+
folders.push(...walkFolders(fullPath));
|
|
168
101
|
}
|
|
169
102
|
}
|
|
170
103
|
|
|
171
|
-
|
|
172
|
-
console.log(chalk.blue("\n--- Publish Summary ---"));
|
|
173
|
-
console.log(chalk.green(`✓ Successful: ${successCount}`));
|
|
174
|
-
if (errorCount > 0) {
|
|
175
|
-
console.log(chalk.red(`✗ Failed: ${errorCount}`));
|
|
176
|
-
// console.log(chalk.yellow("\nErrors encountered:"));
|
|
177
|
-
// errors.forEach(({ index, action, error }) => {
|
|
178
|
-
// console.log(` [${index}] ${action.action.toUpperCase()} ${action.type}: ${error}`);
|
|
179
|
-
// });
|
|
180
|
-
} else {
|
|
181
|
-
console.log(chalk.green("All operations completed successfully! 🎉"));
|
|
182
|
-
}
|
|
104
|
+
return folders;
|
|
183
105
|
};
|
|
184
106
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (
|
|
193
|
-
return
|
|
194
|
-
}
|
|
107
|
+
/**
|
|
108
|
+
* Gets display name for an action in log messages.
|
|
109
|
+
*/
|
|
110
|
+
const getActionDisplayName = (action) => {
|
|
111
|
+
if (action.folderName) return action.folderName;
|
|
112
|
+
if (action.names) return action.names.join(", ");
|
|
113
|
+
if (action.type) return action.type;
|
|
114
|
+
if (action.filePath) return path.basename(action.filePath);
|
|
115
|
+
return '';
|
|
116
|
+
};
|
|
195
117
|
|
|
196
|
-
|
|
197
|
-
const response = await deleteAsset(instanceUrl, apiKey, `/${action.folder}`, action.names).catch(err => ({ error: err.message }));
|
|
198
|
-
if (response?.error) throw new Error(response.error);
|
|
199
|
-
return response;
|
|
200
|
-
}
|
|
118
|
+
/* ==================== ACTION HANDLERS ==================== */
|
|
201
119
|
|
|
202
120
|
/**
|
|
203
|
-
*
|
|
121
|
+
* Handles CREATE action for code entities (ActiveClass/ActivePage).
|
|
204
122
|
*/
|
|
205
123
|
const handleCreateAction = async (instanceUrl, apiKey, action) => {
|
|
206
124
|
const data = {
|
|
@@ -214,38 +132,69 @@ const handleCreateAction = async (instanceUrl, apiKey, action) => {
|
|
|
214
132
|
};
|
|
215
133
|
|
|
216
134
|
/**
|
|
217
|
-
*
|
|
135
|
+
* Handles UPDATE action for code entities (ActiveClass/ActivePage).
|
|
218
136
|
*/
|
|
219
137
|
const handleUpdateAction = async (instanceUrl, apiKey, action) => {
|
|
220
|
-
const data = {
|
|
221
|
-
...action.fields
|
|
222
|
-
};
|
|
138
|
+
const data = { ...action.fields };
|
|
223
139
|
|
|
224
|
-
// If renamed, update the Name field
|
|
225
140
|
if (action.renamed) {
|
|
226
141
|
data.Name = path.basename(action.filePath, path.extname(action.filePath));
|
|
227
142
|
}
|
|
228
143
|
|
|
229
|
-
|
|
144
|
+
await updateEntity(instanceUrl, apiKey, action.entity, action.recordId, data);
|
|
230
145
|
return { recordId: action.recordId };
|
|
231
146
|
};
|
|
232
147
|
|
|
233
148
|
/**
|
|
234
|
-
*
|
|
149
|
+
* Handles DELETE action for code entities (ActiveClass/ActivePage).
|
|
235
150
|
*/
|
|
236
151
|
const handleDeleteAction = async (instanceUrl, apiKey, action) => {
|
|
237
|
-
|
|
238
|
-
|
|
152
|
+
return await deleteEntity(instanceUrl, apiKey, action.entity, action.recordId);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Handles CREATE_STATIC_ASSET action.
|
|
157
|
+
*/
|
|
158
|
+
const handleCreateStaticAssetAction = async (instanceUrl, apiKey, action) => {
|
|
159
|
+
const response = await uploadAsset(instanceUrl, apiKey, `/${action.folder}`, [action.filePath]);
|
|
160
|
+
if (response?.error) throw new Error(response.error);
|
|
161
|
+
return response;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Handles DELETE_STATIC_ASSET action.
|
|
166
|
+
*/
|
|
167
|
+
const handleDeleteStaticAssetAction = async (instanceUrl, apiKey, action) => {
|
|
168
|
+
const response = await deleteAsset(instanceUrl, apiKey, `/${action.folder}`, action.names);
|
|
169
|
+
if (response?.error) throw new Error(response.error);
|
|
170
|
+
return response;
|
|
239
171
|
};
|
|
240
172
|
|
|
241
173
|
/**
|
|
242
|
-
*
|
|
174
|
+
* Handles CREATE_FOLDER action.
|
|
175
|
+
*/
|
|
176
|
+
const handleCreateFolderAction = async (instanceUrl, apiKey, action) => {
|
|
177
|
+
const response = await createFolder(instanceUrl, apiKey, action.parentPath, action.folderName);
|
|
178
|
+
if (response?.error) throw new Error(response.error);
|
|
179
|
+
return response;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Handles DELETE_FOLDER action.
|
|
184
|
+
*/
|
|
185
|
+
const handleDeleteFolderAction = async (instanceUrl, apiKey, action) => {
|
|
186
|
+
const response = await deleteAsset(instanceUrl, apiKey, action.parentPath, [action.folderName]);
|
|
187
|
+
if (response?.error) throw new Error(response.error);
|
|
188
|
+
return response;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Updates cache after successful operations.
|
|
243
193
|
*/
|
|
244
194
|
const updateCacheAfterSuccess = async (action, operationResult) => {
|
|
245
195
|
try {
|
|
246
196
|
switch (action.action) {
|
|
247
|
-
case "create":
|
|
248
|
-
// Tag the file and update base with the content that was actually published
|
|
197
|
+
case "create": {
|
|
249
198
|
const createSnapshot = action.fields && Object.values(action.fields)[0]
|
|
250
199
|
? { content: Object.values(action.fields)[0], hash: action.contentHash }
|
|
251
200
|
: null;
|
|
@@ -257,19 +206,21 @@ const updateCacheAfterSuccess = async (action, operationResult) => {
|
|
|
257
206
|
createSnapshot
|
|
258
207
|
);
|
|
259
208
|
break;
|
|
209
|
+
}
|
|
260
210
|
|
|
261
|
-
case "update":
|
|
262
|
-
// Pass the content snapshot that was actually published to prevent race conditions
|
|
211
|
+
case "update": {
|
|
263
212
|
const updateSnapshot = action.fields && Object.values(action.fields)[0]
|
|
264
213
|
? { content: Object.values(action.fields)[0], hash: sha256(Object.values(action.fields)[0]) }
|
|
265
214
|
: null;
|
|
215
|
+
const type = Object.keys(ENTITY_TYPE_MAP).find(key => ENTITY_TYPE_MAP[key] === action.entity);
|
|
266
216
|
updateBase(
|
|
267
217
|
action.filePath,
|
|
268
|
-
{ Id: action.recordId, Type:
|
|
218
|
+
{ Id: action.recordId, Type: type },
|
|
269
219
|
'',
|
|
270
220
|
updateSnapshot
|
|
271
221
|
);
|
|
272
222
|
break;
|
|
223
|
+
}
|
|
273
224
|
|
|
274
225
|
case "delete":
|
|
275
226
|
removeFromBase(action.recordId);
|
|
@@ -284,51 +235,137 @@ const updateCacheAfterSuccess = async (action, operationResult) => {
|
|
|
284
235
|
case "create_static_asset":
|
|
285
236
|
updateBase(action.filePath, { Id: action.filePath, Type: "File" });
|
|
286
237
|
break;
|
|
238
|
+
|
|
239
|
+
case "create_folder":
|
|
240
|
+
updateBase(action.folderPath, { Id: action.folderPath, Type: "Folder" });
|
|
241
|
+
break;
|
|
242
|
+
|
|
243
|
+
case "delete_folder":
|
|
244
|
+
removeFromBase(action.folderPath);
|
|
245
|
+
break;
|
|
287
246
|
}
|
|
288
247
|
} catch (error) {
|
|
289
248
|
console.warn(chalk.yellow(`Warning: Failed to update cache for ${action.action}: ${error.message}`));
|
|
290
249
|
}
|
|
291
250
|
};
|
|
292
251
|
|
|
293
|
-
/*
|
|
252
|
+
/* ==================== NETWORK REQUEST HANDLER ==================== */
|
|
294
253
|
|
|
295
254
|
/**
|
|
296
|
-
*
|
|
297
|
-
* @param {Object} options - Configuration options
|
|
298
|
-
* @param {boolean} options.silent - If true, suppress summary output
|
|
299
|
-
* @param {boolean} options.skipAuth - If true, skip authentication (caller handles it)
|
|
300
|
-
* @param {Object} options.credentials - Pre-authenticated credentials {instanceUrl, token}
|
|
301
|
-
* @returns {Promise<{actionQueue: Array, results: Object}>}
|
|
255
|
+
* Executes all actions in the queue in parallel and handles results/errors.
|
|
302
256
|
*/
|
|
303
|
-
|
|
304
|
-
const {
|
|
257
|
+
const performNetworkRequest = async (actionQueue) => {
|
|
258
|
+
const { instanceUrl, token } = await ensureValidCredentials();
|
|
305
259
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
260
|
+
const results = await Promise.allSettled(
|
|
261
|
+
actionQueue.map(async (action, index) => {
|
|
262
|
+
try {
|
|
263
|
+
let result;
|
|
264
|
+
switch (action.action) {
|
|
265
|
+
case "create":
|
|
266
|
+
result = await handleCreateAction(instanceUrl, token.value, action);
|
|
267
|
+
break;
|
|
268
|
+
case "update":
|
|
269
|
+
result = await handleUpdateAction(instanceUrl, token.value, action);
|
|
270
|
+
break;
|
|
271
|
+
case "delete":
|
|
272
|
+
result = await handleDeleteAction(instanceUrl, token.value, action);
|
|
273
|
+
break;
|
|
274
|
+
case "create_static_asset":
|
|
275
|
+
result = await handleCreateStaticAssetAction(instanceUrl, token.value, action);
|
|
276
|
+
break;
|
|
277
|
+
case "delete_static_asset":
|
|
278
|
+
result = await handleDeleteStaticAssetAction(instanceUrl, token.value, action);
|
|
279
|
+
break;
|
|
280
|
+
case "create_folder":
|
|
281
|
+
result = await handleCreateFolderAction(instanceUrl, token.value, action);
|
|
282
|
+
break;
|
|
283
|
+
case "delete_folder":
|
|
284
|
+
result = await handleDeleteFolderAction(instanceUrl, token.value, action);
|
|
285
|
+
break;
|
|
286
|
+
default:
|
|
287
|
+
throw new Error(`Unknown action: ${action.action}`);
|
|
288
|
+
}
|
|
289
|
+
return { index, action, result, success: true };
|
|
290
|
+
} catch (error) {
|
|
291
|
+
return { index, action, error: error.message, success: false };
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// Process and display results
|
|
297
|
+
let successCount = 0;
|
|
298
|
+
let errorCount = 0;
|
|
299
|
+
|
|
300
|
+
for (const result of results) {
|
|
301
|
+
if (result.status === "fulfilled") {
|
|
302
|
+
const { index, action, success, error, result: operationResult } = result.value;
|
|
303
|
+
|
|
304
|
+
if (success) {
|
|
305
|
+
successCount++;
|
|
306
|
+
const displayName = getActionDisplayName(action);
|
|
307
|
+
console.log(
|
|
308
|
+
chalk.green(`✓ [${index + 1}]`) +
|
|
309
|
+
` ${chalk.yellow(action.action.toUpperCase())} ${chalk.cyan(displayName)} ` +
|
|
310
|
+
(operationResult?.recordId ? chalk.magenta(operationResult.recordId) : "")
|
|
316
311
|
);
|
|
312
|
+
await updateCacheAfterSuccess(action, operationResult);
|
|
313
|
+
} else {
|
|
314
|
+
errorCount++;
|
|
315
|
+
console.log();
|
|
316
|
+
console.log(chalk.bgRed.bold.white(' ✖ Operation Failed '));
|
|
317
|
+
console.log(chalk.redBright('─'.repeat(48)));
|
|
318
|
+
console.log(chalk.red.bold(`[${index + 1}] ${action.action.toUpperCase()} ${getActionDisplayName(action)} (${action.filePath || action.folderPath || action.folder}):`));
|
|
319
|
+
console.log(formatMultilineError(error));
|
|
320
|
+
console.log(chalk.redBright('─'.repeat(48)));
|
|
317
321
|
}
|
|
318
|
-
|
|
319
|
-
|
|
322
|
+
} else {
|
|
323
|
+
errorCount++;
|
|
324
|
+
console.log();
|
|
325
|
+
console.log(chalk.bgRed.bold.white(' ✖ Unexpected Error '));
|
|
326
|
+
console.log(chalk.redBright('─'.repeat(48)));
|
|
327
|
+
console.log(`${chalk.redBright(' •')} ${chalk.whiteBright(result.reason)}`);
|
|
328
|
+
console.log(chalk.redBright('─'.repeat(48)));
|
|
329
|
+
}
|
|
320
330
|
}
|
|
321
331
|
|
|
322
|
-
|
|
332
|
+
// Summary
|
|
333
|
+
console.log(chalk.blue("\n--- Publish Summary ---"));
|
|
334
|
+
console.log(chalk.green(`✓ Successful: ${successCount}`));
|
|
335
|
+
if (errorCount > 0) {
|
|
336
|
+
console.log(chalk.red(`✗ Failed: ${errorCount}`));
|
|
337
|
+
} else {
|
|
338
|
+
console.log(chalk.green("All operations completed successfully! 🎉"));
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/* ==================== MAIN PUBLISH LOGIC ==================== */
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Core publish logic - can be called from autopublish or directly.
|
|
346
|
+
* @param {Object} options - Configuration options
|
|
347
|
+
* @param {boolean} options.silent - If true, suppress summary output
|
|
348
|
+
* @returns {Promise<{actionQueue: Array, hasChanges: boolean}>}
|
|
349
|
+
*/
|
|
350
|
+
export const runPublish = async (options = {}) => {
|
|
351
|
+
const { silent = false } = options;
|
|
352
|
+
|
|
353
|
+
// Step 1: Authenticate
|
|
354
|
+
await ensureValidCredentials().catch((err) => {
|
|
355
|
+
if (!silent) {
|
|
356
|
+
console.error(chalk.red.bold("Authentication failed:"), chalk.white(err.message));
|
|
357
|
+
}
|
|
358
|
+
throw err;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Step 2: Load cached file state
|
|
323
362
|
const hits = await config.searchObject({}, { filename: "base.json", global: false });
|
|
324
363
|
const cachedResults = hits?.[0]?.value || {};
|
|
325
364
|
|
|
326
365
|
if (!Object.keys(cachedResults).length) {
|
|
327
366
|
if (!silent) {
|
|
328
367
|
console.log(chalk.red.bold("No file cache found!"));
|
|
329
|
-
console.log(
|
|
330
|
-
`Run ${chalk.cyan("magentrix pull")} to initialize your workspace.`
|
|
331
|
-
);
|
|
368
|
+
console.log(`Run ${chalk.cyan("magentrix pull")} to initialize your workspace.`);
|
|
332
369
|
}
|
|
333
370
|
throw new Error("No file cache found");
|
|
334
371
|
}
|
|
@@ -336,99 +373,124 @@ export const runPublish = async (options = {}) => {
|
|
|
336
373
|
const cachedFiles = Object.values(cachedResults).map((c) => ({
|
|
337
374
|
...c,
|
|
338
375
|
tag: c.recordId,
|
|
376
|
+
filePath: c.filePath || c.lastKnownPath,
|
|
339
377
|
}));
|
|
340
378
|
|
|
341
|
-
|
|
342
|
-
const localPaths = await walkFiles(EXPORT_ROOT, { ignore: [path.join(EXPORT_ROOT, '
|
|
379
|
+
// Step 3: Scan local workspace (excluding Assets folder)
|
|
380
|
+
const localPaths = await walkFiles(EXPORT_ROOT, { ignore: [path.join(EXPORT_ROOT, 'Assets')] });
|
|
343
381
|
const localFiles = await Promise.all(
|
|
344
382
|
localPaths.map(async (p) => {
|
|
345
383
|
try {
|
|
346
384
|
const tag = await getFileTag(p);
|
|
347
385
|
return { tag, path: p };
|
|
348
386
|
} catch {
|
|
349
|
-
// console.log(
|
|
350
|
-
// chalk.yellow(
|
|
351
|
-
// `Warning: Could not retrieve tag for ${p}. Treating as new file.`
|
|
352
|
-
// )
|
|
353
|
-
// );
|
|
354
387
|
return { tag: null, path: p };
|
|
355
388
|
}
|
|
356
389
|
})
|
|
357
390
|
);
|
|
358
391
|
|
|
359
|
-
|
|
360
|
-
const cacheById = Object.fromEntries(
|
|
361
|
-
|
|
362
|
-
);
|
|
363
|
-
const localById = Object.fromEntries(
|
|
364
|
-
localFiles.filter((f) => f.tag).map((f) => [f.tag, f])
|
|
365
|
-
);
|
|
392
|
+
// Step 4: Create lookup maps
|
|
393
|
+
const cacheById = Object.fromEntries(cachedFiles.map((c) => [c.tag, c]));
|
|
394
|
+
const localById = Object.fromEntries(localFiles.filter((f) => f.tag).map((f) => [f.tag, f]));
|
|
366
395
|
const newLocalNoId = localFiles.filter((f) => !f.tag);
|
|
396
|
+
const allIds = new Set([...Object.keys(cacheById), ...Object.keys(localById)]);
|
|
367
397
|
|
|
368
|
-
/* 5 — Determine action per recordId */
|
|
369
|
-
const allIds = new Set([
|
|
370
|
-
...Object.keys(cacheById),
|
|
371
|
-
...Object.keys(localById),
|
|
372
|
-
]);
|
|
373
398
|
const actionQueue = [];
|
|
374
399
|
|
|
375
|
-
//
|
|
376
|
-
const assetPaths = await walkFiles(path.join(EXPORT_ROOT, '
|
|
400
|
+
// Step 5: Handle static asset files
|
|
401
|
+
const assetPaths = await walkFiles(path.join(EXPORT_ROOT, 'Assets'));
|
|
377
402
|
for (const assetPath of assetPaths) {
|
|
378
|
-
|
|
379
|
-
if (cachedFiles.find(cr => cr.filePath.toLowerCase() === assetPath.toLowerCase())) {
|
|
403
|
+
if (cachedFiles.find(cr => cr.filePath?.toLowerCase() === assetPath.toLowerCase())) {
|
|
380
404
|
continue;
|
|
381
405
|
}
|
|
382
406
|
|
|
383
|
-
const fileFolderPath = getFolderFromPath(assetPath);
|
|
384
|
-
|
|
385
407
|
actionQueue.push({
|
|
386
408
|
action: "create_static_asset",
|
|
387
|
-
folder:
|
|
409
|
+
folder: toApiPath(assetPath),
|
|
388
410
|
filePath: assetPath
|
|
389
411
|
});
|
|
390
412
|
}
|
|
391
413
|
|
|
414
|
+
// Step 6: Handle folder creation and deletion
|
|
415
|
+
const assetsDir = path.join(EXPORT_ROOT, 'Assets');
|
|
416
|
+
if (fs.existsSync(assetsDir)) {
|
|
417
|
+
const localFolders = walkFolders(assetsDir);
|
|
418
|
+
const cachedFolders = cachedFiles
|
|
419
|
+
.filter(c => c.type === 'Folder' && (c.filePath || c.lastKnownPath))
|
|
420
|
+
.map(c => c.filePath || c.lastKnownPath);
|
|
421
|
+
|
|
422
|
+
// New folders
|
|
423
|
+
for (const folderPath of localFolders) {
|
|
424
|
+
if (!folderPath || cachedFolders.find(cf => cf?.toLowerCase() === folderPath.toLowerCase())) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const parentDir = path.dirname(folderPath);
|
|
429
|
+
if (!parentDir || parentDir === '.' || parentDir === folderPath) continue;
|
|
430
|
+
|
|
431
|
+
actionQueue.push({
|
|
432
|
+
action: "create_folder",
|
|
433
|
+
folderPath: folderPath,
|
|
434
|
+
parentPath: toApiFolderPath(parentDir),
|
|
435
|
+
folderName: path.basename(folderPath)
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Deleted folders
|
|
440
|
+
for (const cachedFolder of cachedFolders) {
|
|
441
|
+
if (!cachedFolder || typeof cachedFolder !== 'string') continue;
|
|
442
|
+
if (localFolders.find(lf => lf?.toLowerCase() === cachedFolder.toLowerCase())) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const parentDir = path.dirname(cachedFolder);
|
|
447
|
+
if (!parentDir || parentDir === '.' || parentDir === cachedFolder) continue;
|
|
448
|
+
|
|
449
|
+
actionQueue.push({
|
|
450
|
+
action: "delete_folder",
|
|
451
|
+
folderPath: cachedFolder,
|
|
452
|
+
parentPath: toApiFolderPath(parentDir),
|
|
453
|
+
folderName: path.basename(cachedFolder)
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Step 7: Process code entities (ActiveClass/ActivePage) and static assets
|
|
392
459
|
for (const id of allIds) {
|
|
393
460
|
try {
|
|
394
461
|
const cacheFile = cacheById[id];
|
|
395
462
|
const curFile = localById[id];
|
|
396
463
|
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
464
|
+
// Skip folders - they're handled separately
|
|
465
|
+
if (cacheFile?.type === 'Folder') continue;
|
|
466
|
+
|
|
467
|
+
// Handle static asset files
|
|
468
|
+
if (cacheFile?.type === 'File') {
|
|
401
469
|
const localAssetExists = fs.existsSync(cacheFile.filePath);
|
|
402
470
|
|
|
403
|
-
// DELETE
|
|
404
471
|
if (!localAssetExists) {
|
|
405
472
|
actionQueue.push({
|
|
406
473
|
action: 'delete_static_asset',
|
|
407
|
-
folder:
|
|
474
|
+
folder: toApiPath(cacheFile.filePath),
|
|
408
475
|
names: [path.basename(cacheFile.filePath)]
|
|
409
476
|
});
|
|
410
|
-
|
|
411
477
|
continue;
|
|
412
478
|
}
|
|
413
479
|
|
|
414
|
-
// UPDATE
|
|
415
480
|
const contentHash = sha256(fs.readFileSync(cacheFile.filePath, 'utf-8'));
|
|
416
481
|
if (contentHash !== cacheFile.contentHash) {
|
|
417
|
-
// This can be handled as a file creation since the Magentrix Upload API will just overwrite the file
|
|
418
482
|
actionQueue.push({
|
|
419
|
-
aliasAction: 'update_static_asset',
|
|
420
483
|
action: "create_static_asset",
|
|
421
|
-
folder:
|
|
484
|
+
folder: toApiPath(cacheFile.filePath),
|
|
422
485
|
filePath: cacheFile.filePath
|
|
423
486
|
});
|
|
424
487
|
}
|
|
425
|
-
|
|
426
488
|
continue;
|
|
427
489
|
}
|
|
428
490
|
|
|
429
|
-
|
|
491
|
+
// Handle code entity deletion
|
|
430
492
|
if (cacheFile && !curFile) {
|
|
431
|
-
const { type, entity } = inferMeta(cacheFile.lastKnownPath);
|
|
493
|
+
const { type, entity } = inferMeta(path.resolve(cacheFile.lastKnownPath));
|
|
432
494
|
actionQueue.push({
|
|
433
495
|
action: "delete",
|
|
434
496
|
recordId: id,
|
|
@@ -439,12 +501,14 @@ export const runPublish = async (options = {}) => {
|
|
|
439
501
|
continue;
|
|
440
502
|
}
|
|
441
503
|
|
|
442
|
-
|
|
504
|
+
// Handle code entity creation
|
|
443
505
|
if (!cacheFile && curFile) {
|
|
444
506
|
const safe = readFileSafe(curFile.path);
|
|
445
507
|
if (!safe) continue;
|
|
508
|
+
|
|
446
509
|
const { content, hash } = safe;
|
|
447
|
-
const { type, entity, contentField } = inferMeta(curFile.path);
|
|
510
|
+
const { type, entity, contentField } = inferMeta(path.resolve(curFile.path));
|
|
511
|
+
|
|
448
512
|
actionQueue.push({
|
|
449
513
|
action: "create",
|
|
450
514
|
filePath: curFile.path,
|
|
@@ -457,16 +521,18 @@ export const runPublish = async (options = {}) => {
|
|
|
457
521
|
continue;
|
|
458
522
|
}
|
|
459
523
|
|
|
460
|
-
|
|
524
|
+
// Handle code entity update
|
|
461
525
|
if (cacheFile && curFile) {
|
|
462
526
|
const safe = readFileSafe(curFile.path);
|
|
463
527
|
if (!safe) continue;
|
|
464
|
-
const { content, hash } = safe;
|
|
465
528
|
|
|
529
|
+
const { content, hash } = safe;
|
|
466
530
|
const renamed = cacheFile.lastKnownPath !== path.resolve(curFile.path);
|
|
467
531
|
const contentChanged = hash !== cacheFile.contentHash;
|
|
532
|
+
|
|
468
533
|
if (renamed || contentChanged) {
|
|
469
|
-
const { type, entity, contentField } = inferMeta(curFile.path);
|
|
534
|
+
const { type, entity, contentField } = inferMeta(path.resolve(curFile.path));
|
|
535
|
+
|
|
470
536
|
actionQueue.push({
|
|
471
537
|
action: "update",
|
|
472
538
|
recordId: id,
|
|
@@ -480,23 +546,24 @@ export const runPublish = async (options = {}) => {
|
|
|
480
546
|
}
|
|
481
547
|
}
|
|
482
548
|
} catch (err) {
|
|
483
|
-
console.
|
|
549
|
+
console.error(chalk.yellow(`Warning: Error processing file with ID ${id}:`), err.message);
|
|
484
550
|
}
|
|
485
551
|
}
|
|
486
552
|
|
|
487
|
-
|
|
553
|
+
// Step 8: Handle brand-new, tag-less files
|
|
488
554
|
for (const f of newLocalNoId) {
|
|
489
555
|
const safe = readFileSafe(f.path);
|
|
490
556
|
if (!safe) {
|
|
491
|
-
console.log(
|
|
492
|
-
chalk.yellow(`Skipping unreadable file: ${f.path}`)
|
|
493
|
-
);
|
|
557
|
+
console.log(chalk.yellow(`Skipping unreadable file: ${f.path}`));
|
|
494
558
|
continue;
|
|
495
559
|
}
|
|
560
|
+
|
|
496
561
|
const { content, hash } = safe;
|
|
497
|
-
const { type, entity, contentField } = inferMeta(f.path);
|
|
562
|
+
const { type, entity, contentField } = inferMeta(path.resolve(f.path));
|
|
563
|
+
|
|
498
564
|
actionQueue.push({
|
|
499
565
|
action: "create",
|
|
566
|
+
filePath: f.path,
|
|
500
567
|
recordId: null,
|
|
501
568
|
type,
|
|
502
569
|
entity,
|
|
@@ -505,55 +572,83 @@ export const runPublish = async (options = {}) => {
|
|
|
505
572
|
});
|
|
506
573
|
}
|
|
507
574
|
|
|
508
|
-
|
|
575
|
+
// Step 9: Filter out redundant file deletions
|
|
576
|
+
const foldersBeingDeleted = actionQueue
|
|
577
|
+
.filter(a => a.action === 'delete_folder')
|
|
578
|
+
.map(a => a.folderPath);
|
|
579
|
+
|
|
580
|
+
const filteredActionQueue = actionQueue.filter(action => {
|
|
581
|
+
if (action.action === 'delete_static_asset') {
|
|
582
|
+
const fileFolder = path.join(EXPORT_ROOT, action.folder);
|
|
583
|
+
for (const deletedFolder of foldersBeingDeleted) {
|
|
584
|
+
if (fileFolder.startsWith(deletedFolder)) {
|
|
585
|
+
return false; // Skip - covered by folder deletion
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return true;
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// Step 10: Display and execute action queue
|
|
509
593
|
if (!silent) {
|
|
510
594
|
console.log(chalk.blue("\n--- Publish Action Queue ---"));
|
|
511
|
-
if (!
|
|
512
|
-
console.log(
|
|
513
|
-
chalk.green("All files are in sync — nothing to publish!")
|
|
514
|
-
);
|
|
595
|
+
if (!filteredActionQueue.length) {
|
|
596
|
+
console.log(chalk.green("All files are in sync — nothing to publish!"));
|
|
515
597
|
} else {
|
|
516
|
-
|
|
598
|
+
filteredActionQueue.forEach((a, i) => {
|
|
517
599
|
const num = chalk.green(`[${i + 1}]`);
|
|
518
600
|
const act = chalk.yellow(a.action.toUpperCase());
|
|
519
|
-
|
|
601
|
+
|
|
602
|
+
let type, displayPath;
|
|
603
|
+
if (a.folderName) {
|
|
604
|
+
type = chalk.cyan(a.folderName);
|
|
605
|
+
displayPath = a.folderPath;
|
|
606
|
+
} else if (a.names) {
|
|
607
|
+
type = chalk.cyan(a.names.join(", "));
|
|
608
|
+
displayPath = a.folder;
|
|
609
|
+
} else if (a.filePath) {
|
|
610
|
+
type = chalk.cyan(a.type || path.basename(a.filePath));
|
|
611
|
+
displayPath = a.filePath;
|
|
612
|
+
} else {
|
|
613
|
+
type = chalk.cyan(a.type || "Unknown");
|
|
614
|
+
displayPath = a.folder || "Unknown";
|
|
615
|
+
}
|
|
616
|
+
|
|
520
617
|
const idInfo = a.recordId ? ` ${chalk.magenta(a.recordId)}` : "";
|
|
521
618
|
const renameInfo = a.renamed
|
|
522
619
|
? ` → ${chalk.gray(a.oldPath)} ${chalk.white("→")} ${chalk.gray(a.filePath)}`
|
|
523
620
|
: "";
|
|
524
621
|
|
|
525
|
-
console.log(`${num} ${act} | ${a.type ? "Type" : "File"}: ${type}${idInfo}${renameInfo} (${
|
|
622
|
+
console.log(`${num} ${act} | ${a.type ? "Type" : (a.folderName ? "Folder" : "File")}: ${type}${idInfo}${renameInfo} (${displayPath})`);
|
|
526
623
|
});
|
|
527
624
|
|
|
528
625
|
console.log(chalk.blue("\n--- Publishing Changes ---"));
|
|
529
626
|
}
|
|
530
627
|
}
|
|
531
628
|
|
|
532
|
-
// Execute
|
|
533
|
-
if (
|
|
629
|
+
// Execute actions
|
|
630
|
+
if (filteredActionQueue.length > 0) {
|
|
534
631
|
if (silent) {
|
|
535
|
-
await performNetworkRequest(
|
|
632
|
+
await performNetworkRequest(filteredActionQueue);
|
|
536
633
|
} else {
|
|
537
634
|
await withSpinner(
|
|
538
635
|
"Working...",
|
|
539
636
|
async () => {
|
|
540
|
-
await performNetworkRequest(
|
|
637
|
+
await performNetworkRequest(filteredActionQueue)
|
|
541
638
|
},
|
|
542
|
-
{
|
|
543
|
-
showCompletion: false
|
|
544
|
-
}
|
|
639
|
+
{ showCompletion: false }
|
|
545
640
|
);
|
|
546
641
|
}
|
|
547
642
|
}
|
|
548
643
|
|
|
549
644
|
return {
|
|
550
|
-
actionQueue,
|
|
551
|
-
hasChanges:
|
|
645
|
+
actionQueue: filteredActionQueue,
|
|
646
|
+
hasChanges: filteredActionQueue.length > 0
|
|
552
647
|
};
|
|
553
648
|
};
|
|
554
649
|
|
|
555
650
|
/**
|
|
556
|
-
* CLI command wrapper for publish
|
|
651
|
+
* CLI command wrapper for publish.
|
|
557
652
|
*/
|
|
558
653
|
export const publish = async () => {
|
|
559
654
|
process.stdout.write("\x1Bc"); // clear console
|