@magentrix-corp/magentrix-cli 1.0.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/LICENSE +25 -0
- package/README.md +471 -0
- package/actions/autopublish.js +283 -0
- package/actions/autopublish.old.js +293 -0
- package/actions/autopublish.v2.js +447 -0
- package/actions/create.js +329 -0
- package/actions/help.js +165 -0
- package/actions/main.js +81 -0
- package/actions/publish.js +567 -0
- package/actions/pull.js +139 -0
- package/actions/setup.js +61 -0
- package/actions/status.js +17 -0
- package/bin/magentrix.js +159 -0
- package/package.json +61 -0
- package/utils/cacher.js +112 -0
- package/utils/cli/checkInstanceUrl.js +29 -0
- package/utils/cli/helpers/compare.js +281 -0
- package/utils/cli/helpers/ensureApiKey.js +57 -0
- package/utils/cli/helpers/ensureCredentials.js +60 -0
- package/utils/cli/helpers/ensureInstanceUrl.js +63 -0
- package/utils/cli/writeRecords.js +223 -0
- package/utils/compare.js +135 -0
- package/utils/compress.js +18 -0
- package/utils/config.js +451 -0
- package/utils/diff.js +49 -0
- package/utils/downloadAssets.js +75 -0
- package/utils/filetag.js +115 -0
- package/utils/hash.js +14 -0
- package/utils/magentrix/api/assets.js +145 -0
- package/utils/magentrix/api/auth.js +56 -0
- package/utils/magentrix/api/createEntity.js +61 -0
- package/utils/magentrix/api/deleteEntity.js +55 -0
- package/utils/magentrix/api/meqlQuery.js +31 -0
- package/utils/magentrix/api/retrieveEntity.js +32 -0
- package/utils/magentrix/api/updateEntity.js +66 -0
- package/utils/magentrix/fetch.js +154 -0
- package/utils/merge.js +22 -0
- package/utils/preferences.js +40 -0
- package/utils/spinner.js +43 -0
- package/utils/template.js +52 -0
- package/utils/updateFileBase.js +103 -0
- package/vars/config.js +1 -0
- package/vars/global.js +33 -0
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
import { walkFiles } from "../utils/cacher.js";
|
|
2
|
+
import { ensureValidCredentials } from "../utils/cli/helpers/ensureCredentials.js";
|
|
3
|
+
import Config from "../utils/config.js";
|
|
4
|
+
import { withSpinner } from "../utils/spinner.js";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import {
|
|
9
|
+
ENTITY_FIELD_MAP,
|
|
10
|
+
ENTITY_TYPE_MAP,
|
|
11
|
+
EXPORT_ROOT,
|
|
12
|
+
TYPE_DIR_MAP,
|
|
13
|
+
} from "../vars/global.js";
|
|
14
|
+
import {
|
|
15
|
+
getFileTag,
|
|
16
|
+
} from "../utils/filetag.js";
|
|
17
|
+
import { sha256 } from "../utils/hash.js";
|
|
18
|
+
import { createEntity } from "../utils/magentrix/api/createEntity.js";
|
|
19
|
+
import { updateEntity } from "../utils/magentrix/api/updateEntity.js";
|
|
20
|
+
import { deleteEntity } from "../utils/magentrix/api/deleteEntity.js";
|
|
21
|
+
import { setFileTag } from "../utils/filetag.js";
|
|
22
|
+
import { removeFromBase, updateBase } from "../utils/updateFileBase.js";
|
|
23
|
+
import { deleteAsset, uploadAsset } from "../utils/magentrix/api/assets.js";
|
|
24
|
+
import { decompressString } from "../utils/compress.js";
|
|
25
|
+
|
|
26
|
+
const config = new Config();
|
|
27
|
+
|
|
28
|
+
/* ---------- Helper utilities ---------- */
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Given a file path, return { type, entity, contentField } for Magentrix.
|
|
32
|
+
*/
|
|
33
|
+
const inferMeta = (filePath) => {
|
|
34
|
+
const parentFolder = path.basename(path.dirname(filePath));
|
|
35
|
+
const type = Object.keys(TYPE_DIR_MAP).find(
|
|
36
|
+
(k) => TYPE_DIR_MAP[k].directory === parentFolder
|
|
37
|
+
);
|
|
38
|
+
return {
|
|
39
|
+
type,
|
|
40
|
+
entity: ENTITY_TYPE_MAP[type],
|
|
41
|
+
contentField: ENTITY_FIELD_MAP[type],
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Safely read file content and hash; returns null on failure.
|
|
47
|
+
*/
|
|
48
|
+
const readFileSafe = (filePath) => {
|
|
49
|
+
try {
|
|
50
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
51
|
+
return { content, hash: sha256(content) };
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error(
|
|
54
|
+
chalk.red.bold("Error:") +
|
|
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
|
+
);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const formatMultilineError = (err) => {
|
|
66
|
+
// Ensure it's a string and split into lines
|
|
67
|
+
const lines = String(err).split(/\r?\n/);
|
|
68
|
+
// First line gets the bullet, rest get indentation
|
|
69
|
+
return lines
|
|
70
|
+
.map((line, i) => {
|
|
71
|
+
const prefix = i === 0 ? `${chalk.redBright(' •')} ` : ' '; // 4 spaces indent
|
|
72
|
+
return prefix + chalk.whiteBright(line);
|
|
73
|
+
})
|
|
74
|
+
.join('\n');
|
|
75
|
+
};
|
|
76
|
+
|
|
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
|
+
/**
|
|
90
|
+
* Executes all actions in the queue in parallel and handles results/errors.
|
|
91
|
+
* Updates cache after successful operations.
|
|
92
|
+
*/
|
|
93
|
+
const performNetworkRequest = async (actionQueue) => {
|
|
94
|
+
const { instanceUrl, token } = await ensureValidCredentials();
|
|
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 = [];
|
|
131
|
+
|
|
132
|
+
for (const result of results) {
|
|
133
|
+
if (result.status === "fulfilled") {
|
|
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
|
+
);
|
|
142
|
+
|
|
143
|
+
// Update cache and file tags after successful operations
|
|
144
|
+
await updateCacheAfterSuccess(action, operationResult);
|
|
145
|
+
} else {
|
|
146
|
+
errorCount++;
|
|
147
|
+
errors.push({ index: index + 1, action, error });
|
|
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)));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Summary
|
|
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
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const handleCreateStaticAssetAction = async (instanceUrl, apiKey, action) => {
|
|
186
|
+
const r = await uploadAsset(
|
|
187
|
+
instanceUrl,
|
|
188
|
+
apiKey,
|
|
189
|
+
`/${action.folder}`,
|
|
190
|
+
[action.filePath]
|
|
191
|
+
).catch(err => ({ error: err.message }));
|
|
192
|
+
if (r?.error) throw new Error(r.error);
|
|
193
|
+
return r;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const handleDeleteStaticAssetAction = async (instanceUrl, apiKey, action) => {
|
|
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
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Handle create action
|
|
204
|
+
*/
|
|
205
|
+
const handleCreateAction = async (instanceUrl, apiKey, action) => {
|
|
206
|
+
const data = {
|
|
207
|
+
Name: path.basename(action.filePath, path.extname(action.filePath)),
|
|
208
|
+
Type: action.type,
|
|
209
|
+
...action.fields
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const result = await createEntity(instanceUrl, apiKey, action.entity, data);
|
|
213
|
+
return { recordId: result.recordId || result.id };
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Handle update action
|
|
218
|
+
*/
|
|
219
|
+
const handleUpdateAction = async (instanceUrl, apiKey, action) => {
|
|
220
|
+
const data = {
|
|
221
|
+
...action.fields
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// If renamed, update the Name field
|
|
225
|
+
if (action.renamed) {
|
|
226
|
+
data.Name = path.basename(action.filePath, path.extname(action.filePath));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const result = await updateEntity(instanceUrl, apiKey, action.entity, action.recordId, data);
|
|
230
|
+
return { recordId: action.recordId };
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Handle delete action
|
|
235
|
+
*/
|
|
236
|
+
const handleDeleteAction = async (instanceUrl, apiKey, action) => {
|
|
237
|
+
const r = await deleteEntity(instanceUrl, apiKey, action.entity, action.recordId);
|
|
238
|
+
return r;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Update cache and file tags after successful operations
|
|
243
|
+
*/
|
|
244
|
+
const updateCacheAfterSuccess = async (action, operationResult) => {
|
|
245
|
+
try {
|
|
246
|
+
switch (action.action) {
|
|
247
|
+
case "create":
|
|
248
|
+
// Tag the file and update base with the content that was actually published
|
|
249
|
+
const createSnapshot = action.fields && Object.values(action.fields)[0]
|
|
250
|
+
? { content: Object.values(action.fields)[0], hash: action.contentHash }
|
|
251
|
+
: null;
|
|
252
|
+
await setFileTag(action.filePath, operationResult.recordId);
|
|
253
|
+
updateBase(
|
|
254
|
+
action.filePath,
|
|
255
|
+
{ Id: operationResult.recordId, Type: action.type },
|
|
256
|
+
'',
|
|
257
|
+
createSnapshot
|
|
258
|
+
);
|
|
259
|
+
break;
|
|
260
|
+
|
|
261
|
+
case "update":
|
|
262
|
+
// Pass the content snapshot that was actually published to prevent race conditions
|
|
263
|
+
const updateSnapshot = action.fields && Object.values(action.fields)[0]
|
|
264
|
+
? { content: Object.values(action.fields)[0], hash: sha256(Object.values(action.fields)[0]) }
|
|
265
|
+
: null;
|
|
266
|
+
updateBase(
|
|
267
|
+
action.filePath,
|
|
268
|
+
{ Id: action.recordId, Type: Object.keys(ENTITY_TYPE_MAP).find(key => ENTITY_TYPE_MAP[key] === action.entity) },
|
|
269
|
+
'',
|
|
270
|
+
updateSnapshot
|
|
271
|
+
);
|
|
272
|
+
break;
|
|
273
|
+
|
|
274
|
+
case "delete":
|
|
275
|
+
removeFromBase(action.recordId);
|
|
276
|
+
break;
|
|
277
|
+
|
|
278
|
+
case "delete_static_asset":
|
|
279
|
+
for (const name of action.names) {
|
|
280
|
+
removeFromBase(`${EXPORT_ROOT}/${action.folder}/${name}`);
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
|
|
284
|
+
case "create_static_asset":
|
|
285
|
+
updateBase(action.filePath, { Id: action.filePath, Type: "File" });
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.warn(chalk.yellow(`Warning: Failed to update cache for ${action.action}: ${error.message}`));
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/* ---------- Main publish routine ---------- */
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Core publish logic - can be called from autopublish or directly
|
|
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}>}
|
|
302
|
+
*/
|
|
303
|
+
export const runPublish = async (options = {}) => {
|
|
304
|
+
const { silent = false, skipAuth = false, credentials = null } = options;
|
|
305
|
+
|
|
306
|
+
/* 1 — Authenticate (unless skipped) */
|
|
307
|
+
let authCreds;
|
|
308
|
+
if (skipAuth && credentials) {
|
|
309
|
+
authCreds = credentials;
|
|
310
|
+
} else {
|
|
311
|
+
authCreds = await ensureValidCredentials().catch((err) => {
|
|
312
|
+
if (!silent) {
|
|
313
|
+
console.error(
|
|
314
|
+
chalk.red.bold("Authentication failed:"),
|
|
315
|
+
chalk.white(err.message)
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
throw err;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* 2 — Load previous cache */
|
|
323
|
+
const hits = await config.searchObject({}, { filename: "base.json", global: false });
|
|
324
|
+
const cachedResults = hits?.[0]?.value || {};
|
|
325
|
+
|
|
326
|
+
if (!Object.keys(cachedResults).length) {
|
|
327
|
+
if (!silent) {
|
|
328
|
+
console.log(chalk.red.bold("No file cache found!"));
|
|
329
|
+
console.log(
|
|
330
|
+
`Run ${chalk.cyan("magentrix pull")} to initialize your workspace.`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
throw new Error("No file cache found");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const cachedFiles = Object.values(cachedResults).map((c) => ({
|
|
337
|
+
...c,
|
|
338
|
+
tag: c.recordId,
|
|
339
|
+
}));
|
|
340
|
+
|
|
341
|
+
/* 3 — Scan current local workspace */
|
|
342
|
+
const localPaths = await walkFiles(EXPORT_ROOT, { ignore: [path.join(EXPORT_ROOT, 'Contents')] });
|
|
343
|
+
const localFiles = await Promise.all(
|
|
344
|
+
localPaths.map(async (p) => {
|
|
345
|
+
try {
|
|
346
|
+
const tag = await getFileTag(p);
|
|
347
|
+
return { tag, path: p };
|
|
348
|
+
} catch {
|
|
349
|
+
// console.log(
|
|
350
|
+
// chalk.yellow(
|
|
351
|
+
// `Warning: Could not retrieve tag for ${p}. Treating as new file.`
|
|
352
|
+
// )
|
|
353
|
+
// );
|
|
354
|
+
return { tag: null, path: p };
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
/* 4 — Fast lookup maps */
|
|
360
|
+
const cacheById = Object.fromEntries(
|
|
361
|
+
cachedFiles.map((c) => [c.tag, c])
|
|
362
|
+
);
|
|
363
|
+
const localById = Object.fromEntries(
|
|
364
|
+
localFiles.filter((f) => f.tag).map((f) => [f.tag, f])
|
|
365
|
+
);
|
|
366
|
+
const newLocalNoId = localFiles.filter((f) => !f.tag);
|
|
367
|
+
|
|
368
|
+
/* 5 — Determine action per recordId */
|
|
369
|
+
const allIds = new Set([
|
|
370
|
+
...Object.keys(cacheById),
|
|
371
|
+
...Object.keys(localById),
|
|
372
|
+
]);
|
|
373
|
+
const actionQueue = [];
|
|
374
|
+
|
|
375
|
+
// HANDLE ASSET CREATION SEPERATELY
|
|
376
|
+
const assetPaths = await walkFiles(path.join(EXPORT_ROOT, 'Contents/Assets'));
|
|
377
|
+
for (const assetPath of assetPaths) {
|
|
378
|
+
// The file is already being handled in the next part
|
|
379
|
+
if (cachedFiles.find(cr => cr.filePath.toLowerCase() === assetPath.toLowerCase())) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const fileFolderPath = getFolderFromPath(assetPath);
|
|
384
|
+
|
|
385
|
+
actionQueue.push({
|
|
386
|
+
action: "create_static_asset",
|
|
387
|
+
folder: fileFolderPath,
|
|
388
|
+
filePath: assetPath
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
for (const id of allIds) {
|
|
393
|
+
try {
|
|
394
|
+
const cacheFile = cacheById[id];
|
|
395
|
+
const curFile = localById[id];
|
|
396
|
+
|
|
397
|
+
// This is an asset so it needs to be treated differently
|
|
398
|
+
// Assets only have CREATE & DELETE & UPDATE actions no rename, if a files content is changed just reupload it essentially.
|
|
399
|
+
// We may have to explore the option of listing all the files from the server first and validating against those instead of the cache
|
|
400
|
+
if (cacheFile.type === 'File') {
|
|
401
|
+
const localAssetExists = fs.existsSync(cacheFile.filePath);
|
|
402
|
+
|
|
403
|
+
// DELETE
|
|
404
|
+
if (!localAssetExists) {
|
|
405
|
+
actionQueue.push({
|
|
406
|
+
action: 'delete_static_asset',
|
|
407
|
+
folder: getFolderFromPath(cacheFile.filePath),
|
|
408
|
+
names: [path.basename(cacheFile.filePath)]
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// UPDATE
|
|
415
|
+
const contentHash = sha256(fs.readFileSync(cacheFile.filePath, 'utf-8'));
|
|
416
|
+
if (contentHash !== cacheFile.contentHash) {
|
|
417
|
+
// This can be handled as a file creation since the Magentrix Upload API will just overwrite the file
|
|
418
|
+
actionQueue.push({
|
|
419
|
+
aliasAction: 'update_static_asset',
|
|
420
|
+
action: "create_static_asset",
|
|
421
|
+
folder: getFolderFromPath(cacheFile.filePath),
|
|
422
|
+
filePath: cacheFile.filePath
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/* DELETE */
|
|
430
|
+
if (cacheFile && !curFile) {
|
|
431
|
+
const { type, entity } = inferMeta(cacheFile.lastKnownPath);
|
|
432
|
+
actionQueue.push({
|
|
433
|
+
action: "delete",
|
|
434
|
+
recordId: id,
|
|
435
|
+
filePath: cacheFile.lastKnownPath,
|
|
436
|
+
type,
|
|
437
|
+
entity,
|
|
438
|
+
});
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/* CREATE */
|
|
443
|
+
if (!cacheFile && curFile) {
|
|
444
|
+
const safe = readFileSafe(curFile.path);
|
|
445
|
+
if (!safe) continue;
|
|
446
|
+
const { content, hash } = safe;
|
|
447
|
+
const { type, entity, contentField } = inferMeta(curFile.path);
|
|
448
|
+
actionQueue.push({
|
|
449
|
+
action: "create",
|
|
450
|
+
filePath: curFile.path,
|
|
451
|
+
recordId: id || null,
|
|
452
|
+
type,
|
|
453
|
+
entity,
|
|
454
|
+
fields: { [contentField]: content },
|
|
455
|
+
contentHash: hash,
|
|
456
|
+
});
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/* UPDATE */
|
|
461
|
+
if (cacheFile && curFile) {
|
|
462
|
+
const safe = readFileSafe(curFile.path);
|
|
463
|
+
if (!safe) continue;
|
|
464
|
+
const { content, hash } = safe;
|
|
465
|
+
|
|
466
|
+
const renamed = cacheFile.lastKnownPath !== path.resolve(curFile.path);
|
|
467
|
+
const contentChanged = hash !== cacheFile.contentHash;
|
|
468
|
+
if (renamed || contentChanged) {
|
|
469
|
+
const { type, entity, contentField } = inferMeta(curFile.path);
|
|
470
|
+
actionQueue.push({
|
|
471
|
+
action: "update",
|
|
472
|
+
recordId: id,
|
|
473
|
+
type,
|
|
474
|
+
entity,
|
|
475
|
+
fields: { [contentField]: content },
|
|
476
|
+
renamed,
|
|
477
|
+
oldPath: cacheFile.lastKnownPath,
|
|
478
|
+
filePath: curFile.path,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
} catch (err) {
|
|
483
|
+
console.log(err)
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/* 6 — Handle brand-new, tag-less files */
|
|
488
|
+
for (const f of newLocalNoId) {
|
|
489
|
+
const safe = readFileSafe(f.path);
|
|
490
|
+
if (!safe) {
|
|
491
|
+
console.log(
|
|
492
|
+
chalk.yellow(`Skipping unreadable file: ${f.path}`)
|
|
493
|
+
);
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const { content, hash } = safe;
|
|
497
|
+
const { type, entity, contentField } = inferMeta(f.path);
|
|
498
|
+
actionQueue.push({
|
|
499
|
+
action: "create",
|
|
500
|
+
recordId: null,
|
|
501
|
+
type,
|
|
502
|
+
entity,
|
|
503
|
+
fields: { [contentField]: content },
|
|
504
|
+
contentHash: hash,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/* 7 — Summary and execution */
|
|
509
|
+
if (!silent) {
|
|
510
|
+
console.log(chalk.blue("\n--- Publish Action Queue ---"));
|
|
511
|
+
if (!actionQueue.length) {
|
|
512
|
+
console.log(
|
|
513
|
+
chalk.green("All files are in sync — nothing to publish!")
|
|
514
|
+
);
|
|
515
|
+
} else {
|
|
516
|
+
actionQueue.forEach((a, i) => {
|
|
517
|
+
const num = chalk.green(`[${i + 1}]`);
|
|
518
|
+
const act = chalk.yellow(a.action.toUpperCase());
|
|
519
|
+
const type = chalk.cyan(a.type || a?.names?.join(", ") || path.basename(a.filePath));
|
|
520
|
+
const idInfo = a.recordId ? ` ${chalk.magenta(a.recordId)}` : "";
|
|
521
|
+
const renameInfo = a.renamed
|
|
522
|
+
? ` → ${chalk.gray(a.oldPath)} ${chalk.white("→")} ${chalk.gray(a.filePath)}`
|
|
523
|
+
: "";
|
|
524
|
+
|
|
525
|
+
console.log(`${num} ${act} | ${a.type ? "Type" : "File"}: ${type}${idInfo}${renameInfo} (${a.filePath || a.folder})`);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
console.log(chalk.blue("\n--- Publishing Changes ---"));
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Execute the action queue if there are changes
|
|
533
|
+
if (actionQueue.length > 0) {
|
|
534
|
+
if (silent) {
|
|
535
|
+
await performNetworkRequest(actionQueue);
|
|
536
|
+
} else {
|
|
537
|
+
await withSpinner(
|
|
538
|
+
"Working...",
|
|
539
|
+
async () => {
|
|
540
|
+
await performNetworkRequest(actionQueue)
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
showCompletion: false
|
|
544
|
+
}
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
actionQueue,
|
|
551
|
+
hasChanges: actionQueue.length > 0
|
|
552
|
+
};
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* CLI command wrapper for publish
|
|
557
|
+
*/
|
|
558
|
+
export const publish = async () => {
|
|
559
|
+
process.stdout.write("\x1Bc"); // clear console
|
|
560
|
+
|
|
561
|
+
try {
|
|
562
|
+
await runPublish({ silent: false });
|
|
563
|
+
} catch (err) {
|
|
564
|
+
console.error(chalk.red.bold("Publish failed:"), err.message);
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
};
|
package/actions/pull.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { ensureValidCredentials } from "../utils/cli/helpers/ensureCredentials.js";
|
|
2
|
+
import Config from "../utils/config.js";
|
|
3
|
+
import { meqlQuery } from "../utils/magentrix/api/meqlQuery.js";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import { withSpinner } from "../utils/spinner.js";
|
|
6
|
+
import { EXPORT_ROOT, TYPE_DIR_MAP } from "../vars/global.js";
|
|
7
|
+
import { mapRecordToFile, writeRecords } from "../utils/cli/writeRecords.js";
|
|
8
|
+
import { updateBase } from "../utils/updateFileBase.js";
|
|
9
|
+
import { compareAllFilesAndLogStatus, promptConflictResolution, showCurrentConflicts } from "../utils/cli/helpers/compare.js";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { compareLocalAndRemote } from "../utils/compare.js";
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import { getFileTag } from "../utils/filetag.js";
|
|
14
|
+
import { downloadAssetsZip, listAssets } from "../utils/magentrix/api/assets.js";
|
|
15
|
+
import { downloadAssets, walkAssets } from "../utils/downloadAssets.js";
|
|
16
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
17
|
+
|
|
18
|
+
const config = new Config();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Pulls all ActiveClass and ActivePage records from Magentrix,
|
|
22
|
+
* saving them to categorized local directories with appropriate extensions.
|
|
23
|
+
*
|
|
24
|
+
* Output Structure:
|
|
25
|
+
* /test/
|
|
26
|
+
* Controllers/
|
|
27
|
+
* Triggers/
|
|
28
|
+
* Classes/
|
|
29
|
+
* Pages/
|
|
30
|
+
* <Name>.<ext>
|
|
31
|
+
*
|
|
32
|
+
* @async
|
|
33
|
+
* @function pull
|
|
34
|
+
* @returns {Promise<void>}
|
|
35
|
+
*/
|
|
36
|
+
export const pull = async () => {
|
|
37
|
+
// Step 1: Authenticate and retrieve instance URL and token
|
|
38
|
+
const { instanceUrl, token } = await withSpinner('Authenticating...', async () => {
|
|
39
|
+
return await ensureValidCredentials();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Clear the terminal
|
|
43
|
+
process.stdout.write('\x1Bc');
|
|
44
|
+
|
|
45
|
+
// Step 2: Prepare queries for both ActiveClass and ActivePage
|
|
46
|
+
const queries = [
|
|
47
|
+
{
|
|
48
|
+
name: "ActiveClass",
|
|
49
|
+
query: "SELECT Id,Body,Name,CreatedOn,Description,ModifiedOn,Type FROM ActiveClass",
|
|
50
|
+
contentField: "Body",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "ActivePage",
|
|
54
|
+
query: "SELECT Id,Content,Name,CreatedOn,Description,ModifiedOn,Type FROM ActivePage",
|
|
55
|
+
contentField: "Content",
|
|
56
|
+
}
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// Step 3: Download records in parallel with spinner
|
|
60
|
+
const [activeClassResult, activePageResult, assets] = await withSpinner("Downloading files...", async () => {
|
|
61
|
+
const meqlResults = await Promise.all(
|
|
62
|
+
queries.map(q => meqlQuery(instanceUrl, token.value, q.query))
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const assetTree = await downloadAssets(instanceUrl, token.value);
|
|
66
|
+
|
|
67
|
+
return [
|
|
68
|
+
...meqlResults,
|
|
69
|
+
assetTree
|
|
70
|
+
]
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Update assets base
|
|
74
|
+
const processAssets = (records) => {
|
|
75
|
+
for (const record of records) {
|
|
76
|
+
if (record?.Type === 'Folder') {
|
|
77
|
+
if (record?.Children?.length === 0) continue;
|
|
78
|
+
processAssets(record.Children);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
updateBase(
|
|
83
|
+
path.join(EXPORT_ROOT, record?.Path),
|
|
84
|
+
{
|
|
85
|
+
...record,
|
|
86
|
+
Id: path.join(EXPORT_ROOT, record?.Path)
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
processAssets(assets.tree);
|
|
93
|
+
|
|
94
|
+
// Remove (clean) the export root directory
|
|
95
|
+
// fs.rmSync(EXPORT_ROOT, { recursive: true, force: true });
|
|
96
|
+
|
|
97
|
+
// Check for conflicts and have user select conflict resolution
|
|
98
|
+
const activeClassRecords = (activeClassResult.Records || []).map(record => {
|
|
99
|
+
record.Content = record.Body;
|
|
100
|
+
delete record.Body;
|
|
101
|
+
return record;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const activePageRecords = (activePageResult.Records || []);
|
|
105
|
+
const allRecords = [...activeClassRecords, ...activePageRecords].map(mapRecordToFile);
|
|
106
|
+
|
|
107
|
+
const issues = [];
|
|
108
|
+
for (const record of allRecords) {
|
|
109
|
+
if (record?.error) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const status = compareLocalAndRemote(
|
|
114
|
+
path.join(EXPORT_ROOT, record.relativePath),
|
|
115
|
+
{ ...record, content: record.Content }
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Missing files will just be written
|
|
119
|
+
if (!['in_sync', 'missing'].includes(status.status)) {
|
|
120
|
+
issues.push({ relativePath: record.relativePath, status: status.status });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let resolutionMethod = 'skip';
|
|
125
|
+
|
|
126
|
+
if (issues.length > 0) {
|
|
127
|
+
resolutionMethod = await promptConflictResolution(issues);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Step 5: Write all ActiveClass and ActivePage records
|
|
131
|
+
await writeRecords(allRecords, resolutionMethod);
|
|
132
|
+
|
|
133
|
+
// Step 7: Success message
|
|
134
|
+
console.log(`\n✅ Successfully pulled:`);
|
|
135
|
+
console.log(` • ${activeClassResult.Records.length} ActiveClass records`);
|
|
136
|
+
console.log(` • ${activePageResult.Records.length} ActivePage records`);
|
|
137
|
+
console.log(`📁 Saved to: ./${EXPORT_ROOT}/ (organized into Controllers, Triggers, Classes, Pages)`);
|
|
138
|
+
// console.log(`🔍 Tip: Run 'magentrix status' to see local vs server differences.`);
|
|
139
|
+
};
|