@magentrix-corp/magentrix-cli 1.2.1 → 1.3.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 +282 -2
- package/actions/autopublish.js +9 -48
- package/actions/iris/buildStage.js +330 -0
- package/actions/iris/delete.js +211 -0
- package/actions/iris/dev.js +338 -0
- package/actions/iris/index.js +6 -0
- package/actions/iris/link.js +377 -0
- package/actions/iris/recover.js +228 -0
- package/actions/publish.js +183 -9
- package/actions/pull.js +107 -4
- package/bin/magentrix.js +43 -1
- package/package.json +2 -1
- package/utils/autopublishLock.js +77 -0
- package/utils/cli/helpers/compare.js +4 -5
- package/utils/iris/backup.js +201 -0
- package/utils/iris/builder.js +304 -0
- package/utils/iris/config-reader.js +296 -0
- package/utils/iris/deleteHelper.js +102 -0
- package/utils/iris/linker.js +490 -0
- package/utils/iris/validator.js +281 -0
- package/utils/iris/zipper.js +239 -0
- package/utils/logger.js +13 -5
- package/utils/magentrix/api/iris.js +235 -0
- package/utils/permissionError.js +70 -0
- package/utils/progress.js +87 -1
- package/utils/updateFileBase.js +10 -2
- package/vars/global.js +1 -0
package/actions/publish.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ENTITY_TYPE_MAP,
|
|
12
12
|
EXPORT_ROOT,
|
|
13
13
|
TYPE_DIR_MAP,
|
|
14
|
+
IRIS_APPS_DIR,
|
|
14
15
|
} from "../vars/global.js";
|
|
15
16
|
import { getFileTag, setFileTag } from "../utils/filetag.js";
|
|
16
17
|
import { sha256 } from "../utils/hash.js";
|
|
@@ -20,6 +21,11 @@ import { deleteEntity } from "../utils/magentrix/api/deleteEntity.js";
|
|
|
20
21
|
import { removeFromBase, updateBase } from "../utils/updateFileBase.js";
|
|
21
22
|
import { deleteAsset, uploadAsset, createFolder } from "../utils/magentrix/api/assets.js";
|
|
22
23
|
import { toApiPath, toApiFolderPath } from "../utils/assetPaths.js";
|
|
24
|
+
import { publishApp } from "../utils/magentrix/api/iris.js";
|
|
25
|
+
import { createIrisZip, hashIrisAppFolder } from "../utils/iris/zipper.js";
|
|
26
|
+
import { validateIrisAppFolder } from "../utils/iris/validator.js";
|
|
27
|
+
import { getLinkedProjects } from "../utils/iris/linker.js";
|
|
28
|
+
import { deleteIrisAppFromServer } from "../utils/iris/deleteHelper.js";
|
|
23
29
|
|
|
24
30
|
const config = new Config();
|
|
25
31
|
|
|
@@ -120,6 +126,8 @@ const walkFolders = (dir) => {
|
|
|
120
126
|
* Gets display name for an action in log messages.
|
|
121
127
|
*/
|
|
122
128
|
const getActionDisplayName = (action) => {
|
|
129
|
+
// Iris app actions
|
|
130
|
+
if (action.slug) return `${action.appName || action.slug} (${action.slug})`;
|
|
123
131
|
if (action.folderName) return action.folderName;
|
|
124
132
|
if (action.names) return action.names.join(", ");
|
|
125
133
|
if (action.type) return action.type;
|
|
@@ -273,6 +281,35 @@ const handleDeleteFolderAction = async (instanceUrl, apiKey, action) => {
|
|
|
273
281
|
}
|
|
274
282
|
};
|
|
275
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Handles CREATE_IRIS_APP or UPDATE_IRIS_APP action.
|
|
286
|
+
*/
|
|
287
|
+
const handlePublishIrisAppAction = async (instanceUrl, apiKey, action) => {
|
|
288
|
+
// Create zip from the app folder
|
|
289
|
+
const zipBuffer = await createIrisZip(action.appPath, action.slug);
|
|
290
|
+
|
|
291
|
+
// Publish via API with app-name parameter
|
|
292
|
+
const response = await publishApp(
|
|
293
|
+
instanceUrl,
|
|
294
|
+
apiKey,
|
|
295
|
+
zipBuffer,
|
|
296
|
+
`${action.slug}.zip`,
|
|
297
|
+
action.appName
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
return response;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Handles DELETE_IRIS_APP action.
|
|
305
|
+
*/
|
|
306
|
+
const handleDeleteIrisAppAction = async (instanceUrl, apiKey, action) => {
|
|
307
|
+
// Use shared delete utility for consistency
|
|
308
|
+
return await deleteIrisAppFromServer(instanceUrl, apiKey, action.slug, {
|
|
309
|
+
updateCache: true // Cache will be updated by the utility
|
|
310
|
+
});
|
|
311
|
+
};
|
|
312
|
+
|
|
276
313
|
/**
|
|
277
314
|
* Synchronizes class name in content with filename, or vice versa.
|
|
278
315
|
*/
|
|
@@ -425,6 +462,28 @@ const updateCacheAfterSuccess = async (action, operationResult) => {
|
|
|
425
462
|
}
|
|
426
463
|
break;
|
|
427
464
|
}
|
|
465
|
+
|
|
466
|
+
case "create_iris_app":
|
|
467
|
+
case "update_iris_app": {
|
|
468
|
+
// Update base.json with new/updated Iris app info including content hash
|
|
469
|
+
updateBase(action.appPath, {
|
|
470
|
+
Id: `iris-app:${action.slug}`,
|
|
471
|
+
Type: 'IrisApp',
|
|
472
|
+
folderName: action.slug,
|
|
473
|
+
appName: action.appName,
|
|
474
|
+
modifiedOn: new Date().toISOString(),
|
|
475
|
+
contentHash: action.contentHash // Store hash for change detection
|
|
476
|
+
}, action.appPath);
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
case "delete_iris_app": {
|
|
481
|
+
// Skip if already cleaned from cache during 404 handling
|
|
482
|
+
if (!operationResult?.cleanedFromCache) {
|
|
483
|
+
removeFromBase(`iris-app:${action.slug}`);
|
|
484
|
+
}
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
428
487
|
}
|
|
429
488
|
} catch (error) {
|
|
430
489
|
console.warn(chalk.yellow(`Warning: Failed to update cache for ${action.action}: ${error.message}`));
|
|
@@ -444,8 +503,8 @@ const groupActionsByResource = (actionQueue) => {
|
|
|
444
503
|
for (let i = 0; i < actionQueue.length; i++) {
|
|
445
504
|
const action = { ...actionQueue[i], originalIndex: i };
|
|
446
505
|
|
|
447
|
-
// Asset operations don't need sequencing
|
|
448
|
-
if (['create_static_asset', 'delete_static_asset', 'create_folder', 'delete_folder'].includes(action.action)) {
|
|
506
|
+
// Asset and Iris app operations don't need sequencing
|
|
507
|
+
if (['create_static_asset', 'delete_static_asset', 'create_folder', 'delete_folder', 'create_iris_app', 'update_iris_app', 'delete_iris_app'].includes(action.action)) {
|
|
449
508
|
assets.push(action);
|
|
450
509
|
continue;
|
|
451
510
|
}
|
|
@@ -544,6 +603,13 @@ const executeAction = async (instanceUrl, token, action) => {
|
|
|
544
603
|
case "delete_folder":
|
|
545
604
|
result = await handleDeleteFolderAction(instanceUrl, token, action);
|
|
546
605
|
break;
|
|
606
|
+
case "create_iris_app":
|
|
607
|
+
case "update_iris_app":
|
|
608
|
+
result = await handlePublishIrisAppAction(instanceUrl, token, action);
|
|
609
|
+
break;
|
|
610
|
+
case "delete_iris_app":
|
|
611
|
+
result = await handleDeleteIrisAppAction(instanceUrl, token, action);
|
|
612
|
+
break;
|
|
547
613
|
default:
|
|
548
614
|
throw new Error(`Unknown action: ${action.action}`);
|
|
549
615
|
}
|
|
@@ -587,7 +653,8 @@ const performNetworkRequestSequential = async (actionQueue) => {
|
|
|
587
653
|
console.log();
|
|
588
654
|
console.log(chalk.bgRed.bold.white(' ✖ Operation Failed '));
|
|
589
655
|
console.log(chalk.redBright('─'.repeat(48)));
|
|
590
|
-
|
|
656
|
+
const actionPath = action.filePath || action.folderPath || action.folder || action.appPath || action.slug || 'unknown';
|
|
657
|
+
console.log(chalk.red.bold(`[${i + 1}] ${action.action.toUpperCase()} ${displayName} (${actionPath}):`));
|
|
591
658
|
console.log(formatMultilineError(error.message));
|
|
592
659
|
console.log(chalk.redBright('─'.repeat(48)));
|
|
593
660
|
}
|
|
@@ -693,7 +760,8 @@ const performNetworkRequestParallel = async (actionQueue) => {
|
|
|
693
760
|
console.log();
|
|
694
761
|
console.log(chalk.bgRed.bold.white(' ✖ Operation Failed '));
|
|
695
762
|
console.log(chalk.redBright('─'.repeat(48)));
|
|
696
|
-
|
|
763
|
+
const actionPath = action.filePath || action.folderPath || action.folder || action.appPath || action.slug || 'unknown';
|
|
764
|
+
console.log(chalk.red.bold(`[${index + 1}] ${action.action.toUpperCase()} ${getActionDisplayName(action)} (${actionPath}):`));
|
|
697
765
|
console.log(formatMultilineError(error));
|
|
698
766
|
console.log(chalk.redBright('─'.repeat(48)));
|
|
699
767
|
}
|
|
@@ -790,6 +858,7 @@ export const runPublish = async (options = {}) => {
|
|
|
790
858
|
...c,
|
|
791
859
|
tag: c.recordId,
|
|
792
860
|
filePath: c.filePath || c.lastKnownPath,
|
|
861
|
+
type: c.type || c.Type, // Normalize Type/type property
|
|
793
862
|
}));
|
|
794
863
|
const mapTime = Date.now() - mapStart;
|
|
795
864
|
|
|
@@ -797,11 +866,16 @@ export const runPublish = async (options = {}) => {
|
|
|
797
866
|
progress.completeStep('load', `✓ Loaded ${cachedFiles.length} entries (${loadTime}ms load, ${mapTime}ms map)`);
|
|
798
867
|
}
|
|
799
868
|
|
|
800
|
-
// Step 3: Scan local workspace (excluding Assets
|
|
869
|
+
// Step 3: Scan local workspace (excluding Assets and iris-apps folders)
|
|
801
870
|
if (progress) progress.startStep('scan');
|
|
802
871
|
|
|
803
872
|
const walkStart = Date.now();
|
|
804
|
-
const localPaths = await walkFiles(EXPORT_ROOT, {
|
|
873
|
+
const localPaths = await walkFiles(EXPORT_ROOT, {
|
|
874
|
+
ignore: [
|
|
875
|
+
path.join(EXPORT_ROOT, 'Assets'),
|
|
876
|
+
path.join(EXPORT_ROOT, IRIS_APPS_DIR)
|
|
877
|
+
]
|
|
878
|
+
});
|
|
805
879
|
const walkTime = Date.now() - walkStart;
|
|
806
880
|
|
|
807
881
|
const tagStart = Date.now();
|
|
@@ -971,6 +1045,9 @@ export const runPublish = async (options = {}) => {
|
|
|
971
1045
|
// Skip folders - they're handled separately
|
|
972
1046
|
if (cacheFile?.type === 'Folder') continue;
|
|
973
1047
|
|
|
1048
|
+
// Skip Iris apps - they're handled separately (check both type and ID prefix)
|
|
1049
|
+
if (cacheFile?.type === 'IrisApp' || id.startsWith('iris-app:')) continue;
|
|
1050
|
+
|
|
974
1051
|
// Handle static asset files
|
|
975
1052
|
if (cacheFile?.type === 'File') {
|
|
976
1053
|
// Use lastKnownActualPath which has the correct path (e.g., "src/Assets/...")
|
|
@@ -1066,6 +1143,94 @@ export const runPublish = async (options = {}) => {
|
|
|
1066
1143
|
progress.completeStep('compare-code', `✓ Compared ${allIdsArray.length} code entities`);
|
|
1067
1144
|
}
|
|
1068
1145
|
|
|
1146
|
+
// Step 7b: Scan and compare Iris apps
|
|
1147
|
+
const irisAppsPath = path.join(EXPORT_ROOT, IRIS_APPS_DIR);
|
|
1148
|
+
|
|
1149
|
+
// Get cached Iris apps (always check, even if local folder doesn't exist)
|
|
1150
|
+
// Also include entries with iris-app: prefix in case type wasn't properly set
|
|
1151
|
+
const cachedIrisApps = cachedFiles
|
|
1152
|
+
.filter(cf => cf.type === 'IrisApp' || (cf.tag && cf.tag.startsWith('iris-app:')))
|
|
1153
|
+
.map(cf => {
|
|
1154
|
+
// Extract slug from folderName, or from the ID (iris-app:<slug>)
|
|
1155
|
+
const slug = cf.folderName || (cf.tag && cf.tag.startsWith('iris-app:') ? cf.tag.replace('iris-app:', '') : null);
|
|
1156
|
+
return {
|
|
1157
|
+
slug,
|
|
1158
|
+
appName: cf.appName || slug,
|
|
1159
|
+
modifiedOn: cf.modifiedOn,
|
|
1160
|
+
contentHash: cf.contentHash || null // Track content hash for change detection
|
|
1161
|
+
};
|
|
1162
|
+
})
|
|
1163
|
+
.filter(app => app.slug); // Filter out any entries without a valid slug
|
|
1164
|
+
|
|
1165
|
+
// Get local Iris apps (empty if folder doesn't exist)
|
|
1166
|
+
const localIrisApps = fs.existsSync(irisAppsPath)
|
|
1167
|
+
? fs.readdirSync(irisAppsPath, { withFileTypes: true })
|
|
1168
|
+
.filter(d => d.isDirectory())
|
|
1169
|
+
.map(d => d.name)
|
|
1170
|
+
: [];
|
|
1171
|
+
|
|
1172
|
+
const cachedIrisSlugs = new Set(cachedIrisApps.map(a => a.slug));
|
|
1173
|
+
const localIrisSlugs = new Set(localIrisApps);
|
|
1174
|
+
|
|
1175
|
+
// Detect new and modified Iris apps
|
|
1176
|
+
for (const slug of localIrisApps) {
|
|
1177
|
+
const appPath = path.join(irisAppsPath, slug);
|
|
1178
|
+
|
|
1179
|
+
// Validate the app folder has required files
|
|
1180
|
+
const validation = validateIrisAppFolder(appPath);
|
|
1181
|
+
if (!validation.valid) {
|
|
1182
|
+
if (!silent) {
|
|
1183
|
+
console.log(chalk.yellow(`⚠ Skipping invalid Iris app '${slug}': ${validation.errors.join(', ')}`));
|
|
1184
|
+
}
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Get app name from linked project config (stored globally) or use slug
|
|
1189
|
+
const linkedProjects = getLinkedProjects();
|
|
1190
|
+
const linkedProject = linkedProjects.find(p => p.slug === slug);
|
|
1191
|
+
const appName = linkedProject?.appName || slug;
|
|
1192
|
+
|
|
1193
|
+
// Calculate content hash for change detection
|
|
1194
|
+
const currentHash = hashIrisAppFolder(appPath);
|
|
1195
|
+
|
|
1196
|
+
// Check if this is a new or existing app
|
|
1197
|
+
const isNew = !cachedIrisSlugs.has(slug);
|
|
1198
|
+
|
|
1199
|
+
if (isNew) {
|
|
1200
|
+
actionQueue.push({
|
|
1201
|
+
action: 'create_iris_app',
|
|
1202
|
+
slug,
|
|
1203
|
+
appName,
|
|
1204
|
+
appPath,
|
|
1205
|
+
contentHash: currentHash
|
|
1206
|
+
});
|
|
1207
|
+
} else {
|
|
1208
|
+
// Only update if content has changed
|
|
1209
|
+
const cachedApp = cachedIrisApps.find(a => a.slug === slug);
|
|
1210
|
+
const contentChanged = !cachedApp?.contentHash || cachedApp.contentHash !== currentHash;
|
|
1211
|
+
|
|
1212
|
+
if (contentChanged) {
|
|
1213
|
+
actionQueue.push({
|
|
1214
|
+
action: 'update_iris_app',
|
|
1215
|
+
slug,
|
|
1216
|
+
appName: linkedProject?.appName || cachedApp?.appName || slug,
|
|
1217
|
+
appPath,
|
|
1218
|
+
contentHash: currentHash
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Detect deleted Iris apps (works even if iris-apps folder doesn't exist)
|
|
1225
|
+
for (const cachedApp of cachedIrisApps) {
|
|
1226
|
+
if (!localIrisSlugs.has(cachedApp.slug)) {
|
|
1227
|
+
actionQueue.push({
|
|
1228
|
+
action: 'delete_iris_app',
|
|
1229
|
+
slug: cachedApp.slug
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1069
1234
|
// Step 8: Handle brand-new, tag-less files
|
|
1070
1235
|
if (progress) progress.startStep('prepare');
|
|
1071
1236
|
|
|
@@ -1146,19 +1311,28 @@ export const runPublish = async (options = {}) => {
|
|
|
1146
1311
|
const num = chalk.green(`[${i + 1}]`);
|
|
1147
1312
|
const act = chalk.yellow(a.action.toUpperCase());
|
|
1148
1313
|
|
|
1149
|
-
let type, displayPath;
|
|
1150
|
-
if (a.
|
|
1314
|
+
let type, displayPath, label;
|
|
1315
|
+
if (a.slug) {
|
|
1316
|
+
// Iris app action
|
|
1317
|
+
type = chalk.cyan(`${a.appName || a.slug}`);
|
|
1318
|
+
displayPath = a.appPath || a.slug;
|
|
1319
|
+
label = "App";
|
|
1320
|
+
} else if (a.folderName) {
|
|
1151
1321
|
type = chalk.cyan(a.folderName);
|
|
1152
1322
|
displayPath = a.folderPath;
|
|
1323
|
+
label = "Folder";
|
|
1153
1324
|
} else if (a.names) {
|
|
1154
1325
|
type = chalk.cyan(a.names.join(", "));
|
|
1155
1326
|
displayPath = a.folder;
|
|
1327
|
+
label = "File";
|
|
1156
1328
|
} else if (a.filePath) {
|
|
1157
1329
|
type = chalk.cyan(a.type || path.basename(a.filePath));
|
|
1158
1330
|
displayPath = a.filePath;
|
|
1331
|
+
label = a.type ? "Type" : "File";
|
|
1159
1332
|
} else {
|
|
1160
1333
|
type = chalk.cyan(a.type || "Unknown");
|
|
1161
1334
|
displayPath = a.folder || "Unknown";
|
|
1335
|
+
label = "File";
|
|
1162
1336
|
}
|
|
1163
1337
|
|
|
1164
1338
|
const idInfo = a.recordId ? ` ${chalk.magenta(a.recordId)}` : "";
|
|
@@ -1166,7 +1340,7 @@ export const runPublish = async (options = {}) => {
|
|
|
1166
1340
|
? ` → ${chalk.gray(a.oldPath)} ${chalk.white("→")} ${chalk.gray(a.filePath)}`
|
|
1167
1341
|
: "";
|
|
1168
1342
|
|
|
1169
|
-
console.log(`${num} ${act} | ${
|
|
1343
|
+
console.log(`${num} ${act} | ${label}: ${type}${idInfo}${renameInfo} (${displayPath})`);
|
|
1170
1344
|
});
|
|
1171
1345
|
|
|
1172
1346
|
console.log(chalk.blue("\n--- Publishing Changes ---"));
|
package/actions/pull.js
CHANGED
|
@@ -5,7 +5,7 @@ import fs from "fs";
|
|
|
5
5
|
import { withSpinner } from "../utils/spinner.js";
|
|
6
6
|
import { ProgressTracker } from "../utils/progress.js";
|
|
7
7
|
import { createLogger, Logger } from "../utils/logger.js";
|
|
8
|
-
import { EXPORT_ROOT, TYPE_DIR_MAP } from "../vars/global.js";
|
|
8
|
+
import { EXPORT_ROOT, TYPE_DIR_MAP, IRIS_APPS_DIR } from "../vars/global.js";
|
|
9
9
|
import { mapRecordToFile, writeRecords } from "../utils/cli/writeRecords.js";
|
|
10
10
|
import { updateBase, removeFromBase, removeFromBaseBulk } from "../utils/updateFileBase.js";
|
|
11
11
|
import { compareAllFilesAndLogStatus, promptConflictResolution, showCurrentConflicts } from "../utils/cli/helpers/compare.js";
|
|
@@ -15,6 +15,8 @@ import chalk from 'chalk';
|
|
|
15
15
|
import { getFileTag, setFileTag } from "../utils/filetag.js";
|
|
16
16
|
import { downloadAssetsZip, listAssets } from "../utils/magentrix/api/assets.js";
|
|
17
17
|
import { downloadAssets, walkAssets } from "../utils/downloadAssets.js";
|
|
18
|
+
import { listApps, downloadApp } from "../utils/magentrix/api/iris.js";
|
|
19
|
+
import { extractIrisZip, hashIrisAppFolder } from "../utils/iris/zipper.js";
|
|
18
20
|
import { v4 as uuidv4 } from 'uuid';
|
|
19
21
|
import readlineSync from 'readline-sync';
|
|
20
22
|
|
|
@@ -55,6 +57,7 @@ export const pull = async () => {
|
|
|
55
57
|
progress.addStep('load', 'Loading cached data...');
|
|
56
58
|
progress.addStep('download-code', 'Downloading code entities...', { hasProgress: true });
|
|
57
59
|
progress.addStep('download-assets', 'Downloading static assets...', { hasProgress: true });
|
|
60
|
+
progress.addStep('download-iris', 'Downloading Iris apps...', { hasProgress: true });
|
|
58
61
|
progress.addStep('process', 'Processing files...', { hasProgress: true });
|
|
59
62
|
progress.addStep('conflicts', 'Checking for conflicts...');
|
|
60
63
|
progress.addStep('write', 'Writing files...', { hasProgress: true });
|
|
@@ -213,6 +216,92 @@ export const pull = async () => {
|
|
|
213
216
|
progress.start();
|
|
214
217
|
}
|
|
215
218
|
|
|
219
|
+
// Step 4c: Download Iris apps
|
|
220
|
+
progress.startStep('download-iris');
|
|
221
|
+
let irisApps = [];
|
|
222
|
+
const irisDownloadErrors = [];
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
logger.info('Fetching Iris apps list');
|
|
226
|
+
const irisListResult = await listApps(instanceUrl, token.value);
|
|
227
|
+
|
|
228
|
+
if (irisListResult.success && irisListResult.apps && irisListResult.apps.length > 0) {
|
|
229
|
+
irisApps = irisListResult.apps;
|
|
230
|
+
const irisAppsDir = path.resolve(EXPORT_ROOT, IRIS_APPS_DIR);
|
|
231
|
+
|
|
232
|
+
// Ensure iris-apps directory exists
|
|
233
|
+
if (!fs.existsSync(irisAppsDir)) {
|
|
234
|
+
fs.mkdirSync(irisAppsDir, { recursive: true });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
logger.info(`Found ${irisApps.length} Iris apps to download`);
|
|
238
|
+
|
|
239
|
+
for (let i = 0; i < irisApps.length; i++) {
|
|
240
|
+
const app = irisApps[i];
|
|
241
|
+
progress.updateProgress('download-iris', i + 1, irisApps.length, `Downloading ${app.folderName}...`);
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
// Download the app as a buffer
|
|
245
|
+
const { buffer } = await downloadApp(instanceUrl, token.value, app.folderName);
|
|
246
|
+
|
|
247
|
+
// Extract to iris-apps directory
|
|
248
|
+
const appDir = path.join(irisAppsDir, app.folderName);
|
|
249
|
+
|
|
250
|
+
// Remove existing app directory if it exists
|
|
251
|
+
if (fs.existsSync(appDir)) {
|
|
252
|
+
fs.rmSync(appDir, { recursive: true, force: true });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Extract the zip (it contains a folder with the app name)
|
|
256
|
+
await extractIrisZip(buffer, irisAppsDir);
|
|
257
|
+
|
|
258
|
+
// Calculate content hash for change detection
|
|
259
|
+
const contentHash = hashIrisAppFolder(appDir);
|
|
260
|
+
|
|
261
|
+
// Update base.json for this Iris app
|
|
262
|
+
updateBase(appDir, {
|
|
263
|
+
Id: `iris-app:${app.folderName}`,
|
|
264
|
+
Type: 'IrisApp',
|
|
265
|
+
folderName: app.folderName,
|
|
266
|
+
uploadedOn: app.uploadedOn,
|
|
267
|
+
modifiedOn: app.modifiedOn,
|
|
268
|
+
size: app.size,
|
|
269
|
+
contentHash // Store hash for change detection
|
|
270
|
+
}, appDir);
|
|
271
|
+
|
|
272
|
+
logger.info(`Downloaded Iris app: ${app.folderName}`);
|
|
273
|
+
} catch (appError) {
|
|
274
|
+
// Detect permission errors and provide helpful hint
|
|
275
|
+
const isPermissionError = appError.code === 'EACCES' || appError.code === 'EPERM' ||
|
|
276
|
+
appError.message?.includes('permission denied') || appError.message?.includes('EACCES');
|
|
277
|
+
|
|
278
|
+
// Create a more informative error message
|
|
279
|
+
const errorDetail = appError.message || String(appError);
|
|
280
|
+
const errorMsg = `Failed to download Iris app ${app.folderName}: ${errorDetail}`;
|
|
281
|
+
|
|
282
|
+
if (isPermissionError) {
|
|
283
|
+
const hint = `Try: sudo chown -R $(whoami):staff "${irisAppsDir}"`;
|
|
284
|
+
logger.error(errorMsg, appError, null, hint);
|
|
285
|
+
irisDownloadErrors.push({ app: app.folderName, error: appError.message, isPermissionError: true, hint });
|
|
286
|
+
progress.addIssue('error', errorMsg, hint);
|
|
287
|
+
} else {
|
|
288
|
+
logger.error(errorMsg, appError);
|
|
289
|
+
irisDownloadErrors.push({ app: app.folderName, error: appError.message });
|
|
290
|
+
progress.addIssue('error', errorMsg);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
progress.completeStep('download-iris', `✓ Downloaded ${irisApps.length} Iris apps`);
|
|
296
|
+
} else {
|
|
297
|
+
progress.completeStep('download-iris', '✓ No Iris apps found');
|
|
298
|
+
}
|
|
299
|
+
} catch (error) {
|
|
300
|
+
logger.error('Error fetching Iris apps', error);
|
|
301
|
+
irisDownloadErrors.push({ stage: 'iris-list', error: error.message });
|
|
302
|
+
progress.completeStep('download-iris', '⚠ Iris apps skipped (API may not be available)');
|
|
303
|
+
}
|
|
304
|
+
|
|
216
305
|
// Check if we have any data to continue with
|
|
217
306
|
if (!activeClassResult?.Records?.length && !activePageResult?.Records?.length && !assets?.tree?.length) {
|
|
218
307
|
throw new Error('Download failed completely. No data available to continue.');
|
|
@@ -300,8 +389,9 @@ export const pull = async () => {
|
|
|
300
389
|
]);
|
|
301
390
|
|
|
302
391
|
// Find code entities in base.json that are no longer on server
|
|
392
|
+
// Exclude Files, Folders, and IrisApps - only ActiveClass and ActivePage
|
|
303
393
|
const cachedCodeEntities = Object.values(cachedResults).filter(c =>
|
|
304
|
-
c.type !== 'File' && c.type !== 'Folder' && c.recordId
|
|
394
|
+
c.type !== 'File' && c.type !== 'Folder' && c.type !== 'IrisApp' && c.recordId
|
|
305
395
|
);
|
|
306
396
|
|
|
307
397
|
const codeEntitiesToDelete = [];
|
|
@@ -355,8 +445,18 @@ export const pull = async () => {
|
|
|
355
445
|
logger.info('Deleted code entity', { path: entityPath, type: entity.type });
|
|
356
446
|
}
|
|
357
447
|
} catch (err) {
|
|
358
|
-
|
|
359
|
-
|
|
448
|
+
// Detect permission errors
|
|
449
|
+
const isPermissionError = err.code === 'EACCES' || err.code === 'EPERM';
|
|
450
|
+
deletionLogs.push({ type: 'error', path: entityPath, error: err.message, isPermissionError });
|
|
451
|
+
|
|
452
|
+
if (isPermissionError) {
|
|
453
|
+
const hint = `Try: sudo chown -R $(whoami):staff "${path.dirname(entityPath)}"`;
|
|
454
|
+
logger.error(`Failed to delete code entity: ${entityPath}`, err, null, hint);
|
|
455
|
+
progress.addIssue('error', `Failed to delete code entity: ${entityPath}`, hint);
|
|
456
|
+
} else {
|
|
457
|
+
logger.error(`Failed to delete code entity: ${entityPath}`, err);
|
|
458
|
+
progress.addIssue('error', `Failed to delete code entity: ${entityPath}`);
|
|
459
|
+
}
|
|
360
460
|
}
|
|
361
461
|
}
|
|
362
462
|
|
|
@@ -546,6 +646,9 @@ export const pull = async () => {
|
|
|
546
646
|
console.log(chalk.bold(`Summary:`));
|
|
547
647
|
console.log(` • ${activeClassResult.Records.length} ActiveClass records`);
|
|
548
648
|
console.log(` • ${activePageResult.Records.length} ActivePage records`);
|
|
649
|
+
if (irisApps.length > 0) {
|
|
650
|
+
console.log(` • ${irisApps.length} Iris apps`);
|
|
651
|
+
}
|
|
549
652
|
console.log(` • ${deletionLogs.length} deletions`);
|
|
550
653
|
console.log(`📁 Saved to: ./${EXPORT_ROOT}/`);
|
|
551
654
|
|
package/bin/magentrix.js
CHANGED
|
@@ -15,6 +15,7 @@ import { EXPORT_ROOT } from '../vars/global.js';
|
|
|
15
15
|
import { publish } from '../actions/publish.js';
|
|
16
16
|
import { update } from '../actions/update.js';
|
|
17
17
|
import { configWizard } from '../actions/config.js';
|
|
18
|
+
import { irisLink, irisDev, irisDelete, irisRecover, vueBuildStage } from '../actions/iris/index.js';
|
|
18
19
|
|
|
19
20
|
// ── Middleware ────────────────────────────────
|
|
20
21
|
async function preMiddleware() {
|
|
@@ -60,7 +61,12 @@ program
|
|
|
60
61
|
{ name: 'status', desc: 'Show file conflicts and sync status', icon: '📊 ' },
|
|
61
62
|
{ name: 'publish', desc: 'Publish pending changes to the remote server', icon: '📤 ' },
|
|
62
63
|
{ name: 'autopublish', desc: 'Watch & sync changes in real time', icon: '🔄 ' },
|
|
63
|
-
{ name: 'update', desc: 'Update MagentrixCLI to the latest version', icon: '⬆️ ' }
|
|
64
|
+
{ name: 'update', desc: 'Update MagentrixCLI to the latest version', icon: '⬆️ ' },
|
|
65
|
+
{ name: 'iris-link', desc: 'Link a Vue project to the CLI', icon: '🔗 ' },
|
|
66
|
+
{ name: 'vue-build-stage', desc: 'Build Vue project and stage for publish', icon: '🏗️ ' },
|
|
67
|
+
{ name: 'iris-dev', desc: 'Start Vue dev server with platform assets', icon: '🌐 ' },
|
|
68
|
+
{ name: 'iris-delete', desc: 'Delete an Iris app with backup', icon: '🗑️ ' },
|
|
69
|
+
{ name: 'iris-recover', desc: 'Recover a deleted Iris app from backup', icon: '♻️ ' }
|
|
64
70
|
];
|
|
65
71
|
|
|
66
72
|
const maxNameLen = Math.max(...commands.map(c => c.name.length));
|
|
@@ -191,6 +197,42 @@ program
|
|
|
191
197
|
.description('Configure CLI settings')
|
|
192
198
|
.action(configWizard);
|
|
193
199
|
|
|
200
|
+
// Iris commands for Vue.js app management
|
|
201
|
+
program
|
|
202
|
+
.command('iris-link')
|
|
203
|
+
.description('Link a Vue project to the CLI for deployment')
|
|
204
|
+
.option('--path <path>', 'Path to the Vue project')
|
|
205
|
+
.option('--unlink', 'Unlink a project instead of linking')
|
|
206
|
+
.option('--list', 'List all linked projects')
|
|
207
|
+
.option('--cleanup', 'Remove invalid (non-existent) linked projects')
|
|
208
|
+
.action(irisLink);
|
|
209
|
+
|
|
210
|
+
program
|
|
211
|
+
.command('vue-build-stage')
|
|
212
|
+
.description('Build a Vue project and stage it for publishing')
|
|
213
|
+
.option('--path <path>', 'Path to the Vue project')
|
|
214
|
+
.option('--skip-build', 'Skip build step and use existing dist/')
|
|
215
|
+
.action(vueBuildStage);
|
|
216
|
+
|
|
217
|
+
program
|
|
218
|
+
.command('iris-dev')
|
|
219
|
+
.description('Start Vue dev server with platform assets injected')
|
|
220
|
+
.option('--path <path>', 'Path to the Vue project')
|
|
221
|
+
.option('--no-inject', 'Skip asset injection')
|
|
222
|
+
.option('--restore', 'Restore config.ts from backup')
|
|
223
|
+
.action(irisDev);
|
|
224
|
+
|
|
225
|
+
program
|
|
226
|
+
.command('iris-delete')
|
|
227
|
+
.description('Delete a published Iris app with recovery backup')
|
|
228
|
+
.action(irisDelete);
|
|
229
|
+
|
|
230
|
+
program
|
|
231
|
+
.command('iris-recover')
|
|
232
|
+
.description('Recover a deleted Iris app from backup')
|
|
233
|
+
.option('--list', 'List available recovery backups')
|
|
234
|
+
.action(irisRecover);
|
|
235
|
+
|
|
194
236
|
// ── Unknown Command Handler ──────────────────
|
|
195
237
|
program.argument('[command]', 'command to run').action((cmd) => {
|
|
196
238
|
const runMain = async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magentrix-corp/magentrix-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "CLI tool for synchronizing local files with Magentrix cloud platform",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@inquirer/prompts": "^7.6.0",
|
|
39
|
+
"archiver": "^7.0.1",
|
|
39
40
|
"chalk": "^5.4.1",
|
|
40
41
|
"chokidar": "^4.0.3",
|
|
41
42
|
"commander": "^14.0.0",
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const LOCK_FILE_NAME = 'autopublish.lock';
|
|
5
|
+
const LOCK_EXPIRY_MS = 3600000; // 1 hour
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get the path to the autopublish lock file.
|
|
9
|
+
* @returns {string} - Path to lock file
|
|
10
|
+
*/
|
|
11
|
+
export function getLockFilePath() {
|
|
12
|
+
return path.join(process.cwd(), '.magentrix', LOCK_FILE_NAME);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if autopublish is currently running.
|
|
17
|
+
* @returns {boolean} - True if autopublish is running (lock file exists and is not stale)
|
|
18
|
+
*/
|
|
19
|
+
export function isAutopublishRunning() {
|
|
20
|
+
const lockFile = getLockFilePath();
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(lockFile)) return false;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const lockData = JSON.parse(fs.readFileSync(lockFile, 'utf-8'));
|
|
26
|
+
const lockAge = Date.now() - lockData.timestamp;
|
|
27
|
+
|
|
28
|
+
// If lock is older than 1 hour, consider it stale
|
|
29
|
+
return lockAge < LOCK_EXPIRY_MS;
|
|
30
|
+
} catch {
|
|
31
|
+
// If we can't read the lock file, assume it's not running
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a lock file to prevent multiple autopublish instances.
|
|
38
|
+
* @returns {boolean} - True if lock was acquired, false if already locked
|
|
39
|
+
*/
|
|
40
|
+
export function acquireAutopublishLock() {
|
|
41
|
+
const lockFile = getLockFilePath();
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
if (fs.existsSync(lockFile)) {
|
|
45
|
+
// Check if the lock is stale (process might have crashed)
|
|
46
|
+
if (isAutopublishRunning()) {
|
|
47
|
+
return false; // Lock is active
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Create lock file
|
|
52
|
+
fs.mkdirSync(path.dirname(lockFile), { recursive: true });
|
|
53
|
+
fs.writeFileSync(lockFile, JSON.stringify({
|
|
54
|
+
pid: process.pid,
|
|
55
|
+
timestamp: Date.now()
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
return true;
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Release the autopublish lock file.
|
|
66
|
+
*/
|
|
67
|
+
export function releaseAutopublishLock() {
|
|
68
|
+
const lockFile = getLockFilePath();
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
if (fs.existsSync(lockFile)) {
|
|
72
|
+
fs.unlinkSync(lockFile);
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Ignore errors during cleanup
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -243,13 +243,12 @@ export async function showCurrentConflicts(rootDir, instanceUrl, token, forceCon
|
|
|
243
243
|
export async function promptConflictResolution(fileIssues) {
|
|
244
244
|
if (!fileIssues.length) return 'skip';
|
|
245
245
|
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
246
|
+
// Add spacing instead of clearing (clearing causes flickering with progress tracker)
|
|
247
|
+
console.log('\n');
|
|
248
|
+
console.log(chalk.gray('─'.repeat(48)));
|
|
250
249
|
console.log(
|
|
251
250
|
chalk.bold.yellow(
|
|
252
|
-
|
|
251
|
+
`${fileIssues.length} file${fileIssues.length > 1 ? 's' : ''} require conflict resolution:\n`
|
|
253
252
|
)
|
|
254
253
|
);
|
|
255
254
|
|