@magentrix-corp/magentrix-cli 1.1.4 → 1.2.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 +156 -8
- package/actions/config.js +182 -0
- package/actions/create.js +30 -9
- package/actions/publish.js +434 -76
- package/actions/pull.js +361 -70
- package/actions/setup.js +2 -2
- package/actions/update.js +248 -0
- package/bin/magentrix.js +13 -1
- package/package.json +1 -1
- package/utils/assetPaths.js +24 -4
- package/utils/cacher.js +122 -53
- package/utils/cli/helpers/ensureInstanceUrl.js +3 -3
- package/utils/cli/writeRecords.js +34 -6
- package/utils/diagnostics/testPublishLogic.js +96 -0
- package/utils/downloadAssets.js +230 -19
- package/utils/logger.js +283 -0
- package/utils/magentrix/api/assets.js +65 -10
- package/utils/progress.js +383 -0
- package/utils/updateFileBase.js +2 -1
package/actions/publish.js
CHANGED
|
@@ -2,6 +2,7 @@ import { walkFiles } from "../utils/cacher.js";
|
|
|
2
2
|
import { ensureValidCredentials } from "../utils/cli/helpers/ensureCredentials.js";
|
|
3
3
|
import Config from "../utils/config.js";
|
|
4
4
|
import { withSpinner } from "../utils/spinner.js";
|
|
5
|
+
import { ProgressTracker } from "../utils/progress.js";
|
|
5
6
|
import fs from "fs";
|
|
6
7
|
import path from "path";
|
|
7
8
|
import chalk from "chalk";
|
|
@@ -22,6 +23,17 @@ import { toApiPath, toApiFolderPath } from "../utils/assetPaths.js";
|
|
|
22
23
|
|
|
23
24
|
const config = new Config();
|
|
24
25
|
|
|
26
|
+
/* ==================== CONFIGURATION ==================== */
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Set to true to process all operations sequentially (one at a time).
|
|
30
|
+
* Set to false to process operations in parallel with intelligent grouping.
|
|
31
|
+
*
|
|
32
|
+
* Sequential mode is slower but avoids any potential race conditions or
|
|
33
|
+
* server-side rate limiting issues.
|
|
34
|
+
*/
|
|
35
|
+
const USE_SEQUENTIAL_PROCESSING = true;
|
|
36
|
+
|
|
25
37
|
/* ==================== UTILITY FUNCTIONS ==================== */
|
|
26
38
|
|
|
27
39
|
/**
|
|
@@ -239,13 +251,15 @@ const handleDeleteFolderAction = async (instanceUrl, apiKey, action) => {
|
|
|
239
251
|
const cachedResults = hits?.[0]?.value || {};
|
|
240
252
|
|
|
241
253
|
for (const [recordId, cachedEntry] of Object.entries(cachedResults)) {
|
|
242
|
-
|
|
254
|
+
// Check all possible path fields
|
|
255
|
+
const entryPath = cachedEntry.lastKnownActualPath || cachedEntry.filePath || cachedEntry.lastKnownPath;
|
|
243
256
|
if (entryPath && typeof entryPath === 'string') {
|
|
244
|
-
const normalizedEntryPath = path.normalize(entryPath);
|
|
245
|
-
const normalizedFolderPath = path.normalize(action.folderPath);
|
|
257
|
+
const normalizedEntryPath = path.normalize(path.resolve(entryPath)).toLowerCase();
|
|
258
|
+
const normalizedFolderPath = path.normalize(path.resolve(action.folderPath)).toLowerCase();
|
|
246
259
|
|
|
247
260
|
// Check if this entry is inside the deleted folder
|
|
248
|
-
if (normalizedEntryPath.startsWith(normalizedFolderPath + path.sep)
|
|
261
|
+
if (normalizedEntryPath.startsWith(normalizedFolderPath + path.sep) ||
|
|
262
|
+
normalizedEntryPath === normalizedFolderPath) {
|
|
249
263
|
removeFromBase(recordId);
|
|
250
264
|
}
|
|
251
265
|
}
|
|
@@ -318,7 +332,7 @@ const updateCacheAfterSuccess = async (action, operationResult) => {
|
|
|
318
332
|
case "delete_folder": {
|
|
319
333
|
// Skip if already cleaned from cache during 404 handling
|
|
320
334
|
if (!operationResult?.cleanedFromCache) {
|
|
321
|
-
// Remove the folder itself from base
|
|
335
|
+
// Remove the folder itself from base using recordId (which is the folderPath)
|
|
322
336
|
removeFromBase(action.folderPath);
|
|
323
337
|
|
|
324
338
|
// Also remove all files and subfolders inside this folder from base
|
|
@@ -326,13 +340,15 @@ const updateCacheAfterSuccess = async (action, operationResult) => {
|
|
|
326
340
|
const cachedResults = hits?.[0]?.value || {};
|
|
327
341
|
|
|
328
342
|
for (const [recordId, cachedEntry] of Object.entries(cachedResults)) {
|
|
329
|
-
|
|
343
|
+
// Check all possible path fields
|
|
344
|
+
const entryPath = cachedEntry.lastKnownActualPath || cachedEntry.filePath || cachedEntry.lastKnownPath;
|
|
330
345
|
if (entryPath && typeof entryPath === 'string') {
|
|
331
|
-
const normalizedEntryPath = path.normalize(entryPath);
|
|
332
|
-
const normalizedFolderPath = path.normalize(action.folderPath);
|
|
346
|
+
const normalizedEntryPath = path.normalize(path.resolve(entryPath)).toLowerCase();
|
|
347
|
+
const normalizedFolderPath = path.normalize(path.resolve(action.folderPath)).toLowerCase();
|
|
333
348
|
|
|
334
349
|
// Check if this entry is inside the deleted folder
|
|
335
|
-
if (normalizedEntryPath.startsWith(normalizedFolderPath + path.sep)
|
|
350
|
+
if (normalizedEntryPath.startsWith(normalizedFolderPath + path.sep) ||
|
|
351
|
+
normalizedEntryPath === normalizedFolderPath) {
|
|
336
352
|
removeFromBase(recordId);
|
|
337
353
|
}
|
|
338
354
|
}
|
|
@@ -349,54 +365,250 @@ const updateCacheAfterSuccess = async (action, operationResult) => {
|
|
|
349
365
|
/* ==================== NETWORK REQUEST HANDLER ==================== */
|
|
350
366
|
|
|
351
367
|
/**
|
|
352
|
-
*
|
|
368
|
+
* Groups actions by resource ID to detect conflicts and sequence operations.
|
|
369
|
+
* @returns {Object} { byResource: Map<recordId, actions[]>, assets: actions[] }
|
|
353
370
|
*/
|
|
354
|
-
const
|
|
355
|
-
const
|
|
371
|
+
const groupActionsByResource = (actionQueue) => {
|
|
372
|
+
const byResource = new Map(); // recordId -> actions[]
|
|
373
|
+
const assets = []; // Asset operations (can run in parallel)
|
|
356
374
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
375
|
+
for (let i = 0; i < actionQueue.length; i++) {
|
|
376
|
+
const action = { ...actionQueue[i], originalIndex: i };
|
|
377
|
+
|
|
378
|
+
// Asset operations don't need sequencing
|
|
379
|
+
if (['create_static_asset', 'delete_static_asset', 'create_folder', 'delete_folder'].includes(action.action)) {
|
|
380
|
+
assets.push(action);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Code entity operations - group by recordId
|
|
385
|
+
const resourceId = action.recordId;
|
|
386
|
+
if (!resourceId) {
|
|
387
|
+
// Create actions without recordId yet - treat as unique resource
|
|
388
|
+
assets.push(action);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (!byResource.has(resourceId)) {
|
|
393
|
+
byResource.set(resourceId, []);
|
|
394
|
+
}
|
|
395
|
+
byResource.get(resourceId).push(action);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return { byResource, assets };
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Resolves conflicts for a single resource and returns actions to execute in sequence.
|
|
403
|
+
* Rules:
|
|
404
|
+
* - If DELETE exists, drop all other actions (delete wins)
|
|
405
|
+
* - If multiple UPDATEs exist, keep them in order (will execute sequentially)
|
|
406
|
+
*/
|
|
407
|
+
const resolveResourceConflicts = (actions) => {
|
|
408
|
+
// Check if there's a delete action
|
|
409
|
+
const hasDelete = actions.some(a => a.action === 'delete');
|
|
410
|
+
|
|
411
|
+
if (hasDelete) {
|
|
412
|
+
// Delete wins - drop all other actions and only keep the delete
|
|
413
|
+
return actions.filter(a => a.action === 'delete');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// No delete - return all actions (they'll execute sequentially)
|
|
417
|
+
return actions;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Executes actions for a single resource sequentially.
|
|
422
|
+
*/
|
|
423
|
+
const executeResourceActions = async (instanceUrl, token, actions) => {
|
|
424
|
+
const results = [];
|
|
425
|
+
|
|
426
|
+
for (const action of actions) {
|
|
427
|
+
try {
|
|
428
|
+
let result;
|
|
429
|
+
switch (action.action) {
|
|
430
|
+
case "create":
|
|
431
|
+
result = await handleCreateAction(instanceUrl, token, action);
|
|
432
|
+
break;
|
|
433
|
+
case "update":
|
|
434
|
+
result = await handleUpdateAction(instanceUrl, token, action);
|
|
435
|
+
break;
|
|
436
|
+
case "delete":
|
|
437
|
+
result = await handleDeleteAction(instanceUrl, token, action);
|
|
438
|
+
break;
|
|
439
|
+
default:
|
|
440
|
+
throw new Error(`Unknown action: ${action.action}`);
|
|
389
441
|
}
|
|
390
|
-
|
|
442
|
+
results.push({ index: action.originalIndex, action, result, success: true });
|
|
443
|
+
} catch (error) {
|
|
444
|
+
results.push({ index: action.originalIndex, action, error: error.message, success: false });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return results;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Executes a single action and returns the result.
|
|
453
|
+
*/
|
|
454
|
+
const executeAction = async (instanceUrl, token, action) => {
|
|
455
|
+
let result;
|
|
456
|
+
switch (action.action) {
|
|
457
|
+
case "create":
|
|
458
|
+
result = await handleCreateAction(instanceUrl, token, action);
|
|
459
|
+
break;
|
|
460
|
+
case "update":
|
|
461
|
+
result = await handleUpdateAction(instanceUrl, token, action);
|
|
462
|
+
break;
|
|
463
|
+
case "delete":
|
|
464
|
+
result = await handleDeleteAction(instanceUrl, token, action);
|
|
465
|
+
break;
|
|
466
|
+
case "create_static_asset":
|
|
467
|
+
result = await handleCreateStaticAssetAction(instanceUrl, token, action);
|
|
468
|
+
break;
|
|
469
|
+
case "delete_static_asset":
|
|
470
|
+
result = await handleDeleteStaticAssetAction(instanceUrl, token, action);
|
|
471
|
+
break;
|
|
472
|
+
case "create_folder":
|
|
473
|
+
result = await handleCreateFolderAction(instanceUrl, token, action);
|
|
474
|
+
break;
|
|
475
|
+
case "delete_folder":
|
|
476
|
+
result = await handleDeleteFolderAction(instanceUrl, token, action);
|
|
477
|
+
break;
|
|
478
|
+
default:
|
|
479
|
+
throw new Error(`Unknown action: ${action.action}`);
|
|
480
|
+
}
|
|
481
|
+
return result;
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Executes all actions sequentially (one at a time) with progress messages.
|
|
486
|
+
*/
|
|
487
|
+
const performNetworkRequestSequential = async (actionQueue) => {
|
|
488
|
+
const { instanceUrl, token } = await ensureValidCredentials();
|
|
489
|
+
|
|
490
|
+
console.log(chalk.blue(`\n🔄 Sequential processing mode (${actionQueue.length} operations)\n`));
|
|
491
|
+
|
|
492
|
+
const results = [];
|
|
493
|
+
let successCount = 0;
|
|
494
|
+
let errorCount = 0;
|
|
495
|
+
|
|
496
|
+
for (let i = 0; i < actionQueue.length; i++) {
|
|
497
|
+
const action = { ...actionQueue[i], originalIndex: i };
|
|
498
|
+
const displayName = getActionDisplayName(action);
|
|
499
|
+
|
|
500
|
+
// Show progress message
|
|
501
|
+
console.log(chalk.gray(`[${i + 1}/${actionQueue.length}] Processing ${action.action.toUpperCase()} ${displayName}...`));
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
const result = await executeAction(instanceUrl, token.value, action);
|
|
505
|
+
results.push({ index: i, action, result, success: true });
|
|
506
|
+
|
|
507
|
+
successCount++;
|
|
508
|
+
console.log(
|
|
509
|
+
chalk.green(`✓ [${i + 1}]`) +
|
|
510
|
+
` ${chalk.yellow(action.action.toUpperCase())} ${chalk.cyan(displayName)} ` +
|
|
511
|
+
(result?.recordId ? chalk.magenta(result.recordId) : "")
|
|
512
|
+
);
|
|
513
|
+
await updateCacheAfterSuccess(action, result);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
results.push({ index: i, action, error: error.message, success: false });
|
|
516
|
+
|
|
517
|
+
errorCount++;
|
|
518
|
+
console.log();
|
|
519
|
+
console.log(chalk.bgRed.bold.white(' ✖ Operation Failed '));
|
|
520
|
+
console.log(chalk.redBright('─'.repeat(48)));
|
|
521
|
+
console.log(chalk.red.bold(`[${i + 1}] ${action.action.toUpperCase()} ${displayName} (${action.filePath || action.folderPath || action.folder}):`));
|
|
522
|
+
console.log(formatMultilineError(error.message));
|
|
523
|
+
console.log(chalk.redBright('─'.repeat(48)));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
console.log(); // Add spacing between operations
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Summary
|
|
530
|
+
console.log(chalk.blue("--- Publish Summary ---"));
|
|
531
|
+
console.log(chalk.green(`✓ Successful: ${successCount}`));
|
|
532
|
+
if (errorCount > 0) {
|
|
533
|
+
console.log(chalk.red(`✗ Failed: ${errorCount}`));
|
|
534
|
+
} else {
|
|
535
|
+
console.log(chalk.green("All operations completed successfully! 🎉"));
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Executes all actions in the queue with proper sequencing and conflict resolution (parallel mode).
|
|
541
|
+
*/
|
|
542
|
+
const performNetworkRequestParallel = async (actionQueue) => {
|
|
543
|
+
const { instanceUrl, token } = await ensureValidCredentials();
|
|
544
|
+
|
|
545
|
+
// Group actions by resource
|
|
546
|
+
const { byResource, assets } = groupActionsByResource(actionQueue);
|
|
547
|
+
|
|
548
|
+
// Resolve conflicts for each resource
|
|
549
|
+
const sequencedCodeActions = [];
|
|
550
|
+
const droppedActions = [];
|
|
551
|
+
|
|
552
|
+
for (const [resourceId, actions] of byResource.entries()) {
|
|
553
|
+
const resolved = resolveResourceConflicts(actions);
|
|
554
|
+
sequencedCodeActions.push(resolved);
|
|
555
|
+
|
|
556
|
+
// Track dropped actions
|
|
557
|
+
const droppedCount = actions.length - resolved.length;
|
|
558
|
+
if (droppedCount > 0) {
|
|
559
|
+
const dropped = actions.filter(a => !resolved.includes(a));
|
|
560
|
+
droppedActions.push(...dropped);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Log dropped actions
|
|
565
|
+
if (droppedActions.length > 0) {
|
|
566
|
+
console.log(chalk.yellow(`\n⚠️ Dropped ${droppedActions.length} redundant operation(s) due to delete:`));
|
|
567
|
+
droppedActions.forEach(action => {
|
|
568
|
+
console.log(chalk.gray(` • ${action.action.toUpperCase()} on ${getActionDisplayName(action)} (superseded by DELETE)`));
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Execute asset operations in parallel (they don't conflict)
|
|
573
|
+
const assetPromises = assets.map(async (action) => {
|
|
574
|
+
try {
|
|
575
|
+
const result = await executeAction(instanceUrl, token.value, action);
|
|
576
|
+
return { index: action.originalIndex, action, result, success: true };
|
|
577
|
+
} catch (error) {
|
|
578
|
+
return { index: action.originalIndex, action, error: error.message, success: false };
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// Execute code entity operations sequentially per resource (but resources in parallel)
|
|
583
|
+
const codePromises = sequencedCodeActions.map(actions =>
|
|
584
|
+
executeResourceActions(instanceUrl, token.value, actions)
|
|
391
585
|
);
|
|
392
586
|
|
|
587
|
+
// Wait for all operations
|
|
588
|
+
const assetResults = await Promise.allSettled(assetPromises);
|
|
589
|
+
const codeResults = await Promise.allSettled(codePromises);
|
|
590
|
+
|
|
591
|
+
// Flatten code results
|
|
592
|
+
const allCodeResults = codeResults
|
|
593
|
+
.filter(r => r.status === 'fulfilled')
|
|
594
|
+
.flatMap(r => r.value);
|
|
595
|
+
|
|
596
|
+
const allAssetResults = assetResults
|
|
597
|
+
.map(r => r.status === 'fulfilled' ? r.value : { status: 'rejected', reason: r.reason });
|
|
598
|
+
|
|
599
|
+
// Combine all results
|
|
600
|
+
const allResults = [...allCodeResults, ...allAssetResults];
|
|
601
|
+
|
|
602
|
+
// Sort by original index to maintain order
|
|
603
|
+
allResults.sort((a, b) => (a.index || 0) - (b.index || 0));
|
|
604
|
+
|
|
393
605
|
// Process and display results
|
|
394
606
|
let successCount = 0;
|
|
395
607
|
let errorCount = 0;
|
|
396
608
|
|
|
397
|
-
for (const result of
|
|
398
|
-
if (result.
|
|
399
|
-
const { index, action, success, error, result: operationResult } = result
|
|
609
|
+
for (const result of allResults) {
|
|
610
|
+
if (result.success !== undefined) {
|
|
611
|
+
const { index, action, success, error, result: operationResult } = result;
|
|
400
612
|
|
|
401
613
|
if (success) {
|
|
402
614
|
successCount++;
|
|
@@ -416,7 +628,7 @@ const performNetworkRequest = async (actionQueue) => {
|
|
|
416
628
|
console.log(formatMultilineError(error));
|
|
417
629
|
console.log(chalk.redBright('─'.repeat(48)));
|
|
418
630
|
}
|
|
419
|
-
} else {
|
|
631
|
+
} else if (result.status === 'rejected') {
|
|
420
632
|
errorCount++;
|
|
421
633
|
console.log();
|
|
422
634
|
console.log(chalk.bgRed.bold.white(' ✖ Unexpected Error '));
|
|
@@ -429,6 +641,9 @@ const performNetworkRequest = async (actionQueue) => {
|
|
|
429
641
|
// Summary
|
|
430
642
|
console.log(chalk.blue("\n--- Publish Summary ---"));
|
|
431
643
|
console.log(chalk.green(`✓ Successful: ${successCount}`));
|
|
644
|
+
if (droppedActions.length > 0) {
|
|
645
|
+
console.log(chalk.yellow(`⊝ Dropped: ${droppedActions.length} (redundant)`));
|
|
646
|
+
}
|
|
432
647
|
if (errorCount > 0) {
|
|
433
648
|
console.log(chalk.red(`✗ Failed: ${errorCount}`));
|
|
434
649
|
} else {
|
|
@@ -436,6 +651,17 @@ const performNetworkRequest = async (actionQueue) => {
|
|
|
436
651
|
}
|
|
437
652
|
};
|
|
438
653
|
|
|
654
|
+
/**
|
|
655
|
+
* Executes all actions in the queue (dispatcher function).
|
|
656
|
+
*/
|
|
657
|
+
const performNetworkRequest = async (actionQueue) => {
|
|
658
|
+
if (USE_SEQUENTIAL_PROCESSING) {
|
|
659
|
+
return await performNetworkRequestSequential(actionQueue);
|
|
660
|
+
} else {
|
|
661
|
+
return await performNetworkRequestParallel(actionQueue);
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
|
|
439
665
|
/* ==================== MAIN PUBLISH LOGIC ==================== */
|
|
440
666
|
|
|
441
667
|
/**
|
|
@@ -447,34 +673,69 @@ const performNetworkRequest = async (actionQueue) => {
|
|
|
447
673
|
export const runPublish = async (options = {}) => {
|
|
448
674
|
const { silent = false } = options;
|
|
449
675
|
|
|
676
|
+
// Create progress tracker
|
|
677
|
+
const progress = silent ? null : new ProgressTracker('Publish to Magentrix');
|
|
678
|
+
if (progress) {
|
|
679
|
+
progress.addStep('auth', 'Authenticating...');
|
|
680
|
+
progress.addStep('load', 'Loading cached data...');
|
|
681
|
+
progress.addStep('scan', 'Scanning local files...');
|
|
682
|
+
progress.addStep('compare-assets', 'Comparing assets...', { hasProgress: true });
|
|
683
|
+
progress.addStep('compare-code', 'Comparing code entities...', { hasProgress: true });
|
|
684
|
+
progress.addStep('prepare', 'Preparing action queue...');
|
|
685
|
+
progress.start();
|
|
686
|
+
// Start first step immediately so UI shows up
|
|
687
|
+
progress.startStep('auth');
|
|
688
|
+
}
|
|
689
|
+
|
|
450
690
|
// Step 1: Authenticate
|
|
451
691
|
await ensureValidCredentials().catch((err) => {
|
|
452
|
-
if (
|
|
692
|
+
if (progress) {
|
|
693
|
+
progress.abort(err.message);
|
|
694
|
+
} else if (!silent) {
|
|
453
695
|
console.error(chalk.red.bold("Authentication failed:"), chalk.white(err.message));
|
|
454
696
|
}
|
|
455
697
|
throw err;
|
|
456
698
|
});
|
|
699
|
+
if (progress) progress.completeStep('auth', '✓ Authenticated');
|
|
457
700
|
|
|
458
701
|
// Step 2: Load cached file state
|
|
702
|
+
if (progress) progress.startStep('load');
|
|
703
|
+
|
|
704
|
+
const loadStart = Date.now();
|
|
459
705
|
const hits = await config.searchObject({}, { filename: "base.json", global: false });
|
|
460
706
|
const cachedResults = hits?.[0]?.value || {};
|
|
707
|
+
const loadTime = Date.now() - loadStart;
|
|
461
708
|
|
|
462
709
|
if (!Object.keys(cachedResults).length) {
|
|
463
|
-
if (
|
|
710
|
+
if (progress) {
|
|
711
|
+
progress.abort('No file cache found');
|
|
712
|
+
} else if (!silent) {
|
|
464
713
|
console.log(chalk.red.bold("No file cache found!"));
|
|
465
714
|
console.log(`Run ${chalk.cyan("magentrix pull")} to initialize your workspace.`);
|
|
466
715
|
}
|
|
467
716
|
throw new Error("No file cache found");
|
|
468
717
|
}
|
|
469
718
|
|
|
719
|
+
const mapStart = Date.now();
|
|
470
720
|
const cachedFiles = Object.values(cachedResults).map((c) => ({
|
|
471
721
|
...c,
|
|
472
722
|
tag: c.recordId,
|
|
473
723
|
filePath: c.filePath || c.lastKnownPath,
|
|
474
724
|
}));
|
|
725
|
+
const mapTime = Date.now() - mapStart;
|
|
726
|
+
|
|
727
|
+
if (progress) {
|
|
728
|
+
progress.completeStep('load', `✓ Loaded ${cachedFiles.length} entries (${loadTime}ms load, ${mapTime}ms map)`);
|
|
729
|
+
}
|
|
475
730
|
|
|
476
731
|
// Step 3: Scan local workspace (excluding Assets folder)
|
|
732
|
+
if (progress) progress.startStep('scan');
|
|
733
|
+
|
|
734
|
+
const walkStart = Date.now();
|
|
477
735
|
const localPaths = await walkFiles(EXPORT_ROOT, { ignore: [path.join(EXPORT_ROOT, 'Assets')] });
|
|
736
|
+
const walkTime = Date.now() - walkStart;
|
|
737
|
+
|
|
738
|
+
const tagStart = Date.now();
|
|
478
739
|
const localFiles = await Promise.all(
|
|
479
740
|
localPaths.map(async (p) => {
|
|
480
741
|
try {
|
|
@@ -485,19 +746,59 @@ export const runPublish = async (options = {}) => {
|
|
|
485
746
|
}
|
|
486
747
|
})
|
|
487
748
|
);
|
|
749
|
+
const tagTime = Date.now() - tagStart;
|
|
750
|
+
|
|
751
|
+
if (progress) {
|
|
752
|
+
progress.completeStep('scan', `✓ Found ${localPaths.length} files (${walkTime}ms walk, ${tagTime}ms tags)`);
|
|
753
|
+
}
|
|
488
754
|
|
|
489
755
|
// Step 4: Create lookup maps
|
|
756
|
+
if (progress) progress.startStep('compare-assets');
|
|
757
|
+
|
|
758
|
+
const mapBuildStart = Date.now();
|
|
490
759
|
const cacheById = Object.fromEntries(cachedFiles.map((c) => [c.tag, c]));
|
|
491
760
|
const localById = Object.fromEntries(localFiles.filter((f) => f.tag).map((f) => [f.tag, f]));
|
|
492
761
|
const newLocalNoId = localFiles.filter((f) => !f.tag);
|
|
493
762
|
const allIds = new Set([...Object.keys(cacheById), ...Object.keys(localById)]);
|
|
763
|
+
const mapBuildTime = Date.now() - mapBuildStart;
|
|
494
764
|
|
|
495
765
|
const actionQueue = [];
|
|
496
766
|
|
|
497
|
-
// Step 5: Handle static asset files
|
|
767
|
+
// Step 5: Handle static asset files - Build fast lookup map first (O(n) instead of O(n²))
|
|
768
|
+
const assetWalkStart = Date.now();
|
|
498
769
|
const assetPaths = await walkFiles(path.join(EXPORT_ROOT, 'Assets'));
|
|
499
|
-
|
|
500
|
-
|
|
770
|
+
const assetWalkTime = Date.now() - assetWalkStart;
|
|
771
|
+
|
|
772
|
+
// Build a Set of normalized cached asset paths for O(1) lookup
|
|
773
|
+
const setStart = Date.now();
|
|
774
|
+
const cachedAssetPaths = new Set();
|
|
775
|
+
cachedFiles
|
|
776
|
+
.filter(cf => cf.type === 'File' || cf.type === 'Folder')
|
|
777
|
+
.forEach(cf => {
|
|
778
|
+
if (cf.lastKnownActualPath) {
|
|
779
|
+
cachedAssetPaths.add(path.normalize(path.resolve(cf.lastKnownActualPath)).toLowerCase());
|
|
780
|
+
}
|
|
781
|
+
if (cf.filePath) {
|
|
782
|
+
cachedAssetPaths.add(path.normalize(path.resolve(cf.filePath)).toLowerCase());
|
|
783
|
+
}
|
|
784
|
+
if (cf.lastKnownPath) {
|
|
785
|
+
cachedAssetPaths.add(path.normalize(path.resolve(cf.lastKnownPath)).toLowerCase());
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
const setTime = Date.now() - setStart;
|
|
789
|
+
|
|
790
|
+
// Now compare assets with O(1) lookup
|
|
791
|
+
for (let i = 0; i < assetPaths.length; i++) {
|
|
792
|
+
const assetPath = assetPaths[i];
|
|
793
|
+
const normalizedAssetPath = path.normalize(path.resolve(assetPath)).toLowerCase();
|
|
794
|
+
|
|
795
|
+
// Update progress every 100 files
|
|
796
|
+
if (progress && i % 100 === 0) {
|
|
797
|
+
progress.updateProgress('compare-assets', i, assetPaths.length, `Checking ${i}/${assetPaths.length} assets`);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// O(1) lookup instead of O(n) find()
|
|
801
|
+
if (cachedAssetPaths.has(normalizedAssetPath)) {
|
|
501
802
|
continue;
|
|
502
803
|
}
|
|
503
804
|
|
|
@@ -508,17 +809,43 @@ export const runPublish = async (options = {}) => {
|
|
|
508
809
|
});
|
|
509
810
|
}
|
|
510
811
|
|
|
511
|
-
|
|
812
|
+
if (progress) {
|
|
813
|
+
progress.completeStep('compare-assets', `✓ Compared ${assetPaths.length} assets (walk:${assetWalkTime}ms, set:${setTime}ms, map:${mapBuildTime}ms)`);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Step 6: Handle folder creation and deletion - Also optimized with Set
|
|
512
817
|
const assetsDir = path.join(EXPORT_ROOT, 'Assets');
|
|
513
818
|
if (fs.existsSync(assetsDir)) {
|
|
514
819
|
const localFolders = walkFolders(assetsDir);
|
|
515
820
|
const cachedFolders = cachedFiles
|
|
516
|
-
.filter(c => c.type === 'Folder' && (c.filePath || c.lastKnownPath))
|
|
517
|
-
|
|
821
|
+
.filter(c => c.type === 'Folder' && (c.filePath || c.lastKnownPath || c.lastKnownActualPath));
|
|
822
|
+
|
|
823
|
+
// Build Set of cached folder paths for O(1) lookup
|
|
824
|
+
const cachedFolderPaths = new Set();
|
|
825
|
+
cachedFolders.forEach(cf => {
|
|
826
|
+
if (cf.lastKnownActualPath) {
|
|
827
|
+
cachedFolderPaths.add(path.normalize(path.resolve(cf.lastKnownActualPath)).toLowerCase());
|
|
828
|
+
}
|
|
829
|
+
if (cf.lastKnownPath) {
|
|
830
|
+
cachedFolderPaths.add(path.normalize(path.resolve(cf.lastKnownPath)).toLowerCase());
|
|
831
|
+
}
|
|
832
|
+
if (cf.filePath) {
|
|
833
|
+
cachedFolderPaths.add(path.normalize(path.resolve(cf.filePath)).toLowerCase());
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
// Build Set of local folder paths for O(1) lookup
|
|
838
|
+
const localFolderPaths = new Set(
|
|
839
|
+
localFolders.map(lf => path.normalize(path.resolve(lf)).toLowerCase())
|
|
840
|
+
);
|
|
518
841
|
|
|
519
|
-
// New folders
|
|
842
|
+
// New folders - O(1) lookup
|
|
520
843
|
for (const folderPath of localFolders) {
|
|
521
|
-
if (!folderPath
|
|
844
|
+
if (!folderPath) continue;
|
|
845
|
+
|
|
846
|
+
const normalizedFolderPath = path.normalize(path.resolve(folderPath)).toLowerCase();
|
|
847
|
+
|
|
848
|
+
if (cachedFolderPaths.has(normalizedFolderPath)) {
|
|
522
849
|
continue;
|
|
523
850
|
}
|
|
524
851
|
|
|
@@ -533,27 +860,41 @@ export const runPublish = async (options = {}) => {
|
|
|
533
860
|
});
|
|
534
861
|
}
|
|
535
862
|
|
|
536
|
-
// Deleted folders
|
|
863
|
+
// Deleted folders - O(1) lookup
|
|
537
864
|
for (const cachedFolder of cachedFolders) {
|
|
538
|
-
|
|
539
|
-
if (
|
|
865
|
+
const cachedPath = cachedFolder.lastKnownActualPath || cachedFolder.lastKnownPath || cachedFolder.filePath;
|
|
866
|
+
if (!cachedPath || typeof cachedPath !== 'string') continue;
|
|
867
|
+
|
|
868
|
+
const normalizedCachedPath = path.normalize(path.resolve(cachedPath)).toLowerCase();
|
|
869
|
+
|
|
870
|
+
if (localFolderPaths.has(normalizedCachedPath)) {
|
|
540
871
|
continue;
|
|
541
872
|
}
|
|
542
873
|
|
|
543
|
-
const parentDir = path.dirname(
|
|
544
|
-
if (!parentDir || parentDir === '.' || parentDir ===
|
|
874
|
+
const parentDir = path.dirname(cachedPath);
|
|
875
|
+
if (!parentDir || parentDir === '.' || parentDir === cachedPath) continue;
|
|
545
876
|
|
|
546
877
|
actionQueue.push({
|
|
547
878
|
action: "delete_folder",
|
|
548
|
-
folderPath:
|
|
879
|
+
folderPath: cachedPath,
|
|
549
880
|
parentPath: toApiFolderPath(parentDir),
|
|
550
|
-
folderName: path.basename(
|
|
881
|
+
folderName: path.basename(cachedPath)
|
|
551
882
|
});
|
|
552
883
|
}
|
|
553
884
|
}
|
|
554
885
|
|
|
555
886
|
// Step 7: Process code entities (ActiveClass/ActivePage) and static assets
|
|
556
|
-
|
|
887
|
+
if (progress) progress.startStep('compare-code');
|
|
888
|
+
|
|
889
|
+
const allIdsArray = Array.from(allIds);
|
|
890
|
+
for (let idx = 0; idx < allIdsArray.length; idx++) {
|
|
891
|
+
const id = allIdsArray[idx];
|
|
892
|
+
|
|
893
|
+
// Update progress every 50 items
|
|
894
|
+
if (progress && idx % 50 === 0) {
|
|
895
|
+
progress.updateProgress('compare-code', idx, allIdsArray.length, `Checking ${idx}/${allIdsArray.length} entities`);
|
|
896
|
+
}
|
|
897
|
+
|
|
557
898
|
try {
|
|
558
899
|
const cacheFile = cacheById[id];
|
|
559
900
|
const curFile = localById[id];
|
|
@@ -563,24 +904,26 @@ export const runPublish = async (options = {}) => {
|
|
|
563
904
|
|
|
564
905
|
// Handle static asset files
|
|
565
906
|
if (cacheFile?.type === 'File') {
|
|
566
|
-
|
|
907
|
+
// Use lastKnownActualPath which has the correct path (e.g., "src/Assets/...")
|
|
908
|
+
const actualPath = cacheFile.lastKnownActualPath || cacheFile.filePath;
|
|
909
|
+
const localAssetExists = fs.existsSync(actualPath);
|
|
567
910
|
|
|
568
911
|
if (!localAssetExists) {
|
|
569
912
|
actionQueue.push({
|
|
570
913
|
action: 'delete_static_asset',
|
|
571
|
-
folder: toApiPath(
|
|
572
|
-
names: [path.basename(
|
|
573
|
-
filePath:
|
|
914
|
+
folder: toApiPath(actualPath),
|
|
915
|
+
names: [path.basename(actualPath)],
|
|
916
|
+
filePath: actualPath // Store actual file path for filtering
|
|
574
917
|
});
|
|
575
918
|
continue;
|
|
576
919
|
}
|
|
577
920
|
|
|
578
|
-
const contentHash = sha256(fs.readFileSync(
|
|
921
|
+
const contentHash = sha256(fs.readFileSync(actualPath, 'utf-8'));
|
|
579
922
|
if (contentHash !== cacheFile.contentHash) {
|
|
580
923
|
actionQueue.push({
|
|
581
924
|
action: "create_static_asset",
|
|
582
|
-
folder: toApiPath(
|
|
583
|
-
filePath:
|
|
925
|
+
folder: toApiPath(actualPath),
|
|
926
|
+
filePath: actualPath
|
|
584
927
|
});
|
|
585
928
|
}
|
|
586
929
|
continue;
|
|
@@ -644,15 +987,25 @@ export const runPublish = async (options = {}) => {
|
|
|
644
987
|
}
|
|
645
988
|
}
|
|
646
989
|
} catch (err) {
|
|
647
|
-
|
|
990
|
+
if (!silent) {
|
|
991
|
+
console.error(chalk.yellow(`Warning: Error processing file with ID ${id}:`), err.message);
|
|
992
|
+
}
|
|
648
993
|
}
|
|
649
994
|
}
|
|
650
995
|
|
|
996
|
+
if (progress) {
|
|
997
|
+
progress.completeStep('compare-code', `✓ Compared ${allIdsArray.length} code entities`);
|
|
998
|
+
}
|
|
999
|
+
|
|
651
1000
|
// Step 8: Handle brand-new, tag-less files
|
|
1001
|
+
if (progress) progress.startStep('prepare');
|
|
1002
|
+
|
|
652
1003
|
for (const f of newLocalNoId) {
|
|
653
1004
|
const safe = readFileSafe(f.path);
|
|
654
1005
|
if (!safe) {
|
|
655
|
-
|
|
1006
|
+
if (!silent) {
|
|
1007
|
+
console.log(chalk.yellow(`Skipping unreadable file: ${f.path}`));
|
|
1008
|
+
}
|
|
656
1009
|
continue;
|
|
657
1010
|
}
|
|
658
1011
|
|
|
@@ -709,6 +1062,11 @@ export const runPublish = async (options = {}) => {
|
|
|
709
1062
|
return true;
|
|
710
1063
|
});
|
|
711
1064
|
|
|
1065
|
+
if (progress) {
|
|
1066
|
+
progress.completeStep('prepare', `✓ Prepared ${filteredActionQueue.length} actions`);
|
|
1067
|
+
progress.finish();
|
|
1068
|
+
}
|
|
1069
|
+
|
|
712
1070
|
// Step 10: Display and execute action queue
|
|
713
1071
|
if (!silent) {
|
|
714
1072
|
console.log(chalk.blue("\n--- Publish Action Queue ---"));
|