@magentrix-corp/magentrix-cli 1.2.0 → 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.
@@ -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;
@@ -185,8 +193,8 @@ const handleDeleteStaticAssetAction = async (instanceUrl, apiKey, action) => {
185
193
  const errorMessage = error?.message || String(error);
186
194
  const errorLower = errorMessage.toLowerCase();
187
195
  const isNotFound = errorLower.includes('404') ||
188
- errorLower.includes('not found') ||
189
- errorLower.includes('item not found');
196
+ errorLower.includes('not found') ||
197
+ errorLower.includes('item not found');
190
198
 
191
199
  if (isNotFound) {
192
200
  // Clean up base.json since file doesn't exist on server
@@ -214,8 +222,8 @@ const handleCreateFolderAction = async (instanceUrl, apiKey, action) => {
214
222
  const errorMessage = error?.message || String(error);
215
223
  const errorLower = errorMessage.toLowerCase();
216
224
  const alreadyExists = errorLower.includes('already exists') ||
217
- errorLower.includes('folder exists') ||
218
- errorLower.includes('duplicate');
225
+ errorLower.includes('folder exists') ||
226
+ errorLower.includes('duplicate');
219
227
 
220
228
  if (alreadyExists) {
221
229
  // Folder already exists, update cache and treat as success
@@ -239,8 +247,8 @@ const handleDeleteFolderAction = async (instanceUrl, apiKey, action) => {
239
247
  const errorMessage = error?.message || String(error);
240
248
  const errorLower = errorMessage.toLowerCase();
241
249
  const isNotFound = errorLower.includes('404') ||
242
- errorLower.includes('not found') ||
243
- errorLower.includes('item not found');
250
+ errorLower.includes('not found') ||
251
+ errorLower.includes('item not found');
244
252
 
245
253
  if (isNotFound) {
246
254
  // Clean up base.json since folder doesn't exist on server
@@ -273,11 +281,109 @@ 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
+
313
+ /**
314
+ * Synchronizes class name in content with filename, or vice versa.
315
+ */
316
+ const syncClassAndFileNames = (action, recordId) => {
317
+ // Only for ActiveClass
318
+ if (action.entity !== 'ActiveClass') return;
319
+
320
+ const filePath = action.filePath;
321
+ // If file was deleted or doesn't exist, skip
322
+ if (!fs.existsSync(filePath)) return;
323
+
324
+ const content = fs.readFileSync(filePath, 'utf-8');
325
+ const filename = path.basename(filePath, path.extname(filePath));
326
+
327
+ // Regex to find class/interface/enum name
328
+ // Matches: public class Name, public interface Name, public enum Name
329
+ // We assume standard formatting
330
+ const classRegex = /public\s+(?:class|interface|enum)\s+(\w+)/;
331
+ const match = content.match(classRegex);
332
+
333
+ if (match) {
334
+ const classNameInContent = match[1];
335
+
336
+ if (classNameInContent !== filename) {
337
+ // Mismatch detected
338
+
339
+ // Case 1: File was renamed (action.renamed is true) -> Update content
340
+ if (action.renamed) {
341
+ const newContent = content.replace(classRegex, (fullMatch, name) => {
342
+ return fullMatch.replace(name, filename);
343
+ });
344
+ fs.writeFileSync(filePath, newContent);
345
+ console.log(chalk.cyan(` ↻ Updated class name in file to: ${filename}`));
346
+
347
+ // Update cache with new content hash
348
+ updateBase(filePath, { Id: recordId, Type: 'ActiveClass' });
349
+ }
350
+ // Case 2: Content was updated (action.renamed is false) -> Rename file
351
+ else {
352
+ // Rename file to match class name
353
+ const dir = path.dirname(filePath);
354
+ const ext = path.extname(filePath);
355
+ const newFilename = `${classNameInContent}${ext}`;
356
+ const newFilePath = path.join(dir, newFilename);
357
+
358
+ if (fs.existsSync(newFilePath)) {
359
+ console.warn(chalk.yellow(` ⚠️ Cannot rename ${filename} to ${classNameInContent}: File already exists.`));
360
+ return;
361
+ }
362
+
363
+ try {
364
+ fs.renameSync(filePath, newFilePath);
365
+ console.log(chalk.cyan(` ↻ Renamed file to match class: ${newFilename}`));
366
+
367
+ // Update cache: update the entry for this recordId to point to new path
368
+ updateBase(newFilePath, { Id: recordId, Type: 'ActiveClass' }, newFilePath);
369
+ } catch (err) {
370
+ console.warn(chalk.yellow(` ⚠️ Failed to rename file: ${err.message}`));
371
+ }
372
+ }
373
+ }
374
+ }
375
+ };
376
+
276
377
  /**
277
378
  * Updates cache after successful operations.
278
379
  */
279
380
  const updateCacheAfterSuccess = async (action, operationResult) => {
280
381
  try {
382
+ // Sync class names/filenames if needed (Bug 2 Fix)
383
+ if (action.action === 'update' && operationResult?.recordId) {
384
+ syncClassAndFileNames(action, operationResult.recordId);
385
+ }
386
+
281
387
  switch (action.action) {
282
388
  case "create": {
283
389
  const createSnapshot = action.fields && Object.values(action.fields)[0]
@@ -356,6 +462,28 @@ const updateCacheAfterSuccess = async (action, operationResult) => {
356
462
  }
357
463
  break;
358
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
+ }
359
487
  }
360
488
  } catch (error) {
361
489
  console.warn(chalk.yellow(`Warning: Failed to update cache for ${action.action}: ${error.message}`));
@@ -375,8 +503,8 @@ const groupActionsByResource = (actionQueue) => {
375
503
  for (let i = 0; i < actionQueue.length; i++) {
376
504
  const action = { ...actionQueue[i], originalIndex: i };
377
505
 
378
- // Asset operations don't need sequencing
379
- 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)) {
380
508
  assets.push(action);
381
509
  continue;
382
510
  }
@@ -475,6 +603,13 @@ const executeAction = async (instanceUrl, token, action) => {
475
603
  case "delete_folder":
476
604
  result = await handleDeleteFolderAction(instanceUrl, token, action);
477
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;
478
613
  default:
479
614
  throw new Error(`Unknown action: ${action.action}`);
480
615
  }
@@ -518,7 +653,8 @@ const performNetworkRequestSequential = async (actionQueue) => {
518
653
  console.log();
519
654
  console.log(chalk.bgRed.bold.white(' ✖ Operation Failed '));
520
655
  console.log(chalk.redBright('─'.repeat(48)));
521
- console.log(chalk.red.bold(`[${i + 1}] ${action.action.toUpperCase()} ${displayName} (${action.filePath || action.folderPath || action.folder}):`));
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}):`));
522
658
  console.log(formatMultilineError(error.message));
523
659
  console.log(chalk.redBright('─'.repeat(48)));
524
660
  }
@@ -624,7 +760,8 @@ const performNetworkRequestParallel = async (actionQueue) => {
624
760
  console.log();
625
761
  console.log(chalk.bgRed.bold.white(' ✖ Operation Failed '));
626
762
  console.log(chalk.redBright('─'.repeat(48)));
627
- console.log(chalk.red.bold(`[${index + 1}] ${action.action.toUpperCase()} ${getActionDisplayName(action)} (${action.filePath || action.folderPath || action.folder}):`));
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}):`));
628
765
  console.log(formatMultilineError(error));
629
766
  console.log(chalk.redBright('─'.repeat(48)));
630
767
  }
@@ -721,6 +858,7 @@ export const runPublish = async (options = {}) => {
721
858
  ...c,
722
859
  tag: c.recordId,
723
860
  filePath: c.filePath || c.lastKnownPath,
861
+ type: c.type || c.Type, // Normalize Type/type property
724
862
  }));
725
863
  const mapTime = Date.now() - mapStart;
726
864
 
@@ -728,11 +866,16 @@ export const runPublish = async (options = {}) => {
728
866
  progress.completeStep('load', `✓ Loaded ${cachedFiles.length} entries (${loadTime}ms load, ${mapTime}ms map)`);
729
867
  }
730
868
 
731
- // Step 3: Scan local workspace (excluding Assets folder)
869
+ // Step 3: Scan local workspace (excluding Assets and iris-apps folders)
732
870
  if (progress) progress.startStep('scan');
733
871
 
734
872
  const walkStart = Date.now();
735
- const localPaths = await walkFiles(EXPORT_ROOT, { ignore: [path.join(EXPORT_ROOT, 'Assets')] });
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
+ });
736
879
  const walkTime = Date.now() - walkStart;
737
880
 
738
881
  const tagStart = Date.now();
@@ -902,6 +1045,9 @@ export const runPublish = async (options = {}) => {
902
1045
  // Skip folders - they're handled separately
903
1046
  if (cacheFile?.type === 'Folder') continue;
904
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
+
905
1051
  // Handle static asset files
906
1052
  if (cacheFile?.type === 'File') {
907
1053
  // Use lastKnownActualPath which has the correct path (e.g., "src/Assets/...")
@@ -997,6 +1143,94 @@ export const runPublish = async (options = {}) => {
997
1143
  progress.completeStep('compare-code', `✓ Compared ${allIdsArray.length} code entities`);
998
1144
  }
999
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
+
1000
1234
  // Step 8: Handle brand-new, tag-less files
1001
1235
  if (progress) progress.startStep('prepare');
1002
1236
 
@@ -1077,19 +1311,28 @@ export const runPublish = async (options = {}) => {
1077
1311
  const num = chalk.green(`[${i + 1}]`);
1078
1312
  const act = chalk.yellow(a.action.toUpperCase());
1079
1313
 
1080
- let type, displayPath;
1081
- if (a.folderName) {
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) {
1082
1321
  type = chalk.cyan(a.folderName);
1083
1322
  displayPath = a.folderPath;
1323
+ label = "Folder";
1084
1324
  } else if (a.names) {
1085
1325
  type = chalk.cyan(a.names.join(", "));
1086
1326
  displayPath = a.folder;
1327
+ label = "File";
1087
1328
  } else if (a.filePath) {
1088
1329
  type = chalk.cyan(a.type || path.basename(a.filePath));
1089
1330
  displayPath = a.filePath;
1331
+ label = a.type ? "Type" : "File";
1090
1332
  } else {
1091
1333
  type = chalk.cyan(a.type || "Unknown");
1092
1334
  displayPath = a.folder || "Unknown";
1335
+ label = "File";
1093
1336
  }
1094
1337
 
1095
1338
  const idInfo = a.recordId ? ` ${chalk.magenta(a.recordId)}` : "";
@@ -1097,7 +1340,7 @@ export const runPublish = async (options = {}) => {
1097
1340
  ? ` → ${chalk.gray(a.oldPath)} ${chalk.white("→")} ${chalk.gray(a.filePath)}`
1098
1341
  : "";
1099
1342
 
1100
- console.log(`${num} ${act} | ${a.type ? "Type" : (a.folderName ? "Folder" : "File")}: ${type}${idInfo}${renameInfo} (${displayPath})`);
1343
+ console.log(`${num} ${act} | ${label}: ${type}${idInfo}${renameInfo} (${displayPath})`);
1101
1344
  });
1102
1345
 
1103
1346
  console.log(chalk.blue("\n--- Publishing Changes ---"));